Atualização dos icones para um modelo chapado

This commit is contained in:
2026-07-20 15:08:09 -03:00
parent 7d3753f9c5
commit 42d06c50a7
34 changed files with 550 additions and 337 deletions

View File

@@ -2,19 +2,105 @@
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")
def icone(nome, cor: "text-brand-laranja", tamanho: "w-[1.05em] h-[1.05em]", classe: nil, titulo: nil)
simbolo = ICONES.fetch(nome.to_sym) { nome.to_s }
css = ["inline-block align-[-0.125em] shrink-0", 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
# <input type="submit">, que não aceita HTML dentro. Nesses casos use
# `button_tag`/`button_to ... do`.
def rotulo(nome, texto, cor: "text-brand-laranja", tamanho: "w-4 h-4")
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-4 h-4"), label])
else
label
end
link_to conteudo, path, class: css
end
# Formata moeda BR (milhar com ".", decimais com ","): R$ 1.234,56
@@ -22,31 +108,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-3.5 h-3.5"), cfg[:label]])
end
end

View File

@@ -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 <svg> 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)) {

View File

@@ -1,7 +1,7 @@
<%# app/views/admin/auditoria_logs/index.html.erb %>
<% content_for :title, 'Auditoria' %>
<h1 class="text-2xl font-bold text-white mb-6">🔍 Auditoria</h1>
<h1 class="text-2xl font-bold text-white mb-6 flex items-center gap-2"><%= icone :auditoria %> Auditoria</h1>
<%= 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 %>

View File

@@ -10,7 +10,7 @@
<%# Pilares de preço %>
<div>
<h2 class="text-lg font-semibold text-white mb-4">💰 Pilares de Preço</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :dinheiro %> Pilares de Preço</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<% @configuracoes.each do |cfg| %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 group relative">
@@ -39,7 +39,7 @@
<%# Informações do banco externo %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-4">🗄️ Banco de Dados Externo</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :arquivar %> Banco de Dados Externo</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5">
<p class="text-gray-400 text-xs mb-1">Tabela monitorada</p>

View File

@@ -20,8 +20,8 @@
<%# Aviso de sincronização %>
<div class="p-4 bg-amber-900/20 border border-amber-500/40 rounded-xl text-amber-200/90 text-sm flex items-start gap-2">
<span class="text-base leading-none">⚠️</span>
<p>As alterações são gravadas no <strong>SimpliRoute na hora</strong> (cada campo salva ao confirmar ),
<%= icone :alerta, cor: nil, tamanho: 'w-4 h-4 mt-0.5' %>
<p>As alterações são gravadas no <strong>SimpliRoute na hora</strong> (cada campo salva ao confirmar <%= icone :confirmar, cor: nil, tamanho: 'w-3 h-3' %>),
mas o painel e as consolidações só refletem <strong>após a próxima sincronização</strong> da base de
rastreio. Toda alteração fica registrada na auditoria.</p>
</div>
@@ -57,15 +57,15 @@
<div class="flex flex-wrap gap-2 mt-2 text-xs text-gray-500">
<span class="px-2 py-0.5 rounded-full bg-white/5">NF <span id="c-nf">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5">Visita <span id="c-id">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5">🚚 <span id="c-motorista">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5">🚗 <span id="c-veiculo">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5">📞 <span id="c-telefone">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5"><%= icone :caminhao %> <span id="c-motorista">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5"><%= icone :veiculo %> <span id="c-veiculo">—</span></span>
<span class="px-2 py-0.5 rounded-full bg-white/5"><%= icone :telefone %> <span id="c-telefone">—</span></span>
</div>
</div>
<button id="btn-historico" type="button"
class="px-4 py-2.5 bg-[#1a1a1a] hover:bg-[#252525] border border-white/10 text-gray-200
text-sm font-semibold rounded-xl transition-colors whitespace-nowrap">
🕑 Histórico
<%= icone :historico %> Histórico
</button>
</div>
@@ -96,9 +96,9 @@
<div id="modal-hist-overlay" class="absolute inset-0 bg-black/70"></div>
<div class="relative bg-[#151515] border border-white/10 rounded-2xl w-full max-w-2xl max-h-[85vh] flex flex-col">
<div class="flex items-center justify-between p-4 border-b border-white/5">
<h3 class="text-white font-bold">🕑 Histórico de alterações</h3>
<h3 class="text-white font-bold flex items-center gap-2"><%= icone :historico %> Histórico de alterações</h3>
<button id="modal-hist-fechar" type="button"
class="w-9 h-9 rounded-lg bg-white/5 hover:bg-white/10 text-gray-300"></button>
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-4 h-4' %></button>
</div>
<div id="modal-hist-corpo" class="p-4 overflow-y-auto space-y-5 text-sm">
<p class="text-gray-400">Carregando…</p>
@@ -121,12 +121,18 @@
let visita = null; // estado atual do lançamento carregado
// ── Config dos campos editáveis ─────────────────────────────
// Ícone do sprite (public/icons.svg). fill=currentColor -> herda a cor do badge.
function ico(id, cls) {
return '<svg class="' + (cls || 'w-3.5 h-3.5') + '" fill="currentColor" ' +
'style="display:inline-block;vertical-align:-2px"><use href="/icons.svg#' + id + '"></use></svg>';
}
const STATUS = {
completed: { label: 'Completa', cls: 'bg-green-600 text-white', icone: '' },
failed: { label: 'Falha', cls: 'bg-red-600 text-white', icone: '' },
pending: { label: 'Pendente', cls: 'bg-yellow-500 text-black', icone: '🕗' },
partial: { label: 'Parcial', cls: 'bg-blue-600 text-white', icone: '◐' },
canceled: { label: 'Cancelada', cls: 'bg-gray-700 text-gray-300', icone: '🚫' }
completed: { label: 'Completa', cls: 'bg-green-600 text-white', icone: 'check-circle-fill' },
failed: { label: 'Falha', cls: 'bg-red-600 text-white', icone: 'x-circle-fill' },
pending: { label: 'Pendente', cls: 'bg-yellow-500 text-black', icone: 'clock-fill' },
partial: { label: 'Parcial', cls: 'bg-blue-600 text-white', icone: 'hourglass-split' },
canceled: { label: 'Cancelada', cls: 'bg-gray-700 text-gray-300', icone: 'slash-circle' }
};
const CAMPOS = [
{ campo: 'status', label: 'Status', tipo: 'status' },
@@ -190,10 +196,10 @@
}
function renderBadge() {
const cfg = STATUS[visita.status] || { label: visita.status || '—', cls: 'bg-gray-800 text-gray-400', icone: '' };
const cfg = STATUS[visita.status] || { label: visita.status || '—', cls: 'bg-gray-800 text-gray-400', icone: 'question-circle' };
const b = document.getElementById('c-badge');
b.className = 'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold ' + cfg.cls;
b.textContent = cfg.icone + ' ' + cfg.label;
b.innerHTML = ico(cfg.icone) + ' ' + esc(cfg.label);
}
function renderFotos() {
@@ -207,7 +213,7 @@
} else {
wrap.innerHTML =
'<div class="w-full h-52 rounded-xl border border-dashed border-white/10 flex items-center justify-center ' +
'text-gray-600 text-sm">📷 Sem foto da fachada para esta entrega</div>';
'text-gray-600 text-sm">' + ico('camera-fill') + ' Sem foto da fachada para esta entrega</div>';
}
const extras = [...(visita.pictures || [])];
@@ -257,7 +263,7 @@
'<p class="text-xs text-gray-500 mb-0.5">' + esc(def.label) + '</p>' +
'<div class="flex items-start justify-between gap-2">' +
'<p class="texto-valor text-sm text-white break-words min-h-[20px]">' + esc(valorExibicao(def)) + '</p>' +
'<span class="text-gray-600 group-hover:text-orange-400 text-sm" title="Editar">✏️</span>' +
'<span class="text-gray-600 group-hover:text-orange-400" title="Editar">' + ico('pencil-fill') + '</span>' +
'</div>';
div.onclick = () => abrirEdicao(div, def);
if (flash) {
@@ -297,8 +303,8 @@
'<p class="text-xs text-orange-400 mb-1">' + esc(def.label) + '</p>' +
editorHtml +
'<div class="flex items-center gap-2 mt-2">' +
'<button type="button" class="btn-ok px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-bold rounded-lg"> Salvar</button>' +
'<button type="button" class="btn-cancelar px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-300 text-xs rounded-lg"> Cancelar</button>' +
'<button type="button" class="btn-ok px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-bold rounded-lg">' + ico('check-lg', 'w-3 h-3') + ' Salvar</button>' +
'<button type="button" class="btn-cancelar px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-300 text-xs rounded-lg">' + ico('x-lg', 'w-3 h-3') + ' Cancelar</button>' +
'<span class="msg-salvando hidden text-xs text-gray-400">Salvando…</span>' +
'</div>' +
'<p class="msg-erro hidden mt-1.5 text-xs text-red-400"></p>';
@@ -388,7 +394,7 @@
function htmlHistorico(data) {
let html = '<div>' +
'<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">📒 Nosso sistema (auditoria)</p>';
'<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">' + ico('journal-text') + ' Nosso sistema (auditoria)</p>';
const interno = (data.interno || []);
if (!interno.length) {
html += '<p class="text-gray-500">Nenhuma alteração registrada pelo sistema.</p>';
@@ -400,14 +406,14 @@
' → <span class="text-green-300">' + esc(fmtValor(campo, l.para[campo])) + '</span></li>').join('');
return '<div class="bg-[#1a1a1a] border border-white/5 rounded-xl p-3 mb-2">' +
'<div class="flex items-center justify-between text-xs text-gray-500 mb-1.5">' +
'<span>👤 ' + esc(l.usuario) + '</span><span>' + esc(l.quando) + '</span>' +
'<span>' + ico('person-fill') + ' ' + esc(l.usuario) + '</span><span>' + esc(l.quando) + '</span>' +
'</div>' +
'<ul class="space-y-0.5 text-xs">' + (mudancas || '<li class="text-gray-500">—</li>') + '</ul>' +
'</div>';
}).join('');
}
html += '</div><div>' +
'<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">🌐 SimpliRoute (API)</p>';
'<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">' + ico('globe2') + ' SimpliRoute (API)</p>';
const api = (data.api || []);
if (!api.length) {
html += '<p class="text-gray-500">Sem registros de histórico na API do SimpliRoute.</p>';

View File

@@ -43,7 +43,7 @@
<button type="submit"
class="px-6 py-3 bg-[#f97316] hover:bg-orange-500 text-white font-semibold rounded-xl
transition-colors min-h-[48px] whitespace-nowrap">
📥 Baixar planilha
<%= icone :baixar, cor: nil %> Baixar planilha
</button>
<% end %>
<% end %>

View File

@@ -103,7 +103,7 @@
<% if @usuarios.empty? %>
<tr>
<td colspan="5" class="px-6 py-16 text-center text-gray-500">
<div class="text-4xl mb-3">👥</div>
<div class="mb-3"><%= icone :usuarios, tamanho: 'w-10 h-10', cor: 'text-gray-600' %></div>
<p>Nenhum usuário encontrado.</p>
</td>
</tr>

View File

@@ -10,7 +10,7 @@
<%# Pilares de preço %>
<div>
<h2 class="text-lg font-semibold text-white mb-4">💰 Pilares de Preço</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :dinheiro %> Pilares de Preço</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<% @configuracoes.each do |cfg| %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 group relative">
@@ -39,7 +39,7 @@
<%# Informações do banco externo %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-4">🗄️ Banco de Dados Externo</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :arquivar %> Banco de Dados Externo</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5">
<p class="text-gray-400 text-xs mb-1">Tabela monitorada</p>

View File

@@ -4,10 +4,10 @@
<div class="max-w-3xl mx-auto">
<div class="mb-6">
<%= link_to 'Voltar (Passo 2)',
<%= link_to rotulo(:voltar, 'Voltar (Passo 2)', cor: nil),
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista),
class: 'inline-flex items-center gap-1 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-[#2a2a2a] hover:border-orange-500 font-semibold px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
<h1 class="text-2xl font-bold text-white mt-2">📋 Revisão — <%= @motorista %></h1>
<h1 class="text-2xl font-bold text-white mt-2 flex items-center gap-2"><%= icone :planilha %> Revisão — <%= @motorista %></h1>
<p class="text-gray-400 text-sm"><%= @consolidacao.nome %></p>
</div>
@@ -71,19 +71,20 @@
<%= c.tipo_cor[:label] %><% if (c.termo? || c.termo_especial?) && c.quantidade > 1 %> ×<%= c.quantidade %><% end %>
</span>
<% if c.manual? %>
<span class="ml-1 bg-orange-500/15 text-orange-400 border border-orange-500/40 px-2 py-1 rounded text-xs font-bold"> Apontamento</span>
<span class="ml-1 bg-orange-500/15 text-orange-400 border border-orange-500/40 px-2 py-1 rounded text-xs font-bold"><%= icone :adicionar, cor: nil, tamanho: 'w-3 h-3' %> Apontamento</span>
<% end %>
</td>
<td class="px-4 py-3 text-right font-bold <%= c.desconto? ? 'text-red-400' : 'text-white' %>">
<div class="flex items-center justify-end gap-2">
<span><%= c.desconto? ? '-' : '' %><%= moeda(c.valor_aplicado) %></span>
<% if c.manual? && !(@consolidacao.finalizada? && current_user.operador?) %>
<%= button_to '✕',
remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, id: c.id, motorista: @motorista),
<%= button_to remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, id: c.id, motorista: @motorista),
method: :delete,
form: { data: { turbo_confirm: 'Remover este apontamento manual?' } },
class: 'text-gray-500 hover:text-red-400 text-sm leading-none px-1',
title: 'Remover apontamento' %>
class: 'text-gray-500 hover:text-red-400 leading-none px-1',
title: 'Remover apontamento' do %>
<%= icone :fechar, cor: nil, tamanho: 'w-3.5 h-3.5' %>
<% end %>
<% end %>
</div>
</td>
@@ -127,11 +128,11 @@
<%# Ações finais %>
<div class="flex flex-col md:flex-row gap-3">
<% if @proximo %>
<%= link_to "Próximo motorista: #{@proximo}",
<%= link_to safe_join(["Próximo motorista: #{@proximo}", icone(:avancar, cor: nil)], ' '),
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @proximo),
class: 'flex-1 bg-orange-500 hover:bg-orange-600 text-black font-bold py-3.5 rounded-lg min-h-[48px] flex items-center justify-center text-center' %>
<% end %>
<%= link_to '💾 Salvar rascunho e voltar', wizard_consolidacao_path(@consolidacao),
<%= link_to rotulo(:salvar, 'Salvar rascunho e voltar', cor: nil), wizard_consolidacao_path(@consolidacao),
class: 'flex-1 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-[#2a2a2a] font-bold py-3.5 rounded-lg min-h-[48px] flex items-center justify-center' %>
</div>
</div>

