const { useState, useMemo, useEffect } = React;

/* Parâmetros da demonstração: todos fictícios, a validar com a operação */
const NOW_H = 11.5; // relógio da demo: 11:30
const CUSTO_KM = 7.2; // R$ por km rodado
const VEL_MEDIA = 55; // km/h
const FATOR_ESTRADA = 1.3; // linha reta x 1,3 = estrada (aproximação)
const INTERVALO_REVISAO = 20000; // km
const VIDA_PNEU = 90000; // km
const PRECO_MEDIO = 5.6; // R$/L vendido ao posto (fictício)
/* 13/11: a Iunes so trabalha com ETANOL. Diesel e gasolina vinham da demo
   generica de agosto e eram invencao minha. Ver iunes-respostas-e-conflitos-13-11.md */
const PRODUCTS = ['Etanol'];

const cities = {
  'Barretos':[-20.557,-48.568], 'Bebedouro':[-20.949,-48.479], 'Olímpia':[-20.737,-48.915], 'Colina':[-20.713,-48.541],
  'Guaíra':[-20.318,-48.311], 'Monte Azul Paulista':[-20.907,-48.641], 'Severínia':[-20.809,-48.806], 'Terra Roxa':[-20.787,-48.331],
  'Altair':[-20.524,-49.057], 'Colômbia':[-20.176,-48.689], 'Jaborandi':[-20.688,-48.411], 'Viradouro':[-20.873,-48.297],
  'Ribeirão Preto':[-21.177,-47.810],
  /* 13/11: cidades que o Paulo citou na reunião. Coordenada do centro da
     malha do IBGE (as antigas usam a sede; diferença de poucos km). */
  'Barrinha':[-21.230,-48.093], 'Buritizal':[-20.205,-47.708], 'Franca':[-20.574,-47.373],
};

function dist(a,b){
  if(a===b) return 0;
  const [la1,lo1]=cities[a],[la2,lo2]=cities[b]; const r=Math.PI/180;
  const h=Math.sin((la2-la1)*r/2)**2+Math.cos(la1*r)*Math.cos(la2*r)*Math.sin((lo2-lo1)*r/2)**2;
  return Math.round(2*6371*Math.asin(Math.sqrt(h))*FATOR_ESTRADA);
}
function routeFrom(start,stops){
  let cur=start,left=[...new Set(stops)],seq=[],km=0;
  while(left.length){ left.sort((x,y)=>dist(cur,x)-dist(cur,y)); const nx=left.shift(); km+=dist(cur,nx); seq.push(nx); cur=nx; }
  return {seq,km,end:cur};
}
const fmt=(v)=>new Intl.NumberFormat('pt-BR').format(v);
const brl=(v)=>'R$ '+new Intl.NumberFormat('pt-BR',{maximumFractionDigits:0}).format(Math.round(v));
const brl2=(v)=>'R$ '+v.toFixed(2).replace('.',',');
const brl3=(v)=>'R$ '+v.toFixed(3).replace('.',',');
const mi=(v)=>(v/1e6).toFixed(2).replace('.',',')+' mi';
const clamp=(v)=>Math.max(0,Math.min(100,v));

/* ===== Gerador de dado de demonstração (12/11) =====================
   Alguns números que o app mostra não existem em cadastro nenhum:
   multa de motorista, preço de um dia específico. Em vez de sortear
   (o que mudaria a cada vez que a tela abre), geramos a partir de uma
   semente fixa: o mesmo texto devolve sempre o mesmo número.
   Trocar por fonte real = trocar quem chama estas funções.
   ================================================================== */
function vaSemente(txt){
  let h=2166136261;
  for(let i=0;i<txt.length;i++){h^=txt.charCodeAt(i);h=Math.imul(h,16777619)>>>0;}
  h^=h>>>13; h=Math.imul(h,1274126177)>>>0; h^=h>>>16;
  return (h>>>0)/4294967296; /* 0 a 1 */
}
const vaDado=(chave,min,max)=>min+vaSemente(chave)*(max-min);
const clock=(h)=>{ const t=Math.round((NOW_H+h)*60); const d=Math.floor(t/1440); const m=t%1440; const s=`${String(Math.floor(m/60)).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`; return d>0?`amanhã ${s}`:s; };
const hrs=(h)=>{ const m=Math.round(h*60); return m<60?`${m} min`:`${Math.floor(m/60)}h${String(m%60).padStart(2,'0')}`; };
const addDays=(n)=>{ const d=new Date(2026,8,21); d.setDate(d.getDate()+n); return d.toLocaleDateString('pt-BR'); };
const newId=(p)=>p+'-'+Math.random().toString(36).slice(2,7).toUpperCase();

