# Dashboard Logística — Visualização Mobile (Responsivo)

**Data:** 2026-05-19
**Sinal:** `componentes/Missoes/Logistica/sinais/Dashboard`
**URL alvo:** `https://dev-galpaotms.goldieapp.com.br/missao/logistica/dashboard`

## Objetivo

Adicionar uma camada responsiva ao Dashboard Operacional de Logística para que ele
tenha **excelente visualização em mobile**, **sem alterar o layout desktop atual**
(que já está pronto e em uso).

## Contexto

- `html/dashboard.php` (~4460 linhas): shell HTML + grande bloco `<style>` + blocos
  `<style>` embutidos espalhados. Nenhum dado é SSR; tudo vem via AJAX.
- `html/dashboard.js` (~5915 linhas): renderização, ApexCharts, tabelas, modais.
- Design system próprio (`dash-*` / componentes `luz/`), **não** usa cards Metronic.
- Media queries hoje são limitadas a colapso de colunas de grid (1200/1024/900/768/700).
- Não há lógica responsiva em JS (sem `matchMedia`/`innerWidth`).
- 6 abas: Visão Geral, Separação, Carregamento, Ocorrências, Pesagem, Ranking.
- Já existe um toggle de tema escuro/claro como IIFE inline em `dashboard.php`.

## Decisões de produto (acordadas com o usuário)

1. **Usuário primário no mobile:** gestor olhando KPIs de relance → o topo da
   Visão Geral (KPIs + tendência) é a vitrine e deve ficar impecável.
2. **Reflow completo:** todas as 6 abas funcionam no mobile, com paridade de
   conteúdo com o desktop, apenas reorganizadas para caber.
3. **Barra superior:** abas com scroll horizontal + filtros (tipo de separação,
   período/dias, calendário, reset, tema, info) numa **gaveta (drawer)** aberta
   por um botão "Filtros".
4. **Abordagem A — camada consolidada:** bloco `@media` isolado + drawer + JS
   mínimo. Não altera o desktop. Risco baixo, reversível, fácil de iterar.

## Escopo

### Dentro do escopo

- Camada CSS responsiva (tiers `≤768px` e `≤480px`) adicionada ao fim do `<style>`
  principal de `dashboard.php`, somente com regras dentro de `@media`.
- Adição cirúrgica de markup: wrapper de filtros + botão "Filtros".
- Drawer off-canvas (CSS + IIFE JS isolado em `dashboard.php`).
- Config global `window.Apex` no início de `dashboard.js` para responsividade
  de todos os gráficos sem editar cada um.
- Dispatch de `resize` em troca de aba / fechar drawer / mudança de orientação.
- Todas as 6 abas, com prioridade de polimento na Visão Geral.

### Fora do escopo

- Qualquer alteração no layout/CSS desktop existente.
- Transformar tabelas em cards (fica como polimento futuro opcional).
- Mudanças de dados, endpoints, handlers ou lógica de negócio.
- Sticky header (usuário escolheu scroll+drawer, não barra fixa).
- Refatoração não relacionada do `dashboard.php`/`dashboard.js`.

## Design

### 1. Estrutura da camada responsiva & breakpoints

- Bloco único `/* ===== MOBILE / RESPONSIVO ===== */` adicionado **antes** do
  `</style>` da linha ~1402 do `dashboard.php`, contendo apenas regras dentro
  de `@media`.
- **Dois tiers:**
  - `@media (max-width: 768px)` — layout mobile principal (tablet em pé +
    celular): grids em 1 coluna, barra reorganizada, drawer ativo.
  - `@media (max-width: 480px)` — ajustes finos de celular: fontes/paddings
    menores, KPIs em 2 colunas compactas.
- Os `@media` existentes (1200/1024/900/700) permanecem intactos; o novo bloco
  apenas soma overrides. Especificidade suficiente para vencer regras base sem
  `!important` onde possível; `!important` só quando inevitável (overrides de
  tema do ApexCharts já usam esse padrão no arquivo).

### 2. Topo / barra (abas com scroll + gaveta de filtros)

Markup atual relevante (`dashboard.php` ~1444–1499):

```
<div class="dash-tabs-row">
  <div class="dash-tabs"> ...6 botões de aba... </div>
  <div class="dash-tipo-sep" id="dash-tipo-sep"> ...3 botões... </div>
  <div class="dash-daterange-wrap">
     <span id="dash-periodo-label">
     <div id="dash-day-buttons">
     <button #dash-btn-calendario> <button #dash-btn-reset>
     <button #dash-btn-tema> <button #btn-dash-info>
  </div>
</div>
```

