diff --git a/app/controllers/admin/edicao_lancamentos_controller.rb b/app/controllers/admin/edicao_lancamentos_controller.rb index fa70edc..8cefe0e 100644 --- a/app/controllers/admin/edicao_lancamentos_controller.rb +++ b/app/controllers/admin/edicao_lancamentos_controller.rb @@ -32,8 +32,10 @@ class Admin::EdicaoLancamentosController < ApplicationController entrega = Entrega.por_nf(nf).first return render_erro("NF #{nf} não encontrada na base de rastreio.") if entrega.nil? - id = client.resolver_id(entrega) - visita = client.visita(id) + id = client.resolver_id(entrega) + visita = client.visita(id) + # Segunda fonte de imagens (POD). Não é obrigatória: se falhar, vem {}. + detalhe = client.detalhe_visita(id) render json: { ok: true, @@ -52,11 +54,9 @@ class Admin::EdicaoLancamentosController < ApplicationController planned_date: visita['planned_date'], contato: visita['contact_name'], telefone: visita['contact_phone'], - # Fotos do card: fachada vem do rastreio (mesma foto do mapa de operações); - # pictures/assinatura vêm da API. Só URLs http(s) chegam ao . - foto_fachada: url_imagem(entrega.try(:foto_da_fachada)), - pictures: Array(visita['pictures']).filter_map { |u| url_imagem(u) }, - signature: url_imagem(visita['signature']), + # Galeria unificada — TODAS as fotos do lançamento, de todas as fontes, + # cada uma etiquetada para o ADM saber o que está conferindo. + fotos: fotos_do_lancamento(entrega, visita, detalhe), motorista: entrega.driver, veiculo: entrega.vehicle }, @@ -218,6 +218,54 @@ class Admin::EdicaoLancamentosController < ApplicationController url.match?(%r{\Ahttps?://}i) ? url : nil end + # Campos de foto customizados do SimpliRoute (extra_field_values). É AQUI que + # ficam as fotos reais tiradas pelo motorista — o array `pictures` só traz uma + # imagem genérica. Mapa: campo da API => [rótulo p/ o ADM, categoria de cor]. + # Campos fora deste mapa ainda aparecem (rótulo derivado do nome do campo). + FOTOS_EXTRA = { + 'foto_prova_visita' => ['Prova da visita', 'prova'], + 'foto_nf' => ['Nota Fiscal', 'nf'], + 'foto_termo' => ['Termo', 'termo'], + 'foto_relatorio2' => ['Relatório', 'relatorio'] + }.freeze + + # Reúne TODAS as imagens do lançamento numa lista única e etiquetada: + # 1. espelho de rastreio -> foto da fachada (mesma do mapa de operações) + # 2. extra_field_values -> fotos reais do motorista (foto_nf, prova, etc.) + # 3. pictures[]/signature -> imagens genéricas + assinatura + # + # A mesma foto pode vir de mais de uma fonte, então deduplica por URL mantendo + # a primeira etiqueta (a ordem abaixo é a mais informativa). + def fotos_do_lancamento(entrega, visita, detalhe) + brutas = [] + brutas << [entrega.try(:foto_da_fachada), 'Fachada', 'fachada', 'Rastreio'] + + # Fotos customizadas — o /detail/ é a fonte mais completa; cai p/ a visita. + extras = detalhe['extra_field_values'].presence || visita['extra_field_values'] || {} + extras.each do |campo, valor| + rotulo, cat = FOTOS_EXTRA[campo] || [rotulo_do_campo(campo), 'extra'] + brutas << [valor, rotulo, cat, 'SimpliRoute'] + end + + [[visita, 'Visita'], [detalhe, 'Comprovante']].each do |fonte, origem| + Array(fonte['pictures']).each { |u| brutas << [u, 'Comprovante', 'entrega', origem] } + brutas << [fonte['signature'], 'Assinatura', 'assinatura', origem] + end + + vistas = Set.new + brutas.filter_map do |valor, rotulo, cat, origem| + url = url_imagem(valor) + next if url.nil? || !vistas.add?(url) + + { url: url, rotulo: rotulo, cat: cat, origem: origem } + end + end + + # "foto_relatorio2" -> "Relatorio2". Fallback p/ campos de foto não mapeados. + def rotulo_do_campo(campo) + campo.to_s.sub(/\Afoto_?/, '').tr('_', ' ').strip.capitalize.presence || campo.to_s + end + # Sem token não há o que fazer — avisa e volta. def garantir_configurado return if SimpliRoute.configurado? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a3c0faa..1efbf3d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,19 +2,117 @@ module ApplicationHelper include Pagy::Frontend + # --------------------------------------------------------------------------- + # Ícones + # + # Substituem os emojis antigos (que davam aparência "genérica de IA" e variavam + # de forma/cor conforme o sistema operacional). Agora são SVG chapados vindos do + # sprite em public/icons.svg (subset do Bootstrap Icons, licença MIT). + # + # O SVG usa `currentColor`, então a cor vem da classe CSS: + # - padrão → laranja da marca (#f97316) + # - ícones de status → passe `cor:` para preservar a leitura semântica + # (ex.: icone(:sucesso, cor: "text-green-400")) + # --------------------------------------------------------------------------- + + # Nome semântico (pt-BR) → id do símbolo no sprite. + ICONES = { + # Navegação + dashboard: "bar-chart-fill", operacoes: "graph-up-arrow", consolidacoes: "box-seam-fill", + arquivadas: "folder-fill", painel: "house-fill", usuarios: "people-fill", + configuracoes: "gear-fill", auditoria: "search", planilha: "box-arrow-in-down", + editar_lancamento: "pencil-fill", sair: "box-arrow-right", menu: "list", + # Domínio + caminhao: "truck", veiculo: "car-front-fill", motorista: "person-badge-fill", + dinheiro: "cash-coin", pagamento: "cash-stack", custo: "cash", + consolidado_pago: "briefcase-fill", calendario: "calendar-event", nota_fiscal: "receipt", + documento: "file-earmark-text-fill", termo_especial: "clipboard-data-fill", + apontamento: "pencil-square", local: "geo-alt-fill", operacao: "hospital-fill", + retirada: "box-seam-fill", desconto: "exclamation-triangle-fill", + global: "globe2", comparar: "arrow-left-right", pessoa: "person-fill", + ticket: "ticket-perforated-fill", tag: "tag-fill", composicao: "puzzle-fill", + ranking: "trophy-fill", historico: "clock-history", relogio: "clock-fill", + parcial: "hourglass-split", diario: "journal-text", vazio: "inbox", + dica: "lightbulb-fill", bonus: "star-fill", extraordinaria: "stars", + calor: "fire", foto: "camera-fill", mapa: "map-fill", + satelite: "broadcast-pin", escuro: "moon-fill", tela_cheia: "arrows-fullscreen", + email: "envelope-fill", telefone: "telephone-fill", chave: "key-fill", + # Ações + adicionar: "plus-lg", fechar: "x-lg", confirmar: "check-lg", + buscar: "search", editar: "pencil-fill", excluir: "trash-fill", + arquivar: "archive-fill", restaurar: "arrow-clockwise", salvar: "floppy-fill", + imprimir: "printer-fill", visualizar: "eye-fill", baixar: "download", + pasta: "folder2-open", travado: "lock-fill", destravado: "unlock-fill", + expandir: "chevron-down", recolher: "chevron-left", + voltar: "arrow-left", avancar: "arrow-right", estornar: "arrow-counterclockwise", + # Status (use sempre com `cor:` semântica) + sucesso: "check-circle-fill", alerta: "exclamation-triangle-fill", + erro: "x-circle-fill", desconhecido: "question-circle", + cancelado: "slash-circle", ponto: "circle-fill", ponto_vazio: "circle" + }.freeze + + # Renderiza um ícone do sprite. + # + # icone(:caminhao) # laranja da marca + # icone(:sucesso, cor: "text-green-400") # status verde + # icone(:fechar, cor: nil) # herda a cor do elemento pai + # icone(:dashboard, titulo: "Painel") # acessível (vira role="img") + # icone(:fechar, espaco: false) # sem margem (ícone sozinho/centralizado) + # + # ATENÇÃO — contraste: em fundo LARANJA (bg-orange-*, bg-[#f97316]) o laranja + # padrão fica invisível. Nesses casos passe `cor: nil` (herda a cor do texto do + # container) ou `cor: "text-black"`. + # + # `espaco:` acrescenta uma margem à direita para o ícone não colar no texto. + # Desligue (`espaco: false`) quando o ícone estiver sozinho ou centralizado — + # senão a margem o tira do eixo — ou quando o container já tiver `gap-*`. + def icone(nome, cor: "text-brand-laranja", tamanho: "w-[1.25em] h-[1.25em]", + classe: nil, titulo: nil, espaco: true) + simbolo = ICONES.fetch(nome.to_sym) { nome.to_s } + css = ["inline-block align-[-0.125em] shrink-0", ("mr-1" if espaco), + tamanho, cor, classe].compact.join(" ") + + tag.svg(class: css, fill: "currentColor", role: (titulo ? "img" : nil), + aria: { hidden: (titulo ? nil : true), label: titulo }) do + tag.use(href: "/icons.svg##{simbolo}") + end + end + + # Rótulo "ícone + texto" para link_to / button_to (que recebem o label como + # argumento, onde não dá para escrever ERB). + # + # link_to rotulo(:salvar, 'Salvar rascunho'), caminho, class: '...' + # + # Atenção: NÃO serve para `f.submit` nem `button_to` sem bloco — ambos geram + # , que não aceita HTML dentro. Nesses casos use + # `button_tag`/`button_to ... do`. + def rotulo(nome, texto, cor: "text-brand-laranja", tamanho: "w-5 h-5") + # O espaçamento vem do `mr-1` do próprio ícone (ver `icone`). + safe_join([icone(nome, cor: cor, tamanho: tamanho), texto]) + end + # Opções [label, valor] para o select de perfil de usuário (form de usuário) def role_options_for_select User::ROLES_LABEL.map { |value, label| [label, value] } end - # Link de navegação com estado ativo - def nav_link_to(label, path, **opts) + # Link de navegação com estado ativo. + # `icon:` aceita um nome semântico de ICONES; no estado ativo o ícone herda o + # preto do fundo laranja, fora dele fica laranja da marca. + def nav_link_to(label, path, icon: nil, **opts) active = current_page?(path) css = [ - "flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all min-h-[44px]", + "flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm font-medium transition-all min-h-[44px]", active ? "bg-orange-500 text-black" : "text-gray-400 hover:bg-[#1a1a1a] hover:text-white" ].join(" ") - link_to label, path, class: css + + conteudo = if icon + safe_join([icone(icon, cor: (active ? "text-black" : "text-brand-laranja"), + tamanho: "w-5 h-5", espaco: false), label]) + else + label + end + link_to conteudo, path, class: css end # Formata moeda BR (milhar com ".", decimais com ","): R$ 1.234,56 @@ -22,31 +120,34 @@ module ApplicationHelper number_to_currency(valor.to_f, unit: 'R$ ', separator: ',', delimiter: '.', precision: 2) end - # Badge de status de consolidação + # Badge de status de consolidação. + # O ícone herda a cor do texto do badge (cor: nil) para não brigar com o fundo. def badge_status(status) cfg = case status.to_s - when 'rascunho' then { cor: 'bg-yellow-500 text-black', icone: '📝', label: 'Rascunho' } - when 'finalizada' then { cor: 'bg-green-600 text-white', icone: '✅', label: 'Finalizada' } - when 'arquivada' then { cor: 'bg-gray-700 text-gray-300', icone: '📁', label: 'Arquivada' } - else { cor: 'bg-gray-800 text-gray-400', icone: '❓', label: status.humanize } + when 'rascunho' then { cor: 'bg-yellow-500 text-black', icone: :apontamento, label: 'Rascunho' } + when 'finalizada' then { cor: 'bg-green-600 text-white', icone: :sucesso, label: 'Finalizada' } + when 'arquivada' then { cor: 'bg-gray-700 text-gray-300', icone: :arquivar, label: 'Arquivada' } + else { cor: 'bg-gray-800 text-gray-400', icone: :desconhecido, label: status.humanize } end - tag.span class: "inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold #{cfg[:cor]}" do - "#{cfg[:icone]} #{cfg[:label]}" - end + badge_com_icone(cfg) end # Badge de status de pagamento de uma consolidação (derivado dos motoristas) def badge_pagamento(status) cfg = case status.to_s - when 'pago' then { cor: 'bg-green-600 text-white', icone: '💰', label: 'Pago' } - when 'parcial' then { cor: 'bg-yellow-500 text-black', icone: '⏳', label: 'Parcial' } - when 'pendente' then { cor: 'bg-gray-700 text-gray-300', icone: '🕗', label: 'Pendente' } - else { cor: 'bg-gray-800 text-gray-400', icone: '❓', label: status.to_s.humanize } + when 'pago' then { cor: 'bg-green-600 text-white', icone: :dinheiro, label: 'Pago' } + when 'parcial' then { cor: 'bg-yellow-500 text-black', icone: :parcial, label: 'Parcial' } + when 'pendente' then { cor: 'bg-gray-700 text-gray-300', icone: :relogio, label: 'Pendente' } + else { cor: 'bg-gray-800 text-gray-400', icone: :desconhecido, label: status.to_s.humanize } end - tag.span class: "inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold #{cfg[:cor]}" do - "#{cfg[:icone]} #{cfg[:label]}" + badge_com_icone(cfg) + end + + def badge_com_icone(cfg) + tag.span class: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold #{cfg[:cor]}" do + safe_join([icone(cfg[:icone], cor: nil, tamanho: "w-4 h-4", espaco: false), cfg[:label]]) end end diff --git a/app/javascript/controllers/validacao_controller.js b/app/javascript/controllers/validacao_controller.js index e75c130..3b1245c 100644 --- a/app/javascript/controllers/validacao_controller.js +++ b/app/javascript/controllers/validacao_controller.js @@ -263,10 +263,12 @@ export default class extends Controller { const lock = chip.querySelector("[data-chip-lock]") if (feito) feito.textContent = dados.classificadas if (total) total.textContent = dados.eligible - if (check) check.textContent = dados.completo ? "✓" : "" + // O check e o cadeado agora são fixos no HTML — alterna a visibilidade + // em vez de trocar textContent (que apagaria o SVG). + if (check) check.classList.toggle("hidden", !dados.completo) if (lock) lock.classList.toggle("hidden", !dados.fechado) - // Cor determinística: selecionado(laranja) > consolidado(verde+🔒) > + // Cor determinística: selecionado(laranja) > consolidado(cadeado+verde) > // pronto(verde) > arquivado(cinza) > neutro. chip.classList.remove(...TODAS) if (this.veiculosValue.includes(chip.dataset.vehicle)) { diff --git a/app/services/simpli_route/client.rb b/app/services/simpli_route/client.rb index 282b6dd..8936ad0 100644 --- a/app/services/simpli_route/client.rb +++ b/app/services/simpli_route/client.rb @@ -84,6 +84,17 @@ module SimpliRoute [] end + # Detalhe do comprovante de entrega (POD): traz foto, assinatura, hora e GPS. + # É uma fonte MAIS COMPLETA de imagens que `visita(id)` — a visita crua às + # vezes vem com `pictures` vazio mesmo havendo foto registrada no checkout. + # Best-effort: se o endpoint não existir/responder, devolve {} e a tela segue + # com o que a visita tiver. + def detalhe_visita(id) + get("/v1/plans/visits/#{id}/detail/") || {} + rescue Error + {} + end + private def get(path) diff --git a/app/views/admin/auditoria_logs/index.html.erb b/app/views/admin/auditoria_logs/index.html.erb index 7f5aaa8..5ac24ae 100644 --- a/app/views/admin/auditoria_logs/index.html.erb +++ b/app/views/admin/auditoria_logs/index.html.erb @@ -1,7 +1,7 @@ <%# app/views/admin/auditoria_logs/index.html.erb %> <% content_for :title, 'Auditoria' %> -