/* Guarda cadastros no navegador de quem usa (não é banco de dados) */
/* ===== Duas visualizações (13/11) ==================================
   Pedido do Guilherme: dentro do acesso dele, poder ver o sistema com
   DADOS SIMULADOS (para conferir qualquer tela mesmo que o cliente não
   tenha preenchido nada) ou com DADOS REAIS (começando vazio, só o que
   o cliente digitar).

   Como funciona: cada visualização tem a sua própria gaveta no
   navegador. Trocar recarrega a página — é o jeito simples de garantir
   que nenhuma lista velha sobreviva à troca.

   ⚠️ Enquanto o app não estiver ligado ao banco (Supabase), o que for
   digitado na visualização REAL fica só NESTE navegador: some se
   trocar de computador ou limpar os dados do site.

   13/11: bases, frota e clientes mudaram com a reunião, e o produto
   virou só etanol — nada do que estava guardado servia mais. */
const VISAO_CHAVE='iunes:visao';
const VISAO=(function(){ try{ return localStorage.getItem(VISAO_CHAVE)==='real'?'real':'sim'; }catch(e){ return 'sim'; } })();
const REAL=(VISAO==='real');
/* Na visualização real toda lista de exemplo nasce vazia */
const seed=(lista)=>REAL?[]:lista;
const trocarVisao=(v)=>{ try{ localStorage.setItem(VISAO_CHAVE,v); }catch(e){} window.location.reload(); };
/* 14/11: o nome da chave era 'viaflux', que é o nome interno do projeto
   na ValorIA. O app é do cliente e leva a marca dele (ver
   claude/regra-marca-apps-de-cliente.md), então a gaveta no navegador
   também. Como a estrutura dos dados mudou de qualquer jeito nesta
   rodada, renomear agora não custa nada: a demonstração já ia recomeçar. */
const STORE='iunes:v1:'+VISAO+':';
function usePersist(key,initial){
  const [v,setV]=useState(()=>{ try{ const s=localStorage.getItem(STORE+key); return s?JSON.parse(s):initial; }catch(e){ return initial; } });
  useEffect(()=>{ try{ localStorage.setItem(STORE+key,JSON.stringify(v)); }catch(e){} },[key,v]);
  return [v,setV];
}

const navItems = [
  { id:'dashboard', label:'Visão geral', short:'Visão', icon:'▦' },
  { id:'logistica', label:'Pedidos e viagens', short:'Viagens', icon:'◆' },
  { id:'clientes', label:'Carteira de clientes', short:'Clientes', icon:'◉' },
  { id:'frota', label:'Frota', short:'Frota', icon:'▰' },
  { id:'mercado', label:'Mercado e cotações', short:'Mercado', icon:'◭', tag:'ao vivo' },
  { id:'relatorios', label:'Relatórios', short:'Relatórios', icon:'▥', tag:'novo' },
  { id:'cadastros', label:'Cadastros', short:'Cadastros', icon:'☰', tag:'novo' },
];

/* ===== Bases de carregamento (revisto em 13/11 com a reunião) =======
   Antes eram 4 bases inventadas. O Paulo disse na reunião que compra
   álcool em várias usinas e que os caminhões saem de Barretos.
   Aqui ficam só as que ele CITOU com clareza. Ele falou de outras que
   a transcrição embaralhou (São Francisco, Santa Tuba, Mendonça):
   entram quando ele confirmar a lista.
   Usina vende etanol; diesel e gasolina vêm da base e do terminal.
   ================================================================== */
const initialBases = [
  {id:'B-01',name:'Base Barretos',city:'Barretos',kind:'Base própria',products:['Etanol'],loadH:1,garage:true},
  /* A CONFIRMAR: o Paulo so disse que carrega em Ribeirao depois da entrega,
     nao disse o que. Como tudo que ele move e etanol, esta assim ate ele dizer. */
  {id:'B-02',name:'Terminal Ribeirão Preto',city:'Ribeirão Preto',kind:'Terminal',products:['Etanol'],loadH:1.5},
  {id:'B-03',name:'Usina Mandu',city:'Guaíra',kind:'Usina',products:['Etanol'],loadH:0.75},
  {id:'B-04',name:'Usina Barrinha',city:'Barrinha',kind:'Usina',products:['Etanol'],loadH:0.75},
  {id:'B-05',name:'Usina Buritizal',city:'Buritizal',kind:'Usina',products:['Etanol'],loadH:0.75},
];

