# Portal de Performance e Remuneração dos Motoristas — contexto do projeto

Leia primeiro: `docs/especificacao-original.md` (escopo completo, 7 etapas, regras de desenvolvimento) e
`docs/mapeamento-planilhas.md` (colunas e chaves de cada planilha).

## Quem é o usuário / como trabalhar
Desenvolvedor autônomo, programa bem, sistema para a operação de distribuição da empresa dele (PT-BR).
Prefere respostas diretas, indo para a implementação, passo a passo, com material de apoio curto para aprender
enquanto implementa. Cada etapa termina com testes passando. Não inventar regra de negócio: em dúvida, perguntar.

## Stack (obrigatória)
PHP 8.4, Laravel 12, MySQL, Inertia + React + TypeScript, Tailwind, shadcn/ui, Lucide, Recharts, Vite,
maatwebsite/excel, Pest. Cálculos de dinheiro/percentual SEMPRE no backend, com DECIMAL e brick/math (nunca float).

## Estado atual
- **Etapa 1 concluída** (commit `da92f01`): Laravel 12 + Pest 4 (Pest 5 exige Symfony 8, incompatível), Breeze React/TS,
  perfis admin/motorista, login do motorista só por CPF, `audit_logs`. 34 testes verdes, `npm run build` ok.
- Pendente do ambiente: criar o banco MySQL `portal_motoristas` e preencher `DB_USERNAME/DB_PASSWORD` no `.env`
  (root exige senha), depois `php artisan migrate` e `php artisan portal:install`.
