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:
137
src/services/report.service.ts
Normal file
137
src/services/report.service.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user