**Mudança de markup (mínima, additiva, reversível):**

- Envolver `.dash-tipo-sep` + `.dash-daterange-wrap` num wrapper
  `<div class="dash-filtros-mobile" id="dash-filtros-mobile">`.
- Adicionar um botão `<button class="dash-btn-filtros" id="dash-btn-filtros">`
  dentro de `.dash-tabs-row` (escondido no desktop).
- Adicionar um backdrop `<div class="dash-drawer-backdrop" id="dash-drawer-backdrop">`.

**CSS desktop (sem regressão):**

- `.dash-filtros-mobile { display: contents; }` — os dois blocos continuam
  sendo flex-items diretos da `.dash-tabs-row`, exatamente como hoje. Layout
  desktop idêntico ao atual.
- `.dash-btn-filtros { display: none; }` e `.dash-drawer-backdrop { display: none; }`.

**CSS mobile (`≤768px`):**

- `.dash-tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity; }`; scrollbar escondida; cada `.dash-tab` com
  `scroll-snap-align: start` e `flex: 0 0 auto`.
- `.dash-btn-filtros { display: inline-flex; }` (ícone engrenagem + "Filtros").
- `.dash-filtros-mobile`: vira painel off-canvas
  (`position: fixed; right: 0; top: 0; height: 100dvh; width: min(86vw, 360px);
  transform: translateX(100%); transition: transform .25s;` + `overflow-y:auto`),
  com classe `.aberto` aplicando `transform: translateX(0)`.
- Dentro do drawer os controles empilham verticalmente com espaçamento tocável
  (alvos ≥ 40px). `.dash-day-buttons` quebra em grade de 7.
- `.dash-drawer-backdrop.aberto { display: block; position: fixed; inset: 0;
  background: rgba(0,0,0,.5); }`.

**IMPORTANTE (timing):** o `<script>` do drawer fica acima do markup
(`#dash-op-root` vem depois), então o IIFE roda no parse antes dos elementos
existirem. O corpo é envolvido em `function init(){…}` e disparado via
`document.readyState==='loading' ? addEventListener('DOMContentLoaded',init) :
init()`. Sem isto, `getElementById` retorna null no parse → guard early-return
→ nenhum listener → botão "Filtros" morto. (Os controles internos —
tipo-separação, dias, calendário, reset — são ligados pelo `dashboard.js`, que
o framework injeta após a view, então sempre funcionaram; o único quebrado era
abrir o drawer.)

**Comportamento (IIFE JS isolado em `dashboard.php`, mesmo padrão do toggle de tema):**

- Clique em `#dash-btn-filtros` → abre drawer + backdrop, `body { overflow: hidden }`.
- Clique no backdrop ou tecla `Esc` → fecha; restaura scroll do body.
- Ao fechar, dispara `window.dispatchEvent(new Event('resize'))` (ver Seção 5).
- Acessibilidade: `aria-expanded` no botão, `aria-hidden` no drawer quando fechado,
  foco movido para o drawer ao abrir e devolvido ao botão ao fechar.

### 3. KPIs + gráfico de tendência (vitrine)

- **Nota de DOM (verificada na implementação):** os cards de KPI são renderizados
  por `luz/luz_card_kpi.php` com a classe raiz **`.g-card-kpi`** (não
  `.dash-kpi-card`), filhos diretos de `.dash-kpi-grade`; valor/título/sparkline
  são `<div>`/`<svg>` inline-styled **sem classe**. Logo, os seletores responsivos
  miram `.dash-kpi-grade` e `.dash-kpi-grade > .g-card-kpi` (com `!important` para
  vencer estilos inline), não classes inexistentes.
- `.dash-kpi-grade`:
  - `≤768px`: 2 colunas (`grid-template-columns: repeat(2,1fr) !important`).
  - `≤480px`: **2 colunas compactas** (não 1) reduzindo padding/min-height/gap do
    card raiz `.g-card-kpi` (via `!important`, pois são estilos inline) para caber
    4–6 KPIs num glance. (Valor/label/sparkline não têm hook de classe; o card
    inteiro é compactado em vez de reestilizar elementos internos.)
  - **KPI órfã ocupa 2 colunas:** o card `[data-kpi="lead-time"]` é
    `display:none` global (phantom child que quebrava `:nth-child`). A regra
    conta só visíveis via CSS L4 `of S`:
    `.dash-kpi-grade > .g-card-kpi:nth-last-child(1 of .g-card-kpi:not([data-kpi="lead-time"])):nth-child(odd of .g-card-kpi:not([data-kpi="lead-time"])) { grid-column: 1 / -1 !important; }`
    — último card visível ocupa a largura inteira quando o nº de visíveis é
    ímpar. Geral p/ qualquer aba. Em browsers sem `of S` (pré-2023) degrada
    para o estado antigo (1 col) — sem regressão.
