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:
28
src/controllers/auth.controller.ts
Normal file
28
src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { loginSchema, changePasswordSchema } from '../schemas/auth.schema';
|
||||
import { loginService, changePassword } from '../services/auth.service';
|
||||
|
||||
export async function loginController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const body = loginSchema.parse(request.body);
|
||||
|
||||
try {
|
||||
const user = await loginService(body);
|
||||
const token = await reply.jwtSign(user, { expiresIn: '1d' });
|
||||
|
||||
return reply.send({ token, user });
|
||||
} catch (error: any) {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePasswordController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const body = changePasswordSchema.parse(request.body);
|
||||
const user = request.user as { id: string };
|
||||
|
||||
try {
|
||||
await changePassword(user.id, body.senhaAtual, body.novaSenha);
|
||||
return reply.send({ message: 'Senha alterada com sucesso' });
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
49
src/controllers/category.controller.ts
Normal file
49
src/controllers/category.controller.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { categorySchema } from '../schemas/category.schema';
|
||||
import * as categoryService from '../services/category.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function createCategoryController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const data = categorySchema.parse(request.body);
|
||||
try {
|
||||
const category = await categoryService.createCategory(data);
|
||||
return reply.status(201).send(category);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCategoriesController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const categories = await categoryService.listCategories();
|
||||
return reply.send(categories);
|
||||
}
|
||||
|
||||
export async function getCategoryController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const category = await categoryService.getCategoryById(id);
|
||||
if (!category) {
|
||||
return reply.status(404).send({ error: 'Categoria não encontrada' });
|
||||
}
|
||||
return reply.send(category);
|
||||
}
|
||||
|
||||
export async function updateCategoryController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = categorySchema.parse(request.body);
|
||||
try {
|
||||
const category = await categoryService.updateCategory(id, data);
|
||||
return reply.send(category);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCategoryController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
try {
|
||||
await categoryService.deleteCategory(id);
|
||||
return reply.status(204).send();
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
94
src/controllers/order.controller.ts
Normal file
94
src/controllers/order.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { createOrderSchema, addItemOrderSchema, updateItemStatusSchema, updateOrderSchema, payOrderSchema } from '../schemas/order.schema';
|
||||
import * as orderService from '../services/order.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function listOrdersController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const querySchema = z.object({
|
||||
status: z.string().optional(),
|
||||
dataInicio: z.string().optional(),
|
||||
dataFim: z.string().optional(),
|
||||
});
|
||||
|
||||
const query = querySchema.parse(request.query);
|
||||
|
||||
const filter: any = {};
|
||||
if (query.status) filter.statuses = query.status.split(',');
|
||||
if (query.dataInicio) filter.dataInicio = query.dataInicio;
|
||||
if (query.dataFim) filter.dataFim = query.dataFim;
|
||||
|
||||
const orders = await orderService.listOrders(
|
||||
(filter.statuses || filter.dataInicio || filter.dataFim) ? filter : undefined
|
||||
);
|
||||
return reply.send(orders);
|
||||
}
|
||||
|
||||
export async function createOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const data = createOrderSchema.parse(request.body);
|
||||
const order = await orderService.createOrder(data);
|
||||
return reply.status(201).send(order);
|
||||
}
|
||||
|
||||
export async function addItemToOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = addItemOrderSchema.parse(request.body);
|
||||
|
||||
try {
|
||||
const item = await orderService.addItemToOrder(id, data);
|
||||
return reply.status(201).send(item);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function listKitchenBarController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const querySchema = z.object({
|
||||
status: z.string().optional(),
|
||||
itemCozinha: z.string().transform(v => v === 'true').optional()
|
||||
});
|
||||
|
||||
const query = querySchema.parse(request.query);
|
||||
const items = await orderService.listKitchenBarItems(query.itemCozinha, query.status);
|
||||
return reply.send(items);
|
||||
}
|
||||
|
||||
export async function updateItemStatusController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = updateItemStatusSchema.parse(request.body);
|
||||
const item = await orderService.updateItemStatus(id, data);
|
||||
return reply.send(item);
|
||||
}
|
||||
|
||||
export async function payOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const body = request.body && Object.keys(request.body as object).length > 0
|
||||
? payOrderSchema.parse(request.body)
|
||||
: undefined;
|
||||
try {
|
||||
const comanda = await orderService.payOrder(id, body);
|
||||
return reply.send(comanda);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = updateOrderSchema.parse(request.body);
|
||||
try {
|
||||
const comanda = await orderService.updateOrder(id, data);
|
||||
return reply.send(comanda);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItemFromOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
try {
|
||||
const comanda = await orderService.deleteItemFromOrder(id);
|
||||
return reply.send(comanda);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
54
src/controllers/product.controller.ts
Normal file
54
src/controllers/product.controller.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { productSchema } from '../schemas/product.schema';
|
||||
import * as productService from '../services/product.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function createProductController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const data = productSchema.parse(request.body);
|
||||
const product = await productService.createProduct(data);
|
||||
return reply.status(201).send(product);
|
||||
}
|
||||
|
||||
export async function listProductsController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const products = await productService.listProducts();
|
||||
return reply.send(products);
|
||||
}
|
||||
|
||||
export async function getTopProductsController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { limit } = z.object({ limit: z.coerce.number().default(10) }).parse(request.query);
|
||||
const products = await productService.getTopProducts(limit);
|
||||
return reply.send(products);
|
||||
}
|
||||
|
||||
export async function searchProductsController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { q } = z.object({ q: z.string().min(1) }).parse(request.query);
|
||||
const products = await productService.searchProducts(q);
|
||||
return reply.send(products);
|
||||
}
|
||||
|
||||
export async function getProductController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const product = await productService.getProductById(id);
|
||||
if (!product) {
|
||||
return reply.status(404).send({ error: 'Produto não encontrado' });
|
||||
}
|
||||
return reply.send(product);
|
||||
}
|
||||
|
||||
export async function updateProductController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = productSchema.partial().parse(request.body);
|
||||
const product = await productService.updateProduct(id, data);
|
||||
return reply.send(product);
|
||||
}
|
||||
|
||||
export async function deleteProductController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
await productService.deleteProduct(id);
|
||||
return reply.status(204).send();
|
||||
}
|
||||
|
||||
export async function getLowStockProductsController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const products = await productService.getLowStockProducts();
|
||||
return reply.send(products);
|
||||
}
|
||||
22
src/controllers/report.controller.ts
Normal file
22
src/controllers/report.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as reportService from '../services/report.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function salesReportController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const querySchema = z.object({
|
||||
inicio: z.string().optional(),
|
||||
fim: z.string().optional(),
|
||||
});
|
||||
const query = querySchema.parse(request.query);
|
||||
|
||||
const inicio = query.inicio ? new Date(query.inicio) : new Date(new Date().setDate(new Date().getDate() - 30));
|
||||
const fim = query.fim ? new Date(query.fim + 'T23:59:59') : new Date();
|
||||
|
||||
const report = await reportService.getSalesReport(inicio, fim);
|
||||
return reply.send(report);
|
||||
}
|
||||
|
||||
export async function dashboardController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const dashboard = await reportService.getDashboardData();
|
||||
return reply.send(dashboard);
|
||||
}
|
||||
43
src/controllers/user.controller.ts
Normal file
43
src/controllers/user.controller.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { userSchema, updateUserSchema, resetPasswordSchema } from '../schemas/user.schema';
|
||||
import * as userService from '../services/user.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function createUserController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const data = userSchema.parse(request.body);
|
||||
try {
|
||||
const user = await userService.createUser(data);
|
||||
return reply.status(201).send(user);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function listUsersController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const users = await userService.listUsers();
|
||||
return reply.send(users);
|
||||
}
|
||||
|
||||
export async function updateUserController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const data = updateUserSchema.parse(request.body);
|
||||
const user = await userService.updateUser(id, data);
|
||||
return reply.send(user);
|
||||
}
|
||||
|
||||
export async function deleteUserController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
await userService.deleteUser(id);
|
||||
return reply.status(204).send();
|
||||
}
|
||||
|
||||
export async function resetPasswordController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
const body = resetPasswordSchema.parse(request.body);
|
||||
try {
|
||||
await userService.resetPassword(id, body.novaSenha);
|
||||
return reply.send({ message: 'Senha redefinida com sucesso' });
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
3
src/lib/prisma.ts
Normal file
3
src/lib/prisma.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
24
src/middlewares/auth.middleware.ts
Normal file
24
src/middlewares/auth.middleware.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
export async function authenticate(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
return reply.status(401).send({ error: 'Não autorizado' });
|
||||
}
|
||||
}
|
||||
|
||||
export function requireRoles(roles: string[]) {
|
||||
return async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const user = request.user as { role: string };
|
||||
|
||||
if (!roles.includes(user.role)) {
|
||||
return reply.status(403).send({ error: 'Acesso negado. Permissão insuficiente.' });
|
||||
}
|
||||
} catch (err) {
|
||||
return reply.status(401).send({ error: 'Não autorizado' });
|
||||
}
|
||||
};
|
||||
}
|
||||
8
src/routes/auth.routes.ts
Normal file
8
src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { loginController, changePasswordController } from '../controllers/auth.controller';
|
||||
import { authenticate } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function authRoutes(app: FastifyInstance) {
|
||||
app.post('/login', loginController);
|
||||
app.patch('/senha', { preHandler: [authenticate] }, changePasswordController);
|
||||
}
|
||||
14
src/routes/category.routes.ts
Normal file
14
src/routes/category.routes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as categoryController from '../controllers/category.controller';
|
||||
import { authenticate, requireRoles } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function categoryRoutes(app: FastifyInstance) {
|
||||
// Leitura aberta a todos autenticados
|
||||
app.get('/categorias', { preHandler: [authenticate] }, categoryController.listCategoriesController);
|
||||
app.get('/categorias/:id', { preHandler: [authenticate] }, categoryController.getCategoryController);
|
||||
|
||||
// Escrita apenas para ADMIN
|
||||
app.post('/categorias', { preHandler: [requireRoles(['ADMIN'])] }, categoryController.createCategoryController);
|
||||
app.put('/categorias/:id', { preHandler: [requireRoles(['ADMIN'])] }, categoryController.updateCategoryController);
|
||||
app.delete('/categorias/:id', { preHandler: [requireRoles(['ADMIN'])] }, categoryController.deleteCategoryController);
|
||||
}
|
||||
23
src/routes/order.routes.ts
Normal file
23
src/routes/order.routes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as orderController from '../controllers/order.controller';
|
||||
import { authenticate, requireRoles } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function orderRoutes(app: FastifyInstance) {
|
||||
// Listagem geral (qualquer autenticado)
|
||||
app.get('/comandas', { preHandler: [authenticate] }, orderController.listOrdersController);
|
||||
|
||||
// Comandas (Garçom, Caixa, Admin)
|
||||
app.post('/comandas', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.createOrderController);
|
||||
app.post('/comandas/:id/itens', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.addItemToOrderController);
|
||||
app.patch('/comandas/:id', { preHandler: [requireRoles(['GARCOM', 'CAIXA', 'ADMIN'])] }, orderController.updateOrderController);
|
||||
|
||||
// Pagamento (Caixa, Admin)
|
||||
app.post('/comandas/:id/pagar', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.payOrderController);
|
||||
|
||||
// Painel Cozinha / Bar
|
||||
app.get('/painel/itens', { preHandler: [requireRoles(['COZINHA', 'BARMAN', 'ADMIN'])] }, orderController.listKitchenBarController);
|
||||
app.patch('/painel/itens/:id/status', { preHandler: [requireRoles(['COZINHA', 'BARMAN', 'ADMIN'])] }, orderController.updateItemStatusController);
|
||||
|
||||
// Excluir item de comanda (apenas CAIXA e ADMIN)
|
||||
app.delete('/comandas/itens/:id', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.deleteItemFromOrderController);
|
||||
}
|
||||
16
src/routes/product.routes.ts
Normal file
16
src/routes/product.routes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as productController from '../controllers/product.controller';
|
||||
import { authenticate, requireRoles } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function productRoutes(app: FastifyInstance) {
|
||||
app.get('/produtos', { preHandler: [authenticate] }, productController.listProductsController);
|
||||
app.get('/produtos/mais-vendidos', { preHandler: [authenticate] }, productController.getTopProductsController);
|
||||
app.get('/produtos/busca', { preHandler: [authenticate] }, productController.searchProductsController);
|
||||
app.get('/produtos/baixo-estoque', { preHandler: [requireRoles(['ADMIN'])] }, productController.getLowStockProductsController);
|
||||
app.get('/produtos/:id', { preHandler: [authenticate] }, productController.getProductController);
|
||||
|
||||
app.post('/produtos', { preHandler: [requireRoles(['ADMIN'])] }, productController.createProductController);
|
||||
app.put('/produtos/:id', { preHandler: [requireRoles(['ADMIN'])] }, productController.updateProductController);
|
||||
app.patch('/produtos/:id', { preHandler: [requireRoles(['ADMIN'])] }, productController.updateProductController);
|
||||
app.delete('/produtos/:id', { preHandler: [requireRoles(['ADMIN'])] }, productController.deleteProductController);
|
||||
}
|
||||
8
src/routes/report.routes.ts
Normal file
8
src/routes/report.routes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as reportController from '../controllers/report.controller';
|
||||
import { requireRoles } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function reportRoutes(app: FastifyInstance) {
|
||||
app.get('/relatorios/vendas', { preHandler: [requireRoles(['ADMIN'])] }, reportController.salesReportController);
|
||||
app.get('/relatorios/dashboard', { preHandler: [requireRoles(['ADMIN'])] }, reportController.dashboardController);
|
||||
}
|
||||
13
src/routes/user.routes.ts
Normal file
13
src/routes/user.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as userController from '../controllers/user.controller';
|
||||
import { requireRoles } from '../middlewares/auth.middleware';
|
||||
|
||||
export async function userRoutes(app: FastifyInstance) {
|
||||
// Apenas ADMIN pode gerenciar usuários
|
||||
app.get('/usuarios', { preHandler: [requireRoles(['ADMIN'])] }, userController.listUsersController);
|
||||
app.post('/usuarios', { preHandler: [requireRoles(['ADMIN'])] }, userController.createUserController);
|
||||
app.put('/usuarios/:id', { preHandler: [requireRoles(['ADMIN'])] }, userController.updateUserController);
|
||||
app.patch('/usuarios/:id', { preHandler: [requireRoles(['ADMIN'])] }, userController.updateUserController);
|
||||
app.patch('/usuarios/:id/senha', { preHandler: [requireRoles(['ADMIN'])] }, userController.resetPasswordController);
|
||||
app.delete('/usuarios/:id', { preHandler: [requireRoles(['ADMIN'])] }, userController.deleteUserController);
|
||||
}
|
||||
15
src/schemas/auth.schema.ts
Normal file
15
src/schemas/auth.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
senha: z.string().min(6),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
senhaAtual: z.string().min(6),
|
||||
novaSenha: z.string().min(6),
|
||||
});
|
||||
|
||||
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
|
||||
7
src/schemas/category.schema.ts
Normal file
7
src/schemas/category.schema.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const categorySchema = z.object({
|
||||
nome: z.string().min(2),
|
||||
});
|
||||
|
||||
export type CategoryInput = z.infer<typeof categorySchema>;
|
||||
37
src/schemas/order.schema.ts
Normal file
37
src/schemas/order.schema.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createOrderSchema = z.object({
|
||||
identificador: z.string().min(1),
|
||||
nomeCliente: z.string().optional(),
|
||||
});
|
||||
|
||||
export const addItemOrderSchema = z.object({
|
||||
produtoId: z.string().uuid(),
|
||||
quantidade: z.number().int().positive(),
|
||||
observacao: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateItemStatusSchema = z.object({
|
||||
status: z.enum(['PENDENTE', 'PREPARANDO', 'PRONTO', 'ENTREGUE']),
|
||||
});
|
||||
|
||||
export const updateOrderSchema = z.object({
|
||||
identificador: z.string().min(1).optional(),
|
||||
nomeCliente: z.string().optional(),
|
||||
status: z.enum(['ABERTA', 'FECHADA', 'PAGA', 'CANCELADA']).optional(),
|
||||
desconto: z.number().min(0).optional(),
|
||||
acrescimo: z.number().min(0).optional(),
|
||||
taxaServico: z.boolean().optional(),
|
||||
motivoCancelamento: z.string().optional(),
|
||||
});
|
||||
|
||||
export const payOrderSchema = z.object({
|
||||
formaPagamento: z.enum(['DINHEIRO', 'CARTAO_CREDITO', 'CARTAO_DEBITO', 'PIX']),
|
||||
});
|
||||
|
||||
export type CreateOrderInput = z.infer<typeof createOrderSchema>;
|
||||
export type AddItemOrderInput = z.infer<typeof addItemOrderSchema>;
|
||||
export type UpdateItemStatusInput = z.infer<typeof updateItemStatusSchema>;
|
||||
export type UpdateOrderInput = z.infer<typeof updateOrderSchema>;
|
||||
export type PayOrderInput = z.infer<typeof payOrderSchema>;
|
||||
|
||||
16
src/schemas/product.schema.ts
Normal file
16
src/schemas/product.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const productSchema = z.object({
|
||||
nome: z.string().min(2),
|
||||
descricao: z.string().optional(),
|
||||
fotoUrl: z.string().url().optional(),
|
||||
preco: z.number().positive(),
|
||||
custo: z.number().positive().optional(),
|
||||
estoqueAtual: z.number().int().nonnegative().default(0),
|
||||
estoqueMinimo: z.number().int().nonnegative().default(0),
|
||||
itemCozinha: z.boolean(),
|
||||
ativo: z.boolean().default(true),
|
||||
categoriaId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type ProductInput = z.infer<typeof productSchema>;
|
||||
25
src/schemas/user.schema.ts
Normal file
25
src/schemas/user.schema.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const userSchema = z.object({
|
||||
nome: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
senha: z.string().min(6),
|
||||
role: z.enum(['ADMIN', 'CAIXA', 'GARCOM', 'COZINHA', 'BARMAN']),
|
||||
ativo: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
nome: z.string().min(2).optional(),
|
||||
email: z.string().email().optional(),
|
||||
role: z.enum(['ADMIN', 'CAIXA', 'GARCOM', 'COZINHA', 'BARMAN']).optional(),
|
||||
ativo: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type UserInput = z.infer<typeof userSchema>;
|
||||
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
novaSenha: z.string().min(6),
|
||||
});
|
||||
|
||||
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
||||
61
src/server.ts
Normal file
61
src/server.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'dotenv/config';
|
||||
import fastify from 'fastify';
|
||||
import jwt from '@fastify/jwt';
|
||||
import { ZodError } from 'zod';
|
||||
import cors from '@fastify/cors';
|
||||
import { authRoutes } from './routes/auth.routes';
|
||||
import { categoryRoutes } from './routes/category.routes';
|
||||
import { productRoutes } from './routes/product.routes';
|
||||
import { orderRoutes } from './routes/order.routes';
|
||||
import { userRoutes } from './routes/user.routes';
|
||||
import { reportRoutes } from './routes/report.routes';
|
||||
|
||||
const app = fastify({ logger: true });
|
||||
|
||||
// Register CORS
|
||||
app.register(cors, {
|
||||
origin: '*', // Permitir de qualquer origem (útil em dev)
|
||||
});
|
||||
|
||||
// Register JWT
|
||||
app.register(jwt, {
|
||||
secret: process.env.JWT_SECRET || 'fallback_secret_change_me'
|
||||
});
|
||||
|
||||
// Global Error Handler
|
||||
app.setErrorHandler((error, request, reply) => {
|
||||
if (error instanceof ZodError) {
|
||||
return reply.status(400).send({
|
||||
message: 'Erro de validação',
|
||||
issues: error.format(),
|
||||
});
|
||||
}
|
||||
|
||||
if (error.statusCode) {
|
||||
return reply.status(error.statusCode).send({ message: error.message });
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return reply.status(500).send({ message: 'Erro interno do servidor' });
|
||||
});
|
||||
|
||||
// Register Routes
|
||||
app.register(authRoutes, { prefix: '/api/auth' });
|
||||
app.register(categoryRoutes, { prefix: '/api' });
|
||||
app.register(productRoutes, { prefix: '/api' });
|
||||
app.register(orderRoutes, { prefix: '/api' });
|
||||
app.register(userRoutes, { prefix: '/api' });
|
||||
app.register(reportRoutes, { prefix: '/api' });
|
||||
|
||||
// Iniciar o servidor
|
||||
const start = async () => {
|
||||
try {
|
||||
await app.listen({ port: 3333, host: '0.0.0.0' });
|
||||
console.log('Servidor rodando na porta 3333');
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
40
src/services/auth.service.ts
Normal file
40
src/services/auth.service.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { LoginInput } from '../schemas/auth.schema';
|
||||
|
||||
export async function loginService({ email, senha }: LoginInput) {
|
||||
const user = await prisma.usuario.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user || !user.ativo) {
|
||||
throw new Error('Credenciais inválidas ou usuário inativo');
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(senha, user.senha);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new Error('Credenciais inválidas ou usuário inativo');
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
nome: user.nome,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
};
|
||||
}
|
||||
|
||||
export async function changePassword(userId: string, senhaAtual: string, novaSenha: string) {
|
||||
const user = await prisma.usuario.findUnique({ where: { id: userId } });
|
||||
if (!user) throw new Error('Usuário não encontrado');
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(senhaAtual, user.senha);
|
||||
if (!isPasswordValid) throw new Error('Senha atual incorreta');
|
||||
|
||||
const passwordHash = await bcrypt.hash(novaSenha, 10);
|
||||
await prisma.usuario.update({
|
||||
where: { id: userId },
|
||||
data: { senha: passwordHash },
|
||||
});
|
||||
}
|
||||
47
src/services/category.service.ts
Normal file
47
src/services/category.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { CategoryInput } from '../schemas/category.schema';
|
||||
|
||||
export async function createCategory(data: CategoryInput) {
|
||||
return prisma.categoria.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listCategories() {
|
||||
return prisma.categoria.findMany({
|
||||
include: { _count: { select: { produtos: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCategoryById(id: string) {
|
||||
return prisma.categoria.findUnique({
|
||||
where: { id },
|
||||
include: { produtos: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCategory(id: string, data: CategoryInput) {
|
||||
return prisma.categoria.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCategory(id: string) {
|
||||
const categoria = await prisma.categoria.findUnique({
|
||||
where: { id },
|
||||
include: { _count: { select: { produtos: true } } },
|
||||
});
|
||||
|
||||
if (!categoria) {
|
||||
throw new Error('Categoria não encontrada');
|
||||
}
|
||||
|
||||
if (categoria._count.produtos > 0) {
|
||||
throw new Error(`Não é possível excluir: a categoria possui ${categoria._count.produtos} produto(s). Remova ou reatribua os produtos primeiro.`);
|
||||
}
|
||||
|
||||
return prisma.categoria.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
210
src/services/order.service.ts
Normal file
210
src/services/order.service.ts
Normal 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 } } },
|
||||
});
|
||||
}
|
||||
88
src/services/product.service.ts
Normal file
88
src/services/product.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { ProductInput } from '../schemas/product.schema';
|
||||
|
||||
export async function getTopProducts(limit = 10) {
|
||||
// Agrega a soma de quantidades vendidas para cada produto
|
||||
const topItems = await prisma.itemPedido.groupBy({
|
||||
by: ['produtoId'],
|
||||
_sum: { quantidade: true },
|
||||
orderBy: { _sum: { quantidade: 'desc' } },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
if (!topItems.length) {
|
||||
// Fallback: retorna os últimos produtos cadastrados
|
||||
return prisma.produto.findMany({
|
||||
where: { ativo: true },
|
||||
include: { categoria: true },
|
||||
orderBy: { nome: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
const ids = topItems.map(t => t.produtoId);
|
||||
const produtos = await prisma.produto.findMany({
|
||||
where: { id: { in: ids }, ativo: true },
|
||||
include: { categoria: true },
|
||||
});
|
||||
|
||||
// Mantém a ordem de vendas + injeta totalVendido
|
||||
return ids.map(id => {
|
||||
const prod = produtos.find(p => p.id === id)!;
|
||||
const vendas = topItems.find(t => t.produtoId === id)?._sum.quantidade ?? 0;
|
||||
return { ...prod, totalVendido: vendas };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function searchProducts(query: string) {
|
||||
return prisma.produto.findMany({
|
||||
where: {
|
||||
ativo: true,
|
||||
nome: { contains: query },
|
||||
},
|
||||
include: { categoria: true },
|
||||
orderBy: { nome: 'asc' },
|
||||
take: 20,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createProduct(data: ProductInput) {
|
||||
return prisma.produto.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listProducts() {
|
||||
return prisma.produto.findMany({
|
||||
include: { categoria: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProductById(id: string) {
|
||||
return prisma.produto.findUnique({
|
||||
where: { id },
|
||||
include: { categoria: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProduct(id: string, data: Partial<ProductInput>) {
|
||||
return prisma.produto.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteProduct(id: string) {
|
||||
return prisma.produto.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLowStockProducts() {
|
||||
const products = await prisma.produto.findMany({
|
||||
where: { ativo: true },
|
||||
include: { categoria: true },
|
||||
orderBy: { estoqueAtual: 'asc' },
|
||||
});
|
||||
return products.filter(p => p.estoqueAtual <= p.estoqueMinimo);
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
49
src/services/user.service.ts
Normal file
49
src/services/user.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { UserInput, UpdateUserInput } from '../schemas/user.schema';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
export async function createUser(data: UserInput) {
|
||||
const existingUser = await prisma.usuario.findUnique({ where: { email: data.email } });
|
||||
if (existingUser) throw new Error('E-mail já cadastrado');
|
||||
|
||||
const passwordHash = await bcrypt.hash(data.senha, 10);
|
||||
|
||||
return prisma.usuario.create({
|
||||
data: {
|
||||
...data,
|
||||
senha: passwordHash,
|
||||
},
|
||||
select: { id: true, nome: true, email: true, role: true, ativo: true, criadoEm: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function listUsers() {
|
||||
return prisma.usuario.findMany({
|
||||
select: { id: true, nome: true, email: true, role: true, ativo: true, criadoEm: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(id: string, data: UpdateUserInput) {
|
||||
return prisma.usuario.update({
|
||||
where: { id },
|
||||
data,
|
||||
select: { id: true, nome: true, email: true, role: true, ativo: true, criadoEm: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string) {
|
||||
return prisma.usuario.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetPassword(id: string, novaSenha: string) {
|
||||
const user = await prisma.usuario.findUnique({ where: { id } });
|
||||
if (!user) throw new Error('Usuário não encontrado');
|
||||
|
||||
const passwordHash = await bcrypt.hash(novaSenha, 10);
|
||||
await prisma.usuario.update({
|
||||
where: { id },
|
||||
data: { senha: passwordHash },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user