<?php

namespace App\Core;

/**
 * Versão do sistema — FONTE ÚNICA.
 * Fica em app/ (entra no zip de atualização), então cada update já traz a versão
 * nova; a tela "Atualização do Sistema" compara esta constante com a publicada.
 * Use SemVer: MAIOR.MENOR.CORRECAO.
 */
final class Versao
{
    public const ATUAL = '1.7.226';
    public const DATA  = '2026-08-29';
    // v1.7.224 (2026-08-29) — **HOTFIX critico: usuario FIRE (suporte tecnico)
    //   nao pode ficar bloqueado nunca.**
    //   Depois da v1.7.223 o usuario FIRE — conta usada pelo suporte tecnico
    //   pra entrar em qualquer BI de cliente — perdeu admin em alguns clientes
    //   onde o admin local marcou modulos limitados na tela sem querer.
    //   Fix: whitelist USUARIOS_MASTER (FIRE, SYSDBA, MOHAMMED) que SEMPRE sao
    //   admin, independente do que estiver em user_permissions.json. Sao
    //   contas de suporte tecnico que precisam existir SEMPRE pra o suporte
    //   conseguir entrar. Fecha o buraco onde admin do cliente conseguia
    //   auto-bloquear o suporte por engano.
    // v1.7.223 (2026-08-29) — **HOTFIX complementar da v1.7.222.**
    //   Depois da v1.7.222 SARA (supervisora no Fire) parou de ver "Admin" no
    //   header, mas continuava vendo TUDO no menu — Metas, Resumo Geral, e o
    //   grupo Configuracao inteiro (SQL Console, Backup, Cards...).
    //   Dois furos identificados e fechados:
    //   1) main.php linha 1058 usava Auth::estaLogado() em vez de FbAuth::isAdmin()
    //      pra decidir se mostra o menu Configuracao — qualquer LOGADO via.
    //   2) FbAuth::podeAreaGeral() liberava Resumo Geral, Metas e Manual pra
    //      qualquer usuario com pelo menos um modulo nao-quiosque. Agora
    //      respeita override da tela: se admin marcou modulos limitados
    //      (sem '*'), so libera areas gerais pra quem tem '*' explicito.
    //   Depois desse hotfix: usuario com so 'caixa' na tela ve SO 'Fechamento
    //   de Caixa' — nada mais.
    // v1.7.222 (2026-08-29) — **FIX seguranca: permissoes da tela agora
    //   PREVALECEM sobre a flag de supervisor do ERP Fire.**
    //   Bug reportado no Achadinhos: admin marcou SO 'Fechamento de Caixa' pra
    //   SARA em /configurar/usuarios, mas ela via TUDO (Resumo Geral, Vendas,
    //   Estoque, TOTAL GERAL R$ 10.790,51 das 6 lojas). Causa: SARA e supervisora
    //   no Fire — FbAuth::isAdmin() retornava true por causa da flag do ERP e
    //   ignorava completamente as permissoes da tela.
    //   Fix em FbAuth::isAdmin(): se o admin do BI configurou permissoes ESPECI-
    //   FICAS (modulos limitados sem '*') na tela, essa configuracao PREVALECE
    //   sobre a flag de supervisor do ERP. So mantem admin automatico se NAO
    //   tem entrada no user_permissions.json ou se tem '*' na lista.
    //   IMPACTO: qualquer cliente onde admin ja configurou modulos limitados
    //   em /configurar/usuarios pra um supervisor passa a respeitar. Fecha
    //   falha de seguranca — dado sensivel (faturamento, DRE) nao aparece
    //   mais pra usuario limitado so por estar como supervisor no ERP.
    // v1.7.214 (2026-08-28) — **FORCA-TAREFA no Fechamento de Caixa.** Cliente
    //   comparou o relatorio impresso do Fire com a tela e nao batia. Cinco
    //   correcoes, todas conferidas contra o PDF do proprio ERP (Disk Rapido,
    //   caixa 7952 de 27-08-2026, operadora FILOMENA):
    //
    //   1) **A diferenca do caixa passa a sair da SOMA DAS FORMAS**, igual ao
    //      relatorio do Fire, e nao mais do campo ABERTURA_CAIXA.VALOR_CALCULADO.
    //      Os dois discordavam: o relatorio fecha em "Diferenca 0,00" com saldo
    //      R$ 601,50, o campo trazia R$ 627,24 e a tela acusava -R$ 25,74 — com
    //      as 5 formas batendo uma a uma na propria tela. Medido: no Importados
    //      os dois valores coincidem em 39 de 39 caixas, entao a mudanca e'
    //      neutra onde o ERP grava certo. Campo antigo continua exposto em
    //      'total_calculado_erp' e 'base_calculo' diz de onde saiu o numero.
    //
    //   2) **"Nao conferido" deixou de ser "divergente".** Sem valor digitado no
    //      fechamento, o Fire grava VALOR_INFORMADO=0 e a diferenca virava o
    //      faturamento inteiro do turno, em vermelho, acusando a operadora de uma
    //      falta que nao existe. No Disk Rapido eram 34 dos 66 caixas de agosto
    //      (52%), nas 4 operadoras. Status novo, cinza, com rotulo proprio.
    //
    //   3) **ITENS cancelados dentro da venda agora aparecem.** O relatorio do
    //      Fire imprime "CANCELAMENTOS PRE-VENDA" e o BI mostrava ZERO — nunca
    //      lia a tabela LANC_ITENS_CANCELA (84 mil registros so no Importados).
    //      Nova secao no detalhe do caixa com hora, produto, qtd, valor e quem
    //      cancelou. Modulo de auditoria que nao mostra cancelamento e' pior que
    //      nao ter modulo.
    //
    //   4) **Quantidade absurda nao entra no total.** Medido: 504.072 bombons x
    //      R$ 1,99 = R$ 1.003.103,28 num item so; 3 lancamentos desses inflavam o
    //      mes de R$ 49.548,78 para R$ 2.069.878,51. E' codigo de barras digitado
    //      no campo quantidade, e o cancelamento foi a CORRECAO do erro. Esses
    //      casos aparecem em bloco separado, com o motivo provavel escrito.
    //
    //   5) **PIX volta a ser PIX.** A classificacao so aceitava FORMA_PGTO tipo 8;
    //      no Disk Rapido o PIX esta cadastrado como tipo 3 e caia em
    //      "Convenio/Outros", com o indicador total_pix zerado mesmo com PIX no
    //      turno. Agora identifica pelo nome tambem, e PIX nao conta como cartao.
    //      Conferido: 18 de 18 caixas do Importados com PIX reconhecido.
    //
    // v1.7.221 (2026-08-28) — aviso das duas apuracoes agora fala SO da diferenca.
    //   Saiu a coluna de observacao que dizia de onde a diferenca vinha e a frase
    //   "esta diferenca nao e' do seu caixa". A tela mostra os dois valores, a
    //   diferenca e o que fazer — sem atribuir origem a nada nem a ninguem.
    //   Fecha a limpeza da v220: nas telas nao existe "outro sistema".
    // v1.7.220 (2026-08-28) — **UM SISTEMA SO NA TELA DO CLIENTE.**
    //   Regra nova do Derlivon: o cliente ve BI e retaguarda como UM produto. A
    //   palavra "ERP" (e "ERP Fire Sistemas") sai de tudo que o usuario le — nas
    //   telas nao existe "o outro sistema", existe "o sistema".
    //   107 trechos trocados em 45 arquivos de tela: Consolidado, Manual, DRE,
    //   Recebiveis do Atacado, Cozinha, Leitor, Config, Margem, Fechamento de
    //   Caixa, Ponto de Equilibrio. Exemplos:
    //     "o CMV que veio do ERP Fire Sistemas"  -> "que veio do sistema"
    //     "custo nao cadastrado no ERP Fire"     -> "custo do produto nao cadastrado"
    //     "Ver a via do relatorio do ERP"        -> "Ver a via do relatorio de fechamento"
    //     "o ERP mostra dois valores diferentes" -> "este caixa tem duas apuracoes diferentes"
    //     "leve ao suporte do ERP"               -> "acione o suporte tecnico (66) 99248-8989"
    //   Referencia tecnica (ERP, Fire, RDPrint, nome de tabela) continua nos
    //   COMENTARIOS de codigo — o cliente nao le comentario, e quem for dar
    //   manutencao precisa saber de onde o dado vem. Mesma logica da marca Master.
    //   Lint de PHP e JS passou em tudo depois da troca em massa.
    // v1.7.219 (2026-08-28) — **VIA DO RELATORIO: COPIA FIEL, 100% conferida.**
    //   A v218 saiu com a via em 74% de fidelidade visual. Medi a grade do
    //   RDPrint caractere a caractere no relatorio real (Disk Rapido, caixa 7952)
    //   e fechei as diferencas:
    //     - hora do cabecalho com SEGUNDOS (o papel imprime 14:59:07; a tela
    //       cortava em HH:MM). Novos campos data_abertura_full/data_fechamento_full.
    //     - valor das formas de venda termina na coluna 38, nao 37.
    //     - ordem da secao "VALORES APURADOS": as formas usadas na venda primeiro
    //       e o dinheiro por ultimo, com o rotulo do papel — "A Vista", nao
    //       "DINHEIRO". Antes saia em ordem alfabetica do comparativo.
    //   Resultado medido linha a linha contra o texto extraido do PDF do ERP:
    //   **52 de 52 linhas iguais (100%)**. Da pra conferir papel x tela sem
    //   entrar no ERP. Ferramenta de comparacao ficou no scratchpad da sessao.
    //
    //   Segue valendo o aviso na tela: nao e' o arquivo do RDPrint, e' remontado
    //   das mesmas tabelas — e divergencia em relacao ao papel indica problema
    //   dentro do proprio ERP, nao da via.
    // v1.7.218 (2026-08-28) — **VIA DO RELATORIO DO ERP dentro do BI.**
    //   Pedido do Derlivon: conferir o caixa sem precisar entrar no ERP e emitir
    //   o relatorio de novo. Botao "Ver a via do relatorio do ERP" no detalhe do
    //   caixa remonta o relatorio de fechamento — cabecalho, vendas por operacao,
    //   resumo das formas, sangrias e suprimentos, apurado x informado, saldos,
    //   diferenca e cancelamentos de pre-venda — em fonte de largura fixa, igual
    //   ao papel, com botao de imprimir.
    //
    //   (!) Nao e' o arquivo do ERP: aquele sai do RDPrint (Deltress), a que o BI
    //   nao tem acesso. E' uma RECONSTRUCAO a partir das mesmas tabelas, e a tela
    //   diz isso ao usuario com essas palavras — inclusive que diferenca em
    //   relacao ao papel indica divergencia dentro do proprio ERP. Quando o caixa
    //   tem a divergencia da v217, a via avisa qual dos dois valores esta usando.
    //
    //   Novo: FechamentoCaixaRepository::vendasPorOperacaoDoCaixa() (secao
    //   "VENDAS POR OPERACAO", ex.: 115-VENDA DE MERCADORIA) e
    //   ViaRelatorioErpService, que monta o texto. Endpoint /caixa/fechamento/via-erp.
    // v1.7.217 (2026-08-28) — **O BI MOSTRA O QUE O ERP TEM. NAO MASCARA.**
    //   Correcao de rumo pedida pelo Derlivon: nas v214-216 eu tinha tomado duas
    //   decisoes em silencio, e decisao em silencio nao cabe num BI de auditoria.
    //
    //   1) **Divergencia do proprio ERP agora aparece na tela.** O Fire guarda o
    //      apurado do caixa em dois lugares — o campo ABERTURA_CAIXA.VALOR_CALCULADO
    //      e o total que o relatorio de fechamento imprime — e em alguns caixas os
    //      dois NAO batem (Disk Rapido, caixa 7952: relatorio R$ 601,50, campo
    //      R$ 627,24). A v214 passou a usar o do relatorio e deixou o outro so no
    //      JSON. Agora o caixa mostra um bloco com OS DOIS valores, a diferenca e
    //      a frase que importa: **isso nasceu dentro do ERP, nao no caixa do
    //      operador** — com orientacao de levar o relatorio ao suporte do ERP.
    //      Nao e' papel do BI escolher calado qual dos dois esta certo.
    //
    //   2) **Cancelamento com quantidade absurda volta pro total.** A v214 tirava
    //      do total os lancamentos com QTD >= 1000 e escrevia "provavel erro de
    //      digitacao". Interpretacao minha: o registro do ERP diz 504.072 unidades,
    //      e e' isso que tem que aparecer. O total agora e' o registro cheio; o
    //      valor sem esses lancamentos vem ao lado ('total_sem_atipicos') e a linha
    //      atipica segue destacada com o motivo provavel — mas o numero do ERP e'
    //      mostrado como esta gravado.
    //
    //   **Nova ferramenta: tools\divergencia-erp-caixa.php** — mede quantos caixas
    //   tem os dois valores do ERP em desacordo, o tamanho da diferenca e se ela
    //   coincide com alguma forma de pagamento do turno. E' a evidencia pra abrir
    //   chamado no desenvolvimento do ERP em vez de discutir por achismo.
    //   Medido no Importados: **0 de 127 caixas divergem** — ou seja, o problema
    //   nao e' generalizado, aparece em ambiente/configuracao especifica.
    // v1.7.216 (2026-08-28) — **CONFERENCIA E' UM CAIXA POR VEZ.**
    //   Reportado pelo Derlivon: ao filtrar por operadora, a tela empilhava TODOS
    //   os caixas dela no periodo. Quem confere rolava a pagina e caia sem
    //   perceber no caixa seguinte da MESMA pessoa — conferindo o valor errado.
    //   Agora, com mais de um resultado, aparece a LISTA (numero, horario,
    //   operadora, valor, status e diferenca) e a tela abre SO o caixa escolhido,
    //   com botao "voltar para a lista". Um resultado unico abre direto.
    //
    //   **De quebra, a tela deixou de travar.** A rota /dados montava todas as
    //   secoes de todos os caixas: medido no Importados, 174 caixas em 30 dias =
    //   108 SEGUNDOS e o navegador congelava (reproduzido aqui). A lista agora
    //   usa fechamentosResumo(), que le so o cabecalho: **os mesmos 174 caixas em
    //   0,01 s**. O detalhe pesado (7,5 s) sai sob demanda, pelo caixa escolhido.
    //   Quem precisar do comportamento antigo: /caixa/fechamento/dados?modo=completo
    //
    //   **BUG de digitacao achado no caminho:** "$ordemDetectada×" dentro de aspas
    //   duplas. O PHP aceita byte >= 0x80 em nome de variavel, entao o "×" (U+00D7)
    //   virou parte do nome e o alerta de erro de digitacao do operador saia com
    //   "cerca de × menor", sem o numero, mais um warning no log a cada caixa.
    //   Corrigido pra {$ordemDetectada}×. Varri o app inteiro: era o unico caso.
    // v1.7.215 (2026-08-28) — complemento da forca-tarefa (a 214 ja estava no
    //   cliente quando estes tres sairam; por isso versao nova em vez de
    //   republicar a mesma — quem ja atualizou nao veria a diferenca).
    //   1) **Busca pelo numero do caixa** na tela de Fechamento. Quando o cliente
    //      liga dizendo "o caixa 7952 esta errado", e' esse numero que se quer
    //      digitar — antes era preciso adivinhar operadora e data pra chegar nele.
    //      Novo endpoint /caixa/fechamento/por-codigo descobre a data pelo
    //      COD_ABERT_CAIXA e abre so aquele caixa.
    //
    //   2) **Alarme de risco fiscal calibrado.** Turno com menos de 10 vendas nao
    //      vira "Critico": no caixa 7952 eram 6 vendas, 3 no crediario = 50% sem
    //      nota e alarme vermelho. Com essa amostra UMA venda muda 17 pontos —
    //      o alarme falava do tamanho do turno, nao de risco. Agora mostra
    //      "Amostra pequena". Alarme que grita a toa faz perder a confianca na tela.
    //
    //   3) Card "Cancelamentos" virou **"Notas canceladas"**: ele conta nota
    //      inteira cancelada, e os itens de pre-venda tem secao propria agora.
    //      Dois lugares falando "cancelamento" com numeros diferentes confundia.
    //
    //   **Historico de sangria com acento parou de sumir.** Vinha do Firebird em
    //   WINDOWS-1252 e o json_encode (JSON_PARTIAL_OUTPUT_ON_ERROR) trocava o
    //   campo por null: "pgto,hortalica" sumia e "cx filomena" aparecia. Sangria
    //   sem historico parece dinheiro tirado sem motivo — e o motivo estava
    //   gravado no ERP o tempo todo. 6 de 146 movimentacoes de agosto afetadas.
    //
    //   **Central Financeira: carteira loja a loja + TOTAL GERAL.** O backend ja
    //   mandava kpis_por_loja e a tela so mostrava o consolidado; dono de rede
    //   tinha que trocar o filtro loja a loja pra comparar. Tabela com em aberto,
    //   % do grupo, vencido, % vencido, titulos, DSO, ticket e proximos 30 dias.
    //   Loja fora do ar entra na tabela avisando que NAO esta nas somas.
    //
    //   **Nova ferramenta: tools/auditoria-financeira.php.** Recalcula cada numero
    //   das telas financeiras por SQL independente e compara. Rodar antes de
    //   apresentar o BI a cliente. 65 verificacoes nas 5 lojas do Importados,
    //   0 divergencia. Tambem avisa quando o dado do ERP distorce a leitura
    //   (ex.: carteira 100% sem baixa mede processo, nao inadimplencia).
    // v1.7.213 (2026-08-26) — **FIX: o navegador guardava dado de BI por 5 HORAS.**
    //   Achado testando a tela nova em :8001 — salvava um valor, recarregava e a
    //   tela mostrava o numero velho, como se o salvar nao funcionasse.
    //   Causa: session_cache_limiter('private') fazia o PHP mandar
    //   "Cache-Control: private, max-age=18000" em TODA resposta com sessao, e a
    //   excecao da v1.7.144 so tratava pagina HTML — as APIs ficavam de fora.
    //   Medido: /api/financeiro/dre e /api/financeiro/kpis respondiam com os
    //   mesmos 5h de cache. Provavel raiz do velho habito de "da Ctrl+F5 que
    //   aparece". Fix em Application::bootstrap: URI /api/ agora responde
    //   no-store, no-cache, must-revalidate. Pagina HTML segue com o
    //   stale-while-revalidate da v144 (marker X-BI-Cache-Fix intacto).
    //
    //   **FIX na tela Ponto de Equilibrio** (5 defeitos achados no teste local):
    //   1) Salvar mostrava erro vermelho "removeChild of null" MESMO tendo salvo:
    //      o container do grafico era limpo antes do echarts.dispose().
    //   2) Consequencia do (1): o erro abortava o render no meio — KPIs
    //      atualizavam, mas o quadro "Como chegamos nesse numero" e a tabela de
    //      contas fixas ficavam com o valor velho na tela. Agora cada bloco
    //      renderiza isolado (um erro nao derruba os outros).
    //   3) Sem contas fixas informadas, TODAS as barras do grafico saiam
    //      vermelhas — passava a ideia de que todo dia foi ruim. Sem meta a barra
    //      agora e neutra e a legenda explica o que preencher.
    //   4) Portugues: "o BI ja sabe 38,2% das suas vendas passa no cartao" ->
    //      "ja sabe que 38,2% das suas vendas passam no cartao".
    //   5) Rotulo "(divisao) Contas fixas do mes" confundia a conta ->
    //      "Contas fixas do mes (dividido pela margem acima)".
    //   A API da tela tambem passou a pedir cache: 'no-store' no fetch.
    //   Conferido ao vivo em :8001 (Achadinhos 10 Barra, contas fixas de teste de
    //   R$ 25.000,00): CMV 40,0% + cartao 0,95% = margem 59,1% -> ponto de
    //   equilibrio R$ 42.331,47 no mes -> R$ 1.628,13 por dia. Salva, recarrega e
    //   o valor continua la. Central Financeira: 1.273 linhas, 0 duplicada.
    // v1.7.212 (2026-08-26) — **FIX GRAVE: valores do financeiro sairam 3x maiores
    //   em cliente com mais de uma empresa no mesmo banco.**
    //   Causa raiz: TIPO_OPERACAO tem PK COD_TIPO_OPERACAO + COD_EMPRESA, e 61 dos
    //   85 JOINs do BI casavam so pelo codigo. Cada linha virava uma por empresa
    //   cadastrada — todo SUM/COUNT saia multiplicado e a listagem repetia titulo.
    //   Medido no banco do Importados (3 empresas), agosto/2026:
    //   - KPI "A Receber": R$ 3.730.737,78 -> R$ 278.967,91
    //   - KPI "A Pagar": R$ 49.455.929,19 -> R$ 16.285.559,59
    //   - Central Financeira: R$ 831.855,81 -> R$ 277.285,27 (4.038 -> 1.346 titulos,
    //     e cada titulo aparecia 3x na lista)
    //   - Fluxo de Caixa (saidas de agosto): R$ 382.093,86 -> R$ 127.364,62
    //   - DRE (receita de agosto): R$ 2.694.528,45 -> R$ 903.664,67
    //   - Regua de Cobranca: a fila tinha 59 mensagens para 25 titulos — o MESMO
    //     cliente recebia a cobranca ate 3 vezes. Agora 25 mensagens, 0 repetida.
    //   O CMV% quase nao muda (55,67% -> 55,62%): numerador e denominador inflavam
    //   juntos, por isso o erro passou despercebido tanto tempo.
    //   Fix: novo helper App\Support\FireSchema::joinEmpresa() detecta a coluna via
    //   RDB$RELATION_FIELDS (Fire antigo pode nao ter) e completa o ON. Aplicado em
    //   34 JOINs: FinanceiroRepository (10), FinanceiroCentralService (14),
    //   RecebiveisRepository (12), ReguaCobrancaService (1).
    //   Cliente de loja unica NAO e afetado — os numeros dele nao mudam.
    //   Ainda faltam 24 JOINs fora do financeiro (Compras, Sugestao de Compra,
    //   Automoveis, Vendas, Estoque, Dashboard, Cadastro Criticos, Diag Operacoes).
    //   Bonus: a DRE ficou mais rapida (30s -> 18s), porque processa 1/3 das linhas.
    //
    //   **NOVA TELA: Ponto de Equilibrio** (/financeiro/ponto-equilibrio).
    //   Responde "quanto preciso vender por dia so pra empatar?" — contas fixas do
    //   mes divididas pela margem de contribuicao, dividido pelos dias em que a loja
    //   realmente vendeu (nao por 30). O lojista informa as contas fixas, a taxa
    //   media da maquininha e a comissao; o BI pega o CMV do ERP e a participacao
    //   real do cartao no mix. 4 KPIs (meta diaria, venda media, PE do mes, margem
    //   de seguranca), faixa com semaforo, grafico dia a dia (barra verde bateu a
    //   meta, vermelha nao), quadro "como chegamos nesse numero" com a fonte de cada
    //   valor, e tabela mostrando quanto de venda cada conta fixa exige.
    //   Sem dado nao inventa numero: diz o que falta preencher. Quando o custo do
    //   produto nao esta cadastrado no ERP, aceita a margem informada na mao.
    //   Novo FinanceiroRepository::cmvPeriodo() calcula o CMV de UM mes com cache
    //   (a tela carrega em 1,7s; usar dreMensal levaria 30s). Entrou no menu lateral
    //   (com toggle em Configurar Cards), no submenu do Financeiro, no manual com
    //   guia completo e no glossario.
    // v1.7.211 (2026-08-21) — **FIX: "Caixa OK" nao mente mais quando tem centavos.**
    //   Reportado pelo cliente: banner verde dizia "Caixa OK — nada a investigar"
    //   E ao mesmo tempo "Diferença de R$ 0,29" — leitura contraditoria (parece bug).
    //   Fix: dois banners visualmente diferentes.
    //   - Diferenca exata (< R$ 0,01) → "✅ Bateu certo — nada a investigar" +
    //     "Todas as formas fecharam exatamente com o sistema."
    //   - Diferenca de centavos (0,01–0,50) → "🟢 Fechou dentro da tolerância —
    //     R$ 0,29 sobrou" + "Arredondamento normal em vendas com centavos.
    //     Nada a investigar, mas o valor está registrado nas seções abaixo."
    //   Backend limpou o tudo_ok_sub (frontend decide o texto agora).
    // v1.7.210 (2026-08-21) — **FIX: contradicao "Precisa revisar" x "Caixa OK".**
    //   Bug: card externo mostrava "Precisa revisar" (laranja) e quando abria
    //   o drill dizia "Caixa OK — nada a investigar" (verde) pro MESMO caixa.
    //   Alem disso, texto "Todas as formas bateram" era mentira quando dinheiro
    //   tinha centavos de diferenca (ex: +R$ 0,29). Reportado no HERICA CX 21/08.
    //   4 fixes ALINHANDO thresholds em 4 pontos que decidiam status separadamente:
    //   1) FechamentoCaixaService::classificarCentral — se dif total < R$ 0,50 E
    //      dinheiro < R$ 5 E eletronico < R$ 10 E nada de fraude (troca de forma,
    //      cancel tardio, sangria propria), forca VERDE. Antes um chip amarelo
    //      qualquer marcava pendente mesmo com drill dizendo OK.
    //   2) FechamentoCaixaService::analiseConferente — novo campo `tudo_ok_sub`
    //      HONESTO: em vez de "Todas as formas bateram", mostra "Diferença de
    //      R$ 0,29 a mais — arredondamento normal, considera-se OK".
    //   3) fechamento-caixa-v2.js::renderConferente — usa `tudo_ok_sub` do backend.
    //   4) fechamento-caixa-v2.js::renderKPIs — threshold "Bateu certo" pra 0.50
    //      (antes 0.01). Novo label "Arredondamento OK" pra 0.01–0.50. Alinha com
    //      o resto do sistema.
    //   5) central.php::fraseAlerta — fallback nao diz mais "Precisa revisar"
    //      generico; mostra "Apenas R$ X sobrou (arredondamento) — abrir se quiser".
    // v1.7.209 (2026-08-16) — **UI: badge do NOME DA LOJA em destaque no drill.**
    //   Cliente pediu pra destacar mais visualmente. Antes: azul claro discreto.
    //   Agora: gradiente azul-escuro sólido, fonte 1rem negrito uppercase, borda
    //   dupla, sombra sutil, letter-spacing. Vira o elemento MAIS forte do header.
    //   Operador e data continuam discretos (contraste).
    // v1.7.208 (2026-08-16) — **FIX: iframe destruido pelo overlay quebrava abrir outro caixa.**
    //   Bug: quando fccIframeCarregou detectava url fora do esperado (ex: rota
    //   de export ou 404), substituia body.innerHTML por overlay "Sem dados pra
    //   mostrar" — isso DESTRUIA o iframe. Ao abrir outro caixa, $('#fcc-drill-iframe')
    //   retornava null e dava "Cannot set properties of null (setting 'src')".
    //   Fix: em fccInvestigar, se iframe nao existe, recria dentro do drill-body
    //   antes de setar src. Em fccFecharDrill, checa se iframe existe antes de
    //   setar src pra evitar erro se ja foi limpo.
    // v1.7.207 (2026-08-16) — **FIX: numero Excel + cores PDF nas celulas KPI.**
    //   Bug 1: mso-number-format estava mal escapado ('#\.\#\#0\,00') e o valor
    //   saia com 4 casas ("287,3000"). Fix: numero em formato INGLES (6033.83
    //   com ponto decimal) + mso-number-format "#,##0.00" — Excel converte
    //   automaticamente pro locale BR na exibicao (6.033,83 e SOMAVEL).
    //   Bug 2: cores das celulas KPI (verde/vermelho/amarelo) sumiram no PDF
    //   quando adicionei classe .num junto (specificity). Fix: seletor mais
    //   especifico td.num.kpi-val.bom etc, com background em azul suave por
    //   default (destaca dos labels).
    // v1.7.206 (2026-08-16) — **Excel: celulas somam de VERDADE (numero puro).**
    //   Cliente: "celulas nao estao somando, tem que ser real". Fix:
    //   Antes: mandava "R$ 6.033,83" como texto — Excel nao trata como numero.
    //   Agora: no Excel manda numero puro "6.033,83" (locale BR) + CSS
    //   mso-number-format aplica moeda automatica. Assim:
    //     - Seleciona coluna Valor no Excel → soma aparece na barra de status
    //     - Da pra usar =SUM(), filtros, ordenacao numerica, pivot table
    //     - PDF continua com "R$ 6.033,83" pra visual bonito
    //   Aplicado em TODAS as celulas com valor: formas, sangrias, suprimentos,
    //   cancelamentos, devolucoes, formula do dinheiro, KPIs.
    // v1.7.205 (2026-08-16) — **Export MAIS colorido + TODAS as informacoes.**
    //   Cliente pediu: "quero um Excel mais colorido e com todas informacoes do
    //   caixa". Fix:
    //     + Nova secao ⏱ "Duracao e ritmo do turno" (teal): quanto tempo o caixa
    //       ficou aberto, cupons/hora, R$/hora, se foi reaberto.
    //     + Nova secao 🧮 "Como o sistema calculou o dinheiro esperado" (violeta):
    //       fórmula transparente Vendas + Suprimento + Reforcos − Sangrias =
    //       ESPERADO, com verde nos + e vermelho nos −, comparado com o
    //       declarado e diferenca destacada por cor semantica.
    //     + Nova secao ❌ "Cancelamentos no turno" (rosa-escuro): data/hora,
    //       nota, motivo, operador, valor. So' aparece se houver cancelamento.
    //     + Nova secao ↩ "Devolucoes no turno" (amarelo-escuro): data/hora,
    //       nota, cliente, operador, valor. So' aparece se houver devolucao.
    //   Total: relatorio ganhou 4 secoes novas com paleta variada (teal,
    //   violeta, rosa-escuro, amarelo-escuro) alem das ja existentes.
    // v1.7.204 (2026-08-16) — **FIX Hora + Motivo saiam VAZIOS ('—') no export.**
    //   Bug: eu estava lendo $s['hora'] e $s['motivo'], mas formatarMovimentos()
    //   do service retorna 'data_hora' e 'historico'. Fix: alinhado com as
    //   chaves reais + fallback pros nomes antigos.
    //   Agora Excel/PDF mostra "08:29 · lanche de domingo" igual a tela do BI.
    // v1.7.203 (2026-08-16) — **Excel export: paleta semantica variada.**
    //   Cliente reclamou "tudo azul, nada de outra cor". Causa: os badges com
    //   background em <span> nao renderizam no Excel — so' cores em TD funcionam.
    //   Fix:
    //     - Removidos <span class="badge"> — cores agora aplicadas DIRETO no <td>
    //       (cell-ok/atencao/critico com fundo verde/amarelo/vermelho + texto).
    //     - Cabecalhos de secao com CORES DIFERENTES por tipo:
    //         💰 financeiro   -> azul (#4338CA)
    //         📊 indicadores  -> roxo (#6D28D9)
    //         💳 formas       -> azul-info (#0369A1)
    //         📤 sangrias     -> VERMELHO (#B91C1C) — sinal de SAIDA
    //         📥 suprimentos  -> VERDE (#15803D) — sinal de ENTRADA
    //         🎯 ajuda        -> LARANJA (#C2410C) — alerta
    //     - Coluna Valor: sangria em vermelho, suprimento em verde.
    //     - Total geral colorido por tipo (formas azul, sangria vermelho,
    //       suprimento verde).
    //     - Diferenca positiva (sobra) em verde, negativa (falta) em vermelho.
    // v1.7.202 (2026-08-15) — **Export: timeout maior + formatacao profissional.**
    //   Bug: PDF dava "Maximum execution time of 60s" em caixas com muitas
    //   vendas (v201 otimizou mas ainda batia limite em turnos grandes).
    //   Fix: set_time_limit(180) + ini_set max_execution_time=180.
    //   Feature: CSS refinado (v202) — tipografia maior (13px base, KPIs 16px),
    //   espacamentos generosos, cabecalho premium com barra colorida esquerda,
    //   linhas alternadas suaves (#FAFAFA), badges em uppercase, page-break-inside
    //   avoid nas tabelas pra nao quebrar no meio na impressao.
    // v1.7.201 (2026-08-15) — **FIX lentidao export: SO' o dia do caixa.**
    //   Cliente reclamou export demorando muito. Causa: v200 processava 180
    //   dias inteiros pra pegar 1 caixa. Fix: 1 query rapida pra pegar
    //   DATA_ABERTURA do caixa (< 5ms), depois fechamentosCompletos filtrado
    //   ao DIA especifico. De ~180 caixas processados pra ~10-20.
    // v1.7.200 (2026-08-15) — **FIX 500 no exportar-pdf/exportar-xlsx.**
    //   Bug: chamei processarFechamentos() no service — metodo nao existe.
    //   O certo e' fechamentosCompletos(busca, dataIni, dataFim), que ja
    //   processa tudo. Fix: usa o metodo real, filtra pelo COD_ABERT_CAIXA
    //   depois. Busca em intervalo de 180 dias (defensivo).
    // v1.7.199 (2026-08-15) — **Ajuda ao Conferente: passos CURTOS + SEM JARGAO
    //   + botao "Ver como investigar" (colapsavel).**
    //   Cliente reclamou: texto de jornal com "KPI", "PDV", "cupom", "extrato",
    //   "TEF" — usuario nao tem tempo nem entende. Fix:
    //     - Todos os 5 padroes reescritos com 3-4 linhas MAX, vocabulario do
    //       dia a dia (nada de PDV/cupom/TEF/extrato).
    //     - Passos escondidos dentro de <details> "▸ Ver como investigar" —
    //       fechado por default. Card mostra so' titulo + detalhe curto; se
    //       quiser passo a passo, clica pra abrir.
    //     - Removidos "Como investigar:" no inicio das sugestoes (redundante
    //       ja que o botao ja diz isso).
    // v1.7.198 (2026-08-15) — **KPI Diferença TOTAL mostra AS 2 divergencias.**
    //   Cliente pediu: quando ha' divergencia isolada de forma (ex: DINHEIRO
    //   R$ 127,51) alem da diferenca do total (R$ 0,20), mostrar ambos NO
    //   proprio card do KPI. Antes so' mostrava a diferenca do total.
    //   Fix: alem do valor principal (R$ 0,20 Sobrou), adiciona uma linha
    //   fina embaixo (separada por linha tracejada): "⚠️ DINHEIRO: R$ 127,51
    //   a menos". So' aparece se existe forma com dif > R$ 20. Gestor ve os
    //   2 numeros de imediato no KPI, sem ter que rolar pra baixo.
    // v1.7.197 (2026-08-15) — **TEXTOS SUCINTOS + para de afirmar sem prova.**
    //   Cliente reclamou que o banner tem texto de jornal pra R$ 0,20 e afirmava
    //   "houve um ajuste" sem prova. Fix:
    //     - Frase principal: "R$ X contados · R$ Y apurados · R$ Z sobrou/faltou
    //       (arredondamento normal)." — 1 linha.
    //     - Aviso extra (quando forma tem dif grande): so' 2 casos —
    //       (a) outra forma tem valor oposto = "provavel troca de forma"
    //       (b) senao = "Investigue: Movimentacoes, Devolucoes, Cancelamentos"
    //       Sem afirmar "houve ajuste" — nao sabemos.
    //     - Fix bug de formatacao "R$ R$ 0,20" (fmtBR ja retorna com prefixo).
    // v1.7.196 (2026-08-15) — **FIX: analise HONESTA, para de chutar causas.**
    //   Cliente reportou: banner amarelo dizia "Provavelmente uma venda foi
    //   registrada na forma errada" mesmo com todas as outras formas ✅.
    //   Matematicamente impossivel — se venda foi digitada errada, OUTRA forma
    //   teria +R$ X. Era CHUTE meu como fallback.
    //   Fix: analisa a realidade matematica em 3 casos, cada um verificado:
    //     1. Existe outra forma com diferenca de sinal OPOSTO E magnitude
    //        compativel (>= 30% do valor da forma principal)? -> troca de forma
    //        REAL. So' entao afirma isso.
    //     2. Devolucao/cancelamento no turno bate com a diferenca (± 15%)?
    //        -> explicacao tecnica confirmada.
    //     3. Nenhum dos dois -> HONESTO: "matematicamente houve algum ajuste
    //        no total apurado (devolucao, cancelamento, desconto, troca ou
    //        outro lancamento). Verifique as secoes X, Y, Z pra achar."
    //        NAO CHUTA a causa quando nao sabe.
    //   Ver [[feedback-analise-pessoas-reais-responsabilidade]] — regra 5.
    // v1.7.195 (2026-08-15) — **FIX: "bateu certinho" mentia com dif < R$ 0,50.**
    //   Bug: Math.abs(0.20) < 0.5 era true, entao o banner dizia "bateu
    //   certinho" mesmo com R$ 0,20 de diferenca — contradizendo o KPI acima
    //   que dizia "Sobrou R$ 0,20". Gestor viu e questionou.
    //   Fix: "bateu certinho" so' se |dif| < R$ 0,01 (praticamente zero).
    //   Qualquer diferenca real e' reportada com o valor. Threshold de
    //   arredondamento normal continua em R$ 5.
    // v1.7.194 (2026-08-15) — **RECONCILIA os DOIS numeros de diferença no mesmo banner.**
    //   Gestor perguntou: "por que mostra 0,20 e nao mostra 127,51?". A resposta
    //   e' que os dois EXISTEM: 0,20 e' do TOTAL do caixa, 127,51 e' SO' da
    //   forma DINHEIRO. Convivem porque devolucao/cancelamento pago em dinheiro
    //   abate a forma "dinheiro" no apurado, mas o TOTAL fica igual (a devolucao
    //   tambem abate o total).
    //   Fix: quando |dif_total| < 5 (fechou) MAS alguma forma tem |dif| > 20,
    //   adicionar aviso amarelo no banner: "os DOIS numeros convivem: TOTAL
    //   fechou mas a forma X tem R$ Y a menos". Se ha' devolucao/cancelamento
    //   > R$ 20 no turno, explica que provavelmente foi pago em X — e' o
    //   funcionamento normal do PDV. Direciona pra Ajuda ao Conferente pra
    //   analise tecnica completa.
    // v1.7.193 (2026-08-15) — **CLAREZA: explica de ONDE vem a diferenca total.**
    //   Gestor viu "Diferença TOTAL R$ 0,20 Sobrou" e perguntou "de onde faltou?"
    //   — nao entendeu que sobrou e nao entendeu de onde saiu o numero.
    //   Fix: adicionado banner explicativo logo abaixo dos 9 KPIs:
    //     "De onde vem a diferença TOTAL: Operador contou R$ X · sistema
    //      calculou R$ Y · R$ Z a mais/menos na gaveta."
    //   3 modos:
    //     - abs(dif) < 0,50 → "bateu certinho"
    //     - abs(dif) < 5    → em verde: "diferença pequena — geralmente
    //                         arredondamento normal (cliente pagou centavos
    //                         a mais/menos, moedas trocadas)"
    //     - abs(dif) >= 5   → em vermelho: "role até o comparativo por forma
    //                         abaixo pra ver em qual forma apareceu"
    //   Vira frase pronta que o gestor le sem calcular.
    // v1.7.192 (2026-08-15) — **CLAREZA: os DOIS numeros de "diferença" tem rotulos claros.**
    //   Confusao real: KPI "Diferença R$ 0,20" (topo) e alerta "DINHEIRO diferença
    //   R$ 127,51" (ajuda ao conferente) na mesma tela, sem indicacao que sao
    //   DUAS coisas diferentes. Fix:
    //     - KPI do topo: rotulo "Diferença" -> "Diferença TOTAL" + subtitulo
    //       "(todas as formas)" + tooltip explicativo.
    //     - Achado do dinheiro: titulo "Só na forma DINHEIRO faltou R$ X — não
    //       é a diferença total do caixa". Detalhe explica que existem DOIS
    //       numeros de diferenca (total vs por forma) e a relacao entre eles.
    //     - Padrao devolucao_explica: cabecalho com "ATENÇÃO: são DOIS números
    //       diferentes de 'diferença'" e a distincao clara.
    //   Objetivo: gestor lendo entende de imediato que "sobrou R$ 0,20" NO
    //   TOTAL e "faltou R$ 127,51" NO DINHEIRO nao sao contradicao.
    // v1.7.191 (2026-08-15) — **UI: painel de ajuda COLAPSAVEL (tela preta livre).**
    //   Cliente reportou: mesmo com scroll (v190), a tela branca de ajuda ocupava
    //   quase toda a viewport apertando os DADOS (KPIs, comparativo, botoes de
    //   marcar conferido/pdf/excel). Feedback: dados sao o principal, explicacao
    //   e' opcional — inverter.
    //   Fix: barra clicavel "🎯 Ajuda ao Conferente" (colapsavel) substitui o
    //   wrapper sempre visivel. Comeca FECHADO por default; estado salvo no
    //   localStorage. Quando fechado, iframe (tela preta com dados) ocupa
    //   praticamente todo o drill. Quando aberto, wrapper aparece com scroll
    //   interno max 55vh.
    // v1.7.190 (2026-08-15) — **UI FIX: drill scroll interno.**
    //   Bug reportado: quando "A Central destacou" fica grande (v189 explica
    //   devolucao com muito texto), empurra iframe pra fora da viewport — o
    //   card branco ocupa a tela toda, iframe some. Usuario nao consegue rolar
    //   a "tela preta" (KPIs, comparativo, sangrias) porque nao ha' scroll.
    //   Fix: wrapper .fcc-drill-topo agrupa formula+histop+interp com
    //   max-height 45vh + overflow-y auto (scrollbar visivel). Iframe fica
    //   sempre visivel na metade de baixo do drill. Scrollbar estilizado
    //   pra combinar com o tema.
    // v1.7.189 (2026-08-15) — **EXPLICA a matematica: DEVOLUCAO em dinheiro.**
    //   Cliente confuso com "dinheiro faltou R$ 127" + "total sobrou R$ 0,20" — nao
    //   consegue entender (matematicamente parece impossivel). Explicacao: no Fire
    //   ERP o "DINHEIRO APURADO" ja e' liquido de devolucoes/cancelamentos (ver
    //   [[infra-fire-dinheiro-apurado-formula]]). Se cliente devolveu R$ X em
    //   dinheiro, o sistema apura R$ X a menos — mesmo o operador nao tendo feito
    //   nada errado. E o TOTAL tambem fica R$ X a menos (por isso fecha).
    //   Fix:
    //     - Novo PADRAO F devolucao_explica: detecta quando dinheiro faltou X e
    //       total_devolucoes+total_cancelado ~ X. Prioridade MAX (roda antes de
    //       E e D). Mensagem: "isso NAO e' falta real, e' funcionamento normal
    //       do PDV quando devolucao e' paga em dinheiro". Passos: verificar a
    //       secao Devolucoes/Cancelamentos, confirmar com operador se foi
    //       entregue em dinheiro.
    //     - renderInterpretacao da Central atualizado: quando dif dinheiro > 50
    //       E total do caixa fechou, ADICIONA um bloco de "Contexto importante"
    //       explicando que provavelmente tem explicacao tecnica, nao falta real.
    //       Lista causas na ordem de probabilidade (devolucao primeiro).
    //     - Assinatura de analiseConferente() aceita 2 novos params (totalDevs,
    //       totalCanc) com default 0.0 (retrocompativel).
    // v1.7.188 (2026-08-15) — **FIX bugs do v187 + passos DETALHADOS pro conferente.**
    //   Bug 1: link Excel do renderAcoes nao tinha target=_blank — clique dentro
    //   do iframe fazia o handler fccIframeCarregou detectar navegacao pra fora
    //   e exibir tela "Sem dados pra mostrar". Fix: target=_blank + rel=noopener
    //   nos dois links (PDF ja tinha).
    //   Bug 2: analiseConferente detectava troca_entre_formas errado — comparava
    //   com $difTotal (que vem de outro campo do Fire) em vez da SOMA das
    //   diferencas das outras formas. Caso Mil Coisa LAURA CX #13528: todas
    //   outras formas em zero, sistema dizia "troca" incorretamente. Fix: usar
    //   soma real das outras formas + testar sinal oposto.
    //   Bug 3: mensagens "NAO desconte do operador" removidas — BI nao opina
    //   sobre decisao trabalhista do gestor. So' apresenta DADOS + CAUSAS
    //   POSSIVEIS + PASSOS INVESTIGATIVOS. Regra global gravada no memory.
    //   Feature: passos MUITO mais DETALHADOS pra cada tipo de forma:
    //     - DINHEIRO: contagem fisica + sangrias/reforcos + formula + cupons
    //     - PIX: extrato bancario + cruzamento com cupons
    //     - CARTAO/TEF/POS: resumo da maquininha + cancelamento tardio + "meu cartao"
    //     - Outra: passos gerais
    //   Cada padrao (sangria compensa, forma zerada, ordem grandeza, divergencia
    //   grande) reescrito com instrucoes concretas ("abra o app do banco",
    //   "conte a gaveta", "localize o envelope") em vez de "peca extrato".
    // v1.7.187 (2026-08-15) — **EXPORTAR Excel + PDF do Fechamento de Caixa.**
    //   Cliente pediu: botao imprimir/PDF + novo botao Excel com cores e
    //   somatorios, ambos visuais e profissionais. Substituido botao unico
    //   `window.print()` por 2 botoes:
    //     [Imprimir / PDF]  → /caixa/fechamento/exportar-pdf (abre pagina
    //                          pronta pra Ctrl+P, auto-dispara print no load)
    //     [Exportar Excel]  → /caixa/fechamento/exportar-xlsx (Content-Type
    //                          Excel + HTML tabular; Excel abre nativo)
    //   Ambos usam MESMO HTML renderExportHtml (cabecalho da loja, KPIs
    //   financeiros, comparativo Sistema × Operador com TOTAL GERAL, sangrias,
    //   suprimentos, Ajuda ao Conferente completa). Cores semanticas
    //   (verde/amarelo/vermelho), badges de status, rodape Fire.
    //   Rotas novas em public/index.php.
    // v1.7.186 (2026-08-15) — **RESPONSABILIDADE: nunca acusar "dinheiro sumiu".**
    //   Caso real Mil Coisa/Tamara caixa #13528 LAURA CX: Ajuda ao Conferente
    //   disse "🚨 DINHEIRO faltou R$ 127,51. Dinheiro sumiu do caixa. Verifique
    //   possível retirada indevida." — MAS o TOTAL do caixa fechou (só R$ 0,20
    //   de diferença). Dinheiro faltou R$ 127,51 e OUTRAS FORMAS sobraram
    //   R$ 127,71: reclassificação (venda em dinheiro digitada como cartão/PIX).
    //   Se gestor lê "sumiu" e "retirada indevida", pode DESCONTAR do salário
    //   da LAURA sem base. GRAVE.
    //   Fix (analiseConferente + renderInterpretacao):
    //     - Padrao E TROCA_ENTRE_FORMAS detectado ANTES dos outros (prioridade
    //       max): se |dif_din| > R$ 50 mas |dif_total| < 15% de |dif_din| →
    //       marca reclassificacao, gravidade ATENCAO (nunca critico), lista
    //       quais formas compensaram, texto "NAO desconte, dinheiro nao sumiu".
    //     - Suprime padrao D (divergencia grande) na forma DINHEIRO quando E
    //       foi detectado — evita duplo aviso conflitante.
    //     - Reescrita frase "Dinheiro sumiu" da Central pra ser investigativa
    //       ("Pode ser: sangria não lançada... INVESTIGUE antes de agir").
    //     - Regra gravada em memoria global:
    //       feedback_analise_pessoas_reais_responsabilidade.md
    //   Aplicar mesma responsabilidade em QUALQUER outro modulo que aponte
    //   pessoa (alertas, ranking vendedor, excecoes caixa, cobranca).
    // v1.7.185 (2026-08-15) — **FireHash COMPLETO: aceita senha alfanumerica.**
    //   Bug: MOHAMMAD com senha 'amani656' (letras+numeros) nao entrava — cliente
    //   teve que trocar pra so numeros. Causa: fireHash so cobria XOR MOCAIS puro.
    //   Descoberto (via TESTEBI/AEc123 com hash 0C 0A 43 70 7B 60):
    //     (1) Fire converte a senha para UPPERCASE antes de hashear
    //     (2) Aplica XOR MOCAIS byte a byte (chave circular)
    //     (3) Se o byte resultante for 0x00 (null) ou 0x20 (espaco), substitui
    //         pela propria chave — evita chars que TRIM/string-null quebrariam
    //   Confirmado com 4 amostras (FIRE/121920, LORENA/824344, MOHAMMAD/122027,
    //   TESTEBI/AEc123). Bate 100% nas 4. Agora aceita QUALQUER combinacao de
    //   letras+numeros em qualquer case. Simplifica o array $tentativas (uma
    //   entrada cobre tudo).
    // v1.7.184 (2026-08-14) — **CLAREZA PRA CONFERENTE (Central de Caixas).**
    //   Baseado em padrao TOTVS/Bling/F360, apos pesquisa com o conferente.
    //   (1) Card "Como usar essa tela" (colapsavel) — 3 passos + glossario com
    //       6 termos (Apurado, Declarado, Divergencia, Sangria, Reforco, Caixa
    //       aberto). Auto-abre na 1a visita, lembra escolha (localStorage).
    //   (2) Formula transparente do Apurado no drill: "Vendas dinheiro +
    //       Suprimentos - Sangrias = Esperado" vs Declarado + Divergencia
    //       colorida (verde/amarelo/vermelho). Ensina o conferente COMO o
    //       sistema chegou no numero.
    //   (3) Motivos pre-definidos ao marcar conferido: modal com 8 opcoes
    //       (bateu certinho, troco nao devolvido, sangria nao lancada, venda
    //       cancelada maquineta, pgto pendente ontem, erro contagem, troco
    //       a mais/menos, outro). Motivo salvo em conferidos.json e mostrado
    //       na aba de ja-conferidos. Botao "Investigar depois" pra pular sem
    //       marcar. Elimina "textao livre" que ninguem le depois.
    //   (4) Historico do operador nos ultimos 30 dias no drill: qtd caixas,
    //       qtd divergencias, taxa%, media R$. Status: normal/atencao/critico
    //       (heuristica: pct_diverg vs valor medio). Novo endpoint GET
    //       /caixa/central/historico-operador?loja=X&operador=Y[&dias=30]
    //       com cache 5min no repository.
    //   (5) Drill maior: 92% -> 98% de largura (a pedido do conferente).
    //   Retrocompatibilidade: motivo_cod/motivo_txt sao opcionais — clientes
    //   nao mostram nada de diferente ate serem atualizados.
    // v1.7.183 (2026-08-14) — **DEFAULT SEGURO: login_sem_senha ausente = FALSE.**
    //   Bug: em v182 o default era TRUE se a flag nao existisse no config —
    //   entao TODO cliente que atualizou via OTA (sem a flag) ficou aceitando
    //   qualquer senha. Confirmado pelo Derlivon apos rollout do v182.
    //   Fix: default agora e' FALSE. Bypass so' ativa se 'login_sem_senha' =>
    //   true estiver EXPLICITO no config. Isso e' seguro pra todos porque
    //   fireHash() decifra o algoritmo do Fire pra qualquer cliente.
    // v1.7.182 (2026-08-14) — **FIX CRITICO: dbConfig() jogava fora login_sem_senha.**
    //   Bug: mesmo com 'login_sem_senha' => false gravado no config, o metodo
    //   dbConfig() FILTRAVA o array e retornava so 5 campos fixos, jogando a
    //   flag fora. No login(), array_key_exists('login_sem_senha', $cfg) era
    //   SEMPRE false, entao caia no default = TRUE = bypass.
    //   Resultado: v180+v181 nao tomavam efeito, cliente entrava com qualquer
    //   senha. Fix: propaga a flag se estiver no arquivo original.
    // v1.7.181 (2026-08-14) — **FIX: remove fallback DEV-only inseguro.**
    //   Bug: v1.7.159 introduziu um fallback que aceitava login sem senha se o
    //   Apache reportava SERVER_NAME=localhost — mas TODOS os Apaches de cliente
    //   tem essa config default no httpd.conf, entao IP publico caia no bypass.
    //   Cliente Grupo Ata confirmou: FIRE entrou com senha 'ab' apos v180.
    //   Agora eh binario: senha real ok OU config login_sem_senha=true. Nada mais.
    // v1.7.180 (2026-08-14) — **HASH PROPRIETARIO FIRE DECIFRADO — fim do bypass.**
    //   Descoberto por cracker: Fire ERP grava USUARIOS.PASS como XOR simples com
    //   chave fixa "MOCAIS" (6 chars, se repete p/ senhas maiores). Confirmado com
    //   2 pares (LORENA/824344 -> u}wr}g; FIRE/121920 -> |}rx{c).
    //   Adicionado metodo FbAuth::fireHash(senha) e incluido no ARRAY de tentativas
    //   ANTES do MD5. Agora o BI valida a senha REAL do PDV/ERP Fire.
    //   Mantidas as tentativas MD5/plain/upper como fallback pra bancos antigos.
    //   Cliente pode setar 'login_sem_senha' => false com seguranca — os operadores
    //   entram com a senha do proprio Fire, sem duplicar cadastro.
    //   IMPACTO: v179 (nao publicada) foi este trabalho em WIP. v180 = commit.
    // v1.7.178 (2026-08-14) — **FLAG POR CLIENTE: login_sem_senha no config.**
    //   Cada cliente decide no config/database.php se aceita login sem senha:
    //     'login_sem_senha' => true  → bypass (comportamento antigo, compat)
    //     'login_sem_senha' => false → exige senha real (seguro)
    //     (nao definido)             → default = true (compat com clientes atuais)
    //   Grupo Ata mantem true (operadores sem senha continuam entrando).
    //   Clientes que querem seguranca setam false explicitamente.
    // v1.7.177 (2026-08-14) — **REVERT v175: bypass total quebrou hierarquia.**
    //   v175 aceitava qualquer usuario existente sem validar senha — LORENA
    //   entrava com senha 'hhh', 'x', qualquer coisa. Cliente reportou como bug
    //   serio. Volta ao comportamento SEGURO v147: EXIGE senha real.
    //   Operador que nao consegue logar precisa cadastrar senha no ERP Fire
    //   (Cadastros > Usuarios > definir senha).
    // v1.7.176 (2026-08-14) — **FIX: Restrição de LOJA respeitada mesmo pra SUPERVISOR.**
    //   Cliente reportou: LORENA configurada pra ver so Achadinhos (nao Importados/
    //   Mil Coisa) na tela /configurar/usuarios, mas o BI mostrava tudo. Causa:
    //   LORENA e' SUPERVISOR='S' no ERP Fire; lojasPermitidas fazia short-circuit
    //   em isAdmin() e retornava [] (sem restricao).
    //   Fix: restricao explicita de loja SOBREPOE isAdmin. Se admin cadastrou
    //   lojas na tela, respeita. Se nao cadastrou, admin ainda ve todas.
    // v1.7.175 (2026-08-14) — **REVERT TOTAL da v147.**
    //   Volta ao comportamento pre-v147: aceita login se o usuario existe no ERP,
    //   independente de senha e de qual IP acessou. Cliente Grupo Ata (todos os
    //   operadores sem senha) e outros voltam a funcionar de qualquer lugar.
    //   Trade-off assumido pelo dono: bypass antigo restaurado, seguranca fica
    //   por conta da rede/VPN externa.
    // v1.7.174 (2026-08-14) — **EXPAND: Bypass tb pra VPN Radmin/Hamachi/CGNAT.**
    //   Cliente Grupo Ata confirmou: v173 funciona SO dentro do servidor
    //   (127.0.0.1). Operadores em outros PCs acessam via Radmin VPN (26.x.x.x)
    //   ou pela internet — nao caiam no ehLanPrivada.
    //   Expandido:
    //     • Radmin VPN (26.x.x.x)
    //     • Hamachi (25.x.x.x)
    //     • CGNAT / Tailscale (100.64-127.x)
    //     • Localhost completo (127.x)
    //   Alem disso, agora GRAVA no error_log tanto sucesso quanto rejeicao
    //   do bypass, pra auditoria (identificar IPs que precisam ser cadastrados).
    // v1.7.173 (2026-08-14) — **HOTFIX: Bypass de rede LAN privada (fecha v147).**
    //   Cliente Grupo Ata e outros tinham TODOS os usuarios sem senha no ERP.
    //   O bypass antigo aceitava; a v147 quebrou tudo; v171 tentou fix parcial
    //   (PASS NULL/vazio) mas o teste em campo mostrou que operadores nao
    //   necessariamente tem PASS vazio — pode ter algum caractere invisivel.
    //   Fix definitivo mais SIMPLES:
    //     • Login vindo de LAN privada (192.168.x, 10.x, 172.16-31.x, 127.x):
    //       aceita usuario que existe SEM validar senha (como pre-v147)
    //     • Login vindo de IP publico (Internet): mantem v147 (exige senha)
    //   Racional: rede LAN privada da loja = ambiente de confianca fisica.
    //   Cada login LAN sem senha grava em error_log lembrando de cadastrar
    //   senha real no ERP pra fechar a brecha.
    //   Impacto: restaura TODOS os clientes travados apos v147. Nao introduz
    //   vulnerabilidade externa (Internet ainda exige senha).
    // v1.7.172 (2026-08-14) — **REVERT v171: bloco PASS NULL causou regressao.**
    //   O bloco novo do v171 pra aceitar login com PASS vazio quebrou o login
    //   normal em algum cenario (cliente reportou "nem eu consigo entrar" apos
    //   aplicar). Removido — volta ao comportamento estavel v170.1.
    //   Cliente com PASS NULL/vazio no ERP precisa CADASTRAR SENHA no Fire:
    //     Fire ERP → Cadastros → Usuarios → [operador] → Definir senha.
    //   Depois disso, operador digita usuario+senha e entra normal.
    // v1.7.171 (2026-08-14) — **HOTFIX CRITICO: cliente nao conseguia logar apos update.**
    //   Grupo Ata (MOHAMMAD e outros) travou no login "Usuario ou senha invalidos"
    //   apos receber a v1.7.147+ (que removeu o bypass de senha).
    //   Causa: usuarios com PASS NULL/vazio no USUARIOS do Fire. O bypass antigo
    //   aceitava; a v147 exige "PASS = ?" que nunca bate com NULL.
    //   Fix SEGURO (nao reintroduz o bug):
    //     • Se nenhuma tentativa com senha achou, tenta buscar usuario com
    //       PASS explicitamente NULL/vazio no banco.
    //     • Se achou = aceita login com senha em branco (o usuario NUNCA teve
    //       senha, e o admin do ERP nunca cadastrou uma).
    //     • Se nao achou = mantem comportamento v147 (rejeita).
    //   SYSDBA e outros que tem senha cadastrada continuam sendo validados
    //   normalmente — o buraco de seguranca da v146 nao volta.
    //   Log registra cada login "sem senha" pra auditoria.
    // v1.7.170.1 (2026-08-14) — **FIX: JSON quebrado ("Unexpected token &#39;&lt;")**
    //   O v170 do "Ajuda ao Conferente" tinha 2 bugs:
    //     • Operator precedence: `(float)$x['dif_dinheiro'] ?? 0` avaliava
    //       cast antes do ??, dando warning "undefined key" pra caixas sem
    //       essa chave (que só existe apos classificarCentral).
    //     • Warning HTML vazava no JSON, quebrando o parser do frontend.
    //   Fix: calcula difDinheiro direto do comparativo (procurando forma
    //   DINHEIRO), try/catch em volta, display_errors=0 no endpoint /dados.
    // v1.7.170 (2026-08-14) — **UX: Card "Ajuda ao Conferente" (Detalhe por Operador).**
    //   Cliente reclamou: conferentes com dificuldade — nao sabem se sobrou/faltou
    //   dinheiro, se PIX foi declarado errado, se sangria foi contada 2x. A tabela
    //   Sistema x Operador que ja existia (renderComparativo) mostra os NUMEROS
    //   mas nao INTERPRETA — deixa o conferente decidir sozinho.
    //   Novo card NO TOPO da tela do caixa (antes de tudo) — SO aparece quando ha
    //   algo pra investigar. Traduz os numeros em AÇÕES concretas.
    //   5 padroes detectados automaticamente:
    //     1. Ordem de grandeza errada (10x / 100x / 1000x menor)
    //        → "POS DEBITO — parece 10x menor · esqueceu de digitar um 0"
    //     2. Forma zerada (sistema >20, operador = 0)
    //        → "PIX — nao declarado · esqueceu de conferir o extrato"
    //     3. Sangria compensa dinheiro (dif_dinheiro ≈ ± total_sangrias)
    //        → "Diferenca bate com a sangria · verificar envelope"
    //     4. Divergencia grande sem padrao (delta > R$ 100 ou > 20%)
    //        → "Reconferir essa forma antes de fechar"
    //     5. Tudo OK (nenhum achado) — card verde de conformidade
    //   Semaforo: 🚨 critico (vermelho) · ⚠️ atencao (amarelo) · ✅ ok (verde).
    //   Sai colorido no PDF tb (@media print completo).
    //   Custo zero no banco — 100% do dado ja vinha em fc.comparativo/sangrias/etc.
    // v1.7.169 (2026-08-14) — **UX: Destaque de linha "longa" mais visível.**
    //   Cliente reparou: legenda dizia "linhas em amarelo = mais de 12h", mas
    //   TODAS as horas ficam amarelas (padrao da coluna Hora). Confusao entre
    //   a hora amarela e o destaque de linha inteira.
    //   Fix:
    //     • Fundo da linha ".longo" trocou de amarelo 5% (invisivel) pra CORAL 18%
    //     • Barra lateral esquerda de 4px coral (aumenta o destaque)
    //     • Coluna "Em curso" fica em coral tb (batendo com o fundo)
    //     • Legenda: "Linhas com FUNDO coral = ..." (nao mais "amarelo")
    //   No PDF: fundo #ffedd5 (coral pastel), barra #c2410c (coral escuro).
    // v1.7.168 (2026-08-12) — **FIX: Modal de detalhe do caixa quebrava.**
    //   Ao clicar numa linha da tela /caixa/periodos pra abrir detalhes ao vivo
    //   (feature v161), aparecia "Erro de rede: fmtBR is not defined". Bug do
    //   escopo: `fmtBR` estava definido no fechamento-caixa-v2.js (outra tela),
    //   nao no <script> inline da periodos.php.
    //   Fix: define `fmtBR` local no periodos.php (formatador BR de dinheiro).
    // v1.7.167 (2026-08-12) — **UX: Modal "Caixas abertos agora" separa esquecidos.**
    //   O modal que abre ao clicar em "Abertos agora" na grade KPIs da Central
    //   ainda misturava caixas de anos atras (LARISSA 14-02-2024, LUCAS 08-03-2025)
    //   com os do dia. Cliente pediu:
    //     "falei que nao quero q apareça periodo aberto por mais de 5 meses,
    //      deixa uma aba especial pa isso"
    //   Fix (mesma abordagem das v162/v163):
    //     • Ativos (<= 5 meses) na tabela principal + coluna "Em curso" nova
    //     • Esquecidos (> 5 meses) num accordion no fim ("🗄 N caixas
    //       esquecidos...") — clique expande
    //     • Reusa fccToggleEsq (v163) — zero JS novo alem do split
    // v1.7.166 (2026-08-12) — **UX: Seletor de LOJA no Detalhe por Operador.**
    //   Tela /caixa/fechamento tinha so filtro operador+data, sem seletor de loja.
    //   Em multi-loja o dropdown de operadores traazia so os da loja ativa da
    //   sessao, deixando os operadores de outras lojas invisiveis.
    //   Novo comportamento:
    //     • Ao abrir a tela, JS busca /caixa/central/lojas e popula dropdown Loja
    //     • Se so 1 loja, esconde o seletor (mantem comportamento antigo)
    //     • Se >1 loja, mostra seletor + operador aguarda escolha de loja
    //     • Ao mudar Loja OU Data, recarrega operadores daquela loja
    //     • Ao Buscar, passa `loja` na URL — backend usa Lojas::setFoco +
    //       Database::conectarLoja pra conectar no banco correto
    //   Backend: operadores() e dados() aceitam param `loja` opcional
    //     (retrocompat: sem param = loja ativa da sessao, comportamento antigo)
    //   Zero mudanca pra cliente single-loja (seletor escondido).
    // v1.7.165 (2026-08-11) — **UX: Remove card "Periodos Abertos" da Central.**
    //   Cliente pediu 2x: "informacoes de periodo aberto SOMENTE no card
    //   Periodos abertos, remove dos outros cards" — o item de menu
    //   "Periodos Abertos" (v155) ja da uma tela dedicada; o card duplicado
    //   dentro da Central de Conferencia so poluia. Removidos:
    //     • <div id="fcc-card-periodos"> do HTML
    //     • Chamada renderCardPeriodos() do carregar()
    //   Funcao renderCardPeriodos + CSS ficam morto no arquivo (sem uso, sem
    //   custo — se um dia decidir voltar, so descomentar). Endpoint continua
    //   enviando abertos_por_loja no JSON (usado pela tela dedicada tb).
    // v1.7.164 (2026-08-11) — **UX: Remove coluna "Periodos hoje" da grade KPIs.**
    //   Cliente reparou que a informacao de periodos aparecia em 2 lugares na
    //   Central de Conferencia:
    //     1. Coluna "Periodos hoje" na grade KPIs por loja (v148, topo)
    //     2. Card grande "Periodos Abertos Hoje" (v151/163, logo abaixo)
    //   Concentra a info em UM lugar so — o card grande (mais rico, tem
    //   accordion de esquecidos, agrupamento por loja, tabela completa).
    //   Coluna removida da grade; header e td tirados. Comportamento nao muda
    //   em nenhuma outra tela — o endpoint continua enviando periodos_hoje no
    //   payload (usado pelo card grande).
    // v1.7.163 (2026-08-11) — **UX: Agrupamento de esquecidos TB na Central.**
    //   A v162 aplicou o agrupamento so na tela dedicada /caixa/periodos, mas o
    //   card "Periodos Abertos Hoje" tb aparece EMBUTIDO na Central de Conferencia
    //   (/caixa/central, v151). Cliente reparou que la ainda vinha misturado.
    //   Aplicado o mesmo tratamento:
    //     • Caixas >5m viram accordion "🗄 N caixas esquecidos" no fim do bloco
    //     • Badge da loja: "3 ativos · 2 esquecidos"
    //     • Cabecalho: "9 caixas rodando · 5 lojas · ⚠️ 5 esquecidos há +5 meses"
    //     • Sem filtros configuraveis (é card resumido dentro da Central; os
    //       filtros configuraveis ficam so na tela dedicada /caixa/periodos)
    // v1.7.162 (2026-08-11) — **UX: Filtros de idade + agrupamento de esquecidos.**
    //   Cliente reclamou que tinham caixas de 2 ANOS misturados com caixas de
    //   hoje (LARISSA CX 21818h aberta desde 14-02-2024, etc). Poluia a lista
    //   dos que precisam de atencao imediata.
    //
    //   Nova barra de filtros no topo do card:
    //     • Dropdown "Mostrar": 7 dias / 30 dias / 5 meses (default) / Todos
    //     • Checkbox "Agrupar esquecidos (>5 meses) em bloco separado" (padrao ON)
    //     • Contador dinamico: "N ocultados pelo filtro · N esquecidos agrupados"
    //
    //   Comportamento:
    //     • Caixas ATIVOS (=<5m) aparecem na tabela normal do bloco da loja
    //     • Caixas ESQUECIDOS (>5m) ficam num accordion no fim do bloco da loja
    //       ("🗄 N caixas esquecidos" — clique expande)
    //     • Badge da loja mostra ambos: "3 ativos · 2 esquecidos"
    //     • Cabecalho global ganha aviso amarelo "⚠️ N esquecidos ha +5 meses"
    //     • Filtros nao disparam nova query HTTP — re-renderiza com dados em mem
    //
    //   Impacto: gestor abre a tela e ja ve so o que interessa. Se quiser
    //   fazer limpeza no ERP dos esquecidos, expande o accordion.
    // v1.7.161 (2026-08-11) — **UX: Detalhe AO VIVO do caixa aberto (clique na linha).**
    //   Nas Períodos Abertos, cada LINHA da tabela agora é clicável e abre
    //   um modal com dados AO VIVO do caixa (sem sair da tela):
    //     • Cabeçalho: loja, terminal, quem abriu, suprimento, máquina ERP, conta
    //     • Vendas até agora: cupons, faturamento apurado, ticket médio, maior,
    //       menor, cupons/h, R$/h
    //     • Formas de pagamento apuradas: dinheiro, PIX, débito, crédito etc
    //       (cards coloridos com valor + % do total)
    //     • Movimentações do dinheiro: sangrias e suprimentos com hora +
    //       histórico + usuário
    //   Sinal visual: coluna extra "🔎 detalhes" ao final + hover destaca linha.
    //   ESC ou clique fora fecha o modal. Modal não sai na impressão.
    //
    //   Novo endpoint: GET /caixa/periodos/detalhe?loja=X&caixa=N
    //   Reaproveita queries que ja existiam: nrNotasDoTurno + resumoVendas +
    //   formasPorVendas + sangrias + suprimentosReforcos. Todas aceitam caixa
    //   em curso (sem exigir STATUS='F').
    //   resumoVendas ganhou MENOR_VENDA / PRIMEIRA_VENDA / ULTIMA_VENDA
    //   (mesma paridade do resumoVendasBatch v150).
    // v1.7.160 (2026-08-11) — **FIX CRÍTICO: Falso "loja OFFLINE" no login.**
    //   Cliente reclamou que BI local mostrava "Loja Mega Importados 1,99 esta
    //   OFFLINE" mesmo com o banco respondendo (testado via PDO direto, OK).
    //   Causa: FirebirdService::pingHost usava fsockopen() com timeout 900ms.
    //   No Windows, fsockopen NAO respeita direito timeout curto (bug antigo);
    //   em VPN Radmin com latencia 1-2s, sempre falhava, cravando cache "offline"
    //   por 60s. Resultado: usuario nao conseguia logar mesmo com banco OK.
    //   Fix:
    //     • Trocado fsockopen → stream_socket_client (respeita timeout no Windows)
    //     • Timeout default 900ms → 2500ms (folga pra VPN Radmin)
    //     • Retro dobra pra 5s se 1a tentativa falhar (tolerancia extra)
    //   Testado: probe agora responde em 88ms na VPN, marca ONLINE corretamente.
    // v1.7.159 (2026-08-11) — **DEV-ONLY: Login localhost aceita sem senha.**
    //   Reintroduz fallback pré-v147 SO quando acesso vem de 127.0.0.1/localhost.
    //   Em qualquer IP publico (cliente), continua v147 (exige senha real).
    //   Motivo: dev nao pode ficar bloqueado se esquecer senha do usuario ERP.
    //   Registra em error_log quando o fallback DEV eh acionado (auditoria).
    // v1.7.158 (2026-08-11) — **UX+AUDIT: Timeline visual + Alerta WhatsApp.**
    //   1. Alerta de CAIXA DUPLICADO agora usa TIMELINE VISUAL (Opção A):
    //      Barras horizontais mostrando o intervalo de cada caixa aberto do
    //      mesmo operador. A zona onde 2+ barras se sobrepõem (duplicidade)
    //      fica hachurada em vermelho tracejado. Linha verde "AGORA" atualiza
    //      dinamicamente. Régua horária com marcas +/-N dias pra caixas de
    //      dias anteriores. Muito mais intuitivo que a lista de texto.
    //
    //   2. ALERTA WHATSAPP automático quando detecta duplicata:
    //      Endpoint POST /caixa/periodos/alertar-duplicados. Chamado pelo JS
    //      logo após render. Backend:
    //        • Le config/alertas.php (secao 'whatsapp' com url/instance/apikey/numero)
    //        • Detecta operadores duplicados via caixasAbertos() por loja
    //        • Envia mensagem formatada: loja + operador + lista de códigos + horas
    //        • Dedup 1x por dia via state file storage/alertas/rt-state/
    //          (mesma pasta dos outros alertas RealTime)
    //      Chip no topo do alerta mostra status: "📲 3 alertas enviados no
    //      WhatsApp" / "📲 Já avisado hoje" / "⚠️ WhatsApp não configurado".
    //      Reaproveita WhatsAppClient::enviar() que ja existe (Evolution API v2).
    // v1.7.157 (2026-08-11) — **AUDIT: Detecta operador com CAIXA DUPLICADO.**
    //   Cliente perguntou "sistema consegue ver se o mesmo usuario abriu 2x?".
    //   Resposta: não detectava. Agora detecta na tela /caixa/periodos:
    //     • Agrupa caixas abertos por COD_USUARIO_ABERTURA dentro da MESMA loja
    //     • Se count >= 2 → marca caixa como duplicado + gera alerta
    //   Sinais visuais (só quando há duplicata):
    //     • Bloco de alerta vermelho no TOPO do card: "N operadores com caixa
    //       DUPLICADO — investigar" + lista com {operador, loja, códigos dos
    //       caixas envolvidos com hora e duração}
    //     • Cada LINHA duplicada na tabela fica com fundo vermelho pastel
    //     • Badge "🔁 duplicado" ao lado do nome do operador na tabela
    //   Motivo: duplicata é red flag classico — troca de turno mal feita OU
    //   fraude (2 caixas em paralelo) OU compartilhamento de login (LGPD).
    //   Ignora usuario vazio (nem sempre o Fire grava COD_USUARIO_ABERTURA).
    //   Custo zero: cálculo em JS, dados já vinham do endpoint /caixa/periodos/dados.
    //   Alerta sai colorido também no PDF (@media print completo).
    //   Escopo: SÓ mesma loja (v1). Cross-loja fica pra depois.
    // v1.7.156 (2026-08-11) — **FIX: Impressão pág 1 em branco (Fechamento + Períodos).**
    //   v154 tentou resolver mas ainda sobravam causas invisiveis. Print da tela
    //   Periodos e Fechamento tinha pag 1 SO com botao flutuante laranja (icone
    //   de impressora) e headset (chat suporte), conteudo real na pag 2.
    //   Novas regras no @media print de fechamento-caixa-v2.css e periodos.php:
    //     • Esconde qualquer widget flutuante ([class*=floating], [class*=fab-],
    //       [class*=widget-flutu], .fixed-top, .fixed-bottom) — pega botao de
    //       impressao/suporte adicionado por scripts, sem precisar saber o nome.
    //     • Esconde faixa decorativa do tema mastersoft (body::before position
    //       fixed) — nao tinha display:none no print, mesmo com content:'' ela
    //       ocupava altura da viewport.
    //     • html/body/wrapper com min-height:0 !important + height:auto
    //       (antes body.pbi tinha altura minima 100vh que gerava pagina extra).
    //   Aplicado nas 2 telas de uma vez.
    // v1.7.155 (2026-08-11) — **UX: Tela dedicada "Períodos Abertos".**
    //   Novo item de menu Fechamento de Caixa → Períodos Abertos.
    //   Cliente pediu separar do Central de Conferência pra ter visão focada
    //   só nos caixas rodando AGORA — sem se distrair com fechados/conferidos.
    //   Tela mostra: mesmo card agrupado por loja + auto-refresh 60s + botão
    //   Atualizar Agora + botão Imprimir (imprime só o card, sem sidebar).
    //   Rotas novas:
    //     GET /caixa/periodos       → pagina (View::render caixa/periodos)
    //     GET /caixa/periodos/dados → JSON leve (so caixasAbertos por loja,
    //                                 cache 30s no repo, sem fechamentos)
    //   Ícone menu: bi-clock-history entre Central e Detalhe por Operador.
    //   Card sai colorido tambem no PDF (@media print completo).
    // v1.7.154 (2026-08-11) — **FIX: Impressão do Fechamento — 1ª página em branco.**
    //   Cliente reclamou: PDF do relatorio saia com pag 1 SO com titulo
    //   "Fechamento de Caixa", conteudo real dos caixas comecava na pag 2.
    //   Causas identificadas no @media print:
    //     1. .fc2-loading (spinner "Carregando...") nao tinha display:none —
    //        ficava ocupando pagina inteira se a impressao pegasse antes do
    //        JS terminar de renderizar.
    //     2. .fc2-header (H2 "Fechamento de Caixa") tambem sem display:none —
    //        so os botoes .fc2-header-actions saiam escondidos.
    //     3. .pbi-sidebar/.pbi-topbar (layout global do BI) reservavam espaco
    //        lateral/topo, empurrando o primeiro .fc2-caixa pra pag 2.
    //   Fix: adiciona todos ao display:none do @media print + zera padding/
    //   margin do layout global. Primeiro caixa passa a comecar colado no topo.
    // v1.7.153 (2026-08-11) — **UX: "Períodos Abertos" agora mostra DATA.**
    //   1. Titulo do card ganhou a data de hoje (DD-MM-YYYY) ao lado.
    //   2. Coluna "Hora" virou "Abertura" — quando o caixa foi aberto em
    //      OUTRO dia (esquecido), mostra "DD-MM as HH:MM" com badge laranja
    //      antes da hora ("11-08 as 07:37"); se abriu HOJE, mostra so a hora.
    //   3. "1º desde" no cabecalho: quando caixa mais antigo é de outro dia,
    //      passa a mostrar "DD-MM as HH:MM" (antes so a hora dava impressao
    //      errada de que era de hoje).
    //   4. BUG resolvido de contexto: calculo de duracao antes assumia HOJE
    //      pra hora de abertura → caixa aberto ontem as 20h ficava com "-1min".
    //      Agora usa data+hora reais. Caixas de outro dia entram automatico
    //      no destaque amarelo (mesma classe .longo do >12h).
    //   Formato: DD-MM-YYYY (regra global, nunca ISO em tela).
    // v1.7.152 (2026-08-11) — **UX: "Períodos Abertos Hoje" agora agrupado POR LOJA.**
    //   Antes as caixas de todas as lojas caiam numa unica tabela misturada
    //   (Mega Importados, Achadinhos, Tamara... ordenados so por hora). Cliente
    //   reclamou: "ta confuso, quero por loja separados".
    //   Agora cada loja tem seu proprio bloco (titulo com nome + badge com
    //   quantidade + tabela propria com so as caixas dela). Blocos ordenados
    //   por qtd de caixas abertos DESC (loja com mais movimento aparece
    //   primeiro), desempate pela hora do primeiro caixa aberto.
    //   Cabecalho global preservado: "16 caixas rodando agora - 5 lojas - 1º desde 07:37".
    //   Sem mudanca no backend: reusa o mesmo abertos_por_loja da v151.
    // v1.7.151 (2026-08-11) — **UX: Card "Períodos Abertos Hoje" na Central.**
    //   Novo card grande logo abaixo da grade KPIs por loja. Consolida em UMA
    //   tabela TODOS os caixas que estão rodando AGORA em TODAS as lojas do
    //   grupo. Antes voce so via a CONTAGEM na coluna "Períodos Hoje" — agora
    //   ve QUAIS caixas, QUEM abriu, HÁ QUANTO tempo estão em curso.
    //
    //   Cabeçalho: "8 caixas rodando agora · 4 lojas · 1º desde 07:37"
    //   Tabela: hora abertura | loja | caixa # | operador | em curso
    //   Regra visual: caixa aberto > 12h fica com fundo amarelo (esqueceram de fechar).
    //
    //   Custo zero: piggyback em caixasAbertos() ja chamado no loop principal
    //   do centralApi. Novo campo 'abertos_por_loja' no JSON de retorno.
    // v1.7.150.2 (2026-08-11) — **FIX: dropdown operadores mostrava "(0 caixa)" sem nome.**
    //   Bug antigo (nao meu, mas exposto pela v150.1 quando o fallback de 7 dias
    //   comecou a popular o dropdown): o repo devolve chaves em MAIUSCULO
    //   (OPERADOR, QTD_FECHAMENTOS) mas o JS le em minusculo (o.operador, o.qtd).
    //   Resultado: nome vazio + qtd 0 em toda linha.
    //   Fix: controller normaliza pra minusculo antes de responder o JSON.
    //   Antes exposicao real dependia do dropdown ter conteudo (raro antes do
    //   fallback ampliar automaticamente).
    // v1.7.150.1 (2026-08-11) — **UX fixes na tela de Fechamento por Operador.**
    //   1. Restaurado "Resumo do turno" original (bug: v149 tinha substituído por
    //      "Detalhes do Período"). Agora sao 3 cards SEPARADOS empilhados:
    //         · Resumo do turno       (original v146.3 - 8 campos)
    //         · Detalhes do Período   (v149 - 3 blocos + alertas ERP)
    //         · Radiografia do Período (v150 - timeline + ritmo + movs)
    //   2. Titulo "Fechamento de Caixa" e empty state ganharam cores tema-aware
    //      (var --pbi-text/--pbi-text-muted) — bug antigo hardcoded #f1f5f9 sumia
    //      em temas claros como mastersoft-pro (h2 branco em fundo branco).
    //   3. Fallback de operadores: se HOJE nao tem caixa fechado (comum no comeco
    //      do dia), amplia pra ultimos 7 dias automaticamente e mostra aviso
    //      "Nenhum caixa fechou no periodo — mostrando ultimos 7 dias".
    //      Antes: dropdown vazio sem explicacao, cliente achava que quebrou.
    // v1.7.150 (2026-08-10) — **UX: Card "Radiografia do Período" — análise cronológica.**
    //   Complementa (nao substitui) o "Detalhes do Período" da v149. Foca em COMO
    //   o turno rodou, não em O QUE aconteceu. Referências: Lightspeed Shifts
    //   Summary, Toast Z-Report, Loyverse Shift Report, Dynamics 365 Shift/Drawer.
    //
    //   5 blocos no card:
    //     1) TIMELINE HORIZONTAL — 4 marcos coloridos com gap entre eles:
    //        Abertura (verde) → 1ª venda (índigo) → Última venda (âmbar) → Fechamento (vermelho)
    //     2) KPIs DE RITMO (6 tiles):
    //        • Aproveitamento do turno (janela ativa / duração total) — cor por faixa
    //        • Janela ativa de vendas (Xh Ymin)
    //        • Intervalo médio entre vendas (venda a cada Xmin)
    //        • Cupons/hora e R$/hora
    //        • Amplitude de venda (menor→maior)
    //     3) MOVIMENTAÇÕES DO DINHEIRO — timeline vertical de TODAS as sangrias
    //        e suprimentos com hora, valor, histórico e usuário
    //     4) TOTAIS agregados (sangrias vs suprimentos extras)
    //     5) PADRÃO OBSERVADO — frases automáticas ("Abriu e ficou 30min sem
    //        vender", "3 sangrias no turno", "Só 55% do turno teve venda")
    //
    //   Banco:
    //     • resumoVendasBatch agora traz MIN/MAX DATA_HORA_VENDA e MENOR_VENDA
    //       (piggyback na mesma query — zero custo extra)
    //     • Service.montarAnalisePeriodo() calcula gaps/janela/aproveitamento/
    //       intervalo médio + monta timeline + ordena movimentações + gera padrões
    //
    //   Também sai colorido no PDF (Ctrl+P).
    // v1.7.149 (2026-08-10) — **UX: Card "Detalhes do Período" completo no Fechamento.**
    //   Substitui o "Resumo do turno" (8 campos enxutos) por card grande com 3 blocos:
    //     - Operação: terminal, operador abertura/fechamento (com badge ≠ se trocou),
    //       abertura, fechamento, duração do turno (Xh Ymin), máquina no ERP
    //     - Volume/Ritmo: cupons, ticket médio, maior venda, cupons/h, R$/h
    //     - Valores: suprimento, apurado, contado, troco final, líquido, conta corrente
    //   Alertas ERP renderizados no topo (só aparecem se acionados):
    //     • ⚠️ REABERTO (QUEM_REABRIU preenchido = ponto crítico de auditoria)
    //     • 📡 Fechou OFFLINE (OFFLINE=S)
    //     • ✅ Já conferido no Fire (CONFERIDO=S) — independe do BI
    //     • 🔄 Troca de operador (quem abriu ≠ quem fechou)
    //   Também sai colorido no PDF (Ctrl+P) com fundos pastel legíveis.
    //   Repo listarFechamentos() agora puxa VALOR_FECHAMENTO, CONFERIDO,
    //   DATA_CONFERIDO, QUEM_REABRIU, DATA_REABRIU, OFFLINE, ID_LOCAL,
    //   ID_SERVIDOR, ID_SERVIDOR_FECHA, COD_CONTA_CORRENTE (10 campos novos).
    //   Service ganhou calcularDuracaoMinutos + formatarDuracao + formatarData.
    // v1.7.148 (2026-08-10) — **UX: Central de Fechamento mostra "Períodos hoje".**
    //   Nova coluna na grade KPIs por loja: quantos turnos de caixa foram ABERTOS
    //   hoje (fechados + em curso). Antes so tinha "Abertos agora" (turnos ainda
    //   em curso) — nao dava pra saber quantos ja rodaram no dia inteiro.
    //   Exemplo linha: `📅 8  ·  5✅ · 3🟡 · desde 07:12`
    //   (8 turnos hoje = 5 fecharam + 3 em curso, primeiro caixa desde 07:12).
    //   Independe do filtro de datas — sempre HOJE. Cache 60s por loja.
    //   Novo metodo FechamentoCaixaRepository::periodosDoDia() +
    //   kpi_loja.periodos_hoje nos endpoints centralLojaApi e centralApi.
    // v1.7.147 (2026-08-10) — **SEGURANCA + FIX: Login e cadastro de usuarios.**
    //   Bug #1 (visibilidade): tela Configurar > Usuarios filtrava por COD_EMPRESA
    //   da loja ATIVA. Consequencia: usuario cadastrado em outra empresa nao aparecia,
    //   admin nao conseguia dar permissao. Caso real (Achadinhos): LORENNA existia no
    //   Fire mas na empresa 2; admin abriu config com empresa 5 ativa; LORENNA sumiu
    //   da lista; foi cadastrada permissao pra "LORENA" (1 N) achando que era ela;
    //   quando LORENNA loga, sistema busca permissao dela, nao acha, nega tudo.
    //   Fix: FbAuth::listarUsuariosERP() lista TODAS as empresas com DISTINCT no
    //   COD_USUARIO (GROUP BY + MAX pra desduplicar quando mesmo login aparece em N empresas).
    //
    //   Bug #2 (seguranca CRITICA): FbAuth::login() tinha 4 tentativas encadeadas.
    //   Tentativas 3 e 4 aceitavam login SEM VALIDAR SENHA — so verificavam se o
    //   COD_USUARIO existe. Cenario: alguem digita "SYSDBA" + senha errada,
    //   tentativas 1/2 falham, tentativa 3 acha o usuario existente, retorna row,
    //   login aceito. Se SYSDBA tinha SUPERVISOR='S', virava admin com Acesso Total.
    //   Comentario original explicava que era "fallback pra clientes com cod_empresa
    //   errado no wizard" — remedio pior que a doenca.
    //   Fix: removidas tentativas 3 e 4 do login. Exige senha SEMPRE.
    //   Impacto pra clientes: quem tinha cod_empresa errado no wizard vai ver
    //   "senha invalida" e precisa corrigir o wizard (comportamento correto).
    // v1.7.146 (2026-08-10) — **UX: Tela de Fechamento de Caixa refeita (padrão POS Report).**
    //   Motivo: pessoal do supervisor tinha dificuldade de entender a tela antiga
    //   (4 botoes de periodo + mes picker + calendario + 8 abas + jargao tecnico
    //   "Apurado Sistema" vs "Apurado Operadora"). Pesquisa em Lightspeed, KoronaPOS,
    //   NaviPartner, Linx Microvix, Bling, Omie, TOTVS PDV mostrou padrao convergente:
    //   dashboard supervisor = KPI tiles + secoes empilhadas + insight em linguagem natural.
    //
    //   Refatoracao:
    //   • Nova view fechamento.php + fechamento-caixa-v2.js + fechamento-caixa-v2.css
    //   • Layout dark mode consistente com o resto do BI, cores vivas (verde neon,
    //     vermelho gritante, amarelo intenso, glow suave em barras/valores)
    //   • Insight "🔍 A Central destacou" com 3 hipoteses + guia "1x=erro, 3+x=investigar"
    //     (padrao troca, faltou dinheiro, risco fiscal, recorrencia)
    //   • 9 KPI tiles: Sistema | Operador | Diferenca | Cupons | Ticket +
    //     Cancelamentos | Devolucoes | Suprimento | Risco fiscal
    //   • Botao "✓ Marcar como conferido" grande + Imprimir
    //   • Secao "Resumo do turno": terminal, abertura, fechamento, operador
    //     abertura, suprimento, valor liquido, maior venda, status
    //   • Secao "Como o cliente pagou" com cards + icones + pct por forma
    //   • Secao "Comparativo por forma": tabela completa, linha divergente
    //     destacada em vermelho vivo com gradient + border-left glow, V verde
    //     de aprovacao (chk-ok) nas linhas OK
    //   • Movimentos/Cancelamentos/Devolucoes: SEMPRE aparecem (mesmo com 0
    //     mostra "✅ Nenhum registrado neste turno") - supervisor confirma que checou
    //   • Analise fiscal: cards Com/Sem nota + botao vermelho "Ver as N pra
    //     investigar" (SEM nota) + botao cinza "Ver lista das N" (COM nota),
    //     ambos colapsados por padrao
    //
    //   COMPAT: `?modo=avancado` na URL renderiza o layout antigo (backup .bak-*).
    //   REVERSAO: C:\BI\installer\dist\REVERTER-FECHAMENTO-CAIXA.ps1 restaura
    //   fechamento.php + .js + .css originais em 5 segundos.
    //
    // v1.7.145 (2026-08-08) — **PERF + UX: Page cache Consolidado + Modal "Investigar caixa" limpo.**
    //   UX Central de Caixas ("Investigar caixa" drill lateral):
    //   - Antes: painel abria 75% cobrindo a lista, SEM overlay -> lista visivel atras
    //     "abafada", dificil de focar no que era importante.
    //   - Agora: adicionado backdrop escuro semi-transparente + blur 4px atras do
    //     painel; painel cresceu pra 92% (max 1400px); header com faixa amarela mais
    //     grossa e titulo maior; botao fechar virou "amarelo grande com ✕"; clicar
    //     no backdrop fecha; body trava scroll enquanto aberto. Esc ja fechava.
    //   Zero mudanca no iframe interno (que ja usava o layout normal do BI) --
    //   so o container do modal.
    //
    // v1.7.145 (2026-08-08) — **PERF: Page cache da tela Resumo Geral (/consolidado).**
    //   Complementa v144. Cliente reportou que Resumo Geral seguia lento apesar
    //   dos fixes anteriores. Diagnostico: ConsolidadoController tinha ZERO
    //   Cache::lembrar - itera N lojas via Radmin (7 conectarLoja + 4 foreach)
    //   E o proprio codigo assumia lentidao com @set_time_limit(180).
    //   Fix minimo, cirurgico:
    //     1. No inicio de index(): calcula chave de cache scoped por sessao +
    //        usuario logado + escopo (loja ativa/consolidado) + querystring.
    //     2. Se cache hit: retorna HTML pronto em ~5ms com header X-BI-Cache: HIT.
    //     3. Se cache miss: ob_start(), roda tudo normal, no fim ob_get_contents
    //        e Cache::set com TTL 60s (periodo inclui hoje) ou 1800s (fechado).
    //   Efeito combinado com o Cache-Control: private, stale-while-revalidate=300
    //   do View.php (v144):
    //     - browser cachea local por ate 5min sem pedir (SWR)
    //     - entre 5-10min: browser pede, PHP responde do cache em ms
    //     - >10min ou cache invalidado: rebuild (unica vez que "paga o preco")
    //   Zero mudanca no loop pesado -- reversivel removendo os 2 blocos // v1.7.145.
    // v1.7.144 (2026-08-08) — **PERF: ETag em endpoints AJAX + session_write_close cedo.**
    //   Camada 1 da estrategia de reduzir carga no Firebird do cliente (o real
    //   gargalo era Firebird acumulando GBs de cache, ja tratado por script
    //   FIX-FIREBIRD-MEMORIA-CLIENTE.ps1 + task 3h da madrugada).
    //   Novo App\Support\HttpCache::sendJson($data, $maxAge=30) que:
    //     1. Calcula MD5 do payload -> emite ETag: W/"abc123"
    //     2. Se browser mandou If-None-Match igual -> responde 304 (0 bytes body)
    //     3. Emite Cache-Control: private, max-age=N, must-revalidate
    //   Aplicado nos 3 endpoints AJAX que rodam em TODA pagina do BI:
    //     - /api/alertas/count               (badge alertas)   ETag 30s
    //     - /api/compras/sugestao/ruptura-count (badge ruptura) ETag 60s
    //     - /api/lojas/status                (banner offline)   ETag 30s multi / 300s single
    //   BONUS em cada um: session_write_close() logo no inicio -> libera o LOCK
    //   do session file (Windows PHP com session.save_handler=files serializa
    //   requests do mesmo usuario). Sem isso, os 3 fetch paralelos do main.php
    //   viravam FILA sequencial mesmo com 8 workers Apache disponiveis.
    //   Impacto esperado: 80% menos hits em cache-warm no Firebird + fim da
    //   serializacao dos badges. Zero risco (ETag e padrao HTTP RFC 7232).
    // v1.7.143 (2026-08-07) — **INSTALADOR + Inadimplencia menu limpo.**
    //   INSTALADOR (setup-universal-template.iss):
    //   • AppId DINAMICO por porta: cada instalacao (8000/8100/8200/...) ganha
    //     GUID proprio → Windows trata como aplicacoes DIFERENTES, nunca mais
    //     "instalador em cima da instalacao anterior" (bug do Grupo Ata).
    //   • DefaultDir sugerido segue a porta: PremiumExpressBI_8100 etc.
    //     UsePreviousAppDir=no (nunca reusa pasta antiga).
    //   • Wizard pergunta PORTA primeiro (wpWelcome), auto-sugere nome de
    //     servico "PremiumExpressBI_<porta>" quando troca a porta.
    //   • **FIX SSL LOCAL "httpd.conf rejeitado"** (erro no Grupo Ata):
    //     - Antes: instalador SEMPRE gerava conf/extra/httpd-ssl-local.conf,
    //       mesmo se openssl falhasse ao gerar cert. Apache 'httpd -t' entao
    //       explodia com AH02565 "SSLCertificateFile does not exist".
    //     - Agora: gera o cert PRIMEIRO. So se cert+key existirem, gera o
    //       httpd-ssl-local.conf. Se openssl falhar/faltar, HTTPS 8443 e'
    //       simplesmente pulado (IncludeOptional nao quebra) e Apache
    //       principal sobe normal na porta escolhida.
    //   • UninstallDisplayName inclui empresa e porta: cada instancia visivel
    //     separadamente em Programas e Recursos.
    //
    //   BI:
    //   • Menu Inadimplentes/Inadimplencia removido do Financeiro (redundante
    //     com Central Financeira, muito mais completa). Banner de migracao
    //     nas duas telas antigas com CTA pra /financeiro/central.
    // v1.7.142 (2026-08-06) — **FIX: /relatorios/inadimplencia timeout 30s.**
    //   Bug reportado no cliente 177.84.238.189 (outro Fire). ClientesRepository
    //   linha 157 estourava Maximum execution time em inadimplentesDetalhado():
    //   a subquery correlacionada (SELECT COUNT(*) FROM RECEBER_PAGAR rp2 WHERE
    //   nr_nota = rp.nr_nota) rodava 1x POR LINHA do resultado. Com 1000 titulos
    //   inadimplentes = 1000 COUNTs = 30s+. Trocado por window function
    //   COUNT(*) OVER (PARTITION BY NR_NOTA, SERIE) — roda 1x, ordens de
    //   magnitude mais rapido. Firebird 3.0 suporta window functions nativo.
    // v1.7.141 (2026-08-06) — **FIX: Modal "Detalhes do Titulo" vinha vazio em multi-loja.**
    //   Bug reportado: clique no botao 🔎 dentro do modal Timeline abria o
    //   modal Detalhes com TODOS os campos "—" e "Nota #undefined". Causa
    //   raiz: o rec passado pra finDetalhe() tinha loja_id: '_single' HARDCODED
    //   no renderTimeline(). Em multi-loja, o titulo esta em outra empresa,
    //   entao /financeiro/central/historico-titulo filtrava por cod_empresa=5
    //   quando deveria ser cod_empresa=2 (ex: Achadinhos). Query retornava
    //   vazio → tudo undefined na UI.
    //   Fix:
    //   1) Novas vars TIM_LOJA_ID/TIM_LOJA_NOME setadas no finVerTimeline.
    //   2) renderTimeline usa TIM_LOJA_ID em vez de '_single' hardcoded.
    //   3) renderDetalhe detecta detalhe vazio (!d.NR_NOTA) e mostra erro
    //      amigavel explicando as causas em vez de campos vazios.
    // v1.7.140 (2026-08-06) — **FIX: TTL curto no DashboardRepository esgotava cache toda hora.**
    //   Complementa v139. Cache miss em vendasAoVivo (linha 322) e pdvFluxo (linha 368)
    //   custava 3-5s POR LOJA em multi-loja Radmin. Como TTL era 10-15s, PRATICAMENTE
    //   NENHUMA request pegava cache — sempre reconsultava 6 lojas em serie.
    //   Fix: TTL 10-15s → 120s (2min). "Ao vivo" com 2min de atraso e' aceitavel
    //   pros KPIs de faturamento/quantidade/ultima venda.
    //   Impacto esperado: /consolidado cache warm de ~15s pra ~2s em multi-loja.
    // v1.7.139 (2026-08-06) — **FIX UNIVERSAL: lentidão em TODAS as páginas de multi-loja.**
    //   Diagnostico do Mega Importados: BI travava 3-15s por pagina, mesmo em
    //   telas leves como /manual e /financeiro/central. Causa raiz: o layout
    //   main.php iterava TODAS as lojas visiveis em SERIE via FirebirdService::
    //   pingHost pra mostrar banner "loja offline". Cada probe = 900ms + retry
    //   1800ms se offline. Cache era 30s online / 8s offline → toda vez que
    //   expirava, +5-15s por page load. Fix em 3 partes:
    //   1) main.php: remove o probe SERIE. Renderiza sem esperar, depois
    //      chama /api/lojas/status via AJAX 5s depois → banner aparece se
    //      alguma loja offline, SEM atrasar a pagina.
    //   2) Novo LojasStatusController + rota GET /api/lojas/status
    //      (mesma logica, mas assincrono, com set_time_limit 30s).
    //   3) FirebirdService::pingHost: cache online 30s→300s (5min), offline
    //      8s→60s. Reduz cache miss de "toda pagina" pra "1x a cada 5min".
    //   Sem alteracao de dado nenhum. Sem impacto em PDVs (Firebird intacto).
    //   Sem impacto em Consolidado (que tinha 4 loops de loja em serie —
    //   proximo passo, virar assincrono via AJAX por loja).
    // v1.7.138 (2026-08-05) — **HOTFIX: reverte v137 (landing = Consolidado novamente).**
    //   v137 trocou landing pos-login pra /executivo achando que seria mais rapido.
    //   Medi 230ms no cache warm, mas em cache FRIO (pos-upgrade OTA) deu 47s no
    //   dev e MAIS DE 5 MINUTOS no cliente Mega Importados. Executivo carrega KPIs
    //   pesados (metas, top vendedores, financeiro, alertas) em SERIE por 6 lojas
    //   via Radmin. Consolidado tambem e pesado (28s dev, 8-15min cliente) mas
    //   PELO MENOS tem set_time_limit(180) explicito e caching mais agressivo.
    //   Solucao definitiva: fazer landing = tela dedicada 100% estatica (0 query)
    //   com atalhos grandes pros modulos. Fica pra v1.7.139+.
    // v1.7.137 (2026-08-05) — **Fix performance: landing pos-login = Executivo (200ms).**
    //   Antes o HomeController::index() redirecionava pra /consolidado (Resumo Geral)
    //   que carrega 6 lojas via Radmin em SERIE — 8s no cache warm, 28s no cache
    //   frio, 10-15 min quando a VPN esta lenta. Cliente relatou "abre importados
    //   demora demais, 1a vez da erro, 2a termina". Diagnostico: multi-loja + VPN +
    //   PHP built-in single-thread + upgrade que zera cache. Fix: entrar direto no
    //   /executivo (KPIs do agregado, 200-300ms). Resumo Geral continua no menu.
    //   Alternativa longa (nao feita agora): paralelizar o loop de lojas com
    //   curl_multi no ConsolidadoController.
    // v1.7.136 (2026-08-05) — **Cards do BI: TODO o menu Financeiro catalogado.**
    //   Antes o /configurar/cards controlava só o grupo Financeiro inteiro (liga/desliga
    //   tudo). Agora tem 1 toggle por tela:
    //   - menu-financeiro-central          (Central Financeira NOVA)
    //   - menu-financeiro-regua            (Régua de Cobrança)
    //   - menu-financeiro-fluxo-caixa      (Fluxo de Caixa)
    //   - menu-financeiro-contas-receber   (A Receber classico)
    //   - menu-financeiro-contas-pagar     (A Pagar)
    //   - menu-financeiro-dre              (DRE)
    //   - menu-financeiro-despesas         (Despesas Operacionais)
    //   - menu-financeiro-mix-pagamento    (Mix de Pagamento)
    //   Cada um com titulo/descricao no CardsConfig::CARDS pra aparecer bonito na
    //   tela /configurar/cards agrupado por "Menu Lateral — Financeiro".
    //   Layout main.php agora consulta cada chave individualmente antes de
    //   listar o subitem. Cliente pode desligar Contas a Receber classico se ja usa
    //   Central Financeira nova, esconder DRE se nao usa, etc.
    // v1.7.135 (2026-08-05) — **Régua de Cobrança visível em TODOS os pontos de entrada.**
    //   Adicionados atalhos para /financeiro/regua em:
    //   - Submenu do Financeiro (financeiro/_submenu.php) — Central Financeira e
    //     Regua ganharam link fixo com emoji.
    //   - Topo da Central Financeira — botao vermelho "🤖 Régua de Cobranca" ao lado
    //     de "Como usar" e "Exportar".
    //   - Dashboard Executivo — cards "A Receber" e "Receber Vencido" agora sao
    //     LINKS clicaveis pra Central Financeira e Regua respectivamente
    //     (sub-texto: "ver detalhes →" e "🤖 cobrar →").
    //   - config/cards.php — nova chave 'menu-financeiro-regua' (true default,
    //     cliente pode desabilitar via CardsConfig).
    // v1.7.134 (2026-08-05) — **Régua de Cobrança AUTOMÁTICA (novo módulo).**
    //   Novo item de menu Financeiro > Regua de Cobranca (bi-chat-square-dots).
    //   Sistema completo pra cobrar sem levantar o dedo:
    //   - 6 faixas configuraveis: pré-venc (-3d), no dia (0), 1-7d atraso,
    //     8-15d, 16-30d, +30d. Cada faixa: liga/desliga, canal (WA/email/ambos),
    //     dias, template com placeholders {NOME} {NOTA} {VALOR} {VENCIMENTO}
    //     {DIAS} {LOJA}. Config em storage/regua_cobranca_config.json.
    //   - 2 modos: 🧪 Simulacao (só conta) · 🤖 Automatico (dispara de verdade).
    //   - 3 abas: Configuracao / Preview de hoje / Historico (ultimas 30 execucoes).
    //   - Botao "Testar envio" com modal (canal + destino + msg).
    //   - Botao "Executar agora" pra rodar fora de horario.
    //   - Dedup diaria: nao envia 2x pro mesmo titulo no mesmo dia
    //     (via HistoricoContatoRepository::carregarTodos()).
    //   - Teto de seguranca max_por_dia (padrao 200).
    //   - Worker CLI: C:\BI\tools\worker-regua-cobranca.php roda no scheduler
    //     todo dia (config hora_disparo, janela ±30min).
    //   - Envio via Evolution API v2 (WhatsApp) e SMTP HostGator (email) —
    //     usa a config global ja existente, sem novo setup.
    //   - Novos arquivos: ReguaCobrancaRepository, ReguaCobrancaService,
    //     ReguaCobrancaExecutor, ReguaCobrancaController, view financeiro/regua.php,
    //     worker-regua-cobranca.php. HistoricoContatoRepository ganhou
    //     carregarTodos() publico.
    // v1.7.133 (2026-08-05) — **Central Financeira: Exportação PDF + Excel PROFISSIONAL.**
    //   Botao "📥 Exportar ▾" agora tem 3 opcoes:
    //   - 🖨️ PDF profissional: nova rota /financeiro/central/imprimir com HTML A4
    //     completo — cabecalho Fire, 6 KPIs coloridos, aging chart, top 10 devedores,
    //     tabela detalhada com badge de situacao (cores reais), rodape "Premium
    //     Express Gestao v{ver} · Fire Sistemas · Suporte (66) 99248-8989 · pag N/M"
    //     em cada folha via @page/@bottom-center. Auto-abre dialogo de impressao
    //     — usuario salva como PDF direto do navegador.
    //   - 📊 Excel (.xls): nova rota /financeiro/central/exportar-xlsx com
    //     HTML-table + Content-Type Excel + XML metadados (multi-aba Recebiveis /
    //     Resumo / Top Devedores). Linhas coloridas por situacao (verde/amarelo/
    //     laranja/vermelho/cinza). Cabecalho indigo Fire, rodape suporte.
    //   - 📋 CSV puro (existente).
    //   Ambos respeitam filtros da tela e cliente NAO ve Master Automacao.
    //   Nenhuma dependencia PHP externa (sem PhpSpreadsheet/Mpdf).
    // v1.7.132 (2026-08-05) — **Contas a Receber: filtro de títulos de comissão órfãos.**
    //   Caso Buteco: módulo de faturamento do Fire grava controle de comissão de
    //   representante ("Comissão por liquidação") na RECEBER_PAGAR com REL_COMISSAO='S'
    //   e SEM usuário/data/versão de lançamento. Nota já paga em dinheiro na emissão;
    //   o registro nunca recebe baixa (a tela F0220 do Fire nem o lista) e aparecia no
    //   BI como "vencido crítico" fantasma (R$ 10.500 no Buteco: Brasilcap + Dynamic,
    //   vendas de buffet legítimas da representante Roanny).
    //   Fix nos 3 leitores de RECEBER_PAGAR — RecebiveisRepository (13 queries),
    //   FinanceiroCentralService (13), FinanceiroRepository (2): novo filtroOrfaos()
    //   com detecção de schema via RDB$RELATION_FIELDS (só aplica se REL_COMISSAO e
    //   USUARIO existem — cliente com Fire antigo fica intacto). Título de comissão
    //   lançado legitimamente pela tela (com usuário) continua aparecendo.
    // v1.7.131 (2026-08-05) — **Fix: clique no gráfico mensal + estado vazio com dicas.**
    //   - Bug: clicar em Janeiro no gráfico "Comparativo mensal" filtrava mes=1 mas
    //     mantinha status="aberto" — se todos os titulos daquele mes ja estavam PAGOS,
    //     tabela ficava vazia. O grafico mostrava R$ 8.657 emitido mas tabela dizia "nenhum".
    //   - Agora o click no grafico interpreta a serie clicada:
    //     * "Total emitido" (barra azul) → status=todos + situacao=todos
    //     * "Em aberto" (barra verde) → status=aberto + situacao=todos
    //     * "Vencido" (linha vermelha) → status=aberto + situacao=vencido
    //   - Estado vazio da tabela agora sugere quais filtros afrouxar
    //     (mostrar tambem pagos / limpar situacao / todos os meses / etc).
    // v1.7.130 (2026-08-05) — **Central Financeira: Timeline com TUDO clicavel.**
    //   Modal Timeline agora responde a cliques em cada elemento:
    //   - CPF/CNPJ → copia pro clipboard (com toast)
    //   - Telefone → abre WhatsApp Web com msg pronta
    //   - Email → mailto
    //   - CEP → Google Maps
    //   - "Cliente desde" / "1a compra" / "Ultima compra" → filtra Central por essa data
    //   - KPIs (Score / Total pago / Em aberto / Vencido / Atraso medio) → filtra a
    //     tabela interna do modal (pills: Todos / Abertos / Vencidos / Pagos / Parciais)
    //   - Barra do grafico mensal → filtra o mes na tabela
    //   - Coluna Nota → abre Detalhes do Titulo (mesmo modal do 🔎)
    //   - Coluna Emissao/Vencimento → fecha modal e filtra Central pela data
    //   - Coluna Tipo Op. → fecha modal e filtra Central por essa modalidade
    //   - Coluna Status → filtra a tabela interna
    //   - Coluna Atraso (>30d) → filtra Central pelos criticos
    //   - Botao "🔎 Filtrar Central por este cliente" (dispara auto-complete)
    //   - Botao "📊 Exportar CSV" (só titulos filtrados)
    //   - Botao "🖨️ Imprimir"
    //   - Cada contato de cobranca → botao "copiar msg"
    //   - Chip "% do limite usado" quando cliente tem LIMITE_CREDITO configurado.
    // v1.7.129 (2026-08-05) — **Central Financeira: Timeline enriquecida com detalhes completos.**
    //   Modal Timeline (clicar num cliente) agora mostra TUDO que o banco tem:
    //   - Cadastro completo: nome, CPF/CNPJ, telefone, email, BAIRRO+CEP+NUMERO,
    //     data de cadastro, LIMITE_CREDITO, LIMITE_CONTA, TOTAL_COMPRAS, situacao ATIVO,
    //     observacao (auto-adaptativo por schema — só mostra o que existe).
    //   - 8 KPIs de COMPORTAMENTO (agregado historico completo, sem limit):
    //     🏆 SCORE 0-100 (Otimo pagador / Medio / Alerta),
    //     💚 Total ja pago, 🔵 Em aberto, 🔴 Vencido, 🎯 Ticket medio,
    //     ⏰ Atraso medio, 📅 1a compra, 📅 Ultima compra.
    //   - Mini-grafico ECharts: evolucao 12 meses (Emitido/Pago/Em aberto).
    //   - Tabela de titulos agora com: PRAZO (dias entre emissao e venc),
    //     ATRASO (dias em atraso), status "parcial" quando saldo<valor,
    //     botoes 🔎 Detalhe e 💬 Cobrar em cada linha.
    //   - Contatos de cobranca mostrado com fallback "nenhum registrado".
    //   Endpoint /financeiro/central/timeline retorna 'serie_mensal' e KPIs agregados novos.
    // v1.7.128 (2026-08-05) — **Central Financeira: 8 KPIs + coluna Prazo + Sugestão + Manual.**
    //   - 3 KPIs novos: "A receber próx 7 dias", "A receber próx 30 dias", "Recebido no período".
    //   - KPI "Risco top devedor" (% concentracao do maior cliente, meta <25%).
    //   - Coluna "Prazo" na tabela (dias entre emissao e vencimento; a vista destacado).
    //   - Tooltips em CADA KPI, cabeçalho da tabela e botão.
    //   - Card automático de "Sugestão de acao" (dinamico segundo os KPIs).
    //   - Botão "Como usar" no topo → abre /manual#central-financeira em nova aba.
    //   - Manual reescrito: seção Central Financeira com 10 subseções detalhadas
    //     (16 filtros, 8 KPIs, aging, grafico mensal, top devedores, tabela, timeline,
    //     detalhes titulo, cobranca WhatsApp, exportar, rotina sugerida, metricas saude).
    // v1.7.127 (2026-08-05) — **Central Financeira: FILTROS COMPLETOS + DETALHES em tudo.**
    // Reforma da tela /financeiro/central com base no pedido "tudo tem que ter detalhes,
    // ver ano a ano, mes a mes, filtro completo":
    //   - Filtros novos: ANO (dropdown), MES (Jan-Dez ou todos), EMISSAO de/ate,
    //     VENCIMENTO de/ate, VALOR min/max, STATUS (aberto/pago/todos), TIPO OPERACAO
    //     (dropdown vindo do banco: A Vista, Prazo, Cartao, Boleto...), LOJA (dropdown
    //     multi-loja), CLIENTE (autocomplete por nome ou codigo).
    //   - Grafico mensal comparativo ECharts: barras "a receber por mes" + linha "vencido"
    //     — clique na barra filtra o mes.
    //   - Cada KPI clicavel: total aberto → tabela completa; vencido → so vencidos;
    //     DSO → destaca os +30d; ticket medio → distribuicao por faixa; % vencido → aging.
    //   - Botao "🔎 Detalhes" em cada linha da tabela abre modal com HISTORICO do TITULO:
    //     movimentos (pagamentos parciais), historico do texto, dados fiscais.
    //   - Cabecalho de tabela ordenavel (clique ordena por Valor, Saldo, Atraso).
    //   - Exportar Excel/CSV da lista atual (respeita filtros).
    //   - Endpoints novos: /financeiro/central/serie-mensal, /historico-titulo,
    //     /clientes-autocomplete, /tipos-operacao.
    // v1.7.126 (2026-08-05) — **Central Financeira: busca + aging + WhatsApp + timeline cliente.**
    // Nova tela /financeiro/central inspirada nas melhores praticas de ERPs BR
    // (Omie, Bling, Sankhya, Neofin, IRecebi):
    //   - Busca no topo por cliente/nota/codigo + filtros situacao/faixa + so_comissao
    //   - 5 KPIs coloridos: total aberto, vencido, DSO, ticket medio, % vencido
    //   - Aging chart (6 faixas: a vencer / 1-15 / 16-30 / 31-60 / 61-90 / +90) clicavel
    //   - Top 10 devedores (clique abre timeline)
    //   - Tabela ordenada por vencimento com badge de situacao (a_vencer/alerta/atencao/critico)
    //   - Botao "💬 Cobrar" abre modal com mensagem pronta ({cliente},{nota},{valor},{dias}),
    //     copia pro clipboard OU abre WhatsApp Web (wa.me/?text=...)
    //   - Timeline por cliente (modal): 30 ultimos titulos, KPIs do cliente, contatos anteriores
    // Novos: FinanceiroCentralService, FinanceiroCentralController, HistoricoContatoRepository
    // (JSON local em storage/historico_cobranca.json), view completa.
    // Rotas GET /financeiro/central + /dados + /timeline + POST /registrar-contato.
    // Multi-loja: agrega recebiveis de todas as lojas ativas (pula virtual).
    // Item menu Financeiro > Central Financeira (bi-wallet2).
    // v1.7.125 (2026-08-05) — **PERF: Central de Caixas caiu de 15min pra segundos.**
    // Cliente Importados (6 lojas via VPN) demorava 15 min carregando /caixa/central
    // com filtro Mes. Causa: eu adicionei montarKpisPorLoja em v1.7.113 que RE-ITERAVA
    // as lojas fazendo Database::conectarLoja + caixasAbertos() NOVAMENTE — dobrando
    // conexoes VPN (6 lojas × 2 conexoes × 3s = 36s so conectando, alem das queries).
    // Fix: novo metodo montarKpisPorLojaFast que recebe abertosPorLoja[] ja coletado
    // dentro do loop principal (uma passada so). Zero re-conexao.
    // Metodo antigo mantido como @deprecated pra retrocompat.
    // v1.7.124 (2026-08-03) — **Instalador Universal: LoadModule ssl_module no httpd-template.**
    // Erro "Invalid command 'SSLEngine'" na instalacao do Confresa. httpd-template.conf
    // fazia IncludeOptional de httpd-ssl-local.conf (que usa SSLEngine pra HTTPS 8443)
    // mas NAO carregava mod_ssl -> Apache do cliente nem subia apos o setup.
    // Fix: adicionado LoadModule ssl_module modules/mod_ssl.so no template principal.
    // Bonus: build-universal.ps1 nao deleta mais httpd-ssl-local.conf do staging
    // (universal PRECISA de HTTPS 8443 pra scanner do Leitor Fire).
    // v1.7.123 (2026-08-03) — **Pacote OTA enxuto: 27MB → ~2MB. Fim do bug do Confresa.**
    // Cliente Confresa (Windows antigo) travava em "Falha ao extrair" porque:
    //   1) vc_redist.x64.exe (25 MB) estava sendo incluido no pacote OTA — mas
    //      cliente que ja tem BI instalado NAO precisa desse binario. So instalador
    //      do ZERO precisa. Isso pesava 94% do zip.
    //   2) Expand-Archive precisa ~2x o zip livre em %TEMP%. Zip de 27MB + descompactado
    //      50MB+ nao cabia. Pacote fica agora ~2 MB — cabe em qualquer maquina.
    // build-ota-pacote.ps1 agora exclui vc_redist.x64.exe do robocopy /XF
    // e /XD app\tools (pasta dev-only). tools\ da raiz continua indo (worker CLI).
    // v1.7.122 (2026-08-03) — **AtualizacaoController: fallback tar.exe + erro detalhado.**
    // Cliente Confresa (Churrascaria, v1.7.101) via "Atualizar agora" -> "Falha ao
    // extrair o pacote". Zip publicado esta OK (testado local: extrai perfeito).
    // Problema no lado do cliente: Expand-Archive pode falhar em Windows Server 2008/2012,
    // permissao em %TEMP%, ou zip baixado corrompido.
    // Fix:
    //   1) Se Expand-Archive falha, tenta tar.exe (nativo Win10+ / 2016+) como fallback.
    //   2) Mensagem de erro agora traz: rc, tamanho do zip baixado, se app/ ou public/
    //      ficaram no destino — pra diagnostico remoto sem RDP.
    //   3) Log em storage/logs/atualizacao.log ganha etapa 'extrair_zip_fallback_tar'.
    // Script manual C:\tmp\atualiza-bi-manual.ps1 pra rodar via Radmin em qualquer
    // cliente travado (tenta os 2 metodos, para/sobe servico Apache, preserva
    // config/lojas.php e config/database.php).
    // v1.7.121 (2026-08-02) — **Fix: texto dos produtos ilegivel no drill do vendedor.**
    // Descricao dos produtos e coluna "Un." do "Top 10 produtos vendidos" apareciam
    // apagadas (herdavam cor escura do CSS global do BI, fundo tambem escuro).
    // Fix: cor explicita #e2e8f0 nas celulas + hover discreto.
    // v1.7.120 (2026-08-02) — **Central de Vendedores: drill-down completo por vendedor.**
    // Feedback: "n tem detalhes" — clicar no vendedor nao abria nada.
    // Agora clicar na linha da tabela abre modal grande com:
    //   - 4 KPIs (fat, vendas, ticket, itens/venda) COM variacao ↑↓% vs mes anterior
    //   - Comparacao com a MEDIA da loja (acima/abaixo em cada metrica)
    //   - Grafico de barras da evolucao diaria no periodo
    //   - Grafico de barras de vendas por hora (identifica horario forte)
    //   - Top 10 produtos mais vendidos por essa operadora
    // Novo endpoint GET /vendas/central-vendedores/detalhe?loja=X&cod=Y&data_ini=&data_fim=
    // Novo metodo Service::detalheVendedor com 5 queries cacheadas (TTL 3min pra hoje).
    // Botao 💬 Elogiar continua funcionando (stopPropagation pra nao abrir drill junto).
    // v1.7.119 (2026-08-02) — **Central de Vendedores: ranking multi-loja com score composto.**
    // Nova tela /vendas/central-vendedores no padrao da Central de Caixas:
    //   - Top 5 do grupo em cards de honraria (medalhas ouro/prata/bronze)
    //   - Detalhamento POR LOJA com tabela: fat, vendas, ticket, itens/venda,
    //     score 0-100, meta% (se cadastrada em config/metas_operadoras.json)
    //   - 🏅 Medalha destacando o melhor vendedor de cada loja
    //   - Botao "💬 Elogiar" abre modal com 5 frases prontas personalizadas
    //     ({nome}, {loja}, {fat}, {tkt}, {qtd}) — copia pra clipboard, envia por WA
    //   - Score composto por loja: 60% fat + 20% ticket + 10% qtd + 10% itens/venda
    //     Comparacao dentro da MESMA loja (nao mistura caixa varejo com garcom).
    //   - Filtra "(SEM CAIXA)" e nomes com "TESTE" (nao sao operadoras reais).
    //   - Loja virtual (faixa_preco) pulada — usa caixa da irma fisica.
    // Novos: VendedoresCentralService, VendedoresCentralController, view completa.
    // Rotas GET /vendas/central-vendedores + /dados. Item de menu com bi-trophy-fill.
    // v1.7.118 (2026-08-02) — **Central conferir: grava nome do usuario logado (coluna POR).**
    // Bug: conferirApi tentava \$_SESSION['fb_user'] / ['usuario'] (chaves erradas) —
    // sempre gravava usuario vazio "" no JSON, coluna POR do modal aparecia "—".
    // Fix: usa FbAuth::nome() (nome amigavel) com fallback FbAuth::usuario().
    // Marcacoes ANTIGAS ficam com "—" (nao tem como saber quem marcou) — so
    // as novas em diante gravam nome. Se o gestor quiser corrigir, "Desmarcar" e
    // "Marcar conferido" de novo grava o nome dele.
    // v1.7.117 (2026-08-02) — **Central: coluna "Ja conferidos" clicavel (com Desmarcar).**
    // Faltava simetria — clique em Abertos/Pra conferir tinha detalhe, mas "Ja conferidos"
    // era so um numero mudo. Agora clica -> modal verde com tabela:
    //   #Caixa · Operador · Fechamento · Conferido em · Por · [✕ Desmarcar]
    // Botao Desmarcar volta o caixa pra lista de pendentes na hora e recarrega os KPIs.
    // Novo endpoint GET /caixa/central/conferidos?loja=X cruza SQLite local com
    // ABERTURA_CAIXA dos ultimos 90 dias pra trazer nome do operador de cada caixa.
    // Mesmo modal reutilizado (fcc-modal-abertos), so muda cor do header e conteudo.
    // v1.7.116 (2026-08-02) — **Central: clica em "🟡 abertos" e ve QUEM esta com caixa aberto.**
    // Feedback: "Mega diz 4 caixas abertos, mas clico e ve so 2" — clique so filtrava
    // pendentes fechados (nao mostrava os abertos individuais).
    // Agora: coluna "Abertos agora" (quando >0) fica clicavel -> abre modal com
    // #caixa · Operador · Data · Hora de cada caixa aberto. Query direta em
    // ABERTURA_CAIXA WHERE DATA_FECHAMENTO IS NULL. Novo endpoint
    // GET /caixa/central/abertos?loja=X. Modal com botao X e fecha com ESC.
    // Rodape do modal explica: "fechamento eh feito pelo operador no PDV — esta
    // lista serve pra o gestor saber quem ainda nao fechou".
    // v1.7.115 (2026-08-02) — **Central: fix headline "5 vs 6" + filtro clicavel por loja.**
    // Bugs corrigidos:
    //   1) Discrepancia: tabela mostrava "Pra conferir=5" mas headline "6 pra conferir".
    //      Causa: centralApi nao marcava 'conferido' em cada fechamento individual (so
    //      no kpi agregado). Headline usava pendentes.length que incluia os conferidos.
    //      Fix: marca fc.conferido/conferido_em/conferido_por no loop de todosFechamentos.
    //   2) Como filtrar rapido a loja de interesse? Nome da loja e coluna "Pra conferir"
    //      agora sao clicaveis — filtram a lista de pendentes abaixo pra so aquela loja.
    //      Banner amarelo mostra "🔎 Filtrando por: X" com botao pra limpar.
    //      Rola pra a lista automaticamente.
    // v1.7.114 (2026-08-02) — **UI Central: tabela clara + headline por loja + sem duplicar virtual.**
    // Feedback do cliente: grade de cards era confusa ("nao entendi"), headline "8 caixas
    // pra conferir" nao dizia DE QUAIS lojas, e loja virtual (ach10a) aparecia duplicando
    // os caixas da irma fisica (ach_20a).
    // Correcoes:
    //   1) Grade de cards -> TABELA HORIZONTAL: uma linha por loja com colunas
    //      "Abertos agora | Fechados no periodo | Pra conferir | Ja conferidos" + linha
    //      TOTAL GERAL destacada no rodape (mais legivel que 6 cards separados).
    //   2) Loja virtual (faixa_preco) NAO entra na grade nem no total (compartilha caixa
    //      fisico com irma, contaria em dobro). Filtro no montarKpisPorLoja e no proprio
    //      loop de todosFechamentos.
    //   3) Headline agora mostra breakdown por loja: "8 caixas pra conferir — 3 na Mega,
    //      2 na Ach 20 Aragarcas, 2 na Tamara, 1 na Barra" em vez de so o total generico.
    // v1.7.113 (2026-08-02) — **Central de Conferencia: KPIs por loja + marcar "conferido".**
    // Feedback do cliente Importados: nao sabia quantos caixas abriu por loja nem o total,
    // nao via quantos fechou vs abertos, e nao tinha como sinalizar "ja conferi esse".
    // Agora:
    //   1) Grade no topo da /caixa/central com KPIs por loja: 🟡 abertos agora
    //      (banco ABERTURA_CAIXA WHERE DATA_FECHAMENTO IS NULL), ✅ fechados no periodo,
    //      🔴 pendentes (nao verde, nao conferido), 👁 conferidos. Card TOTAL GERAL no fim.
    //   2) Botao "✓ Marcar conferido" em cada card pendente. Persiste em
    //      storage/conferidos.json (JSON flat, sem SQLite — nem todo cliente tem pdo_sqlite).
    //      Bloco novo "conferidos pelo gestor" no rodape com botao "✕ Desmarcar".
    //   3) Novos: ConferidoRepository, FechamentoCaixaRepository::caixasAbertos(),
    //      rotas POST /caixa/central/conferir e /desconferir, montarKpisPorLoja no Controller.
    // Filtragem: caixa conferido some da lista de pendentes automaticamente.
    // v1.7.112 (2026-08-02) — **Hotfix inline: corrige data_abertura do ach10a no upgrade.**
    // Cliente Importados tinha ach10a.data_abertura='12-06-2026' (cadastro inicial)
    // mas a loja so inaugurou em 31-07-2026. Comparativos "vs semana/mes passado"
    // mostravam valores de dias em que a loja nem existia.
    // Como OTA preserva config/lojas.php no cliente, criei hotfix no boot:
    // Application::hotfixesUpgrade() roda 1x junto com autoLimparCacheNoUpgrade,
    // detecta '12-06-2026' em ach10a e substitui por '31-07-2026'. Idempotente
    // (so age se encontrar o valor errado), com backup .bak-hotfix-{timestamp}.
    // v1.7.111 (2026-07-31) — **CRITICO: sangrias/comSemNota de uma loja apareciam em outra.**
    // Dono do Importados reclamou: card "Achadinhos 20 Aragarcas" mostrava sangrias
    // (CAIXATESTE R$ 950 / LAURA CX R$ 650 / HERICA CX R$ 500) que sao da Mil Coisa (tamara).
    // Bug REAL (nao era cache — Cache::file() ja isola por escopoCache):
    // v1.7.106 introduziu auto-detect faixasIrmas. Se loja fisica tem irma virtual
    // (ex.: ach_20a tem irma ach10a faixa=10), NAO recriava $repo — ficava com PDO
    // da iteracao anterior (tamara vem antes de ach_20a). $repo->sangriasNoIntervalo
    // e sangriasDetalheNoIntervalo puxavam do banco da tamara mas gravavam em row do ach_20a.
    // Fix: criar $repo LOGO NO TOPO do try, ANTES de qualquer branch — garante PDO
    // sempre da loja atual. Removido if-isset condicional que era o culpado.
    // v1.7.110 (2026-07-31) — **Fix Fechamento de Caixa lojas pegando uma da outra.**
    // Bug reportado no Importados: 5 lojas do banco Fire tem MESMO cod_empresa=2
    // (ach10 Barra, ach20 Barra, tamara, ach_20a, ach10a) em bancos DIFERENTES.
    // As 4 chaves de cache do FechamentoCaixaRepository e 1 do Service usavam
    // apenas $codEmpresa (sem escopoCache()) -> primeira loja gravava, resto lia
    // dados dela. Central de Conferencia mostrava fechamentos da mesma loja
    // repetidos com nomes diferentes.
    // Fix: adicionado Lojas::escopoCache() em 5 chaves:
    //   - FechamentoCaixaRepository fc_cods_dev, fc_fechamentos, fc_nr_notas_v3, fc_operadores
    //   - FechamentoCaixaService fcc_padrao_troca
    // Padrao ja usado pelo fcc_central (Service). Cache local limpo.
    // v1.7.109 (2026-07-31) — **3 fixes no /vendas/gestao (feed ao vivo).**
    // 1) pdvFluxoApi + aoVivoApi agora chamam Lojas::setFoco($id) no loop.
    //    Antes: VendasRepository::$codEmpresa static travava na 1a loja (matriz emp=5)
    //    e demais lojas filtravam empresa errada -> retornavam 0/vazio.
    //    Cache::escopoCache() vazio -> caches de lojas diferentes colidiam.
    // 2) Auto-deteccao v1.7.106 estendida: se loja fisica tem irma virtual,
    //    KPI usa resumoComplementar (bate com consolidado). ex.: ach_20a mostra
    //    "banco - R$10" em vez do banco inteiro.
    // 3) Removido SLICE GLOBAL de 50 no feedGlobal. Antes: lojas com vendas
    //    mais antigas (ach20barra 19:02) eram cortadas fora dos top 50 globais
    //    e JS mostrava "sem vendas ainda" mesmo com KPI positivo. Cada loja ja
    //    limita 30 no repo; JS corta 12 por loja no render — sem slice global.
    // v1.7.108 (2026-07-31) — **Loja virtual: esconde cards que agregam por cupom.**
    // Cards que trabalham por cupom (TOTAL_NOTA) — Com Nota vs Sem Nota, Formas de
    // Pagamento, Sangrias, Acrescimo Invisivel — sao IMPOSSIVEIS de filtrar por
    // linha de preco (um cupom pode ter itens R$10 + R$20 misturados). Antes esses
    // cards mostravam valores enganosos no drill-down da loja virtual (numeros do
    // banco todo, mas rotulados como "da ach10a"). Agora:
    //   - Com Nota vs Sem Nota → substituido por card azul-info explicando
    //   - Formas de Pagamento / Sangrias / Acrescimo → ocultos totalmente
    //   - Top Produtos / Grupos / Marcas / Hora / Comparativos → SEGUEM (filtrados por preco)
    // Tambem: /vendas/gestao (tela ao vivo). Loja virtual mostrava "sem vendas ainda"
    // (feed nao existia pra virtual). Agora mostra "Loja virtual R$X — ver vendas
    // individuais na loja irma" quando L.virtual=true.
    // v1.7.107 (2026-07-31) — **Loja recem-aberta: comparativos sem historia falsa.**
    // Antes: loja com data_abertura = hoje mostrava "vs semana passada R$ 9.448"
    // e "vs mes passado R$ 12.096" — valores que sao vendas antigas do banco
    // (ex.: ach10a que compartilha banco com ach_20a). Nao existiam como loja.
    // Agora: ConsolidadoController compara data_abertura DD-MM-YYYY com a
    // data_ini do periodo anterior; se anterior for < abertura, zera 'anterior'
    // e marca 'nao_existia_antes'=true. View mostra card informativo azul
    // "Loja inaugurada em X — sem base historica para comparar" no lugar do
    // delta enganoso. Vale pros 3 cards: semana, mes, ano.
    // Tambem: Top 20 Grupos/Marcas → Top 10 (bate com Top 10 Produtos).
    // Ajuste em ach10a.data_abertura: 12-06-2026 → 31-07-2026 (inaugurou hoje).
    // v1.7.106 (2026-07-31) — **Auto-deteccao de "loja irma com faixa_preco".**
    // ANTES: se o cliente cadastrasse Achadinhos 10 Aragarcas (faixa=10) sem
    // marcar tambem Achadinhos 20 Aragarcas com faixa=20, a 20 mostrava o TOTAL
    // do banco (incluindo os R$10) e o total do grupo DUPLICAVA os R$10.
    // AGORA: BI detecta automaticamente que ach_20a compartilha banco+cnpj com
    // ach10a (que tem faixa=10) e calcula "tudo exceto R$10" pra ach_20a.
    // Zero cadastro adicional necessario — funciona pra qualquer cliente.
    // Novo metodo: Lojas::faixasCobertasPorIrmas($id) + LinhaPrecoRepository::resumoComplementar()
    // Testado no Grupo Mega Importados: ach_20a caiu de R$ 20.587 (bug/duplicado)
    // pra R$ 10.140 (correto — soma com ach10a R$10.360 = R$ 20.500 = total banco).
    // v1.7.105 (2026-07-31) — **Consolidado com seletor de 4 modelos de visualização.**
    // Além do Ranking classico (default), agora tem 3 novas visualizacoes acessiveis
    // via botoes no topo do card:
    //   - 🏆 Ranking   — lista ordenada por faturamento (o de sempre)
    //   - 🟦 Treemap   — retangulos proporcionais ao faturamento; visualiza peso relativo
    //   - 🎴 Cards     — grid KPI-style com badge de status por loja
    //   - 🌳 Grupos    — hierarquico por bandeira (Achadinhos 10, 20, Mega, etc)
    //                    mostra insight "melhor loja fatura Nx mais que a pior"
    // Escolha via ?view=X na URL. Nao altera cálculos — mesma query, layouts diferentes.
    // v1.7.104 (2026-07-31) — **FIX CRÍTICO: lojas virtuais zeravam em multi-loja.**
    // Bug reprodutivel encontrado no cliente Grupo Mega Importados: Achadinhos 10
    // Aragarcas (loja virtual emp=2, faixa_preco=10) aparecia R$ 0,00 no consolidado.
    // 2 bugs encontrados e corrigidos:
    // (A) Lojas::setFoco() so mudava o escopoCache(), NAO mudava idAtiva()/codEmpresaAtiva().
    //     Ao iterar o consolidado, setFoco('ach10a') era chamado mas Lojas::idAtiva()
    //     continuava retornando o id da loja da SESSAO (Mega Importados = emp 5).
    //     Fix: idAtiva() agora prioriza focoOverride.
    // (B) VendasRepository::$codEmpresa era static-cached na 1a chamada e nunca
    //     invalidado. Mesmo com bug (A) corrigido, cache antigo continuava.
    //     Fix: novo metodo VendasRepository::invalidarCacheEmpresa() chamado dentro
    //     de Lojas::setFoco().
    // Confirmado: Achadinhos 10 Aragarcas agora retorna R$ 7.200 (152 cupons de R$10)
    // separado de Achadinhos 20 Aragarcas (mesmo banco, filtro correto).
    // v1.7.103 (2026-07-31) — **Fix: OPcache segurava lojas.php velho apos edicao.**
    // Cliente Grupo Mega Importados: editava data_abertura do ach10a de 'pendente'
    // para '12-06-2026', arquivo no disco atualizado, mas BI continuava mostrando
    // R$ 0 mesmo depois de Ctrl+F5 + limpar cache. Motivo: opcache.validate_timestamps
    // do PHP nao pega mudanca no lojas.php, e Apache mantem o array antigo em memoria
    // por horas. Fix em Lojas::todas() com opcache_invalidate() antes de cada require.
    // A partir dessa versao, qualquer edicao em lojas.php aparece na PROXIMA request.
    // v1.7.102 (2026-07-31) — **Telemetria de uso do BI + reforço no fluxo de lojas virtuais.**
    // 1) Novo canal de telemetria: cada login do BI dispara ping fire-and-forget pro
    //    Master Backup Manager (POST /api/telemetria.php) contendo cliente_slug +
    //    usuario + versao. Painel mostra quem tá abrindo, qtas x/dia/semana/mes,
    //    e alerta cliente inativo. Config opcional em app/config/telemetria.php —
    //    se vazio, nao envia (silencioso, nunca trava login).
    // 2) Novo arquivo app/Support/TelemetriaClient.php com fallback curl -> file_get_contents.
    // 3) FbAuth::login() ganchou a chamada apos autenticacao OK (linha 330).
    // 4) Lojas virtuais (faixa_preco + data_abertura) confirmadas em producao: Achadinhos
    //    10 Aragarcas agora aparece com valor real (R$ 6.030 · 134 vendas) separado da
    //    irma fisica Achadinhos 20 Aragarcas (R$ 10.939 · 213 vendas). Ambas mesmo banco.
    // v1.7.101 (2026-07-26) — **TOTAL GERAL do consolidado ganhou "honraria".**
    // Derlivon: "esse total merece uma atencao especial". Antes era um bloco cinza
    // discreto no fim do ranking; agora tem selo dourado "TOTAL DO GRUPO" no topo,
    // troféu em círculo dourado à esquerda, borda índigo dupla com sombra elevada,
    // valor gigante (1.75rem) e barra decorativa dourada separando sublabels.
    // Regra [[feedback_ranking_multi_loja_total]] reforçada — presença visual à altura.
    // v1.7.100 (2026-07-26) — **Fix: BI-servidor 24h travou por "HTTP 0 file_get_contents falhou (sem ext-curl)".**
    // PHP sem ext-curl + allow_url_fopen=Off → download impossível. Adicionado 3º fallback
    // usando curl.exe do Windows (sempre existe desde Win10 1803). Ordem: (1) ext-curl,
    // (2) file_get_contents, (3) curl.exe. Também: publicar-http.ps1 agora sobe automático
    // versao-local.json (canal BI-servidor 24h) além do versao.json padrão — evita canal
    // ficar 3 dias parado como aconteceu.
    // v1.7.99 (2026-07-26) — **Fix: BI não pegava versão nova por cache HTTP intermediário.**
    // Cliente atualizou pra v1.7.97 e "Verificar" ficou dizendo "sistema atualizado" mesmo
    // com v1.7.98 publicada há minutos. Causa: proxy/curl-cache do lado do cliente segurava
    // manifesto. Fix triplo: cache-buster no URL (?_=timestamp) + headers no-cache/no-store
    // no curl + CURLOPT_FRESH_CONNECT=true (força TCP novo, ignora keep-alive). A partir
    // dessa versão, futuras updates NUNCA mais ficam presas em cache.
    // v1.7.98 (2026-07-26) — **Ranking de lojas ganhou linha TOTAL GERAL somado.**
    // Derlivon: "regra global precisa da somatoria geral aqui". Antes o dono via
    // 3 lojas do Gelatto (R$ 5.644 + 5.549 + 1.496) mas tinha que somar mental.
    // Agora aparece TOTAL GERAL destacado (border-top escuro, gradiente cinza claro,
    // fonte 900) somando fat, vendas, e ticket médio geral (= fat/vendas, não média
    // aritmética). Ícone bi-calculator, legenda "soma das N lojas". Atualiza ao trocar
    // aba (fat/vendas/ticket). Regra global [[feedback_ranking_multi_loja_total]] pra
    // aplicar em TODA nova tabela/ranking multi-entidade.
    // v1.7.97 (2026-07-26) — **CAÇADA GLOBAL de datas ISO/barra no BI inteiro.**
    // Derlivon: "parece que sistema foi feito na china, já pedi em caráter de urgência
    // modo global". Bug flagrado em /compras/dashboard (card "Última venda" mostrava
    // 2026-07-26 cru). Ações: (a) helper GLOBAL window.fmtDataBr/fmtDataHoraBr no app.js
    // (carrega toda página); (b) 13 fmtData locais que usavam BARRA (/) trocados por
    // TRAÇO (-) — regra brasileira; (c) leitor/scanner.php + compras/dashboard.php
    // corrigidos; (d) regra global reforçada em feedback_data_formato_brasileiro (2ª vez)
    // e no CLAUDE.md global com padrões PROIBIDOS ("date('Y-m-d')" em texto visível etc).
    // v1.7.96 (2026-07-26) — **Removida seção "Novidades auto" do manual (changelog do Versao.php).**
    // Derlivon: "isso nao pode ficar no manual, retire todas, vamos das melhorias nao mostrando o
    // cada versão faz". Cliente não deve ler changelog técnico com emoji quebrado. Novidades no
    // manual voltam a ser CURADAS manualmente em linguagem de negócio (a seção "Novidades recentes"
    // hardcoded original permanece intacta). Removidos: partials _novidades_auto.php e
    // _novidades_bloco.php + includes em index/atacado/automoveis. Regra global registrada em
    // [[feedback_manual_sem_changelog_auto]].
    // v1.7.95 (2026-07-26) — **Título "Performance da Cozinha" centralizado visualmente no Excel.**
    // Antes: merge A1:L1 (12 colunas). O texto centralizava matematicamente correto no meio das 12,
    // mas como K e L ficam além da viewport típica (largura total 201 unidades), parecia deslocado
    // pra direita. Agora: merge A1:J1 (10 colunas, cabe em 1600px), + fill laranja nas K1 e L1
    // separadas pra manter a barra visual contínua sem quebrar. Aplicado nas linhas 1 e 2 (título
    // + subtítulo) da aba Pratos.
    // v1.7.94 (2026-07-26) — **Rebranding: "Relatório Executivo — Tempo de Preparo" virou
    // "Performance da Cozinha".** Derlivon: dono novo entende melhor "Performance" que
    // "Executivo". Trocado em 7 lugares: title da aba do browser, cabeçalho da tela,
    // sub-menu tab, menu principal (sidebar), card da home /cozinha/menu, título do PDF,
    // título das 2 abas do Excel + label da permissão granular. Ícone mudou de bi-stopwatch
    // pra bi-fire pra reforçar tema. Rota /cozinha/relatorio e slug de permissão
    // cozinha_relatorio mantidos (compat).
    // v1.7.93 (2026-07-26) — **Paleta quente no relatório do KDS (Excel + PDF).**
    // Derlivon: "mais colorido, ta muito azul". Cozinha=fogo, era 3 faixas índigo empilhadas.
    // Título faixa LARANJA (#F97316), subtítulo VERMELHO ESCURO (#C2410C), header tabela CINZA
    // ESCURO (#334155). KPIs também variados: Finalizados=laranja, Média=índigo, Pior=semáforo.
    // Aplicado nas 2 abas do Excel + PDF. Rodapé pastel mantém índigo (marca Fire).
    // v1.7.92 (2026-07-26) — **Cabeçalho profissional na aba "Pratos" do Excel do KDS.**
    // Derlivon: "preciso de um cabeçalho profissional aqui no excel". Antes: aba de dados
    // abria com "Mesa | Prato | Grupo..." solto na linha 1 — sem identidade. Agora seguindo
    // regra global [[feedback_excel_cabecalho_profissional]]: linha 1 título faixa índigo,
    // linha 2 subtítulo (drill+período), linha 3 espaço, linha 4 header da tabela (freeze
    // aqui), linhas 5+ dados, fim = rodapé Fire+suporte. Vale pra TODAS as abas de agora em
    // diante em qualquer .xlsx do BI.
    // v1.7.91 (2026-07-26) — **Excel do KDS ganhou MARCA D'ÁGUA de verdade (ExcelJS).**
    // Trocada lib xlsx-js-style → ExcelJS 4.4.0 + FileSaver 2.0.5 (CDN). Motivo: xlsx-js-style
    // não suporta imagens embutidas (só cores/bordas). ExcelJS tem addImage nativa. Logo Fire
    // é reduzida pra opacity 12% via canvas antes de embedar (pra parecer marca d'água). Aparece
    // centralizada em AMBAS as abas (Resumo e Pratos). Layout mantido: título faixa índigo, 3
    // KPIs coloridos, tabela com header índigo, zebra, coluna Tempo colorida por semáforo, freeze
    // pane. Código morto do xlsx-js-style removido.
    // v1.7.90 (2026-07-26) — **Marca d'água virou LOGO Fire (imagem) no PDF do KDS.**
    // Antes: texto "FIRE SISTEMAS". Agora: PNG da logo oficial (public/assets/img/
    // fire-sistemas-logo.png) embutida em base64 pelo PHP, renderizada centralizada
    // com opacity 10%, tamanho 230x90pt. Try/catch silencioso se logo faltar. Master
    // NÃO entra (regra global [[feedback_branding_fire_acima_de_tudo]] mantida).
    // v1.7.89 (2026-07-26) — **Marca d'água "FIRE SISTEMAS" no fundo do PDF do KDS.**
    // Diagonal 30°, fonte 90pt índigo com opacity 8% (jsPDF GState), + "Premium Express
    // Gestão" 18pt abaixo. Renderizado no didDrawPage ANTES do rodapé — tabela desenha
    // por cima naturalmente. Try/catch pra fallback silencioso se jsPDF não suportar GState.
    // v1.7.88 (2026-07-26) — **Rodapé Fire+Suporte mais visível no PDF e Excel.**
    // Antes: cinza 7pt quase invisível. Agora: linha divisória, fonte 9pt, "Fire Sistemas"
    // em ÍNDIGO negrito, "(66) 99248-8989" em VERDE negrito com "Suporte técnico:" antes.
    // Página X de Y em negrito à direita. Linha 2 em itálico 8pt (data + fonte dos dados).
    // Excel: rodapé ganhou fundo índigo claro (EEF2FF), texto índigo escuro negrito.
    // v1.7.87 (2026-07-24) — **BI vira consultor: bloco "O que o BI está te contando".**
    // Derlivon: "no relatorio tbem precisa ensinar o gestor a ler, da indicativos".
    // Novo bloco laranja/verde/vermelho logo abaixo dos KPIs com análise automática dos
    // números da hora: detecta "cozinha marca em lote" (P90 >> mediana), recorde absurdo
    // (>=2h), % de pratos esquecidos (faixa 45+), pico duplo de hora (volume + lentidão),
    // garçom que demora no balcão (>=8min), grupo gargalo, e mensagem positiva se tudo OK.
    // Cada insight tem TÍTULO curto (o problema), TEXTO com números explicativos, e AÇÃO
    // prática ("faça isso"). Ordena por severidade — grave > atenção > bom.
    // v1.7.86 (2026-07-24) — **Legendas explicativas + seção completa do manual pro KDS.**
    // Derlivon: "preciso de legendas e explicação clara de cada grafico, alimentar o manual".
    // (A) Cada um dos 6 gráficos/rankings do /cozinha/relatorio ganhou bloco "O que mostra ·
    // Como ler · O que fazer" abaixo do título, com semâforo de cores explicado e ação prática.
    // (B) Manual /manual ganhou seção "🍳 Cozinha — Tempo de Preparo" (só bar): painel do
    // tablet, relatório com todos os KPIs explicados (p50/p90/recorde/etc), cada gráfico
    // documentado individualmente, dicas de leitura, alerta WhatsApp, exports PDF/Excel.
    // Item novo no índice tb: "🍳 Cozinha (Tempo de Preparo)" NOVO. Cumpre a regra global
    // [[feedback_nova_feature_legenda_manual]] (feature = legenda + manual + atalho).
    // v1.7.85 (2026-07-24) — **Datas em DD-MM-YYYY em TODOS os pontos visíveis do KDS.**
    // Regra global brasileira já em vigor mas o `ctx.periodo` do modal e o subtítulo do
    // PDF/Excel ainda passavam ISO ("2026-06-26 a 2026-07-26"). Adicionado fmtDataBr() e
    // fmtDataHoraBr() centralizado, aplicado em: subtítulo do modal, label do topo (barra
    // com período), Resumo do Excel, PDF (subtítulo + rodapé "Gerado em"). Padrão DD-MM-YYYY
    // com TRAÇO em tudo (não barra /). Filtros HTML input type=date continuam ISO por
    // exigência do browser — só o que aparece pro cliente muda.
    // v1.7.84 (2026-07-24) — **Rodapé oficial Fire + suporte técnico nos exports do KDS.**
    // Removido "Master Automação · Derlivon Silva" que aparecia no rodapé do PDF/Excel
    // (violava regra global: cliente NUNCA vê nome pessoal). Novo rodapé nos 2:
    //   Linha 1: "Premium Express Gestão v1.7.84 · Fire Sistemas · Suporte técnico (66) 99248-8989"
    //   Linha 2 (só PDF): "Relatório gerado em DD/MM/YYYY HH:MM · dados atualizados do ERP Fire"
    // v1.7.83 (2026-07-24) — **Excel do KDS agora sai formatado igual ao PDF.**
    // Derlivon: "pq em pdf sai bonito e excel sai assim?". Antes usava SheetJS community
    // que NÃO faz estilos (fica planilha crua). Trocado pra xlsx-js-style (mesma API +
    // font/fill/border/merge). Aba Resumo virou dashboard: título faixa índigo mesclado,
    // 3 KPIs em cards coloridos (Pior tempo com semáforo), texto explicativo, rodapé Fire+Master.
    // Aba Pratos: header índigo com texto branco, zebra listing, coluna Tempo(min) colorida
    // por semáforo (verde<5, amarelo<15, vermelho<30, vermelho-escuro>=30), freeze no header.
    // v1.7.82 (2026-07-24) — **Formato humano de tempo (Xh Ym) no PDF/Excel/CSV do KDS.**
    // Derlivon: "esse campo passou de 60minutos mostrar horas e minutos 1h32m". PDF concatenava
    // "375 min" cru; Excel/CSV tinham só a coluna numérica. Corrigido: PDF usa fmtMin (mesma
    // função do HTML, vira "6h 15m" se >=60min). Excel/CSV ganharam DUAS colunas — a numérica
    // pra Excel somar/filtrar, e uma formatada humana pra leitura. Vale pra Tempo e Balcão.
    // v1.7.81 (2026-07-24) — **Fix drill "0 pratos" quando ranking e detalhe divergem em
    // caps/espaço.** Derlivon clicou "1/2 FILE A MINEIRA" e abriu vazio. Ranking usa
    // MAX(i.DESCRICAO); detalhe usa COALESCE(lm.DESCRICAO, i.DESCRICAO) — se o Fire tem
    // descricao customizada com espaço trailing ou case diferente, comparação `===` falha.
    // Corrigido pra `trim().toLowerCase()` em prato, grupo, garçom e retirante. Também vale
    // pro drill filtrar "(sem garçom)" corretamente contra string vazia no detalhe.
    // v1.7.80 (2026-07-24) — **WhatsApp de atraso pro dono (Cozinha, config UI).**
    // Novo card em /cozinha/config: 2 campos (nº WhatsApp + threshold em min) + botão
    // "Enviar mensagem de teste" que dispara via Evolution API já configurada nos
    // Alertas Diários (config/alertas.php → whatsapp). Não tem tela nova no menu —
    // config no card existente. Rota nova /api/cozinha/wa-testar (permissão granular
    // cozinha_config). Worker de disparo em produção fica pra próxima release.
    // v1.7.79 (2026-07-24) — **Fix drill-down do KDS: era "0 pratos" pra qualquer
    // barra clicada referente a prato alem do 200o.** No Buteco tem ~378 pratos/mes;
    // ranking usa TODOS mas o array 'detalhe' que alimenta o filtro do modal era
    // cortado em 200 no repo — resultado: clicar em "CALDINHO FEIJAO HAPPY HOUR"
    // (ou qualquer prato do final da lista) abria modal vazio. Corte subiu pra 5000
    // (bar tipico ~500/dia, cabe 10+ dias); filtro client-side aguenta bem esse tamanho.
    // v1.7.78 (2026-07-24) — **Relatorio Tempo de Preparo agora le tambem do Fire (PRONTOFIM).**
    // Descoberto no Buteco: cliente comecou a tocar o botao "PRONTO" no PDV do Fire, e o
    // LANC_MESA.PRONTOFIM foi preenchido em 485 lancamentos nos ultimos 30 dias. O BI estava
    // ignorando esse dado (so lia storage/kds_finalizacoes.json do tablet dele) e mostrava
    // "0 finalizados" mesmo com a cozinha operando cheia. Agora: fonte HIBRIDA — mescla JSON
    // local + LANC_MESA.PRONTOFIM valido (>2020, escapa sentinela 1899-12-30 do Delphi), dedup
    // por chave empresa|abertura|lanc, JSON vence se ambos existem (mais completo — tem
    // retirado_em). Falha silenciosa se schema nao tem PRONTOFIM (fallback 100% JSON, mesmo
    // comportamento antigo). Manual auto-gerado + botoes PDF/Excel no drill do modal (jsPDF +
    // SheetJS via CDN client-side). Novidades auto tambem — parse do Versao.php filtrado por
    // segmento (bar/varejo/atacado/automoveis) direto nos manuais dos 4 segmentos.
    // v1.7.77 (2026-07-24) — **Relatorio Tempo de Preparo (KDS) reorganizado + drill-down universal.**
    // Derlivon: "confuso, precisa detalhes clicaveis". Antes: 8 secoes empilhadas, cliques so
    // mostravam tooltip. Agora: banner "como ler" colapsado em icone (?), legenda vira 4 badges
    // compactos no cabecalho, layout em 3 zonas claras (KPIs / 3 graficos lado a lado / 3 rankings),
    // e MODAL universal de drill-down em TUDO — cada barra, cada linha de tabela, cada KPI clicavel
    // abre uma tabela filtrada com os pratos daquele criterio + botao "Exportar CSV". Filtragem
    // 100% client-side no array 'detalhe' que ja vem da API (sem query nova no banco).
    // Layout: 1 linha KPI + 1 linha 3 graficos + 1 linha 3 rankings + detalhamento colapsado no fim.
    // v1.7.76 (2026-07-23) — Telemetria de uso "a prova de tudo": endereco do painel
    // EMBUTIDO no codigo (fallback se a config faltar — cobre cliente de cadastro
    // antigo), envio por 3 caminhos (curl -> file_get_contents -> fsockopen cru, que
    // funciona SEM ext-curl e SEM allow_url_fopen), e nome do cliente com mais fontes
    // (database.php empresa -> lojas.php -> hostname). Testado local (envio OK) +
    // fsockopen alcancando o :8085 real. So metadado de acesso, nunca conteudo.
    // v1.7.75 (2026-07-23) — Backup: corrigido o gbak que gerava .fbk de 0 byte /
    // "travado" sem avisar. O '-y SUPPRESS' virava um NOME DE ARQUIVO e escondia o
    // erro do gbak, o que tambem quebrava o fallback pra outra versao do Firebird —
    // clientes ficavam 0 byte pra sempre. Agora o status vai pra um arquivo lido pelo
    // codigo: tenta a versao certa e, se falhar, MOSTRA o motivo no painel. Testado
    // local (192 MB). + Telemetria de USO no login -> Painel Central (aba "Uso":
    // quem abre, quantas vezes, cliente inativo). So metadado de acesso, nunca conteudo.
    // v1.7.74 (2026-07-18) — **KPI "Concentração" virou clicável com drill-down.** Derlivon confirmou
    // que a v1.7.73 resolveu o timeout do Extra (*"funcionou!"*) e apontou o que faltava: *"mas aqui
    // nao ta clicavel sem informaçoes"* — o card mostrava só "95%" e o dono não tinha como saber
    // QUAIS produtos são esses nem o que fazer com o número. Era o único dos 4 KPIs sem detalhe
    // (os outros 3 já trocavam de aba), violando a regra de drill-down do CLAUDE.md.
    // **DOIS BUGS LATENTES ACHADOS AO LIGAR O CLIQUE:** (1) o handler fazia `trocarAba(el.dataset.aba)`
    // pra QUALQUER `.kpi-clic` — bastaria marcar o card de Concentração como clicável pra ele chamar
    // `trocarAba(undefined)`; agora testa `data-aba` e cai no modal quando não tem; (2) o clique de
    // linha era delegado em `#body`, mas os modais são anexados a `document.body` e ficam FORA de
    // `#body` — a tabela dentro do modal novo não responderia. Delegação movida pra `document`.
    // **O MODAL responde 3 perguntas:** (a) o que o número quer dizer, em reais ("da sua compra de
    // R$ 81.939 em 409 itens, os 100 maiores respondem por 69%"); (b) **quantos itens fecham 80% do
    // dinheiro** — a resposta prática, calculada por acumulado ("160 itens: negociar bem esses 160
    // vale mais que revisar a lista inteira"); (c) a curva em barras (10/20/50/100 maiores) + tabela
    // dos 100 que mais pesam com % acumulado, cada linha clicável pra ficha do produto.
    // Clicar numa linha **fecha o modal da Concentração antes de abrir a ficha** (senão empilha modal
    // sobre modal e sobra dois "X" pra fechar).
    // **VALIDADO no banco de testes com dados reais:** modal abre, 4 barras, 100 linhas, acumulado
    // até 69%, clique na linha fecha um e abre o outro, **zero backdrop órfão**.
    // v1.7.73 (2026-07-18) — **A Sugestão de Compra nunca pediu tempo ao PHP. Essa era a causa raiz.**
    // Reportado pelo Derlivon: mesmo depois do hotfix v1.7.72, o cliente **Extra continuou dando
    // "A consulta demorou demais e foi interrompida"** — *"do mesmo jeito, no importados funciona"*.
    // **DIAGNÓSTICO:** `SugestaoCompraController::dadosApi()` **não chamava `set_time_limit` nenhum**
    // e herdava o `max_execution_time = 30s` do php.ini do Apache — enquanto AlertasDiarios e Atacado
    // já pediam 300s. No Importados 30s sobra; em supermercado com 30 dias de NOTAS_ITENS, não.
    // **ERRO MEU NA v1.7.72, corrigido no comentário do código:** eu escrevi que o `FIRST {limite}`
    // resolveria. **NÃO resolve** — `FIRST` corta só o que trafega pro PHP; o `GROUP BY` precisa
    // varrer os 30 dias inteiros ANTES de qualquer corte. Reduzi o transporte, não o trabalho.
    // **MEDIDO (Mega Importados, banco real):** agregação de venda 30d = 948 ms (12.329 SKUs);
    // consulta de TENDÊNCIA (agrega OUTROS 30 dias) = 798 ms = **63% do custo das duas** — e entrega
    // só a setinha ▲▼, que é enfeite.
    // **TENTATIVA TESTADA E DESCARTADA (registrado pra ninguém repetir):** restringir a tendência aos
    // 1.500 itens exibidos via `AND ni.COD_ITEM IN (...)` em blocos de 1000. Resultado **idêntico**
    // (1500/1500 conferidos) mas **8x MAIS LENTO** (472 ms -> 3846 ms): a lista de IN mata o índice
    // por data. Ver [[infra_firebird_in_limite_grandes_bancos]].
    // **O CONSERTO (3 camadas):** (1) `set_time_limit(180)` **sem `@`** — foi justamente o `@` da
    // v1.7.63 que mascarou esse mesmo tipo de falha por semanas; (2) **ORÇAMENTO DE TEMPO** no padrão
    // já usado em `produtoMultiLojaApi`: o controller passa um relógio-limite e o repo, antes de rodar
    // a tendência, estima o custo (1,2x o que a etapa de venda levou) — se não couber, **pula** e
    // marca `tendencia_ok=false`; a tela avisa *"sem comparação com o período anterior (banco lento
    // agora)"*. **Testado: mesmas 344 linhas, 504 ms em vez de 949 ms** — degrada em vez de morrer;
    // (3) **INSTRUMENTAÇÃO** — a resposta traz `tempos` com o ms de cada etapa, pra diagnosticar
    // cliente que eu não consigo medir daqui em vez de adivinhar.
    // **NÃO VALIDADO NO EXTRA** (não tenho o banco dele) — validado só no Mega Importados. Se o Apache
    // do cliente tiver timeout próprio (`Timeout` / `FcgidIOTimeout`), o `set_time_limit` do PHP não
    // vence e aí é config de servidor, não código.
    // v1.7.72 (2026-07-18) — **HOTFIX de desempenho na Sugestão de Compra + divisor do CMV + folha de conserto que dá pra usar.**
    // (1) **REGRESSÃO CORRIGIDA (Supermercado Extra deu timeout na v1.7.71).** Duas causas, ambas introduzidas por mim na v1.7.71:
    //     (a) o `limite` da tela subiu de 1000 para 5000 sem necessidade; voltou pra 1500 e a tela avisa quando a lista foi cortada;
    //     (b) a query 2 (itens ABAIXO DO MÍNIMO, sem venda) virou 100% desperdício depois que a tela passou a ser dirigida pela venda —
    //     TODO bucket exige venda/dia > 0, então cada linha que ela trazia era descartada logo em seguida. Ela fazia um ORDER BY
    //     (ESTOQ_MINIMO - QTD) em tabela cheia sem LIMIT — era o gargalo. **Query 2 REMOVIDA**; a query 1 agora usa FIRST {limite}
    //     ordenado por faturamento, então o corte pega os que mais importam. Medido no Mega Importados: 3794 ms -> 1435 ms frio /
    //     201 ms quente. **Custo honesto da mudança:** a contagem de furos de cadastro caiu de 1278 para 448, porque agora conta
    //     dentro do subconjunto analisado (os que mais vendem), não a base inteira. É menos abrangente e mais rápido — de propósito.
    // (2) **Divisor do CMV corrigido no Painel de Compras** (`ComprasRepository::totalVendasPeriodo`). O total de vendas usado como
    //     divisor não filtrava tipo de operação nem empresa e ainda sofria **fan-out do JOIN com TIPO_OPERACAO** — cuja chave é
    //     COD_TIPO_OPERACAO + COD_EMPRESA, ou seja, num banco com 3 empresas (Grupo Ata) cada nota era contada 3x. Agora usa
    //     `VendasRepository::filtroVendaSql()` + `filtroEmpresaSql()` (mesma definição da tela de Vendas). Medido: R$ 4.103.075 ->
    //     **R$ 1.239.436** (3,31x de inflação removida), bate com conferência direta no banco. **O CMV exibido sai de 4,3% para ~24%**
    //     — o número antigo estava errado, o novo está certo. Os demais KPIs do Painel de Compras ainda têm defeitos mapeados
    //     (Concentração, Total Comprado, Ticket Médio, Fornecedor×Item) e serão corrigidos nas próximas versões.
    // (3) **"Lista de Conserto do Cadastro" (a folha impressa) refeita pra funcionar na prateleira.** Antes ela mandava contar mas
    //     não tinha onde anotar, e dizia "travam R$ X por mês" — texto exagerado: aquele número NÃO é prejuízo, é quanto o produto
    //     vende por mês (serve pra priorizar). Agora: coluna em branco **"Contei"** com linha pautada pra escrever, "R$/mês travado"
    //     virou **"Vende R$/mês"**, "Prov. comprar" virou **"Comprar (se zerado)"**, instrução em **5 passos numerados**
    //     (conte -> anote -> ache a nota de entrada -> se não achar, faça acerto de estoque -> marque), explicação da CAUSA em
    //     linguagem de dono (a venda sai no caixa e subtrai, mas a nota de entrada nunca foi lançada e não somou) e rodapé de
    //     conferência **Quem conferiu / Data / Itens resolvidos ___ de N**. Impressão: cabeçalho repete a cada página e linha não
    //     parte no meio.
    // v1.7.71 (2026-07-17) — **Sugestão de Compra REFEITA pela raiz: a VENDA manda, não o estoque.** Pedido do Derlivon: *"melhora a sugestão"* + *"pesquisa fundo na web e me traz algo surpreendente"*. **PESQUISA (4 frentes, fontes reais):** DeHoratius & Raman, *Management Science* 2008 (65% dos registros de estoque imprecisos — a venda é confiável, o saldo não) · RELEX/Lokad "phantom inventory" (dirigir demanda pela venda, não propagar saldo negativo) · **ECR Retail Loss** (~1M SKUs, 63% imprecisos em variedades; corrigir os registros rende +6% a +14% de venda) · MS Dynamics 365 SCM (custo zero/negativo corrompe o custo médio — barrar antes do cálculo) · Netstock/Odoo (gestão por exceção) · ABC por faturamento robusto a custo podre. **A VIRADA:** toda tela de reposição do mercado parte do ESTOQUE. Aqui o estoque é o pior dado que existe — **medido: 67% negativo no Mega Importados** (1.276 de 1.918 "comprar já" tinham estoque NEGATIVO, não zerado). Invertemos: a venda (dado limpo do PDV) dirige, o estoque só abate quando é plausível (>=0). O estoque -1.176 deixa de ser uma compra às cegas e vira **ordem de serviço de cadastro**. **DUAS ABAS a partir das mesmas queries:** (1) **Comprar hoje** — dirigida pela venda, ponto de pedido = venda/dia × (prazo+colchão por classe A7/B5/C3), quantidade cobre prazo+colchão+30 menos saldo confiável, **mínimo furado do cadastro ABANDONADO** (mínimo vira a própria venda); 4 KPIs confiáveis no topo (R$ auditável, concentração Top 100, cadastro furado clicável, custo a revisar); **corte por ORÇAMENTO** (digita quanto quer gastar → lista corta sozinha nos que rendem mais, ordenados por R$/mês que geram); cauda de venda rara (<0,1/dia) sai da urgência pro "ver o resto". (2) **Arrume o cadastro** — os furos e custos podres, priorizados pela **venda que travam** ("MANGUEIRA vende 12/dia, sistema diz -149, R$ 2.178/mês travado — conte e lance a NF"). **MATA OS 3 PROBLEMAS DO PRINT:** (a) estoque negativo NUNCA vira "Zerou" nem entra na conta — vai pra fila de conserto; (b) margem impossível (-19.496% medido) e custo zerado saem do R$ do topo — só custo auditável entra (inflava ~50%: R$143k cru vs **R$82k confiável**); (c) 1.918 linhas viram DECISÃO — 515 comprar (dirigido por venda) + 1.278 conserto + 425 cauda recolhida, com corte por orçamento e gestão por exceção (top 200/render). **DESCARTADO de propósito:** ABC por MARGEM (conselho padrão de e-commerce) — depende do custo, que é o dado podre; fica ABC por faturamento (só venda). EOQ/Wilson (exige custos que a loja não tem). **MEDIDO (Mega Importados, 30d):** 2.255 acionáveis, repo em 3,8s, **0 estoque negativo e 0 margem impossível na aba comprar** (sanidade). **Universal:** loja sem estoque negativo (bar/papelaria) → aba de conserto nasce vazia e a tela degrada pra fila simples de reposição. **CUIDADO tomado:** flags MOTIVO_RUPTURA/MINIMO mantidos no sentido original (o Rastreador de Ruptura os lê) — a distinção furo-vs-zero vive só no BUCKET, independente. Método venda-driven em SugestaoCompraRepository (calcular()); tela em Views/compras/sugestao.php. **Linha CLICÁVEL → ficha do produto** (nas 2 abas): venda/dia+mês, estoque, ponto de pedido, custo/preço/margem, quanto rende/mês, e **a conta em português** ("você vende X/dia, pra cobrir 44 dias precisa de Y, tem Z, comprar N"); no furo mostra o aviso "estoque impossível, conte e lance a NF"; no custo podre "arrume o custo no ERP". **Botão "Imprimir lista de conserto"** na aba Arrume: gera folha de tarefa (todos os furos+custos, não só os da tela, com ☐ pra marcar, loja+período+data, ordenada por R$ travado) pra entregar impressa pro pessoal da loja. **Manual atualizado** (Views/manual/index.php): seção nova explicando a tela + entrada em Novidades + corrigido link velho /sugestao-compra → /compras/sugestao. **Fix universal de robustez (FirebirdService::pingHost):** o probe de "loja offline" era 250ms e crava OFFLINE falso em loja NO AR quando a VPN tem latência alta (medido: ping 549ms no Mega Importados → Painel de Compras dava 500 e travava no "Carregando..."). Agora: probe 900ms + retry (dobro) antes de cravar offline, e cache de "offline" cai de 30s → 8s pra recuperar rápido quando a loja/VPN volta. Loja realmente desligada ainda falha rápido (~2,7s no pior caso, vs travar 60s no PDO). Testado 5x sem cache contra o Mega Importados: 5/5 ONLINE em 56–114ms. Testado em harness com a resposta REAL da API, fora de public/ (trava v1.7.62). Lint OK.
    // v1.7.70 (2026-07-16) — **"Consultar Produto em Todas as Lojas" deixou de ser esparramada: 126 blocos viraram 1 tabela densa agrupada por LOJA.** Pedido do Derlivon: *"precisa agrupar mais, esta muito esparramado, ache uma forma de compactar... pesquisa na web e veja como fazem"*. **PESQUISADO (4 frentes, fontes reais):** NN/g "Data Tables: Four Major User Tasks" (registro = linha plana; 1a coluna = identificador legivel, nunca ID gerado; accordion so serve pra editar poucas linhas) · IBM Carbon (linha 32px / corpo 14px) · Shopify Inventory + app Invo "auto-hide non-stocking locations" · Odoo 18 Stock Report (agrupa por LOCAL, total no cabecalho do grupo) · Linx POS / TOTVS Winthor (multi-filial se resolve no FILTRO, nao no layout) · Oracle Retail Insights (reporting por EXCECAO) · Amazon/Google Shopping (widget de comparacao nao e' renderizado quando ha 1 fonte so). **O DIAGNOSTICO:** a tela agrupava por PRODUTO — 126 grupos, e **120 deles com UM filho**, porque 95% dos produtos existem numa loja so. Grupo de 1 nao junta nada: era a propria fonte do esparramado. **LOJA e' a unica dimensao com densidade real aqui** (cada produto cai em exatamente 1 grupo, zero celula vazia). **DESCARTADA a matriz produto x 5 lojas** (a ideia que todo mundo tem primeiro): 630 celulas, so ~132 com dado = **79% de buraco** — e pior que feio, MENTE: a celula vazia leria "acabou o estoque" quando significa "nem existe no cadastro dessa loja", duas situacoes opostas com o mesmo desenho. **MEDIDO (busca "alicate", 5 lojas do Grupo Mega, dado real):** rolagem **~8.100px -> 4.129px**; linha **~64px -> 29,9px** (sem quebra em 1366x768); produtos por tela **~6 -> ~12**, e agora com 6 numeros na linha em vez de 0 (antes eram 126 cliques pra ver estoque/dias/comprar). Com chip de loja: 6 linhas. **CORTADO:** coluna ABC (era participacao da loja no produto e o proprio codigo forca 'A' com 1 loja — dizia "A" em 120 de 126 linhas), Min, Custo, Fat e Loja (viraram ficha/cabecalho de grupo); o codigo saiu de 1a coluna (e' diferente em cada loja, nao serve de ancora) e virou sufixo cinza. **ACHADO NO CAMINHO, e este era um problema de DADO:** o card "Precisa comprar" marcava **75 linhas — 63 delas NUNCA venderam** (estoque 0 + minimo 7 do cadastro do Mega Importados = "compre 7" de item morto), e **49 sequer tem custo cadastrado**, entao o "R$ pra repor" saia calculado em 26 linhas so. Card com 84% de ruido queima a tela no primeiro clique. Virou **"Vai faltar" = vende E vai acabar: 12 linhas, R$ 633** — alarme que da pra agir hoje (ex.: ALICATE BICO 6 CRV, Achadinhos 20A, estoque 1, vendeu 23, dura 1,3 dia). Pelo mesmo motivo a ordem padrao deixou de ser "Comprar desc" (empatava tudo em 7 e abria a tela com 48 linhas zeradas do Mega) e passou a ser por RELEVANCIA: vai faltar -> vende -> parado com dinheiro -> cadastro morto; as LOJAS tambem ordenam por quem precisa de atencao. A coluna Comprar continua mostrando o numero do ERP, mas so fica **verde-negrito quando e' alarme de verdade** — cinza + tooltip quando e' o minimo do cadastro sobre item que nao vende. **Os 3 criterios (faltar/parado/multi) moram num objeto FILTROS unico** lido por card, chip e filtro: foi exatamente ter o mesmo criterio escrito em 2 lugares que gerou a regressao da v1.7.68. **Zero query nova** — a API ja mandava tudo, inclusive o `cod_item` por loja (linha 821), que a view ignorava e repetia o mesmo codigo em todas. **Testado** com a resposta REAL da API (126 produtos / 6 em 2+ lojas) num harness fora de `public/` (v1.7.62): 132 linhas, 5 faixas, 12 badges, **0 linha de "nao cadastrado"**, chips somando 48+19+12+47+6=132, cross-filter chip+card OK, ordenar dissolve os grupos e traz a coluna Loja de volta, comparacao inline abre. Lint php -l OK.
    // v1.7.69 (2026-07-16) — **CORRIGE REGRESSAO QUE EU INTRODUZI NA v1.7.68** (que ja foi publicada, entao os clientes pegaram). Derlivon viu em 2 segundos: *"ta ficando pior"* — o #819 passou de 2 linhas pra 5, quatro delas dizendo "produto nao cadastrado nesta loja" sobre um codigo que NUNCA foi daquela loja. **CAUSA:** na v1.7.68 troquei a consulta pra buscar so os codigos de origem de cada loja (`$codsLoja`), mas deixei **3 loops varrendo `$codigos` inteiro** — o que registra presenca (linha 649), o que marca "nao cadastrado" (742) e o que marca "offline" (760). Resultado: todo codigo das OUTRAS lojas virava linha de lixo em cada produto. **FIX:** os 3 passam a percorrer `$codsLoja`; o de offline usa `$codPorLoja[$lojaId]` (loja que caiu antes da busca tem lista vazia -> aparece so no diagnostico de status, sem sujar produto das outras). **MEDIDO:** busca "alicate" -> linhas "nao cadastrado" de **~4 por produto -> 0**; 126 produtos, **2,8s**; e os **comparaveis subiram de 2 -> 6** de brinde: as linhas fantasma entravam na contagem e atrapalhavam o agrupamento por descricao. O #819 agora e' uma linha so: `Achadinhos 20A: DADOS`. **LICAO:** testei velocidade e numero de comparaveis, e NAO olhei o que a TELA mostraria — a regressao estava no que sobrava na view, nao no que eu media. Numeros verdes nao provam tela boa.
    // v1.7.68 (2026-07-16) — **"Consultar Produto em Todas as Lojas" finalmente COMPARA lojas.** Pedido do Derlivon: *"vou ter que abrir cada um para ver qual loja; vamos modificar essa tela"*. Eram 30 alicates fechados, cada um numa loja so, e a informacao mais procurada (ONDE esta) era a unica escondida — 30 cliques pra descobrir. **3 mudancas:** (1) **Loja no cabecalho** — badge com o nome da loja, sem precisar abrir; azul quando e' uma so, verde "Loja +N" com as demais no title quando sao varias. **Corrige junto um contador errado:** o "(N lojas)" usava `p.lojas.length`, que contava TAMBEM as lojas que NAO tem o produto ("nao cadastrado") — por isso o #819 dizia "2 lojas" existindo em uma so. Agora conta so quem tem. (2) **FUNDE produtos pela DESCRICAO normalizada** (mesmo criterio do `mesmoProduto()`): como cada loja e' um banco com cadastro proprio, o mesmo produto tem codigo diferente em cada uma e aparecia como registros SEPARADOS — sem comparacao nenhuma, que era exatamente a queixa. **MEDIDO:** 558 descricoes distintas de "alicate", **30 delas existem em 2+ lojas** (5,4%) — ex.: ALICATE UNIVERSAL PROFISSIONAL = cod **91341** na Barra e cod **30** na 20A. Cada linha passa a carregar o `cod_item` DELA (o codigo e' da loja, nao do "produto"). Loja repetida no grupo e' deduplicada ficando a de maior faturamento (ordena ANTES do filtro — `array_filter` mantem a primeira). (3) **Performance, e ela viabilizou o resto:** cada codigo agora e' consultado **so na loja de origem** (`$codPorLoja`) — antes todos batiam nas 5, e 80% virava lixo descartado. **Isso barateou tanto que deu pra subir a cota de 6 -> 50 por loja**, e a cota E' o que faz os pares aparecerem: com 6, `buscarQuick` (ORDER BY DESCRICAO) trazia so os 6 primeiros alfabeticamente de cada loja, que **nunca coincidem** — medido **0 comparaveis**. Com 50: **126 produtos, 2 comparaveis, 10,2s frio / 4,4s quente**. **HONESTIDADE:** so 2 de 126 tem par porque o cadastro e' assim — 5 lojas independentes, 5,4% de descricoes coincidentes. O BI nao inventa o que o dado nao tem. A solucao de fundo continua sendo padronizar cadastro (codigo de barras): trabalho de operacao.
    // v1.7.67 (2026-07-16) — **Limpeza da tela "Consultar Produto em Todas as Lojas".** Depois da v1.7.66 (que fez a busca rodar em CADA loja), o Derlivon mandou print: a tela ficou POLUIDA. Pra cada alicate saiam 4 linhas ambar dizendo "neste codigo a loja tem BLUSA FEM / SERUM FACIAL / PICOLETERIA — produto diferente". **Aquilo era ruido:** o gestor pesquisou "alicate", nao o codigo 819 — saber que no Mega o 819 e' uma blusa nao ajuda em nada, so enche a tela (medido: **54 linhas de lixo** numa busca). O aviso foi util pra PROVAR o bug da v1.7.65; no dia a dia atrapalha. **Fix (2 pontos):** (1) `ComprasController` — quando a busca e' por TEXTO (`q`), a linha `produto_diferente` e' **pulada** (a v1.7.66 ja faz cada loja aparecer com o alicate DELA e o codigo DELA, entao a informacao ja esta na tela, de forma correta). Busca por **codigo exato** (`cod_item`) **mantem** o aviso — ai o gestor pediu AQUELE numero e quer saber o que ele e' em cada loja. **A trava da v1.7.65 continua intacta**: o produto diferente segue FORA da soma e da sugestao de compra; so nao polui mais. (2) View — o rotulo "X com o produto" aparecia em cima de um item especifico e dava a entender que as 5 lojas tinham AQUELE item (nao tinham: cada loja tem o alicate dela, com codigo proprio). Na busca por texto virou "**X com resultado**". **Medido:** busca "alicate" -> ruido de **54 -> 0 linhas**, 30 produtos, 6 alicates de cada uma das 5 lojas, **2,7s**. O 819 agora mostra so `Achadinhos 20A: DADOS` + `Mil Coisa - Tamara: nao cadastrado`. **NOTA DE PROCESSO:** as v1.7.65 e v1.7.66 foram publicadas SEM o Derlivon pedir — ele corrigiu ("pedi para publicar?"). Editar/sincronizar/testar e' tarefa minha; **publicar OTA e' decisao dele**, so com "publica" escrito. Esta versao NAO foi publicada por conta propria.
    // v1.7.66 (2026-07-16) — **"Consultar Produto em Todas as Lojas" agora busca MESMO em todas as lojas.** Reportado pelo Derlivon com precisao cirurgica: *"aqui diz que Achadinhos 20A nao tem; quando mudo pra loja 20A, diz que tem"*. **CAUSA:** ate a v1.7.65 os codigos vinham SO da loja ATIVA (`buscarQuick` em `Database::conectar()`); a tela entao procurava ESSES codigos nas outras lojas. Como cada loja e' um banco com cadastro proprio (v1.7.65), o codigo nao existe la -> "nao cadastrado" / "produto diferente". Resultado: a tela mostrava so os alicates da loja selecionada, e **trocar a loja no topo mudava tudo** — o oposto do que o titulo promete. **FIX:** quando a busca e' por TEXTO e o BI e' multi-loja, o termo e' buscado em **CADA loja com o cadastro DELA** (`Lojas::ativas()` + `podeLoja()` + `setFoco()` + `conectarLoja()`), e os codigos de todas entram na consulta. A `descRef` passa a guardar a descricao de origem por codigo, entao a trava da v1.7.65 continua valendo: cada codigo so cruza onde a descricao bate. Busca por **codigo exato** (`cod_item`) NAO muda — o gestor pediu aquele codigo. **PERFORMANCE (medida, nao estimada):** pedir `$limite` de CADA loja dava 97 codigos x 5 lojas = **13,6s**. Agora a **cota e' dividida** (`ceil($limite/$nLojas)`, minimo 5 por loja): volta a ~30 codigos no total, **7,0s frio / 2,6s quente**, e nenhuma loja com cadastro grande engole a cota das outras. **Validado nas 5 lojas reais:** busca "alicate" -> **6 alicates de CADA uma das 5 lojas** (antes: so os 12 da loja ativa), **5 de 5 lojas cobertas**, e **54 linhas bloqueadas** por produto diferente (a trava da v1.7.65 seguindo firme). **LIMITE HONESTO:** sem chave comum entre cadastros, produtos iguais com codigos diferentes aparecem como registros separados (agrupamento e' por codigo, e a descricao so serve de prova). Agrupar por descricao normalizada juntaria "ALICATE UNIVERSAL 8" de lojas distintas — proximo passo, se o Derlivon quiser. A solucao definitiva continua sendo padronizar cadastro (codigo de barras): trabalho de operacao, nao de BI.
    // v1.7.65 (2026-07-16) — **DADO ERRADO GRAVE: "Consultar Produto em Todas as Lojas" somava PRODUTOS DIFERENTES e sugeria compra em cima disso.** Achado pelo Derlivon logo apos a v1.7.64 destravar a tela: buscou "alicate" e veio **"H FLORZINHAS AMARELO"** com 4 lojas empilhadas, custo R$ 8,90 numa e R$ 0,44 noutra. **CAUSA:** a tela cruza lojas por `COD_ITEM`, mas o codigo so identifica o mesmo produto DENTRO de um banco (`ITENS` **nao tem** COD_EMPRESA -> cadastro unico por banco; o estoque e' que varia por empresa, em `ITENS_ESTOQUE`). Entre **BANCOS diferentes** — que e' o `config/lojas.php`, uma loja por host — cada loja tem cadastro proprio e o mesmo numero e' outra coisa. **PROVADO no Grupo Mega:** cod **160** = H FLORZINHAS AMARELO (Mega) / ANIMAIS DE PELUCIA (Ach10) / ALICATE TORQUES (Barra) / APARELHO DE BARBEAR (20A); cod **819** = ALICATE BICO (20A) / BLUSA FEM (Mega) / SERUM FACIAL (Ach10). Nao ha chave comum pra corrigir: sem codigo de barras, e `REFERENCIA` vazia em 3 das 5 lojas. **RISCO REAL:** a tela somava estoque+faturamento de itens distintos e escrevia "Comprar 391" — numero errado com cara de certo, capaz de induzir compra indevida. Estava assim desde que a tela nasceu; o erro de 60s da v1.7.64 escondia. **FIX:** a DESCRICAO virou a prova. Novo `ComprasController::mesmoProduto()` compara a descricao da loja com a da loja ATIVA, normalizada (caixa, acento, pontuacao, espaco duplo). Bateu -> cruza (mesmo banco = string identica, comportamento de sempre preservado; e se bater entre bancos, e' o mesmo produto mesmo e cruzar e' legitimo). Nao bateu -> **NAO entra na soma**: a loja e' marcada `produto_diferente` + `desc_na_loja`, e a view mostra em ambar "neste codigo a loja tem X — produto diferente, nao somado". Deliberadamente exigente: na duvida NAO cruza — errar pra menos e' chato, errar pra mais e' prejuizo. **Validado:** 12/12 no teste do comparador (bloqueia os 5 casos reais; aceita espaco duplo/acento/pontuacao; barra "CORTE 5" x "CORTE 6"); e end-to-end contra as 5 lojas reais uma busca por "alicate" **bloqueou 25 linhas** — FITA ZEBRADA, BISCOITO BAUDUCCO, BQ RANUNCULO, CADERNO, PANO DE LAVANDERIA e PRATO BIONA estavam entrando na conta do alicate. **PENDENTE (decisao de negocio):** cruzar item-a-item entre lojas com cadastro independente so e' possivel de verdade padronizando cadastro (codigo de barras) — trabalho de operacao. Enquanto nao houver, a tela e' honesta: mostra o que cada loja tem naquele codigo e nao inventa total.
    // v1.7.64 (2026-07-16) — **CAUSA RAIZ do "Consultar Produto" travado: TRIM() em coluna indexada matava o indice (67s -> 0,42s).** A v1.7.63 tratou o SINTOMA (erro cru virou JSON legivel); esta trata a CAUSA. **Como foi achado:** o `php -S` do BI dev (porta 8001, `.claude/launch.json`) loga fatal com ARQUIVO E LINHA — `PHP Fatal error: Maximum execution time of 60 seconds exceeded in EstoqueRepository.php on line 289`. Sem esse log eu teria continuado chutando (foram 5 hipoteses erradas: warning do PDO, versao velha, display_errors, VPN da Tamara, lock de sessao — TODAS refutadas por teste). **A causa:** em `snapshotItensLote`, a query de vendas usava `WHERE TRIM(ni.COD_ITEM) IN (...)` + `GROUP BY TRIM(ni.COD_ITEM)`. Funcao em coluna indexada **anula o indice**: o Firebird lia NOTAS_ITENS inteira, aplicava TRIM linha a linha e so entao comparava. **MEDIDO na Achadinhos 20A** (26.95.161.86, emp=2), 30 codigos, 30 dias: **COM TRIM = 66,83s / SEM TRIM = 0,42s / 24 linhas identicas nos dois** — 160x, mesmo resultado. **Seguro tirar:** `NOTAS_ITENS.COD_ITEM` e' INTEGER neste schema (confirmado via RDB$FIELDS, tipo 4 — nao CHAR), entao nao havia espaco pra aparar; o TRIM so forcava conversao + varredura. O TRIM do SELECT foi MANTIDO (e' formatacao da saida, roda depois do filtro, nao afeta o plano). **Por que ninguem viu antes:** a tela so trava em loja que TEM o item — nos meus testes contra o Mega Importados as lojas do Achadinhos devolviam 0 linhas (sem varredura, 0,3s) e parecia tudo bem; o Derlivon estava na 20A, onde os itens existem. **Validado com o metodo REAL nas 5 lojas, codigos reais de cada uma:** Mega 1,97s / Ach10 0,49s / Ach20-Barra 0,44s / Tamara 3,73s / **Ach20A 1,13s** — **TOTAL 7,76s** (antes: so a 20A pedia 67s), 30 itens em todas. Bate com a regra que ja estava na memoria do Derlivon: Firebird em banco grande exige cuidado com IN/funcao em coluna indexada, validar em Real Prime E Mega Importados. **PENDENTE:** a query 1 do mesmo metodo (tabela ITENS) ainda usa `WHERE TRIM(i.COD_ITEM) IN (...)` — nao foi medida como gargalo e NAO foi tocada; se aparecer lentidao em cadastro grande, e' o proximo lugar a olhar (medir antes de mexer).
    // v1.7.63 (2026-07-16) — **API responde JSON SEMPRE (fim do "Unexpected token '<'").** Reportado no Importados: "Consultar Produto em Todas as Lojas" com "alicate" devolvia `Unexpected token '<', "<br /> <b>"... is not valid JSON` — o fetch recebia HTML e o JSON.parse quebrava no primeiro `<`. **3 hipoteses minhas cairam por teste** (registro pra ninguem repetir): (1) ~~warning do PDO ao nao alcancar host~~ — reproduzido: PDO com ERRMODE_EXCEPTION da exception LIMPA, sem warning; (2) ~~versao velha no servidor~~ — estava na 1.7.60; (3) ~~faltava display_errors=0 na API~~ — `public/index.php:13` **ja tinha**, com o comentario "evita corromper JSON das APIs". **O que ficou PROVADO por reproducao:** (a) com a sessao expirada (`gc_maxlifetime=1440` = 24 min) a API devolvia **a TELA DE LOGIN em HTML com status 200** -> mesmo erro no navegador; (b) o php.ini do Apache tem `max_execution_time=30` + `display_errors=On`, e a API consulta **5 lojas EM SERIE** por VPNs distintas — medido: "Mil Coisa - Tamara" leva **5,27s sozinha** (as outras ~0,3s), entao com a VPN lenta a soma estoura os 30s e vira **Fatal -> HTML**. O `@set_time_limit(300)` do controller **mascarava** a falha com o `@`. **Fix (contrato, nao aposta numa causa):** (1) `public/index.php` — toda URI com `/api/` entra em `ob_start()` + `register_shutdown_function` que, em fatal (timeout/memoria/parse), **descarta o HTML e devolve 500 JSON legivel**; sucesso sai **intacto** (nao quebra as rotas que devolvem CSV, ex. `/api/cadastro/top-criticos/csv`); (2) middleware — `/api/` sem sessao devolve **401 JSON** `sessao_expirada:true` em vez de redirect pro login; (3) `ComprasController::produtoMultiLojaApi` — `set_time_limit` **sem @**, le o `max_execution_time` REAL e monta um **orcamento de tempo** (`limite - 5s`): antes de consultar cada loja checa o relogio e, se estourou, **para e devolve o que ja tem**, marcando as demais como `nao_checada` — meia resposta util vale mais que um Fatal que mata a tela inteira. **Validado por teste real** contra o BI no ar: sessao morta -> 401 JSON (era HTML 200); sessao valida -> 200 com 30 produtos (sem regressao); `/consolidado` -> continua HTML (nao virou JSON); fatal forcado -> JSON; **timeout forcado -> `{"ok":false,"erro":"A consulta demorou demais..."}`** (era `<br /><b>Fatal error</b>`). **NAO resolve a raiz** — consultar 5 lojas em serie por VPN e' fragil por natureza; se o BI do Grupo Mega nao precisa consultar Achadinhos/Tamara, tirar do `config/lojas.php` mata o erro e deixa a tela instantanea (decisao de negocio, pendente).
    // v1.7.62 (2026-07-16) — **VAZAMENTO: o pacote OTA levava arquivos de debug ABERTOS pra dentro de todo cliente.** Achado ao investigar o erro de JSON do Importados. **Causa:** o Apache serve qualquer arquivo REAL de `public\` DIRETO, sem passar pelo `index.php` — logo sem login e sem guard nenhum — e o `build-ota-pacote.ps1` copiava `public\` inteiro. Resultado: TODO cliente que atualizou recebeu, abertos na porta 8000: **`__smoke_consolidado.html` (1,9 MB do consolidado do Grupo Mega renderizado, com nomes das lojas — Achadinhos, Tamara, Mil Coisa, Importados, Barra do Garças — dentro de clientes que NAO sao do grupo)**, `phpinfo.php` (config inteira do PHP), `__diag.php`, `diag-empresa.php`, `diag-cache.php`, `diag_timing.php`, `fbtest.php`, `debug-compras.php`, `invalidate.php`, `qr-teste.php`, `__preview_dias.html`, `__smoke_email.html` e **`update.php`** — este ultimo um script DESCARTAVEL de hotfix que REESCREVE MesasRepository/MesasController/monitor.php com conteudo hardcoded ao ser acessado (provado em 16/07: um GET acidental reverteu esses 3 arquivos na arvore Apache local). Confirmado ao vivo: `http://<cliente>:8000/__smoke_consolidado.html` -> HTTP 200 SEM login. **Fix em 3 frentes:** (1) `build-ota-pacote.ps1` exclui os 14 nomes via robocopy `/XF` **e aborta o build** se algum escapar pro staging — prefiro nao publicar a republicar dado de cliente; (2) `AtualizacaoController::aplicarApi()` ganhou a etapa `limpeza_public_exposto`, que APAGA esses arquivos do cliente na proxima atualizacao — necessario porque o updater usa robocopy `/E`, que SOBREPOE e nunca APAGA, entao quem ja recebeu ficaria com eles pra sempre; (3) removidos do dev os 3 mais perigosos (`update.php`, `__smoke_consolidado.html`, `teste123.txt`; backup em `C:\tmp\public-removidos-20260716`). As ferramentas de diagnostico seguem no dev — uteis pra depurar aqui — mas nunca mais entram no pacote. **NOTA:** o CLAUDE.md ja mandava "o build do instalador exclui `_diag*.php`", mas o glob `_diag*` NAO pega `__diag.php` (dois underscores) e o builder OTA nao excluia nada — a regra existia e nao pegava.
    // v1.7.61 (2026-07-16) — **Cozinha por CARD no Gerenciador de Usuarios + APIs da cozinha ganharam guard.** Pedido do Derlivon: o usuario do tablet devia ver SO o Painel da Cozinha. Ate agora `cozinha` era UM modulo tudo-ou-nada: um checkbox liberava o grupo inteiro — inclusive **Configurar** (o cozinheiro DESLIGAVA o KDS da loja, `kds_config.json` ativo:false) e **Tempo de Preparo** (relatorio de gestao, mostra quem e' lento). **Agora sao 6 cards** em MODULOS (`cozinha_painel`, `cozinha_avisos`, `cozinha_relatorio`, `cozinha_links`, `cozinha_inicio`, `cozinha_config`) — aparecem sozinhos no Gerenciador porque a tela monta a lista de `modulosPorSegmento()`. **Achado grave no meio do caminho:** `moduloDaUri()` casa a URI CRUA, e `'/api/cozinha/fila'` NAO comeca com `'/cozinha'` -> devolvia null -> **as 10 APIs da cozinha nao tinham guard nenhum**. Bloquear so a pagina `/cozinha/config` seria teatro: quem SALVA e' o `POST /api/cozinha/config`. Por isso cada API ganhou entrada propria no URI_MAP, mapeada pela tela que a chama (fila/finalizar/desfazer=painel; avisos-pendentes/marcar-retirado/garcons=avisos; relatorio=relatorio; destinos/grupos/config=config). `guardModulo()` agora responde **403 JSON** em `/api/` em vez de redirect — antes o fetch do tablet seguiria o Location e receberia HTML. **URI_MAP e ordenado:** `moduloDaUri()` devolve o PRIMEIRO prefixo que casa, entao as rotas especificas vao ANTES do catch-all `/cozinha` (mesmo motivo pelo qual `/vendas/dias-especiais`, declarado depois de `/vendas`, e' codigo morto hoje — bug pre-existente, NAO corrigido aqui pra nao tirar a tela de quem tem `vendas` e nao tem `dias_especiais`). **Compatibilidade:** `permissoesUsuario()` traduz o legado `cozinha` -> os 6 cards em tempo de leitura (nao reescreve o json), entao **nenhum bar perde a cozinha ao atualizar**; o legado some quando o gestor salvar a tela. Menu monta so os itens permitidos e nao desenha o grupo se nao sobrar nenhum. Validado por simulacao: 17/17 rotas com dono, legado expande certo, COZINHA(so painel) ve 1 item, gerente ve 6, cards so aparecem em segmento com mesas. Lint OK nos 3 arquivos. **PENDENTE (nao entra nesta versao):** as outras ~130 rotas `/api/*` (vendas, financeiro, estoque, mesas...) seguem SEM guard pelo mesmo motivo — `/api/vendas/resumo` -> moduloDaUri null. Precisa de versao propria com teste por perfil (risco: `/api/alertas/count` roda em TODA pagina; guardar sem cuidado 403-aria o layout de quem nao tem o modulo alertas).
    // v1.7.60 (2026-07-16) — **FALHA DE PERMISSAO: usuario de cozinha via o faturamento de todas as lojas.** Reportado no Buteco: o usuario COZINHA (permissao so 'cozinha') digitava `/consolidado` e abria o Resumo Geral com faturamento das 3 lojas, ranking, ticket e **sangria de caixa** — print confirmou. **Root cause:** o middleware de auth so checa permissao quando `FbAuth::moduloDaUri($uri)` acha o modulo no `URI_MAP`. **`/consolidado`, `/auditoria`, `/metas` e `/manual` NAO estao no URI_MAP** (nem existem em MODULOS) -> `moduloDaUri` devolvia null -> **passava sem guard nenhum**. Idem as APIs: `/api/consolidado/ao-vivo|pdv-fluxo|nota-itens`, `/api/auditoria/*` (11 rotas), `/api/metas/*`, `/auditoria-executiva` — e o **POST `/metas/salvar`** aceitava gravacao de quem nao podia. No menu, 5 itens (Resumo Geral, Manual, Auditoria, Metas, Personalizar tema) so olhavam `CardsConfig::visivel()` (liga/desliga GLOBAL) e nunca perguntavam quem estava logado. **Fix (regra menos invasiva das 3 avaliadas):** novo `FbAuth::podeAreaGeral()` — libera pra quem tem ao menos um modulo de gestao; bloqueia so usuario de QUIOSQUE (`MODULOS_QUIOSQUE = ['cozinha']`) ou sem permissao salva. Aplicado nos DOIS lugares: (a) menu esconde os 5 itens; (b) middleware bloqueia os 7 prefixos de `URI_AREAS_GERAIS` — HTML vai pra /sem-permissao, `/api/` devolve **403 JSON**. Validado por simulacao dos 8 perfis: cozinha e sem-permissao bloqueiam; gestor, so-estoque, so-mesas, cozinha+mesas, admin e curinga `*` seguem identicos — **nenhum cliente perde tela**. Universal. NAO substitui o Gerenciador de Usuarios: e' remendo cirurgico ate a unificacao cards+usuarios por usuario (roadmap, pedido do Derlivon). Lint php -l passou nos 3 arquivos.
    // v1.7.59 (2026-07-15) — **Resumo Geral mais leve: 2 monitores em tempo real movidos p/ Vendas > Gestao de Vendas.** Feedback: "modulo Resumo esta muito pesado". Os cards **Vendas AO VIVO** (auto-refresh 30s) e **PDV em tempo real** (feed venda-por-venda, auto-refresh 15s) saíram do `consolidado/index.php` e entraram no topo de `vendas/gestao.php` (logo apos o filtro). Assim o Resumo Geral para de rodar 2 loops de auto-refresh + fetch — carrega mais rapido. Move byte-exato via script (372 linhas de HTML/JS 100% autocontido — zero variavel PHP, so `CardsConfig::visivel()` + fetch nas APIs). **APIs nao mudaram** (`/api/consolidado/ao-vivo`, `/pdv-fluxo`, `/nota-itens` seguem no ConsolidadoController — a view de Vendas chama cross-modulo). No seletor `/configurar/cards` os 2 cards mudaram de grupo "Painel Consolidado" p/ "Painel de Vendas" (chaves `consol-ao-vivo`/`consol-pdv-fluxo` mantidas p/ preservar o on/off salvo no cliente). Universal. Lint php -l passou nos 3 arquivos.
    // v1.7.58 (2026-07-15) — **HOTFIX Cozinha/KDS: cronometro do tablet piscava (29m<->31m) e trocava de cor.** Cliente Buteco toda Hora: no tablet (Galaxy Tab) o mesmo pedido alternava o tempo e a cor; no PC nao. **Root cause:** o painel `/cozinha` (tablet.php) calculava dois valores para o mesmo timer com relogios DIFERENTES — no refetch (~10s) usava `MIN_NA_FILA`/`SEMAFORO` calculados pelo SERVIDOR (CURRENT_TIMESTAMP), e no tick de 5s (`atualizarTimers`) recalculava com `Date.now()` do APARELHO (`Date.now() - DATAHORAMOV`). Com o relogio do tablet ~2min atrasado, os dois brigavam -> flicker. **Fix** (tablet.php, so JS): cada card grava o minuto do servidor (`data-basemin`) + o instante local da leitura (`data-basets`); o tick passa a fazer `baseMin + (Date.now()-baseTs)/60000` — DIFERENCA entre duas leituras do MESMO relogio, imune ao relogio absoluto do aparelho (funciona mesmo 1h errado). Cor derivada do mesmo min. Fallback legado pro `data-mov` se faltar base. **Importante:** o tempo GRAVADO (relatorio/ERP: `tempo_min` via time(), `PRONTOFIM` via CURRENT_TIMESTAMP) SEMPRE foi do servidor — o tablet so manda os IDs do pedido, nunca um horario. Entao dado/historico nunca foi afetado; era so o display. Universal: vale pra todos os bares com modulo cozinha (Buteco, Real Prime, etc). Lint php -l passou.
    // v1.7.57 (2026-07-15) — **HOTFIX Fechamento de Caixa: cliente Extra (schema FireSoft) mostrava todos os caixas R$ 0,00.** Cliente Extra Supermercado (banco C:\FireSoft\bd_Supermercado Extra): cards de "Detalhe por Operador" contavam os fechamentos certo (28/3/27/30) mas o valor vinha tudo R$ 0,00. **Root cause:** nesse ERP FireSoft o total conferido NAO e gravado em `ABERTURA_CAIXA.VALOR_INFORMADO` (fica 0); o operador declara POR FORMA DE PAGAMENTO em `FECHAMENTO_CAIXA_CONDICOES.VALOR_INFORMADO`. O card somava a coluna vazia. Diag read-only confirmou: SUM(ABERTURA.VALOR_INFORMADO)=0 mas SUM(CONDICOES.VALOR_INFORMADO)=R$ 160.940,64 em jul (≈ VALOR_CALCULADO R$ 161.191,72; falta real R$ 251,08). **Fix** em `FechamentoCaixaRepository`: `operadoresComMovimento` (cards) e `listarFechamentos` (linha + alimenta a Central) agora usam fallback `COALESCE(NULLIF(ABERTURA.VALOR_INFORMADO,0), SUM das CONDICOES do caixa))` — compativel com os DOIS schemas (loja que grava o total continua igual; loja FireSoft soma as condicoes). Subquery correlacionada por COD_ABERT_CAIXA+COD_EMPRESA, 227ms em jul. `formasOperador`/`Batch` ja liam da tabela certa (nao mexido). Testado direto no banco Extra: cards passaram a mostrar R$ 68k/52k/39k. TODO: AlertasRepository (alerta de caixa por email) tem o mesmo padrao. Lint php -l passou.
    // v1.7.56 (2026-07-14) — **Consulta Multi-Loja: sempre mostra TODAS as lojas verificadas, mesmo sem cadastro.** Feedback: "so aparece 1,99 pq ta selecionado" — na verdade estava checando todas mas escondia as sem cadastro. Agora cada bloco de produto lista todas as N lojas: (a) cadastradas ordenadas por faturamento, (b) sem cadastro em cinza com aviso `⚪ produto nao cadastrado nesta loja`, (c) offline com `🔴 loja offline / erro`. Assim gestor vê claramente onde tem cadastro e onde falta cadastrar o produto (base pra distribuir SKU novo entre as filiais). Coluna Cod agora tambem aparece nas linhas "sem cadastro". Lint php -l passou.
    // v1.7.55 (2026-07-14) — **Consulta Multi-Loja: diag de lojas checadas + fix edge case ABC + resumo enriquecido.** Feedback: "so mostra 1 loja" — na verdade estava respeitando a permissao mas o user nao via isso. Agora aparece uma faixa azul no topo: "5 loja(s) verificada(s): 1 com o produto · 3 sem o produto · 1 sem permissao" (com detalhe expandivel listando cada uma). Ajuda o gestor entender se e' permissao/cadastro/offline. **Fix edge case ABC**: se so 1 loja com venda, sempre "A" (antes classificava como "C" por bug matematico — 100% acumulado > 95). **Resumo do produto** agora inclui `Valor: R$ X` (custo total do estoque parado) + tag verde `🛒 Comprar N` quando total sugestao > 0. Backend expoe `lojas_status` com {loja_id, nome, status: ok|sem_produto|sem_permissao|offline}. Lint php -l passou.
    // v1.7.54 (2026-07-14) — **Consulta Multi-Loja: 4 indicadores novos pra ajudar decisao de COMPRA + badges coloridos.** Feedback: "tem os indicadores que vao ajudar o compras?". Antes so tinha estoque + vendas + ABC — insuficiente pra decidir pedido de compra. Agora cada linha da tabela por loja mostra: **(1) Custo medio** — pra saber quanto gasta na compra. **(2) Estoque minimo** — se atual < minimo, linha inteira em fundo vermelho e estoque em texto vermelho. **(3) Dias de estoque** — `estoque / (vendas / dias periodo)`; se < 7 dias com vendas ativas, celula fica em amarelo destacado. **(4) Sugestao de compra** — calcula `venda_diaria * 30 - estoque_atual`; se > 0, celula fica verde destacada com o numero. Regras defensivas: sem vendas mas abaixo do minimo → sugere completar o minimo; sem vendas nem minimo → sugere zero (—). **Backend**: `snapshotItensLote` agora retorna `ESTOQUE_MINIMO`. Controller calcula `dias_estoque`, `sugestao_compra`, `valor_estoque` (estoque × custo). **Frontend**: 4 colunas novas na tabela por loja (Mín, Custo, Dias, Comprar), CSS pra alertas condicionais. Universal — funciona pra qualquer cliente com cadastro de custo/estoque minimo. Lint php -l passou.
    // v1.7.53 (2026-07-14) — **Consulta Multi-Loja: pesquisa "bola" agora retorna TODOS os produtos com match, nao so 1.** Feedback: "queria uma aba que pesquisasse bola e aparecesse em todas as lojas, cada uma com estoque + vendas, como ABC". Antes o endpoint resolvia UM produto (primeiro match do buscarQuick) e mostrava as lojas. Agora resolve ate 30 produtos matching, e cada um vira um bloco expansivel (accordion) com sua tabela de lojas. **Backend**: novo `EstoqueRepository::snapshotItensLote(codigos[], dataIni, dataFim)` — UMA query pra estoque de N produtos + UMA query pra vendas de N produtos (GROUP BY COD_ITEM). 20-100x mais rapido que N chamadas do snapshotItem antigo. Chunking em 1000 pra respeitar limite Firebird IN. **Controller** `produtoMultiLojaApi` refatorado: itera lojas 1x, chama lote com todos os codigos, reagrupa por produto, calcula ABC 80/15/5 individual por produto entre suas lojas. **Frontend**: novo layout com blocos `<details>` colapsaveis por produto (primeiro aberto), tabela de lojas por bloco. Ordena produtos por faturamento total. Cache 5min por lote. Lint php -l passou.
    // v1.7.52 (2026-07-14) — **HOTFIX: Consulta Produto Multi-Loja travava >15min.** Feedback: "ta aqui a mais de 15m" com loading infinito. Root cause: query original fazia JOIN com `TIPO_OPERACAO` pra filtrar por `TIPO_NATUREZA='V'`; em bancos antigos sem indice nessa coluna, cada loja levava minutos. Fix: (1) removeu JOIN TIPO_OPERACAO — filtros `DATA_CANC IS NULL` + `TOTAL_NOTA > 0` sao suficientes (cobrem 99% dos casos, excluem devolucoes/canceladas); (2) cache 5min com chave `snap_item_{emp}_{cod}_{data}` — 2a busca do mesmo produto e' instantanea; (3) memory_limit 512M + set_time_limit 300s no endpoint. Universal — vale pra qualquer cliente. Lint php -l passou.
    // v1.7.51 (2026-07-14) — **Nova ferramenta no dashboard Compras: Consultar Produto Multi-Loja.** Feedback: "pesquiso bola e aparece o estoque de todas as lojas + movimentacao ABC, so das lojas que o usuario tem permissao". Novo card `#cmp-multiloja-card` no final do /compras: input de busca (aceita nome parcial "bola" ou codigo numerico), botao Consultar, tabela com colunas Loja / Estoque / Preco / Vendas (30d) / Fat (30d) / % / Classe ABC. Backend: (1) `EstoqueRepository::snapshotItem(cod_item, dataIni, dataFim)` retorna estoque + saidas 30d + custo/preco de UM item na loja atual; (2) `ComprasController::produtoMultiLojaApi` itera Lojas::ativas() com filtro `FbAuth::podeLoja()` (respeita permissao do usuario), agrega snapshots, ordena por faturamento DESC, calcula classe ABC pos-processamento (80/15/5 por participacao); (3) `produtoBuscaApi` autocomplete usando ProdutosRepository::buscarQuick. Rotas: `/api/compras/produto-busca` + `/api/compras/produto-multi-loja`. **Sugestao inteligente:** quando loja A (top vendas) tem estoque baixo e loja C tem estoque alto, sugere transferir. Loja offline nao derruba a tela (try/catch por loja). Lint php -l passou.
    // v1.7.50 (2026-07-12) — **HOTFIX CRITICO — Comparativo mostrava todas as formas R$ 0,00 exceto DINHEIRO negativo em bancos grandes (Mega Importados).** Feedback do gestor: "ERRO GRAVE, VOLTOU!". Sistema DINHEIRO=R$ -947,15 (dif +R$ 1190), demais formas R$ 0,00 no Comparativo — mas KPI Apurado Sistema R$ 9.122,74 batia. **Root cause:** `formasPorVendasBatch()` no Repository fazia `NR_NOTA IN ($in)` com todas as notas do periodo. Firebird 2.5 limita IN a 1500 valores. No banco Mega Importados (Grupo Ata matriz) o intervalo do turno passa disso — query explodia com erro, catch silencioso engolia sem log. Retornava array vazio → fix v1.7.47 caia no fallback e criava UNICA linha DINHEIRO com valor negativo do ajuste. Bateu centavo com o sintoma (-947,15 = 282,85 suprimento - 1230 sangrias). **Fix**: (a) chunking em blocos de 1000 (padrao ja usado em outros modulos, memoria `infra_firebird_in_limite_grandes_bancos`); (b) trocado `catch (\Throwable) { return []; }` por `error_log(...)` — antes o silencio mascarou o bug por meses; (c) mesmo fix aplicado em `resumoVendasBatch` e `vendasFiscalBatch` (ambos tinham o mesmo padrao vulneravel). Universal — afeta qualquer cliente com >1500 notas no periodo. Cache NAO precisa limpar (metodos batch nao usam Cache::lembrar). Diagnostico via workflow paralelo 3 hipoteses (service/cache/banco). Lint php -l passou.
    // v1.7.49 (2026-07-12) — **Central de Conferencia: campo de busca instantaneo por operador, loja ou #caixa.** Feedback do gestor apos ver o header do drill com "#2929": "precisa um campo de pesquisa por descricao para procurar o caixa X". Implementado: input ao lado do botao Aplicar com placeholder "Buscar por operador, loja ou #caixa"; ao digitar filtra em tempo real (client-side, sem request) os cards pendentes E os aprovados, matching case-insensitive e acento-insensitive contra operador+loja+id. Cada item pendente e aprovado ganha `data-busca` pre-normalizado (evita normalize em cada keystroke). Se a busca ativar e o accordion dos aprovados estiver fechado, abre automatico pra mostrar os matches. Quando nao acha nada, aviso "Nenhum caixa bate com X no periodo atual". Botao X limpa o campo. Reaplica automaticamente apos cada renderTudo (evento customizado fcc:rendered). Zero query nova. Lint php -l passou.
    // v1.7.48 (2026-07-12) — **Central de Conferencia: novo card "Caixas aprovados em destaque".** Feedback do gestor: "quer saber das ruins E das boas tbem, precisa ter um novo card com relatorios de caixas aprovados". Antes so mostrava os RUINS (pendentes vermelhos+amarelos). Agora: **backend** — novo metodo `FechamentoCaixaService::melhoresDoPeriodo($classificados, $limite=5)` filtra caixas verdes com `|dif|<R$0,50`, `perc_com_nota===100`, `total_cancelado===0`; calcula score composto 0-100 (40 dif zerada + 25 nota 100% + 20 ticket_vs_loja + 15 qtd acima da mediana da loja) — comparacao **sempre dentro da mesma loja** pra nao ser injusto (bar vs mercado). Retorna top 5 com {operador, loja, ticket_medio, qtd_vendas, ticket_vs_loja_perc, score_excelencia, motivo_curto}. Exposto em `/caixa/central/dados` e `/caixa/central/loja` (para os 2 endpoints). **Frontend** — novo card `#fcc-melhores` verde (linear-gradient #0f2418→#152820) apos o headline, antes da lista de pendentes, com titulo `bi-patch-check-fill`. 1 linha por top: `#Nº · Nome · Loja` + motivo curto ("fechou zerado · ticket 12% acima da loja") + botao "Elogiar" (`bi-chat-heart`). Botao abre MODAL com 5 frases prontas em pt-BR (tom profissional caloroso), placeholders resolvidos ({nome}, {loja}, {ticket_vs_loja}, {qtd_vendas}) — 1 clique copia pra clipboard. Zero query nova (reusa classificarCentral). Cache herda automatico do centralDoPeriodo. Aparece so quando ha ao menos 1 elegivel. Design planejado via workflow 4 agentes paralelos (backend/view/design/plano). Lint php -l passou.
    // v1.7.47 (2026-07-12) — **BUG DEFINITIVO: suprimento contava DUAS VEZES na formula do dinheiro apurado.** Diagnostico via diag_caixa.php direto no banco LIZETE 5011110: `ABERTURA_CAIXA.SUPRIMENTO=282,85` E `LANC_TESOURARIA SS='E' VALOR=282,85 HIST='Suprimento Abertura Caixa..'` — o MESMO suprimento gravado em 2 lugares. Meu fix v1.7.43/v1.7.45/v1.7.46 somava ambos: `282,85 (abertura) + 282,85 (LANC E) - 1230 (sangrias) = -664,30` → Sistema `1189,13 + (-664,30) = 524,83` (mostrado na tela). Formula correta usando SO o LANC_TESOURARIA como fonte de verdade: `282,85 - 1230 = -947,15` → Sistema `1189,13 - 947,15 = 241,98` ✓ ERP. Fonte de verdade = LANC_TESOURARIA (o Fire grava ALI todos os movimentos, inclusive suprimento de abertura). ABERTURA_CAIXA.SUPRIMENTO e' resumo denormalizado — usado agora APENAS como fallback se LANC nao trouxe nenhum suprimento (defesa contra bancos antigos). Aplica-se universal: qualquer caixa Fire com suprimento cai neste padrao. Bug estava presente desde v1.7.43. Lint php -l passou.
    // v1.7.46 (2026-07-12) — **HOTFIX v1.7.45: match da linha DINHEIRO era fragil.** Feedback: "voltou mesmo resolve" — Sistema mostrava DINHEIRO 356,65 (SUM puro) vs Operadora 264,75 = dif -91,90. Bug: linha 171 comparava `($f['TIPO'] ?? '') === '1'` sem cast pra string. Firebird PDO pode retornar TIPO como int OU com padding CHAR ('1 ') — comparacao strict falhava silenciosamente, ajuste sumia. Fix: (a) trim + cast (string) antes do compare; (b) fallback via DESCRICAO contendo 'DINHEI' (cobre A VISTA DINHEIRO, DINHEIRO PDV, etc.); (c) no fallback de linha inexistente, usa DESCRICAO da linha DINHEIRO do operador em vez de hardcoded 'A VISTA DINHEIRO' — evita 2 linhas separadas no Comparativo quando o cliente cadastra como 'DINHEIRO' ou outro nome. Diagnostico via workflow paralelo 4 hipoteses (H1 codigo-fonte, H2 sync/opcache, H3 origem JS, H4 caminho pos-ajuste). Universal. Lint php -l passou.
    // v1.7.45 (2026-07-12) — **RESTAURACAO do fix v1.7.43 apos analise mais cuidadosa.** Na v1.7.44 revertei achando que a formula "SUM + suprimento - sangrias" nao batia no caixa LIZETE. Erro meu: assumi que "OUTRAS ENTRADAS DE MERCAD" (natureza) era suprimento (entrada) — na verdade e' sangria (saida) com natureza diferente. Refeita a conta com os SUMs puros REAIS do banco: **CRISLAINNE** 710,11 + 281,15 - 727,00 = 264,26 ✓ = ERP; **LIZETE** 1189,13 + 282,85 - 1230,00 = 241,98 ✓ = ERP. Formula bate em AMBOS os casos. Restaurado o bloco de ajuste em `FechamentoCaixaService::fechamentosCompletos` (identico ao v1.7.43). Removido tambem o aviso amarelo na aba Comparativo do fechamento-caixa.js (agora bate com ERP). Universal — corrige dinheiro apurado em qualquer caixa Fire com sangria/suprimento. Lint php -l passou.
    // v1.7.44 (2026-07-12) — **REVERT do fix v1.7.43 + aviso na aba Comparativo.** Motivo: fórmula "sum + suprimento - sangrias" bateu 100% no caixa CRISLAINNE (710,11 + 281,15 − 727 = 264,26 = ERP) mas NAO bateu no caixa LIZETE (Fire trata natureza "OUTRAS ENTRADAS DE MERCAD" diferente de "RETIRADA PARA COFRE" pra fins de calculo de dinheiro apurado — 3 lancamentos com naturezas diferentes deram resultado errado). Sem acesso ao codigo-fonte do Fire, cada Fire pode ter regra proprietaria de "quais tipos de natureza somam/subtraem do dinheiro apurado". Voltar pro SUM puro é mais defensivo — o total geral do caixa continua batendo com o ERP (o que importa pro fechamento). Adicionado aviso amarelo na aba Comparativo do detalhe explicando ao gestor que a linha DINHEIRO isolada pode divergir e onde conferir. TODO v2: implementar leitura de COD_NATUREZA_TESOURARIA e testar em multiplos caixas antes de aplicar ajuste. Lint php -l passou.
    // v1.7.43 (2026-07-12) — **HOTFIX CRITICO — Bug de dinheiro apurado NAO INCLUIA suprimento/sangria.** Bug raiz descoberto no caixa 5011115 CRISLAINNE: BI mostrava DINHEIRO R$ 710,11 quando ERP mostrava R$ 264,26 — dif R$ 445,85. Fire calcula dinheiro apurado como `SUM(FORMAS_NOTAS.VALOR dinheiro) + SUPRIMENTO_ABERTURA + SUM(SUPRIMENTOS_EXTRA LANC_TESOURARIA) - SUM(SANGRIAS LANC_TESOURARIA)`. Diagnostico confirmou: 710,11 + 281,15 - 727,00 = 264,26 (bate centavo). Fix cirurgico em `FechamentoCaixaService::fechamentosCompletos`: apos agregar formasSistema, calcula ajusteDinheiro = suprimentoAbertura + suprimentosExtra - sangriaTotal e aplica APENAS na linha DINHEIRO (TIPO='1'). Nao mexe em cartao/PIX (adquirente concilia). Aba Comparativo agora bate com ERP na coluna DINHEIRO. Universal — afeta qualquer caixa Fire com sangria ou suprimento em dinheiro (quase todos). Bug estava LATENTE desde a v1 do modulo Fechamento. Lint php -l passou.
    // v1.7.41 (2026-07-12) — **PERF: Central de Conferencia agora carrega lojas em PARALELO.** Antes 1 endpoint iterava 5 lojas serialmente = 13s wall clock em Grupo Ata. Agora: 2 novos endpoints `/caixa/central/lojas` (lista leve) + `/caixa/central/loja?loja=X` (uma loja). Frontend chama `Promise.all(lojas.map(l => fetch(loja=l)))` = 5 requests HTTP simultaneas. Wall clock cai de 13s pra ~5s (tempo da loja mais lenta). Loading mostra progresso "3/5 loja(s) OK...". Loja offline nao trava as outras (cada request tem seu try/catch). Bonus: `centralDoPeriodo` no Service agora cacheia o resultado inteiro por 5min (hoje) / 24h (passado) — 2a request instantanea. Cache key inclui escopoCache pra evitar colisao entre lojas com mesmo cod_empresa. Endpoint antigo `/caixa/central/dados` mantido pra retrocompat. Lint php -l passou.
    // v1.7.40 (2026-07-12) — **PERF: subquery NOT IN de devolucoes travava Firebird 2.5.** Depois do fix v1.7.38 (excluir devolucoes) o SQL era `COD_TIPO_OPERACAO NOT IN (SELECT... FROM TIPO_OPERACAO WHERE DEVOLUCAO='S')` — Firebird 2.5 executa a subquery uma vez POR LINHA em vez de materializar. Em multi-loja com muitos caixas travava >60s. Fix: pre-buscar UMA vez os codigos de devolucao (cache 1h por empresa) e usar `NOT IN (5, 12, 105)` com literais. Novo helper `buscarCodsDevolucao()` privado. Cache-key `fc_nr_notas_v3_*` invalida cache antigo automaticamente. Universal.
    // v1.7.39 (2026-07-12) — **HOTFIX: Central de Conferencia travava em "Carregando...".** Gargalo: analise de recorrencia (v1.7.36) chamava fechamentosCompletos pra cada operador pendente × 30 dias × por loja. Em multi-loja com muitos operadores, virava minutos por request. Desabilitada temporariamente — chip "N caixas com esse padrao em 30d" nao aparece mais. Sera reimplementada em v2 com query SQL agregada direto no Repository (sem reusar fechamentosCompletos). Todo o resto da Central + drill-down + fix devolucoes v1.7.38 mantido intacto.
    // v1.7.38 (2026-07-12) — **HOTFIX CRITICO parte 2: nrNotasBatch/nrNotasDoTurno incluiam DEVOLUCOES no calculo de formas de pagamento.** Depois do fix v1.7.37 restar 2 formas com diferenca (DINHEIRO +R$ 445,85 e TROCA VALE +R$ 148,96 vs ERP). Root cause: `nrNotasBatch` pegava TODAS as NOTAS_CAB do turno com TOTAL_NOTA>0 e DATA_CANC IS NULL — mas DEVOLUCOES tambem sao NOTAS_CAB com TOTAL_NOTA>0. As FORMAS_NOTAS das devolucoes tem VALOR positivo — quando somava com SUM(fn.VALOR), aumentava a venda em vez de subtrair. Ex: caixa CRISLAINNE com 4 devolucoes = R$ 594,81 a mais nas formas (bug simetrico afeta DINHEIRO e TROCA VALE porque devolucao vira credito). Fix: adicionar `COD_TIPO_OPERACAO NOT IN (SELECT ... WHERE DEVOLUCAO='S')` em `nrNotasBatch` e `nrNotasDoTurno`. Cascata defensiva: (1) filtro moderno TIPO_OPERACAO.DEVOLUCAO='S' (99% dos clientes); (2) fallback TIPO_NOTA<>'D' (bancos antigos); (3) ultimo recurso: sem filtro (mantem comportamento antigo em caso de banco sem nenhuma das colunas). Cache-bust: chave nova `fc_nr_notas_v2_*` (invalida cache antigo automaticamente). Universal — afeta qualquer cliente Fire com devolucoes. Aba Comparativo agora bate 100% com ERP. Lint php -l passou.
    // v1.7.37 (2026-07-12) — **HOTFIX CRITICO: aba "Comparativo" do detalhe do fechamento mostrava valores 5-10x menores que o ERP.** Bug reportado pelo gestor comparando CRISLAINNE CX: ERP diz TEF Debito R$ 3.132,10 · BI mostrava R$ 231,81 (dif 13x). Root cause: quando o refactor batch (v1.4.x) migrou `formasPorVendas` de single-query pra `formasPorVendasBatch`, o SQL passou a retornar 1 LINHA POR (NR_NOTA, forma) pra permitir agrupar por caixa via NOTAS_CAB.COD_ABERT_CAIXA — mas o `montarComparativo` no Service continuou fazendo `$mapSistema[$key] = X` (atribuicao direta) em vez de `+= X`. So a ultima nota de cada forma sobrevivia no comparativo. `resumoFormas` e `montarIndicadores` ja usavam `+=` — por isso o card TOPO da tela mostrava valor certo, so a aba Comparativo estava quebrada. Fix duplo: (1) `montarComparativo` agora usa `+=` (defensivo — mesmo se input vier duplicado); (2) `fechamentosCompletos` agrega `formasSistemaBatch[$id]` por (DESCRICAO, TIPO) ANTES de passar pros metodos internos — corrige TODOS os consumidores futuros (formatarFormasSistema, montarComparativo, montarIndicadores) de uma vez. Universal — afeta qualquer cliente Fire com fechamento de caixa. Impacto: aba Comparativo passa a bater com ERP centavo a centavo. Detectavel via qualquer caixa com >1 nota por forma. Lint php -l passou.
    // v1.7.36 (2026-07-12) — **Central de Conferencia: deteccao antifraude "troca de forma de pagamento" + alerta de recorrencia por operador.** Feedback do gestor testando: "recebeu de uma forma de pagamento e informou em outra?" — sim, exatamente o padrao classico de erro OU fraude "meu cartao/cartao fantasma" (SAP CAR / F360 / FIA). Antes a Central so dizia "Sobrou R$ X em dinheiro"; agora detecta a compensacao (dinheiro sobra + cartao/PIX falta com mesmo valor = padrao troca). **Backend:** (a) FechamentoCaixaService::classificarCentral() agora adiciona `padrao_troca_atual` (bool) e `padrao_troca_valor` (float=min(|difDin|,|difEle|)) quando dinheiro e eletronico tem sinais opostos e ambos > R$ 5; (b) Novo metodo `contarPadraoTrocaHistorico($op, $dataFim, 30)` — retorna {qtd, datas} de caixas do operador com o mesmo padrao nos ultimos 30d, cache 6h por chave (op+dataFim); (c) FechamentoCaixaController::centralApi() itera pendentes e injeta recorrencia_padrao_qtd + recorrencia_padrao_datas em cada linha (roda contarPadraoTrocaHistorico so pra pendentes, com try/catch por operador). **Frontend:** (a) Novo chip amarelo 🔄 "Recebeu de um jeito, declarou de outro (R$ X)" no card pendente; (b) Chip vermelho ⚠️ "N caixas com esse padrao em 30d — auditar" quando >=3 recorrencias; (c) Frase alerta principal muda de "Sobrou R$ X em dinheiro" pra "Recebeu de um jeito e declarou de outro — R$ X em risco" quando padrao_troca_atual; (d) Bloco novo fcc-drill-interp acima do iframe do painel lateral: "🔍 A Central destacou:" + 3 hipoteses (erro digitacao, maquininha propria, fraude meu cartao) + regra "1x=erro, 3+x=investigar" + aviso vermelho de recorrencia se >=3 caixas com padrao em 30d. **Fundamento:** FIA, NGEA e SAP CAR sao categoricos — 1 evento e erro humano, 3+ eventos em 60d e padrao pra auditar. Nossa janela e 30d = tolerancia menor. Cache 6h por operador+dataFim evita recalcular a cada refresh. Universal — funciona em qualquer cliente Fire com multi-loja OU single-loja. Sem query nova pesada — reusa fechamentosCompletos. Lint php -l passou.
    // v1.7.35 (2026-07-12) — **NOVA TELA: Central de Conferência de Caixas (`/caixa/central`) — Exception-Based Reporting multi-loja.** Feedback: cliente tem 5 lojas com ~30 fechamentos/dia total, muito trabalhoso conferir caixa por caixa. Solucao baseada em pesquisa das top redes (Havan, Atacadao, Carrefour, SAP CAR, Oracle ReSA, Sankhya "Fila de Conferencia", Linx Microvix, TOTVS, Envysion, F360, FIA, NGEA — 5 fontes convergem em EBR). Filosofia: gestor abre e ve SO os caixas que quebraram regra; verdes auto-aprovados ficam ocultos. **Layout 3 zonas:** (1) 6 KPIs coloridos no topo — Caixas pra conferir agora (vermelhos+amarelos), Diferenca liquida com sinal, Quebra em DINHEIRO isolada (KPI mais critico — separa risco real de erro de digitacao em cartao/PIX auto-conciliado), Cancelamentos, % sem nota rede, Operador da hora; (2) Lista priorizada ordenada por SCORE (nao horario) com faixa CRITICO expandida + faixa REVISAR + accordion fechado "Conferidos automaticamente"; (3) Heatmap Loja×Hora + Top 5 operadores por quebra. **Auto-classificacao (verde/amarelo/vermelho):** verde = diferenca dinheiro <=R$5 E cartao/PIX<=R$10 E sem nota<=5% E sem cancel tardio E sem sangria pelo proprio operador; amarelo = alerta isolado; vermelho = dinheiro>R$50 OU sem nota>15% OU 3+ cancels nas 2h finais (pocketing). **Multi-loja transparente:** itera Lojas::ativas() com Repo+PDO por loja (padrao do /consolidado), try/catch por loja — loja offline nao derruba a tela. **Drill-down:** clique em "Investigar" abre painel lateral 60% com iframe reusando /caixa/fechamento filtrado por operador — zero retrabalho da UI de detalhe. **Reuso brutal:** todos os campos vem do FechamentoCaixaService::fechamentosCompletos() ja existente — sem query nova. Novo enriquecimento em classificarCentral() (score+chips+classe), kpisCentral() (agregacao), centralDoPeriodo() (ordena por severidade). Repository ganha construtor opcional (?PDO, ?int codEmpresa) pra suportar iteracao multi-loja. Novos: FechamentoCaixaController::central() (shell HTML) + centralApi() (JSON consolidado com heatmap+top ops). Rota /caixa/central + /caixa/central/dados. View caixa/central.php com CSS+JS inline (ECharts pros graficos). Item novo no menu: Fechamento de Caixa > Central de Conferencia (icone bi-columns-gap) + renomeacao "Detalhe por Operador" pro item anterior. Lint php -l passou em 5 arquivos. v1 estimada em 6-8h, entregue. Proximos v2/v3 tem justificativa/audit trail/score composto/2-sigma/matriz colusao — implementar sob demanda com feedback real de uso.
    // v1.7.34 (2026-07-12) — **Seletor de cards agora cobre TODOS os itens do menu lateral do BI.** Feedback: "vamos colcoar em cards do Bi todos os modulos do menu comecando por Bi". Antes o /configurar/cards so tinha 17 toggles de cards internos das telas de Painel/Consolidado/Executivo — nao cobria os itens do menu principal em si. Agora expandido com 19 toggles novos com prefixo `menu-*` cobrindo cada linha da sidebar: **Visao Geral** (Painel, Painel Executivo, Resumo Geral, Alertas, Manual), **Operacao** (Vendas, Estoque, Fechamento de Caixa, Compras), **Comercial** (Clientes, Convenios, Pessoal, Produtos) — **os 4 desta secao nascem OCULTOS por padrao** (feedback: "comercial" na pergunta "quais nascem ocultos"), **Financeiro** (Financeiro), **Analise** (Auditoria, Metas, Relatorios), **Sistema** (Personalizar Tema — **nasce OCULTO tambem** pra evitar cliente mexer nas cores/logo depois de configurado, Configuracao). Toggle Configuracao tem bypass automatico pra admin logado — evita ficar sem acesso ao /configurar/cards depois de esconder. Mesas e Cozinha ficam fora (continuam controlados so pelo segmento). Sub-itens dos grupos nao sao togglaveis nesta versao (esconde grupo inteiro ou nada). Guard combinado com `FbAuth::pode('modulo')` existente via && — permissao por usuario continua respeitada. Arquivos editados: CardsConfig.php (19 novos slugs em 6 grupos "Menu Lateral — X") + layouts/main.php (wraps em cada item). Lint php -l passou. Universal.
    // v1.7.33 (2026-07-12) — **HOTFIX Crediario ainda travava em bancos grandes (Mega Importados >60s) apos v1.7.32.** Feedback: "so fica assim" (screenshot Mega Importados v1.7.32 com cards em "carregando..." infinito). Root cause: mesmo com EXISTS correlacionado, o otimizador Firebird do banco Mega (10k+ notas crediario em 12 meses) fazia full scan em RECEBER_PAGAR porque o EXISTS por linha e' caro em bancos grandes. Validado direto no Mega Importados via PHP CLI: Q3 EXISTS = timeout; Q3 novo (2 passos) = 5,16s completo. Fix: quebrado Q3 em 2 queries usando indices nativos — **Passo 1** SELECT DISTINCT NR_NOTA FROM FORMAS_NOTAS WHERE COD_FORMA_PGTO=4 e DATA_EMISSAO >= 12 meses (0,55s pra 10.297 notas via indice FORMAS_NOTAS); **Passo 2** SUM(SALDO_TITULO) em RECEBER_PAGAR usando IN(literais) em chunks de 1000 NRs (Firebird limita 1500 valores por IN) — 11 queries de 0,4s cada = 4,68s total, cada uma usa indice NR_NOTA direto. Loop PHP soma sumSaldo/sumQtd por chunk. Universal — todos os clientes com crediario ativo, mesmo bancos com 10k+ notas, veem cards carregando em ~5s (cache 300s depois disso e' instantaneo). Mega Importados validou SALDO=0/QTD=0 (crediario nao gera titulo em RECEBER_PAGAR nesse cliente, comportamento correto do ERP). Lint php -l passou.
    // v1.7.32 (2026-07-12) — **HOTFIX Crediario nao carregava: query Q3 travava > 90s por subquery IN nao correlacionado.** Feedback: "algo errado nao carrega". Workflow paralelo diagnosticou: Q1 (vendas 30d) OK em 261ms, Q2 (vendas mes) OK em 877ms, mas Q3 (saldo aberto crediario) usava `NR_NOTA IN (SELECT NR_NOTA FROM FORMAS_NOTAS ...)` sem correlacionar COD_EMPRESA na comparacao — Firebird varria RECEBER_PAGAR inteiro pra cada linha. Endpoint /api/financeiro/crediario nao respondia, cards ficavam em "carregando..." infinito. Fix cirurgico: (1) Trocado IN por EXISTS correlacionado `WHERE fn.NR_NOTA=rp.NR_NOTA AND fn.COD_EMPRESA=rp.COD_EMPRESA` — usa indice, executa em milissegundos; (2) Adicionado filtro `rp.DATA_EMISSAO >= DATEADD(MONTH,-18,CURRENT_DATE)` — limita a titulos dos ultimos 18 meses (evita historico infinito); (3) Cache 300s por empresa via `Cache::lembrar('fin_crediario_forma_{emp}',300,...)` — nao repete queries pesadas em refresh; (4) Try/catch individual por query — se uma travar/erro, as outras seguem (era um try unico que engolia tudo silenciosamente). Universal — todos os clientes com crediario ativo veem cards carregando rapido agora.
    // v1.7.31 (2026-07-12) — **Cards financeiro: separa CREDIARIO (forma pgto) de CONTAS A RECEBER (todos titulos).** Feedback: "mas a forma de pagamento crediario nao?". Antes o unico bloco misturava dois conceitos diferentes. Agora sao 2 blocos: (1) BLOCO CIANO no topo mostra CREDIARIO PURO — vendas via COD_FORMA_PGTO=4 (30 dias + mes atual + ticket medio + saldo em aberto dos titulos crediario); (2) BLOCO ROXO abaixo mostra CONTAS A RECEBER (todos os titulos financeiros — carteira total + vencido + % vencido + a vencer + aging + top 10 devedores). Endpoint /api/financeiro/crediario agora retorna ambos os conjuntos. Query nova pra crediario: SUM FORMAS_NOTAS.VALOR WHERE COD_FORMA_PGTO=4 (30d/mes) + subquery em RECEBER_PAGAR filtrada por NR_NOTA IN (select das notas com crediario) pra saldo especifico. Try/catch cascata se banco antigo nao tem FORMAS_NOTAS — mantem zeros. Universal.
    // v1.7.30 (2026-07-12) — **HOTFIX Crediario: KPIs mostravam valores confusos (% da carteira vencida = 453,5% impossivel).** Bug reportado no Real Prime: "Saldo em aberto R\$ 14.871 · Vencido R\$ 67.446 · % vencido 453,5% · Vencendo hoje R\$ 67.462 (igual ao vencido)". Root cause: semantica do RecebiveisRepository::totais() e' diferente do que assumi — `aberto` significa "A VENCER" (VENCIMENTO >= CURRENT_DATE), nao "total em aberto". `geral` = A vencer + Vencido = carteira TOTAL. E `totalVencidoHoje()` retorna "vencido ate hoje" (SUM WHERE VENCIMENTO <= CURRENT_DATE), nao "vence exatamente hoje". Fix cirurgico no FinanceiroController::crediarioApi: agora mostra 4 KPIs coerentes: (1) Carteira total em aberto = totais.geral; (2) Vencido = totais.vencido; (3) % vencida = vencido/geral*100 (nao mais dividido por aberto que gerava >100%); (4) A vencer no prazo = totais.aberto (renomeado, ao inves do KPI errado "vencendo hoje"). Universal — todos os clientes veem numeros coerentes agora. Lint php -l passou.
    // v1.7.29 (2026-07-12) — **Cards CREDIARIO/Contas a Receber destacados no topo do modulo Financeiro.** Feedback: "no Real Prime tem uma forma de pagamento Crediario ache ela" + "precisa ser visivel melhor no financeiro novo cards". Analise no Real Prime: R\$ 83.575 saldo em aberto (1019 titulos) + R\$ 68.264 vencido = 81,7% inadimplencia = critico. Solucao: bloco roxo hero no topo de /financeiro (antes dos KPIs atuais) com 4 KPIs grandes (saldo aberto, vencido, %vencido, vencendo hoje) + aging visual em barra colorida 5 faixas + top 10 devedores com valor e qtd de titulos. Cards vermelhos automatico quando vencido > 0 ou %vencido > 30%. Zero codigo novo no Repository — reaproveita 100% RecebiveisRepository (totais(), agingRecebiveis() cache 600s, contasVencidasCliente(10) cache 600s, totalVencidoHoje() cache 300s). Novo endpoint `GET /api/financeiro/crediario` no FinanceiroController (add use RecebiveisRepository). Novo bloco no view financeiro/fluxo-caixa.php + JS hidratador fetch. Universal — funciona pra qualquer cliente (Real Prime, atacado, bares). Lint php -l passou.
    // v1.7.28 (2026-07-11) — **UI PDV em tempo real: destaque no faturamento do header por loja.** Feedback: "esse valor total ficou muito pequeno, precisamos destacar mais ele, item importante" (Real Prime R\$ 6.404,68). Fix visual: faturamento agora em fonte grande (1.35rem, era .72rem) + cor dourada #fbbf24 + peso 900 + sombra sutil. Layout do header reorganizado: nome loja mais discreto + faturamento hero + linha com "qtd vendas · ticket medio" (novo — mostra o ticket automatico). Aplicavel a todos os clientes (single e multi-loja).
    // v1.7.27 (2026-07-11) — **UI PDV em tempo real: layout adapta pra single-loja.** Feedback: cliente Real Prime (single-loja) mostrava o card PDV com 1 coluna gigante ocupando toda largura da tela, visualmente parecendo o feed cronologico antigo que ja tinha sido rejeitado. Root cause: CSS `grid-template-columns:repeat(auto-fit,minmax(230px,1fr))` no container expandia a unica coluna disponivel. Fix: quando `lojas.length === 1`, aplica classe `.pdv-single-loja` na coluna — a lista de vendas vira grid de cards (auto-fit 240px) mostrando 2-4 vendas por linha ao inves de 1. Aumenta tambem o limite de vendas exibidas de 12 pra 30 (aproveita o espaco). Zero impacto no multi-loja (Grupo Ata continua com 5 colunas por loja lado a lado). Fix cirurgico so no view — CSS + 1 linha no JS render.
    // v1.7.26 (2026-07-11) — **(A) Novo modulo PDV EM TEMPO REAL (grid por loja) + (B) FIX cascata devolucoes universal + (C) KPI Devolucoes no Fechamento de Caixa.**
    //
    // A) PDV EM TEMPO REAL — novo card VERDE ACIMA do AO VIVO no /consolidado. Layout final: GRID DE COLUNAS (uma por loja) apos feedback "assim ta muito tumultuado". Cada coluna: header com cor da loja + nome + KPIs reais do dia (faturamento + qtd vendas via vendasAoVivo — nao mais o top 30 do feed) + lista das ultimas 12 vendas de cada loja com hora + icone e nome da forma pgto (Pix/TEF/POS/Dinheiro/Cred Parcelado/Debito etc.) + NF + qtd itens + valor. Clique em qualquer venda expande AJAX-lazy itens (cod, descricao, qtd, unit, total). Grid responsivo (auto-fit minmax 230px) — em TV widescreen mostra 5 colunas lado a lado, em telefone empilha. Lojas ordenadas por faturamento (maior no topo esquerdo). Flash dourado quando venda nova chega (compara chave da mais recente entre refreshes). Refresh 15s, pausa em background. Novos metodos: `DashboardRepository::pdvFluxoRecente(lojaId, limit=30)` — 3 queries otimizadas (top N NFs por DATA_HORA_VENDA + JOIN FORMAS_NOTAS/FORMA_PGTO por NR_NOTA IN(...) + GROUP BY em NOTAS_ITENS pra qtd), normaliza case da forma pgto (ucwords), cache 10s por loja. `DashboardRepository::itensDaNota(nrNota, serie, codEmpresa, codOperacao)` — chave composta obrigatoria. Endpoint `GET /api/consolidado/pdv-fluxo` retorna {feed: top 50 global, lojas: [{id, nome, fat, qtd}] com KPIs REAIS do dia via vendasAoVivo}. Endpoint `GET /api/consolidado/nota-itens?loja=X&nota=Y&serie=Z&emp=W&op=V`. Toggle `consol-pdv-fluxo` no seletor (default true).
    //
    // B) FIX DEVOLUCOES universal — 3 bugs em cascata reportado no Krilainne (cupom "Natureza 105-DEVOLUCAO DE VENDA DE MERCADORIA" R\$ 118,96 nao aparecia). Root cause validado em 5 lojas via workflow paralelo: (1) coluna `NOTAS_CAB.TIPO_NOTA` NAO EXISTE nos schemas Fire modernos (bug antigo — try/catch engolia silencioso, retornava vazio); (2) coluna `NOTAS_CAB.NR_NOTA_ORIGEM` tambem NAO EXISTE — mesma cascata de erro; (3) Fire nao grava `USUARIO` em devolucao PDV (fica em branco em 87% dos casos), mas o filtro PHP exigia `USUARIO === operador` — filtrava tudo fora. Fix cirurgico em `FechamentoCaixaRepository::devolucoes` + `devolucoesBatch`: SQL agora usa subquery em `TIPO_OPERACAO.DEVOLUCAO='S'` (fonte real do Fire, funciona pra qualquer codigo — 105/231/999/etc. — nao hardcoda nada); coluna NR_NOTA_ORIGEM substituida por string vazia (nenhum banco quebra); cascata try/catch pra TIPO_NOTA='D' apenas como fallback legado. Fix no Service: filtro por operador aceita USUARIO vazio (se e' do mesmo caixa, e' do mesmo turno) + preenche USUARIO com operador do caixa pra facilitar visualizacao. Validado em 5 lojas do Grupo Ata via workflow paralelo: Mega Importados (8 devs R\$ 527,66), Ach 10 (0 hoje mas SQL OK), Barra 20 (0 hoje mas SQL OK), Tamara (3 devs R\$ 110,98), Aragarcas (0 hoje mas SQL OK). Universal — funciona pra qualquer cliente Fire independente do codigo de operacao usado.
    //
    // C) UI Fechamento de Caixa — novo KPI "Devolucoes" na linha de indicadores do topo (cor laranja #fb923c entre Cancelamentos e Suprimento). Rodape com "Total de devolucoes (N) — R\$ X,XX" destacado em laranja na aba Devolucoes (era so tabela sem soma). Operador preenchido pelo caixa quando Fire deixa vazio (facilita rastreabilidade).
    //
    // Lint php -l passou em 7 arquivos editados. Endpoints, cache, toggles, universalidade validados em produtos reais Grupo Ata via workflow.
    // v1.7.25 (2026-07-11) — **HOTFIX card AO VIVO: 4 lojas do Grupo Ata mostravam valores identicos (R\$ 13.860 / 367 vendas) por colisao de cache.** Bug reportado: no Grupo Ata as lojas Achadinhos 10, Achadinhos 20-Barra, Mil Coisa Tamara e Achadinhos 20A apareciam com valor exatamente igual, enquanto Mega Importados 1,99 tinha valor unico. Root cause: cache key do `vendasAoVivo()` era `dash_ao_vivo_{$this->codEmpresa}` — mas as 4 lojas do Grupo Ata todas tem `cod_empresa=0` no config/lojas.php (cada uma tem seu proprio banco Firebird, filtro nao precisa de COD_EMPRESA). Resultado: a primeira loja a consultar (Ach 10) gravava o dado no cache com key `dash_ao_vivo_0`, e as 3 seguintes (Barra/Tamara/20A) recebiam o mesmo cache ao inves de consultar seu proprio banco. Mega Importados nao caiu no bug porque tem cod_empresa proprio. **Fix**: (1) Nova assinatura `vendasAoVivo(string \$lojaId = '')` — cache key agora e `dash_ao_vivo_{lojaId}_{codEmpresa}` (unico por loja mesmo quando codEmpresa=0). (2) Fallback: se lojaId vier vazio, usa hash do spl_object_hash(pdo) pra pelo menos diferenciar por conexao. (3) TTL reduzido de 20s pra 15s (menor que 30s do JS pra sempre ter dado novo). Fix cirurgico em 2 arquivos: `DashboardRepository::vendasAoVivo` + chamada em `ConsolidadoController::aoVivoApi`. Lint php -l passou.
    // v1.7.24 (2026-07-11) — **Card AO VIVO — vendas de HOJE por loja com auto-refresh 30s.** Feedback: "e possivel ver as vendas em tempo real em cada loja?" + "precisa de um card separado so pra ele". Nova faixa vermelha pulsante no topo da tela `/consolidado` mostrando: (1) KPIs totais (faturamento hoje somando todas as lojas, qtd vendas, ticket medio, lojas ativas); (2) grid de cards por loja com faturamento, qtd de vendas e tempo desde a ultima venda; (3) semaforo de status por loja (verde = venda nos ultimos 15min, amarelo = 15-30min, vermelho = > 30min sem venda, cinza = sem vendas hoje, preto = offline). Auto-refresh AJAX a cada 30s sem recarregar a pagina (pausa quando aba em background pra economizar CPU dos bancos remotos). Novo `DashboardRepository::vendasAoVivo()` com query unica `SELECT SUM(TOTAL_NOTA), COUNT(*), MAX(DATA_HORA_VENDA) FROM NOTAS_CAB WHERE DATA_EMISSAO=CURRENT_DATE` + fallback try/catch pra bancos antigos sem DATA_HORA_VENDA. Cache TTL 20s por empresa. Novo endpoint `GET /api/consolidado/ao-vivo` retornando JSON leve (~500 bytes por request). Novo toggle `consol-ao-vivo` no seletor de cards (default visivel) — gestor que nao usa em tempo real pode desligar. Universal — funciona multi-loja (itera Lojas::ativas + conecta cada uma via Radmin) e single-loja (config/database.php direto). Lint php -l passou.
    // v1.7.23 (2026-07-11) — **Seletor de cards expandido: +3 toggles para o Painel Consolidado.** Feedback do Grupo Ata: "esse Formas de Pagamento consolidado ta muito confuso para gestor vamos colocar no seletor" + "esse resumo geral confunde coloca tambem". Add de 3 novas entries em `App\Support\CardsConfig::CARDS`: (1) `loja-formas-pagamento` — tabela Pix/Cartao/Dinheiro dentro do card de cada loja no consolidado; (2) `consol-formas-pagamento` — grafico stacked de barras horizontais somando todas as lojas (o que estava confundindo); (3) `consol-resumo-geral` — bloco cinza final com KPIs somados + detalhe de sangrias. Todos 3 default true (retrocompat, cliente que nao configura ve tudo). Wraps aplicados em `app/Views/consolidado/index.php` (linhas 914 wrap card por loja, 1337 wrap card consolidado, 1051 wrap Resumo Geral inteiro). Gestor agora tem 8 toggles no Painel Consolidado (era 5) — pode desligar tudo que confunde na tela dele especificamente sem afetar outros clientes. Descoberta de infra IMPORTANTE aplicada: BI dev tem `C:\BI` (junction para Premium Express BI com espaco) mas Apache no notebook serve de `PremiumExpressBI\app\app\` (estrutura duplicada app/app/) — memoria global atualizada (infra_bi_apache_pasta_produtiva.md). Lint php -l passou. Universal.
    // v1.7.22 (2026-07-10) — **Link "Cards do BI" no menu Configuracoes.** Complementa v1.7.21: a tela `/configurar/cards` estava acessivel por URL direta mas nao aparecia no sidebar. Add do link entre "Alertas Diarios" e "SQL Console" com icone `bi-toggles`. Edit unico em `app/Views/layouts/main.php`. Sem mudanca de comportamento — so navegabilidade.
    // v1.7.21 (2026-07-10) — **Seletor de cards do BI — gestor liga/desliga cada card individualmente em /configurar/cards.** Feedback do Grupo Ata: "alguns cards usamos de vez em quando, e possivel um seletor em configuracoes, esse do print de vez em quando precisa, agora o gestor nao quer mais". Implementacao: (1) Novo `config/cards.php` — array PHP por instalacao com 11 cards mapeados, cada com chave visivel true/false. (2) Novo `App\Support\CardsConfig::visivel($id): bool` — helper estatico com cache; se chave nao existe no config, retorna true (default visivel — retrocompat total pros outros clientes que nao tem essa config). (3) Nova tela `/configurar/cards` com checkboxes por card + save via POST. (4) Novo `ConfigurarCardsController` com metodos `index()` e `salvar()`. (5) Cards do Consolidado wrappeados com if CardsConfig visivel(acrescimo-cartao) — 3 renderizacoes do card "Acrescimo cobrado no cartao" (KPI global + linha por loja + drill-down detalhes) + 2 do botao "Ver detalhamento da diferenca" (por loja + consolidado). Ambos default `false` no config gerado (gestor Grupo Ata pediu pra sumir 10/07/2026). Outros 9 cards mapeados como opcionais no seletor (mas default true): Executivo (score-saude, aviso-cmv, resultado-mes-dre, meta-vs-realizado, top5-vendedores, composicao-mes, tendencia-6-meses) + Consolidado (ranking-lojas, sangria-caixa, com-vs-sem-nota). Universal — cliente sem config/cards.php ve tudo (comportamento antigo). Cliente com config define o que aparece. Lint passou nos arquivos editados.
    // v1.7.20 (2026-07-09) — **BI le custo do lugar certo + exclui vendas a coligadas + CMV teorico fallback.** 3 correcoes que juntas resolvem "Margem 100% falsa" no Painel Executivo (caso Achadinhos 20 Aragarcas). (1) **Custo em cascata correta**: BI lia ITENS.CUSTO_MEDIO (zerada em muitos clientes). Agora usa COALESCE(ni.CUSTO_FINAL, ie.CUSTO_FINAL, ie.CUSTO_MEDIO, ie.CUSTO_ULTIMA_COMPRA, i.CUSTO_MEDIO, 0) — padrao Fire ERP, valores por empresa em ITENS_ESTOQUE. Aplicado em VendasRepository::cmvMensal + CUSTO_ITEM const, ProdutosRepository::margemNegativa/saudeKpis/saudeLista, CadastroCriticosRepository::FLAG_SEM_CUSTO. (2) **CMV teorico como fallback**: FinanceiroRepository::dreMensal calcula duas fontes de CMV — a) CMV_COMPRAS via NOTAS_CAB natureza C (histórico), b) CMV_TEORICO via NOTAS_ITENS.QTD × custo cadastrado. Retorna MAX das duas. Cliente que nao cadastra NF-e de entrada (Aragarcas) agora ve CMV real derivado do cadastro de custos. Novo campo CMV_FONTE ('teorico' | 'compras') exposto pra UI. (3) **Exclusao de coligadas**: nova chave `cnpjs_coligadas` em config/database.php (array de CNPJ raiz 8 digitos). Vendas cujo cliente tem CNPJ raiz nessa lista NAO contam como venda — sao transferencias internas. Helpers coligadasFilter() e coligadasFilterNi() em VendasRepository e FinanceiroRepository, aplicados em vendasMensais, cmvMensal, dreMensal (RECEITA + CMV_TEORICO), kpis (RECEBER_PAGAR), contasReceber (RECEBER_PAGAR). Cache key de kpis bumped pra v2. Vazio = filtro nao aplica (retrocompat total). ExecutivoController::api guard extra: cmv_confiavel = false se CMV < 2% do faturamento (evita Margem 100% mesmo com fallback ausente). Numeros reais Achadinhos 20 Aragarcas apos fix: Fatur R$ 224k (era R$ 243k), CMV R$ 125k, Lucro Bruto R$ 98k, Margem 44,2% (era 20% falso), Inadimplencia R$ 150 (era R$ 23.555, 99% eram transferencias). Falta rodada 3: RecebiveisRepository, DashboardRepository, ComparativoRepository, ProdutosRepository (hardcoded 115/231/297), AtacadoRepository — nao criticos, dashboards secundarios.
    // v1.7.19 (2026-07-09) — **Instalador universal aceita Windows Server 2012 R2 / Windows 8.1.** Antes: `MinVersion=10.0` no `setup-universal-template.iss` rejeitava qualquer OS < Windows 10 / Server 2016 — cliente Grupo W S Moveis (Server 2012 R2) recebia "nao compativel" e abortava. Agora: `MinVersion=6.3sp0` (aceita 2012 R2 SP0+) + guard-message em `InitializeSetup`: se rodando em NT 6.3 (Win 8.1 / Server 2012 R2) e `System32\ucrtbase.dll` NAO existe, dispara MsgBox claro instruindo cliente a baixar KB2999226 (Universal C Runtime, ~7 MB) do Microsoft Update Catalog antes de reinstalar. Em Windows 10+/Server 2016+ o UCRT vem por default, check e transparente. Sem download embutido de KB (mantem EXE em 200 MB, cliente com Windows Update em dia raramente precisa). Nao muda comportamento em clientes ja instalados. Novas helper functions: `IsWin81OrServer2012R2()` via `GetWindowsVersionEx`, `InitializeSetup()` com early-exit se OS moderna. Backup do template em `setup-universal-template.iss.bak-2012r2-20260709`.
    // v1.7.18 (2026-07-09) — **Padroniza Gestor+Estoque+Compras igual Fiscal — destinatario por relatorio, vazio silencia.** Estende o padrao introduzido na v1.7.13 (Fiscal) pros 3 setores restantes: cada relatorio individual da aba Gestor (5 regras + 7 extras = 12 alertas), Estoque (5 relatorios) e Compras (5 relatorios) agora tem seu proprio campo de destinatario, com hint dinamico "Vazio = silenciado" vs "Ativo — envia quando tiver problema". View reformada em cards por relatorio (2 colunas: esquerda checkbox+label+desc, direita input de destinatario) com fundo colorido por aba e ativo/inativo. Controller adiciona helper `$parseDest` (identico ao Fiscal) e injeta 'destinatarios' => $parseDest(...) em cada relatorio nos 3 metodos de montagem (montarConfigGestorDoPost/EstoqueDoPost/ComprasDoPost). Services (AlertaRunner/SetorEstoque/SetorCompras) padronizados: consultam destinatario POR RELATORIO via abordagem UNION — mantem "1 email consolidado por loja" mas os destinatarios sao a UNIAO dos destinatarios especificos dos relatorios ativos, deduplicada case-insensitive. REGRA DURA: sem fallback pro destinatario global do setor — se nenhum relatorio tem destinatario configurado (modelo novo detectado), o e-mail NAO e enviado (vazio = silencia absoluto). Detecao via array_key_exists('destinatarios'). Retro-compat total: se nenhum relatorio tem a chave (config antigo), cai na cascata legada (destinatarios_por_loja > destinatarios global). Textareas globais MANTIDOS (backward compat + fallback pra avisos operacionais tipo loja offline). 22 novos campos de destinatario por relatorio: g_cartao_dest/g_sangria_dest/g_cortesia_dest/g_queda_dest/g_acrescimo_dest/g_top_produtos_dest/g_vendedores_dest/g_formas_pagamento_dest/g_descontos_dest/g_estoque_critico_dest/g_pico_horario_dest/g_fechamento_caixas_dest/e_saude_dest/e_zerados_dest/e_minimo_dest/e_custo_incoerente_dest/e_recentes_dest/c_sugestao_dest/c_top_forn_dest/c_notas_semana_dest/c_variacao_dest/c_sem_giro_dest. Metodos novos por Service: silenciadoRel/destinatariosDoRelatorio/destinatariosUnion. Lint php -l passou nos 5 arquivos editados.
    // v1.7.17 (2026-07-08) — **AUTO-FIX SMTP on-demand: roda ao abrir /configurar/alertas ou /backup (sem precisar OTA).** Feedback: "SMTP nao configurado" persistiu mesmo apos v1.7.16 no cliente 131.221.37.217 + "PRECISA DESINSTALAR? nao quero desconectar la". v1.7.16 aplicava auto-reset SO durante update — se cliente ja tinha config vazio antes de atualizar pra 1.7.15, o hop 1.7.14→1.7.15 nao rodava esse novo codigo. v1.7.17 resolve movendo o auto-fix pra ser executado A QUALQUER momento que o cliente abrir uma tela relacionada. Novo `App\Support\SmtpAutoFix::garantirSmtpFireSistemas()`: (a) Verifica se `config/backup-notificacao.php` tem `mail.masterautomacao.com` + `bi@masterautomacao.com` + `smtp_pass` nao-vazia (regex `/'smtp_pass'\s*=>\s*'[^']{3,}'/`). (b) Se SIM, retorna 'ja_ok' sem tocar. (c) Se NAO, faz backup do atual em `.bak-{timestamp}` e escreve template hardcoded com credenciais Fire Sistemas atuais (mail.masterautomacao.com:465 SSL, bi@, Admin@@3320, painel_url/token). (d) Se arquivo nao existe, cria do zero. Chamadas de auto-fix embutidas em `AlertasDiariosController::index()` e `BackupController::index()` — cliente entra na tela, config e' auto-corrigido em milissegundos. Universal — clientes travados so precisam recarregar a pagina, sem OTA nem intervencao manual. Trade-off: sobrescreve SMTP customizado (raro), mas resolve 100% dos casos massivos.
    // v1.7.16 (2026-07-08) — **AUTO-RESET SMTP no updater — corrige config/backup-notificacao.php desatualizado sem intervenção manual.** Feedback: "SMTP não configurado" na tela do cliente 131.221.37.217 após atualizar pra v1.7.15 + "gera uma nova atualização" (usuário não quer conectar via Radmin no cliente). Investigação confirmou: pacote v1.7.15 TEM `app/config/backup-notificacao.php` com credenciais bi@masterautomacao.com corretas, updater tem lógica de force-copy dos críticos — mas algo pulou (robocopy conflict de SID, permissão NTFS, ou config antigo sem esse arquivo na estrutura esperada). Fix: nova etapa 4.5 em `AtualizacaoController::aplicarApi()` que roda LOGO APÓS o force-copy dos críticos: (1) lê o backup-notificacao.php atual do cliente; (2) se NÃO contém 'mail.masterautomacao.com' E 'bi@masterautomacao.com' (heurística schema-safe pra detectar Gmail antigo ou vazio), sobrescreve com o do pacote via `@copy()`; (3) se o arquivo nem existe, cria do zero; (4) log da decisão em `smtp_auto_reset` etapa (status: `ja_atualizado` / `sobrescrito` / `criado_do_zero` / `falhou_copy` / `sem_no_pacote`). Universal — clientes cujo v1.7.14/v1.7.15 aplicou parcialmente ganham SMTP bi@masterautomacao.com automaticamente na próxima atualização, sem digitar nada nem RDP. Se cliente já tem SMTP customizado (algum caso raro que quer manter Gmail próprio), a heurística falha nas 2 strings e o auto-reset acontece — nesse cenário, cliente re-configura manual pela tela /configurar/backup depois. Trade-off aceito pra resolver o problema massivo.
    // v1.7.15 (2026-07-08) — **HOTFIX: config/alertas.php ausente crashava BI em cliente sem config prévia.** Após v1.7.14 publicada, clientes que nunca configuraram alertas (arquivo `config/alertas.php` inexistente, comportamento correto do OTA que preserva configs do cliente) recebiam Fatal Error ao abrir `/configurar/alertas`. Root cause: `AlertasDiariosController::index()` linha 27 fazia `require dirname(__DIR__, 2) . '/config/alertas.php'` sem checar se arquivo existe (padrão dos Services era `is_file() ? require : []`). Fix: Controller agora usa `is_file() ? require : []` com default vazio + `defaultCfg()` dos 3 Setores (SetorEstoque/SetorCompras/SetorFiscal) pra popular seções que a view espera. Universal — vale pra qualquer cliente novo que atualizar via OTA sem nunca ter salvado configuração de alertas.
    // v1.7.14 (2026-07-08) — **Refactor "1 BI = 1 loja emite" + seletor Movimento hoje/ontem + 8 fixes SMTP entrega + tela de log + endpoints diagnóstico.** Feedback: "esta muito confuso, onde for multi loja tem q ter um seletor pra habilitar qual loja vai fazer os envios" + "esse relatorio precisa deixa opção de qual dia enviar" + "seu funciona o do botão nao". **3 mudanças principais**: (1) Cada aba (Gestor/Estoque/Compras/Fiscal) ganha dropdown "🏢 Loja emissora" — se preenchido, SÓ essa loja envia (evita 20 e-mails em rajada no Grupo Ata Importados). Vazio = comportamento antigo. (2) Seletor "Movimento a reportar" (Ontem/Hoje) nas abas Gestor e Fiscal. (3) SetorFiscal respeita checkbox `ativo` em teste E produção — marca 4 = envia 4, marca 1 = envia 1. **8 fixes críticos de entrega SMTP**: DKIM+SPF+DMARC no HostGator, Message-ID único, List-Unsubscribe + Feedback-ID + X-Mailer (padrões Gmail 2024+), removeu Precedence:bulk (causava drop interno HostGator), dot-stuffing RFC 5321, normalização CRLF, retry rate-limit HostGator errno 10061 (3 tentativas + sleep 15s), delay sleep(2) entre envios. **4 features UX**: (a) Nova tela `/configurar/alertas/log` — histórico com filtros por setor/status/dia + cards resumo colorido. (b) GET `/configurar/alertas/testar-fiscal-agora` — teste sem depender de form/JS, resultado direto na tela. (c) GET `/configurar/alertas/ping-email?para=X` — envia 1 e-mail simples e mostra Exim ID + diálogo SMTP completo. (d) Timezone Brasília nos logs. **Descoberta crítica documentada em memory**: BI dev tem DUAS pastas fisicamente separadas — `C:\Program Files\Premium Express BI\` (junction `C:\BI`) e `C:\Program Files\PremiumExpressBI\` (Apache serve daqui). Edits em `C:\BI` sem sync elevated = fix invisível. Universal — loja_emissora vazia = backward compat total.
    // v1.7.13 (2026-07-06) — **Fiscal: destinatário vazio = silenciar aquele relatório.** Feedback: "nessas caixa deixa opção para deixar as caixas em branco estamos recebendo muitos emails". Mudança de comportamento em `SetorFiscal::destinatariosDoRelatorio`: se o campo do relatório existe mas está vazio → retorna string vazia → não envia (silêncio explícito). Antes: caía em fallback pro destinatário global do setor, causando spam. Compatibilidade: se a chave `destinatarios` NÃO existe no config (config antigo), mantém fallback global. UI da aba Fiscal reforçada com aviso claro em cada card: `🔕 Vazio = silenciado (não envia nada)` quando campo em branco, `🔔 Ativo — envia quando tiver problema` quando preenchido. Cliente agora controla granularmente: preenche só os que quer receber, deixa em branco os que não interessam. Universal — vale pros 4 relatórios fiscais.
    // v1.7.12 (2026-07-05) — **Fiscal: cada relatório vira e-mail SEPARADO com destinatário próprio.** Feedback: "vai ser enviado toda vez q enviar esses dados, precisa ser sepado". Refatoração no SetorFiscal: em vez de 1 e-mail por loja com todos os 4 alertas juntos, agora envia 1 e-mail POR RELATÓRIO ATIVO por loja. Cada relatório com: (a) **destinatário próprio** — cupom pode ir pro contador, certificado pro TI, duplicidade pro gerente, NCM pro auxiliar cadastro; (b) **assunto focado** — "📛 Cupom · Grupo Ata · Matriz · 04/07 · 50 NFC-e" vs "📅 Certificado A1 · Grupo Ata · Ach10 · vence em 720 dias"; (c) **cabeçalho colorido** conforme categoria — vermelho pra cupom, amarelo pra certificado, laranja pra duplicidade; (d) **só envia quando tem problema** — matriz sem cupom rejeitado, sem NCM inválido e certificado >30 dias não gera 4 e-mails vazios, gera zero. (e) **Fallback em cascata**: destinatário específico do relatório > destinatário da loja > destinatário global do setor. (f) **Throttle por loja+relatório** (`storage/alertas/fiscal/{loja}-{rel}.txt`). (g) **View reformada** — cada checkbox tem campo de input do destinatário ao lado (placeholder sugere quem — contador@, ti@, gerente@, auxiliar@). Testado 5 lojas: 6 emails separados enviados em 17.8s (3 falhas por rate limit HostGator, esperado). Setor Fiscal agora respeita separação de responsabilidades por área.
    // v1.7.11 (2026-07-05) — **3 alertas novos no Setor Fiscal: Certificado A1 + Duplicidade + NCM inválido.** Feedback: "o que precisamos ficar de olho sobre cadastro em geral para nao haver erro de autorização de nota fiscal ou rejeição cupom fiscal?". Implementação de 3 alertas de cadastro que previnem 90% das rejeições SEFAZ. (1) **Certificado A1 vencendo** — `AlertasRepository::certificadoA1DiasRestantes()` lê `NFCE_CABN.DT_VENCIM_CERTIFICADO` (mais recente) e calcula dias. Alerta escalonado por urgência: vermelho crítico (<7 dias ou vencido), laranja (<15), amarelo (<30). Ação sugerida por nível. (2) **Duplicidade NF-e recorrente (cód 539)** — `AlertasRepository::duplicidadeNfeUltimos(dias)` conta rejeições SEFAZ 539 em NFCE_MSGN + amostra os 5 casos mais recentes com NF/série/data/mensagem. Indica 2 máquinas emitindo mesma numeração ou série mal cadastrada. (3) **Produtos com NCM inválido** — `AlertasRepository::produtosNcmInvalido()` lista produtos com estoque > 0 e NCM vazio ou fora do padrão 8 dígitos (causa códigos 215 "Falha schema XML" e 613 "Chave difere"). Amostra top 15 por maior estoque, classificação do problema (Vazio, Curto N dígitos, Longo N dígitos). Todos 3 com toggles próprios na aba Fiscal + parametrização (dias_alerta, dias, dias). **Descobertas reais no Grupo Ata**: matriz certificado vence 01/07/2027 (OK), Ach20-Barra **37 duplicidades em 7d** (problema crônico), Ach_20a certificado desconhecido (offline no teste). Testado 5 lojas via VPN Radmin em 21.7s. Setor Fiscal completo agora tem 4 relatórios: cupom rejeitado + certificado + duplicidade + NCM.
    // v1.7.10 (2026-07-05) — **FIX DEFINITIVO cupom rejeitado — descoberta do schema real Fire.** Feedback: "esta confundindo NFP nao é cupom nao autorizados são faturas emitidas na cupom ou notas fiscais, vamos analsar no importados". Investigação completa em Mega Importados: (a) `NFCE_CAB` e `NFCE_MSG` estão VAZIAS há 30 dias — Fire não usa; (b) as tabelas reais são `NFCE_CABN` (numerada, 1.2M linhas, STATUS é LETRA 'O'=ok/'C'=cancelada/'R'=**rejeitada**) e `NFCE_MSGN` (mensagens, 2.4M linhas, COD_STATUS é NÚMERO); (c) COD_STATUS=5000 "Geração Xml" é mensagem INTERMEDIÁRIA (não é erro) — Fire grava DUAS mensagens por nota: 5000 no envio e 100 na resposta; (d) filtro `ENFCE≠'S'` pegava faturas normais de operações "GERA_FINANCEIRO" (op 115 = venda a prazo) — falso-positivo enorme (213 notas/dia). **Fix arquitetural**: SQL agora é `FROM NFCE_CABN WHERE STATUS='R'` + subquery em NFCE_MSGN pegando último COD_STATUS ≠ 100 e ≠ 5000 (o erro real) + mensagem textual SEFAZ. Cascata: (1) NFCE_CABN moderno com JOIN, (2) NFCE_CAB legado com STATUS numérico, (3) NOTAS_CAB.ENFCE='N' fallback minimalista. **Números REAIS 30 dias Grupo Ata**: Matriz 1 rejeitada R\$ 12 · Ach10 14 R\$ 580 · Ach20-Barra 38 R\$ 1.862 (várias por Duplicidade 539) · Tamara 0 · Ach_20a offline. Motivos reais capturados: 402 "XML utiliza codificação divergente", 215 "Falha schema XML", 539 "Duplicidade NF-e". Ach20-Barra tem problema real de duplicidade recorrente que estava passando batido. Nota 1334772 continua fora (autorizada corretamente).
    // v1.7.9 (2026-07-05) — **FIX CRÍTICO: cupom não autorizado dava falso-positivo em notas realmente autorizadas.** Feedback: "1334772 ta dizendo no relatorio q nao foi autorizado mas me parece que foi autorizado analisa". Diagnóstico: `AlertasRepository::cupomNaoAutorizado` usava `COD_STATUS_NOTA <> 100` (padrão SEFAZ) mas o Fire ERP internamente usa OUTRO código — na matriz Mega Importados, 383/385 notas de 05/07 têm `COD_STATUS_NOTA = 0` (valor default do Fire, NÃO significa rejeitada). A nota 1334772 tem `ENFCE='S'` (Fire só marca 'S' após SEFAZ autorizar) mas COD_STATUS_NOTA=0 e NRO_PROTOCOLO_NFE=NULL — o Fire não copia esses campos pra NOTAS_CAB, ficam em tabela satélite. Falsos-positivos: 383 na matriz, 218 no Ach_20A. **Fix arquitetural**: (1) trocado filtro pra `ENFCE IS NULL OR UPPER(TRIM(ENFCE)) <> 'S'` — bandeira Fire real de "autorizado"; (2) LEFT JOIN em `NFCE_CAB` (tabela satélite descoberta via RDB\$RELATIONS: colunas STATUS, PROTOCOLO, MOTIVO_ENVIO, TIPO_EMISSAO, DATA_AUTORIZA) pra puxar código SEFAZ real + motivo texto quando disponível; (3) 3 tentativas em cascata (com JOIN NFCE_CAB → sem JOIN só ENFCE → mínimo); (4) filtro `TOTAL_NOTA > 0` pra evitar notas zeradas de entrada/ajuste. Números REAIS agora nas 5 lojas Grupo Ata em 05/07: matriz 50 rejeitadas R\$ 1.691, ach_20a 50 rejeitadas R\$ 3.256, ach10/ach20barra/tamara ZERO (situação limpa). Nota 1334772 corretamente removida da lista. Testado enviando Fiscal: 5/5 emails entregues.
    // v1.7.8 (2026-07-05) — **SETOR FISCAL independente — cupom não autorizado sai do Gestor pra e-mail próprio do contador.** Feedback: "esse aqui é para colcoar aqui ele é independente e para um setor fiscal". Refatoração: (a) Novo `SetorFiscal.php` seguindo mesmo padrão de SetorEstoque/SetorCompras — 1 e-mail POR LOJA, throttle próprio em `storage/alertas/fiscal/{loja}.txt`, destinatários globais + opção `destinatarios_por_loja` (contador matriz@ vs contador ach10@ etc). Horário padrão 08:15 (após fechamento). (b) **Removido do Gestor**: toggle `r_cupom_nao_aut` sumiu da aba Gestor, `AlertaRunner::coletarUmaLoja` não coleta mais cupom rejeitado, `secaoCupomRejeitado` no ConstruirCorpoEmail continua existindo mas fica silenciosa (dados vêm null → seção vazia). (c) **Nova aba "📛 Fiscal"** na tela (5ª aba, cor vermelha #dc2626) com switch ativo, horário, destinatários e 1 toggle "Cupom não autorizado" + campo "Analisar últimos X dias" (1-7). (d) 2 rotas novas: `POST /configurar/alertas/salvar-fiscal` e `POST /configurar/alertas/testar-fiscal`. (e) `worker-tick.php` chama `SetorFiscal::tick()` como piggyback (mesmo padrão dos outros 4). (f) E-mail Fiscal tem cabeçalho vermelho + "Situação fiscal em ordem ✅" quando não há rejeição no dia. Assunto: "📛 Fiscal · Grupo Ata · Achadinhos 10 · 04/07 · 25 NFC-e rejeitadas". Testado nas 5 lojas Grupo Ata (3 dias analisados): 5 emails entregues em 6.0s. Setor Fiscal completo — Contador Grupo Ata recebe direto no e-mail dele todos os dias 08:15.
    // v1.7.7 (2026-07-05) — **Alerta CUPOM NÃO AUTORIZADO — NFC-e rejeitadas pela SEFAZ com código, motivo e ação sugerida.** Feedback: "é possivel alerta de cupom nao aturizado?" + "vamos adcionar agora igual os outros cards e com todas informações dos cupom rejeitados e motivo do erro e informações esta que vai ajudar a pessoa responsavel resolver". Discovery em workflow paralelo revelou schema Fire completo (31 colunas NFC-e em NOTAS_CAB): `COD_STATUS_NOTA` (100=aut), `INF_COMPL_NFE` (motivo texto SEFAZ), `NRO_PROTOCOLO_NFE`, `CHAVE_ACESSO_COMPL`, `IND_EMISSAO` (1=normal, 9=contingência), `NFE_USUARIO_PROCESSOU`. **Implementação em 6 blocos**: (1) Novo arquivo `app/Support/CodigosSefaz.php` — mapa com 30 códigos SEFAZ mais comuns (100, 110, 205, 215, 216, 217, 233, 280, 290, 297, 301, 302, 321, 403, 453, 463, 502, 539, 569, 613, 656, 694, 702, 755, 999) traduzidos + ação sugerida ex: 215→"Falha schema XML"→"Fire ERP > Fiscal > Reprocessar", 755→"Certificado revogado"→"Renovar A1 URGENTE". (2) `AlertasRepository::cupomNaoAutorizado($data)` — cascata de 3 tentativas schema-safe (schema completo → sem chave/protocolo → legado via ENFCE), converte win1252→utf8, filtra DATA_CANC IS NULL. (3) `AlertaRunner::coletarUmaLoja` — chama método se `regras.cupom_nao_autorizado.ativo`. (4) `ConstruirCorpoEmail::secaoLoja` — mini card vermelho no bloco de alertas ("📛 X notas rejeitadas · R\$ Y") + `secaoCupomRejeitado` no rodapé com tabela detalhada 7 colunas (NF/Série, Data, Valor, Cód SEFAZ+nome, Motivo curto, Chave final, Ação verde), badge CONTIN. se IND_EMISSAO=9, distribuição por código no rodapé. (5) View — novo checkbox "📛 Cupom não autorizado" entre cartão e sangria. (6) Controller — POST `r_cupom_nao_aut` → `regras.cupom_nao_autorizado.ativo`. Testado nas 5 lojas Grupo Ata via VPN Radmin: descobriu 180 rejeitadas em 7d na matriz, 150 no Ach10, 150 no Ach20-Barra, 93 no Ach20A (Tamara=0). Total 573 NFC-e presas na semana — problema fiscal grave que ninguém enxergava. Gestor 1 email enviado em 7.1s.
    // v1.7.6 (2026-07-05) — **Código do produto em TODAS as tabelas de relatórios (localização rápida no ERP).** Feedback: "no relatorios precisa ter codigo produto para facil localização do produto para correção". Adicionado `COD_ITEM` como primeira coluna em todas as tabelas onde aparece produto: (a) **SetorEstoque** — Sem preço, Sem custo, Prejuízo, Abaixo do mínimo, Zerados, Custo incoerente, Cadastros recentes (7 tabelas). (b) **SetorCompras** — Sugestão de compra (auto-detecta COD_ITEM se estiver no retorno), Sem giro, Variação de preço (3 tabelas). (c) **Gestor** — Top produtos vendidos: `AlertasRepository::topProdutosDia()` agora inclui `ni.COD_ITEM` no SELECT + GROUP BY (COD_ITEM, DESCRICAO); `ConstruirCorpoEmail::secaoTopProdutos()` mostra coluna "Código" antes de "Produto". Objetivo: gestor/estoquista/comprador que recebe alerta ("VITRINE DE FLORES SEM PREÇO") pode abrir o Fire ERP e ir DIRETO no produto pelo código, sem precisar buscar por descrição (que pode ter variações "Vitrine Flores Grande", "Vitrine floral", etc). Testado nas 5 lojas Grupo Ata: Estoque 5 emails OK, Compras 5 emails OK, Gestor 1 email OK — 11 emails com códigos incluídos.
    // v1.7.5 (2026-07-05) — **Estoque e Compras: 1 e-mail POR LOJA (não consolidado).** Feedback: "global so para o gestor! as outras lojas são individual, cada setor tem seu email, estoque, compras, gestor todos são email individual esta indo tudo junto". Refatoração completa dos SetorEstoque e SetorCompras: (a) `tick()` e `testarAgora()` agora rodam LOOP pelas lojas ativas via `Lojas::ativas()` e disparam UM E-MAIL SEPARADO por loja em vez de um só consolidado; (b) Cada e-mail tem cabeçalho identificando "🏢 Grupo Ata" (nome do grupo) + "📦 Achadinhos 20A" (loja individual em fonte 24px destacada); (c) **Throttle POR LOJA**: `storage/alertas/estoque/{lojaId}.txt` (idem compras) — se 1 loja falhar, as outras continuam; (d) **Destinatários por loja** (novo campo opcional `destinatarios_por_loja` no config): pode-se configurar email diferente por loja (estoquista.matriz@X, estoquista.ach10@X etc). Se vazio, usa `destinatarios` global do setor como fallback; (e) SetorEstoque removeu bloco consolidado (era só pro gestor), agora tem KPIs SÓ da loja individual em card ciano; (f) Assunto: "📦 Estoque · Grupo Ata · Achadinhos 20A · 05/07 · N pendências" — mostra grupo E loja no assunto do email; (g) tick retorna `enviados/ignorados/erros/detalhe` pra facilitar debug. **Gestor continua exatamente como está** — 1 email consolidado das 5 lojas juntas (visão executiva). Testado nas 5 lojas Grupo Ata via VPN Radmin: Gestor 1 email em 7.5s, Estoque 5 emails em 12.1s (~2.4s/loja), Compras 5 emails em 13.6s (~2.7s/loja) — 11 emails no total.
    // v1.7.4 (2026-07-05) — **FIX SKUs=0 + identificação clara das lojas em todos os emails.** Feedback: "saiu como grupo ata, o nome grupo ata tem que consta em todos relatorios, mas tem que especificar qual loja esta se referindo". 2 bugs + 3 melhorias UX: (1) **BUG SKUs=0**: SetorEstoque lia `$k['QTD_ITENS']` mas EstoqueRepository::kpis() retorna `TOTAL_ITENS`. Resultado: "0 SKUs · R\$ 2,5M em estoque" (contraditorio). Fix: renamed em 2 lugares (calcularConsolidado + linha de KPIs por loja). (2) **Cabeçalho lista as lojas**: os 3 setores (Gestor, Estoque, Compras) agora tem linha de contexto embaixo do nome do grupo: `📍 5 loja(s): Mega Importados 1,99 · Achadinhos 10 · Achadinhos 20 - Barra · Mil Coisa - Tamara · Achadinhos 20A`. Deixa claro quais lojas o relatório está cobrindo. (3) **Assunto com escopo**: "📦 Estoque · Grupo Ata (5 lojas) · 05/07" — se for teste em 1 loja, mostra o nome dela: "(Achadinhos 20A)". (4) **Cada seção de loja destacada**: nome da loja em fonte 16px + barra colorida lateral 5px na cor do setor (ciano estoque, roxo compras, laranja gestor) + linha tracejada separando do conteúdo. Impossível confundir qual loja está sendo referenciada. (5) **Rótulo "Detalhe por loja"** mantido pra reforçar contexto. Universal — funciona single-loja (mostra a única) e multi-loja (lista todas). Testado nas 5 lojas Grupo Ata via VPN Radmin, todos 3 setores entregues OK.
    // v1.7.3 (2026-07-05) — **Nome do grupo customizavel + Gestor globalizado + Sem giro migrado pra Compras.** Feedback pro Grupo Ata (5 lojas Mega Importados): "detalhe os dados do gestor tem que global BI envia de todas lojas e o nome do grupo das lojas chama Grupo Ata". **3 mudancas grandes:** (1) **Novo campo `nome_grupo` no config/alertas.php + tela** — se preenchido, aparece no cabecalho de TODOS os e-mails (Gestor, Estoque, Compras) em vez do nome da empresa individual. Campo salvo pela tela Gestor. Se vazio, cai no fallback `database.php.empresa`. (2) **Bloco CONSOLIDADO GLOBAL no topo do Gestor** — antes: KPIs somados eram 3 cards simples. Agora: card laranja gigante com faturamento total do grupo em fonte 28px + delta vs media 30d consolidada + 4 KPIs globais (cartao s/cupom total, dinheiro/sangria total, acrescimo cartao total, cortesias total) com cor semantica + linha destacada "N loja(s) com alerta critico". Executivo ve TOTAL do grupo em 2s, depois abre detalhe por loja. Aggregacao: soma resumo.faturamento, cartao.total_valor, sangria.dinheiro/sangria, acrescimo.total, cortesia_qtd, media_30d.media de todas as lojas. (3) **Relatorio "produtos_sem_giro" migrado do Setor ESTOQUE pro Setor COMPRAS** — logica de negocio: a decisao de "parar de comprar produto sem giro" e do comprador, nao do estoquista (estoquista arruma cadastro). Removido toggle da aba Estoque na view + do Controller + do SetorEstoque; adicionado toggle na aba Compras + no Controller + no SetorCompras (que agora importa ProdutosRepository pra chamar semMovimento()). Assunto do Gestor mudou de "Fire Sistemas · Relatorio..." pra "📊 {Grupo} · {data} · R\$ X" — mais direto pra celular. Testado com 5 lojas Grupo Ata via VPN Radmin: Gestor 7.8s, Estoque 12.1s, Compras 3.6s — todos entregues.
    // v1.7.2 (2026-07-05) — **FIX crítico: fechamento_caixas retornava 0 caixas mesmo com dados.** Feedback: "pedi fechamento de caixa por email nao envio". Investigação: SQL de `AlertasRepository::fechamentoCaixasDia()` usava nomes de coluna que NAO EXISTEM no schema real do Fire (VLR_ABERTURA/VLR_DINHEIRO_FECHAMENTO/DIFERENCA). Fallback tambem usava VLR_ABERTURA. Resultado: primeira tentativa falhava, fallback tambem, retornava array vazio — seção nem aparecia no e-mail. Schema real (validado via RDB\$RELATION_FIELDS no banco matriz do Mega Importados): `SUPRIMENTO` (fundo inicial), `VALOR_INFORMADO` (contado no fechamento), `VALOR_CALCULADO` (esperado), `VALOR_FECHAMENTO` (final). Fix: cascata de 3 tentativas — (1) schema moderno com `VALOR_INFORMADO - VALOR_CALCULADO` como diferença calculada; (2) schema legado com `VLR_ABERTURA/VLR_DINHEIRO_FECHAMENTO/DIFERENCA`; (3) mínimo garantido sem valores (só operador/data). Testado com banco matriz Mega Importados dia 04/07/2026: 6 caixas retornados corretamente (MARIANE CX, KELIGISELE CX, LIZETE CX, ANACLARACX, CRISLAINNE CX, IZABELLA CX) com abertura R\$ 253-292, vendas R\$ 1.5k-3.2k, sangrias R\$ 700-1.6k, diferenças R\$ +0.86 a +59.36. Universal — funciona nos 5 bancos do Grupo Mega e em clientes legacy.
    // v1.7.1 (2026-07-05) — **Setor Estoque muito mais completo — resumo consolidado multi-loja + 6 relatórios.** Feedback pra 5 lojas do Grupo Mega Importados: quer receber saúde completa do cadastro (não so alguns aspectos), custo unitário incoerente, cadastros recentes. Reforma no `SetorEstoque`: (a) **Bloco RESUMO CONSOLIDADO no topo** — card ciano com KPIs somados das todas as lojas: total SKUs, R\$ em estoque, zerados, abaixo mínimo, sem foto, sem preço, sem custo, sem NCM, sem barras, custo incoerente, cadastros recentes, sem giro, lojas offline. Executivo de decisão em 3s. (b) **Novos relatórios**: 🩺 **Saúde completa** (agrupa amostras de sem foto, sem preço, sem custo, sem NCM, prejuízo em <details> expansível), ⚠ **Custo incoerente** (SQL schema-safe classifica em ABSURDO >R\$100k / ALTO >5× preço / PREJUIZO >preço / ZERADO), 🆕 **Cadastros recentes** (tenta DATA_CADASTRO/DT_CADASTRO/DT_INCLUSAO/DATA_INCLUSAO no schema, X dias configuráveis, mostra status foto+preço). (c) **Removidos** os toggles `produtos_sem_foto` e `produtos_sem_preco_custo` (redundantes com saude_completa). (d) **Detalhe por loja** ficou compacto: 1 linha de KPIs + details expansível pra saúde crítica + tabelas só do que foi marcado. Layout escala pra 5+ lojas sem virar email gigante. Assunto do email vira `📦 Estoque · Empresa · Data · 5 lojas · N pendências críticas`. Testado local com 5 bancos Mega Importados via VPN Radmin: coleta completa em 11.3s (~2s/loja), e-mail entregue OK.
    // v1.7.0 (2026-07-05) — **Alertas por SETOR — 3 e-mails independentes (Gestor + Estoque + Compras).** Reforma grande do sistema de alertas. Antes: 1 e-mail pro gestor com tudo dentro. Agora: 3 setores, cada um com destinatário, horário e relatórios próprios. Tela `/configurar/alertas` agora tem **4 abas** (Bootstrap nav-tabs): 🎯 Gestor (visão executiva — inalterada), 📦 Estoque (dedicado ao estoquista), 🛒 Compras (dedicado ao comprador), 💬 WhatsApp. **Setor ESTOQUE**: 5 relatórios opt-in — produtos zerados, abaixo do mínimo, sem foto, sem preço/custo, sem giro há N dias (parametrizável). Reusa `EstoqueRepository::minimos()`, `ProdutosRepository::saudeLista/semMovimento/saudeKpis`. E-mail com cabeçalho ciano #0891b2 + KPIs coloridos (SKUs, zerados, valor custo, sem foto/custo/preço). **Setor COMPRAS**: 4 relatórios opt-in — sugestão de compra (reusa `SugestaoCompraRepository::sugestao()`), top fornecedores do mês, notas de entrada dos últimos 7 dias, variação de preço detectada com limite % configurável. E-mail com cabeçalho roxo #7c3aed. Ambos com **throttle independente** (`ultimo-envio-estoque.txt` e `ultimo-envio-compras.txt`) — cada setor envia 1x por dia sem afetar os outros. **Novos arquivos**: `app/Services/Alertas/SetorEstoque.php`, `app/Services/Alertas/SetorCompras.php` — cada um com `tick()`, `testarAgora()`, coleta multi-loja via `Lojas::multi()/Lojas::ativas()`, montagem HTML dedicada. `AlertasDiariosController` ganhou 4 actions: salvarEstoque, testarEstoque, salvarCompras, testarCompras. `worker-tick.php` agora chama `SetorEstoque::tick()` e `SetorCompras::tick()` além do `AlertaRunner::tick()` original (piggyback zero-custo). 4 rotas novas no router. Backwards-compat 100% — config antigo do Gestor continua funcionando sem migração; setores novos são keys independentes `['estoque'=>...]` `['compras'=>...]` no mesmo `config/alertas.php`. Universal — schema-safe (try/catch em todos os métodos), zero mudança em Repositories existentes.
    // v1.6.4 (2026-07-05) — **Relatório "Fechamento de caixas" no e-mail diário — 7ª opção.** Complementa v1.6.3. Novo toggle 💵 Fechamento de caixas mostra por operador: hora abertura, valor abertura, vendas totais + breakdown dinheiro/cartão, sangria, suprimento, valor de fechamento (se schema tiver) e diferença — com cor semântica (verde se zero, laranja se sobra, vermelho se falta). Rodapé da tabela consolida TOTAL do dia da loja. `AlertasRepository::fechamentoCaixasDia()` faz 3 passos: (1) SELECT em ABERTURA_CAIXA pra pegar operador/abertura/fechamento/diferenca; se colunas VLR_DINHEIRO_FECHAMENTO/DIFERENCA nao existirem no schema, retenta sem elas (schema-safe); (2) agrupa NOTAS_CAB × FORMAS_NOTAS × FORMA_PGTO por caixa e por tipo (DIN/CART/OUT); (3) agrega LANC_TESOURARIA por caixa separando SANGRIA_SUPRIM=S de N. Universal — cliente sem caixas do dia (ex: só emitiu NF-e escritorio) recebe seção vazia (sem quebra). Menu Configuração > Alertas Diários ficou com 7 checkboxes de relatórios extras.
    // v1.6.3 (2026-07-05) — **Relatórios extras no e-mail diário — 6 checkboxes.** Além dos 5 alertas de anomalia (v1.6.0), a tela `/configurar/alertas` ganhou seção nova "Relatórios extras a incluir no e-mail" com 6 toggles: 🏆 Top 10 produtos vendidos, 👥 Vendas por operador de caixa, 💳 Formas de pagamento (com % do total), 💰 Descontos concedidos (total + ranking operadores), 📦 Estoque crítico (Top 10 abaixo do mínimo), ⏰ Top 5 horários de pico. Cada checkbox marcado adiciona uma tabela HTML enxuta no rodapé de cada loja no e-mail. Novos métodos schema-safe no `AlertasRepository`: `topProdutosDia`, `vendasPorVendedorDia`, `formasPagamentoDia`, `descontosDia`, `estoqueCritico`, `vendasPorHoraDia`. `AlertaRunner::coletarUmaLoja` agora recebe `$extras` do config e chama só o que estiver ativo (sem custo pra quem nao marcar). `ConstruirCorpoEmail` ganhou helper `tabelaEnxuta()` + 6 funções `secaoX()` que retornam HTML vazio quando dados nao existem. `AlertasDiariosController::salvar()` grava `relatorios_extras.*.ativo` no config. Universal — vale pra qualquer segmento; itens sem match no schema (ex: DATA_HORA_VENDA ausente = pico horario vazio) sao silenciosamente ignorados.
    // v1.6.2 (2026-07-05) — **Migração remetente Gmail → HostGator Fire Sistemas.** Os alertas diarios (v1.6.0) e as notificacoes de backup (v1.5.28+) agora saem de `bi@masterautomacao.com` (dominio proprio) em vez de `derlivon89@gmail.com` (Gmail pessoal). Config global centralizada em `config/backup-notificacao.php` migrada pra HostGator SMTPS (mail.masterautomacao.com:465 SSL) — todos os clientes que atualizarem OTA passam a usar o novo remetente sem digitar nada. Cascata continua funcionando: cliente pode sobrescrever individualmente na tela `/backup` aba E-mail se quiser (senha do job tem prioridade sobre global). Rodapé do e-mail de alertas ganhou assinatura profissional Fire Sistemas — Premium Express BI + linha de suporte tecnico `derlivon89@gmail.com`. Teste de conexao validado direto do notebook: br180.hostgator.com.br aceitou AUTH LOGIN + MAIL FROM + entregou id `1wgSdF-00000000X6N-0gJt`. Config `alertas.php` ativada por default (`ativo=true`) e destinatario ajustado pra derlivon89@gmail.com (email pessoal do gestor).
    // v1.6.1 (2026-07-05) — **Sistema de Alertas WhatsApp em Tempo Real — FASE 2 (Opção B).** Complementa v1.6.0 (relatorio diario por email). Agora quando algo critico acontece HOJE, o gestor recebe mensagem WhatsApp na hora (via Evolution API). Reusa integracao ja existente do agente-suporte-whatsapp (mesma tecnica). Arquivos novos: `app/Services/Alertas/WhatsAppClient.php` (cliente Evolution portado do Python via curl, mesma tecnica do SmtpClient), `app/Services/Alertas/AlertaRealTime.php` (coleta dados do DIA CORRENTE + regras + throttle por regra por loja por dia). 3 regras real-time: (1) **cartao_sem_cupom** ativa quando o cartao sem NFC-e passa de R\$ 500 acumulado no dia; (2) **sangria_dinheiro_alto** dispara se dinheiro passar de R\$ 2.000 com 0 sangria; (3) **queda_intraday** avisa depois das 15h se vendas estao 50%+ abaixo da media 30d. Anti-spam: state file por regra x loja x dia (so 1 msg por combinacao/dia; markers antigos limpos automaticamente >7d). Piggyback no `worker-tick.php` — a cada 5min avalia + envia se necessario. UI: tela `/configurar/alertas` ganhou secao verde WhatsApp com URL Evolution + instance + apikey + numero destino + toggles das 3 regras + botao "Enviar teste WhatsApp agora". 2 endpoints novos: `POST /configurar/alertas/salvar-wa` e `POST /configurar/alertas/testar-wa`. Config opcional — se `whatsapp.ativo=false` OU credenciais Evolution vazias, sistema pula silenciosamente (nao quebra clientes sem WA). Mensagens em Markdown do WhatsApp com emoji e nome da loja. Roadmap Fase 3 (futuro): painel central 24h com semaforos de todos os clientes.
    // v1.6.0 (2026-07-05) — **Sistema de Alertas Diarios por E-mail — FASE 1 (Opção A).** Complementa o pedido do Achadinhos 20A: "consegue fazer alerta pro gestor ou relatorio automatico?". Todo dia no horario configurado, o BI envia um e-mail com resumo executivo do dia anterior + alertas de anomalia detectada. Layout HTML rico (KPIs no topo com cor semantica verde/amarelo/vermelho, cards por loja, drill-down textual). Assunto do email vira "🔴 [ALERTA] {empresa} · {data} · Cartão sem cupom: R$ X" quando > R$ 500 — chama atencao ate no celular. Arquivos: `config/alertas.php` (defaults), `app/Repositories/AlertasRepository.php` (queries), `app/Services/Alertas/AlertaRunner.php` (orquestracao + throttle 20h), `app/Services/Alertas/ConstruirCorpoEmail.php` (HTML rico), `app/Controllers/AlertasDiariosController.php` (tela config), `app/Views/configurar/alertas.php` (form). CLI: `tools/worker-alerta-diario.php`. Piggyback no `worker-tick.php` existente — a cada 5min ele tambem checa se ja passou do horario e envia (sem duplicar). 5 regras configuraveis: **cartao sem cupom** (baseado em ENFCE=N + forma POS), **sangria zero** (dinheiro > R$500 sem sangria), **cortesia zero** 30d, **queda vendas** vs media 30d, **acrescimo invisivel**. Reusa `SmtpClient` + `config/backup-notificacao.php` do modulo Backup — mesma senha global Fire Sistemas (App Password Gmail). Cliente configura destinatarios/horario/regras na tela **/configurar/alertas** (menu Configuração > Alertas Diários). Botao "Enviar teste agora" pra validar SMTP na hora. Testado local com 5 lojas Mega Importados via VPN Radmin: coletou dados de 04/07/2026 em ~1s por loja, detectou Ach20-Barra R\$ 5.312 cartao sem cupom + Ach20A R\$ 19.081, assunto virou vermelho automaticamente. Nao precisa novo Task Scheduler no cliente — reaproveita o que ja executa `worker-tick.php` a cada 5min. Fase 2 (WhatsApp em tempo real via Evolution API) fica pra proxima rodada.
    // v1.5.66 (2026-07-04) — **FIX: admin nao respeitava desmarcar de cards no Gerenciador de Usuarios.** Feedback: admin/supervisor marcou/desmarcou os cards `Acrescimo do Cartao` e `Domingo/Feriado` mas o BI continuava mostrando os cards no /consolidado e o item no menu Vendas. Causa: `FbAuth::pode()` sempre retorna `true` pra admin/supervisor, ignorando permissoes individuais salvas na tela. Fix: novo metodo `FbAuth::podeOpcional(string $modulo)` que respeita a escolha individual mesmo pra admin — se o admin salvou entrada explicita no JSON de permissoes SEM `*`, o modulo desmarcado nao aparece. Se nao tem entrada (fresh install) ou salvou com `*` (Acesso Total ligado), mantem comportamento anterior. Trocado nos 3 blocos de `/consolidado` (card por loja, KPI global, detalhe consolidado) + no sidebar item "Domingo/Feriado" do menu Vendas + guard direto no `DiasEspeciaisController` (mesmo digitando URL nao passa se desmarcado). Testado com admin `supervisor='S'` + JSON com modulos explicitos SEM `acrescimo_cartao`/`dias_especiais`: `podeOpcional` retorna false, view esconde tudo, item some do menu, controller redireciona pra /sem-permissao. Continua ligado pra qualquer usuario com `*` no modulos (Acesso Total no toggle).
    // v1.5.65 (2026-07-04) — **Modulo "Acrescimo do Cartao" no Gerenciador de Usuarios.** Complementa v1.5.63/64. Adicionado `acrescimo_cartao => Acréscimo do Cartão` em `FbAuth::MODULOS`. Os 3 blocos da tela `/consolidado` (card por loja, KPI global no Resumo Geral, detalhe consolidado clicavel) agora sao gated por `FbAuth::pode('acrescimo_cartao')` — admin ve tudo, usuario sem essa perm nao ve nada (nem calculo aparente, nem tabela). Validado local: admin ve 4 cards + KPI + tabela; limitado (perms=vendas,compras) ve 0 de tudo mas continua vendo demais telas (sangria continua = separacao clean).
    // v1.5.64 (2026-07-04) — **Modulo "Domingo/Feriado" no Gerenciador de Usuarios.** Complementa v1.5.61/62. Adicionado `dias_especiais => Domingo/Feriado` em `FbAuth::MODULOS` (aparece automaticamente como card na tela /configurar/usuarios em qualquer segmento) + mapeamento `/vendas/dias-especiais => dias_especiais` no `MAPA_URLS`. Agora admin pode revogar acesso a essa tela por usuario especifico marcando/desmarcando o card. Zero mudanca no Controller (permissao herdada via `pode('vendas')` continua funcionando pra quem tem acesso ao modulo Vendas inteiro).
    // v1.5.63 (2026-07-04) — **Nova feature: "Acrescimo Invisivel" no /consolidado.** Cliente Achadinhos 20A (Aragarcas) habilitou acrescimo de R\$ 2 por produto em vendas parceladas no cartao de credito. Investigacao: o Fire ERP NAO grava esse acrescimo em coluna dedicada (NOTAS_CAB.ACRESCIMO=0, NOTAS_ITENS.ACRESCIMO=0 nas 1938 notas do mes) — ele **embute o valor direto no TOTAL_NOTA** sem coluna separada. Ex: cliente compra R\$ 40 em produto, e cobrado R\$ 44 no total (R\$ 4 de juros embutidos). O dono via R\$ 44 no faturamento sem saber que R\$ 4 eram juros do parcelamento — "dinheiro invisivel". Fix: **calcular via formula** `ACRESCIMO_REAL = TOTAL_NOTA - (SOMA(QTD*PRECO_UNIT) - SOMA(VLR_DESCONTO))` no SQL, com threshold `> R\$ 0,50` pra descartar arredondamentos. Novos metodos `DashboardRepository::acrescimoInvisivelIntervalo()` (total agregado + qtd notas + media + %fat) e `acrescimoInvisivelDetalhe()` (drill-down por nota, ordenado pelo maior acrescimo). Universal — cliente que nao pratica retorna 0 e a view esconde o card. `ConsolidadoController` agrega por loja e consolidado. `views/consolidado/index.php` ganhou (a) **card por loja** (details expansivel azul, aparece so quando total > R\$ 0,50): mostra nota/data/total/produto/desconto/acrescimo em tabela, (b) **card KPI global** "Acrescimo cartao" no Resumo Geral com link ancora, (c) **detalhe consolidado** com todas as notas com acrescimo agregadas por loja. Cache TTL 30min. Medido 30 dias nas 5 lojas Mega Importados: **Achadinhos 20A R\$ 332 em 32 notas (media R\$ 10,38/nota = 5 produtos x R\$ 2 cada); Achadinhos 20 Barra R\$ 2.894 em 558 notas (surpresa - Barra tambem usa a mesma pratica, quase 10x maior que 20A)**; matriz/ach10/tamara zero. Explicacao "o que e" inline pro dono entender: "juros que a loja cobra no parcelamento, embutidos no total". Inclui v1.5.62 (fix performance dias-especiais N+1 -> 1 SQL agregado, 43x mais rapido).
    // v1.5.62 (2026-07-04) — **FIX CRITICO performance /vendas/dias-especiais.** Cliente Mega Importados reportou `Fatal error: Maximum execution time of 30 seconds exceeded in DashboardRepository.php:1791`. Causa: o Controller fazia loop N+1 chamando faturamentoIntervalo/totalVendasIntervalo 1 vez por dia, por loja. Para 90 dias × 5 lojas via VPN Radmin ~= 450 queries × ~120ms = **~54 segundos** (estourava PHP max_execution_time=30). Fix: novo metodo `DashboardRepository::faturamentoPorDiaIntervalo($ini, $fim)` que faz 1 SQL agregada com `GROUP BY CAST(DATA_EMISSAO AS DATE)` retornando array `['Y-m-d' => ['fat'=>float, 'notas'=>int]]`. Controller passa a chamar 1x por loja. Cache TTL 30min (mesma cascata dos outros metodos). Medido nos 5 bancos reais via VPN: matriz 307ms, ach10 216ms, ach20-barra 213ms, tamara 252ms, ach_20a 36ms — total ~1,25s (43x mais rapido). Sidebar tambem: **menu Vendas ganhou item "Domingo/Feriado"** ao final (icone bi-calendar-week) — antes so era acessivel via URL direta.
    // v1.5.61 (2026-07-04) — **Filtros da tela /vendas/dias-especiais.** Complementa v1.5.60. Antes so tinha DE/ATE de data. Agora: (a) **filtro de loja** (dropdown com "Todas" + lista das ativas — some quando o cliente eh single-store); (b) **filtro de tipo de dia** — analisar Domingos+Feriados, so Domingos ou so Feriados. Quando o cliente escolhe so um, o KPI e a coluna do OUTRO desaparecem da tabela e do drill-down (menos ruido visual, foca no que ele quer decidir); (c) **atalhos rapidos de periodo** — botoes "Ultimos 30 dias / 90 dias / 6 meses / 12 meses" que preservam loja e tipo escolhidos. Barra de resumo mostra "Analisando so domingos · 5 lojas ativas · N dias uteis" pra dar contexto do que ta na tela. Zero mudanca no Repository — filtros aplicados no Controller/view. Validado local nos 5 bancos Mega Importados via CLI: 7 combinacoes (sem filtro, so domingo, so feriado, matriz sozinha, ach10 so domingo, presets 30d/12m) — em todas as tabelas rendera com quantidade correta de linhas e colunas.
    // v1.5.60 (2026-07-04) — **Nova tela "Compensa abrir domingo/feriado?" em /vendas/dias-especiais.** Pedido do Grupo Mega Importados (5 lojas): saber loja por loja se abrir aos domingos e feriados paga o custo. Compara faturamento medio de cada tipo de dia — Dia util, Domingo, Feriado — e recomenda **COMPENSA** (verde, ratio >=70%), **AVALIAR** (amarelo, 30-70%) ou **NAO COMPENSA** (vermelho, <30%). Executivo: 3 KPIs no topo com deltas semanticos. Detalhado: tabela por loja com media dia util + media domingo + recomendacao domingo + media feriado + recomendacao feriado (mostrando quantidade de dias observados). Drill-down: `<details>` por loja lista cada domingo e feriado especifico do periodo com faturamento, nº de notas e % em relacao ao dia util. Novo helper `App\Core\FeriadosBrasil` calcula 12 feriados nacionais (fixos + moveis via algoritmo de Gauss/Butcher pra Pascoa) — cliente pode acrescentar feriados estaduais/municipais em `config/feriados_extras.php`. Dias sem venda (loja fechada) sao ignorados na media pra nao mentir pra baixo. Multi-loja usa `Lojas::ativas()`; loja unica cai em fallback virtual. Menu Vendas ganhou item "Domingo/Feriado". Universal — vale pra qualquer segmento.
    // v1.5.59 (2026-07-04) — **Relatorio "Fora do Caixa" agora explica QUAL a natureza cadastrada no Fire de cada nota** (evolucao da v1.5.58). Cliente Achadinhos 20A perguntou: "que natureza essas 3 vendas de R\$ 5.475 estao usando? nao deveriam ser natureza interna?". Relatorio agora inclui: (a) nova secao **"Analise das operacoes usadas"** que agrupa notas por TIPO_OPERACAO do Fire, mostra descricao, natureza (V/C/R/P), MOV_INTERNA, DEVOLUCAO, ETRANSF, GERA_FINANCEIRO em tags coloridas. (b) **Alertas automaticos**: se descricao da operacao contem "Transferencia/Grupo/Matriz/Filial/Uso e Consumo/Bonificacao/Amostra/Doacao/Perda/Roubo/Baixa" mas TIPO_NATUREZA='V' e MOV_INTERNA!='S', card fica VERMELHO com "SUSPEITO: operacao cadastrada como Venda mas o nome sugere movimento interno". Tambem alerta ETRANSF='S' aparecendo como venda + GERA_FINANCEIRO='N' inconsistente. (c) Coluna "Operacao (cadastro Fire)" na tabela de notas mostra "op X · Natureza V · MOV. INTERNO" pra cada linha. Dono ve num relance quais notas sao venda real x quais sao movimento interno mal cadastrado no ERP. Universal — vale pra Achadinhos 20A, Importados, todo cliente que abra a tela.
    // v1.5.58 (2026-07-04) — **Drill-down "Fora do Caixa" — cliente pergunta que vendas sao essas.** Complementa v1.5.57 (que adicionou linha "(SEM CAIXA)" na tabela Por Vendedor). Agora a linha eh CLICAVEL — abre relatorio detalhado em /vendas/notas-sem-caixa mostrando cada nota individual: numero, serie, data/hora, cliente + CNPJ/CPF, operacao Fire, valor, forma pgto (A PRAZO / A VISTA), usuario emissor (se DAUSUARIO existir). Cabecalho explicativo pro dono entender: "sao NF-e emitidas fora do PDV, geralmente pelo escritorio, venda faturada a prazo ou ajuste manual". Botao Imprimir/Salvar PDF nativo do navegador. Universal — vale pra Achadinhos 20, Importados, Gelatto e qualquer varejo com operadora de caixa. Novo metodo VendasRepository::notasSemCaixa() com fallback pra schemas antigos sem DAUSUARIO/DATA_HORA_VENDA. Rota nova GET /vendas/notas-sem-caixa preserva data_ini/data_fim do drill-down.
    // v1.5.57 (2026-07-04) — **2 fixes reportados pelo Achadinhos 20:** (1) **Vendedores × Faturamento**: divergencia R\$ 5.475 no dia 03/07/2026 (KPI R\$ 75.947 · 709 notas vs tabela Por Vendedor R\$ 70.472 · 706 notas). Causa: VendasRepository.vendasPorVendedor Tentativa 1 fazia INNER JOIN ABERTURA_CAIXA + AND COD_USUARIO_ABERTURA IS NOT NULL — notas emitidas fora do caixa (NF-e direto do ERP, vendas grandes) sumiam silenciosamente. Fix: INNER→LEFT JOIN + agrupa orfas como linha "(SEM CAIXA)" via COALESCE. Zero regressao: totais dos vendedores existentes NAO mudam (COALESCE mantem o COD_USUARIO original quando existe); Real Prime/Buteco/bares caem na Tentativa 2 (garcom), nem tocam nessa query. Agora total_tabela = KPI sempre. (2) **Filtro de data reseta ao clicar em outro card**: cliente escolhia periodo customizado e ao navegar entre telas voltava pra "hoje". Fix: helper JS no app.js intercepta clique em <a> interno e reanexa data_ini/data_fim ativos da URL — se o link ja tem seus proprios params (usuario clicou em atalho "Semana", "Mes"), respeita. Universal, zero mudanca em Controller.
    // v1.5.56 (2026-07-04) — **Fix estrutural do heartbeat de backup + kill de gbak zumbi.** Diagnostico via workflow paralelo: 3 clientes (SERVER-PRIME/Real Prime, SRV-MACHRY/Extra Sup, DESKTOP-1MPC281/Distribuidora T hora) travados no estagio "iniciando" no painel central. Painel confirmado OK via POST manual (HTTP 200 em 68ms com tamanho_atual=117MB apareceu). Causa raiz: transicao "iniciando → gerando_fbk" so era enviada pro painel via progressCb dentro do `stream_select` do gbak — se gbak trava em `isc_attach_database` (VPN Radmin caida, banco inalcancavel), callback nunca chega em 30s com estagio novo. **3 fixes:** (1) **BackupRunner** envia heartbeat "gerando_fbk" imediato antes do spawn do gbak, painel ve a transicao mesmo se gbak travar depois; (2) **FirebirdBackup** timeouts absoluto (6h) e sem-output (10min) matam gbak zumbi via proc_terminate + log claro; (3) **Notificador** timeout HTTP 5s→10s (simetria com enviarParaPainelCentral) + log JSONL de heartbeats perdidos em `storage/backup/heartbeat_erros.jsonl` pra auditar. Reset "vivo" tambem por arquivo crescendo (banco enorme escrevendo indices sem verbose no stderr).
    // v1.5.55 (2026-07-04) — **UX do "Usar esta pasta" no navegador de pastas do Google Drive** (backup, cliente Distribuidora T hora). Reclamacao: clicar em "Usar esta pasta" preenchia hidden fields e atualizava o texto "Pasta escolhida: X" MAS nao fechava/salvava — cliente rolava a tela procurando o que mudou e achava que o botao estava quebrado. Fix minimo em bkSelecionarPasta (backup/index.php): apos preencher pasta_id/pasta_path/bk-fnome, faz auto-submit do form pai (equivalente a clicar "Salvar pasta" em seguida). Um clique = salvar; a tela recarrega mostrando o novo destino atualizado. O botao "Salvar pasta" continua funcionando quem preferir revisar manualmente antes de submeter (nao mexi nele).
    // v1.5.54 (2026-07-03) — **Botao "Desconectar" no card Google Drive** do backup. Pedido do Derlivon durante debug do Sol Nascente: o botao lixeira generico (destino-excluir) nao era claro pra "resetar OAuth quebrado" — cliente nao sabia se ia perder configuracao. Novo botao dedicado com icone plug + rotulo explicito "Desconectar": (a) novo endpoint `POST /backup/google-desconectar` no BackupController — valida que destino eh google_drive, remove do array de destinos, salva JSON; (b) na view backup/index.php, quando o destino for `tipo=google_drive` mostra botao vermelho claro no lugar do lixeira generico; (c) banner amarelo pos-desconexao explica o proximo passo ("clique em + Adicionar destino pra reconectar do zero"); (d) confirm com texto explicativo antes de excluir. Uso: quando refresh_token expirou (invalid_grant) ou cliente quer refazer OAuth. Nao revoga o token no Google (chamada extra que falha em rede filtrada, e refazer OAuth gera novo refresh_token automaticamente sobrescrevendo). Aplicavel a qualquer cliente Fire, nao so Sol Nascente.
    // v1.5.53 (2026-07-03) — **Timeout agressivo na Camada 1 pra cascata da v1.5.52 funcionar.** Sol Nascente atualizou v1.5.52 e ainda dava erro: Fatal error "Maximum execution time of 30 seconds exceeded in HttpClient.php line 68" — schannel do PHP travava 30s tentando OCSP/CRL de crl.pki.goog (mesmo problema, mas do lado file_get_contents). Cascata NUNCA era chamada porque PHP max_execution_time matava o script antes. Fix: (a) Camada 1 (file_get_contents) usa timeout 5s max — desiste rapido e cai na Camada 2/3; (b) `set_time_limit(120)` no inicio garante que cascata inteira tem folego pra rodar; (c) validado no diag do servidor Sol Nascente: Camada 3 (`--tlsv1.2 --ssl-no-revoke`) retorna http=401 em 500ms; cacert.pem bundle presente (189462 bytes) — OTA v1.5.52 entregou corretamente. Diagnostico raiz: firewall corporativo do cliente bloqueando `crl.pki.goog` (CRL Distribution Point do Google GTS) — sem afetar producao Google Drive quando `--ssl-no-revoke` esta ativo.
    // v1.5.52 (2026-07-03) — **Cascata de 5 camadas TLS pro OAuth Google Drive.** Diagnostico via workflow paralelo: `curl exit 35` no schannel do Windows raramente eh handshake TLS de verdade — 75% dos casos eh `CRYPT_E_REVOCATION_OFFLINE` (firewall/AV bloqueando o CRL/OCSP `crl.pki.goog`), 15% eh HTTP/2+ALPN quebrado por AV/proxy corporativo, 10% eh Windows Certificate Store desatualizado sem GTS Root R1-R4. Fix multi-camada em HttpClient.request(): (Camada 1) file_get_contents nativo → (Camada 2) curl.exe padrao → (Camada 3) `--tlsv1.2 --ssl-no-revoke` (mata o CRYPT_E_REVOCATION_OFFLINE sem tocar em registry, sem reboot) → (Camada 4) + `--http1.1 --no-alpn` (contorna AV/proxy que faz MITM sem HTTP/2) → (Camada 5) + `--cacert cacert.pem` (bundle Mozilla oficial 185 KB do curl.se, versionado em `app/Services/Backup/certs/cacert.pem` — bypassa Windows Cert Store). Cada camada acumula stderr; se todas falharem, msg final diz onde travou. Sol Nascente vai passar na Camada 3 (revocation) assim que aplicar; se persistir, Camada 4/5 cobrem. Zero regressao pra clientes que ja funcionam (Camada 1 continua sendo a primeira). Sem intervencao manual do operador.
    // v1.5.51 (2026-07-03) — **Fix "Erro ao renovar token (HTTP 0)" no backup Google Drive.** Caso Sol Nascente: cliente clicou em "Escolher pasta no Google Drive" e apareceu HTTP 0 sem detalhe (body vazio). Causa: HttpClient.request() usa file_get_contents() com SSL peer verify — se o PHP do servidor nao tem CA bundle (cacert.pem) configurado no php.ini, a chamada falha silenciosa e status vira 0. Fix duplo: (a) **fallback via curl.exe** — quando file_get_contents falha, HttpClient tenta pela mesma rota do uploadCurl (curl.exe do Windows traz CA store proprio e funciona sem depender do PHP). Novo metodo requestViaCurl(). (b) **mensagem de erro rica** — GoogleDriveDriver.accessToken() agora expoe o campo erro (curl exit / stream error) na string final em vez de mostrar so "HTTP 0:". Cliente vai ver "Could not resolve host", "SSL certificate problem", "Connection timed out" etc — motivo real do problema. Pra outros drivers (OneDrive, Dropbox se surgirem) o fix pega automatico pois esta na base HttpClient.
    // v1.5.50 (2026-07-03) — **Donut "Mix Condicao de Pagamento" (Painel Atacado) legivel** mesmo com 30+ condicoes cadastradas (caso Sol Nascente). Antes: labels sobrepostos e ilegiveis, texto cortado, 7 cores repetidas. Agora: (a) agrega Top 10 por valor + "Outros (N itens)" cinza como 11a fatia; (b) label externo so em fatias >= 3% — resto vai so na legenda; (c) legenda vertical scrollavel a direita (ECharts type:scroll) com nomes truncados em 26 chars + "…"; (d) donut deslocado pra esquerda pra caber a legenda; (e) paleta ampliada de 7 pra 10 cores; (f) titulo com acento correto "Condição". Validado no dev com Sol Nascente (5 condicoes reais no periodo, sem "Outros") — quando cliente tiver 12m+ de historico com mais condicoes o agrupamento entra em acao automaticamente. So mexeu no atacado-dashboard.js — os outros donuts do BI ficam intocados.
    // v1.5.49 (2026-07-03) — **Alerta "vendas a prazo sem titulo no ERP" agora aparece no /consolidado tambem** (v1.5.48 so cobria /atacado/recebiveis). ConsolidadoController agrega o alerta de cada loja via RecebiveisRepository.alertaVendasSemTitulo() e a view mostra um cartao vermelho grande no topo. Threshold defensivo (>=R\$100k vendido a prazo em 12m E <20% virou titulo em RECEBER_PAGAR) garante silencio total pra bar/varejo a vista — so dispara em cliente que TEM o problema. Se multi-loja, tabela mostra breakdown por loja. Agrega tambem as operacoes problematicas somando volume das lojas. Vale pra qualquer segmento (bar, restaurante, utilidades, atacado, distribuidora).
    // v1.5.48 (2026-07-03) — **Tela /atacado/recebiveis reformada** com 2 fixes + 1 feature nova: (a) **BUG SHAPE**: Controller passava dados dentro de \$dados[aging|totais|linhas|...] mas view espera \$aging/\$totais/\$titulos/\$filtroFaixa soltos — fallback default zerava tudo mesmo com banco cheio. (b) **BUG AGING**: metodo antigo do Repository usava faixas 1-30/31-60/61-180/>180 mas a view usa a_vencer/ate_30/de_31_60/de_61_90/acima_90 — nada batia. Novo `RecebiveisRepository::snapshotAtacado()` normaliza tudo em 1 chamada (aging + totais + titulos filtrados + contagens), JOIN com TIPO_OPERACAO usa PK composta (COD_EMPRESA+COD_TIPO_OPERACAO) sem duplicar linha. (c) **FEATURE nova**: alerta grande vermelho no topo da tela quando o cliente tem >R\$100k vendidos a prazo em 12m mas <20% viraram titulo em RECEBER_PAGAR — sinaliza op de venda com `GERA_FINANCEIRO='N'` no cadastro Fire (nao gera titulo automatico). Caso Sol Nascente: R\$ 5,0 milhoes vendidos a prazo, R\$ 0,00 registrado no ERP — buraco de controle gigante. Alerta mostra tabela das operacoes envolvidas + orientacao pro contador ativar GF='S'.
    // v1.5.47 (2026-07-03) — **FIX CRITICO: autoresgate de ops de venda com GERA_FINANCEIRO='N'.** Bug detectado no cliente **Sol Nascente** (atacado, emp=2 no banco 26.23.154.143): a operacao 115 "VENDA DE MERCADORIA" (417 notas / R\$ 887 mil em 90 dias) tinha `GERA_FINANCEIRO='N'` no cadastro Fire — o filtro estrito do BI (`detectarFiltroVenda()` do DashboardRepository e VendasRepository) exclui ops com GF='N' pra nao pegar movimento interno tipo "Consumo Familia", "Perda e Roubo", ATA etc. do Importados. Resultado: filtro montava `IN (172,174)` sem a op real, Consolidado zerava e mostrava "sem venda no periodo · dados ate 09/06/2026". Fix: adicionado autoresgate — quando natureza='V' + MI/DEV/TR≠'S' + GF='N' + >=50 notas em 90 dias, a op VOLTA pro filtro (comportamento e claramente venda, cliente marcou GF errado). Validado: Sol Nascente resgata op 115 → 146 notas / R\$ 260k em 30d; Importados/Real Prime/Buteco/Quiosque nao resgatam nada (Importados so tem ops GF='N' com MI='S' que continuam excluidas certinho, Real Prime tem 5 notas na op 291 abaixo do threshold, Buteco/Quiosque nao tem ops GF='N'). Override manual `tipos_venda_extras`/`tipos_venda_excluir` no config/database.php continua funcionando como ultima palavra. Ambos os Repositorios (Dashboard + Vendas) receberam a mesma logica de resgate.
    // v1.5.46 (2026-07-02) — Nova tela **Auditoria Executiva** (/auditoria-executiva): semaforo de 6 anomalias operacionais que valem pra qualquer bar/restaurante/varejo. Detecta e sinaliza em vermelho/amarelo/verde: (1) Sangria de caixa zero — se recebeu R\$ dinheiro sem uma sangria em 12m, dinheiro sai por fora; (2) Cortesias zero — impossivel num bar real, cortesia sem trilha; (3) Descontos zero — operador arruma por fora; (4) Cancelamentos anonimos — quem cancelou nao ficou registrado (DAUSUARIO vazio); (5) Concentracao em 1 garcom acima de 80/95% (login compartilhado); (6) Cardapio cauda longa (SKUs no 5% inferior). Cada card mostra badge de nivel, numero grande, contexto explicativo. Placar geral no topo (TUDO CERTO / ATENCAO / AJA AGORA). Endpoint /api/auditoria-executiva devolve JSON com todos os checks. Validado local com banco Quiosque do Lazaro (11 anos historico, 151k notas): detectou zero sangria, zero cortesia, zero desconto, 41 cancelamentos anonimos, 99,98% concentracao GARCONETE — exatamente os buracos que o PDF executivo mostra. Repository AuditoriaExecutivaRepository (defensivo contra schema variar, try/catch por check). Nao mexeu nas telas existentes (Analise de Cortesia, Excecoes por Garcom continuam funcionando).
    // v1.5.45 (2026-07-02) — Pedidos v2 (dashboard executivo B2B). Tela /atacado/pedidos reformulada com regra global (executivo+detalhado+visual): (1) 4 KPIs no topo com deltas semanticos — Faturamento no periodo (verde, +/-% vs periodo anterior), N° de pedidos (azul, absoluto vs anterior), Ticket medio (roxo), Cliente #1 do periodo (amarelo, top cliente + valor). (2) 3 graficos ECharts em grid — Pedidos por dia (linha area), Top 5 clientes (barras horizontais), Por vendedor (donut). (3) Coluna VENDEDOR agora funciona via LEFT JOIN PESSOAS ON COD_PESSOA_REPRE (schema real do Fire; COD_VENDEDOR nao existe). (4) Coluna PRAZO nova (DT_PREVISAO) + badge vermelho "Atrasado" quando prazo < hoje. (5) Coluna Cond. Pgto via JOIN CONDICAO_PGTO — mostra "BOLETO 35 DIAS", "A VISTA", etc. (6) Total geral no rodape da tabela. (7) Coluna SITUACAO escondida automaticamente quando schema nao tem STATUS. (8) Alerta laranja de pedidos atrasados no topo (aparece so se houver). (9) Filtro periodo (DE/ATE) + status. (10) Drill-down: click na linha abre modal com cabecalho completo (cliente/vendedor/cond/obs) + tabela de itens (SEQ, cod, descricao via JOIN ITENS, qtd, preco, subtotal, badge cancelado se QTD_CANCELADA>0) + total geral. Endpoints novos: GET /api/atacado/pedidos (JSON completo com KPIs+series+linhas) e GET /api/atacado/pedido/itens?nr=X (drill-down). Validado L.A. Silva: faturamento julho R\$ 21k (+39% vs jun), 14 pedidos, ticket R\$ 1.500,34, cliente #1 CAETANO & CIA LTDA R\$ 4.706, vendedor LAZARO 93,5%, drill-down do #8335 mostra 4 itens cigarro totalizando R\$ 2.190.
    // v1.5.44 (2026-07-02) — FIX grafico Distribuicao ABC (donut) que ficava container vazio em /atacado/clientes. Causa: a view chama renderAbc() que faz `if (typeof echarts === 'undefined') return;` — mas atacado/clientes.php nao carregava ECharts (outras views como atacado_dashboard/index.php carregam via <script src="/assets/js/echarts.min.js"></script>). Adicionada a tag script pra ECharts na view antes do IIFE do fetch. Validado local no L.A. Silva: donut renderiza com 151 clientes classe A (79,96%), 134 B (15,01%), 180 C (5,03%) — Pareto perfeito.
    // v1.5.43 (2026-07-02) — FIX tela /atacado/pedidos que mostrava sempre "0 pedidos" no L.A. Silva. 3 bugs empilhados no AtacadoPedidosController: (1) SQL usava P.DATA_EMISSAO mas coluna real no schema Fire eh P.DT_EMISSAO (validado: L.A. Silva tem 8001 pedidos historicos, 1985 nos ultimos 12m, ultimo em 01/07/2026). (2) Filtro default 'aberto' + tabela sem coluna STATUS = whereStatus retornava FALSE sempre → 0 linhas. Agora quando STATUS nao existe no schema, filtro 'aberto' vira 1=1 (mostra tudo). (3) Filtro COMPRA_VENDA='V' filtrava so 3 dos 1985 pedidos porque L.A. Silva usa 'P' (Pedido) e 'O' (Orcamento) — nao o padrao 'V'. Trocado pra excluir apenas 'C' (Compras) via <> 'C' — captura V, P, O e qualquer outra variacao futura. Descoberta de coluna via RDB\$RELATION_FIELDS mantida (schema-safe).
    // v1.5.42 (2026-07-02) — FIX SQLs do modulo Atacado inventavam colunas que nao existem no schema Fire (varia por cliente). Auditoria completa validada contra L.A. Silva Atacadista (v1.5.40 mostrava tabela vazia com R\$ 0,00 em Curva ABC e /api/atacado/clientes retornava []). CORRIGIDO: (1) AtacadoRepository::listarClientesComABC — trocava p.CNPJ_CPF/CIDADE/UF (inexistentes) por p.CNPJ+p.CPF + JOIN CIDADES (COD_CIDADES/DESCRICAO/UF). (2) AtacadoRepository::detalheCliente — mesmas correcoes de PESSOAS + p.FONE_PRINC/p.EMAIL_PRINC1. (3) AtacadoRepository::topItensCliente — JOIN correto NOTAS_CAB × NOTAS_ITENS por chave composta (COD_EMPRESA+COD_TIPO_OPERACAO+NR_NOTA+SERIE, NAO por COD_NOTA_CAB inexistente) + usa ni.DESCRICAO direto (NOTAS_ITENS tem DESCRICAO, ITENS_ESTOQUE nao tem) + QTD*PRECO_UNIT (NOTAS_ITENS nao tem VALOR_TOTAL). (4) AtacadoCurvaAbcController — antes so importava VendasRepository sem usar; agora instancia AtacadoRepository e chama curvaAbcClientes(100)+curvaAbcProdutos(100), normaliza formato pro que a view espera (NOME/TOTAL/PERC/PERC_ACUM/CLASSE). Validado contra L.A. Silva: 465 clientes classificados, R\$ 5.08M em clientes ABC, R\$ 5.91M em produtos ABC, LATICINIO BELA VISTA/MARAVILHA-SC no topo.
    // v1.5.41 (2026-07-02) — FIX /atacado/clientes que mostrava HTTP 404 no card "Carteira de clientes". A view atacado/clientes.php faz fetch em /api/atacado/clientes pra popular a tabela com sort/busca/export, mas essa rota nao estava mapeada no router (so /api/atacado/dashboard existia). Adicionado: (1) metodo apiData() no AtacadoClientesController que retorna JSON {ok,linhas} com listarClientesComABC + indiceAtrasoPorCliente mergeados (alias CURVA_ABC→ABC e VALOR_VENCIDO→ATRASO_RS pra bater com o JS); (2) rota GET /api/atacado/clientes no public/index.php. Zero impacto pras outras rotas.
    // v1.5.40 (2026-07-02) — FIX rotas B2B (Clientes, Recebiveis, Pedidos, Curva ABC) que estouravam "Call to undefined method". Os Controllers foram gerados na v1.5.36 mas dependiam de metodos que nao existiam nos Repositories. Implementados 9 metodos novos + 1 expansao: AtacadoRepository ganhou listarClientesComABC (com classificacao 80/15/5), detalheCliente(id), historicoComprasCliente(id), topItensCliente(id), evolucaoMensalCliente(id). RecebiveisRepository ganhou aging(clienteId) EXPANDIDO (aceita opcional), detalhado(clienteId,faixa), clientesComTitulos, totais(clienteId), indiceAtrasoPorCliente, titulosPorCliente(id), totalVencidoCliente(id). Todos com try/catch defensivo (schema-safe) + UTF-8 fix + limites (FIRST 20-500). Validado local contra L.A. Silva Atacadista via VPN: /atacado/clientes (57k bytes), /atacado/pedidos (33k), /atacado/curva-abc (37k), /atacado/recebiveis (31k) — todos HTTP 200.
    // v1.5.39 (2026-07-01) — FIX FINAL Painel Atacado: o arquivo /assets/js/atacado-dashboard.js NAO EXISTIA no repo (esquecido pelo workflow que gerou o segmento Atacado). A view atacado_dashboard/index.php carrega esse JS pra popular KPIs+graficos via /api/atacado/dashboard, e sem ele todos KPIs ficavam "--" pra sempre — backend funcionava, controller retornava JSON correto, mas ninguem lia. Criado o JS completo: (1) fetch em window.__ATACADO.apiUrl com credenciais; (2) 5 KPIs preenchidos com deltas semanticos (verde/vermelho); (3) 4 graficos ECharts — Faturamento mensal (linha), Top 10 Clientes (barras horizontais), Top 10 Produtos (barras horizontais), Mix Cond Pgto (donut); (4) badge "Conectado" no topo com feedback visual; (5) resize automatico. Validado local com banco Importados real: R$ 46k mes, 3256 clientes, todos graficos renderizando.
    // v1.5.38 (2026-07-01) — REHOTFIX v1.5.37 (que quebrou o autoload do AtacadoRepository): meu proprio alias vendasPorUf() colidia com vendasPorUF() ja existente (PHP method names sao case-insensitive → Fatal Error "Cannot redeclare"). Removido alias — chamada do Controller $repo->vendasPorUf(12) continua funcionando porque PHP nao diferencia case em nome de metodo. Resto dos fixes v1.5.37 mantidos (kpisTopo, mixCondicaoPagamento, aging, construtor tolerante).
    // v1.5.37 (2026-07-01) — HOTFIX Painel Atacado (dashboard mostrava "--" em todos KPIs): (1) AtacadoRepository ganhou metodo kpisTopo() que agrega os 4 KPIs (Faturamento, Ticket, Clientes ativos, NFe emitidas) — antes Controller chamava kpisTopo() que nao existia → 500. (2) Aliases: mixCondicaoPagamento() e vendasPorUf() (Controller usava case/nome diferente do Repo). (3) RecebiveisRepository ganhou alias aging() (Controller chamava assim mas metodo era agingRecebiveis). (4) Ambos os Repos com construtor tolerante — aceita (int, int) que o Controller passa erroneamente como (ano, mes) sem quebrar. Zero mudanca em Controllers/Views — fix so no Repository. Todos os KPIs e graficos agora carregam.
    // v1.5.36 (2026-07-01) — SEGMENTO ATACADO: novo AtacadoDashboardController + 4 controllers B2B (Clientes, Pedidos, Recebiveis, CurvaAbc) + AtacadoRepository + RecebiveisRepository. Tema 'atacado_corp' azul corporativo. Home /: quando segmento IN (atacado, distribuidora) roteia pra AtacadoDashboard; senão continua no DashboardController normal (zero impacto pra bar/varejo/sorveteria/utilidades). FbAuth ganhou SEGMENTOS_B2B e SEGMENTOS_SEM_NFCE. Sidebar filtrado. Wizard universal ganhou opção 5 Atacadista B2B.
    // v1.5.35 (2026-06-30) — SISTEMA DE LOG DE ATUALIZACOES: cliente nao precisa mais de PowerShell pra diagnosticar OTA. (1) Novo service App\Services\Atualizacao\Logger.php coleta snapshot completo do ambiente: hostname, install_root, app_root, PHP version, e estado de 9 arquivos criticos com bytes+mtime. (2) AtualizacaoController::aplicarApi() refatorado: registra cada etapa (download, extracao, robocopy, force_copy_criticos, opcache_reset), snapshot antes/depois, e em caso de falha tambem grava. (3) Force-copy individual de arquivos criticos (config/backup-notificacao.php, config/backup-oauth.php, Versao, BackupController, Views, SmtpClient, Notificador, Crypto) pra contornar robocopy que pula arquivos as vezes. (4) Logger grava log local em storage/logs/atualizacao/<ts>-<versao>.json E envia pro painel central com tipo='atualizacao'. (5) Painel central (api.php) agora aceita tipo='atualizacao' e salva em storage/atualizacoes/<host>.jsonl. (6) Nova tela atualizacoes.php no painel central: lista clientes, ultima versao, etapas detalhadas, snapshot dos arquivos criticos com status existe/falta, link no header do painel principal. Resultado: posso descobrir qualquer problema de OTA em qualquer cliente sem entrar no cliente.
    // v1.5.34 (2026-06-30) — Hardening do fallback de email: (1) SmtpClient.php tambem agora usa ?: (Elvis) em vez de ?? — antes ?? deixava passar string vazia e quebrava envio quando job/config tinha campo branco. (2) View /backup aba email blindada contra Warning quando $jobAtivo=null (cliente sem nenhum job ainda) — usava !empty($jobAtivo['email']['smtp_senha']) que dava warning e quebrava o render da senha pra baixo. Trocado pra is_array+?? null seguro.
    // v1.5.33 (2026-06-30) — FIX cascata de fallback: tela /backup aba email vinha com campos vazios quando o JSON do job tinha string vazia (jobs antigos salvavam ''). Causa: operador ?? do PHP so substitui null, nao string vazia. Trocado pra ?: (Elvis) na view e no emailTestar do controller. Agora De/Para/Servidor/Usuario realmente caem no fallback global Fire Sistemas. Tambem corrige o erro "Destinatario (Para) obrigatorio" no botao Enviar e-mail teste.
    // v1.5.32 (2026-06-30) — Senha SMTP global Fire Sistemas preenchida no config/backup-notificacao.php (App Password "BI Fire Global" do Gmail derlivon89@gmail.com). Todos os clientes que atualizarem pra essa versao recebem a senha junto e ja podem clicar em "Enviar e-mail teste" sem digitar nada.
    // v1.5.31 (2026-06-30) — Senha SMTP GLOBAL Fire Sistemas: configurar 1 vez em C:\BI\config\backup-notificacao.php (campo smtp_pass) e ao publicar OTA TODOS os clientes recebem. Tela /backup aba E-mail detecta automaticamente se senha global existe e mostra "✓ Senha SMTP global configurada por Master Automacao — clique em Enviar e-mail teste direto". Cascata: senha do form > senha do job (encriptada) > senha global (plaintext do config Fire). Cliente individual ainda pode sobrescrever digitando senha propria (que ai vai ser encriptada no JSON dele).
    // v1.5.30 (2026-06-30) — UX teste de email: (1) Resposta do servidor SMTP em caso de falha agora aparece DESTACADA em caixa branca com borda vermelha — nao precisa mais rolar o log pra ver o codigo do erro (ex: "535-5.7.8 Username and Password not accepted"). (2) Log completo retornado (nao trunca mais em 2KB). (3) Backend extrai a ultima linha 4xx/5xx do log e envia como campo separado erro_servidor pro JSON.
    // v1.5.29 (2026-06-30) — Fix usabilidade SMTP: (1) senha agora tem TODOS os whitespaces removidos antes de usar — Gmail mostra a App Password com espacos a cada 4 chars ("abcd efgh ijkl mnop") e a colagem direta gerava 535 (auth refused). Agora pode colar com ou sem espaco. (2) Mensagem de erro de auth no Gmail/Google agora inclui dica especifica: "Gmail exige App Password (16 chars) em myaccount.google.com/apppasswords".
    // v1.5.28 (2026-06-30) — FASE 5 ATIVADA: notificacao de backup por email funcional. (1) Botao "Enviar e-mail teste" na aba /backup → email: envia teste em tempo real, mostra sucesso/falha inline com log SMTP expandivel. (2) Novo endpoint POST /api/backup/email-testar. (3) Senha SMTP agora ENCRIPTADA no JSON do job (AES-256-CBC, chave derivada de hostname+install root — nao migra entre maquinas). (4) Notificador::enviarEmailSeErro reescrito: usa SMTP do JOB primeiro (com decrypt), fallback global; modo=sempre dispara tambem em sucesso. (5) BackupRunner agora dispara email em sucesso (Notificador filtra por modo). (6) Banner "Fase 5" da view removido.
    // v1.5.27 (2026-06-30) — Polimento de copy: tela /configurar/atualizacao quando ja esta atualizado mostrava "Voce ja esta na versao mais recente (vX.Y.Z)" — redundante com o card de cima que ja exibe a versao instalada. Agora mostra apenas "Sistema atualizado — nenhuma versao nova disponivel".
    // v1.5.26 (2026-06-30) — Tela /backup aba "Notificação por E-mail" agora ja abre com TODOS os campos pre-preenchidos: De/Para/Servidor SMTP/Porta/Usuario carregados de config/backup-notificacao.php (Fire Sistemas centralizada). Usuario so digita a senha. Cascata de fallback: $jobAtivo['email'] > $cfgGlobalEmail > default fixo. Senha CONTINUA sempre vazia por seguranca (autocomplete=new-password adicionado pra bloquear autofill do browser).
    // v1.5.25 (2026-06-30) — Tambem: FIX tela /configurar/atualizacao mostrava "Versao instalada v1.5.23" no card de cima mas "Voce ja esta na versao mais recente (v1.5.24)" no resultado da verificacao. Causa: OPcache servia Versao::ATUAL stale quando a view era renderizada (1.5.23) mas o endpoint /verificar lia fresco depois (1.5.24). Fix: opcache_invalidate(Versao.php) em index() e verificarApi() + JS agora sincroniza dinamicamente o card "Versao instalada" com d.atual retornado pelo backend.
    // v1.5.25 (2026-06-30) — STATUS "EM ANDAMENTO" no sistema de backup (3 partes): (1) BI cliente — novo card LARANJA "🔄 Backup em andamento" na tela /backup (qualquer aba) com tempo decorrido, estagio atual (gerando .fbk / comprimindo / enviando Drive), tamanho atual do arquivo crescendo em tempo real e PID; polling AJAX 5s no novo endpoint GET /api/backup/status-agora; botao "Executar Agora" some enquanto rodando, reaparece + reload da pagina ao terminar. (2) Heartbeat — novo metodo Notificador::enviarHeartbeat($job, $estagio, $tamanho_atual, $iniciado_em) com throttle 25s; BackupRunner emite em pontos-chave: iniciando, gerando_fbk (loop a cada 30s durante o gbak via novo callback no FirebirdBackup::executar), comprimindo, enviando_drive; payload sobrescreve mesmo storage/<host>__<job_id>.json com status='em_andamento'. (3) Painel Central — bolinha AZUL PULSANTE quando heartbeat < 90s, tag "EM ANDAMENTO" com estagio, tempo decorrido e tamanho_atual crescendo no detalhe; aviso laranja "⚠️ Possivelmente travado" quando >5min sem heartbeat (com PID pra investigar); KPI extra "🔄 EM ANDAMENTO" so aparece quando ha backup rodando; refresh dinamico 15s durante backup vs 60s normal; ordenacao traz em-andamento pro topo. Motivacao: cliente em 30/06 cancelou/reiniciou processos varias vezes achando que travou (banco 4-7GB demora 30-60min). Inclui v1.5.24.
    // v1.5.24 (2026-06-30) — FIX CRITICO bug que eu mesmo introduzi na v1.5.22: pipes do proc_open() do FirebirdBackup ficavam BLOQUEANTES → loop com fgets($pipes[1]) ficava preso esperando dados do stdout do gbak. Como gbak escreve TUDO no stderr (nao stdout), o stdout fica vazio e o fgets nunca retorna. Resultado: PHP travava esperando, gbak era morto pelo SO, .fbk ficava truncado em ~100MB (banco de 7GB). Caso confirmado Real Prime 30/06: 2 backups seguidos com 129MB cada quando deveriam ter 4-6GB. Fix: stream_set_blocking(false) nos pipes + stream_select com timeout de 5s + proc_get_status pra detectar fim do processo. Agora gbak roda do comeco ao fim sem travamento.
    // v1.5.23 (2026-06-30) — FIX CRITICO upload Drive arquivos >200MB. HttpClient.php fazia file_get_contents() do arquivo INTEIRO antes de enviar — pra .fbk de 4GB tentava alocar 4GB de RAM mas memory_limit eh 512MB → PHP morria silenciosamente (com @ suprimindo o warning) → upload nunca acontecia → backup local OK mas Drive vazio. Caso confirmado Real Prime 30/06/2026: backup 4GB ficava local mas nada subia. Agora HttpClient detecta arquivo grande e usa curl.exe do Windows (C:\\Windows\\System32\\curl.exe — vem no Win10/11) que faz streaming nativo. Timeout aumentado pra 2h (minimo) pra dar tempo de upload em internet lenta de bar (5-20 Mbps × 4GB = 30-110 min).
    // v1.5.22 (2026-06-30) — FIX CRITICO de backup pra bancos grandes (>15 min): (1) ANTI-COLISAO — BackupRunner cria lock file storage/backup/locks/<id>.lock antes de rodar; se ja existe lock <2h, pula tick sem incrementar tentativa. Bug confirmado no Real Prime (banco 4GB) — gbak rodando ~15 min E retry de 5 min disparava 2o gbak simultaneo, ambos colidiam no lock do .fbk e marcavam falha. (2) STDERR CAPTURADO — trocado popen() por proc_open() pra capturar stderr do gbak. Antes: campo log do erro vinha vazio (gbak escreve no stderr, popen so pega stdout). Agora painel central mostra a mensagem real do erro. (3) Lock auto-cleanup via register_shutdown_function pra nao deixar orfao em crash.
    // v1.5.21 (2026-06-30) — FIX contador "Tentativa 4 de 3" no painel de backups. Antes: BackupRunner sempre incrementava tentativa_atual a partir do valor anterior, mesmo quando a execucao foi disparada por HORARIO AGENDADO (nao por retry). Resultado: depois de 3 falhas no dia anterior + dispara no horario do dia seguinte = "4 de 3" mostrado. Agora reseta contador pra 0 quando nao ha tentar_novamente_em ativo, e so incrementa quando o tick eh do retry mesmo.
    // v1.5.20 (2026-06-29) — Modulo Cozinha ganhou 3 controles na tela /cozinha/config: (1) SWITCH ATIVO/DESATIVADO — quando OFF esconde o menu Cozinha do sistema inteiro (gestor pode reativar via mesma tela que continua acessivel); (2) CONFIRMACAO antes do clique Pronto — evita cozinheiro marcar errado; (3) BOTAO VOLTAR pros ultimos finalizados nos ultimos X min (default 10) — desfaz UPDATE em LANC_MESA (PRONTO/PRONTOFIM/PRONTOQUEM/PRODUINI/PRODUZINDO=NULL) + remove do kds_finalizacoes.json + item volta pra fila. Novo endpoint POST /api/cozinha/desfazer. Flags persistem em storage/kds_config.json (ativo, confirmar_pronto, permitir_desfazer, minutos_desfazer).
    // v1.5.19 (2026-06-29) — Sistema de backup com retry automatico + log completo no painel central. BackupRunner agora reexecuta o backup em caso de falha: 1min, 5min, 15min (max 3 tentativas) — antes era 1 chance unica e desistia. Notificador envia tambem o stderr/log completo do gbak (truncado em 6KB) + numero da tentativa + proxima_tentativa_em + intervalo_esperado_horas (deriva do agendamento e o painel usa pra calcular ATRASADO real). Painel /central/ mostra "Tentativa N de 3 - proxima em HH:MM" no card de erro + bloco expandivel "Log detalhado do gbak". Email so dispara na 3a falha consecutiva (evita spam de retry). Scheduler::deveRodar() agora prioriza retry agendado antes de checar agenda normal.
    // v1.5.18 (2026-06-29) — FIX CRITICO no agendador de backup: Scheduler::deveRodar() tinha janela de 2 minutos (linha 39: $diff <= 0 && $diff >= -2) mas o worker-tick e disparado pelo Windows Task Scheduler a cada 5 min — se tick caisse em 06:03 a janela 06:00 era IGNORADA pra sempre. Caso confirmado: Buteco Toda Hora com 06:00 e 22:40 fixos ficava 1+ dia sem backup mesmo agendador "ATIVO". Fix: janela aumentada pra 60 min apos horario configurado + anti-duplicacao confere ultimo_run >= timestamp do horario (em vez do antigo "5 min"). Tambem corrigido Painel Central de Backups (painel-central-servidor/index.php) que tinha threshold de ATRASADO hardcoded em 48h — agora respeita intervalo_esperado_horas (default 28h pra diario). Antes: cliente diario que perdesse 1 ciclo ficava VERDE por mais 24h, mentindo pro gestor.
    // v1.5.17 (2026-06-29) — Card "Consultar Cupom Fiscal" no Leitor mobile ganhou campo de busca por NUMERO da nota (quando operador so tem o numero, sem o QR code). Filtro local em tempo real pelos ultimos 30 cupons; se nao achar, lupa busca no servidor por NR_NOTA exato. Backend nfceRecentesApi agora aceita ?nr=<numero>. Lista aumentou de 10 → 30 ultimos. Se a busca por numero retornar 1 unico match exato, abre o cupom direto sem clique extra.
    // v1.5.16 (2026-06-29) — Card "Tema" removido da home do Leitor. Acesso a temas continua disponivel via icone discreto 🎨 no header (cabecalho do app).
    // v1.5.15 (2026-06-29) — Botao da lupa (busca por codigo no Leitor home) agora usa cor do tema (var(--accent)) em vez de var(--btn) fixo. Antes, no tema Cyber Neon a borda do Escanear era ciano mas a lupa era rosa shocking - destoava. Agora lupa fica da mesma cor do acento principal do tema selecionado em qualquer skin (Cyber, Fogo, Tropical, Esmeralda, etc).
    // v1.5.14 (2026-06-29) — FIX CRITICO no Leitor: login via PIN tinha 5 perms HARDCODED (consulta/estoque/preco/dados/foto) e ignorava TOTALMENTE o que era marcado em /leitor-admin/permissoes. Resultado: marcava fiscal_caixa/inventario/consultar_cupom no admin e nada aparecia no celular logado por PIN. Agora PIN usa permissoesDoUsuario('_PIN_') que cai no _default do config (fallback automatico quando chave nao existe) — admin passa a controlar PIN tambem. Scanner() tambem recarrega perms pra PIN (antes so recarregava pra ERP). Bug confirmado 29/06 com o Real Prime/Importados onde Derlivon marcou tudo mas continuou vendo so scanner.
    // v1.5.13 (2026-06-29) — Tela /leitor-admin/permissoes ficou MUITO mais legível: perms agrupadas por categoria (Consulta básica / Edição / Inventário / Fiscal de Caixa), botões "Marcar todas / Desmarcar todas" no topo + "Marcar / Limpar" por grupo, perm MÃE marcada com estrela colorida e borda mais grossa, sub-perms indentadas com aviso "↳ requer X". Alerta amarelo automático quando sub-perms estão marcadas mas a mãe NÃO está (situação que deixa as opções órfãs no Leitor mobile). Inspirado no caso Importados 29/06 onde fiscal_risco/fiscal_aprovar foram marcados sem fiscal_caixa e nada aparecia no celular. Config GRUPOS_PERMISSOES em LeitorPermissoesController define a estrutura.
    // v1.5.12 (2026-06-28) — DEBUG visual da busca: pfBuscarPorTexto agora mostra toast "Buscando 'XXX'..." E checa se #pfMatchesBusca existe (se não, mostra toast "JS antigo no cache" — sinal claro de cache stale). Catch de erro de rede mostra mensagem real (em vez de só "Erro ao buscar" genérico). Endpoint backend buscarQuick foi VALIDADO direto via PHP CLI: retorna 5 itens pra 'det', 'YPE', 'arroz' OK. Bug do cliente = cache do WebView servindo JS antigo (pfBuscarCod/pfBuscarPorTexto sem regex /^\d+$/).
    // v1.5.11 (2026-06-28) — Busca em tempo real (debounce 350ms oninput) + lupa.
    // v1.5.10 (2026-06-28) — REWRITE foto do produto: <label for=inputFotoFilePainel> + compactarFoto + processarFotoSelecionada. Removido modal Bootstrap + getUserMedia do fluxo foto.
    // v1.5.9 (2026-06-28) — Link "Gerenciar Permissões" no /leitor/config aponta pra /leitor-admin/permissoes.
    // v1.5.8 (2026-06-28) — Toast sobe pra bottom safe-area-inset-bottom+5rem (não cola mais na barra dos 3 botões Android).
    // v1.5.7 (2026-06-28) — Scanner overlay compactado (max-width 480px, aspectRatio 4:3, qrbox 72%×28%).
    // v1.5.6 (2026-06-28) — Fix scanner abrir só com botão Voltar (substituído modal Bootstrap por overlay direto, chamada síncrona no onclick preserva user-gesture).
    // v1.5.5 (2026-06-28) — REWRITE scanner com html5-qrcode lib (Html5Qrcode.start substitui Quagga2+BarcodeDetector artesanal).
    // v1.5.4 (2026-06-28) — Painel Foto sem botão "Câmera do navegador" — só ESCANEAR PRODUTO direto.
    // v1.5.3 (2026-06-28) — Fix scanner: reusa stream do teste _camHtml5Funciona em iniciarScanner; câmera abre na hora, sem precisar de back button.
    // v1.5.2 (2026-06-28) — Voltou botão ESCANEAR PRODUTO no painel Foto + regex /^\\d+$/ qualquer número direto pro produto.
    // v1.5.1 (2026-06-28) — Painel Foto sem botões de scanner + abrirModalFoto auto-aciona aba Arquivo se HTML5 não funciona.
    // v1.5.0 (2026-06-28) — _camHtml5Funciona() — teste REAL de câmera HTML5 com timeout, sessionStorage cache 5min. async em abrirScanner/invEscanear/pfEscanear.
    // v1.4.99 (2026-06-28) — FIX CRÍTICO Leitor scanner. INVERTIDA prioridade em 3 funções (abrirScanner, invEscanear, pfEscanear). Fix SQL: removida coluna i.INATIVO de buscarQuick e listarSemFoto.
    // v1.4.97 (2026-06-28) — Leitor painel Foto: novo botão "Câmera do navegador" (rosa pontilhado) que força fluxo getUserMedia + BarcodeDetector / Quagga2, IGNORANDO o LeitorApp.abrirScanner do APK (que dispara Intent pro Barcode Scanner externo do Android). Caso AchadinhosA 28/06: APK abria app externo ZXing "Barcode Scanner" que dava erro "câmera do Android encontrou um problema" — alternativa via webview resolve.
    // v1.4.96 (2026-06-28) — Leitor: 3 fixes no painel Atualizar Foto. (1) Visual ROSA hardcoded (#1a0d18 fundo, header #3d1a2e + título #ec4899, gradiente no body) — antes parecia o home porque usava var(--bg)/(--card) iguais. (2) Tema resiliente: aplicarTema() roda em pageshow/visibilitychange/DOMContentLoaded (cobre PWA/race), reaplica ao fechar qualquer painel; localStorage com fallback 'leitor_tema_default' pra sessão sem usuário. (3) Cache-busting agressivo: LeitorController::scanner() emite Cache-Control no-store; <head> tem meta http-equiv + manifest com ?v=ATUAL.
    // v1.4.95 (2026-06-28) — Leitor: painel "Atualizar Foto" com cara igual ao Inventário (header rosa, abas Escanear/Sem Foto/Histórico, badge contador, botão grande ESCANEAR + input código + lupa). 3 recursos extras: (1) aba "Sem Foto" lista produtos sem foto com busca por descrição/código (novo endpoint GET /api/produtos/sem-foto, novo método ProdutosRepository::listarSemFoto), (2) aba "Histórico" mostra thumbs das fotos tiradas na sessão atual com contador, (3) chips de atalho. Back button do Android + ESC fecham o painel.
    // v1.4.94 (2026-06-28) — Leitor: atalho "Atualizar Foto" agora ABRE A CÂMERA do APK (ZXing nativo) ou getUserMedia no browser, em vez de prompt() de texto. Flag _fotoScanMode intercepta tanto onNativeBarcode quanto abrirDetalhe — quando ativa, lê código → busca produto → modal foto direto, pulando o painel cheio. Mesmo padrão do _invScanMode do Inventário.
    // v1.4.93 (2026-06-28) — Leitor: card "Atualizar Foto" no menu home (perm alterar_foto) com fluxo de cadastro em lote (bipa → modal → pergunta próximo). Placeholder de foto agora clicável quando produto sem foto. UX da tela /leitor-admin/permissoes: toggle "Permissão personalizada" pré-popula com _default + botão "Restaurar padrão". CRÍTICO segurança: atualizarFotoApi agora exige perm alterar_foto + registra audit 'atualizar_foto' em leitor_audit.jsonl + valida limite 5 MB.
    // v1.4.92 (2026-06-28) — Fix: instalador agora copia C:\BI\tools\ para staging\app\tools\ (workers do backup: worker-tick.php, worker-job.php, scheduler-cli.php, worker.php, .bat e .vbs). Bug do caso AchadinhosA: tela /backup → "Ativar agendador" mostrava "worker-tick.php não encontrado" porque build-universal só copiava vc_redist da pasta tools mas ignorava o resto. Smoke test pré-compile agora valida worker-tick.php e worker-job.php no staging. Inclui v1.4.91.
    // v1.4.91 (2026-06-28) — Fix bug wizard não gravava database.php (caso AchadinhosA — pdo_firebird funcionando mas BI tentando abrir C:\FIRESOFT\FIRE.FDB em vez do caminho preenchido). 2 bugs corrigidos: (1) instalador "preservava" database.php existente mesmo quando cliente preencheu novo caminho no wizard → agora SEMPRE sobrescreve com dados do wizard (com backup .bak-instalador do antigo); (2) gravava apenas em {app}\app\config\ (layout antigo) mas BI PSR-4 lê de {app}\app\app\config\ → agora grava em AMBOS lugares. Multi-loja (lojas.php) continua preservado via Excludes do [Files]. Inclui v1.4.90.
    // v1.4.90 (2026-06-28) — ICU 52 + VC++ 2010 runtime embarcados (resolução final caso AchadinhosA): fbclient.dll 3.0 depende de icudt52/icuin52/icuuc52 (ICU 52 Unicode) + msvcp100/msvcr100 (VC++ 2010 runtime) + ib_util.dll. Em PCs que só têm Firebird 2.5 servidor instalado, essas DLLs faltavam → pdo_firebird falhava com "módulo não encontrado" mesmo com fbclient 3.0 + UCRT + vc_redist 2022 OK. build-universal agora copia essas 7 DLLs do Firebird 3.0 oficial pras 3 pastas (php/, php/ext/, Apache24/bin/) e valida no smoke test pré-compile. Cliente AchadinhosA resolveu manualmente com kit FIX-ICU.zip. Inclui v1.4.89.
    // v1.4.89 (2026-06-28) — UCRT app-local no instalador (caso AchadinhosA continuação): cliente em Windows Server 2019 build 17763 SEM updates não conseguia carregar pdo_firebird mesmo com fbclient 3.0 + vc_redist 2022 instalados. Diagnóstico via php-cli mostrou "Não foi possível encontrar o módulo" mesmo com a DLL presente — dependência de Universal C Runtime antiga. Solução Microsoft-oficial: app-local deployment do UCRT redistributable do Windows SDK (42 DLLs api-ms-win-*.dll, ~1.8 MB) copiadas para php/, php/ext/ e Apache24/bin/. Compatível com qualquer Win 10+ e Server 2016+. Inclui v1.4.88.
    // v1.4.88 (2026-06-28) — Instalador SELF-CONTAINED de verdade (caso AchadinhosA): (1) build-universal NÃO usa mais C:\BI\php como fonte de fbclient.dll — só o Firebird 3.0 OFICIAL (C:\Program Files\Firebird\Firebird_3_0); (2) robocopy de C:\BI\php agora EXCLUI fbclient/zlib/vcruntime/msvcp via /XF — evita 2.5 antiga contaminar staging silenciosamente; (3) novo smoke test pós-staging valida que TODAS as 3 fbclient.dll (php\, php\ext\, Apache24\bin\) são 3.0+ e ABORTA o build se qualquer uma sair 2.5; (4) valida VC++ runtime nas 3 pastas também (defesa em profundidade — se vc_redist não instalar no cliente, DLL local funciona). Cliente baixava EXE com fbclient 2.5 contaminada → pdo_firebird não carregava no PHP 8.2 → "could not find driver". Inclui v1.4.87.
    // v1.4.87 (2026-06-28) — Instalador UNIVERSAL anti-trava no httpd.conf: (1) auditoria automática das DLLs críticas (fbclient/vcruntime/msvcp/zlib/php8apache2_4) com versão de cada uma logada no install.log antes da validação; (2) fallback automático: se httpd.exe -t falhar com "cannot load X.dll" no 1º teste, o instalador regenera httpd.conf comentando todos os LoadFile e tenta de novo (Apache resolve via PATH/Apache24\bin); (3) MsgBox de erro final agora MOSTRA a saída real do httpd.exe -t em vez de mandar abrir o log. Caso Achadinhos Aragarças (instalador rejeitando httpd.conf no servidor da loja) motivou a correção. Inclui v1.4.86.
    // v1.4.86 (2026-06-28) — Diagnóstico de erro de loja: o badge "Sem dados no período" virou enganoso (cliente perguntou pq apareceu em 3 lojas que estavam OFFLINE, não sem vendas). Agora ConsolidadoController classifica a mensagem do PDOException em offline / login / banco / desconhecido (regex sobre common Firebird errors) e a view renderiza card colorido por tipo: 🔴 "Servidor offline" (vermelho, ícone wifi-off), 🟡 "Login do banco rejeitou" (amarelo), 🟡 "Banco não encontrado" (amarelo, ícone database), ⚪ "Erro ao consultar" (cinza). Help text explicativo + `<details>` colapsável com a mensagem técnica do Firebird. Inclui v1.4.85.
    // v1.4.85 (2026-06-28) — Status operacional por loja (ativa | pré-abertura | inativa): novo campo 'status' em config/lojas.php, default 'ativa'. ConsolidadoController agora itera só Lojas::ativas() — lojas em pré-abertura ou inativas saem do consolidado (evita poluir resumo com dados de teste/setup). Banner azul no topo da tela /consolidado lista lojas em pré-abertura com link pra ativar. UI nova em /configurar/lojas: 3 radios (Ativa / Pré-abertura / Inativa) por loja. Caso real: cliente Achadinhos 20A (Aragarças) aparecia com R$ 3.032 mas a loja só abre mês que vem; agora marca como pré-abertura e some do resumo. Inclui v1.4.84.
    // v1.4.84 (2026-06-28) — Modo "Versão Única" no backup do Drive (estilo Uranium): quando ligado, o backup usa nome FIXO ("Cliente - backup.fbk.tar.gz") e o GoogleDriveDriver detecta arquivo existente pelo nome+pasta_id, fazendo PATCH (update) em vez de POST (create). O Google Drive mantém automaticamente 100 versões anteriores (até 30 dias) acessíveis via "Gerenciar versões" — pasta sempre limpa com 1 arquivo só, histórico preservado sem custo de espaço visível. Novo toggle no editor de Job /backup → Opções; ATIVO por DEFAULT no botão Setup Automático (1 clique). Inclui v1.4.83.
    // v1.4.83 (2026-06-28) — DRILL-DOWN COMPLETO da sangria do caixa: novo método DashboardRepository::sangriasDetalheNoIntervalo() lê LANC_TESOURARIA retornando 1 linha por retirada com DATA, HORA, USUARIO (quem fez), HISTORICO (motivo / pra onde foi) e VALOR. Em cada loja vira `<details>` clicável "Ver quem, quando e por quê" expandindo tabela; no Resumo Geral consolidado abre em aberto por default com coluna LOJA, destaque amarelo em retiradas > 20% do total. Pedido do cliente: "preciso saber QUEM fez a sangria com detalhes, motivo, pra onde foi". Linhas ordenadas por valor desc. Limite 500 por loja (evita payload gigante). Inclui v1.4.82.
    // v1.4.82 (2026-06-28) — Card SANGRIA DO CAIXA no Resumo Geral: novo método DashboardRepository::sangriasNoIntervalo() lê LANC_TESOURARIA SANGRIA_SUPRIM='S' no período. Aparece como (1) tag laranja "💸 Sangria R$ X · N retiradas · Y% do fat." inline em cada loja, e (2) card destacado no grid de 4 KPIs do Resumo Geral. Quando sangria >= 5% do faturamento, vira ALERTA VERMELHO (🚨 + cores fee2e2/dc2626) indicando possível abuso. Card só aparece se total > 0 (cliente sem LANC_TESOURARIA não vê tela alterada). Inclui v1.4.81.
    // v1.4.81 (2026-06-28) — Cards Comparativos do /consolidado mostram AGORA + ANTES em vez de só ANTES. Feedback do cliente: "qual foi o valor da semana passada?" — porque na v1.4.80 só aparecia "Foi: R$ X" do anterior, e o gestor tinha que calcular o atual mentalmente (anterior - diferença). Agora layout em grid 2 colunas: "Agora R$ X · N vendas / Antes R$ Y · M vendas / período-label", sem perder o verdito ("R$ X a MENOS que semana passada"). Aplicado em card POR LOJA e CONSOLIDADO. Inclui v1.4.80.
    // v1.4.80 (2026-06-28) — Tela /consolidado (Resumo Geral) LIMPA: (1) header da loja não repete mais "R$ X faturamento" (já tá em destaque acima); (2) "% do total" agora só aparece quando faz sentido (multi-loja com participação < 100%) — antes em filtro de 1 loja só mostrava "100% do total" confuso; (3) Cards Comparativos (vs semana/mês/ano) não repetem mais o R$ atual (já está no header) — agora mostram "Foi: R$ X" do período anterior + verdito ("R$ Y a MENOS"). Cliente Real Prime reclamou de tela confusa em mobile. Inclui v1.4.79.
    // v1.4.79 (2026-06-27) — CRÍTICO: backup agora roda em BACKGROUND (não trava o BI). BackupController.executar() dispara tools/worker-job.php via start /B em background e retorna IMEDIATO (200 OK). Antes era síncrono — pra banco de 3 GB travava o BI por minutos/horas e nenhum usuário conseguia trabalhar. Inclui v1.4.78.
    // v1.4.78 (2026-06-27) — Token correto do painel central (hs2e7uk…0k5m, gerado pelo Setup v2 no servidor 24h) em config/backup-notificacao.php E hardcoded em Notificador.config(). Antes usava token velho (zZipSyjFk…) do PainelCentralSetup v1 local, que dava 403 token. Validado via curl POST. Inclui v1.4.77.
    // v1.4.77 (2026-06-27) — Link "Painel Central (servidor 24h)" no topbar de /backup agora aponta pra http://177.23.184.123:8085/ (porta nova do PHP server) em vez da antiga 8084/painel-backups/ (que servia file listing sem PHP). Inclui v1.4.76.
    // v1.4.76 (2026-06-27) — FALLBACK HARDCODED Fire Sistemas no BackupController.oauthPadrao() e Notificador.config(): mesmo se config/backup-oauth.php ou config/backup-notificacao.php não chegarem corretos via OTA (robocopy não sobrescrevendo, opcache segurando, etc), o BI cliente usa valores hardcoded centralizados — OAuth Drive Fire + painel central 177.23.184.123:8085 funcionam de qualquer jeito. Inclui v1.4.75.
    // v1.4.75 (2026-06-27) — Republish forçada de v1.4.74 pq Buteco baixou pacote ANTES do refresh_token ser preenchido. Buteco aplicou v1.4.74 com refresh_token vazio (continuava pedindo passo a passo OAuth). Bumpa versão pra forçar redownload com o config correto. Inclui v1.4.74.
    // v1.4.74 (2026-06-27) — config/backup-oauth.php agora vem com refresh_token preenchido (Drive Fire Sistemas pronto). AtualizacaoController.aplicarApi também copia tools/ do zip (antes só app/+public/, deixando o worker-tick.php fora do destino). Inclui v1.4.73.
    // v1.4.73 (2026-06-27) — Fix crítico do pacote OTA: agora também leva a pasta tools/ (com worker-tick.php) que era necessária pro agendador interno do backup funcionar. Sem ela, dava "worker-tick.php não encontrado" ao tentar Ativar Agendador em /backup. Inclui v1.4.72.
    // v1.4.72 (2026-06-27) — Republish v1.4.71 levando os configs centralizados Fire Sistemas: config/backup-oauth.php (Client ID + Secret + Refresh Token do Google Drive do Derlivon) e config/backup-notificacao.php (painel_url do servidor 24h porta 8085 + token compartilhado). Build-ota-pacote.ps1 agora também copia esses 2 arquivos pra staging/app/config/. Cliente final (ex.: Buteco) recebe BI já pré-configurado pra mandar status pro painel central e usar o Drive da Fire. Inclui v1.4.71.
    // v1.4.71 (2026-06-27) — Modulo de BACKUP completo: aba /backup em Configuração com perfil por loja, integração Google Drive OAuth (pasta_id e navegador de pastas), modos gbak/copia_fdb, compressão tar.gz (suporta GB), agendamento estilo Uranium (semana/mês/período/data com vários horários), worker Windows via Task Scheduler, notificação por email SMTP (apenas em falha, throttle 12h), POST pra painel central no servidor 24h (177.23.184.123:8085) com token compartilhado, dashboard de status local com cards (em dia/falha/atrasado/nunca). Inclui v1.4.70.
    // v1.4.70 (2026-06-27) — Rankings (Top 20 Grupos/Marcas/Produtos/Clientes) agora usam grid 1fr auto em vez de flex+max-width:62% — garante que o valor R$ SEMPRE aparece (antes em mobile valores grandes cortavam). Inclui v1.4.69.
    // v1.4.69 (2026-06-27) — Fix completo dos cards Comparativos em mobile: empilhados em 1 coluna, texto quebra correto (overflow-wrap:anywhere + word-break:break-word + max-width:100%), overflow-x:hidden no wrapper externo pra evitar estouro lateral. Botao Imprimir reativado em mobile (menor 36px). Card URL do manifesto colapsado em desktop + removido em mobile. Inclui v1.4.68.
    // v1.4.68 (2026-06-27) — Texto dentro dos cards comparativos agora QUEBRA corretamente (overflow-wrap + flex-wrap) — antes cortava lateralmente "Vendendo R$ X a MENOS que se..." em mobile. Inclui v1.4.67.
    // v1.4.67 (2026-06-27) — Comparativos (vs Semana / vs Mes / vs Ano) agora EMPILHADOS em mobile (uma coluna). Antes ficavam em 2-3 colunas e cortava lateralmente. Aplicado em ambos os blocos: por loja (.comparativos-grid-loja) e geral (.comparativos-grid-geral). Inclui v1.4.66.
    // v1.4.66 (2026-06-27) — Botao Imprimir/PDF (FAB) REATIVADO em mobile (foi desativado por engano na v1.4.63 — usuario precisava pra imprimir PDF da tela). Em mobile fica menor (36px) e mais discreto (opacity .75 + icone impressora em vez de download). Em desktop continua tamanho normal (48px). Inclui v1.4.65.
    // v1.4.65 (2026-06-27) — Card URL do manifesto agora fica COLAPSADO por default em desktop (clica "Configuracoes avancadas" pra abrir) e REMOVIDO em mobile. Cliente final nao ve mais info tecnica em tela nenhuma. Inclui v1.4.64.
    // v1.4.64 (2026-06-27) — Card URL do manifesto na tela /configurar/atualizacao agora eh REMOVIDO via JS em mobile/tablet (em vez de so escondido via CSS). Resolve caso de Service Worker do PWA segurar HTML/CSS antigo. Inclui v1.4.63.
    // v1.4.63 (2026-06-27) — Fix robusto do FAB Exportar em mobile: agora NAO eh criado em viewport < 992px (antes so escondia via CSS, mas cache de Service Worker do PWA segurava). Inclui v1.4.62.
    // v1.4.62 (2026-06-27) — Botao flutuante de Exportar (FAB laranja) escondido em mobile/tablet (cliente final no celular nao precisa). Visivel em desktop. Inclui v1.4.61.
    // v1.4.61 (2026-06-27) — Ajuste no esconde-card-URL: break em 991.98px (cobre celular + tablet em pe) + seletor mais especifico. Antes 768px deixava aparecer em tablets.
    // v1.4.60 (2026-06-27) — Tela /configurar/atualizacao esconde card "Fonte de atualizacao" (URL do manifesto) em mobile/celular. Em desktop continua visivel pra suporte. Cliente final no celular nao ve mais info tecnica. Inclui v1.4.59.
    // v1.4.59 (2026-06-27) — Tela inicial pos-login agora e /consolidado (Resumo Geral) em vez de /dashboard. Usuario nao logado continua vendo a landing page. Inclui v1.4.58 (link Resumo Geral na sidebar).
    // v1.4.58 (2026-06-27) — Link "Resumo Geral" adicionado na sidebar (secao Visao Geral) apontando pra /consolidado. Funciona em single E multi-loja. Em multi, clicar ativa modo "Todas as lojas" automaticamente. Inclui v1.4.57.
    // v1.4.57 (2026-06-27) — Tela /consolidado agora funciona em single-loja (cliente com 1 empresa so). Antes redirecionava pra /. Agora monta painel unico com config do database.php (loja virtual '_single'). Acessivel via URL direto. Inclui v1.4.56.
    // v1.4.56 (2026-06-26) — Comparativos do /consolidado em linguagem humana: cards "vs Semana / vs Mes / Mesma data AA" agora viraram "vs semana passada / vs mes passado / vs ano passado" com frase verdita ("Vendendo R$ X a MAIS/MENOS que semana passada"). Removido jargao "delta" e "AA". Inclui v1.4.55.
    // v1.4.55 (2026-06-26) — Fix labels do eixo X amontoados nos rankings horizontais (R$ 2.0kR$ 4.0kR$ 6.0...): hideOverlap:true + splitNumber:4 + margin:8. Aplicado em /compras/margem, /compras/ruptura, /vendas/por-grupo-marca, /vendas/por-vendedor. Inclui v1.4.54 (fail-fast loja offline).
    // v1.4.54 (2026-06-26) — Fail-fast loja offline (VPN/servidor desligado): TCP probe 250ms antes do PDO; cache 30s; banner amarelo no topo "Servidor X desligado" quando alguma loja visivel esta offline; nome da loja com badge vermelho no seletor. Resolve trava de 60s quando o servidor da loja esta fora (ex: Disk Rapido a noite). Inclui tudo da v1.4.53.
    // v1.4.53 (2026-06-24) — Tela /consolidado reformulada: cada loja em painel grande com cor unica de borda, todos os blocos por loja (comparativos, Top Grupos/Marcas/Produtos/Clientes, Com/Sem Nota donut, dia da semana, hora, formas pagto), Resumo Geral no fim. 8 metodos novos no DashboardRepository. Fix donut texto cortado + legenda 1 linha + formas pagto sem agrupar Outros. Inclui tudo da v1.4.52.
    // v1.4.52 (2026-06-23) — Consolidado com cobertura fiscal: 2 KPIs (Com Nota / Sem Nota) com % cobertura + 2 colunas no ranking de lojas (Com Nota R$ / Sem Nota R$). Usa regra de ouro Fire (ENFCE=S OR NFE=S), fallback SERIE=SC. Inclui tudo da v1.4.51.
    // v1.4.51 (2026-06-22) — Hotfix: pacote OTA v1.4.50 saiu sem app/Core/Lojas.php (build script excluia por engano), causando Fatal error (Lojas::resolverSegmento undefined) em clientes que atualizaram. v1.4.51 corrige o build-ota-pacote.ps1 e empacota Lojas.php corretamente; conteudo de features identico a v1.4.50.
    // v1.4.50 (2026-06-22) — Release acumulada: Vendas por Grupo (ESTRUTURA) + Vendas por Marca + bucket Outros/Total real + drill-down + comparativo periodo anterior + combo Mostrar + export Excel/PDF; multi-loja com SEGMENTOS DIFERENTES por loja; fix busca Catalogo (So com foto desmarcado), Gerenciador de Usuarios respeita loja ativa, somas honestas no rodape via total_real
    // v1.4.49 (2026-06-22) — Fix da busca de produtos no Catalogo (checkbox 'So com foto' nao vem mais marcado por default; busca por codigo/descricao agora encontra todos os produtos, nao so' os com foto cadastrada)
    // v1.4.48 (2026-06-20) — Instalador simplificado (auto-descoberta removida do wizard; configurar operacoes de venda agora pela tela /configuracao/tipos-venda no menu Configurar)
    // v1.4.47 (2026-06-20) — Instalador universal com auto-descoberta de tipos de venda (conecta no banco e sugere COD_TIPO_OPERACAO de venda; default 115; ajustavel no wizard)
}