const initialSellers = [
  {id:'V-01',name:'Juliana Prado',region:'Barretos, Colina e Terra Roxa',meta:3500000},
  {id:'V-02',name:'Marcelo Tavares',region:'Olímpia, Altair e Severínia',meta:1700000},
  {id:'V-03',name:'Ricardo Souza',region:'Bebedouro, Guaíra e Monte Azul',meta:1400000},
];

const initialClients=[
  {id:'C-01',name:'Auto Posto Irmãos Colina',city:'Colina',sellerId:'V-01',postos:2,vol:800000,margem:0.12,carga:45000,atraso:1,compras:24,modo:'entrega',note:'Acesso fácil, recebe carreta cheia'},
  {id:'C-02',name:'Rede Horizonte',city:'Altair',sellerId:'V-02',postos:1,vol:1000000,margem:0.09,carga:15000,atraso:2,compras:34,modo:'entrega',note:'Acesso restrito: só caminhão de até 15.000 L'},
  {id:'C-03',name:'Rede Barretos',city:'Barretos',sellerId:'V-01',postos:4,vol:1600000,margem:0.08,carga:30000,atraso:6,compras:40,modo:'retira',note:'Maior volume da carteira'},
  {id:'C-04',name:'Posto Avenida',city:'Bebedouro',sellerId:'V-03',postos:1,vol:420000,margem:0.11,carga:30000,atraso:0,compras:14,modo:'entrega',note:'Paga sempre em dia'},
  {id:'C-05',name:'Posto Imperial',city:'Olímpia',sellerId:'V-02',postos:1,vol:300000,margem:0.13,carga:15000,atraso:12,compras:10,modo:'entrega',note:'Tanques pequenos: entregas fracionadas'},
  {id:'C-06',name:'Posto Nogueira',city:'Colina',sellerId:'V-01',postos:1,vol:260000,margem:0.10,carga:20000,atraso:1,compras:9,modo:'entrega',note:''},
  {id:'C-07',name:'Posto Santa Rita',city:'Monte Azul Paulista',sellerId:'V-03',postos:1,vol:180000,margem:0.14,carga:10000,atraso:0,compras:8,modo:'entrega',note:'Pedidos pequenos e urgentes'},
  {id:'C-08',name:'Auto Posto Guaíra',city:'Guaíra',sellerId:'V-03',postos:2,vol:650000,margem:0.095,carga:30000,atraso:3,compras:20,modo:'retira',note:''},
  {id:'C-09',name:'Posto Via Rio',city:'Severínia',sellerId:'V-02',postos:1,vol:220000,margem:0.12,carga:15000,atraso:25,compras:7,modo:'entrega',note:'Histórico de atraso no pagamento'},
  {id:'C-10',name:'Rede Terra Roxa',city:'Terra Roxa',sellerId:'V-01',postos:3,vol:900000,margem:0.085,carga:30000,atraso:4,compras:26,modo:'retira',note:''},
];

const initialOrders = [
  {id:'#PED-2841',client:'Posto Avenida',city:'Bebedouro',volume:30000,window:'Hoje, 14:00',deadlineH:2.5,product:'Etanol',priority:'Alta'},
  {id:'#PED-2845',client:'Posto Santa Rita',city:'Monte Azul Paulista',volume:8000,window:'Hoje, 15:00',deadlineH:3.5,product:'Etanol',priority:'Alta'},
  {id:'#PED-2837',client:'Rede Barretos',city:'Barretos',volume:45000,window:'Hoje, 16:30',deadlineH:5,product:'Etanol',priority:'Alta'},
  {id:'#PED-2834',client:'Posto Imperial',city:'Olímpia',volume:15000,window:'Amanhã, 08:00',deadlineH:20.5,product:'Etanol',priority:'Normal'},
  {id:'#PED-2832',client:'Posto Nogueira',city:'Colina',volume:20000,window:'Amanhã, 09:30',deadlineH:22,product:'Etanol',priority:'Normal'},
];