View File

@@ -18,10 +18,10 @@
class: 'inline-flex items-center gap-1 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-[#2a2a2a] hover:border-orange-500 font-semibold px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
<div class="flex items-center justify-between mt-2">
<div>
<h1 class="text-2xl font-bold text-white">🚚 <%= @motorista %></h1>
<h1 class="text-2xl font-bold text-white flex items-center gap-2"><%= icone :caminhao %> <%= @motorista %></h1>
<p class="text-gray-400 text-sm"><%= @consolidacao.nome %> · <%= l @consolidacao.data_inicio, format: :short %> → <%= l @consolidacao.data_fim, format: :short %></p>
</div>
<%= link_to 'Revisar (Passo 3)',
<%= link_to safe_join(['Revisar (Passo 3)', icone(:avancar, cor: nil)], ' '),
revisar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista),
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg min-h-[48px] flex items-center' %>
</div>
@@ -31,7 +31,7 @@
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
<div class="flex justify-between text-sm mb-2">
<span class="text-white font-semibold">
<% if @veiculos_sel.any? %>🚚 <%= @veiculos_sel.join(', ') %> — <% end %>
<% if @veiculos_sel.any? %><%= icone :caminhao %> <%= @veiculos_sel.join(', ') %> — <% end %>
<span data-validacao-target="contadorClassificadas"><%= @classificadas %></span>
de <span data-validacao-target="contadorTotal"><%= @total_entregas %></span> entregas classificadas
<span data-validacao-target="manuaisInfo" class="<%= 'hidden' unless @manuais.positive? %> text-blue-400 font-normal">·
@@ -53,8 +53,8 @@
<% if @veiculos_status.size > 1 %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
<div class="flex items-center gap-2 mb-2 flex-wrap">
<span class="text-gray-400 text-sm font-semibold">🚗 Fechar por veículo:</span>
<span class="text-gray-600 text-xs">clique no nome para filtrar; consolide o carro quando estiver 100% (🔒)</span>
<span class="text-gray-400 text-sm font-semibold"><%= icone :veiculo %> Fechar por veículo:</span>
<span class="text-gray-600 text-xs">clique no nome para filtrar; consolide o carro quando estiver 100% (<%= icone :travado, cor: nil, tamanho: 'w-3 h-3' %>)</span>
</div>
<div class="flex flex-wrap gap-2">
<%# Chip "Todos" — limpa a seleção (mostra todos os carros) %>
@@ -68,7 +68,7 @@
<% selecionado = @veiculos_sel.include?(v[:vehicle]) %>
<%# Alterna este veículo dentro da seleção atual (multi-seleção) %>
<% nova_sel = selecionado ? (@veiculos_sel - [v[:vehicle]]) : (@veiculos_sel + [v[:vehicle]]) %>
<%# Cor determinística: selecionado(laranja) > consolidado(verde+🔒) >
<%# Cor determinística: selecionado(laranja) > consolidado(verde+cadeado) >
pronto(verde) > arquivado(cinza tracejado) > neutro. %>
<% cor = if selecionado then 'border-orange-500 text-orange-400 bg-orange-500/10'
elsif v[:fechado] then 'border-green-500 text-green-400 bg-green-500/10'
@@ -78,11 +78,11 @@
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: nova_sel.presence),
data: { 'validacao-target': 'chipVeiculo', vehicle: v[:vehicle], eligible: v[:eligible] },
class: "px-3 py-2 rounded-lg text-sm font-semibold border min-h-[40px] flex items-center gap-2 transition-colors #{cor}" do %>
<span data-chip-lock class="<%= 'hidden' unless v[:fechado] %>">🔒</span>
<span data-chip-lock class="<%= 'hidden' unless v[:fechado] %>"><%= icone :travado, cor: nil, tamanho: 'w-3 h-3' %></span>
<span><%= v[:vehicle] %></span>
<span class="text-xs <%= v[:completo] ? 'text-green-400' : 'text-gray-500' %>">
<span data-chip-feito><%= v[:classificadas] %></span>/<span data-chip-total><%= v[:eligible] %></span>
<span data-chip-check><%= '✓' if v[:completo] %></span>
<span data-chip-check class="<%= 'hidden' unless v[:completo] %>"><%= icone :confirmar, cor: nil, tamanho: 'w-3 h-3' %></span>
</span>
<% end %>
<% end %>
@@ -97,32 +97,35 @@
<p class="text-gray-500 text-xs">Selecione <strong>um</strong> veículo (clique no nome) para consolidá-lo separadamente.</p>
<% elsif status_unico[:fechado] %>
<div class="flex items-center gap-2 flex-wrap">
<span class="text-green-400 text-sm font-semibold">🔒 <%= sel_unico %> consolidado.</span>
<%= button_to '🔓 Reabrir veículo',
reabrir_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
<span class="text-green-400 text-sm font-semibold"><%= icone :travado, cor: nil %> <%= sel_unico %> consolidado.</span>
<%= button_to reabrir_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
method: :post,
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
data: { turbo_confirm: "Reabrir o veículo #{sel_unico} para edição?" },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-orange-500 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
form: { data: { turbo_confirm: "Reabrir o veículo #{sel_unico} para edição?" } },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-orange-500 px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :destravado, cor: nil, tamanho: 'w-3.5 h-3.5' %> Reabrir veículo
<% end %>
</div>
<% else %>
<div class="flex items-center gap-2 flex-wrap">
<%# Habilita só quando o carro está 100% classificado; o JS liga/desliga
conforme a barra do escopo selecionado chega (ou não) a 100%. %>
<%= button_to '🔒 Consolidar este veículo',
fechar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
<%= button_to fechar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
method: :post,
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
disabled: !status_unico[:completo],
data: { 'validacao-target': 'consolidarBtn',
turbo_confirm: "Consolidar o veículo #{sel_unico}? Você poderá reabrir depois." },
class: "font-bold px-4 py-2 rounded-lg text-xs min-h-[40px] #{status_unico[:completo] ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" %>
<%= button_to '🗄️ Arquivar veículo',
arquivar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
data: { 'validacao-target': 'consolidarBtn' },
form: { data: { turbo_confirm: "Consolidar o veículo #{sel_unico}? Você poderá reabrir depois." } },
class: "font-bold px-4 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5 #{status_unico[:completo] ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" do %>
<%= icone :travado, cor: nil, tamanho: 'w-3.5 h-3.5' %> Consolidar este veículo
<% end %>
<%= button_to arquivar_veiculo_consolidacao_consolidacao_entregas_path(@consolidacao),
method: :post,
params: { motorista: @motorista, vehicle: sel_unico, vehicle_sel: @veiculos_sel },
data: { turbo_confirm: "Arquivar o veículo #{sel_unico}? Ele sai da finalização (as entregas já classificadas continuam no pagamento)." },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
form: { data: { turbo_confirm: "Arquivar o veículo #{sel_unico}? Ele sai da finalização (as entregas já classificadas continuam no pagamento)." } },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :arquivar, cor: nil, tamanho: 'w-3.5 h-3.5' %> Arquivar veículo
<% end %>
<span class="text-gray-500 text-xs" data-validacao-target="consolidarHint">
<%= status_unico[:completo] ? 'Pronto para consolidar.' : 'Classifique 100% deste veículo para consolidar.' %>
</span>
@@ -160,7 +163,7 @@
<%# Oculta/mostra o resumo lateral (mais largura para a lista) %>
<button type="button" data-action="validacao#toggleResumo"
class="text-gray-400 hover:text-white text-sm border border-[#2a2a2a] hover:border-orange-500 px-3 py-2 rounded-lg whitespace-nowrap">
📊 <span data-validacao-target="btnResumoLabel">Ocultar resumo</span>
<%= icone :dashboard %> <span data-validacao-target="btnResumoLabel">Ocultar resumo</span>
</button>
<%= link_to params[:apenas_pendentes] == '1' ? 'Mostrar todas' : 'Apenas não classificadas',
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: @veiculos_sel.presence,
@@ -182,20 +185,20 @@
<%# Botão único + menu suspenso %>
<button type="button" data-action="ferramentas#toggle"
class="w-full md:w-auto inline-flex items-center justify-center gap-2 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-dashed border-[#3a3a3a] hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px]">
Adicionar lançamento
<span class="text-gray-500">▾</span>
<%= icone :adicionar %> Adicionar lançamento
<%= icone :expandir, cor: 'text-gray-500' %>
</button>
<div data-ferramentas-target="menu"
class="hidden absolute z-40 mt-2 w-72 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl shadow-xl overflow-hidden">
<button type="button" data-action="apontamento-manual#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-l-2 border-transparent hover:border-purple-500">
<span class="block text-white font-semibold">📝 Registrar entrega manualmente</span>
<span class="block text-white font-semibold"><%= icone :apontamento %> Registrar entrega manualmente</span>
<span class="block text-gray-500 text-xs">fora da base de rastreio</span>
</button>
<button type="button" data-action="apontamento-termo#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-t border-[#2a2a2a] border-l-2 border-l-transparent hover:border-l-blue-500">
<span class="block text-white font-semibold">📄 Entrega de termo</span>
<span class="block text-white font-semibold"><%= icone :documento %> Entrega de termo</span>
<span class="block text-gray-500 text-xs">sem nota/código — só quantidade</span>
</button>
</div>
@@ -207,7 +210,7 @@
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white">Registro manual de entrega</h2>
<button type="button" data-action="apontamento-manual#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none"></button>
class="text-gray-500 hover:text-white leading-none" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<p class="text-gray-400 text-sm mb-4">Lançe uma entrega à mão, sem buscar na base de rastreio. Use quando o motorista foi ao local mas a nota ficou registrada com outro motorista.</p>
@@ -251,16 +254,16 @@
<%# Pilares (múltiplos) %>
<label class="block text-gray-400 text-sm mb-2">Classificar como <span class="text-gray-600">(pode marcar mais de uma)</span></label>
<div class="grid grid-cols-2 gap-2 mb-3">
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
['retirada', '📦 Retirada', Configuracao.preco_retirada],
['bonus', 'Bônus', Configuracao.preco_bonus],
['desconto', '⚠️ Desconto', Configuracao.preco_desconto],
['extraordinaria', 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, label, preco| %>
<% [['entrega_normal', :caminhao, 'Entrega Normal', Configuracao.preco_entrega],
['retirada', :retirada, 'Retirada', Configuracao.preco_retirada],
['bonus', :bonus, 'Bônus', Configuracao.preco_bonus],
['desconto', :desconto, 'Desconto', Configuracao.preco_desconto],
['extraordinaria', :extraordinaria, 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, ic, label, preco| %>
<label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-purple-500 text-sm text-white">
<input type="checkbox" value="<%= tipo %>" data-apontamento-manual-target="tipoCheck"
<%= 'checked' if tipo == 'entrega_normal' %>
class="accent-purple-500">
<span><%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
<span><%= icone ic %> <%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
</label>
<% end %>
</div>
@@ -274,7 +277,7 @@
<button type="button" data-action="apontamento-manual#adicionar"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar apontamento
Adicionar apontamento <%= icone :confirmar, cor: nil %>
</button>
</div>
</div>
@@ -284,9 +287,9 @@
class="hidden fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl w-full max-w-md p-6">
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white">📄 Entrega de termo</h2>
<h2 class="text-xl font-bold text-white flex items-center gap-2"><%= icone :documento %> Entrega de termo</h2>
<button type="button" data-action="apontamento-termo#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none"></button>
class="text-gray-500 hover:text-white leading-none" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<p class="text-gray-400 text-sm mb-4">Entrega sem nota fiscal nem código de rastreio. Informe quantos termos de cada tipo foram entregues.</p>
@@ -295,7 +298,7 @@
<%# Motorista (contexto da tela) %>
<div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-3 mb-4">
<p class="text-gray-500 text-xs">Motorista</p>
<p class="text-white font-semibold">🚚 <%= @motorista %></p>
<p class="text-white font-semibold"><%= icone :caminhao %> <%= @motorista %></p>
</div>
<%# Veículo (Q3) — a qual carro estes termos pertencem %>
@@ -311,7 +314,7 @@
<%# Steppers de quantidade — termo normal e termo especial %>
<div class="grid grid-cols-2 gap-3 mb-5">
<div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3">
<p class="text-white text-sm font-semibold text-center">📄 Termo Normal</p>
<p class="text-white text-sm font-semibold text-center"><%= icone :documento %> Termo Normal</p>
<p class="text-blue-400 text-xs text-center mb-2"><%= moeda(@precos[:termo]) %> cada</p>
<div class="flex items-center justify-center gap-2">
<button type="button" data-action="apontamento-termo#menos"
@@ -323,7 +326,7 @@
</div>
</div>
<div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3">
<p class="text-white text-sm font-semibold text-center">📋 Termo Especial</p>
<p class="text-white text-sm font-semibold text-center"><%= icone :termo_especial %> Termo Especial</p>
<p class="text-cyan-400 text-xs text-center mb-2"><%= moeda(@precos[:termo_especial]) %> cada</p>
<div class="flex items-center justify-center gap-2">
<button type="button" data-action="apontamento-termo#menosEspecial"
@@ -338,7 +341,7 @@
<button type="button" data-action="apontamento-termo#adicionar"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar termos
Adicionar termos <%= icone :confirmar, cor: nil %>
</button>
</div>
</div>
@@ -351,10 +354,10 @@
<%# ── Lista de entregas (3 colunas) — área rolável ──── %>
<div class="lg:col-span-3 space-y-2 lg:overflow-y-auto lg:pr-1" data-validacao-target="listaCol">
<%# 🔍 Busca na listagem — filtra os cards por NF, local, endereço ou veículo %>
<%# Busca na listagem — filtra os cards por NF, local, endereço ou veículo %>
<div class="sticky top-0 z-20 bg-[#0a0a0a] pb-2">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">🔍</span>
<span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"><%= icone :buscar, cor: 'text-gray-500' %></span>
<input type="search" data-validacao-target="busca" data-action="input->validacao#filtrarBusca"
placeholder="Buscar NF, local, endereço ou veículo…" autocomplete="off"
class="w-full bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg pl-10 pr-3 py-2.5 text-sm text-gray-200 placeholder-gray-500 focus:border-orange-500 focus:outline-none">
@@ -379,15 +382,15 @@
<p class="text-white font-semibold truncate">
NF <%= e.numero_nf %> · <%= e.local %>
<% if e.falhada? %>
<span class="ml-1 align-middle text-[10px] font-bold uppercase tracking-wide bg-red-500/20 text-red-400 border border-red-500/40 px-1.5 py-0.5 rounded">⚠️ Insucesso</span>
<span class="ml-1 align-middle text-[10px] font-bold uppercase tracking-wide bg-red-500/20 text-red-400 border border-red-500/40 px-1.5 py-0.5 rounded"><%= icone :alerta, cor: nil, tamanho: 'w-3 h-3' %> Insucesso</span>
<% else %>
<span class="ml-1 align-middle text-[10px] font-bold uppercase tracking-wide bg-green-500/20 text-green-400 border border-green-500/40 px-1.5 py-0.5 rounded"> Entregue</span>
<span class="ml-1 align-middle text-[10px] font-bold uppercase tracking-wide bg-green-500/20 text-green-400 border border-green-500/40 px-1.5 py-0.5 rounded"><%= icone :confirmar, cor: nil, tamanho: 'w-3 h-3' %> Entregue</span>
<% end %>
</p>
<p class="text-gray-400 text-xs truncate">📍 <%= e.address.presence || 'sem endereço' %></p>
<p class="text-gray-400 text-xs truncate"><%= icone :local %> <%= e.address.presence || 'sem endereço' %></p>
<p class="text-gray-500 text-xs">
📅 <%= e.checkout&.strftime('%d/%m/%Y') || '—' %> · 🚚 <%= e.vehicle.presence || 'sem veículo' %>
<% if e.atraso_minutos > 0 %> · ⏱️ atraso <%= e.atraso_minutos %>min<% end %>
<%= icone :calendario %> <%= e.checkout&.strftime('%d/%m/%Y') || '—' %> · <%= icone :caminhao %> <%= e.vehicle.presence || 'sem veículo' %>
<% if e.atraso_minutos > 0 %> · <%= icone :relogio, cor: 'text-amber-400' %> atraso <%= e.atraso_minutos %>min<% end %>
</p>
</div>
@@ -412,7 +415,7 @@
<% end %>
<% else %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-10 text-center">
<p class="text-4xl mb-3">🎉</p>
<p class="mb-3"><%= icone :sucesso, tamanho: 'w-10 h-10', cor: 'text-green-500' %></p>
<p class="text-gray-400">Nenhuma entrega pendente neste filtro.</p>
</div>
<% end %>
@@ -421,7 +424,7 @@
<%# ── Resumo lateral (1 coluna) — rola junto se precisar ─────── %>
<div class="lg:col-span-1 lg:overflow-y-auto" data-validacao-target="resumo">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-5 space-y-4">
<h3 class="text-white font-bold">📊 Resumo</h3>
<h3 class="text-white font-bold flex items-center gap-2"><%= icone :dashboard %> Resumo</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between items-center">
@@ -465,13 +468,13 @@
</div>
<div class="border-t border-[#2a2a2a] pt-4 text-xs text-gray-500 space-y-1">
<p>💰 Normal: <%= moeda(@precos[:entrega_normal]) %></p>
<p>📦 Retirada: <%= moeda(@precos[:retirada]) %></p>
<p> Bônus: <%= moeda(@precos[:bonus]) %></p>
<p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p>
<p> Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
<p>📄 Termo: <%= moeda(@precos[:termo]) %></p>
<p>📋 Termo Especial: <%= moeda(@precos[:termo_especial]) %></p>
<p><%= icone :dinheiro %> Normal: <%= moeda(@precos[:entrega_normal]) %></p>
<p><%= icone :retirada %> Retirada: <%= moeda(@precos[:retirada]) %></p>
<p><%= icone :bonus %> Bônus: <%= moeda(@precos[:bonus]) %></p>
<p><%= icone :desconto %> Desconto: -<%= moeda(@precos[:desconto]) %></p>
<p><%= icone :extraordinaria %> Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
<p><%= icone :documento %> Termo: <%= moeda(@precos[:termo]) %></p>
<p><%= icone :termo_especial %> Termo Especial: <%= moeda(@precos[:termo_especial]) %></p>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
</div>
<div style="background: #ffffff; padding: 28px;">
<h2 style="color: #0a0a0a; margin-top: 0;">Olá, <%= @user.nome.split.first %>!</h2>
<h2 style="color: #0a0a0a; margin-top: 0;">Olá, <%= @user.nome.split.first %>!</h2>
<p style="color: #374151; font-size: 15px; line-height: 1.6;">
Seu pagamento de entregas foi <strong>efetuado</strong> pela empresa.

View File

@@ -7,7 +7,7 @@
</div>
<div style="background: #ffffff; padding: 28px;">
<h2 style="color: #0a0a0a; margin-top: 0;">Olá, <%= @user.nome.split.first %>! 👋</h2>
<h2 style="color: #0a0a0a; margin-top: 0;">Olá, <%= @user.nome.split.first %>!</h2>
<p style="color: #374151; font-size: 15px; line-height: 1.6;">
Seu pagamento de entregas foi fechado pela empresa.

View File

@@ -2,8 +2,8 @@
<% content_for :title, 'Consolidações arquivadas' %>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-white">📁 Consolidações arquivadas</h1>
<%= link_to 'Voltar às ativas', consolidacoes_path,
<h1 class="text-2xl font-bold text-white flex items-center gap-2"><%= icone :arquivadas %> Consolidações arquivadas</h1>
<%= link_to rotulo(:voltar, 'Voltar às ativas', cor: nil), consolidacoes_path,
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center' %>
</div>
@@ -26,12 +26,12 @@
<%= badge_status(c.status) %>
</div>
<p class="text-gray-400 text-sm">
📅 <%= l c.data_inicio, format: :short %> <%= l c.data_fim, format: :short %>
· 👤 <%= c.consolidacao_motoristas.count %> motorista(s)
<%= icone :calendario %> <%= l c.data_inicio, format: :short %> <%= icone :avancar, tamanho: 'w-3 h-3' %> <%= l c.data_fim, format: :short %>
· <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s)
· Criada por <%= c.criador.nome_display %>
</p>
<p class="text-gray-500 text-xs mt-1">
📁 Arquivada
<%= icone :arquivar %> Arquivada
<% if c.arquivado_em.present? %>em <%= l c.arquivado_em, format: :short %><% end %>
<% if c.arquivador.present? %>por <%= c.arquivador.nome_display %><% end %>
</p>
@@ -45,12 +45,16 @@
<% if policy(c).destroy? %>
<div class="flex gap-2">
<%= button_to '♻️ Reativar', reativar_consolidacao_path(c), method: :post,
<%= button_to reativar_consolidacao_path(c), method: :post,
form: { data: { turbo_confirm: 'Reativar esta consolidação? Ela voltará para a lista de ativas.' } },
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
<%= button_to '🗑️ Excluir', consolidacao_path(c), method: :delete,
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-2.5 rounded-lg text-sm min-h-[44px] inline-flex items-center gap-2' do %>
<%= icone :restaurar, cor: nil, tamanho: 'w-4 h-4' %> Reativar
<% end %>
<%= button_to consolidacao_path(c), method: :delete,
form: { data: { turbo_confirm: 'EXCLUIR PERMANENTEMENTE esta consolidação e todas as suas classificações? Esta ação NÃO pode ser desfeita.' } },
class: 'text-red-400 hover:text-white hover:bg-red-600 border border-red-900 px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
class: 'text-red-400 hover:text-white hover:bg-red-600 border border-red-900 px-4 py-2.5 rounded-lg text-sm min-h-[44px] inline-flex items-center gap-2' do %>
<%= icone :excluir, cor: nil, tamanho: 'w-4 h-4' %> Excluir
<% end %>
</div>
<% end %>
</div>
@@ -60,7 +64,7 @@
</div>
<% else %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-12 text-center">
<p class="text-5xl mb-4">🗂️</p>
<p class="mb-4"><%= icone :pasta, tamanho: 'w-12 h-12', cor: 'text-gray-600' %></p>
<p class="text-gray-400 mb-4">Nenhuma consolidação arquivada.</p>
<%= link_to 'Ver consolidações ativas', consolidacoes_path,
class: 'inline-block bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg' %>

View File

@@ -4,14 +4,14 @@
<div data-controller="nota-avulsa"
data-nota-avulsa-buscar-url-value="<%= buscar_nf_consolidacoes_path %>">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-white">📦 Consolidações</h1>
<h1 class="text-2xl font-bold text-white flex items-center gap-2"><%= icone :consolidacoes %> Consolidações</h1>
<div class="flex gap-2">
<%= link_to '📁 Arquivadas', arquivadas_consolidacoes_path,
<%= link_to rotulo(:arquivadas, 'Arquivadas'), arquivadas_consolidacoes_path,
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center transition-colors' %>
<% if policy(Consolidacao).create? %>
<button type="button" data-action="nota-avulsa#abrir"
class="text-white border border-dashed border-orange-500/60 hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px] flex items-center transition-colors">
Nota avulsa
class="text-white border border-dashed border-orange-500/60 hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px] flex items-center gap-2 transition-colors">
<%= icone :adicionar %> Nota avulsa
</button>
<% end %>
<%= link_to '+ Nova Consolidação', new_consolidacao_path,
@@ -25,9 +25,9 @@
class="hidden fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto text-left">
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white"> Nota avulsa</h2>
<h2 class="text-xl font-bold text-white flex items-center gap-2"><%= icone :adicionar %> Nota avulsa</h2>
<button type="button" data-action="nota-avulsa#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none"></button>
class="text-gray-500 hover:text-white leading-none" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<p class="text-gray-400 text-sm mb-4">Para uma nota que chegou por fora: busque a NF e crie uma consolidação já com o apontamento do motorista.</p>
@@ -50,14 +50,14 @@
<div data-nota-avulsa-target="confirmar" class="hidden border-t border-[#2a2a2a] pt-4">
<label class="block text-gray-400 text-sm mb-2">Classificar como <span class="text-gray-600">(pode marcar mais de uma)</span></label>
<div class="grid grid-cols-2 gap-2 mb-4">
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
['retirada', '📦 Retirada', Configuracao.preco_retirada],
['bonus', 'Bônus', Configuracao.preco_bonus],
['desconto', '⚠️ Desconto', Configuracao.preco_desconto],
['extraordinaria', 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, label, preco| %>
<% [['entrega_normal', :caminhao, 'Entrega Normal', Configuracao.preco_entrega],
['retirada', :retirada, 'Retirada', Configuracao.preco_retirada],
['bonus', :bonus, 'Bônus', Configuracao.preco_bonus],
['desconto', :desconto, 'Desconto', Configuracao.preco_desconto],
['extraordinaria', :extraordinaria, 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, ic, label, preco| %>
<label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white">
<%= check_box_tag 'tipos[]', tipo, tipo == 'entrega_normal', id: nil, class: 'accent-orange-500' %>
<span><%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
<span><%= icone ic %> <%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
</label>
<% end %>
</div>
@@ -77,11 +77,11 @@
class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 focus:border-orange-500 focus:outline-none' %>
<%= f.select :status,
options_for_select([['Todos os status', ''], ['📝 Rascunho', 'rascunho'], ['Finalizada', 'finalizada']], params[:status]),
options_for_select([['Todos os status', ''], ['Rascunho', 'rascunho'], ['Finalizada', 'finalizada']], params[:status]),
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
<%= f.select :pagamento,
options_for_select([['💵 Pagamento (todos)', ''], ['💰 Pagas', 'pago'], ['Parciais', 'parcial'], ['🕗 Pendentes', 'pendente']], params[:pagamento]),
options_for_select([['Pagamento (todos)', ''], ['Pagas', 'pago'], ['Parciais', 'parcial'], ['Pendentes', 'pendente']], params[:pagamento]),
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
<%= f.date_field :inicio, value: params[:inicio],
@@ -110,12 +110,12 @@
<% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %>
</div>
<p class="text-gray-400 text-sm">
📅 <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %>
· 👤 <%= c.consolidacao_motoristas.count %> motorista(s)
<%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %>
· <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s)
· Criada por <%= c.criador.nome_display %>
</p>
<% if c.operacoes.present? %>
<p class="text-gray-500 text-xs mt-1">🗂️ <%= c.operacoes_label.join(' · ') %></p>
<p class="text-gray-500 text-xs mt-1"><%= icone :pasta %> <%= c.operacoes_label.join(' · ') %></p>
<% end %>
</div>
<div class="text-right">
@@ -128,7 +128,7 @@
</div>
<% else %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-12 text-center">
<p class="text-5xl mb-4">📭</p>
<p class="mb-4"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600' %></p>
<p class="text-gray-400 mb-4">Nenhuma consolidação encontrada.</p>
<%= link_to 'Criar primeira consolidação', new_consolidacao_path,
class: 'inline-block bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg' %>

View File

@@ -2,13 +2,13 @@
<% content_for :title, 'Nova Consolidação' %>
<div class="max-w-3xl mx-auto">
<h1 class="text-2xl font-bold text-white mb-6"> Nova Consolidação</h1>
<h1 class="text-2xl font-bold text-white mb-6 flex items-center gap-2"><%= icone :adicionar %> Nova Consolidação</h1>
<%= form_with model: @consolidacao, class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-6 space-y-6' do |f| %>
<% if @consolidacao.errors.any? %>
<div class="bg-red-900/40 border border-red-600 rounded-lg p-4">
<p class="text-red-400 font-bold mb-1">⚠️ Corrija os erros:</p>
<p class="text-red-400 font-bold mb-1 flex items-center gap-2"><%= icone :alerta, cor: nil %> Corrija os erros:</p>
<ul class="text-red-300 text-sm list-disc list-inside">
<% @consolidacao.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
@@ -20,7 +20,7 @@
<%# Operação — multi-select opcional (pré-preenche e filtra por NF) %>
<div>
<label class="block text-white font-semibold mb-2">
🗂️ Operação <span class="text-gray-500 font-normal text-sm">(opcional — preenche motoristas, veículos e período, e filtra as entregas por nota fiscal)</span>
<%= icone :pasta %> Operação <span class="text-gray-500 font-normal text-sm">(opcional — preenche motoristas, veículos e período, e filtra as entregas por nota fiscal)</span>
</label>
<div id="operacoes-lista"
class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3 max-h-48 overflow-y-auto space-y-1">

View File

@@ -15,7 +15,7 @@
<% if @consolidacao.finalizada? %><%= badge_pagamento(@consolidacao.status_pagamento) %><% end %>
</div>
<p class="text-gray-400 text-sm mt-1">
📅 <%= l @consolidacao.data_inicio, format: :short %> <%= l @consolidacao.data_fim, format: :short %>
<%= icone :calendario %> <%= l @consolidacao.data_inicio, format: :short %> <%= icone :avancar, tamanho: 'w-3 h-3' %> <%= l @consolidacao.data_fim, format: :short %>
· Criada por <%= @consolidacao.criador.nome_display %>
<% if @consolidacao.finalizada? %>
· Finalizada por <%= @consolidacao.finalizador&.nome_display %> em <%= l @consolidacao.finalizado_em, format: :short %>
@@ -23,7 +23,7 @@
</p>
<% if @consolidacao.operacoes.present? %>
<p class="text-gray-400 text-sm mt-1">
🗂️ Operação:
<%= icone :pasta %> Operação:
<% @consolidacao.operacoes_label.each do |op| %>
<span class="inline-block bg-[#2a2a2a] text-gray-200 rounded-full px-2.5 py-0.5 text-xs"><%= op %></span>
<% end %>
@@ -32,16 +32,18 @@
</div>
<div class="flex gap-2">
<% if @consolidacao.rascunho? %>
<%= link_to '✏️ Continuar validação', wizard_consolidacao_path(@consolidacao),
<%= link_to rotulo(:editar, 'Continuar validação', cor: nil), wizard_consolidacao_path(@consolidacao),
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg min-h-[48px] flex items-center' %>
<% end %>
<%= link_to '📊 Relatório financeiro', gerar_pdf_financeiro_consolidacao_path(@consolidacao),
<%= link_to rotulo(:dashboard, 'Relatório financeiro'), gerar_pdf_financeiro_consolidacao_path(@consolidacao),
data: { turbo: false },
class: 'bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px] flex items-center' %>
<% if policy(@consolidacao).destroy? && !@consolidacao.arquivada? %>
<%= button_to '📁 Arquivar', arquivar_consolidacao_path(@consolidacao), method: :post,
<%= button_to arquivar_consolidacao_path(@consolidacao), method: :post,
form: { data: { turbo_confirm: 'Arquivar esta consolidação? Ela não será excluída, apenas movida para o arquivo.' } },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px]' %>
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center gap-2' do %>
<%= icone :arquivar, cor: nil, tamanho: 'w-4 h-4' %> Arquivar
<% end %>
<% end %>
</div>
</div>
@@ -49,7 +51,7 @@
<%# ── Pagamento da operação inteira ─────────────────────── %>
<% if pode_pagar %>
<div class="flex flex-col sm:flex-row sm:items-center gap-2 mt-4 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
<span class="text-gray-400 text-sm font-medium mr-1">💵 Operação inteira:</span>
<span class="text-gray-400 text-sm font-medium mr-1"><%= icone :pagamento %> Operação inteira:</span>
<% if @consolidacao.status_pagamento != :pago %>
<%# Form único (id) — os inputs de NF e os botões "Marcar pago" de cada
linha se associam a ele via atributo HTML5 form=, evitando <form> aninhado. %>
@@ -57,15 +59,19 @@
id: 'form-pagar-tudo', class: 'flex items-center gap-2' do |f| %>
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 text-sm' %>
<%= f.submit '💰 Marcar tudo como pago',
<%= f.button type: :submit,
data: { turbo_confirm: 'Marcar TODOS os motoristas pendentes desta consolidação como PAGOS? Confirme que a Nota Fiscal de cada um foi preenchida.' },
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-4 py-2.5 rounded-lg text-sm cursor-pointer min-h-[44px]' %>
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-4 py-2.5 rounded-lg text-sm cursor-pointer min-h-[44px] inline-flex items-center gap-2' do %>
<%= icone :dinheiro, cor: nil, tamanho: 'w-4 h-4' %> Marcar tudo como pago
<% end %>
<% end %>
<% end %>
<% if @consolidacao.status_pagamento != :pendente %>
<%= button_to '↩ Estornar tudo', cancelar_pagamento_consolidacao_path(@consolidacao), method: :post,
data: { turbo_confirm: 'Estornar o pagamento de TODOS os motoristas?' },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
<%= button_to cancelar_pagamento_consolidacao_path(@consolidacao), method: :post,
form: { data: { turbo_confirm: 'Estornar o pagamento de TODOS os motoristas?' } },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-2.5 rounded-lg text-sm min-h-[44px] inline-flex items-center gap-2' do %>
<%= icone :estornar, cor: nil, tamanho: 'w-4 h-4' %> Estornar tudo
<% end %>
<% end %>
</div>
<% end %>
@@ -78,7 +84,7 @@
</div>
<%# Motoristas + botões de PDF %>
<h2 class="text-white font-bold text-lg mb-3">👥 Motoristas</h2>
<h2 class="text-white font-bold text-lg mb-3 flex items-center gap-2"><%= icone :usuarios %> Motoristas</h2>
<div class="space-y-3">
<% @motoristas.each do |cm| %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-5">
@@ -91,45 +97,49 @@
<% end %>
</div>
<p class="text-orange-500 font-black text-xl"><%= moeda(cm.valor_total) %></p>
<%# Breakdown por veículo (consolidados 🔒 / abertos). Informativo — o
<%# Breakdown por veículo (consolidados / abertos). Informativo — o
pagamento continua sendo do motorista inteiro. %>
<% veiculos = @veiculos_por_motorista[cm.motorista_nome] %>
<% if veiculos.present? %>
<div class="flex flex-wrap gap-1.5 mt-1">
<% veiculos.each do |cv| %>
<span class="text-xs px-2 py-0.5 rounded border <%= cv.fechado? ? 'border-green-500/50 text-green-400' : 'border-[#2a2a2a] text-gray-400' %>">
<%= cv.fechado? ? '🔒' : '○' %> <%= cv.vehicle %> · <%= moeda(cv.valor_total) %>
<%= cv.fechado? ? icone(:travado, cor: nil, tamanho: 'w-3 h-3') : icone(:ponto_vazio, cor: nil, tamanho: 'w-3 h-3') %> <%= cv.vehicle %> · <%= moeda(cv.valor_total) %>
</span>
<% end %>
</div>
<% end %>
<% if cm.pago? %>
<p class="text-green-400 text-xs mt-1">
💰 Pago em <%= l cm.pago_em, format: :short %>
<%= icone :dinheiro, cor: nil %> Pago em <%= l cm.pago_em, format: :short %>
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
<% if cm.pagador.present? %>por <%= cm.pagador.nome_display %><% end %>
</p>
<% if cm.nota_fiscal.present? %>
<p class="text-gray-300 text-xs mt-0.5">🧾 NF <%= cm.nota_fiscal %></p>
<p class="text-gray-300 text-xs mt-0.5"><%= icone :nota_fiscal %> NF <%= cm.nota_fiscal %></p>
<% end %>
<% end %>
<% if pode_pagar %>
<div class="mt-2">
<% if cm.pago? %>
<%= button_to '↩ Estornar pagamento', cancelar_pagamento_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id), method: :post,
data: { turbo_confirm: "Estornar o pagamento de #{cm.motorista_nome}?" },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
<%= button_to cancelar_pagamento_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id), method: :post,
form: { data: { turbo_confirm: "Estornar o pagamento de #{cm.motorista_nome}?" } },
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :estornar, cor: nil, tamanho: 'w-3.5 h-3.5' %> Estornar pagamento
<% end %>
<% else %>
<%# Inputs/botão associados ao form único #form-pagar-tudo via atributo form=. %>
<div class="flex items-center gap-2 flex-wrap">
<%= text_field_tag "notas_fiscais[#{cm.id}]", nil, form: 'form-pagar-tudo',
placeholder: 'Nº Nota Fiscal',
class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-2 py-2 text-xs w-36' %>
<%= button_tag '💰 Marcar pago', type: :submit, form: 'form-pagar-tudo',
<%= button_tag type: :submit, form: 'form-pagar-tudo',
name: 'consolidacao_motorista_id', value: cm.id,
data: { turbo_confirm: "Marcar #{cm.motorista_nome} como pago?" },
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2 rounded-lg text-xs cursor-pointer min-h-[40px]' %>
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2 rounded-lg text-xs cursor-pointer min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :dinheiro, cor: nil, tamanho: 'w-3.5 h-3.5' %> Marcar pago
<% end %>
</div>
<% end %>
</div>
@@ -139,13 +149,13 @@
<%# Preview abre modal %>
<button onclick="abrirPreview('<%= preview_extrato_consolidacao_path(@consolidacao, motorista: cm.motorista_nome) %>')"
class="bg-[#0a0a0a] hover:bg-[#2a2a2a] text-white border border-orange-500 font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px]">
👁️ Ver antes de gerar
<%= icone :visualizar %> Ver antes de gerar
</button>
<%= link_to '📄 Relatório Individual',
<%= link_to rotulo(:documento, 'Relatório Individual', cor: nil),
gerar_pdf_relatorio_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
data: { turbo: false },
class: 'bg-orange-800 hover:bg-orange-700 text-white font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
<%= link_to '🧾 Gerar Extrato',
<%= link_to rotulo(:nota_fiscal, 'Gerar Extrato', cor: nil),
gerar_pdf_extrato_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
data: { turbo: false },
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
@@ -158,7 +168,7 @@
<%# ── Apontamentos manuais (notas que chegaram por fora) ─────────────── %>
<% if @apontamentos.present? %>
<% pode_editar = !(@consolidacao.finalizada? && current_user.operador?) %>
<h2 class="text-white font-bold text-lg mt-8 mb-3"> Apontamentos manuais</h2>
<h2 class="text-white font-bold text-lg mt-8 mb-3 flex items-center gap-2"><%= icone :adicionar %> Apontamentos manuais</h2>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-[#0a0a0a] text-gray-400">
@@ -179,9 +189,9 @@
<tr>
<td class="px-4 py-3">
<p class="text-white font-bold"><%= if pilares.first.termo?
'📄 Entrega de termo'
rotulo(:documento, 'Entrega de termo')
elsif pilares.first.termo_especial?
'📋 Entrega de termo especial'
rotulo(:termo_especial, 'Entrega de termo especial')
else
"NF #{nf_exibida.presence || '—'}"
end %></p>
@@ -200,11 +210,12 @@
<td class="px-4 py-3 text-right font-bold <%= total.negative? ? 'text-red-400' : 'text-white' %>"><%= moeda(total) %></td>
<% if pode_editar %>
<td class="px-4 py-3 text-right">
<%= button_to '✕ Remover',
remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, tracking_id: pilares.first.tracking_id, motorista: motorista),
<%= button_to remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, tracking_id: pilares.first.tracking_id, motorista: motorista),
method: :delete,
form: { data: { turbo_confirm: eh_termo ? "Remover a entrega de termo de #{motorista}?" : "Remover o apontamento da NF #{nf_exibida.presence || '—'} de #{motorista}?" } },
class: 'text-gray-400 hover:text-red-400 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
class: 'text-gray-400 hover:text-red-400 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :fechar, cor: nil, tamanho: 'w-3 h-3' %> Remover
<% end %>
</td>
<% end %>
</tr>
@@ -219,8 +230,8 @@
<div id="modal-preview" class="fixed inset-0 bg-black/80 z-50 hidden items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl max-w-2xl w-full max-h-[85vh] flex flex-col">
<div class="flex items-center justify-between px-6 py-4 border-b border-[#2a2a2a]">
<h3 class="text-white font-bold">👁️ Pré-visualização do Extrato</h3>
<button onclick="fecharPreview()" class="text-gray-400 hover:text-white text-2xl min-w-[44px] min-h-[44px]"></button>
<h3 class="text-white font-bold flex items-center gap-2"><%= icone :visualizar %> Pré-visualização do Extrato</h3>
<button onclick="fecharPreview()" class="text-gray-400 hover:text-white min-w-[44px] min-h-[44px]" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<div id="modal-preview-conteudo" class="overflow-y-auto p-6 flex-1">
<div class="flex justify-center py-10"><div class="spinner w-10 h-10"></div></div>
@@ -228,11 +239,11 @@
<div class="flex gap-3 px-6 py-4 border-t border-[#2a2a2a]">
<a id="modal-confirmar" href="#" data-turbo="false"
class="flex-1 bg-orange-500 hover:bg-orange-600 text-black font-bold py-3 rounded-lg text-center min-h-[48px] flex items-center justify-center">
Confirmar e baixar PDF
<%= icone :confirmar, cor: nil %> Confirmar e baixar PDF
</a>
<button onclick="fecharPreview()"
class="px-6 py-3 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-lg min-h-[48px]">
Voltar
<%= icone :voltar, cor: nil, tamanho: 'w-4 h-4' %> Voltar
</button>
</div>
</div>

