- Backend: Fastify + Prisma/SQLite com autenticacao JWT - CRUD completo: usuarios, categorias, produtos, comandas - Sistema de comandas com itens, status, pagamento - Controle de estoque (entrada/saida automatica) - Relatorios e dashboard com KPIs - Notificacoes sonoras na cozinha - Frontend: SPA vanilla TS com tema dark glassmorphism - Mobile-first com bottom-sheet e cards compactos - Busca de comandas por periodo - Modal de detalhes com secao editavel colapsavel
201 lines
9.4 KiB
TypeScript
201 lines
9.4 KiB
TypeScript
import { api } from '../api';
|
|
import { renderNavbar, toast, formatBRL } from '../utils';
|
|
|
|
export async function renderDashboard(container: HTMLElement) {
|
|
container.innerHTML = `
|
|
${renderNavbar('/dashboard')}
|
|
<div class="container animate-fade-in">
|
|
<div class="page-header">
|
|
<h1>Dashboard & Relatórios 📊</h1>
|
|
<button class="btn btn-secondary btn-sm" id="btn-refresh-dashboard">🔄 Atualizar</button>
|
|
</div>
|
|
|
|
<!-- KPI Cards -->
|
|
<div id="kpi-section" class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
|
<div class="card" style="text-align:center;">
|
|
<p class="card-subtitle" style="margin-bottom:0.5rem;">Vendas Hoje</p>
|
|
<p style="font-size:1.75rem;font-weight:700;color:var(--success);" id="kpi-vendas-hoje">R$ 0,00</p>
|
|
<p class="card-subtitle" id="kpi-comandas-hoje">0 comandas</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;">
|
|
<p class="card-subtitle" style="margin-bottom:0.5rem;">Vendas Semana</p>
|
|
<p style="font-size:1.75rem;font-weight:700;color:var(--accent-primary);" id="kpi-vendas-semana">R$ 0,00</p>
|
|
<p class="card-subtitle" id="kpi-comandas-semana">0 comandas</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;">
|
|
<p class="card-subtitle" style="margin-bottom:0.5rem;">Comandas Abertas</p>
|
|
<p style="font-size:1.75rem;font-weight:700;color:var(--warning);" id="kpi-comandas-abertas">0</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;">
|
|
<p class="card-subtitle" style="margin-bottom:0.5rem;">Estoque Baixo</p>
|
|
<p style="font-size:1.75rem;font-weight:700;color:var(--accent-primary);" id="kpi-estoque-baixo">0</p>
|
|
<p class="card-subtitle" id="kpi-total-produtos">0 produtos ativos</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Relatório de Vendas -->
|
|
<div class="glass-panel" style="margin-bottom:2rem;">
|
|
<div class="page-header" style="margin-bottom:1rem;">
|
|
<h3 style="margin:0;">Relatório de Vendas</h3>
|
|
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
|
<label style="color:var(--text-secondary);font-size:0.85rem;">Período:</label>
|
|
<input type="date" id="rel-inicio" class="form-control" style="width:auto;">
|
|
<span style="color:var(--text-secondary);">até</span>
|
|
<input type="date" id="rel-fim" class="form-control" style="width:auto;">
|
|
<button class="btn btn-sm" id="btn-gerar-relatorio">Gerar</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="relatorio-resumo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:1.5rem;">
|
|
</div>
|
|
|
|
<div id="relatorio-top-produtos" style="margin-bottom:1.5rem;"></div>
|
|
|
|
<div id="relatorio-vendas-dia" style="margin-bottom:1.5rem;"></div>
|
|
|
|
<div id="relatorio-pagamento"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set default dates
|
|
const hoje = new Date().toISOString().split('T')[0];
|
|
const inicioMes = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
|
|
(document.getElementById('rel-inicio') as HTMLInputElement).value = inicioMes;
|
|
(document.getElementById('rel-fim') as HTMLInputElement).value = hoje;
|
|
|
|
const loadDashboard = async () => {
|
|
try {
|
|
const data = await api.get('/relatorios/dashboard');
|
|
document.getElementById('kpi-vendas-hoje')!.textContent = formatBRL(data.vendasHoje.total);
|
|
document.getElementById('kpi-comandas-hoje')!.textContent = `${data.vendasHoje.comandas} comandas`;
|
|
document.getElementById('kpi-vendas-semana')!.textContent = formatBRL(data.vendasSemana.total);
|
|
document.getElementById('kpi-comandas-semana')!.textContent = `${data.vendasSemana.comandas} comandas`;
|
|
document.getElementById('kpi-comandas-abertas')!.textContent = `${data.comandasAbertas}`;
|
|
document.getElementById('kpi-estoque-baixo')!.textContent = `${data.produtosEstoqueBaixo}`;
|
|
document.getElementById('kpi-total-produtos')!.textContent = `${data.totalProdutos} produtos ativos`;
|
|
} catch (err: any) {
|
|
toast(err.message, 'error');
|
|
}
|
|
};
|
|
|
|
const loadRelatorio = async () => {
|
|
const inicio = (document.getElementById('rel-inicio') as HTMLInputElement).value;
|
|
const fim = (document.getElementById('rel-fim') as HTMLInputElement).value;
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (inicio) params.set('inicio', inicio);
|
|
if (fim) params.set('fim', fim);
|
|
const data = await api.get(`/relatorios/vendas?${params.toString()}`);
|
|
|
|
// Resumo
|
|
document.getElementById('relatorio-resumo')!.innerHTML = `
|
|
<div class="card" style="text-align:center;padding:1rem;">
|
|
<p class="card-subtitle" style="margin-bottom:0.25rem;">Total Vendido</p>
|
|
<p style="font-size:1.25rem;font-weight:700;color:var(--success);">${formatBRL(data.resumo.totalVendas)}</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1rem;">
|
|
<p class="card-subtitle" style="margin-bottom:0.25rem;">Comandas</p>
|
|
<p style="font-size:1.25rem;font-weight:700;">${data.resumo.totalComandas}</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1rem;">
|
|
<p class="card-subtitle" style="margin-bottom:0.25rem;">Ticket Médio</p>
|
|
<p style="font-size:1.25rem;font-weight:700;color:var(--accent-primary);">${formatBRL(data.resumo.ticketMedio)}</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1rem;">
|
|
<p class="card-subtitle" style="margin-bottom:0.25rem;">Descontos</p>
|
|
<p style="font-size:1.25rem;font-weight:700;color:var(--warning);">${formatBRL(data.resumo.totalDescontos)}</p>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1rem;">
|
|
<p class="card-subtitle" style="margin-bottom:0.25rem;">Acréscimos</p>
|
|
<p style="font-size:1.25rem;font-weight:700;color:var(--accent-secondary);">${formatBRL(data.resumo.totalAcrescimos)}</p>
|
|
</div>
|
|
`;
|
|
|
|
// Top produtos
|
|
if (data.topProdutos.length > 0) {
|
|
document.getElementById('relatorio-top-produtos')!.innerHTML = `
|
|
<h4 style="margin-bottom:0.75rem;">🏆 Mais Vendidos</h4>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>#</th><th>Produto</th><th style="text-align:center;">Qtd</th><th style="text-align:right;">Total</th></tr></thead>
|
|
<tbody>
|
|
${data.topProdutos.map((p: any, i: number) => `
|
|
<tr>
|
|
<td>${i + 1}º</td>
|
|
<td><strong>${p.nome}</strong></td>
|
|
<td style="text-align:center;">${p.quantidade}</td>
|
|
<td style="text-align:right;">${formatBRL(p.total)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('relatorio-top-produtos')!.innerHTML = '';
|
|
}
|
|
|
|
// Vendas por dia
|
|
if (data.vendasPorDia.length > 0) {
|
|
document.getElementById('relatorio-vendas-dia')!.innerHTML = `
|
|
<h4 style="margin-bottom:0.75rem;">📅 Vendas por Dia</h4>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Data</th><th style="text-align:center;">Comandas</th><th style="text-align:right;">Total</th></tr></thead>
|
|
<tbody>
|
|
${data.vendasPorDia.map((d: any) => `
|
|
<tr>
|
|
<td>${new Date(d.data + 'T12:00:00').toLocaleDateString('pt-BR')}</td>
|
|
<td style="text-align:center;">${d.comandas}</td>
|
|
<td style="text-align:right;">${formatBRL(d.vendas)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('relatorio-vendas-dia')!.innerHTML = '';
|
|
}
|
|
|
|
// Vendas por pagamento
|
|
if (data.vendasPorPagamento.length > 0) {
|
|
const pagLabels: Record<string, string> = {
|
|
DINHEIRO: '💵 Dinheiro', CARTAO_CREDITO: '💳 Cartão Crédito',
|
|
CARTAO_DEBITO: '💳 Cartão Débito', PIX: '📱 PIX', NAO_INFORMADO: '❓ Não informado',
|
|
};
|
|
document.getElementById('relatorio-pagamento')!.innerHTML = `
|
|
<h4 style="margin-bottom:0.75rem;">💰 Por Forma de Pagamento</h4>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Forma</th><th style="text-align:right;">Total</th></tr></thead>
|
|
<tbody>
|
|
${data.vendasPorPagamento.map((p: any) => `
|
|
<tr>
|
|
<td>${pagLabels[p.forma] ?? p.forma}</td>
|
|
<td style="text-align:right;">${formatBRL(p.total)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('relatorio-pagamento')!.innerHTML = '';
|
|
}
|
|
} catch (err: any) {
|
|
toast(err.message, 'error');
|
|
}
|
|
};
|
|
|
|
document.getElementById('btn-refresh-dashboard')?.addEventListener('click', () => {
|
|
loadDashboard();
|
|
loadRelatorio();
|
|
});
|
|
|
|
document.getElementById('btn-gerar-relatorio')?.addEventListener('click', loadRelatorio);
|
|
|
|
await Promise.all([loadDashboard(), loadRelatorio()]);
|
|
}
|