🔍 Auditoria

+

<%= icone :auditoria, espaco: false %> Auditoria

<%= form_with url: admin_auditoria_logs_path, method: :get, class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-6 flex flex-wrap gap-3' do %> diff --git a/app/views/admin/configuracoes/index.html.erb b/app/views/admin/configuracoes/index.html.erb index 4661a99..f90d925 100644 --- a/app/views/admin/configuracoes/index.html.erb +++ b/app/views/admin/configuracoes/index.html.erb @@ -10,7 +10,7 @@ <%# Pilares de preço %>
-

💰 Pilares de Preço

+

<%= icone :dinheiro, espaco: false %> Pilares de Preço

<% @configuracoes.each do |cfg| %>
@@ -39,7 +39,7 @@ <%# Informações do banco externo %>
-

🗄️ Banco de Dados Externo

+

<%= icone :arquivar, espaco: false %> Banco de Dados Externo

Tabela monitorada

diff --git a/app/views/admin/edicao_lancamentos/show.html.erb b/app/views/admin/edicao_lancamentos/show.html.erb index 20a8158..2f4eccf 100644 --- a/app/views/admin/edicao_lancamentos/show.html.erb +++ b/app/views/admin/edicao_lancamentos/show.html.erb @@ -20,8 +20,8 @@ <%# Aviso de sincronização %>
- ⚠️ -