- **Etapa 2 (quase):** login admin por usuário "Admin"; CRUD unidades/motoristas; indicadores + metas (histórico, precedência motorista>unidade>geral, `TargetResolver`); dashboard com contagens reais. 48 testes verdes. FALTA: shadcn/ui (decisão pendente) e importar cadastro 01.20.01.47 (vai na Etapa 3).
- **Etapa 3 (andamento):** importação por unidade em 2 fases, painel de atualização (mês atual obrigatório), avisos, saudação. Prontos: cadastro 01.20.01.47, PCD, Abastecimento, Devolução HL (xlsx via maatwebsite/excel 4). MPD/Jornada Líquida (xlsx) também pronto — exige colunas JL e Meta JL em HH:MM; a planilha antiga de amostra é recusada de propósito. ETAPA 3 COMPLETA. Exclusão de motorista é definitiva — rever quando houver resultados (inativar em vez de apagar). Vínculo manual de nomes (abastecimento) ainda não existe.
- **Etapa 4 (andamento):** motor `App\Services\Indicators\IndicatorEngine` + `indicator_results` (com memória de cálculo) + tela Resultados (por competência/unidade, detalhe do motorista) + fechar/reabrir mês (`competency_closures`; mês fechado congela cálculo e importações). Recalcula sozinho ao confirmar/excluir importação. Caixas, JL e Devolução HL prontos; **Combustível calcula Σkm÷Σlitros mas a META está `pending_rule`** (usuário está definindo; Abastecimento não mexer até ele decidir). Devolução HL: % e meta vêm da planilha, sem validação de período. Importações podem ser canceladas (prévia) ou excluídas (confirmadas, exceto cadastro de motoristas). Pesos/premiação (R$) ficam para a Etapa 5.
- **Etapa 5 (andamento):** remuneração em `App\Services\Remuneration\RemunerationEngine` (R$ por indicador, versionada por vigência em `remuneration_rules/items`; valores saíram de `indicators`), cálculo por motorista/mês com memória, estados estimate→final (fechar mês)→homologated (botão Homologar; bloqueado se houver `incomplete`, ex.: combustível). Tela Remuneração + Regras. Arredonda cada item a 2 casas (meio p/ cima). Decididos pelo usuário: pesos FORA; sem volume mínimo de HL; NÃO trocar exclusão de motorista por inativação; combustível ainda a definir. 
- **Etapa 6 concluída:** portal do motorista (`Driver\PortalController` + `DashboardController`): início com remuneração/indicadores do mês, Indicadores (seletor de mês, memória), Evolução (6 meses, Recharts), Ganhos (remuneração com estado estimativa/fechada/homologada), Histórico. Motorista sempre vem do usuário logado (sem ID na URL). 114 testes verdes. FALTA: Etapa 7.
- **Cód. Promax (05/11):** `drivers.registration` virou `promax_code` (e nas tabelas de dados importados). NÃO é único global: a chave é **Unidade + Cód. Promax** (Matriz e Filial repetem códigos). Importações casam só com motoristas da unidade escolhida e avisam quando o código só existe em outra unidade; o motor de indicadores filtra por `branch_id` + `promax_code`.
- **Design System Disbec (05/11):** cores em UM lugar: `resources/js/lib/tokens.js` (usado pelo Tailwind e pelos gráficos via `lib/theme.ts`). Tokens: `brand` (azul-marinho), `accent` (amarelo), `success/warning/danger/info` (semânticas, nunca trocar por azul/amarelo), `gray` = família slate. Componentes CSS em `resources/css/app.css` (`btn-primary/secondary/ghost/accent/danger`, `field`, `card`, `badge-*`, `alert-*`, `empty-state`, `spinner`) e React em `Components/ui.tsx` (`Alert`, `Badge`, `Card`, `EmptyState`, `Spinner`) e `Components/BrandLogo.tsx`. Logo em `public/images/logo-disbec.png` (original, sem alterar). Não usar mais emerald/indigo. **Modo escuro:** `darkMode: class`; as cores viram variáveis CSS `--c-*` geradas de `tokens.js` (`light`/`dark`), então use papéis (`bg-surface`, `text-brand-ink`, `bg-sidebar`, `text-gray-*`) e nunca `bg-white`/cores fixas. Alternância em `Components/ThemeToggle.tsx` + `lib/theme.ts` (salva em localStorage; script no `<head>` evita piscar). Logo com contorno branco para fundo escuro: `public/images/logo-disbec-dark.png`. Nome do sistema: **Workstation Entrega** (`APP_NAME` em `tokens.js` e `.env`).
- **Painel "Situação dos dados" (05/11):** `Freshness` agora devolve também `status` por fonte: ok (em dia) / attention (1 período de atraso: diária=ontem, mensal=mês passado) / late (2+ períodos) / competency (há dados, mas não os da competência do mês atual) / none (nunca importado). `state` e `pending()` NÃO mudaram. Importação recente de outra competência não deixa em dia. Login: CPF ganha "." e "-" automáticos enquanto só houver números.
- **Caixas/Entrega (05/11):** o indicador agora é a RAZÃO Σ caixas ÷ Σ entregas do mês (2 casas) e a remuneração é R$ 0,03 × a razão (decisão do usuário). **Veículos (05/11):** módulo `Admin\VehicleController` (tabelas `vehicles` e `vehicle_assignments`): cadastro manual, importação do CSV do ERP DENTRO do módulo (prévia em cache + confirmar; ignora a coluna Motorista; não apaga veículos ausentes) e vínculo manual veículo×motorista com vigência (`starts_on`/`ends_on`; motorista ativo e da mesma unidade). **AINDA NÃO usado pelo cálculo de combustível** (Abastecimento continua atribuindo por placa+data do PCD): definir com o usuário a prioridade. Paginação: sempre `Components/Pagination.tsx` (traduz "pagination.next/previous").
- **Combustível (05/11) — meta definida:** meta = média ideal da planilha. Como varia por veículo, a meta do motorista é Σkm ÷ Σ(km ÷ média ideal de cada abastecimento) (para 1 veículo = a média ideal dele); atinge se Σkm÷Σlitros >= meta. Abastecimento sem média ideal fica fora (sem meta -> `no_target`). `pending_rule` não é mais produzido, mas o motor de remuneração ainda o trata (remuneração incompleta bloqueia homologação). **Atribuição do abastecimento:** PCD (placa+dia) > vínculo manual do veículo (módulo Veículos, vale por data) > nome da planilha; o motor aplica o vínculo na hora do cálculo (vale para importações antigas) e o importador também. **Rota de hoje (motorista):** `/motorista/rota` (`Services\Driver\TodayRoute`), mapas do PCD com `delivery_date = hoje`. Resultados e Remuneração paginam 15 por página (`Support\Paginate`); totais cobrem todos.
- **Decidido:** Devolução HL = R$ 80; meta HL vem das colunas `Meta - Devolução HL` e `% Devolução HL` da planilha. Hospedagem: decidir depois.

