feat: sistema GastroBar PDV completo

- 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
This commit is contained in:
2026-07-21 12:47:15 -03:00
commit 402ac776ac
55 changed files with 7403 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
import { prisma } from '../lib/prisma';
export async function getSalesReport(inicio: Date, fim: Date) {
const comandas = await prisma.comanda.findMany({
where: {
status: 'PAGA',
fechadoEm: { gte: inicio, lte: fim },
},
include: {
itens: {
include: { produto: true },
},
},
orderBy: { fechadoEm: 'desc' },
});
const totalVendas = comandas.reduce((acc, c) => acc + c.total, 0);
const totalDescontos = comandas.reduce((acc, c) => acc + c.desconto, 0);
const totalAcrescimos = comandas.reduce((acc, c) => acc + c.acrescimo, 0);
const totalComandas = comandas.length;
// Produtos mais vendidos
const produtoVendas: Record<string, { nome: string; quantidade: number; total: number }> = {};
for (const c of comandas) {
for (const item of c.itens) {
const key = item.produtoId;
if (!produtoVendas[key]) {
produtoVendas[key] = { nome: item.produto.nome, quantidade: 0, total: 0 };
}
produtoVendas[key].quantidade += item.quantidade;
produtoVendas[key].total += item.precoUnitario * item.quantidade;
}
}
const topProdutos = Object.values(produtoVendas)
.sort((a, b) => b.quantidade - a.quantidade)
.slice(0, 10);
// Vendas por dia
const vendasPorDia: Record<string, { vendas: number; comandas: number }> = {};
for (const c of comandas) {
const dia = c.fechadoEm!.toISOString().split('T')[0];
if (!vendasPorDia[dia]) vendasPorDia[dia] = { vendas: 0, comandas: 0 };
vendasPorDia[dia].vendas += c.total;
vendasPorDia[dia].comandas += 1;
}
// Vendas por forma de pagamento
const vendasPorPagamento: Record<string, number> = {};
for (const c of comandas) {
const fp = c.formaPagamento || 'NAO_INFORMADO';
vendasPorPagamento[fp] = (vendasPorPagamento[fp] || 0) + c.total;
}
return {
periodo: { inicio, fim },
resumo: {
totalVendas,
totalDescontos,
totalAcrescimos,
totalComandas,
ticketMedio: totalComandas > 0 ? totalVendas / totalComandas : 0,
},
topProdutos,
vendasPorDia: Object.entries(vendasPorDia).map(([data, v]) => ({ data, ...v })),
vendasPorPagamento: Object.entries(vendasPorPagamento).map(([forma, total]) => ({ forma, total })),
comandas: comandas.map(c => ({
id: c.id,
identificador: c.identificador,
nomeCliente: c.nomeCliente,
total: c.total,
desconto: c.desconto,
acrescimo: c.acrescimo,
formaPagamento: c.formaPagamento,
criadoEm: c.criadoEm,
fechadoEm: c.fechadoEm,
itensCount: c.itens.length,
})),
};
}
export async function getDashboardData() {
const now = new Date();
const inicioDia = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const inicioSemana = new Date(now);
inicioSemana.setDate(now.getDate() - now.getDay());
inicioSemana.setHours(0, 0, 0, 0);
const inicioMes = new Date(now.getFullYear(), now.getMonth(), 1);
const [comandasHoje, comandasSemana, comandasMes, totalProdutos, produtosEstoqueBaixo, comandasAbertas] = await Promise.all([
prisma.comanda.findMany({
where: { status: 'PAGA', fechadoEm: { gte: inicioDia } },
include: { itens: true },
}),
prisma.comanda.findMany({
where: { status: 'PAGA', fechadoEm: { gte: inicioSemana } },
include: { itens: true },
}),
prisma.comanda.findMany({
where: { status: 'PAGA', fechadoEm: { gte: inicioMes } },
include: { itens: true },
}),
prisma.produto.count({ where: { ativo: true } }),
prisma.produto.findMany({
where: { ativo: true },
include: { categoria: true },
}),
prisma.comanda.count({ where: { status: 'ABERTA' } }),
]);
const sumTotal = (cs: any[]) => cs.reduce((acc, c) => acc + c.total, 0);
const sumItens = (cs: any[]) => cs.reduce((acc, c) => acc + c.itens.reduce((a: number, i: any) => a + i.quantidade, 0), 0);
const estoqueBaixo = produtosEstoqueBaixo.filter(p => p.estoqueAtual <= p.estoqueMinimo);
return {
vendasHoje: {
total: sumTotal(comandasHoje),
comandas: comandasHoje.length,
itens: sumItens(comandasHoje),
},
vendasSemana: {
total: sumTotal(comandasSemana),
comandas: comandasSemana.length,
itens: sumItens(comandasSemana),
},
vendasMes: {
total: totalProdutos,
comandas: comandasMes.length,
itens: sumItens(comandasMes),
totalVendas: sumTotal(comandasMes),
},
totalProdutos,
produtosEstoqueBaixo: estoqueBaixo.length,
comandasAbertas,
};
}