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

274 lines
11 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>
<%# 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>