## Decisões já tomadas (respostas do usuário)
1. **Login do motorista: somente CPF, sem senha.** Apenas o admin tem senha (gerada uma única vez por
   `php artisan portal:install`). O pacote atual usa CPF+senha: ALTERAR `DriverLoginController`, `DriverLogin.tsx`
   e os testes para CPF-only (manter rate limit por CPF+IP, validar CPF com `App\Rules\Cpf`, só `role=driver` e `active`).
   Risco já avisado ao usuário (CPF não é segredo); ele decidiu assim. Mitigações baratas: throttle, mensagem de erro
   genérica, log de acessos em `audit_logs`, motorista só vê os próprios dados.
2. Volume: 32 motoristas, 2 unidades (Matriz e Filial). Granularidade: **mensal**. Um arquivo = um mês.
3. Hospedagem: ainda não informada (pendente).
4. Unidade do motorista não vem em nenhuma planilha: vem do cadastro. CPF também não vem de nenhuma planilha →
   precisamos de importação/cadastro inicial de motoristas (matrícula, nome, CPF, unidade).

## Indicadores e premiação (valores em R$ por indicador; premia só se bater a meta)
| Indicador | Fonte | Regra de meta | Premiação |
|---|---|---|---|
| Média de Combustível | Abastecimento (CSV) | Média real ≥ Média esperada (3,5 km/L, coluna `Média Ideal`) | R$ 60 se bater |
| Devolução | Devolução (XLSX) | % real ≤ meta da planilha (ver abaixo) | R$ 80 se bater |
| Caixas/Entrega (Produtividade) | PCD (CSV) | sem meta mínima | R$ 0,03 × total de caixas do mês |
| Jornada Líquida | MPD (XLSX) | % JL do motorista (média do `% JL` dos mapas) ≥ `% Meta JL` | R$ 60 se bater |

Regras de apuração:
- **Combustível:** média do mês = Σ km rodados ÷ Σ litros (ponderada, nunca média das médias). Ignorar abastecimento com
  km = 0 (primeiro da placa) e sem tanque cheio. **Atribuir o abastecimento ao motorista que dirigiu a placa naquele dia
  (cruzar com MPD/PCD por placa+data)**, pois a coluna `Motorista` do CSV muitas vezes é quem registrou (um nome aparece
  em 91 de 149 linhas, em vários caminhões).
- **Devolução:** a planilha tem metas de PDV (1,6%), NF (2,6%), HL (1,0%), R$ (2,5%). Pergunta do usuário foi se somava ou
  separava HL e PDV. **Proposta adotada (CONFIRMAR com o usuário):** dois sub-indicadores independentes, HL e PDV,
  cada um com a própria meta da planilha e **R$ 40 cada** (R$ 80 dividido), tudo-ou-nada em cada um. NF e R$ ficam
  só informativos. Perguntar também se existe volume mínimo (há motoristas com 1 PDV no mês).
- **Jornada Líquida (formato ATUAL, 05/11):** o MPD traz `% Meta JL` (ex.: 0,7) e `% JL` por mapa (0 ou 1). O % do motorista = média do `% JL` dos mapas; meta = média do `% Meta JL`; bate se >=. A coluna `JL` (número) não é usada. Amostra: `docs/amostras/Planificador MPD (2).xlsx`.
- **Caixas/Entrega:** pagamento = 0,03 × Σ `Total de caixas` do mês (sem limiar). Entregas serve para exibir a razão
  caixas/entrega. Descartar mapas cancelados/transferidos (`Ação` = Cancel./Transf.) e matrícula 0.

Modelo (ajustes sobre a proposta anterior): `users.cpf` único (login do motorista), `users.email` opcional;
`remuneration_rule_items` com `payout_mode` (`fixed_on_goal` | `per_unit`), `payout_amount`, `unit_source`;
`driver_aliases (driver_id, alias, source)` para o de-para de nome do Abastecimento; regras versionadas por vigência;
competência fechada não muda; memória de cálculo em JSON por item; `audit_logs`.

## Decisões da rodada 2 (respostas do usuário)
- **Devolução: somente HL** (as demais metas — PDV, NF, R$ — não aparecem). Premiação do indicador: R$ 80 (CONFIRMAR: 80 ou 40).
- **Jornada Líquida:** JL do mês = média das JL dos mapas do motorista (confirmado).
- **Cadastro inicial de motoristas:** relatório do ERP **01.20.01.47** (CSV `;` cp1252, um arquivo por unidade/filial,
  o admin escolhe a unidade ao importar). Colunas: `Codigo` (= matrícula; PCD usa `000000000015` → 15), `Nome` (truncado em
  30 chars; maiúsculas ou misto), `CPF` no formato `125.965.564.48` (último separador é ponto; **zeros à esquerda
  perdidos**: normalizar com `str_pad(11,'0',LEFT)`; 49/49 válidos assim; 2 sem CPF: 104 e 127 → entram sem acesso ao
  portal até o admin preencher), `Status`, demais colunas ignoradas (Habilitacao, Registro, Validade, Supervisor...).
  Linhas de dados terminam com `;` extra (11 campos, header com 12). Amostra recebida: 52 motoristas.
