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