As alterações são gravadas no SimpliRoute na hora (cada campo salva ao confirmar ✓), + <%= icone :alerta, cor: nil, tamanho: 'w-5 h-5 mt-0.5', espaco: false %> +

As alterações são gravadas no SimpliRoute na hora (cada campo salva ao confirmar <%= icone :confirmar, cor: nil, tamanho: 'w-3.5 h-3.5' %>), mas o painel e as consolidações só refletem após a próxima sincronização da base de rastreio. Toda alteração fica registrada na auditoria.

@@ -57,28 +57,26 @@
NF Visita - 🚚 - 🚗 - 📞 + <%= icone :caminhao %> + <%= icone :veiculo %> + <%= icone :telefone %>
- <%# Coluna esquerda — fotos %> + <%# Coluna esquerda — galeria com TODAS as fotos do lançamento %>
-

Foto da fachada

-
- +

+ Fotos do lançamento +

+
<%# Coluna direita — campos click-to-edit %> @@ -96,15 +94,47 @@
-

🕑 Histórico de alterações

+

<%= icone :historico, espaco: false %> Histórico de alterações

+ class="w-9 h-9 rounded-lg bg-white/5 hover:bg-white/10 text-gray-300" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5', espaco: false %>
+ + <%# Visualizador de foto — amplia e navega entre todas as imagens do lançamento %> +
diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 74f76b1..28055e9 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -18,11 +18,11 @@
- <%= nav_link_to 'Usuários', usuarios_path, icon: '👥' %> - <%= nav_link_to 'Configurações', configuracoes_path, icon: '⚙️' %> + <%= nav_link_to 'Usuários', usuarios_path, icon: :usuarios %> + <%= nav_link_to 'Configurações', configuracoes_path, icon: :configuracoes %> <% end %> <%# PDFs (fase 7 — link desabilitado por enquanto) %> @@ -41,7 +41,7 @@

Relatórios

- 📄 + <%= icone :documento, cor: nil, tamanho: 'w-6 h-6' %> Relatórios PDF Em breve
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e9d0fda..0eef752 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -100,7 +100,7 @@