- **Aba ativa no load (mobile):** `trocarAba()` centraliza a aba no clique,
  mas o load inicial usa `carregarAba()` + `.ativo` server-side; um IIFE no
  bootstrap faz `scrollIntoView` da `.dash-tab.ativo` (rAF) para ela aparecer
  na faixa rolável. Desktop = no-op (faixa não rola).
- Gráfico "Fluxo: Separações vs Carregamentos" (`#chart-fluxo`): largura total,
  `min-height` reduzido (~220px) no mobile; responsividade via config global do
  Apex (Seção 5): legenda embaixo, labels do eixo X reduzidas/rotacionadas,
  toolbar off.

### 4. Conteúdo pesado (reflow completo)

- **Charts em geral:** responsividade resolvida pela config global `window.Apex`
  (Seção 5). Containers já são fluidos em largura.
- **Séries densas (por hora) e timelines Gantt** (ex.: `#card-geral-timeline`,
  timelines de separação/carregamento): envolver o conteúdo num container com
  `overflow-x: auto` e `min-width` interno (~720px) para rolar horizontalmente
  em vez de espremer ilegível. Dica visual "← deslize →" no mobile.
- **Tabelas:** wrapper com `overflow-x: auto` + `min-width` apropriado
  (preserva todas as colunas e dados; baixo risco; paridade com desktop).
  Cartões de pessoa/ranking (`luz_destaque_pessoa`, etc.) que já são "card-like"
  apenas empilham. Transformar tabela→cards fica como polimento futuro opcional.
- **Modais** (drill-down, "Cálculos & Informações"): no mobile ocupam tela cheia
  (`width: 100%`, `height: 100dvh`, corpo com `overflow-y: auto`).
- **Tooltips** (ApexCharts e custom com `min-width: 240–380px`): `max-width`
  limitado a `calc(100vw - 1rem)` no mobile.
- **Nota de DOM (verificada):** `.dash-grid-2/.dash-grid-3/.dash-top3-grid` são
  CSS morto (definidos mas nunca aplicados). Os grids multi-coluna reais com
  classe são `.dash-grid-sep-top` (Separação, repeat(5) inline),
  `.dash-grid-carg-top` (Carregamento, repeat(3) inline) e `.dash-info-grid`
  (modal Cálculos). Esses colapsam para 1 coluna no `≤768px` via `!important`.
  **Grids inline-styled sem classe** (a aba Ocorrências e outras montam layout
  com `style="display:grid; grid-template-columns: repeat(12|6|4|2,…)"` +
  filhos `grid-column: span N`) são colapsados por regra geral por atributo,
  escopada/segura:
  `.dash-aba-section [style*="display:grid"] { grid-template-columns:1fr !important }`
  + `.dash-aba-section [style*="grid-column:"] { grid-column:1/-1 !important }`.
  Não atinge `.dash-kpi-grade` (grid vem de CSS, não de `style=` inline) nem o
  desktop. Resolve as 14 grids inline de todas as abas de uma vez.
  **Importante:** as regras de colapso usam `grid-template-columns:
  minmax(0,1fr)` (não `1fr`) + os itens recebem `min-width:0`. `1fr`≡
  `minmax(auto,1fr)` tem piso = min-content; um filho de largura fixa (donut
  `flex:0 0 230px` em Separação/Ocorrências) inflava o track e fazia as cards
  estourarem a margem da aba. `minmax(0,1fr)`+`min-width:0` deixam o track/item
  encolher → cards = largura do conteúdo, alinhadas. Residual menor: em telas
  bem estreitas o donut pode ficar apertado *dentro* do próprio card (contido;
  ajuste opcional futuro: shrink/wrap do `.chart-erros-*-pie` no ≤480px).
- **Timeline de Carregamento (Gantt) — modo "cabe na tela" (opção B):** no
  `≤768px` o eixo de tempo é fixado na janela real de atividade (1º início →
  último fim das rotas, +3% pad) via JS gated por `matchMedia`
  (`opts.xaxis = Object.assign({}, opts.xaxis, {min,max})` — preserva
  `type:'datetime'`/`labels`; `tickAmount:4`+rotate vêm do `window.Apex`
  global). CSS: override carg-only `#card-carg-timeline{overflow-x:hidden}` +
  `#carg-timeline-chart{min-width:0}` neutraliza o scroll horizontal da Task 6
  só para o carregamento (geral/sep seguem em scroll). Cabe no celular sem
  arraste lateral; hora exata no toque (tooltip). Desktop inalterado.
