JornadaJourneyJornada
A tela de Jornada abre o dia do representante de vendas: registra a quilometragem do odômetro ao iniciar e ao encerrar a jornada, junto da posição GPS e do horário. Cada início/fim é uma transação de escrita que sobe pelo Dispatcher. Onde o mercado habilita, também coleta a placa do veículo (uma transação própria, só BR). The Journey screen opens the sales rep's day: it records the odometer reading when starting and when finishing the journey, along with GPS position and time. Each start/finish is a write transaction uploaded via the Dispatcher. Where the market enables it, it also collects the vehicle plate (its own transaction, BR only). La pantalla de Jornada abre el día del representante de ventas: registra el kilometraje del odómetro al iniciar y al finalizar la jornada, junto con la posición GPS y la hora. Cada inicio/fin es una transacción de escritura que sube por el Dispatcher. Donde el mercado lo habilita, también recoge la matrícula del vehículo (una transacción propia, solo BR).
O que é e para que serveWhat it is and what it's forQué es y para qué sirve
A Jornada é a primeira e a última coisa que o representante de vendas faz num dia de trabalho: registra o odômetro do veículo no início (ao entrar no app depois do login) e no fim (ao encerrar o dia pela Home). Serve para a empresa saber a quilometragem rodada — reembolso, controle de frota e conciliação de rota. Responde três perguntas: The Journey is the first and last thing the sales rep does on a working day: it records the vehicle odometer at the start (when entering the app after login) and at the finish (when ending the day from Home). It lets the company know the mileage driven — reimbursement, fleet control and route reconciliation. It answers three questions: La Jornada es lo primero y lo último que hace el representante de ventas en un día de trabajo: registra el odómetro del vehículo al inicio (al entrar en la app tras el login) y al fin (al terminar el día desde el Home). Permite a la empresa conocer el kilometraje recorrido — reembolso, control de flota y conciliación de ruta. Responde tres preguntas:
Quanto rodou?How much was driven?¿Cuánto se recorrió?
A leitura do odômetro no início e no fim — a diferença é a quilometragem do dia.The odometer reading at start and finish — the difference is the day's mileage.La lectura del odómetro al inicio y al fin — la diferencia es el kilometraje del día.
De onde e quando?Where and when?¿Desde dónde y cuándo?
Cada leitura carrega a posição GPS e o horário exato do início e do fim.Each reading carries the GPS position and the exact start and finish time.Cada lectura lleva la posición GPS y la hora exacta del inicio y del fin.
Qual o veículo?Which vehicle?¿Qué vehículo?
Onde o mercado habilita (BR), a placa do carro é coletada junto com o odômetro de início.Where the market enables it (BR), the car plate is collected along with the start odometer.Donde el mercado lo habilita (BR), la matrícula del auto se recoge junto con el odómetro de inicio.
Tela de escritaWrite screenPantalla de escritura Diferente das telas de lista, a Jornada não lê uma lista do backend — ela grava. Ao iniciar, o app fica preso na tela até a jornada abrir; ao encerrar, a jornada aberta é fechada e o app volta à Home. Unlike list screens, Journey doesn't read a backend list — it writes. On start, the app is locked on the screen until the journey opens; on finish, the open journey is closed and the app returns to Home. A diferencia de las pantallas de lista, la Jornada no lee una lista del backend — graba. Al iniciar, la app queda fija en la pantalla hasta que la jornada abre; al finalizar, la jornada abierta se cierra y la app vuelve al Home.
Como acessarHow to openCómo acceder
A tela tem dois modos e dois caminhos de entrada, decididos pelo estado da jornada atual:The screen has two modes and two entry paths, decided by the current journey state:La pantalla tiene dos modos y dos caminos de entrada, decididos por el estado de la jornada actual:
- Início — automático após o loginStart — automatically after loginInicio — automático tras el loginDepois do login e da sincronização, o
SessionGateverifica se há jornada aberta. Se não houver, a tela de Jornada abre em modo início — não dá para pular, o app fica nela até a jornada começar.After login and sync, theSessionGatechecks for an open journey. If there's none, the Journey screen opens in start mode — you can't skip it, the app stays there until the journey begins.Tras el login y la sincronización, elSessionGateverifica si hay jornada abierta. Si no la hay, la pantalla de Jornada abre en modo inicio — no se puede saltar, la app permanece hasta que la jornada comienza. - Home — quando já há jornada abertaHome — when a journey is already openHome — cuando ya hay jornada abiertaSe já há jornada aberta, o app vai direto para a Home. Lá, a pílula "Encerrar jornada" ao lado da saudação abre um modal de confirmação (com os pendentes do dia).If a journey is already open, the app goes straight to Home. There, the "End journey" pill next to the greeting opens a confirmation modal (with the day's pending items).Si ya hay jornada abierta, la app va directo al Home. Allí, la píldora "Finalizar jornada" junto al saludo abre un modal de confirmación (con los pendientes del día).
- Fim — confirmando no modalFinish — confirming on the modalFin — confirmando en el modalConfirmar no modal abre a tela de Jornada em modo fim, pedindo só o odômetro final (maior que o de início).Confirming on the modal opens the Journey screen in finish mode, asking only for the final odometer (greater than the start one).Confirmar en el modal abre la pantalla de Jornada en modo fin, pidiendo solo el odómetro final (mayor que el de inicio).
Estrutura da telaScreen structureEstructura de la pantalla
- SaudaçãoGreetingSaludo
- "Bom dia/boa tarde/boa noite, {primeiro nome}" — o prefixo varia com a hora do dia."Good morning/afternoon/evening, {first name}" — the prefix varies with the time of day."Buenos días/buenas tardes/buenas noches, {primer nombre}" — el prefijo varía con la hora del día.
- Relógio ao vivoLive clockReloj en vivo
- Hora com segundos (atualiza a cada segundo) e a data no formato do mercado.Time with seconds (updates every second) and the date in the market's format.Hora con segundos (se actualiza cada segundo) y la fecha en el formato del mercado.
- Título e subtítuloTitle & subtitleTítulo y subtítulo
- Muda conforme o modo: iniciar ou encerrar a jornada.Changes with the mode: start or finish the journey.Cambia según el modo: iniciar o finalizar la jornada.
- Última leituraLast readingÚltima lectura
- Uma linha "Último odômetro: N" — no fim, mostra o odômetro de início; no início, o último odômetro conhecido (se houver).A "Last odometer: N" line — on finish, it shows the start odometer; on start, the last known odometer (if any).Una línea "Último odómetro: N" — al fin, muestra el odómetro de inicio; al inicio, el último odómetro conocido (si lo hay).
- Campo do odômetroOdometer fieldCampo del odómetro
- Input numérico (só dígitos), centralizado, estilo pílula. Sempre presente.Numeric input (digits only), centered, pill style. Always present.Input numérico (solo dígitos), centrado, estilo píldora. Siempre presente.
- Campo da placaPlate fieldCampo de la matrícula
- Input em maiúsculas, limitado a 7 caracteres. Aparece só no início e só onde o mercado habilita
car_license_plate(BR).Uppercase input, limited to 7 characters. Shows only on start and only where the market enablescar_license_plate(BR).Input en mayúsculas, limitado a 7 caracteres. Aparece solo al inicio y solo donde el mercado habilitacar_license_plate(BR). - BotãoButtonBotón
- "Iniciar" ou "Encerrar" (formato pílula, largura cheia). Habilita só quando os campos são válidos; mostra loading enquanto envia."Start" or "Finish" (stadium shape, full width). Enabled only when fields are valid; shows loading while submitting."Iniciar" o "Finalizar" (forma píldora, ancho completo). Se habilita solo cuando los campos son válidos; muestra loading mientras envía.
Modos e estadosModes & statesModos y estados
A tela é a mesma; o que muda é o modo (início vs fim) e as regras de validação. O modo é decidido pela jornada atual: se há uma jornada aberta, a tela está em modo fim; senão, em modo início.The screen is the same; what changes is the mode (start vs finish) and the validation rules. The mode is decided by the current journey: if there's an open journey, the screen is in finish mode; otherwise, start mode.La pantalla es la misma; lo que cambia es el modo (inicio vs fin) y las reglas de validación. El modo lo decide la jornada actual: si hay una jornada abierta, la pantalla está en modo fin; si no, en modo inicio.
| AspectoAspectAspecto | Modo inícioStart modeModo inicio | Modo fimFinish modeModo fin |
|---|---|---|
| QuandoWhenCuándo | Sem jornada abertaNo open journeySin jornada abierta | Jornada abertaJourney openJornada abierta |
| Validação do odômetroOdometer validationValidación del odómetro | > 0> 0> 0 | > odômetro de início> start odometer> odómetro de inicio |
| Campo da placaPlate fieldCampo de matrícula | Onde habilitado (BR)Where enabled (BR)Donde habilitado (BR) | Nunca (só campos com availableOnFinish)Never (only availableOnFinish fields)Nunca (solo campos con availableOnFinish) |
| Ao concluirOn completeAl completar | Segue para a HomeProceeds to HomeSigue al Home | Aviso verde + voltaGreen notice + backAviso verde + volver |
Cada tentativa de envio termina num de cinco resultados, e cada um vira um aviso ao representante:Every submit attempt ends in one of five results, and each becomes a notice to the rep:Cada intento de envío termina en uno de cinco resultados, y cada uno se convierte en un aviso al representante:
Não dá para sairNo way outNo hay salida
No modo início, a tela usa PopScope(canPop: false) e não tem seta de voltar nem menu — o representante tem que iniciar a jornada para usar o app. Se a leitura da jornada falhar, uma tela de erro com "Tentar de novo" substitui o formulário.
In start mode, the screen uses PopScope(canPop: false) and has no back arrow or menu — the rep must start the journey to use the app. If reading the journey fails, an error view with "Retry" replaces the form.
En modo inicio, la pantalla usa PopScope(canPop: false) y no tiene flecha de volver ni menú — el representante debe iniciar la jornada para usar la app. Si la lectura de la jornada falla, una vista de error con "Reintentar" reemplaza el formulario.
Iniciar e encerrar a jornadaStart & finish the journeyIniciar y finalizar la jornada
IniciarStartIniciar
- Digite o odômetroType the odometerEscriba el odómetroDeve ser maior que zero. Onde o mercado exige, digite também a placa (formato BR válido: antigo
AAA0000ou MercosulAAA0A00).Must be greater than zero. Where the market requires it, also type the plate (valid BR format: legacyAAA0000or MercosulAAA0A00).Debe ser mayor que cero. Donde el mercado lo exige, escriba también la matrícula (formato BR válido: antiguoAAA0000o MercosurAAA0A00). - Toque em IniciarTap StartToque IniciarO odômetro sobe pelo Dispatcher (transação de odômetro) com a posição GPS e o horário; em sucesso, a jornada é gravada localmente como aberta. Onde há placa, ela sobe como uma transação própria.The odometer uploads via the Dispatcher (odometer transaction) with GPS position and time; on success, the journey is saved locally as open. Where there's a plate, it uploads as its own transaction.El odómetro sube por el Dispatcher (transacción de odómetro) con la posición GPS y la hora; en éxito, la jornada se graba localmente como abierta. Donde hay matrícula, sube como una transacción propia.
- Segue para a HomeProceeds to HomeSigue al HomeCom a jornada aberta, o app libera a Home e o resto das telas.With the journey open, the app unlocks Home and the rest of the screens.Con la jornada abierta, la app libera el Home y el resto de las pantallas.
EncerrarFinishFinalizar
- Toque na pílula "Encerrar jornada"Tap the "End journey" pillToque la píldora "Finalizar jornada"Na Home. Abre um modal com os pendentes do dia e um botão para confirmar.On Home. It opens a modal with the day's pending items and a button to confirm.En el Home. Abre un modal con los pendientes del día y un botón para confirmar.
- Digite o odômetro finalType the final odometerEscriba el odómetro finalDeve ser maior que o de início. Não há campo de placa no fim.Must be greater than the start one. There's no plate field on finish.Debe ser mayor que el de inicio. No hay campo de matrícula al fin.
- Toque em EncerrarTap FinishToque FinalizarA jornada é fechada e sobe pelo Dispatcher; um aviso verde confirma e o app volta.The journey is closed and uploaded via the Dispatcher; a green notice confirms and the app goes back.La jornada se cierra y sube por el Dispatcher; un aviso verde confirma y la app vuelve.
Falha só da placaPlate-only failureFallo solo de la matrícula Se o odômetro subir mas só a placa falhar, um modal pergunta se o representante quer continuar mesmo assim — nesse caso a placa é gravada localmente e será reenviada depois; a jornada não fica travada por causa da placa. If the odometer uploads but only the plate fails, a modal asks whether the rep wants to continue anyway — the plate is then saved locally and resent later; the journey isn't blocked by the plate. Si el odómetro sube pero solo la matrícula falla, un modal pregunta si el representante quiere continuar de todos modos — la matrícula se graba localmente y se reenvía después; la jornada no queda bloqueada por la matrícula.
Arquitetura e fluxo de dadosArchitecture & data flowArquitectura y flujo de datos
Clean Architecture + Riverpod + Freezed + ObjectBox. A Jornada não tem proto de leitura próprio: a leitura é local (a última jornada gravada no ObjectBox) e a habilitação de campos vem do EMC; a única saída para a rede são as escritas, que vão pelo Dispatcher. Por isso, dois grafos: leitura (composição local) e escrita (transação).Clean Architecture + Riverpod + Freezed + ObjectBox. Journey has no read proto of its own: the read is local (the last journey stored in ObjectBox) and field enablement comes from the EMC; the only network output are the writes, which go through the Dispatcher. Hence two graphs: read (local composition) and write (transaction).Clean Architecture + Riverpod + Freezed + ObjectBox. La Jornada no tiene proto de lectura propio: la lectura es local (la última jornada grabada en ObjectBox) y la habilitación de campos viene del EMC; la única salida a la red son las escrituras, que van por el Dispatcher. De ahí dos grafos: lectura (composición local) y escritura (transacción).
Leitura — montagem no _load()Read — assembly in _load()Lectura — armado en _load()
- GetCurrentOdometerJourneyUseCaseexecute()
- getLatestJourneyOdometerJourneyRepositorylocal-only
- query order startedAt descOdometerJourneyLocalDataSourceObjectBox
- toDomainOdometerJourneyEntity?+ journeyConfig (EMC) · repType
- _loadJourneyNotifier + State
- → UIJourneyPage
- _loadJourneyNotifier + State
- toDomainOdometerJourneyEntity?+ journeyConfig (EMC) · repType
- query order startedAt descOdometerJourneyLocalDataSourceObjectBox
- getLatestJourneyOdometerJourneyRepositorylocal-only
Escrita — iniciar / encerrar (odômetro)Write — start / finish (odometer)Escritura — iniciar / finalizar (odómetro)
- JourneyNotifierstartJourney / finishJourney
- execute(...)Start/FinishOdometerJourneyUseCase
- build(input)BuildOdometerJourney…UseCaseDispatcherEnvelope
- submit(envelope)SubmitOdometerJourneyUseCase→ DispatcherOrchestrator
- on SuccesssaveJourneyObjectBox local
- invalidate + _loadJourneyState
- on SuccesssaveJourneyObjectBox local
- submit(envelope)SubmitOdometerJourneyUseCase→ DispatcherOrchestrator
- build(input)BuildOdometerJourney…UseCaseDispatcherEnvelope
- execute(...)Start/FinishOdometerJourneyUseCase
As escritas são remote-first (§36): a transação é disparada primeiro e a jornada só é gravada localmente depois do sucesso. A placa (só no início, só BR) segue o mesmo padrão numa transação separada (Build/SubmitCarLicensePlateUseCase).Writes are remote-first (§36): the transaction fires first and the journey is only saved locally after success. The plate (start only, BR only) follows the same pattern in a separate transaction (Build/SubmitCarLicensePlateUseCase).Las escrituras son remote-first (§36): la transacción se dispara primero y la jornada se graba localmente solo tras el éxito. La matrícula (solo al inicio, solo BR) sigue el mismo patrón en una transacción separada (Build/SubmitCarLicensePlateUseCase).
Modelo de dadosData modelModelo de datos
A Jornada não tem proto nem DTO: não faz gRPC de leitura. Existe uma única estrutura persistida — OdometerJourney (Entity ↔ Model, ObjectBox local) — e dois inputs de escrita (payload inputs do Dispatcher) que carregam entities cruas. O JourneyState compõe a jornada atual (do cache local) com os campos habilitados (do EMC).Journey has no proto and no DTO: it does no read gRPC. There's a single persisted structure — OdometerJourney (Entity ↔ Model, local ObjectBox) — and two write inputs (Dispatcher payload inputs) carrying raw entities. JourneyState composes the current journey (from local cache) with the enabled fields (from the EMC).La Jornada no tiene proto ni DTO: no hace gRPC de lectura. Existe una única estructura persistida — OdometerJourney (Entity ↔ Model, ObjectBox local) — y dos inputs de escritura (payload inputs del Dispatcher) que llevan entities crudas. El JourneyState compone la jornada actual (del caché local) con los campos habilitados (del EMC).
Fontes de dadosData sourcesFuentes de datos
Cada campo do JourneyState e de onde vem — não há RPC de leitura próprio:Each JourneyState field and where it comes from — there's no screen-owned read RPC:Cada campo del JourneyState y de dónde viene — no hay RPC de lectura propio:
| Campo do StateState fieldCampo del State | TipoTypeTipo | OrigemOriginOrigen |
|---|---|---|
currentJourney | OdometerJourneyEntity? | GetCurrentOdometerJourneyUseCase → repository.getLatestJourney() (ObjectBox, mais recente porlatest bymás reciente por startedAt) |
fields | List<JourneyFieldConfig> | MarketConfiguration.journeyConfig.visibleFieldsFor(repType) |
odometerSubmitted | bool | client-state (default false) — evita reenviar o odômetro se só a placa falhou no inícioclient-state (default false) — avoids resending the odometer if only the plate failed on startclient-state (default false) — evita reenviar el odómetro si solo la matrícula falló al inicio |
Estruturas de dadosData structuresEstructuras de datos
Um dropdown por estrutura. OdometerJourney é a única que persiste (Entity ↔ Model, sem DTO nem proto); os dois payload inputs são só Entity (entrada crua para os builders do Dispatcher, §36).One dropdown per structure. OdometerJourney is the only one that persists (Entity ↔ Model, no DTO or proto); the two payload inputs are Entity-only (raw input for the Dispatcher builders, §36).Un dropdown por estructura. OdometerJourney es la única que persiste (Entity ↔ Model, sin DTO ni proto); los dos payload inputs son solo Entity (entrada cruda para los builders del Dispatcher, §36).
OdometerJourney persistida (local)persisted (local)persistida (local) 11 camposfieldscampos
Campo Model Entity id@Id int— uuid@Index StringString reconIdString String statusString OdometerJourneyStatusodometerStartint int odometerEndint? int? startedAtDateTimeDateTime endedAtDateTime?DateTime? startLatitudedouble double startLongitudedouble double endLatitudedouble? double? endLongitudedouble? double? idexiste só no Model (chave ObjectBox), reaproveitado no save (busca poruuid).startedAt/endedAtsão@Property(type: date)— não há parse de String.statusé gravado como.name("open"/"closed") e lido de volta combyName.idexists only in the Model (ObjectBox key), reused on save (lookup byuuid).startedAt/endedAtare@Property(type: date)— no String parse.statusis stored as.name("open"/"closed") and read back withbyName.idexiste solo en el Model (clave ObjectBox), reutilizado en el save (búsqueda poruuid).startedAt/endedAtson@Property(type: date)— sin parse de String.statusse graba como.name("open"/"closed") y se lee conbyName.OdometerJourneyDispatcherPayloadInput entrada de escritawrite inputentrada de escritura 2 camposfieldscampos
Campo Entity journeyOdometerJourneyEntityresourceResourceEntityInput cru (§36): a jornada e o representante como estão no domínio. Toda construção wire — arrays
Odometer/ReconStatus, formatação de data,vanId(resource.locationSfid),Status(wireValue) — mora nobuild()do builder.A raw input (§36): the journey and the rep as they are in the domain. All wire construction —Odometer/ReconStatusarrays, date formatting,vanId(resource.locationSfid),Status(wireValue) — lives in the builder'sbuild().Input crudo (§36): la jornada y el representante como están en el dominio. Toda construcción wire — arraysOdometer/ReconStatus, formateo de fecha,vanId(resource.locationSfid),Status(wireValue) — vive en elbuild()del builder.CarLicensePlateDispatcherPayloadInput entrada de escrita · BRwrite input · BRentrada de escritura · BR 4 camposfieldscampos
Campo Entity default carLicensePlateString — resourceResourceEntity— submittedAtDateTime— isTradePlatebool false Compartilhado com Settings. O Notifier injeta
submittedAt: DateTimeUtils.now(); a normalização da placa,resourceSfid/usernamee a data ficam nobuild()do builder.Shared with Settings. The Notifier injectssubmittedAt: DateTimeUtils.now(); plate normalization,resourceSfid/usernameand the date live in the builder'sbuild().Compartido con Settings. El Notifier inyectasubmittedAt: DateTimeUtils.now(); la normalización de la matrícula,resourceSfid/usernamey la fecha viven en elbuild()del builder.
Mappers
Só OdometerJourney tem mapper — duas direções (sem JSON/Proto/DTO, é local puro):Only OdometerJourney has a mapper — two directions (no JSON/Proto/DTO, it's local-only):Solo OdometerJourney tiene mapper — dos direcciones (sin JSON/Proto/DTO, es local puro):
| DireçãoDirectionDirección | MétodoMethodMétodo |
|---|---|
| Entity → Model | toModel() (status.name)(status.name)(status.name) |
| Model → Entity | toDomain() (OdometerJourneyStatus.values.byName)(OdometerJourneyStatus.values.byName)(OdometerJourneyStatus.values.byName) |
Os únicos deltasThe only deltasLos únicos deltas
- nenhum proto/DTO — a Jornada não faz gRPC de leitura (só escrita via Dispatcher + cache local)no proto/DTO — Journey does no read gRPC (write-only via Dispatcher + local cache)sin proto/DTO — la Jornada no hace gRPC de lectura (solo escritura vía Dispatcher + caché local)
statusString→OdometerJourneyStatus(tipado só na Entity)(typed only in the Entity)(tipado solo en la Entity)OdometerJourneyModel.idexiste só no Model (chave ObjectBox), reaproveitado no save poruuidexists only in the Model (ObjectBox key), reused on save byuuidexiste solo en el Model (clave ObjectBox), reutilizado en el save poruuidstartedAt/endedAtgravados comoDateTime(PropertyType.date), sem parse de Stringstored asDateTime(PropertyType.date), no String parsegrabados comoDateTime(PropertyType.date), sin parse de String- o
wireValue("Open"/"Closed") só aparece no payload do Dispatcher, nunca é persistidothewireValue("Open"/"Closed") appears only in the Dispatcher payload, never persistedelwireValue("Open"/"Closed") solo aparece en el payload del Dispatcher, nunca se persiste
Repository
O OdometerJourneyRepositoryImpl (implements OdometerJourneyRepositoryInterface) injeta só o datasource local — é local puro, sem rede, sem mock nem remote. A escrita para a rede não passa por aqui: acontece nos UseCases de escrita via Dispatcher. Método a método:OdometerJourneyRepositoryImpl (implements OdometerJourneyRepositoryInterface) injects only the local datasource — it's local-only, no network, no mock or remote. The network write doesn't go through here: it happens in the write UseCases via the Dispatcher. Method by method:El OdometerJourneyRepositoryImpl (implements OdometerJourneyRepositoryInterface) inyecta solo el datasource local — es local puro, sin red, sin mock ni remote. La escritura a la red no pasa por aquí: ocurre en los UseCases de escritura vía Dispatcher. Método a método:
getLatestJourney() local
RetornaReturnsDevuelve Result<OdometerJourneyEntity?, Failure>
Lê a jornada mais recente do ObjectBox (ordenada por startedAt descendente, findFirst); null vira Success(null). Exceção de cache → Error(Failure) via FailureMapper (logada em LogCategory.dataSync).Reads the latest journey from ObjectBox (ordered by startedAt descending, findFirst); null becomes Success(null). Cache exception → Error(Failure) via FailureMapper (logged under LogCategory.dataSync).Lee la jornada más reciente del ObjectBox (ordenada por startedAt descendente, findFirst); null es Success(null). Excepción de caché → Error(Failure) vía FailureMapper (registrada en LogCategory.dataSync).
saveJourney({journey}) local
RetornaReturnsDevuelve Result<void, Failure>
Grava a jornada na box. Chamado depois do envio da transação (remote-first). Exceção → Error(Failure).Writes the journey to the box. Called after the transaction upload (remote-first). Exception → Error(Failure).Graba la jornada en la box. Llamado después del envío de la transacción (remote-first). Excepción → Error(Failure).
Datasources
Um único datasource: o local (ObjectBox). Não há mock nem remote de leitura — a saída para a rede é o Dispatcher (documentado nos UseCases de escrita).A single datasource: the local one (ObjectBox). There's no mock or remote read — the network output is the Dispatcher (documented in the write UseCases).Un único datasource: el local (ObjectBox). No hay mock ni remote de lectura — la salida a la red es el Dispatcher (documentado en los UseCases de escritura).
Local OdometerJourneyLocalDataSource ObjectBox
Envio / fluxo: persistência local via ObjectBox (box OdometerJourneyModel) — sem rede. Erro: falhas viram CacheException (propagada, não engolida).Sends / flow: local persistence via ObjectBox (OdometerJourneyModel box) — no network. Error: failures become CacheException (propagated, not swallowed).Envío / flujo: persistencia local vía ObjectBox (box OdometerJourneyModel) — sin red. Error: fallos son CacheException (propagada, no tragada).
getLatestJourney()
- RetornoReturnRetorno
OdometerJourneyEntity?- ComportamentoBehaviorComportamiento
- query ordenada por
startedAtdesc,findFirst()?.toDomain().query ordered bystartedAtdesc,findFirst()?.toDomain().query ordenada porstartedAtdesc,findFirst()?.toDomain().
saveJourney({journey})
- RetornoReturnRetorno
void- ComportamentoBehaviorComportamiento
- busca por
uuid; se existir, reaproveita oid;toModel()+_box.put(model)— atualiza a mesma linha (ex.: início → fim).looks up byuuid; if it exists, reuses theid;toModel()+_box.put(model)— updates the same row (e.g. start → finish).busca poruuid; si existe, reutiliza elid;toModel()+_box.put(model)— actualiza la misma fila (ej.: inicio → fin).
Enums e labelsEnums & labelsEnums y labels
Um dropdown por enum, todos os valores.One dropdown per enum, all values.Un dropdown por enum, todos los valores.
OdometerJourneyStatus 2 valoresvaluesvalores
| case | wireValue |
|---|---|
open | Open |
closed | Closed |
Persistido como .name (open/closed); o wireValue (Open/Closed) só é usado no payload da transação. Getters da Entity: isOpen, isClosed.Persisted as .name (open/closed); the wireValue (Open/Closed) is used only in the transaction payload. Entity getters: isOpen, isClosed.Persistido como .name (open/closed); el wireValue (Open/Closed) se usa solo en el payload de la transacción. Getters de la Entity: isOpen, isClosed.
JourneyFieldType 4 valoresvaluesvalores
| case | value | availableOnFinish | availableInSettings |
|---|---|---|---|
odometer | odometer | true | false |
carLicensePlate | car_license_plate | false | true |
printerMacAddress | printer_mac_address | false | true |
unknown | unknown | false | false |
Dirige quais inputs a Jornada renderiza (via journeyConfig). availableOnFinish = também aparece no modo fim (só o odômetro). fromString normaliza (lowercase, espaços/hífens → _) e loga valor não mapeado.Drives which inputs Journey renders (via journeyConfig). availableOnFinish = also shows in finish mode (odometer only). fromString normalizes (lowercase, spaces/hyphens → _) and logs unmapped values.Dirige qué inputs renderiza la Jornada (vía journeyConfig). availableOnFinish = también aparece en modo fin (solo el odómetro). fromString normaliza (minúsculas, espacios/guiones → _) y registra valores no mapeados.
JourneyActionResult 5 valoresvaluesvalores
| case | Efeito na UIUI effectEfecto en la UI |
|---|---|
success | início → proceed() (Home); fim → aviso verde + voltastart → proceed() (Home); finish → green notice + backinicio → proceed() (Home); fin → aviso verde + volver |
invalidOdometer | aviso warning (odômetro inválido)warning notice (invalid odometer)aviso warning (odómetro inválido) |
invalidPlate | aviso warning (placa inválida)warning notice (invalid plate)aviso warning (matrícula inválida) |
failure | aviso error (falha no envio)error notice (submission failure)aviso error (fallo de envío) |
plateSubmissionFailed | modal "continuar mesmo assim?" (odômetro subiu, placa não)"continue anyway?" modal (odometer uploaded, plate didn't)modal "¿continuar de todos modos?" (odómetro subió, matrícula no) |
DispatcherType tipos de escritawrite typestipos de escritura
| case | serviceName | destination | enabledMarkets |
|---|---|---|---|
odometerJourney | OdometerAPI | salesforce (default) | [BR, CL, ZA] |
odometerCarPlate | CarLicensePlate | batchApi | [BR] |
O serviceName discrimina a transação no Dispatcher (proto único sendTransaction); enabledMarkets define onde a escrita vale. Detalhe da transação de odômetro: Dispatcher · 29 Odometer.serviceName discriminates the transaction in the Dispatcher (single sendTransaction proto); enabledMarkets defines where the write applies. Odometer transaction detail: Dispatcher · 29 Odometer.serviceName discrimina la transacción en el Dispatcher (proto único sendTransaction); enabledMarkets define dónde vale la escritura. Detalle de la transacción de odómetro: Dispatcher · 29 Odometer.
UseCases
Um dropdown por UseCase, com tipos de retorno exatos. Leitura (1) + escrita de odômetro (start/finish + builder + submit) + escrita de placa (builder + submit + save local).One dropdown per UseCase, with exact return types. Read (1) + odometer write (start/finish + builder + submit) + plate write (builder + submit + local save).Un dropdown por UseCase, con tipos de retorno exactos. Lectura (1) + escritura de odómetro (start/finish + builder + submit) + escritura de matrícula (builder + submit + save local).
GetCurrentOdometerJourneyUseCase
| Método | Retorna | Uso |
|---|---|---|
execute() | Result<OdometerJourneyEntity?, Failure> | delega a repository.getLatestJourney(); a jornada atual (aberta ou fechada).delegates to repository.getLatestJourney(); the current journey (open or closed).delega a repository.getLatestJourney(); la jornada actual (abierta o cerrada). |
StartOdometerJourneyUseCase
| Método | Retorna | Uso |
|---|---|---|
execute({odometerStart, latitude, longitude, startedAt, resource}) | Result<OdometerJourneyEntity, Failure> | gera uuid (SLREP-…) e reconId, monta a OdometerJourneyEntity (status open), build → submit (Dispatcher) e, em sucesso, saveJourney local.generates uuid (SLREP-…) and reconId, builds the OdometerJourneyEntity (status open), build → submit (Dispatcher) and, on success, local saveJourney.genera uuid (SLREP-…) y reconId, arma la OdometerJourneyEntity (status open), build → submit (Dispatcher) y, en éxito, saveJourney local. |
FinishOdometerJourneyUseCase
| Método | Retorna | Uso |
|---|---|---|
execute({openJourney, odometerEnd, latitude, longitude, endedAt, resource}) | Result<OdometerJourneyEntity, Failure> | copyWith na jornada aberta (status closed, odômetro/hora/GPS de fim), build → submit e, em sucesso, saveJourney local (mesma linha).copyWith on the open journey (status closed, finish odometer/time/GPS), build → submit and, on success, local saveJourney (same row).copyWith en la jornada abierta (status closed, odómetro/hora/GPS de fin), build → submit y, en éxito, saveJourney local (misma fila). |
BuildOdometerJourneyDispatcherPayloadUseCase builder
| Método | Retorna | Uso |
|---|---|---|
build({input}) | DispatcherEnvelope | implements DispatcherPayloadBuilder; monta o payload JSON e o envelope (type odometerJourney).implements DispatcherPayloadBuilder; builds the JSON payload and envelope (type odometerJourney).implements DispatcherPayloadBuilder; arma el payload JSON y el envelope (type odometerJourney). |
Payload: array Odometer (uid, OdameterStart, OdameterEnd, starttime, endtime, lat/long de início e fim, SubmissionDate) + array ReconStatus (ReconId, Status=wireValue, isNew "0"/"1", vanId=resource.locationSfid). transactionReference = journey.uuid. (As chaves OdameterStart/OdameterEnd são do contrato wire — grafia preservada.)Payload: Odometer array (uid, OdameterStart, OdameterEnd, starttime, endtime, start/finish lat/long, SubmissionDate) + ReconStatus array (ReconId, Status=wireValue, isNew "0"/"1", vanId=resource.locationSfid). transactionReference = journey.uuid. (The OdameterStart/OdameterEnd keys are from the wire contract — spelling preserved.)Payload: array Odometer (uid, OdameterStart, OdameterEnd, starttime, endtime, lat/long de inicio y fin, SubmissionDate) + array ReconStatus (ReconId, Status=wireValue, isNew "0"/"1", vanId=resource.locationSfid). transactionReference = journey.uuid. (Las claves OdameterStart/OdameterEnd son del contrato wire — grafía preservada.)
SubmitOdometerJourneyUseCase
| Método | Retorna | Uso |
|---|---|---|
submit({envelope}) | Result<DispatcherAck, Failure> | delega ao DispatcherOrchestrator.dispatch (store/tracking/reenvio idempotente).delegates to DispatcherOrchestrator.dispatch (idempotent store/tracking/retry).delega al DispatcherOrchestrator.dispatch (store/tracking/reenvío idempotente). |
BuildCarLicensePlateDispatcherPayloadUseCase builder · BR
| Método | Retorna | Uso |
|---|---|---|
build({input}) | DispatcherEnvelope | payload: CarLicensePlate (normalizado), resourceSfid, username, date, isTradePlate; type odometerCarPlate. Compartilhado com Settings.payload: CarLicensePlate (normalized), resourceSfid, username, date, isTradePlate; type odometerCarPlate. Shared with Settings.payload: CarLicensePlate (normalizado), resourceSfid, username, date, isTradePlate; type odometerCarPlate. Compartido con Settings. |
SubmitCarLicensePlateUseCase BR
| Método | Retorna | Uso |
|---|---|---|
submit({envelope}) | Result<DispatcherAck, Failure> | delega ao DispatcherOrchestrator.dispatch. Chamado só no início e só onde há placa.delegates to DispatcherOrchestrator.dispatch. Called only on start and only where there's a plate.delega al DispatcherOrchestrator.dispatch. Llamado solo al inicio y solo donde hay matrícula. |
SaveVehicleInfoUseCase placa locallocal platematrícula local
| Método | Retorna | Uso |
|---|---|---|
execute({carLicensePlate}) | Result<void, Failure> | grava a placa normalizada localmente (VehicleInfo) após o envio — reaproveitada pela tela de Settings.writes the normalized plate locally (VehicleInfo) after upload — reused by the Settings screen.graba la matrícula normalizada localmente (VehicleInfo) tras el envío — reutilizada por la pantalla de Settings. |
Notifier & State
O JourneyNotifier (@riverpod, FutureOr<JourneyState> build()) monta a tela. O build() é magro e retorna _load(), que lê a jornada atual e resolve os campos habilitados via journeyConfig.visibleFieldsFor(repType). As chamadas de escrita (§39) partem do Notifier. Não há refresh() de pull-to-refresh; a leitura é reexecutada invalidando o currentOdometerJourneyProvider (provider dependente, não self — §37).The JourneyNotifier (@riverpod, FutureOr<JourneyState> build()) assembles the screen. build() is thin and returns _load(), which reads the current journey and resolves the enabled fields via journeyConfig.visibleFieldsFor(repType). Write calls (§39) originate in the Notifier. There's no pull-to-refresh refresh(); the read is re-run by invalidating currentOdometerJourneyProvider (a dependent provider, not self — §37).El JourneyNotifier (@riverpod, FutureOr<JourneyState> build()) arma la pantalla. El build() es magro y retorna _load(), que lee la jornada actual y resuelve los campos habilitados vía journeyConfig.visibleFieldsFor(repType). Las llamadas de escritura (§39) parten del Notifier. No hay refresh() de pull-to-refresh; la lectura se reejecuta invalidando el currentOdometerJourneyProvider (provider dependiente, no self — §37).
MétodosMethodsMétodos
build() / _load() async
RetornoReturnRetorno FutureOr<JourneyState>
build() retorna _load(). O _load(): execute() do GetCurrentOdometerJourneyUseCase (getOrThrow), lê marketConfigurationProvider e sessionResourceProvider, resolve o ResourceTypeItem e monta o State com currentJourney + fields.build() returns _load(). _load(): execute() on GetCurrentOdometerJourneyUseCase (getOrThrow), reads marketConfigurationProvider and sessionResourceProvider, resolves the ResourceTypeItem and assembles the State with currentJourney + fields.build() retorna _load(). El _load(): execute() del GetCurrentOdometerJourneyUseCase (getOrThrow), lee marketConfigurationProvider y sessionResourceProvider, resuelve el ResourceTypeItem y arma el State con currentJourney + fields.
startJourney({odometer, carLicensePlate}) write
RetornoReturnRetorno Future<JourneyActionResult>
Valida odômetro (> 0) e placa (se visível e exige validação). Se o odômetro ainda não foi enviado (odometerSubmitted), dispara o StartOdometerJourneyUseCase com a posição atual do GPS e marca odometerSubmitted: true. Se não há placa, retorna success. Com placa: build → submit da transação da placa; sucesso → grava local + success; erro → plateSubmissionFailed.Validates odometer (> 0) and plate (if visible and requires validation). If the odometer wasn't submitted yet (odometerSubmitted), fires StartOdometerJourneyUseCase with the current GPS position and marks odometerSubmitted: true. If there's no plate, returns success. With plate: build → submit the plate transaction; success → save local + success; error → plateSubmissionFailed.Valida odómetro (> 0) y matrícula (si es visible y exige validación). Si el odómetro aún no se envió (odometerSubmitted), dispara el StartOdometerJourneyUseCase con la posición GPS actual y marca odometerSubmitted: true. Si no hay matrícula, retorna success. Con matrícula: build → submit de la transacción de la matrícula; éxito → graba local + success; error → plateSubmissionFailed.
finishJourney({odometer}) write
RetornoReturnRetorno Future<JourneyActionResult>
Exige jornada aberta e odômetro > início. Dispara o FinishOdometerJourneyUseCase (GPS + hora de fim); em sucesso, invalida o currentOdometerJourneyProvider, recarrega o State (_load()) e retorna success; erro → failure.Requires an open journey and odometer > start. Fires FinishOdometerJourneyUseCase (GPS + finish time); on success, invalidates currentOdometerJourneyProvider, reloads the State (_load()) and returns success; error → failure.Exige jornada abierta y odómetro > inicio. Dispara el FinishOdometerJourneyUseCase (GPS + hora de fin); en éxito, invalida el currentOdometerJourneyProvider, recarga el State (_load()) y retorna success; error → failure.
saveCarLicensePlateLocally({carLicensePlate}) · proceed()
saveCarLicensePlateLocally → Future<void> — grava a placa normalizada via SaveVehicleInfoUseCase (usado no caminho "continuar mesmo assim").writes the normalized plate via SaveVehicleInfoUseCase (used in the "continue anyway" path).graba la matrícula normalizada vía SaveVehicleInfoUseCase (usado en el camino "continuar de todos modos").
proceed → Future<void> — invalida e aguarda o currentOdometerJourneyProvider, fazendo o SessionGate reavaliar e liberar a Home.invalidates and awaits currentOdometerJourneyProvider, making the SessionGate re-evaluate and unlock Home.invalida y espera el currentOdometerJourneyProvider, haciendo que el SessionGate reevalúe y libere el Home.
State disponível para a PageState available to the PageState disponible para la Page
JourneyState campos + gettersfields + getterscampos + getters
| campo | tipo | default |
|---|---|---|
currentJourney | OdometerJourneyEntity? | null |
fields | List<JourneyFieldConfig> | [] |
odometerSubmitted | bool | false |
Getters: isFinishMode (jornada aberta), referenceOdometer (odômetro de início no fim / último odômetro no início), showField(type) (o campo está na lista e, no fim, tem availableOnFinish), fieldRequiresValidation(type). O privado _fieldConfig(type) resolve o JourneyFieldConfig pelo tipo.Getters: isFinishMode (journey open), referenceOdometer (start odometer on finish / last odometer on start), showField(type) (field is in the list and, on finish, has availableOnFinish), fieldRequiresValidation(type). Private _fieldConfig(type) resolves the JourneyFieldConfig by type.Getters: isFinishMode (jornada abierta), referenceOdometer (odómetro de inicio al fin / último odómetro al inicio), showField(type) (el campo está en la lista y, al fin, tiene availableOnFinish), fieldRequiresValidation(type). El privado _fieldConfig(type) resuelve el JourneyFieldConfig por tipo.
Page e widgetsPage & widgetsPage y widgets
A JourneyPage (ConsumerStatefulWidget) observa o journeyProvider e é embrulhada num PopScope(canPop: false). Loading e erro são globais (stateAsync.when); o formulário existe só no ramo data. O envio de escrita é disparado pelo Notifier (§39); o widget só orquestra modal/navegação. Árvore de composição:JourneyPage (ConsumerStatefulWidget) watches journeyProvider and is wrapped in a PopScope(canPop: false). Loading and error are global (stateAsync.when); the form exists only in the data branch. The write submit is fired by the Notifier (§39); the widget only orchestrates modal/navigation. Composition tree:La JourneyPage (ConsumerStatefulWidget) observa el journeyProvider y está envuelta en un PopScope(canPop: false). Loading y error son globales (stateAsync.when); el formulario existe solo en la rama data. El envío de escritura lo dispara el Notifier (§39); el widget solo orquesta modal/navegación. Árbol de composición:
- JourneyPage PopScope canPop:false
- AppPageShell no back · no drawer
- CustomLoadingIndicator loading
- FailureStateView error → invalidate(journeyProvider)
- SingleChildScrollView data · _buildForm
- CustomText greeting (GreetingUtils)
- _JourneyClock Timer 1s · time+date
- CustomText title + subtitle (start/finish)
- CustomText referenceOdometer (se houver)
- CustomInput odometer · numeric · digitsOnly
- CustomInput plate · showField(carLicensePlate) · uppercase · max 7
- CustomButton start/finish · onSubmit → notifier
- CarPlateSubmissionFailedModalContent plateSubmissionFailed → continue/cancel
- AppPageShell no back · no drawer
O encerrar começa fora desta page, na Home: a EndJourneyPill (widget compartilhado) abre a EndJourneyModalContent (em home/widgets/modals, mostra os pendentes do dia); confirmar chama AppRouter.goToJourney, que abre esta page em modo fim.Finishing starts outside this page, on Home: the EndJourneyPill (shared widget) opens EndJourneyModalContent (in home/widgets/modals, showing the day's pending items); confirming calls AppRouter.goToJourney, which opens this page in finish mode.El finalizar comienza fuera de esta page, en el Home: la EndJourneyPill (widget compartido) abre la EndJourneyModalContent (en home/widgets/modals, muestra los pendientes del día); confirmar llama a AppRouter.goToJourney, que abre esta page en modo fin.
Notas por mercadoMarket notesMercados
A Jornada é dirigida por configuração de mercado (End Market Configuration) e por DispatcherType.enabledMarkets: o odômetro é universal nos mercados ativos, mas a transação de odômetro (OdometerAPI) só está habilitada em BR/CL/ZA; a placa (campo + transação CarLicensePlate) é só BR. Presente em três mercados:Journey is driven by market configuration (End Market Configuration) and by DispatcherType.enabledMarkets: the odometer is universal in active markets, but the odometer transaction (OdometerAPI) is only enabled in BR/CL/ZA; the plate (field + CarLicensePlate transaction) is BR only. Present in three markets:La Jornada se rige por configuración de mercado (End Market Configuration) y por DispatcherType.enabledMarkets: el odómetro es universal en los mercados activos, pero la transacción de odómetro (OdometerAPI) solo está habilitada en BR/CL/ZA; la matrícula (campo + transacción CarLicensePlate) es solo BR. Presente en tres mercados:
O que muda por mercado é o campo/transação da placa. A matriz completa (uma linha por item):What changes per market is the plate field/transaction. The full matrix (one row per item):Lo que cambia por mercado es el campo/transacción de la matrícula. La matriz completa (una fila por ítem):
| ItemItemÍtem | BR | CL | ZA | AR | PY | PE |
|---|---|---|---|---|---|---|
| Tela de Jornada (odômetro)Journey screen (odometer)Pantalla de Jornada (odómetro) | x | x | x | — | — | — |
| Transação odômetro (odometerJourney / OdometerAPI)Odometer transaction (odometerJourney / OdometerAPI)Transacción odómetro (odometerJourney / OdometerAPI) | x | x | x | — | — | — |
| Campo da placa (journeyConfig car_license_plate)Plate field (journeyConfig car_license_plate)Campo de matrícula (journeyConfig car_license_plate) | x | — | — | — | — | — |
| Validação da placa (requiresValidation)Plate validation (requiresValidation)Validación de matrícula (requiresValidation) | x | — | — | — | — | — |
| Transação placa (odometerCarPlate / CarLicensePlate)Plate transaction (odometerCarPlate / CarLicensePlate)Transacción matrícula (odometerCarPlate / CarLicensePlate) | x | — | — | — | — | — |
Odômetro + placaOdometer + plateOdómetro + matrícula
O journeyConfig do BR inclui car_license_plate com requiresValidation: true — só aqui o campo da placa aparece no início, com validação de placa brasileira (antigo AAA0000 ou Mercosul AAA0A00), e sobe pela transação CarLicensePlate (odometerCarPlate, batchApi, BR-only).
BR's journeyConfig includes car_license_plate with requiresValidation: true — only here does the plate field appear on start, with Brazilian plate validation (legacy AAA0000 or Mercosul AAA0A00), uploaded via the CarLicensePlate transaction (odometerCarPlate, batchApi, BR-only).
El journeyConfig de BR incluye car_license_plate con requiresValidation: true — solo aquí aparece el campo de matrícula al inicio, con validación de matrícula brasileña (antigua AAA0000 o Mercosur AAA0A00), y sube por la transacción CarLicensePlate (odometerCarPlate, batchApi, solo BR).
Só odômetroOdometer onlySolo odómetro
A tela existe e registra o odômetro de início e fim (transação OdometerAPI habilitada), mas o journeyConfig desses mercados só tem odometer — não há campo de placa.
The screen exists and records the start/finish odometer (OdometerAPI transaction enabled), but these markets' journeyConfig only has odometer — no plate field.
La pantalla existe y registra el odómetro de inicio y fin (transacción OdometerAPI habilitada), pero el journeyConfig de estos mercados solo tiene odometer — no hay campo de matrícula.
AR · PY · PE
Existem como mercados do app (config PANGEA mínima), mas não estão em odometerJourney.enabledMarkets — a transação de odômetro não vale ali, então a Jornada é tratada como ausente.
They exist as app markets (minimal PANGEA config), but are not in odometerJourney.enabledMarkets — the odometer transaction doesn't apply there, so Journey is treated as absent.
Existen como mercados de la app (config PANGEA mínima), pero no están en odometerJourney.enabledMarkets — la transacción de odómetro no aplica allí, por lo que la Jornada se trata como ausente.
Pendências / roadmapPending / roadmapPendientes / roadmap
O JourneyFieldType.printerMacAddress já existe no enum, mas nenhum mercado o habilita no journeyConfig e não há widget que o renderize (o journey_config.detailed.jsonc descreve o campo previsto para CL+BR, gated a prompt_sales_rep). É um campo previsto, ainda não implementado — a doc reflete o estado atual do código.
JourneyFieldType.printerMacAddress already exists in the enum, but no market enables it in journeyConfig and no widget renders it (the JSONC describes the planned field for CL+BR, gated to prompt_sales_rep). It's a planned, not-yet-implemented field — the doc reflects the current state of the code.
JourneyFieldType.printerMacAddress ya existe en el enum, pero ningún mercado lo habilita en journeyConfig y no hay widget que lo renderice (el JSONC describe el campo previsto para CL+BR, gated a prompt_sales_rep). Es un campo previsto, aún no implementado — la doc refleja el estado actual del código.