const initialTrips = [
  {id:'VG-1084',truck:'TRK-04 · GHF-7D21',driver:'Rafael Moreira',route:'Barretos → Bebedouro',volume:50000,capacity:50000,status:'Em rota',eta:'12:12',orders:2},
  {id:'VG-1082',truck:'TRK-05 · EKL-3B09',driver:'Wagner Dias',route:'Barretos → Bebedouro',volume:40000,capacity:50000,status:'Em rota',eta:'11:55',orders:1},
  {id:'VG-1086',truck:'TRK-02 · GBC-9F40',driver:'André Luiz',route:'Base Barretos',volume:50000,capacity:50000,status:'Carregando',eta:'Saída 13:20',orders:3},
  {id:'VG-1087',truck:'TRK-07 · EPR-4H16',driver:'Paulo César',route:'Barretos → Colina',volume:30000,capacity:50000,status:'Programada',eta:'Saída 15:00',orders:1},
  {id:'VG-1080',truck:'TRK-01 · GAB-1C22',driver:'Rogério Lima',route:'Guaíra → Barretos',volume:50000,capacity:50000,status:'Concluída',eta:'11:08',orders:2},
];

/* ===== Frota (REVISTA em 13/11 com a reunião do Paulo) =============
   Antes: 12 caminhões de 6 fabricantes, 45.000 e 30.000 L, km/L de
   2,09 a 2,88. Isso era invenção minha.
   O Paulo disse: "hoje está com SETE caminhões", "os caminhões são
   IGUAIS porque eu padronizei aqui", "caminhões de CINQUENTA MIL litros".

   Compartimentos: ele disse "duas bocas de dez e seis compartimentos
   de cinco" -> 2 x 10.000 + 6 x 5.000 = 50.000 L. Fecha com o total,
   mas a transcrição estava ruim: A CONFIRMAR com ele.

   Consumo: com frota padronizada o km/L NÃO vem do modelo, vem do
   desgaste e do motorista. Por isso a faixa aqui é estreita (2,35 a
   2,58) e o número é MEDIDO, não especificação. Valor de demonstração
   até existir o lançamento de abastecimento.

   A CONFIRMAR com o Paulo: marca e modelo, ano de cada um, nº de eixos.
   ================================================================== */
const CAP_PADRAO = 50000; /* 2 bocas de 10.000 + 6 compartimentos de 5.000 */
const COMP_PADRAO = 8;