- Modais: sistema real é `.dash-modal-overlay`/`.dash-modal-card` (drill/picker)
  e `#dash-modal-ocorrencias` (NÃO Bootstrap `.modal-*`). Tabelas com scroll
  horizontal escopadas a `.dash-aba-section .dash-table` (preserva o thead
  sticky e o scroll próprio da tabela do modal de ocorrências).

### 5. Mudanças em JS (mínimas, isoladas)

1. **Drawer:** IIFE isolado em `dashboard.php` (próximo ao IIFE de tema já
   existente, ~linha 1412). Sem threading pelo `dashboard.js`.
2. **ApexCharts responsivo global:** definir `window.Apex = { ... }` no início de
   `dashboard.js` (antes de qualquer `new ApexCharts`). ApexCharts mescla esses
   defaults globais em todo gráfico. Conteúdo:
   - `responsive: [{ breakpoint: 768, options: { legend: { position: 'bottom' },
     dataLabels: { enabled: false }, chart: { toolbar: { show: false } },
     xaxis: { tickAmount: ~4 }, ...fontes menores } }]`.
   - Não edita nenhum gráfico individualmente.
   - Verificar que nenhum gráfico depende de `window.Apex` já definido (não há
     uso atual; confirmar na implementação).
3. **Re-fit:** disparar `window.dispatchEvent(new Event('resize'))` ao:
   trocar de aba, fechar o drawer e em `orientationchange`. Gráficos em aba
   oculta renderizam com width 0; o resize força recálculo. Confirmar na
   implementação se o lazy-load por aba (`data-loaded`) já cobre o caso de
   primeira abertura — se sim, manter apenas os disparos de drawer/orientation.
4. **Full-width no mobile (resolvido em campo — ver memória
   `project_dashboard_tema_genezes_viewport`):** a rota usa o tema **genezes**
   (`#genezes_app_root`, NÃO o goldie). No mobile `body`/`clientWidth` = 375px
   mas o viewport de layout `window.innerWidth` = 512px (= largura do cabeçalho
   fixo, a referência visível). IIFE no bootstrap força `#dash-op-root` +
   ancestrais a `width = max(innerWidth, clientWidth)px` via
   `setProperty(...,'important')` (vence cache + cascata do tema), reaplicado em
   timeouts (120/400/900/1800ms) pois o tema reescreve a largura pós-bootstrap;
   sem listener de `resize` (evita loop no iOS). Mobile-gated, reversível no
   desktop, escopo da página (não edita o tema → zero blast radius global).

### 6. Plano de teste / iteração

- Larguras de teste: 360, 390, 414/430, 768 e landscape (DevTools) + URL de dev real.
- Ordem de polimento: **Visão Geral** → Separação/Carregamento → Ocorrências/
  Pesagem/Ranking. Iteração incremental ("vamos testando").
- Verificação:
  - Checagem responsiva manual por aba nas larguras acima.
  - Desktop visualmente inalterado (comparar antes/depois em ≥1200px).
  - Drawer: abre/fecha por botão, backdrop e Esc; scroll do body travado;
    foco gerenciado.
  - Suítes PHP/JS existentes do sinal seguem verdes (mudança é só apresentação).

## Riscos & mitigações

- **`display: contents` em wrapper de layout:** suportado em navegadores
  modernos; usado só para não introduzir caixa no desktop. Mitigação: validar
  desktop visualmente inalterado em ≥1200px.
- **Gráficos com width 0 em aba oculta:** mitigado pelo dispatch de `resize`
  (Seção 5.3); confirmar interação com o lazy-load `data-loaded`.
- **`window.Apex` global afetar gráfico que sobrescreve opções:** baixo risco
  (defaults mesclados, opções locais vencem); confirmar na implementação.
- **Especificidade CSS:** novo bloco depende de `@media` + seletores
  específicos; usar `!important` apenas onde o arquivo já estabelece o padrão.

## Critérios de sucesso

- Em 360–480px a Visão Geral mostra KPIs legíveis (2 col, órfã full-width) e o
  gráfico de tendência limpo, sem scroll horizontal indesejado na página.
- Barra: abas rolam horizontalmente; filtros acessíveis via drawer funcional.
- Todas as 6 abas utilizáveis no mobile (gráficos/timelines/tabelas com scroll
  horizontal quando necessário; modais em tela cheia).
- Layout desktop (≥1200px) idêntico ao atual.
- Testes existentes verdes.