View File

@@ -8,7 +8,7 @@
<%= link_to '← Voltar para o resumo', consolidacao_path(@consolidacao), class: 'text-gray-500 hover:text-orange-500 text-sm' %>
<h1 class="text-2xl font-bold text-white mt-2"><%= @consolidacao.nome %></h1>
<p class="text-gray-400 text-sm">
📅 <%= l @consolidacao.data_inicio, format: :short %> <%= l @consolidacao.data_fim, format: :short %>
<%= icone :calendario %> <%= l @consolidacao.data_inicio, format: :short %> <%= icone :avancar, tamanho: 'w-3 h-3' %> <%= l @consolidacao.data_fim, format: :short %>
· <%= badge_status(@consolidacao.status) %>
</p>
</div>
@@ -42,20 +42,20 @@
<%# Botão único + menu suspenso %>
<button type="button" data-action="ferramentas#toggle"
class="w-full md:w-auto inline-flex items-center justify-center gap-2 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-dashed border-[#3a3a3a] hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px]">
Adicionar / Ferramentas
<span class="text-gray-500">▾</span>
<%= icone :adicionar %> Adicionar / Ferramentas
<%= icone :expandir, cor: 'text-gray-500' %>
</button>
<div data-ferramentas-target="menu"
class="hidden absolute z-40 mt-2 w-72 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl shadow-xl overflow-hidden">
<button type="button" data-action="apontamento#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-l-2 border-transparent hover:border-orange-500">
<span class="block text-white font-semibold">📝 Apontamento manual</span>
<span class="block text-white font-semibold"><%= icone :apontamento %> Apontamento manual</span>
<span class="block text-gray-500 text-xs">nota que chegou por fora (busca pela NF)</span>
</button>
<button type="button" data-action="adicionar-motorista#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-t border-[#2a2a2a] border-l-2 border-l-transparent hover:border-l-green-500">
<span class="block text-white font-semibold">🧑‍✈️ Adicionar motorista</span>
<span class="block text-white font-semibold"><%= icone :motorista %> Adicionar motorista</span>
<span class="block text-gray-500 text-xs">incluir/restaurar um motorista nesta consolidação</span>
</button>
</div>
@@ -67,7 +67,7 @@
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white">Apontamento manual</h2>
<button type="button" data-action="apontamento#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none"></button>
class="text-gray-500 hover:text-white leading-none" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<p class="text-gray-400 text-sm mb-4">Busque a nota pela base de rastreio e adicione ao fechamento do motorista correspondente.</p>
@@ -89,22 +89,22 @@
<div data-apontamento-target="confirmar" class="hidden border-t border-[#2a2a2a] pt-4">
<label class="block text-gray-400 text-sm mb-2">Classificar como <span class="text-gray-600">(pode marcar mais de uma)</span></label>
<div class="grid grid-cols-2 gap-2 mb-4">
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
['retirada', '📦 Retirada', Configuracao.preco_retirada],
['bonus', 'Bônus', Configuracao.preco_bonus],
['desconto', '⚠️ Desconto', Configuracao.preco_desconto],
['extraordinaria', 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, label, preco| %>
<% [['entrega_normal', :caminhao, 'Entrega Normal', Configuracao.preco_entrega],
['retirada', :retirada, 'Retirada', Configuracao.preco_retirada],
['bonus', :bonus, 'Bônus', Configuracao.preco_bonus],
['desconto', :desconto, 'Desconto', Configuracao.preco_desconto],
['extraordinaria', :extraordinaria, 'Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, ic, label, preco| %>
<label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white">
<input type="checkbox" value="<%= tipo %>" data-apontamento-target="tipoCheck"
<%= 'checked' if tipo == 'entrega_normal' %>
class="accent-orange-500">
<span><%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
<span><%= icone ic %> <%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>
</label>
<% end %>
</div>
<button type="button" data-action="apontamento#apontar"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar apontamento
Adicionar apontamento <%= icone :confirmar, cor: nil %>
</button>
</div>
</div>
@@ -115,9 +115,9 @@
class="hidden fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl w-full max-w-md p-6">
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white">🧑‍✈️ Adicionar motorista</h2>
<h2 class="text-xl font-bold text-white flex items-center gap-2"><%= icone :motorista %> Adicionar motorista</h2>
<button type="button" data-action="adicionar-motorista#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none"></button>
class="text-gray-500 hover:text-white leading-none" aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5' %></button>
</div>
<p class="text-gray-400 text-sm mb-4">Inclua um motorista nesta consolidação. Se ele estava arquivado, será restaurado.</p>
@@ -128,7 +128,7 @@
class="w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-3 text-base focus:border-orange-500 focus:outline-none mb-4">
<datalist id="motoristas-disponiveis">
<% Array(@motoristas_periodo).each do |m| %>
<option value="<%= m[:nome] %>" label="<%= '🚚 ' + m[:veiculos].join(', ') if m[:veiculos].any? %>"></option>
<option value="<%= m[:nome] %>" label="<%= m[:veiculos].join(', ') if m[:veiculos].any? %>"></option>
<% end %>
</datalist>
@@ -141,7 +141,7 @@
class="w-full text-left px-3 py-2 bg-[#0a0a0a] border border-[#2a2a2a] hover:border-orange-500 rounded-lg">
<span class="block text-white text-sm font-semibold"><%= m[:nome] %></span>
<% if m[:veiculos].any? %>
<span class="block text-gray-500 text-xs">🚚 <%= m[:veiculos].join(', ') %></span>
<span class="block text-gray-500 text-xs"><%= icone :caminhao %> <%= m[:veiculos].join(', ') %></span>
<% end %>
</button>
<% end %>
@@ -152,7 +152,7 @@
<button type="submit"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar
Adicionar <%= icone :confirmar, cor: nil %>
</button>
<% end %>
</div>
@@ -174,7 +174,7 @@
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center text-xl font-black
<%= m[:completo] ? 'bg-green-600 text-white' : 'bg-orange-500 text-black' %>">
<%= m[:completo] ? '✓' : m[:registro].motorista_nome.first.upcase %>
<%= m[:completo] ? icone(:confirmar, cor: nil, tamanho: 'w-6 h-6') : m[:registro].motorista_nome.first.upcase %>
</div>
<div>
<p class="text-white font-bold text-lg"><%= m[:registro].motorista_nome %></p>
@@ -184,13 +184,13 @@
<%= m[:classificadas] %>/<%= m[:total] %> classificadas
</span>
<% if m[:apontamentos].positive? %>
· <span class="text-orange-400"> <%= m[:apontamentos] %> apontamento<%= 's' if m[:apontamentos] > 1 %></span>
· <span class="text-orange-400"><%= icone :adicionar, cor: nil, tamanho: 'w-3 h-3' %> <%= m[:apontamentos] %> apontamento<%= 's' if m[:apontamentos] > 1 %></span>
<% end %>
</p>
<% if m[:veiculos_total] > 0 %>
<p class="text-xs mt-0.5">
<span class="<%= m[:veiculos_fechados] == m[:veiculos_total] ? 'text-green-400' : 'text-gray-400' %>">
🚗 <%= m[:veiculos_fechados] %>/<%= m[:veiculos_total] %> veículo<%= 's' if m[:veiculos_total] > 1 %> consolidado<%= 's' if m[:veiculos_fechados] != 1 %>
<%= icone :veiculo, cor: nil %> <%= m[:veiculos_fechados] %>/<%= m[:veiculos_total] %> veículo<%= 's' if m[:veiculos_total] > 1 %> consolidado<%= 's' if m[:veiculos_fechados] != 1 %>
</span>
<% if m[:veiculos_pendentes].any? %>
<span class="text-orange-400">· pendente<%= 's' if m[:veiculos_pendentes].size > 1 %>: <%= m[:veiculos_pendentes].join(', ') %></span>
@@ -201,7 +201,7 @@
</div>
<div class="text-right <%= 'pr-10' if pode_arquivar_motorista %>">
<p class="text-orange-500 font-bold"><%= moeda(m[:registro].valor_total) %></p>
<span class="text-gray-600 text-2xl">→</span>
<%= icone :avancar, cor: 'text-gray-600', tamanho: 'w-5 h-5' %>
</div>
</div>
<div class="mt-3">
@@ -214,13 +214,14 @@
<% end %>
<% if pode_arquivar_motorista %>
<%= button_to '🗄️',
arquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: m[:registro].id),
<%= button_to arquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: m[:registro].id),
method: :post,
form: { data: { turbo_confirm: "Arquivar #{m[:registro].motorista_nome} desta consolidação? Você poderá restaurar depois." },
class: 'absolute top-3 right-3' },
class: 'text-gray-500 hover:text-red-400 border border-[#2a2a2a] hover:border-red-500/50 rounded-lg w-9 h-9 flex items-center justify-center',
title: "Arquivar #{m[:registro].motorista_nome}" %>
title: "Arquivar #{m[:registro].motorista_nome}" do %>
<%= icone :arquivar, cor: nil, tamanho: 'w-4 h-4' %>
<% end %>
<% end %>
</div>
<% end %>
@@ -229,7 +230,7 @@
<%# Motoristas arquivados — restauráveis %>
<% if @arquivados.present? %>
<div class="mt-6">
<h2 class="text-gray-400 font-semibold text-sm mb-2">🗄️ Motoristas arquivados (<%= @arquivados.size %>)</h2>
<h2 class="text-gray-400 font-semibold text-sm mb-2 flex items-center gap-2"><%= icone :arquivar %> Motoristas arquivados (<%= @arquivados.size %>)</h2>
<div class="space-y-2">
<% @arquivados.each do |cm| %>
<div class="flex items-center justify-between bg-[#141414] border border-[#2a2a2a] border-dashed rounded-xl px-4 py-3">
@@ -239,17 +240,19 @@
</div>
<div class="flex items-center gap-2">
<% if pode_arquivar_motorista %>
<%= button_to '♻️ Restaurar',
desarquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
<%= button_to desarquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
method: :post,
class: 'text-green-400 hover:text-green-300 border border-[#2a2a2a] hover:border-green-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
class: 'text-green-400 hover:text-green-300 border border-[#2a2a2a] hover:border-green-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :restaurar, cor: nil, tamanho: 'w-3.5 h-3.5' %> Restaurar
<% end %>
<% end %>
<% if policy(@consolidacao).destroy? %>
<%= button_to '🗑️ Excluir',
excluir_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
<%= button_to excluir_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
method: :delete,
form: { data: { turbo_confirm: "Excluir DEFINITIVAMENTE #{cm.motorista_nome} desta consolidação? Os lançamentos dele serão apagados e isso não pode ser desfeito." } },
class: 'text-red-400 hover:text-red-300 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
class: 'text-red-400 hover:text-red-300 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px] inline-flex items-center gap-1.5' do %>
<%= icone :excluir, cor: nil, tamanho: 'w-3.5 h-3.5' %> Excluir
<% end %>
<% end %>
</div>
</div>
@@ -272,25 +275,28 @@
<% if @consolidacao.rascunho? %>
<div class="flex flex-col sm:flex-row gap-2">
<% if !tudo_completo && tem_consolidados && tem_pendentes %>
<%= button_to '🔒 Arquivar pendentes e finalizar',
finalizar_parcial_consolidacao_path(@consolidacao),
<%= button_to finalizar_parcial_consolidacao_path(@consolidacao),
method: :post,
data: { turbo_confirm: 'Arquivar TODOS os veículos ainda não classificados 100% e finalizar? Eles ficam de fora desta consolidação (você resolve depois). Os veículos consolidados são pagos normalmente.' },
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3.5 rounded-lg min-h-[48px] flex items-center justify-center text-center' %>
form: { data: { turbo_confirm: 'Arquivar TODOS os veículos ainda não classificados 100% e finalizar? Eles ficam de fora desta consolidação (você resolve depois). Os veículos consolidados são pagos normalmente.' } },
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3.5 rounded-lg min-h-[48px] flex items-center justify-center text-center gap-2' do %>
<%= icone :travado, cor: nil, tamanho: 'w-4 h-4' %> Arquivar pendentes e finalizar
<% end %>
<% end %>
<%= button_to 'Finalizar consolidação ✓', finalizar_consolidacao_path(@consolidacao),
<%= button_to finalizar_consolidacao_path(@consolidacao),
method: :post,
disabled: !tudo_completo,
data: { turbo_confirm: 'Finalizar? Após finalizada, a consolidação não poderá ser editada por operadores.' },
class: "font-bold px-6 py-3.5 rounded-lg min-h-[48px] transition-colors
#{tudo_completo ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" %>
form: { data: { turbo_confirm: 'Finalizar? Após finalizada, a consolidação não poderá ser editada por operadores.' } },
class: "font-bold px-6 py-3.5 rounded-lg min-h-[48px] transition-colors inline-flex items-center justify-center gap-2
#{tudo_completo ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" do %>
Finalizar consolidação <%= icone :confirmar, cor: nil %>
<% end %>
</div>
<% end %>
</div>
</div>
<% unless tudo_completo %>
<p class="text-gray-500 text-sm mt-2 text-center">
⚠️ Classifique todas as entregas de todos os motoristas para finalizar tudo<% if tem_consolidados && tem_pendentes %> — ou use <strong>“Arquivar pendentes e finalizar”</strong> para fechar só os veículos já consolidados.<% end %>
<%= icone :alerta, cor: 'text-amber-400' %> Classifique todas as entregas de todos os motoristas para finalizar tudo<% if tem_consolidados && tem_pendentes %> — ou use <strong>“Arquivar pendentes e finalizar”</strong> para fechar só os veículos já consolidados.<% end %>
</p>
<% end %>
</div>

View File

@@ -55,7 +55,7 @@
<%# Filtro de calendário — seleciona uma faixa de datas %>
<form id="dashboard-filtro-form" method="get" action="<%= dashboard_path %>" data-turbo="false" class="flex flex-wrap items-center gap-2">
<div class="relative">
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500">📅</span>
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2"><%= icone :calendario %></span>
<input type="text" id="periodo-range" readonly placeholder="Selecione o período"
value="<%= @periodo_inicio.strftime('%d/%m/%Y') %> até <%= @periodo_fim.strftime('%d/%m/%Y') %>"
class="cursor-pointer pl-4 pr-10 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl
@@ -79,14 +79,14 @@
<% if @operacoes.any? %>
<details class="relative" id="operacao-dropdown">
<summary class="cursor-pointer list-none pl-4 pr-4 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl text-white text-sm whitespace-nowrap hover:border-orange-500">
🗂️ <%= @operacao_filtro.present? ? "#{@operacao_filtro.size} operação(ões)" : 'Todas as operações' %>
<%= icone :pasta %> <%= @operacao_filtro.present? ? "#{@operacao_filtro.size} operação(ões)" : 'Todas as operações' %>
</summary>
<div class="absolute right-0 mt-2 z-30 w-72 max-h-80 overflow-y-auto bg-[#1a1a1a] border border-white/10 rounded-xl p-2 shadow-xl">
<%# Limpar seleção — recarrega o dashboard sem filtro de operação %>
<div class="flex items-center justify-between px-2 pb-2 mb-1 border-b border-white/10">
<span class="text-xs uppercase tracking-wider text-gray-400">Operações</span>
<% if @operacao_filtro.present? %>
<%= link_to '✕ Limpar', dashboard_path(inicio: @periodo_inicio.strftime('%Y-%m-%d'), fim: @periodo_fim.strftime('%Y-%m-%d')),
<%= link_to rotulo(:fechar, 'Limpar', cor: nil, tamanho: 'w-3 h-3'), dashboard_path(inicio: @periodo_inicio.strftime('%Y-%m-%d'), fim: @periodo_fim.strftime('%Y-%m-%d')),
data: { turbo: false }, class: 'text-xs text-orange-400 hover:text-orange-300 font-semibold' %>
<% end %>
</div>
@@ -107,7 +107,7 @@
<% end %>
<%# Imprimir relatório financeiro do período/operação atuais %>
<%= link_to '🖨️ Relatório',
<%= link_to rotulo(:imprimir, 'Relatório'),
dashboard_relatorio_financeiro_path(
inicio: @periodo_inicio.strftime('%Y-%m-%d'),
fim: @periodo_fim.strftime('%Y-%m-%d'),
@@ -124,7 +124,7 @@
<div class="sm:col-span-2 xl:col-span-1 bg-[#f97316] rounded-2xl p-6 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-orange-400/20 to-transparent"></div>
<div class="relative">
<p class="text-orange-100 text-sm font-medium mb-2">💰 Valor Estimado</p>
<p class="text-orange-100 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :dinheiro, cor: nil %> Valor Estimado</p>
<p class="text-4xl font-black text-white mb-1">
<%= moeda(@valor_estimado) %>
</p>
@@ -136,7 +136,7 @@
<%# Card destacado: total consolidado e pago (consolidações finalizadas do período) %>
<div class="sm:col-span-2 xl:col-span-1 bg-[#1a1a1a] rounded-2xl border border-green-500/30 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">💼 Consolidado / Pago</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :consolidado_pago %> Consolidado / Pago</p>
<p class="text-3xl font-black text-white mb-1"><%= moeda(@fin_custo_total) %></p>
<p class="text-gray-400 text-xs mb-2">consolidado no período</p>
<div class="flex items-center gap-2 text-sm border-t border-white/5 pt-2">
@@ -147,7 +147,7 @@
<%# Card 2: Total de entregas %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">📦 Total Entregas</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :consolidacoes %> Total Entregas</p>
<p class="text-4xl font-black text-white mb-1"><%= @total_entregas %></p>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
<span class="text-green-400"><%= @entregas_pagas %> entregues</span>
@@ -160,7 +160,7 @@
<%# Card 3: Consolidações %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">📋 Consolidações</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :planilha %> Consolidações</p>
<p class="text-4xl font-black text-white mb-1"><%= @consolidacoes_mes.count %></p>
<div class="flex items-center gap-2 text-sm">
<span class="text-yellow-500"><%= @consolidacoes_abertas %> abertas</span>
@@ -171,7 +171,7 @@
<%# Card 4: Motoristas ativos %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">🚗 Motoristas</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :veiculo %> Motoristas</p>
<p class="text-4xl font-black text-white mb-1"><%= @motoristas.count %></p>
<p class="text-gray-400 text-sm">com entregas no período</p>
</div>
@@ -180,22 +180,22 @@
<%# KPIs FINANCEIROS (consolidações finalizadas, já com filtro de operação) %>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">💰 Custo total</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :dinheiro %> Custo total</p>
<p class="text-3xl font-black text-white"><%= moeda(@fin_custo_total) %></p>
<p class="text-gray-400 text-xs mt-1"><%= @fin_qtd_entregas %> entregas classificadas</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">🎫 Ticket médio</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :ticket %> Ticket médio</p>
<p class="text-3xl font-black text-white"><%= moeda(@fin_ticket_medio) %></p>
<p class="text-gray-400 text-xs mt-1">por entrega</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">✅ Pago</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :sucesso, cor: 'text-green-500' %> Pago</p>
<p class="text-3xl font-black text-green-400"><%= moeda(@fin_pago) %></p>
<p class="text-gray-400 text-xs mt-1">já confirmado</p>
</div>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">🕗 A pagar</p>
<p class="text-gray-400 text-sm font-medium mb-2 flex items-center gap-2"><%= icone :relogio, cor: 'text-amber-400' %> A pagar</p>
<p class="text-3xl font-black text-yellow-500"><%= moeda(@fin_pendente) %></p>
<p class="text-gray-400 text-xs mt-1">pendente</p>
</div>
@@ -238,7 +238,7 @@
</div>
<% else %>
<div class="flex-1 min-h-48 flex flex-col items-center justify-center text-gray-400">
<div class="text-3xl mb-2">💸</div>
<div class="mb-2"><%= icone :custo, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div>
<p class="text-sm">Nenhum pagamento realizado neste período</p>
<% if @pag_pend_valor.positive? %>
<p class="text-xs mt-1">
@@ -252,7 +252,7 @@
<%# Top motoristas %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-4">🏆 Motoristas</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :ranking %> Motoristas</h2>
<% if @motoristas.any? %>
<div class="space-y-3">
@@ -282,7 +282,7 @@
</div>
<% else %>
<div class="text-center py-8 text-gray-400">
<div class="text-3xl mb-2">🚚</div>
<div class="mb-2"><%= icone :caminhao, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div>
<p class="text-sm">Nenhuma entrega concluída neste período</p>
</div>
<% end %>
@@ -294,34 +294,34 @@
<%# Custo por operação (barra) %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-1">🏷️ Custo por operação</h2>
<h2 class="text-lg font-semibold text-white mb-1 flex items-center gap-2"><%= icone :tag %> Custo por operação</h2>
<p class="text-gray-400 text-sm mb-4">Valor consolidado no período</p>
<% if @fin_por_operacao.any? %>
<div class="relative h-64"><canvas id="grafico-custo-operacao"></canvas></div>
<% else %>
<div class="text-center py-8 text-gray-400"><div class="text-3xl mb-2">🏷️</div><p class="text-sm">Sem dados no período</p></div>
<div class="text-center py-8 text-gray-400"><div class="mb-2"><%= icone :tag, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div><p class="text-sm">Sem dados no período</p></div>
<% end %>
</div>
<%# Composição por tipo (rosca) %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-1">🧩 Composição do custo</h2>
<h2 class="text-lg font-semibold text-white mb-1 flex items-center gap-2"><%= icone :composicao %> Composição do custo</h2>
<p class="text-gray-400 text-sm mb-4">Por tipo de lançamento</p>
<% if @fin_por_tipo.any? %>
<div class="relative h-64"><canvas id="grafico-composicao-tipo"></canvas></div>
<% else %>
<div class="text-center py-8 text-gray-400"><div class="text-3xl mb-2">🧩</div><p class="text-sm">Sem dados no período</p></div>
<div class="text-center py-8 text-gray-400"><div class="mb-2"><%= icone :composicao, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div><p class="text-sm">Sem dados no período</p></div>
<% end %>
</div>
<%# Custo por motorista (barra) %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-1">👤 Custo por motorista</h2>
<h2 class="text-lg font-semibold text-white mb-1 flex items-center gap-2"><%= icone :pessoa %> Custo por motorista</h2>
<p class="text-gray-400 text-sm mb-4">Por motorista no período</p>
<% if @fin_por_motorista.any? %>
<div class="relative h-64"><canvas id="grafico-custo-motorista"></canvas></div>
<% else %>
<div class="text-center py-8 text-gray-400"><div class="text-3xl mb-2">👤</div><p class="text-sm">Sem dados no período</p></div>
<div class="text-center py-8 text-gray-400"><div class="mb-2"><%= icone :pessoa, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div><p class="text-sm">Sem dados no período</p></div>
<% end %>
</div>
</div>
@@ -331,12 +331,12 @@
<%# Rosca pago vs a pagar (por valor) %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-1">💵 Pagamentos</h2>
<h2 class="text-lg font-semibold text-white mb-1 flex items-center gap-2"><%= icone :pagamento %> Pagamentos</h2>
<p class="text-gray-400 text-sm mb-4">Pagos por data de pagamento · pendentes no período</p>
<% if (@pag_pago_qtd + @pag_pend_qtd).zero? %>
<div class="text-center py-8 text-gray-400">
<div class="text-3xl mb-2">🧾</div>
<div class="mb-2"><%= icone :nota_fiscal, tamanho: 'w-8 h-8', cor: 'text-gray-600' %></div>
<p class="text-sm">Nenhum pagamento ou pendência no período</p>
</div>
<% else %>
@@ -365,7 +365,7 @@
<%# Pagamentos realizados no período (filtrados por pago_em) %>
<div>
<h2 class="text-lg font-semibold text-white mb-4">✅ Pagamentos realizados no período</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :sucesso, cor: 'text-green-500' %> Pagamentos realizados no período</h2>
<% if @pagamentos_feitos.any? %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
@@ -405,7 +405,7 @@
<%# Pendentes (consolidações finalizadas do período sem pagamento) %>
<div>
<h2 class="text-lg font-semibold text-white mb-4">🕗 Pagamentos pendentes</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :relogio, cor: 'text-amber-400' %> Pagamentos pendentes</h2>
<% if @pagamentos_pendentes.any? %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
@@ -435,7 +435,7 @@
<div class="flex gap-1" id="pagamentos-pendentes-abas"></div>
</div>
<% else %>
<p class="text-gray-400 text-sm py-3">Nenhuma pendência no período. 🎉</p>
<p class="text-gray-400 text-sm py-3">Nenhuma pendência no período. <%= icone :sucesso, cor: 'text-green-500' %></p>
<% end %>
</div>
</div>
@@ -444,7 +444,7 @@
<%# Por operação %>
<% if @por_operacao.any? %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<h2 class="text-lg font-semibold text-white mb-4">🏥 Entregas por Local</h2>
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"><%= icone :operacao %> Entregas por Local</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
<% total_op = @por_operacao.values.sum.to_f %>
<% @por_operacao.each do |local, qtd| %>

View File

@@ -6,7 +6,7 @@
<div class="text-center mb-8">
<div class="w-16 h-16 bg-[#f97316] rounded-2xl flex items-center justify-center mx-auto mb-3">
<span class="text-3xl">🔒</span>
<%= icone :travado, tamanho: 'w-8 h-8' %>
</div>
<h1 class="text-white font-bold text-2xl">Definir nova senha</h1>
</div>

View File

@@ -6,7 +6,7 @@
<div class="text-center mb-8">
<div class="w-16 h-16 bg-[#f97316] rounded-2xl flex items-center justify-center mx-auto mb-3">
<span class="text-3xl">🔑</span>
<%= icone :chave, tamanho: 'w-8 h-8' %>
</div>
<h1 class="text-white font-bold text-2xl">Recuperar senha</h1>
<p class="text-gray-400 text-sm mt-1">Informe seu e-mail para receber as instruções</p>
@@ -38,7 +38,7 @@
</div>
<p class="text-gray-600 text-xs text-center mt-6">
⚠️ O envio de e-mail depende da configuração SMTP no servidor.<br>
<%= icone :alerta, cor: 'text-amber-400' %> O envio de e-mail depende da configuração SMTP no servidor.<br>
Sem SMTP, peça ao administrador para redefinir sua senha.
</p>

View File

@@ -25,12 +25,12 @@
<button id="tab-email"
onclick="switchTab('email')"
class="flex-1 py-4 text-sm font-medium transition-colors tab-active text-[#f97316] border-b-2 border-[#f97316]">
📧 E-mail
<%= icone :email, cor: nil %> E-mail
</button>
<button id="tab-pin"
onclick="switchTab('pin')"
class="flex-1 py-4 text-sm font-medium transition-colors text-gray-400 hover:text-white border-b-2 border-transparent">
🔑 PIN Motorista
<%= icone :chave, cor: nil %> PIN Motorista
</button>
</div>
@@ -114,7 +114,7 @@
class="h-14 rounded-xl bg-[#0a0a0a] border border-white/10 text-gray-400
text-sm hover:bg-red-900/40 hover:border-red-500/50 hover:text-red-400
active:scale-95 transition-all">
<%= icone :fechar, cor: nil, tamanho: 'w-4 h-4' %>
</button>
<button type="button"
onclick="pinPress('0')"

View File

@@ -36,31 +36,31 @@
<%# Dashboard — admin/gerente/operador/externo (todos menos motorista) %>
<% unless current_user.motorista? %>
<%= nav_link_to '📊 Dashboard', dashboard_path %>
<%= nav_link_to '📈 Operações', operacoes_dashboard_path %>
<%= nav_link_to 'Dashboard', dashboard_path, icon: :dashboard %>
<%= nav_link_to 'Operações', operacoes_dashboard_path, icon: :operacoes %>
<% end %>
<%# Consolidações — só quem pode consolidar (exclui o externo) %>
<% if current_user.pode_consolidar? %>
<%= nav_link_to '📦 Consolidações', consolidacoes_path %>
<%= nav_link_to '📁 Arquivadas', arquivadas_consolidacoes_path %>
<%= nav_link_to 'Consolidações', consolidacoes_path, icon: :consolidacoes %>
<%= nav_link_to 'Arquivadas', arquivadas_consolidacoes_path, icon: :arquivadas %>
<% end %>
<%# Motorista %>
<% if current_user.motorista? %>
<%= nav_link_to '🏠 Meu Painel', motorista_dashboard_path %>
<%= nav_link_to 'Meu Painel', motorista_dashboard_path, icon: :painel %>
<% end %>
<%# Admin/Gerente — Configurações %>
<% if current_user.pode_ver_config? %>
<div class="pt-4 mt-4 border-t border-[#2a2a2a]">
<p class="px-3 text-xs text-gray-600 uppercase tracking-wider mb-2">Administração</p>
<%= nav_link_to '👥 Usuários', admin_usuarios_path %>
<%= nav_link_to '⚙️ Configurações', admin_configuracoes_path %>
<%= nav_link_to '🔍 Auditoria', admin_auditoria_logs_path %>
<%= nav_link_to '📥 Planilha SimpliRoute', admin_planilha_simpli_route_path %>
<%= nav_link_to 'Usuários', admin_usuarios_path, icon: :usuarios %>
<%= nav_link_to 'Configurações', admin_configuracoes_path, icon: :configuracoes %>
<%= nav_link_to 'Auditoria', admin_auditoria_logs_path, icon: :auditoria %>
<%= nav_link_to 'Planilha SimpliRoute', admin_planilha_simpli_route_path, icon: :planilha %>
<% if current_user.admin? %>
<%= nav_link_to '✏️ Editar Lançamento', admin_edicao_lancamento_path %>
<%= nav_link_to 'Editar Lançamento', admin_edicao_lancamento_path, icon: :editar_lancamento %>
<% end %>
</div>
<% end %>
@@ -70,7 +70,7 @@
<div class="px-6 py-4 border-t border-[#2a2a2a]">
<%= link_to destroy_user_session_path, data: { turbo_method: :delete, turbo_confirm: 'Deseja sair?' },
class: "flex items-center gap-2 text-gray-400 hover:text-red-400 text-sm transition-colors" do %>
<span>🚪</span> Sair
<%= icone :sair, cor: nil, tamanho: 'w-4 h-4' %> Sair
<% end %>
</div>
</aside>
@@ -86,15 +86,19 @@
group-hover:border group-hover:border-orange-500/70
focus:w-7 focus:h-16 focus:bg-[#1a1a1a] focus:text-orange-500"
title="Esconder/mostrar menu" aria-label="Esconder ou mostrar o menu">
<span id="sidebar-toggle-icon" class="text-lg leading-none">◀</span>
<%# Gira 180° quando a sidebar está recolhida (ver syncSidebarToggleIcon) %>
<span id="sidebar-toggle-icon" class="leading-none transition-transform duration-200">
<%= icone :recolher, cor: nil, tamanho: 'w-4 h-4' %>
</span>
</button>
</div>
<%# Topbar mobile %>
<header class="md:hidden fixed top-0 left-0 right-0 z-30 bg-[#111111] border-b border-[#2a2a2a] px-4 py-3 flex items-center justify-between">
<button onclick="toggleSidebar()"
class="text-orange-500 text-2xl min-h-[48px] min-w-[48px] flex items-center justify-center">
class="text-orange-500 min-h-[48px] min-w-[48px] flex items-center justify-center"
aria-label="Abrir menu">
<%= icone :menu, cor: nil, tamanho: 'w-7 h-7' %>
</button>
<div class="flex items-center gap-2">
<%= image_tag '/logo-reem.png', alt: 'Reem Transporte', class: 'h-8 w-auto object-contain' %>
@@ -121,8 +125,8 @@
}
function syncSidebarToggleIcon() {
const icon = document.getElementById('sidebar-toggle-icon');
if (icon) icon.textContent =
document.documentElement.classList.contains('sidebar-collapsed') ? '▶' : '◀';
if (icon) icon.classList.toggle('rotate-180',
document.documentElement.classList.contains('sidebar-collapsed'));
}
syncSidebarToggleIcon();
</script>

View File

@@ -18,11 +18,11 @@
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto scrollbar-hidden">
<%# Dashboard (todos exceto motorista) %>
<%= nav_link_to 'Dashboard', dashboard_path, icon: '📊' %>
<%= nav_link_to 'Dashboard', dashboard_path, icon: :dashboard %>
<%# Consolidações (admin, gerente, operador) %>
<% if current_user.pode_criar_consolidacao? %>
<%= nav_link_to 'Consolidações', consolidacoes_path, icon: '📋' %>
<%= nav_link_to 'Consolidações', consolidacoes_path, icon: :consolidacoes %>
<% end %>
<%# Separador Admin %>
@@ -31,8 +31,8 @@
<p class="px-4 text-xs font-semibold text-gray-600 uppercase tracking-wider">Administração</p>
</div>
<%= 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 @@
<p class="px-4 text-xs font-semibold text-gray-600 uppercase tracking-wider">Relatórios</p>
</div>
<div class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-gray-600 cursor-not-allowed">
<span class="text-lg w-5 text-center">📄</span>
<%= icone :documento, cor: nil, tamanho: 'w-5 h-5' %>
<span>Relatórios PDF</span>
<span class="ml-auto text-[10px] bg-gray-800 text-gray-500 px-1.5 py-0.5 rounded">Em breve</span>
</div>

View File

@@ -100,7 +100,7 @@
</video>
<button type="button" onclick="event.stopPropagation(); encerrarSplash()"
class="absolute bottom-6 right-6 text-gray-400 hover:text-white text-sm border border-white/20 rounded-lg px-4 py-2">
Pular
Pular <%= icone :fechar, cor: nil, tamanho: 'w-3.5 h-3.5' %>
</button>
</div>
<script>
@@ -130,7 +130,7 @@
<div id="flash-notice"
class="fixed top-4 right-4 z-50 bg-orange-500 text-black font-semibold px-5 py-3 rounded-lg shadow-lg flex items-center gap-2 animate-fade-in"
data-controller="flash">
<span>✅</span> <%= notice %>
<%= icone :sucesso, cor: nil, tamanho: 'w-5 h-5' %> <%= notice %>
</div>
<% end %>
@@ -138,7 +138,7 @@
<div id="flash-alert"
class="fixed top-4 right-4 z-50 bg-red-600 text-white font-semibold px-5 py-3 rounded-lg shadow-lg flex items-center gap-2"
data-controller="flash">
<span>⚠️</span> <%= alert %>
<%= icone :alerta, cor: nil, tamanho: 'w-5 h-5' %> <%= alert %>
</div>
<% end %>

View File

@@ -3,13 +3,13 @@
<div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold text-white mb-1">Olá, <%= current_user.nome.split.first %>! 👋</h1>
<h1 class="text-3xl font-bold text-white mb-1">Olá, <%= current_user.nome.split.first %>!</h1>
<p class="text-gray-300 text-lg mb-6">Aqui estão seus valores de entregas.</p>
<%# ── Aviso de pagamento recém-confirmado ───────────────── %>
<% if @pagamentos_recentes.any? %>
<div class="bg-green-600/15 border border-green-600/40 rounded-2xl p-5 mb-6">
<p class="text-green-400 font-bold uppercase tracking-wide mb-3">✅ Pagamento confirmado!</p>
<p class="text-green-400 font-bold uppercase tracking-wide mb-3 flex items-center gap-2"><%= icone :sucesso, cor: nil %> Pagamento confirmado!</p>
<div class="space-y-3">
<% @pagamentos_recentes.each do |cm| %>
<div class="flex flex-col gap-0.5">
@@ -18,9 +18,9 @@
pela consolidação <span class="font-semibold"><%= cm.consolidacao.nome %></span>
</p>
<p class="text-gray-300 text-sm">
💰 <%= l cm.pago_em.to_date, format: :short %>
<%= icone :dinheiro %> <%= l cm.pago_em.to_date, format: :short %>
<% if cm.forma_pagamento.present? %>· <%= cm.forma_pagamento.humanize %><% end %>
<% if cm.nota_fiscal.present? %>· 🧾 NF <%= cm.nota_fiscal %><% end %>
<% if cm.nota_fiscal.present? %>· <%= icone :nota_fiscal %> NF <%= cm.nota_fiscal %><% end %>
</p>
</div>
<% end %>
@@ -33,20 +33,20 @@
<%# Card 1 — Valor Estimado (LARANJA) %>
<div class="bg-orange-500 rounded-2xl p-6">
<p class="text-black/80 font-bold uppercase tracking-wide">💰 Valor estimado deste mês</p>
<p class="text-black/70 text-sm mt-1">📅 <%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %></p>
<p class="text-black/80 font-bold uppercase tracking-wide flex items-center gap-2"><%= icone :dinheiro, cor: nil %> Valor estimado deste mês</p>
<p class="text-black/70 text-sm mt-1"><%= icone :calendario, cor: nil %> <%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %></p>
<p class="text-black font-black text-4xl sm:text-5xl mt-2 leading-tight whitespace-nowrap"><%= moeda(@valor_estimado) %></p>
<p class="text-black/80 text-base mt-2">
<%= @entregas_mes %> entregas feitas e confirmadas
</p>
<p class="text-black/60 text-sm mt-3">
⚠️ Valor aproximado. O valor certo sai quando a empresa fechar o pagamento.
<%= icone :alerta, cor: nil %> Valor aproximado. O valor certo sai quando a empresa fechar o pagamento.
</p>
</div>
<%# Card 2 — Valor Consolidado (BRANCO) %>
<div class="bg-white rounded-2xl p-6">
<p class="text-gray-600 font-bold uppercase tracking-wide">✅ Valor fechado para pagamento</p>
<p class="text-gray-600 font-bold uppercase tracking-wide flex items-center gap-2"><%= icone :sucesso, cor: 'text-green-600' %> Valor fechado para pagamento</p>
<p class="text-black font-black text-4xl sm:text-5xl mt-2 leading-tight whitespace-nowrap"><%= moeda(@valor_consolidado) %></p>
<p class="text-gray-600 text-base mt-2">
Soma dos pagamentos já confirmados pela empresa
@@ -55,7 +55,7 @@
</div>
<%# ── Lista de consolidações ──────────────────────────── %>
<h2 class="text-white font-bold text-xl mb-3">📋 Meus pagamentos</h2>
<h2 class="text-white font-bold text-xl mb-3 flex items-center gap-2"><%= icone :planilha %> Meus pagamentos</h2>
<% if @minhas_consolidacoes.any? %>
<div class="space-y-3">
@@ -72,27 +72,27 @@
<% end %>
</div>
<p class="text-gray-300 text-base mt-1">
📅 <%= l c.data_inicio, format: :short %> a <%= l c.data_fim, format: :short %>
<%= icone :calendario %> <%= l c.data_inicio, format: :short %> a <%= l c.data_fim, format: :short %>
</p>
<p class="text-orange-500 font-black text-3xl mt-1 whitespace-nowrap"><%= moeda(cm&.valor_total || 0) %></p>
<% if cm&.pago? %>
<p class="text-green-400 text-sm mt-1">
💰 Pago em <%= l cm.pago_em.to_date, format: :short %>
<%= icone :dinheiro, cor: 'text-green-400' %> Pago em <%= l cm.pago_em.to_date, format: :short %>
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
</p>
<% if cm.nota_fiscal.present? %>
<p class="text-gray-300 text-sm mt-0.5">🧾 Nota Fiscal: <%= cm.nota_fiscal %></p>
<p class="text-gray-300 text-sm mt-0.5"><%= icone :nota_fiscal %> Nota Fiscal: <%= cm.nota_fiscal %></p>
<% end %>
<% end %>
</div>
<% if c.finalizada? %>
<%= link_to '⬇️ Baixar meu extrato',
<%= link_to rotulo(:baixar, 'Baixar meu extrato', cor: nil),
gerar_pdf_extrato_consolidacao_path(c, motorista: current_user.nome),
data: { turbo: false },
class: 'w-full md:w-auto bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %>
<% else %>
<span class="w-full md:w-auto text-gray-300 text-base px-5 py-4 border border-[#2a2a2a] rounded-xl text-center">
Aguardando fechamento
<%= icone :parcial, cor: 'text-amber-400' %> Aguardando fechamento
</span>
<% end %>
</div>
@@ -101,7 +101,7 @@
</div>
<% else %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-10 text-center">
<p class="text-5xl mb-3">📭</p>
<p class="mb-3"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600' %></p>
<p class="text-gray-200 text-lg">Você ainda não aparece em nenhum pagamento.</p>
<p class="text-gray-400 text-base mt-1">Quando a empresa fechar um período com suas entregas, ele aparece aqui.</p>
</div>

View File

@@ -43,13 +43,13 @@
</button>
<button type="submit" id="btn-entrar" disabled
class="bg-orange-500 disabled:bg-[#2a2a2a] disabled:text-gray-600 text-black font-bold text-xl rounded-xl py-5 min-h-[64px] transition-colors">
<%= icone :confirmar, cor: nil, tamanho: 'w-6 h-6' %>
</button>
</div>
<% end %>
<p class="text-gray-300 text-base text-center mt-6">
💡 Você também pode escanear o QR Code do seu extrato para entrar direto.
<%= icone :dica %> Você também pode escanear o QR Code do seu extrato para entrar direto.
</p>
<p class="text-center mt-4">
<%= link_to 'Sou administrador/gerente →', new_user_session_path, class: 'text-orange-500 hover:text-orange-400 text-sm' %>

View File

@@ -18,7 +18,7 @@
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 class="text-lg font-semibold text-white">📋 Planilha da operação</h2>
<h2 class="text-lg font-semibold text-white flex items-center gap-2"><%= icone :planilha %> Planilha da operação</h2>
<p class="text-gray-400 text-sm">Espelho por NF — pesquise por nota, motorista, unidade, ocorrência, veículo...</p>
</div>
@@ -27,7 +27,7 @@
<input type="hidden" name="<%= k %>" value="<%= v %>">
<% end %>
<div class="relative">
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">🔎</span>
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2"><%= icone :buscar, cor: 'text-gray-500' %></span>
<input type="search" name="q" value="<%= @busca %>" placeholder="Pesquisar..."
class="pl-9 pr-3 py-2 bg-[#0a0a0a] border border-white/10 rounded-xl text-white text-sm
w-52 sm:w-72 focus:outline-none focus:border-orange-500 placeholder-gray-500">
@@ -37,7 +37,7 @@
Buscar
</button>
<% if @busca.present? %>
<%= link_to '✕ Limpar', operacoes_planilha_path(request.query_parameters.except('q', 'pg')),
<%= link_to rotulo(:fechar, 'Limpar', cor: nil, tamanho: 'w-3 h-3'), operacoes_planilha_path(request.query_parameters.except('q', 'pg')),
data: { turbo: false }, class: 'text-xs text-orange-400 hover:text-orange-300 whitespace-nowrap' %>
<% end %>
</form>

View File

@@ -12,12 +12,12 @@
</div>
<div class="map-toggle inline-flex rounded-lg overflow-hidden border border-white/10 text-xs">
<button type="button" data-view="heat"
class="px-3 py-1.5 bg-orange-500 text-black font-semibold">🔥 Calor</button>
class="px-3 py-1.5 bg-orange-500 text-black font-semibold"><%= icone :calor, cor: nil, tamanho: 'w-3.5 h-3.5' %> Calor</button>
<button type="button" data-view="pontos"
class="px-3 py-1.5 text-gray-300 hover:text-white">📍 Pontos (<%= pontos.size %>)</button>
class="px-3 py-1.5 text-gray-300 hover:text-white"><%= icone :local, cor: nil, tamanho: 'w-3.5 h-3.5' %> Pontos (<%= pontos.size %>)</button>
</div>
<button type="button" class="map-fullscreen px-3 py-1.5 rounded-lg border border-white/10 text-gray-300 hover:text-white text-xs" title="Ver em tela cheia">
Tela cheia
<%= icone :tela_cheia, cor: nil, tamanho: 'w-3.5 h-3.5' %> Tela cheia
</button>
</div>
</div>
@@ -30,7 +30,7 @@
data-pontos="<%= pontos.to_json %>"
data-centro="<%= metricas.centro_mapa.to_json %>"></div>
<p class="text-xs text-gray-600 mt-2">
🔥 Calor mostra a densidade · 📍 Pontos mostra cada entrega (clique no marcador para detalhes; coordenada de check-out).
<%= icone :calor %> Calor mostra a densidade · <%= icone :local %> Pontos mostra cada entrega (clique no marcador para detalhes; coordenada de check-out).
</p>
<% end %>
</div>

View File

@@ -25,7 +25,7 @@
<p class="text-gray-400 text-sm font-medium mb-1">Total de Entregas</p>
<p class="text-6xl font-black text-white leading-none inline-block origin-left transition-transform duration-200 group-hover:scale-110"><%= metricas.total %></p>
<p class="text-xs text-gray-500 mt-2 h-0 overflow-hidden opacity-0 transition-all duration-200 group-hover:h-4 group-hover:opacity-100">
✅ <%= metricas.sucesso %> · <span class="text-blue-500">●</span> <%= metricas.recusas %> · ⏳ <%= metricas.pendentes %>
<%= icone :sucesso, cor: 'text-green-500', tamanho: 'w-3.5 h-3.5' %> <%= metricas.sucesso %> · <%= icone :ponto, cor: 'text-blue-500', tamanho: 'w-3.5 h-3.5' %> <%= metricas.recusas %> · <%= icone :parcial, cor: 'text-amber-400', tamanho: 'w-3.5 h-3.5' %> <%= metricas.pendentes %>
</p>
</div>
<div class="group bg-[#1a1a1a] rounded-2xl border border-white/5 p-7 transition-all duration-200 hover:border-green-500/40 hover:bg-[#202020] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/40">

View File

@@ -30,7 +30,7 @@
data: { turbo: false },
title: (ativo ? 'Clique para remover o filtro' : "Filtrar por #{cel}"),
class: "inline-flex items-center gap-1 hover:text-orange-400 #{'text-orange-400 font-semibold' if ativo}" do %>
<%= cel %><% if ativo %> <span aria-hidden="true">✕</span><% end %>
<%= cel %><% if ativo %> <%= icone :fechar, cor: nil, tamanho: 'w-3 h-3' %><% end %>
<% end %>
<% else %>
<%= cel %>

View File

@@ -52,9 +52,9 @@
<h1 class="text-2xl font-bold text-white">Dashboard de Operações</h1>
<p class="text-gray-400 text-sm mt-0.5">
<% if @modo == 'global' %>
🌐 Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> <%= @periodo_fim.strftime('%d/%m/%Y') %>
<%= icone :global %> Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> <%= @periodo_fim.strftime('%d/%m/%Y') %>
<% elsif @modo == 'comparar' %>
⚖️ Comparação entre operações (cada uma no seu próprio período)
<%= icone :comparar %> Comparação entre operações (cada uma no seu próprio período)
<% elsif @metricas&.data_inicio %>
<%= @metricas.operacoes_label %> · <%= @metricas.data_inicio.strftime('%d/%m/%Y') %> <%= @metricas.data_fim.strftime('%d/%m/%Y') %>
<% elsif @operacao %>
@@ -69,7 +69,7 @@
<% if @modo == 'global' %>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500">📅</span>
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2"><%= icone :calendario %></span>
<input type="text" id="periodo-range" readonly placeholder="Selecione o período"
value="<%= @periodo_inicio.strftime('%d/%m/%Y') %> até <%= @periodo_fim.strftime('%d/%m/%Y') %>"
class="cursor-pointer pl-4 pr-10 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl
@@ -89,12 +89,12 @@
</div>
<%# ── Abas de modo + atalho para a planilha ─────────────── %>
<% modos = { 'operacao' => '🏥 Operação', 'global' => '🌐 Global', 'comparar' => '⚖️ Comparar' } %>
<% modos = { 'operacao' => [:operacao, 'Operação'], 'global' => [:global, 'Global'], 'comparar' => [:comparar, 'Comparar'] } %>
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-white/5 pb-3">
<div class="flex flex-wrap items-center gap-2">
<% modos.each do |m, label| %>
<% modos.each do |m, (ic, label)| %>
<% ativo = @modo == m %>
<%= link_to label,
<%= link_to rotulo(ic, label, cor: (ativo ? nil : 'text-brand-laranja')),
operacoes_dashboard_path(ctx_ops.merge(modo: m, inicio: ini_iso, fim: fim_iso)),
data: { turbo: false },
class: "px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500'}" %>
@@ -106,7 +106,7 @@
.slice('f_driver', 'f_sts', 'f_status', 'f_obs', 'f_resultado', 'f_data')
.merge(modo: (@modo == 'global' ? 'global' : 'operacao'), inicio: ini_iso, fim: fim_iso) %>
<% planilha_ctx[:operacao] = @operacao if @operacao %>
<%= link_to '📋 Planilha da operação', operacoes_planilha_path(planilha_ctx),
<%= link_to rotulo(:planilha, 'Planilha da operação', cor: nil), operacoes_planilha_path(planilha_ctx),
data: { turbo: false },
class: 'px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap bg-[#1a1a1a]
text-orange-400 border border-orange-500/40 hover:bg-orange-500 hover:text-black' %>
@@ -125,7 +125,7 @@
<% @chips.each do |chip| %>
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-orange-500/15 border border-orange-500/30 text-orange-300 text-xs">
<%= chip[:rotulo] %>: <strong class="text-white"><%= chip[:display] || chip[:valor] %></strong>
<%= link_to '✕', operacoes_dashboard_path(request.query_parameters.except(chip[:param])),
<%= link_to icone(:fechar, cor: nil, tamanho: 'w-3 h-3'), operacoes_dashboard_path(request.query_parameters.except(chip[:param])),
data: { turbo: false }, class: 'hover:text-white font-bold', title: 'Remover filtro' %>
</span>
<% end %>
@@ -160,7 +160,7 @@
<% elsif @modo == 'global' %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-4 text-sm text-gray-300">
🌐 Visão <strong class="text-white">Global</strong> — todas as
<%= icone :global %> Visão <strong class="text-white">Global</strong> — todas as
<%= @tabelas_validas.size %> operações agregadas no período.
</div>
<% if @metricas %>
@@ -208,7 +208,7 @@
<%# Detalhe completo de cada operação (recolhível) %>
<details class="bg-[#1a1a1a] rounded-2xl border border-white/5">
<summary class="cursor-pointer px-6 py-4 text-sm text-gray-300 hover:text-white select-none">
🔎 Ver detalhe completo de cada operação
<%= icone :buscar %> Ver detalhe completo de cada operação
</summary>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 p-4 pt-0">
<div>
@@ -442,6 +442,12 @@
});
}
// Ícone do sprite (public/icons.svg) — mesmos SVG usados no resto do sistema.
function ico(id, cor) {
return '<svg width="12" height="12" fill="' + (cor || '#f97316') + '" style="vertical-align:-1px">' +
'<use href="/icons.svg#' + id + '"></use></svg>';
}
function popupHtml(p) {
const linha = (icone, val) => val ? ('<div>' + icone + ' ' + escapeHtml(val) + '</div>') : '';
const foto = p.foto
@@ -449,22 +455,22 @@
'<img src="' + escapeHtml(p.foto) + '" alt="Fachada" loading="lazy" ' +
'onerror="this.style.display=\'none\';this.parentNode.nextElementSibling.style.display=\'block\'" ' +
'style="margin-top:8px;width:100%;max-height:160px;object-fit:cover;border-radius:6px;display:block"></a>' +
'<div style="display:none;margin-top:6px;color:#9ca3af;font-size:11px">📷 Foto indisponível (link expirado/sem acesso)</div>'
: '<div style="margin-top:6px;color:#6b7280;font-size:11px">📷 Sem foto da fachada para esta entrega</div>';
'<div style="display:none;margin-top:6px;color:#9ca3af;font-size:11px">' + ico('camera-fill', '#9ca3af') + ' Foto indisponível (link expirado/sem acesso)</div>'
: '<div style="margin-top:6px;color:#6b7280;font-size:11px">' + ico('camera-fill', '#6b7280') + ' Sem foto da fachada para esta entrega</div>';
const statusLinha = p.sucesso
? '<div style="color:#16a34a;font-size:11px;margin-bottom:6px"> Entrega realizada</div>'
: '<div style="color:#3b82f6;font-size:11px;margin-bottom:6px"> Insucesso' +
? '<div style="color:#16a34a;font-size:11px;margin-bottom:6px">' + ico('check-lg', '#16a34a') + ' Entrega realizada</div>'
: '<div style="color:#3b82f6;font-size:11px;margin-bottom:6px">' + ico('x-lg', '#3b82f6') + ' Insucesso' +
(p.motivo ? ' — ' + escapeHtml(p.motivo) : '') + '</div>';
return '' +
'<div style="min-width:210px;max-width:240px">' +
'<div style="font-weight:700;color:#f97316">NF ' + escapeHtml(p.nf) + '</div>' +
'<div style="font-weight:600;margin-bottom:4px">' + escapeHtml(p.nome) + '</div>' +
statusLinha +
linha('📍', p.endereco) +
linha('🏥', p.unidade) +
linha('🚚', p.motorista) +
linha('🚗', p.veiculo) +
linha('🕒', p.checkout) +
linha(ico('geo-alt-fill'), p.endereco) +
linha(ico('hospital-fill'), p.unidade) +
linha(ico('truck'), p.motorista) +
linha(ico('car-front-fill'), p.veiculo) +
linha(ico('clock-fill'), p.checkout) +
foto +
'</div>';
}
@@ -513,7 +519,9 @@
const mapa = L.map(el, { layers: [escuro] }).setView(centro, 11);
L.control.layers(
{ '🌙 Escuro': escuro, '🗺️ Claro': claro, '🛰️ Satélite': satelite },
{ [ico('moon-fill') + ' Escuro']: escuro,
[ico('map-fill') + ' Claro']: claro,
[ico('broadcast-pin') + ' Satélite']: satelite },
{}, { position: 'topright', collapsed: true }
).addTo(mapa);