- **Hospedagem:** provavelmente Cloudflare (a confirmar; Laravel/PHP não roda em Workers — decidir antes do deploy).
- **Fechamento:** admin vê botão "Fechar mês" ao visualizar o mês atual (competência fechada fica imutável).
- **PCD real de 10/2026 recebido** (`PCD_10_2026.csv`, 01–31/10): usar como amostra do importador; inclui mapas
  `Ação`=Transf., matrícula `000000000000` ("Não cadastrada"), e mapas sem veículo/motorista fixo (matrícula 6, 10...).
- **Login do admin: usuário "Admin" (sem e-mail)**, senha gerada pelo `portal:install`. Implica coluna `users.username`.
- Sessão do motorista: sem preferência; manter padrão.

## Planilhas (amostras do usuário, mês 10/2026) — colocar em `docs/amostras/`
- `abastecimentos-dashboard.csv`: `;` UTF-8 com BOM, decimal vírgula, motorista por NOME (de-para).
- `Devolucao.10.2026.xlsx`: aba Export, `Motorista`=matrícula; descartar matrícula 0, linha Total, linha vazia e rodapé
  "Filtros aplicados". ATENÇÃO: o filtro do rodapé cobre 01/09–31/10 (dois meses), mas o usuário confirmou que o arquivo
  correto é de **um mês**; validar na importação que o período bate com a competência.
- `Planificador_MPD.xlsx`: aba Export, `Motorista`=matrícula, descartar linhas sem `Mapa`/`Motorista`.
- `PCD.csv`: `;` **cp1252**, decimal vírgula, matrícula com zeros à esquerda (`000000000102`→`102`). A amostra enviada é
  uma roteirização de 03–04/11 (não é um mês realizado); pedir o arquivo real do mês antes de testar com dados reais.
- Importação em 2 fases (upload → prévia/validação em `import_records` → confirmação → job transacional), apontando
  matrículas inexistentes, aliases sem vínculo, duplicatas e período divergente da competência.

## Próximos passos
1. Etapa 1 verde (acima) + login CPF-only.
2. Etapa 2: CRUD de motoristas (com CPF/unidade/ativo), unidades, indicadores, metas, dashboard admin, shadcn/ui.
3. Etapa 3: importação das 4 planilhas (+ cadastro inicial de motoristas e de-para de nomes).
4. Etapas 4–7 conforme `docs/especificacao-original.md`.

## Pendências a perguntar ao usuário
Confirmar R$ 80 vs 40 para HL; sobre o `Disbec.csv` em Downloads (é catálogo de produtos, não o de motoristas);
decisão final de hospedagem; senha/usuário do MySQL local; volume mínimo de HL (não respondido).

