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

425 lines
17 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">
<% end %>
<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 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-[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') %>">
</form>
</div>
<%# CARDS GRANDES — KPIs %>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 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 pagas
</p>
</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 items-center gap-2 text-sm">
<span class="text-green-400"><%= @entregas_pagas %> pagas</span>
<span class="text-gray-600">·</span>
<span class="text-yellow-500"><%= @entregas_pendentes %> pendentes</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-600">·</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-500 text-sm">com entregas no período</p>
</div>
</div>
<%# GRÁFICO + MOTORISTAS (lado a lado no desktop) %>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
<%# Gráfico de linha diário (Chart.js) %>
<div class="xl:col-span-2 bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-lg font-semibold text-white">Evolução Diária</h2>
<p class="text-gray-400 text-sm">Valor estimado por dia no período</p>
</div>
<div class="flex items-center gap-2 text-xs text-gray-500">
<span class="inline-block w-3 h-0.5 bg-[#f97316] rounded"></span> Valor (R$)
</div>
</div>
<div class="relative h-64">
<canvas id="grafico-diario"></canvas>
</div>
</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">🏆 Top 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-600' %>">
<%= 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].split.first %></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-500 text-xs"><%= m[:entregas] %> entregas</span>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-8 text-gray-500">
<div class="text-3xl mb-2">🚚</div>
<p class="text-sm">Nenhuma entrega paga neste 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-500">
<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-500 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>
<% @pagamentos_feitos.each do |cm| %>
<tr class="border-b border-white/5">
<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>
<% else %>
<p class="text-gray-500 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-500 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>
<% @pagamentos_pendentes.each do |cm| %>
<tr class="border-b border-white/5">
<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>
<% else %>
<p class="text-gray-500 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-600 text-xs mt-1"><%= ((qtd / total_op) * 100).round %>%</p>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<%# Flatpickr — date range picker (CDN, mesmo padrão do Chart.js) %>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
<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 () {
const input = document.getElementById('periodo-range');
if (!input || typeof flatpickr === 'undefined') return;
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');
flatpickr(input, {
mode: 'range',
locale: 'pt',
dateFormat: 'd/m/Y',
defaultDate: [inicio, fim],
maxDate: 'today',
onClose: function (selectedDates) {
if (selectedDates.length === 2) {
document.getElementById('inicio-hidden').value = toISO(selectedDates[0]);
document.getElementById('fim-hidden').value = toISO(selectedDates[1]);
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.toFixed(2).replace('.', ',')}`,
` 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>
<%# 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.toFixed(2).replace('.', ',')}`
}
}
}
}
});
})();
</script>