View File

@@ -15,10 +15,10 @@
<%# ── Cabeçalho ─────────────────────────────────────────── %>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-white">📋 Planilha da Operação</h1>
<h1 class="text-2xl font-bold text-white flex items-center gap-2"><%= icone :planilha %> Planilha da Operação</h1>
<p class="text-gray-400 text-sm mt-0.5">
<% if @modo == 'global' %>
🌐 Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> <%= @periodo_fim.strftime('%d/%m/%Y') %>
<%= icone :global %> Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> <%= @periodo_fim.strftime('%d/%m/%Y') %>
<% elsif @metricas&.data_inicio %>
<%= @metricas.operacoes_label %> · <%= @metricas.data_inicio.strftime('%d/%m/%Y') %> <%= @metricas.data_fim.strftime('%d/%m/%Y') %>
<% elsif @operacao %>
@@ -27,7 +27,7 @@
</p>
</div>
<%= link_to 'Dashboard de Operações',
<%= link_to rotulo(:voltar, 'Dashboard de Operações', cor: nil),
operacoes_dashboard_path({ modo: @modo, operacao: @operacao, inicio: ini_iso, fim: fim_iso }.compact),
data: { turbo: false },
class: 'px-4 py-2.5 rounded-xl text-sm whitespace-nowrap bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500' %>
@@ -35,10 +35,10 @@
<%# ── Seleção de escopo: operação única ou global ───────── %>
<div class="flex flex-wrap items-center gap-2">
<% pills = { 'operacao' => '🏥 Operação', 'global' => '🌐 Global' } %>
<% pills.each do |m, label| %>
<% pills = { 'operacao' => [:operacao, 'Operação'], 'global' => [:global, 'Global'] } %>
<% pills.each do |m, (ic, label)| %>
<% ativo = @modo == m %>
<%= link_to label,
<%= link_to rotulo(ic, label, cor: (ativo ? nil : 'text-brand-laranja')),
operacoes_planilha_path({ modo: m, operacao: @operacao, inicio: ini_iso, fim: fim_iso, q: @busca.presence }.compact),
data: { turbo: false },
class: "px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-white/10 hover:border-orange-500'}" %>
@@ -64,7 +64,7 @@
<%# Planilha Entregas preenchida (modelo entregue ao cliente) %>
<% if @operacao %>
<%= link_to '⬇️ Baixar Excel preenchido', operacoes_planilha_baixar_path(operacao: @operacao),
<%= link_to rotulo(:baixar, 'Baixar Excel preenchido', cor: nil), operacoes_planilha_baixar_path(operacao: @operacao),
data: { turbo: false },
title: 'Planilha Entregas da operação com status/ocorrências preenchidos pelo rastreio',
class: 'px-4 py-2.5 bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl text-sm whitespace-nowrap' %>
@@ -79,7 +79,7 @@
<% @chips.each do |chip| %>
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-orange-500/15 border border-orange-500/30 text-orange-300 text-xs">
<%= chip[:rotulo] %>: <strong class="text-white"><%= chip[:display] || chip[:valor] %></strong>
<%= link_to '✕', operacoes_planilha_path(request.query_parameters.except(chip[:param], 'pg')),
<%= link_to icone(:fechar, cor: nil, tamanho: 'w-3 h-3'), operacoes_planilha_path(request.query_parameters.except(chip[:param], 'pg')),
data: { turbo: false }, class: 'hover:text-white font-bold', title: 'Remover filtro' %>
</span>
<% end %>

View File

@@ -103,7 +103,7 @@
<% if @usuarios.empty? %>
<tr>
<td colspan="5" class="px-6 py-16 text-center text-gray-500">
<div class="text-4xl mb-3">👥</div>
<div class="mb-3"><%= icone :usuarios, tamanho: 'w-10 h-10', cor: 'text-gray-600' %></div>
<p>Nenhum usuário encontrado.</p>
</td>
</tr>