Files
Reem-Notas/app/views/dashboard/index.html.erb

845 lines
37 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%# app/views/dashboard/index.html.erb %>
<%# Fase 3: Dashboard com cards, gráfico Chart.js e cálculo ao vivo %>
<%# Não cachear no Turbo: o snapshot congelava o gráfico/calendário (DOM já
<%# alterado pelo JS, sem reexecutar scripts) e só voltava com F5. %>
<% content_for :head do %>
<meta name="turbo-cache-control" content="no-cache">
<%# Flatpickr — CSS no <head> (evita flash sem estilo) + tema escuro oficial %>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/themes/dark.css">
<style>
/* Ajustes do calendário ao tema Reem (preto/laranja) */
.flatpickr-calendar { background: #1a1a1a; border: 1px solid rgba(255,255,255,.1); box-shadow: 0 10px 30px rgba(0,0,0,.5); }
.flatpickr-months, .flatpickr-weekdays, .flatpickr-weekday { background: #1a1a1a; color: #9ca3af; }
.flatpickr-current-month, .flatpickr-monthDropdown-months, .numInput { color: #fff; }
.flatpickr-day { color: #e5e7eb; }
.flatpickr-day:hover { background: #2a2a2a; border-color: #2a2a2a; }
.flatpickr-day.today { border-color: #f97316; }
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected:hover,
.flatpickr-day.startRange:hover,
.flatpickr-day.endRange:hover {
background: #f97316; border-color: #f97316; color: #0a0a0a;
}
.flatpickr-day.inRange { background: rgba(249,115,22,.18); border-color: rgba(249,115,22,.18); box-shadow: -5px 0 0 rgba(249,115,22,.18), 5px 0 0 rgba(249,115,22,.18); }
.flatpickr-day.flatpickr-disabled, .flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: #4b5563; }
.flatpickr-months .flatpickr-prev-month svg, .flatpickr-months .flatpickr-next-month svg { fill: #f97316; }
</style>
<% end %>
<%# Quick-ranges no formato YYYY-MM-DD para os atalhos do filtro %>
<% hoje = Date.current %>
<% atalhos = {
'Hoje' => [hoje, hoje],
'7 dias' => [hoje - 6, hoje],
'Este mês' => [hoje.beginning_of_month, hoje],
'30 dias' => [hoje - 29, hoje]
} %>
<div class="space-y-6">
<%# Header + filtro por faixa de datas %>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-white">Dashboard</h1>
<p class="text-gray-400 text-sm mt-0.5">
<%= @periodo_inicio.strftime('%d/%m/%Y') %> <%= @periodo_fim.strftime('%d/%m/%Y') %>
· Atualizado às <%= Time.now.strftime('%H:%M') %>
</p>
</div>
<%# 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>
<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
text-white text-sm w-full sm:w-[240px] focus:outline-none focus:border-orange-500 placeholder-gray-500">
</div>
<input type="hidden" name="inicio" id="inicio-hidden" value="<%= @periodo_inicio.strftime('%Y-%m-%d') %>">
<input type="hidden" name="fim" id="fim-hidden" value="<%= @periodo_fim.strftime('%Y-%m-%d') %>">
<%# Atalhos rápidos de período %>
<div class="flex flex-wrap items-center gap-1">
<% atalhos.each do |label, (ini, fim)| %>
<% ativo = @periodo_inicio == ini && @periodo_fim == fim %>
<%= link_to label,
dashboard_path(inicio: ini.strftime('%Y-%m-%d'), fim: fim.strftime('%Y-%m-%d'), operacoes: @operacao_filtro),
data: { turbo: false },
class: "px-3 py-2.5 rounded-xl text-sm whitespace-nowrap border #{ativo ? 'bg-orange-500 text-black border-orange-500 font-bold' : 'bg-[#1a1a1a] text-gray-300 border-white/10 hover:border-orange-500'}" %>
<% end %>
</div>
<%# Filtro de operação — multi-seleção; submete o form ao mudar %>
<% 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' %>
</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')),
data: { turbo: false }, class: 'text-xs text-orange-400 hover:text-orange-300 font-semibold' %>
<% end %>
</div>
<% @operacoes_agrupadas.each do |titulo, ops| %>
<p class="px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wider text-gray-400"><%= titulo %></p>
<% ops.each do |op| %>
<label class="flex items-center gap-2 text-white text-sm hover:bg-[#0a0a0a] rounded px-2 py-2 cursor-pointer">
<input type="checkbox" name="operacoes[]" value="<%= op[:tabela] %>"
<%= 'checked' if @operacao_filtro.include?(op[:tabela]) %>
class="op-filtro rounded text-orange-500 bg-[#0a0a0a] border-white/10">
<%= op[:label] %>
</label>
<% end %>
<% end %>
</div>
</details>
<% end %>
<%# Imprimir relatório financeiro do período/operação atuais %>
<%= link_to '🖨️ Relatório',
dashboard_relatorio_financeiro_path(
inicio: @periodo_inicio.strftime('%Y-%m-%d'),
fim: @periodo_fim.strftime('%Y-%m-%d'),
operacoes: @operacao_filtro),
data: { turbo: false },
class: 'pl-4 pr-4 py-2.5 bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl text-sm whitespace-nowrap' %>
</form>
</div>
<%# CARDS GRANDES — KPIs %>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
<%# Card 1: Valor estimado %>
<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-4xl font-black text-white mb-1">
<%= moeda(@valor_estimado) %>
</p>
<p class="text-orange-200 text-sm">
<%= @entregas_pagas %> entregas concluídas
</p>
</div>
</div>
<%# 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-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">
<span class="text-green-400 font-semibold"><%= moeda(@fin_pago) %></span>
<span class="text-gray-400">pago</span>
</div>
</div>
<%# 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-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>
<span class="text-gray-400">·</span>
<span class="text-yellow-500"><%= @entregas_pendentes %> pendentes</span>
<span class="text-gray-400">·</span>
<span class="text-red-400"><%= @entregas_falhadas %> falhadas</span>
</div>
</div>
<%# 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-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>
<span class="text-gray-400">·</span>
<span class="text-green-400"><%= @consolidacoes_fechadas %> fechadas</span>
</div>
</div>
<%# 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-4xl font-black text-white mb-1"><%= @motoristas.count %></p>
<p class="text-gray-400 text-sm">com entregas no período</p>
</div>
</div>
<%# 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-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-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-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-3xl font-black text-yellow-500"><%= moeda(@fin_pendente) %></p>
<p class="text-gray-400 text-xs mt-1">pendente</p>
</div>
</div>
<%# GRÁFICO + MOTORISTAS (lado a lado no desktop) %>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
<%# Gráficos diários (Chart.js) — flex-col p/ preencher a altura do card de motoristas %>
<div class="xl:col-span-2 bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 flex flex-col">
<div class="flex flex-wrap items-center justify-between gap-2 mb-6">
<div>
<h2 class="text-lg font-semibold text-white">Evolução do custo</h2>
<p class="text-gray-400 text-sm">Valor estimado por dia no período<%= ' · operação filtrada' if @operacao_filtro.present? %></p>
</div>
<div class="flex items-center gap-2 text-xs text-gray-400">
<span class="inline-block w-3 h-0.5 bg-[#f97316] rounded"></span> Valor (R$)
</div>
</div>
<div class="relative flex-1 min-h-56">
<canvas id="grafico-diario"></canvas>
</div>
<%# Pagamentos por dia (fluxo de caixa) — quando o dinheiro saiu de fato %>
<div class="flex flex-wrap items-center justify-between gap-2 mt-6 mb-4 pt-6 border-t border-white/5">
<div>
<h2 class="text-lg font-semibold text-white">Pagamentos por dia</h2>
<p class="text-gray-400 text-sm">Valor pago aos motoristas por data de pagamento</p>
</div>
<% if @pag_pago_qtd.positive? %>
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-400">
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-2 bg-green-500 rounded-sm"></span> Pago no dia</span>
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-0.5 bg-gray-300 rounded"></span> Acumulado</span>
</div>
<% end %>
</div>
<% if @pag_pago_qtd.positive? %>
<div class="relative flex-1 min-h-48">
<canvas id="grafico-pagamentos-dia"></canvas>
</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>
<p class="text-sm">Nenhum pagamento realizado neste período</p>
<% if @pag_pend_valor.positive? %>
<p class="text-xs mt-1">
<span class="text-yellow-500 font-semibold"><%= moeda(@pag_pend_valor) %></span>
aguardando pagamento (<%= @pag_pend_qtd %> motorista(s))
</p>
<% end %>
</div>
<% end %>
</div>
<%# 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>
<% if @motoristas.any? %>
<div class="space-y-3">
<% @motoristas.each_with_index do |m, i| %>
<div class="flex items-center gap-3">
<span class="text-xs font-bold w-5 text-center
<%= i == 0 ? 'text-yellow-400' : i == 1 ? 'text-gray-300' : i == 2 ? 'text-orange-600' : 'text-gray-400' %>">
<%= i + 1 %>
</span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1">
<span class="text-white text-sm font-medium truncate"><%= m[:nome] %></span>
<span class="text-[#f97316] text-sm font-semibold ml-2 flex-shrink-0">
<%= moeda(m[:valor]) %>
</span>
</div>
<%# Barra de progresso %>
<div class="w-full bg-white/5 rounded-full h-1.5">
<% max = @motoristas.first[:entregas].to_f %>
<div class="h-1.5 rounded-full bg-[#f97316]"
style="width: <%= [(m[:entregas] / max * 100).round, 100].min %>%"></div>
</div>
<span class="text-gray-400 text-xs"><%= m[:entregas] %> entregas</span>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-8 text-gray-400">
<div class="text-3xl mb-2">🚚</div>
<p class="text-sm">Nenhuma entrega concluída neste período</p>
</div>
<% end %>
</div>
</div>
<%# ── FINANCEIRO POR OPERAÇÃO ───────────────────────────── %>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
<%# 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>
<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>
<% 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>
<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>
<% 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>
<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>
<% end %>
</div>
</div>
<%# ── PAGAMENTOS (pago x pendente) ──────────────────────── %>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
<%# 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>
<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>
<p class="text-sm">Nenhum pagamento ou pendência no período</p>
</div>
<% else %>
<div class="relative h-44 mb-4">
<canvas id="grafico-pagamentos"></canvas>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-green-400">● Pago no período</span>
<span class="text-white font-semibold">
<%= moeda(@pag_pago_valor) %> · <%= @pag_pago_qtd %> pagamento(s)
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-400">● A pagar</span>
<span class="text-white font-semibold">
<%= moeda(@pag_pend_valor) %> · <%= @pag_pend_qtd %> motorista(s)
</span>
</div>
</div>
<% end %>
</div>
<%# Tabelas: pagamentos realizados (com data) + pendentes %>
<div class="xl:col-span-2 bg-[#1a1a1a] rounded-2xl border border-white/5 p-6 space-y-6">
<%# 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>
<% if @pagamentos_feitos.any? %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-gray-400 text-left border-b border-white/5">
<th class="py-2 pr-3 font-medium">Pago em</th>
<th class="py-2 px-3 font-medium">Motorista</th>
<th class="py-2 px-3 font-medium">Consolidação</th>
<th class="py-2 px-3 font-medium">Forma</th>
<th class="py-2 pl-3 font-medium text-right">Valor</th>
</tr>
</thead>
<tbody id="pagamentos-feitos-tbody">
<% @pagamentos_feitos.each do |cm| %>
<tr class="border-b border-white/5 pagamento-feito-row">
<td class="py-2.5 pr-3 text-gray-300 whitespace-nowrap"><%= l cm.pago_em.to_date, format: :short %></td>
<td class="py-2.5 px-3 text-white"><%= cm.motorista_nome %></td>
<td class="py-2.5 px-3">
<%= link_to cm.consolidacao.nome, consolidacao_path(cm.consolidacao),
class: 'text-gray-300 hover:text-orange-500' %>
</td>
<td class="py-2.5 px-3 text-gray-400"><%= cm.forma_pagamento&.humanize || '—' %></td>
<td class="py-2.5 pl-3 text-right text-green-400 font-semibold"><%= moeda(cm.valor_total) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div id="pagamentos-feitos-paginacao" class="flex items-center justify-between mt-3 hidden">
<p class="text-gray-400 text-xs" id="pagamentos-feitos-info"></p>
<div class="flex gap-1" id="pagamentos-feitos-abas"></div>
</div>
<% else %>
<p class="text-gray-400 text-sm py-3">Nenhum pagamento realizado neste período.</p>
<% end %>
</div>
<%# Pendentes (consolidações finalizadas do período sem pagamento) %>
<div>
<h2 class="text-lg font-semibold text-white mb-4">🕗 Pagamentos pendentes</h2>
<% if @pagamentos_pendentes.any? %>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-gray-400 text-left border-b border-white/5">
<th class="py-2 pr-3 font-medium">Motorista</th>
<th class="py-2 px-3 font-medium">Consolidação</th>
<th class="py-2 pl-3 font-medium text-right">Valor</th>
</tr>
</thead>
<tbody id="pagamentos-pendentes-tbody">
<% @pagamentos_pendentes.each do |cm| %>
<tr class="border-b border-white/5 pagamento-pendente-row">
<td class="py-2.5 pr-3 text-white"><%= cm.motorista_nome %></td>
<td class="py-2.5 px-3">
<%= link_to cm.consolidacao.nome, consolidacao_path(cm.consolidacao),
class: 'text-gray-300 hover:text-orange-500' %>
</td>
<td class="py-2.5 pl-3 text-right text-gray-400 font-semibold"><%= moeda(cm.valor_total) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div id="pagamentos-pendentes-paginacao" class="flex items-center justify-between mt-3 hidden">
<p class="text-gray-400 text-xs" id="pagamentos-pendentes-info"></p>
<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>
<% end %>
</div>
</div>
</div>
<%# 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>
<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| %>
<div class="bg-[#0a0a0a] rounded-xl p-4 border border-white/5 text-center">
<p class="text-2xl font-bold text-[#f97316]"><%= qtd %></p>
<p class="text-gray-400 text-xs mt-1 leading-tight"><%= local.to_s.truncate(20) %></p>
<p class="text-gray-400 text-xs mt-1"><%= ((qtd / total_op) * 100).round %>%</p>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<%# Flatpickr — date range picker (JS via CDN; CSS já carregado no <head>) %>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/pt.js"></script>
<script>
(function () {
function initPeriodo() {
const input = document.getElementById('periodo-range');
if (!input || typeof flatpickr === 'undefined') return;
if (input._flatpickr) return; // já inicializado nesta página
const inicio = document.getElementById('inicio-hidden').value;
const fim = document.getElementById('fim-hidden').value;
// formata Date local → YYYY-MM-DD (evita deslocamento de fuso do toISOString)
const toISO = (d) =>
d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
// Os valores vêm em ISO (YYYY-MM-DD), mas o dateFormat exibido é d/m/Y.
// Convertendo para Date local antes evita o flatpickr interpretar errado
// (era a causa da caixa mostrar uma data solta em vez do período).
const parseISO = (s) => {
const [y, m, d] = s.split('-').map(Number);
return new Date(y, m - 1, d);
};
flatpickr(input, {
mode: 'range',
locale: Object.assign({}, flatpickr.l10ns.pt, { rangeSeparator: ' até ' }),
dateFormat: 'd/m/Y',
defaultDate: [parseISO(inicio), parseISO(fim)],
maxDate: 'today',
onClose: function (selectedDates) {
if (selectedDates.length === 0) return;
// 1 dia selecionado → início e fim iguais; 2 dias → faixa completa.
const ini = selectedDates[0];
const f = selectedDates[selectedDates.length - 1];
document.getElementById('inicio-hidden').value = toISO(ini);
document.getElementById('fim-hidden').value = toISO(f);
document.getElementById('dashboard-filtro-form').requestSubmit();
}
});
}
// Executa no carregamento inicial e também em navegação Turbo (sem F5).
initPeriodo();
document.addEventListener('turbo:load', initPeriodo);
})();
</script>
<%# Paginação client-side das tabelas de pagamentos (10 por página) %>
<script>
(function () {
function paginarTabela(prefixo, classeLinha, rotulo) {
const tbody = document.getElementById(prefixo + '-tbody');
const nav = document.getElementById(prefixo + '-paginacao');
if (!tbody || !nav || nav.dataset.iniciado) return;
nav.dataset.iniciado = '1';
const porPagina = 10;
const linhas = Array.from(tbody.querySelectorAll('.' + classeLinha));
const paginas = Math.ceil(linhas.length / porPagina);
if (paginas <= 1) return;
const abas = document.getElementById(prefixo + '-abas');
const info = document.getElementById(prefixo + '-info');
nav.classList.remove('hidden');
function mostrarPagina(p) {
linhas.forEach(function (tr, i) {
tr.style.display = (i >= (p - 1) * porPagina && i < p * porPagina) ? '' : 'none';
});
const de = (p - 1) * porPagina + 1;
const ate = Math.min(p * porPagina, linhas.length);
info.textContent = de + '' + ate + ' de ' + linhas.length + ' ' + rotulo;
abas.querySelectorAll('button').forEach(function (b) {
const ativa = Number(b.dataset.pagina) === p;
b.className = 'px-3 py-1.5 rounded-lg text-xs font-semibold transition-colors ' +
(ativa ? 'bg-[#f97316] text-white'
: 'bg-[#0a0a0a] text-gray-400 border border-white/5 hover:text-white');
});
}
for (let p = 1; p <= paginas; p++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = p;
btn.dataset.pagina = p;
btn.addEventListener('click', function () { mostrarPagina(p); });
abas.appendChild(btn);
}
mostrarPagina(1);
}
function initPaginacaoPagamentos() {
paginarTabela('pagamentos-feitos', 'pagamento-feito-row', 'pagamentos');
paginarTabela('pagamentos-pendentes', 'pagamento-pendente-row', 'pendências');
}
initPaginacaoPagamentos();
document.addEventListener('turbo:load', initPaginacaoPagamentos);
})();
</script>
<%# Filtro de operação — submete o form ao marcar/desmarcar %>
<script>
(function () {
document.querySelectorAll('.op-filtro').forEach(function (c) {
c.addEventListener('change', function () {
document.getElementById('dashboard-filtro-form').requestSubmit();
});
});
})();
</script>
<%# Chart.js via CDN %>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
// IIFE: evita "Identifier already declared" quando o Turbo re-executa o script
// ao navegar (o que antes fazia o gráfico não aparecer).
(function () {
const ctx = document.getElementById('grafico-diario');
if (!ctx || typeof Chart === 'undefined') return;
// Destrói gráfico anterior preso ao mesmo canvas (evita "Canvas is already
// in use" quando o Turbo reexecuta o script ao navegar).
Chart.getChart(ctx)?.destroy();
const labels = <%= raw @grafico_diario[:labels].to_json %>;
const valores = <%= raw @grafico_diario[:valores].to_json %>;
const qtds = <%= raw @grafico_diario[:qtds].to_json %>;
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Valor (R$)',
data: valores,
borderColor: '#f97316',
backgroundColor: 'rgba(249, 115, 22, 0.08)',
borderWidth: 2.5,
pointBackgroundColor: '#f97316',
pointBorderColor: '#1a1a1a',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0a0a0a',
borderColor: '#f97316',
borderWidth: 1,
titleColor: '#9ca3af',
bodyColor: '#ffffff',
padding: 12,
callbacks: {
label: (ctx) => {
const idx = ctx.dataIndex;
return [
` Valor: R$ ${ctx.parsed.y.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
` Entregas: ${qtds[idx]}`
];
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.04)', drawBorder: false },
ticks: { color: '#6b7280', font: { size: 11 }, maxTicksLimit: 10 }
},
y: {
grid: { color: 'rgba(255,255,255,0.04)', drawBorder: false },
ticks: {
color: '#6b7280',
font: { size: 11 },
callback: (v) => `R$ ${v.toFixed(0)}`
}
}
}
}
});
})();
</script>
<%# Fluxo de pagamentos: barras (pago no dia) + linha (acumulado no período) %>
<script>
(function () {
const ctx = document.getElementById('grafico-pagamentos-dia');
if (!ctx || typeof Chart === 'undefined') return;
Chart.getChart(ctx)?.destroy();
const labels = <%= raw @grafico_pagamentos_dia[:labels].to_json %>;
const valores = <%= raw @grafico_pagamentos_dia[:valores].to_json %>;
const acumulado = <%= raw @grafico_pagamentos_dia[:acumulado].to_json %>;
const brl = (v) => `R$ ${v.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Pago no dia',
data: valores,
backgroundColor: 'rgba(34, 197, 94, 0.65)',
hoverBackgroundColor: '#22c55e',
borderRadius: 4,
borderSkipped: false,
maxBarThickness: 28,
order: 2
},
{
label: 'Acumulado',
data: acumulado,
type: 'line',
borderColor: '#d1d5db',
borderWidth: 2,
borderDash: [5, 4],
pointRadius: 0,
pointHoverRadius: 4,
tension: 0.3,
order: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0a0a0a',
borderColor: '#22c55e',
borderWidth: 1,
titleColor: '#9ca3af',
bodyColor: '#ffffff',
padding: 12,
callbacks: {
label: (c) => ` ${c.dataset.label}: ${brl(c.parsed.y)}`
}
}
},
scales: {
x: {
grid: { display: false, drawBorder: false },
ticks: { color: '#6b7280', font: { size: 11 }, maxTicksLimit: 10 }
},
y: {
beginAtZero: true,
grid: { color: 'rgba(255,255,255,0.04)', drawBorder: false },
ticks: { color: '#6b7280', font: { size: 11 }, callback: (v) => `R$ ${v.toFixed(0)}` }
}
}
}
});
})();
</script>
<%# Rosca de pagamentos (pago x a pagar, por valor) %>
<script>
(function () {
const ctx = document.getElementById('grafico-pagamentos');
if (!ctx || typeof Chart === 'undefined') return;
Chart.getChart(ctx)?.destroy();
const pago = <%= @pag_pago_valor.to_f %>;
const aPagar = <%= @pag_pend_valor.to_f %>;
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Pago', 'A pagar'],
datasets: [{
data: [pago, aPagar],
backgroundColor: ['#22c55e', '#374151'],
borderColor: '#1a1a1a',
borderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '62%',
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0a0a0a',
borderColor: '#22c55e',
borderWidth: 1,
titleColor: '#9ca3af',
bodyColor: '#ffffff',
padding: 12,
callbacks: {
label: (c) => ` ${c.label}: R$ ${c.parsed.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
}
}
}
});
})();
</script>
<%# ── Gráficos financeiros por operação ─────────────────────── %>
<script>
(function () {
const moedaTick = (v) => 'R$ ' + Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 0 });
const moedaFull = (v) => 'R$ ' + Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const tooltipBase = {
backgroundColor: '#0a0a0a', borderColor: '#f97316', borderWidth: 1,
titleColor: '#9ca3af', bodyColor: '#ffffff', padding: 12
};
const eixoCinza = { grid: { color: 'rgba(255,255,255,0.04)', drawBorder: false }, ticks: { color: '#6b7280', font: { size: 11 } } };
function barraHorizontal(id, labels, valores, cor) {
const ctx = document.getElementById(id);
if (!ctx || typeof Chart === 'undefined') return;
Chart.getChart(ctx)?.destroy();
new Chart(ctx, {
type: 'bar',
data: { labels: labels, datasets: [{ data: valores, backgroundColor: cor, borderRadius: 4 }] },
options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: Object.assign({}, tooltipBase, { callbacks: { label: (c) => ' ' + moedaFull(c.parsed.x) } })
},
scales: {
x: Object.assign({}, eixoCinza, { ticks: Object.assign({}, eixoCinza.ticks, { callback: moedaTick }) }),
y: eixoCinza
}
}
});
}
// 1) Custo por operação
barraHorizontal('grafico-custo-operacao',
<%= raw @fin_por_operacao.map { |l, _| l }.to_json %>,
<%= raw @fin_por_operacao.map { |_, v| v.to_f }.to_json %>,
'#f97316');
// 2) Custo por motorista
barraHorizontal('grafico-custo-motorista',
<%= raw @fin_por_motorista.map { |n, _| n }.to_json %>,
<%= raw @fin_por_motorista.map { |_, v| v.to_f }.to_json %>,
'#9a3412');
// 3) Composição por tipo (rosca)
(function () {
const ctx = document.getElementById('grafico-composicao-tipo');
if (!ctx || typeof Chart === 'undefined') return;
Chart.getChart(ctx)?.destroy();
const labels = <%= raw @fin_por_tipo.map { |h| h[:label] }.to_json %>;
const valores = <%= raw @fin_por_tipo.map { |h| h[:valor] }.to_json %>;
const cores = ['#f97316', '#9a3412', '#fed7aa', '#374151', '#9333ea'];
new Chart(ctx, {
type: 'doughnut',
data: { labels: labels, datasets: [{ data: valores, backgroundColor: cores, borderColor: '#1a1a1a', borderWidth: 3 }] },
options: {
responsive: true, maintainAspectRatio: false, cutout: '60%',
plugins: {
legend: { position: 'bottom', labels: { color: '#9ca3af', font: { size: 11 }, padding: 12 } },
tooltip: Object.assign({}, tooltipBase, { callbacks: { label: (c) => ' ' + c.label + ': ' + moedaFull(c.parsed) } })
}
}
});
})();
})();
</script>