- **Veículos e Abastecimento SEM unidade (decidido pelo usuário):** `vehicles` não tem mais `branch_id`; o import de veículos e o de Abastecimento não pedem unidade (continuam importações separadas). Abastecimento: lote `branch_id` nulo, um por competência (reimportar substitui); atribuição por PCD (placa+dia, em QUALQUER unidade) > vínculo manual (qualquer motorista ativo, vale por data) > nome da planilha; `fuel_records.branch_id` guarda a unidade do MOTORISTA atribuído (nulo se não identificado). Painel de dados: Abastecimento aparece uma vez no cartão "Todas as unidades" (`ImportBatch::GLOBAL_TYPES`).
- **Rota de hoje:** colunas `cities`/`regions` do PCD (`Cidades +Entregas`, `Região +Entregas`) em `delivery_maps`, exibidas por mapa. **Imprimir remuneração:** `/admin/remuneracao/imprimir` (folha A4 para a folha de pagamento).
- **Etapa 7 concluída (sem deploy, 05/11):** revisão de segurança (rotas Breeze de e-mail/recuperação removidas; `php artisan portal:reset-admin-password`; limite de login por IP além de CPF+IP; cabeçalhos de segurança; `SecurityTest` varre todas as rotas), teste de fluxo completo `EndToEndFlowTest` com as planilhas reais, guias em `docs/guia-admin.md` e `docs/guia-motorista.md`. 146 testes verdes. Deploy/hospedagem: adiado pelo usuário.
- **Refinamentos de UX (06/11):** `Support\Paginate` aceita `?por_pagina=` (5/10/15/25/50, padrão 10) em Motoristas, Resultados e Remuneração (`Components/PerPage.tsx`). Cadastros/edições de Unidade, Motorista e Indicador em modal (`Components/FormModal.tsx`); a edição do motorista busca o CPF por JSON (`GET motoristas/{id}/edit`, sem página de create/edit). "Novo indicador" removido da tela (rota `store` ainda existe). Alterar remuneração = `Components/RuleModal.tsx` (cria nova versão da regra; usado em Indicadores e em Remuneração > Regras). Dashboard: resumo do mês (atingimento por indicador + remuneração prevista); o registro técnico saiu de lá e virou `/admin/atividades` (`Support\ActivityLabel` traduz `audit_logs` em frases; aba "Acessos dos motoristas" separada). **Ouvidoria:** URL em `config/portal.php` (`OUVIDORIA_URL`), card no início do portal do motorista. **Lembretes (imagens):** tabela `reminders`, admin em `/admin/lembretes` (upload JPG/PNG/WEBP até 5 MB, disco privado `local`, servidas por `/lembretes/{id}/imagem` só para logados), faixa "Para lembrar" no início do motorista com ampliação ao tocar. 154 testes verdes.
- **Ajudantes + Comunicação (06/11):** ajudantes ficam em `drivers` com `kind='helper'` (chave Unidade+Tipo+Cód. Promax; `Driver::drivers()/helpers()` são os escopos; TODA consulta que casa por Cód. Promax usa `drivers()`). Sem login no portal. Importações novas: `helpers` (01.20.01.48, cria/atualiza, não exclui, CPF ignorado) e `schedule` (03.11.29 Escala de Equipes, por unidade E mês (competência escolhida; datas fora dele são erro); tabela `team_assignments`; reimportar substitui por mapa; mês fechado bloqueia como nos demais). Ajudante só tem o indicador Caixas/Entrega = Σ caixas ÷ Σ entregas dos mapas do PCD em que foi escalado (`IndicatorEngine::boxes`), pago R$ 0,03 × razão como o motorista (decisão inferida do pedido, CONFIRMAR). Resultados/Remuneração/Motoristas têm filtro `tipo=ajudantes`; a folha impressa agrupa "Unidade — Ajudantes". Na amostra, o ajudante 49 "EXTERNO" é um cadastro normal (ativo). **Comunicação:** Avisos virou "Comunicação" (`/admin/avisos`): destinatário = todos, uma unidade ou motoristas escolhidos (`announcement_driver`), leitura em `announcement_reads` ("Lido por X de Y"); `Announcement::visibleTo($driver)`. Portal do motorista: sino no cabeçalho (`unreadNotices`, prop compartilhada) -> `/motorista/avisos` (abrir marca como lido); Lembretes agora em `/motorista/lembretes` (só títulos, imagem ao tocar) com card no Início. Admin ganhou o menu Atividades/Lembretes (ver acima). 160 testes verdes.
- **Ajudantes v2 (06/11):** módulo próprio `/admin/ajudantes` (`HelperController extends DriverController`; a tela `Admin/Drivers/Index` recebe `module.base/noun`). Cadastro 01.20.01.48 agora é IGUAL ao de motoristas (`HelperRegistryImporter extends DriverRegistryImporter`): snapshot da unidade, exclui quem não está no arquivo, CPF cria login. O ajudante "EXTERNO" é descartado (não entra no portal nem recebe) e ignorado na escala. Ajudante tem acesso ao portal e recebe avisos; vê só o indicador Caixas/Entrega (`Driver::indicators()`) e a rota do dia vem da escala. **Portal do operador agora em `/distribuicao`** (nomes `driver.*` iguais; o login POST segue em `/motorista/entrar`). Navegação ganhou Lembretes. Avisos do operador: abas Não lidos/Lidos/Gerais; só "Confirmar leitura" marca como lido (`POST /distribuicao/avisos/{id}/confirmar`); o Início mostra só avisos GERAIS (`Announcement::general()`); mensagens diretas ficam na central/sino. Modal agora rola na vertical (`Modal.tsx`). 161 testes verdes.
- **Administradores, CNH e Veículos/Abastecimento por unidade (06/11):** módulo `/admin/administradores` (`Admin\AdminUserController`, CRUD de usuários `role=admin`; não exclui/desativa a si mesmo nem o último ativo; usuário único sem diferenciar maiúsculas). Textos que valem para motorista+ajudante dizem "operador". **CNH:** coluna `Validade` do 01.20.01.47 grava `drivers.license_expires_on` (opcional; sem a coluna, como em ajudantes, não mexe); `Driver::license()` -> expired/expiring/ok com `config('portal.license_alert_days')` (30, `CNH_ALERT_DAYS`); alertas no dashboard do admin, coluna "CNH" em Motoristas (campo editável no modal) e faixa no início do motorista. **DECISÃO FINAL (06/11): Veículos e Abastecimento são UM arquivo para todas as unidades** (sem unidade na importação; `vehicles` sem `branch_id`; lote de abastecimento `branch_id` nulo, um por mês; atribuição por PCD (placa+dia, qualquer unidade) > vínculo manual > nome; `fuel_records.branch_id` = unidade do MOTORISTA atribuído; `ImportBatch::GLOBAL_TYPES = ['fuel']`; cartão "Todas as unidades" no painel de dados). Não voltar a pedir unidade nesses dois.
- **Seleção na importação + Ranking (06/11):** a prévia permite desmarcar registros OK/aviso (`skip[]` no confirm; viram status `skipped`, nenhum importador os toca; só os 600 primeiros aparecem). **Ranking** (`Services\Ranking\MonthlyRanking::board/for`): mesma unidade e mesmo tipo, só ativos com dado apurado no mês, empate total divide a posição, nunca mostra R$. **Motorista** = metas batidas, desempate pelo **Caixas/Entrega** do mês (Σ caixas ÷ Σ entregas, 2 casas, PCD); **ajudante** = Caixas/Entrega dos mapas em que foi escalado (03.11.29 x PCD), sem desempate (decisão do usuário; NÃO é total de caixas). Top 3 com pódio (`Components/Podium.tsx`: ouro/prata/bronze). Menu do admin em grupos (Cadastros, Desempenho, Dados, Comunicação, Sistema) em `AdminLayout` (`NAV_GROUPS`). Operador: `/distribuicao/ranking` (`Driver/Ranking`, posição + top 3; o Início só tem um atalho). Admin: `/admin/ranking` (lista completa por unidade, filtros mês/tipo/unidade). `window.axios` NÃO existe (Inertia 2): use `fetch` para JSON. 171 testes verdes.
- **Telemetria (06/11):** importação `telemetry` (CSV `Ranking_Motoristas_AAAA-MM-DD_AAAA-MM-DD`, SEM unidade; motorista achado pelo CPF; mês escolhido; período do nome do arquivo de outro mês é recusado). **A importação fica na tela de Telemetria** (`POST /admin/telemetria/importar`; não aparece na lista de Importações; `ImportBatch::UNTRACKED` a tira do painel de dados; confirmar/excluir usam a prévia padrão e respeitam o mês fechado). A pontuação é só acompanhamento e agora TAMBÉM alimenta o **indicador `telemetry_score` ("Telemetria")**: meta 100 pontos, R$ 60 `fixed_on_goal`, sem linha do motorista no arquivo do mês = `no_data` (não paga); `IndicatorEngine::telemetry`. **PENDENTE (usuário vai definir):** qual pontuação compara com a meta — hoje PROVISÓRIO `portal.telemetry_basis` (`TELEMETRY_BASIS`, padrão `month` = 100 − pontos do mês; `cycle` = acumulado do ciclo). Confirmar antes de fechar/homologar mês. Dashboard: card de contagem de CNH (sem lista). Decisão do usuário: importa-se o MÊS acumulado e os meses SOMAM; reimportar o mesmo mês substitui. Pontuação = 100 − Σ "Total de pontos" dos meses do **ciclo** (`telemetry_cycles`: início em mês + `ended_on`); mais pontos = pior posição; empate divide posição; um ranking só para todas as unidades (`Services\Ranking\TelemetryRanking`). Sem ciclo, o 1º mês confirmado define o início. Admin `/admin/telemetria` (lista, alterar início do ciclo atual, **Novo ciclo** = todos voltam a 100, histórico por ciclo). Motorista: botão "Ranking" no menu inferior → `/distribuicao/ranking` com abas Desempenho | Telemetria (ajudante só vê Desempenho). 182 testes verdes.
- **Relatos, ranking e telas (09/11):** novo indicador `reports` ("Relatos", motorista E ajudante: `Indicator::HELPER_CODES`), planilha "Acompanhamento Reuniões" (`Relatos.11.2026.xlsx`, xlsx, SEM unidade, importação `reports` em Importações, `ReportsImporter`, tabela `report_records`). Sem matrícula: casa pelo NOME (sem acento/maiúsculas; nome do cadastro cortado em 30 letras vale como começo) e pela função (Motorista*/Ajudante*); nome repetido entre unidades ou não achado = aviso, conta 0. Meta = Total >= 4 no mês (`config('portal.reports_target')`). **É um PORTÃO** (`payout_mode='gate'`, sem R$): se não bater a meta, zera TODA a remuneração do mês (itens ficam com a memória "seria R$ X"). Pessoa fora da planilha do mês = 0 relatos; sem planilha do mês importada = `no_data` e o portão NÃO se aplica (não zera, não bloqueia homologação; o painel de dados mostra o atraso). **Ranking:** entram TODOS os ativos da unidade, com ou sem dado; meta sem dado = não batida; total de metas = indicadores ativos exceto Caixas/Entrega (inclui Relatos). Ajudante sem dado entra com razão 0. **Telemetria:** empate na pontuação desempata por km (mais km na frente). UI: menu do admin com rolagem própria preservada entre páginas, cards de Resultados em uma linha, telas Telemetria e Indicadores enxutas. Menu do operador: barra com 4 itens + "Mais" (painel Indicadores/Evolução/Histórico/Lembretes/Avisos). 186 testes verdes.
- **Relatos por unidade + vínculo manual (09/11):** a planilha de Relatos volta a ser importada POR UNIDADE (`GLOBAL_TYPES = [fuel]`; lote com `branch_id`; reimportar substitui só a unidade/mês; o nome só casa com ativos da unidade escolhida). O motor considera "importado" por unidade da pessoa (`ImportBatch` confirmado do mês na unidade dela); unidade sem planilha = `no_data`, o portão não se aplica. Na prévia, linhas sem vínculo ganham um seletor "Identificar operador" (`links[registro]=driver_id` no confirmar; `ReportsImporter::link` valida ativo, mesma unidade e mesmo tipo, e recusa a mesma pessoa em duas linhas). Os outros importadores (PCD/MPD/Devolução por Cód. Promax) ainda NÃO têm esse campo. 188 testes verdes.
- **JL 90%, portão só no total, Importações novas (09/11):** Jornada Líquida agora tem meta FIXA de 90% (`portal.jl_target`, `JL_TARGET`); a coluna `% Meta JL` da planilha segue obrigatória mas não é mais usada. Portão de Relatos: cada indicador MANTÉM o que ganhou e só o TOTAL do mês zera (`memory.gate_failed`; `RemunerationCalculation::gateBlock()` dá o "ganharia R$ X"); o item Relatos mostra a quantidade ("3 de 4 relatos", `RemunerationCalculationItem::gateLabel`) em vez de R$, na lista e no detalhe do admin e no portal do operador (Início e Ganhos). Regra segue Total >= 4 (o pedido dizia "<= 4": tratado como erro de digitação, CONFIRMAR). Aba Importações reconstruída: cartões por arquivo (de onde vem, o que alimenta, unidade/mês), passo 2 com envio, Situação dos dados e histórico em tabela. 190 testes verdes.
- **Decisões fechadas (10/11):** Telemetria compara a pontuação ACUMULADA do ciclo com a meta (`TELEMETRY_BASIS` padrão `cycle`); o botão "Novo ciclo" virou **"Fechar ciclo"** (mesma ação: encerra o ciclo e todos voltam a 100). Relatos: Total >= 4 bate a meta (confirmado). Devolução HL = R$ 80 (confirmado). Ajudante: Caixas/Entrega igual ao motorista + Relatos com a mesma regra (confirmado). CPFs dos motoristas 104/127: cadastro em produção. Guia do admin atualizado (JL 90%, Relatos por unidade). 190 testes verdes.
