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,210 @@
import { prisma } from '../lib/prisma';
import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateOrderInput, PayOrderInput } from '../schemas/order.schema';
export interface ListOrdersFilter {
statuses?: string[];
dataInicio?: string;
dataFim?: string;
}
export async function listOrders(filter?: ListOrdersFilter) {
const where: any = {};
if (filter?.statuses?.length) {
where.status = { in: filter.statuses };
}
if (filter?.dataInicio || filter?.dataFim) {
where.criadoEm = {};
if (filter.dataInicio) where.criadoEm.gte = new Date(filter.dataInicio);
if (filter.dataFim) {
const end = new Date(filter.dataFim);
end.setHours(23, 59, 59, 999);
where.criadoEm.lte = end;
}
}
return prisma.comanda.findMany({
where,
orderBy: { criadoEm: 'desc' },
include: {
itens: {
include: { produto: true }
}
}
});
}
export async function createOrder(data: CreateOrderInput) {
return prisma.comanda.create({
data: {
identificador: data.identificador,
nomeCliente: data.nomeCliente,
status: 'ABERTA',
total: 0,
},
});
}
export async function addItemToOrder(comandaId: string, data: AddItemOrderInput) {
const produto = await prisma.produto.findUnique({
where: { id: data.produtoId },
});
if (!produto) {
throw new Error('Produto não encontrado');
}
if (produto.estoqueAtual < data.quantidade) {
throw new Error(`Estoque insuficiente. Disponível: ${produto.estoqueAtual}`);
}
const precoTotalItem = produto.preco * data.quantidade;
const [, itemPedido] = await prisma.$transaction([
prisma.produto.update({
where: { id: data.produtoId },
data: { estoqueAtual: { decrement: data.quantidade } },
}),
prisma.itemPedido.create({
data: {
quantidade: data.quantidade,
observacao: data.observacao,
precoUnitario: produto.preco,
produtoId: data.produtoId,
comandaId: comandaId,
status: 'PENDENTE',
},
}),
prisma.comanda.update({
where: { id: comandaId },
data: { total: { increment: precoTotalItem } },
}),
]);
return itemPedido;
}
export async function listKitchenBarItems(itemCozinha?: boolean, status?: string) {
const filter: any = {};
if (status) {
filter.status = status;
}
if (typeof itemCozinha === 'boolean') {
filter.produto = {
itemCozinha,
};
}
return prisma.itemPedido.findMany({
where: filter,
include: {
produto: true,
comanda: true,
},
});
}
export async function updateItemStatus(itemId: string, data: UpdateItemStatusInput) {
return prisma.itemPedido.update({
where: { id: itemId },
data: {
status: data.status,
},
});
}
export async function payOrder(comandaId: string, data?: PayOrderInput) {
const comanda = await prisma.comanda.findUnique({
where: { id: comandaId },
});
if (!comanda) {
throw new Error('Comanda não encontrada');
}
if (comanda.status !== 'ABERTA') {
throw new Error('Comanda não está aberta para pagamento');
}
return prisma.comanda.update({
where: { id: comandaId },
data: {
status: 'PAGA',
fechadoEm: new Date(),
formaPagamento: data?.formaPagamento,
},
});
}
export async function updateOrder(comandaId: string, data: UpdateOrderInput) {
const comanda = await prisma.comanda.findUnique({
where: { id: comandaId },
});
if (!comanda) {
throw new Error('Comanda não encontrada');
}
const updateData: any = {};
if (data.identificador !== undefined) updateData.identificador = data.identificador;
if (data.nomeCliente !== undefined) updateData.nomeCliente = data.nomeCliente;
if (data.status !== undefined) updateData.status = data.status;
if (data.desconto !== undefined) updateData.desconto = data.desconto;
if (data.acrescimo !== undefined) updateData.acrescimo = data.acrescimo;
if (data.taxaServico !== undefined) updateData.taxaServico = data.taxaServico;
if (data.motivoCancelamento !== undefined) updateData.motivoCancelamento = data.motivoCancelamento;
if (data.status === 'PAGA' && comanda.status !== 'PAGA') {
updateData.fechadoEm = new Date();
}
return prisma.comanda.update({
where: { id: comandaId },
data: updateData,
include: {
itens: {
include: { produto: true }
}
}
});
}
export async function deleteItemFromOrder(itemId: string) {
const item = await prisma.itemPedido.findUnique({
where: { id: itemId },
include: { comanda: true }
});
if (!item) {
throw new Error('Item do pedido não encontrado');
}
if (item.comanda.status !== 'ABERTA') {
throw new Error('Não é possível remover itens de uma comanda fechada/paga/cancelada');
}
const precoTotalItem = item.precoUnitario * item.quantidade;
const [comanda] = await prisma.$transaction([
prisma.itemPedido.delete({
where: { id: itemId }
}),
prisma.produto.update({
where: { id: item.produtoId },
data: { estoqueAtual: { increment: item.quantidade } },
}),
prisma.comanda.update({
where: { id: item.comandaId },
data: { total: { decrement: precoTotalItem } },
}),
]);
return prisma.comanda.findUnique({
where: { id: item.comandaId },
include: { itens: { include: { produto: true } } },
});
}