const initialFleet = [
  {id:'TRK-01',plate:'GAB-1C22',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2021,eixos:7,kmL:2.44,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Rogério Lima',status:'base',pos:'Barretos',odo:312400,nextRev:320000,lastRev:'12/08/2026',tireKm:241000,dailyKm:390},
  {id:'TRK-02',plate:'GBC-9F40',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2020,eixos:7,kmL:2.37,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'André Luiz',status:'carregando',pos:'Barretos',trip:'VG-1086',odo:287900,nextRev:290000,lastRev:'03/07/2026',tireKm:230500,dailyKm:410},
  {id:'TRK-03',plate:'FHT-5A61',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2019,eixos:7,kmL:2.35,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Sérgio Nunes',status:'manutencao',pos:'Barretos',note:'Troca de embreagem · previsão 23/09',odo:401200,nextRev:420000,lastRev:'19/09/2026',tireKm:330000,dailyKm:300},
  {id:'TRK-04',plate:'GHF-7D21',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2022,eixos:7,kmL:2.55,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Rafael Moreira',status:'rota',pos:'Bebedouro',trip:'VG-1084',busyH:0.7,odo:198300,nextRev:210000,lastRev:'28/08/2026',tireKm:150200,dailyKm:420},
  {id:'TRK-05',plate:'EKL-3B09',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2021,eixos:7,kmL:2.49,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Wagner Dias',status:'rota',pos:'Bebedouro',trip:'VG-1082',busyH:0.4,sobra:{product:'Etanol',vol:10000},odo:256700,nextRev:270000,lastRev:'15/08/2026',tireKm:199000,dailyKm:360},
  {id:'TRK-06',plate:'GJC-1B93',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2023,eixos:7,kmL:2.58,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Leandro Martins',status:'rota',pos:'Guaíra',sobra:{product:'Etanol',vol:50000},busyH:0,odo:144800,nextRev:145600,lastRev:'02/06/2026',tireKm:100000,dailyKm:400},
  {id:'TRK-07',plate:'EPR-4H16',modelo:'Bitrem tanque · 50.000 L',tipo:'bitrem',ano:2020,eixos:7,kmL:2.41,cap:CAP_PADRAO,comp:COMP_PADRAO,driver:'Paulo César',status:'carregando',pos:'Barretos',trip:'VG-1087',odo:338100,nextRev:350000,lastRev:'21/08/2026',tireKm:300000,dailyKm:330},
];
/* ===== Ficha do veículo (12/11) =====================================
   Pedido dele: cadastrar modelo, ano, eixos, placa e autonomia.
   Os campos modelo/tipo/ano/eixos/kmL são dados de DEMONSTRAÇÃO,
   coerentes com a capacidade e o número de compartimentos de cada
   caminhão. Na operação real vêm do cadastro do cliente.
   Autonomia (kmL) fica no VEÍCULO, não no motorista: é característica
   da frota. O ranking de motoristas lê este mesmo número.
   =================================================================== */
const TIPO_LABEL={truck:'Truck',carreta:'Carreta',bitrem:'Bitrem'};

/* Econômico ou não, comparado com a média da própria frota */
function consumoFrota(fleet){
  const v=fleet.map(t=>t.kmL).filter(Boolean);
  const media=v.reduce((a,b)=>a+b,0)/(v.length||1);
  return {media,min:Math.min(...v),max:Math.max(...v)};
}
function faixaConsumo(kmL,media){
  if(kmL>=media*1.06) return {k:'bom',rot:'Econômico'};
  if(kmL<=media*0.94) return {k:'ruim',rot:'Gasta mais'};
  return {k:'medio',rot:'Na média'};
}
/* Quanto custa rodar 1 km de combustível, para comparar a frota no mesmo pé */
const PRECO_DIESEL=6.18; /* R$/L, mesmo valor base do calendário */
const custoCombKm=(kmL)=>kmL?PRECO_DIESEL/kmL:0;

/* ===== Custo por km de CADA caminhão (12/11) ========================
   Antes o app usava CUSTO_KM (R$ 7,20) igual para os 12 caminhões, então
   o km/L da ficha aparecia na tela de Frota mas NÃO entrava na escolha
   do melhor veículo. Agora o custo é partido em duas peças:

     custo/km = parte fixa (motorista, pneu, manutenção, depreciação)
              + diesel ÷ km/L do próprio caminhão

   A parte fixa é CALIBRADA para o caminhão médio da frota continuar
   custando exatamente CUSTO_KM. É de propósito: nenhum outro número do
   app se desloca, só passa a existir diferença entre um caminhão e outro.
   13/11: com a frota PADRONIZADA que o Paulo descreveu, o consumo
   não vem mais do modelo e sim do desgaste e do motorista. A diferença
   entre o melhor e o pior caiu de R$ 0,81/km para ~R$ 0,23/km (cerca
   de R$ 141 numa viagem de 600 km). Continua sendo dinheiro, mas deixou
   de ser o critério mais forte da escolha: distância e horário pesam
   mais. Os números da tela são calculados, então se ajustam sozinhos.
   ================================================================== */
const CUSTO_KM_FIXO = CUSTO_KM - custoCombKm(consumoFrota(initialFleet).media);
const custoKm = (t) => (t && t.kmL) ? CUSTO_KM_FIXO + custoCombKm(t.kmL) : CUSTO_KM;
/* Frase para as notas de rodapé das telas. Calculada, não escrita à mão:
   se o km/L da frota mudar, o texto muda junto e não vira mentira. */
function textoCustoKm(fleet){
  const c=consumoFrota(fleet);
  return `custo por km é de cada caminhão: ${brl2(CUSTO_KM_FIXO)} fixo (motorista, pneu, manutenção, depreciação) mais o diesel do próprio veículo (${brl2(PRECO_DIESEL)}/L dividido pelo km/L) — de ${brl2(CUSTO_KM_FIXO+custoCombKm(c.max))} a ${brl2(CUSTO_KM_FIXO+custoCombKm(c.min))} nesta frota`;
}

const statusLabel={base:'Na base',retorno:'Retornando vazio',rota:'Em rota',carregando:'Carregando',manutencao:'Manutenção'};
const modeLabel={encaixe:'Encaixe em rota',base:'Sai da garagem',retorno:'Voltando vazio',rota:'Após a entrega atual'};

/* ===== Motor de sugestão do melhor caminhão (considera todas as bases de carregamento) ===== */
function suggest(fleet,sel,bases){
  const ranked=[],excluded=[];
  if(!sel.length) return {ranked,excluded,note:''};
  const cons=consumoFrota(fleet); /* media da frota viva, para a faixa de consumo */
  const garage=(bases.find(b=>b.garage)||bases[0]||{city:'Barretos'}).city;
  const vol=sel.reduce((s,o)=>s+o.volume,0);
  const stops=sel.map(o=>o.city);
  const products=[...new Set(sel.map(o=>o.product))];
  const okBases=bases.filter(b=>products.every(p=>b.products.includes(p)));
  const deadline=Math.min(...sel.map(o=>o.deadlineH));
  const winLabel=sel.find(o=>o.deadlineH===deadline).window;
  if(!okBases.length) return {ranked,excluded,note:`Nenhuma base cadastrada tem ${products.join(' + ')} juntos. Divida em viagens separadas ou cadastre a base em Cadastros.`};
  for(const t of fleet){
    const kmLeft=t.nextRev-t.odo;
    if(t.status==='manutencao'){excluded.push({t,why:`em manutenção (${t.note})`});continue;}
    if(t.status==='carregando'){excluded.push({t,why:`já alocado na ${t.trip}`});continue;}
    const reasons=[];
    let mode,km,arrival,readyH=0,seq,cap=t.cap,base=null;
    const canFit=t.status==='rota'&&t.sobra&&products.length===1&&products[0]===t.sobra.product&&vol<=t.sobra.vol;
    if(canFit){
      mode='encaixe'; cap=t.sobra.vol;
      const r=routeFrom(t.pos,stops); seq=r.seq;
      km=Math.max(0,r.km+dist(r.end,garage)-dist(t.pos,garage));
      arrival=(t.busyH||0)+dist(t.pos,r.seq[0])/VEL_MEDIA;
      reasons.push({tone:'good',text:`Já está em ${t.pos} com ${fmt(t.sobra.vol)} L de ${t.sobra.product} a bordo`});
      reasons.push({tone:'good',text:`Só ${km} km a mais no caminho de volta, sem passar por nenhuma base`});
    } else {
      if(t.cap<vol){excluded.push({t,why:`capacidade de ${fmt(t.cap)} L, menor que a carga de ${fmt(vol)} L`});continue;}
      if(t.comp<sel.length){excluded.push({t,why:`${t.comp} compartimentos para ${sel.length} pedidos`});continue;}
      const busy=t.busyH||0, baseline=dist(t.pos,garage);
      let best=null;
      for(const b of okBases){
        const r=routeFrom(b.city,stops);
        const driven=dist(t.pos,b.city)+r.km+dist(r.end,garage);
        const inc=Math.max(0,driven-baseline);
        const ready=busy+dist(t.pos,b.city)/VEL_MEDIA;
        const arr=ready+b.loadH+dist(b.city,r.seq[0])/VEL_MEDIA;
        const s=inc*custoKm(t)+Math.max(0,arr-deadline)*600;
        if(!best||s<best.s) best={b,r,driven,inc,ready,arr,s};
      }
      if(kmLeft<best.driven){excluded.push({t,why:`revisão vence no meio da viagem (faltam ${fmt(kmLeft)} km)`});continue;}
      base=best.b; seq=best.r.seq; km=best.inc; readyH=best.ready; arrival=best.arr; mode=t.status;
      const other=base.city!==garage;
      if(mode==='base') reasons.push({tone:'good',text:`Parado na garagem, pronto para sair agora`});
      if(mode==='retorno') reasons.push({tone:other?'good':'warn',text:other?`Voltando vazio de ${t.pos}: passa na ${base.name} no caminho`:`Voltando vazio de ${t.pos}: chega à ${base.name} em ${hrs(readyH)}`});
      if(mode==='rota') reasons.push({tone:other?'good':'warn',text:other?`Termina a entrega em ${t.pos} e carrega na ${base.name}, sem voltar a ${garage}`:`Termina a entrega em ${t.pos} e volta: livre na base às ${clock(readyH)}`});
      reasons.push({tone:'neutral',text:`Carrega na ${base.name} (${base.city}) · ${hrs(base.loadH)} de carregamento`});
    }
    const late=Math.max(0,arrival-deadline);
    if(late>0.05) reasons.push({tone:'bad',text:`Chega ${hrs(late)} depois da janela (${winLabel})`});
    else reasons.push({tone:'good',text:`Chega dentro da janela: ~${clock(arrival)} (limite ${winLabel.replace('Hoje, ','')})`});
    const occ=Math.round(vol/cap*100);
    if(mode!=='encaixe'){
      if(occ>=85) reasons.push({tone:'good',text:`Ocupação de ${occ}%: tanque bem aproveitado`});
      else if(occ<60) reasons.push({tone:'warn',text:`Ocupação de só ${occ}%: sobra capacidade parada`});
      else reasons.push({tone:'neutral',text:`Ocupação de ${occ}%`});
    }
    if(kmLeft<=3000) reasons.push({tone:'warn',text:`Revisão em ${fmt(kmLeft)} km: programar oficina após a viagem`});
    const tirePct=Math.round((t.odo-t.tireKm)/VIDA_PNEU*100);
    if(tirePct>=85) reasons.push({tone:'warn',text:`Pneus com ${tirePct}% da vida útil`});
    const cKm=custoKm(t);
    const fx=t.kmL?faixaConsumo(t.kmL,cons.media):null;
    const kmLtxt=String(t.kmL).replace('.',',');
    if(fx) reasons.push({tone:fx.k==='bom'?'good':fx.k==='ruim'?'warn':'neutral',
      text:fx.k==='bom'?`Faz ${kmLtxt} km/L, acima da média da frota: ${brl2(cKm)}/km`
         :fx.k==='ruim'?`Faz só ${kmLtxt} km/L: ${brl2(cKm)}/km, o mais caro de rodar`
         :`Faz ${kmLtxt} km/L, na média da frota: ${brl2(cKm)}/km`});
    const cost=km*cKm;
    const score=cost+late*600+(mode==='encaixe'?0:(cap-vol)/1000*6)+(kmLeft<=3000?120:0)+(tirePct>=85?60:0);
    ranked.push({t,mode,km,cost,cKm,kmL:t.kmL,faixa:fx,arrival,readyH,occ,seq,cap,late,score,reasons,base});
  }
  ranked.sort((a,b)=>a.score-b.score);
  return {ranked,excluded,note:''};
}

/* ===== Frota ===== */
function fleetInfo(t){
  const kmLeft=t.nextRev-t.odo;
  const days=Math.max(0,Math.floor(kmLeft/t.dailyKm));
  const tirePct=Math.round((t.odo-t.tireKm)/VIDA_PNEU*100);
  const alerts=[];
  if(t.status==='manutencao') alerts.push({tone:'bad',text:t.note});
  if(kmLeft<=0) alerts.push({tone:'bad',text:'Revisão vencida'});
  else if(kmLeft<=3000) alerts.push({tone:'warn',text:`Revisão em ${fmt(kmLeft)} km (~${days} ${days===1?'dia':'dias'})`});
  if(tirePct>=100) alerts.push({tone:'bad',text:'Pneus acima da vida útil'});
  else if(tirePct>=85) alerts.push({tone:'warn',text:`Pneus com ${tirePct}% da vida útil`});
  return {...t,kmLeft,days,tirePct,alerts,revDate:addDays(days)};
}

/* ===== Rating de clientes ===== */
function rateClients(list,sellers,garage){
  const g=garage||'Barretos';
  const ev=list.map(c=>{ const km=Math.max(dist(g,c.city),8); const custoL=2*km*CUSTO_KM/c.carga; const liq=c.margem-custoL; const s=sellers.find(x=>x.id===c.sellerId); return {...c,seller:s?s.name:'Sem vendedor',km,custoL,liq,result:c.vol*liq}; });
  const maxR=Math.max(1,...ev.map(c=>c.result));
  return ev.map(c=>{
    const parts=[
      {label:'Rentabilidade (resultado R$/mês)',w:50,v:clamp(c.result/maxR*100)},
      {label:'Pagamento em dia',w:25,v:clamp(100-c.atraso*4)},
      {label:'Recorrência de compra',w:15,v:clamp(c.compras/20*100)},
      {label:'Margem líquida por litro',w:10,v:clamp(c.liq/0.13*100)},
    ];
    const score=Math.round(parts.reduce((s,p)=>s+p.v*p.w/100,0));
    const rating=score>=75?'A':score>=60?'B':score>=45?'C':'D';
    return {...c,parts,score,rating};
  });
}
const ratingColor={A:'#1f9d63',B:'#2f7ddd',C:'#e0a030',D:'#d4533f'};
