commit 402ac776acb3d4b1d2dc3c031556d796761f8179 Author: Welton Moura Date: Tue Jul 21 12:47:15 2026 -0300 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b93a640 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env +*.db +*.db-journal +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..5eb6cad --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# Gastrobar PDV + +Sistema de PDV (Ponto de Venda) e gerenciamento para gastrobar/restaurante. Gerencia comandas de clientes, pedidos, cardápio de produtos, usuários com diferentes perfis de acesso e fluxo de preparo na cozinha/bar. + +## Stack + +### Backend +- **Runtime:** Node.js + TypeScript +- **Framework:** Fastify +- **ORM:** Prisma +- **Banco:** SQLite (desenvolvimento) +- **Validação:** Zod +- **Auth:** JWT (@fastify/jwt) + bcrypt + +### Frontend +- **Bundler:** Vite +- **Linguagem:** TypeScript vanilla (sem framework) +- **Estilo:** CSS customizado com dark theme e glassmorphism + +## Funcionalidades + +- **Autenticação JWT** com controle por roles (ADMIN, CAIXA, GARÇOM, COZINHA, BARMAN) +- **Painel de Cozinha/Bar** com atualização em tempo real e notificações sonoras +- **Gestão de Comandas** com formas de pagamento, desconto, acréscimo e taxa de serviço +- **Caixa/Checkout** com busca avançada e seleção de pagamento +- **Cardápio** com gestão de produtos e categorias, controle de estoque +- **Equipe** com gestão de usuários e redefinição de senha +- **Relatórios** de vendas por período +- **Dashboard** com KPIs para administradores + +## Estrutura do Projeto + +``` +app_ces/ +├── prisma/ # Schema do banco e seed +│ ├── schema.prisma +│ └── seed.ts +├── src/ # Backend (API) +│ ├── server.ts +│ ├── lib/prisma.ts +│ ├── middlewares/ +│ ├── controllers/ +│ ├── services/ +│ ├── schemas/ +│ └── routes/ +└── web/ # Frontend (SPA) + └── src/ + ├── main.ts + ├── api.ts + ├── utils.ts + └── views/ +``` + +## Como Rodar + +### Backend + +```bash +# Instalar dependências +npm install + +# Gerar cliente Prisma e aplicar schema +npx prisma db push + +# Popular banco com dados iniciais +npm run db:seed + +# Iniciar servidor de desenvolvimento +npm run dev +``` + +O servidor estará disponível em `http://localhost:3333`. + +### Frontend + +```bash +cd web + +# Instalar dependências +npm install + +# Iniciar dev server +npm run dev +``` + +O frontend estará disponível em `http://localhost:5173`. + +## Rotas da API + +### Autenticação +| Método | Rota | Descrição | +|--------|------|-----------| +| POST | `/api/auth/login` | Login e geração de JWT | +| PATCH | `/api/auth/senha` | Alterar própria senha | + +### Categorias +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/categorias` | Listar categorias | Autenticado | +| GET | `/api/categorias/:id` | Obter categoria | Autenticado | +| POST | `/api/categorias` | Criar categoria | ADMIN | +| PUT | `/api/categorias/:id` | Atualizar categoria | ADMIN | +| DELETE | `/api/categorias/:id` | Deletar categoria | ADMIN | + +### Produtos +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/produtos` | Listar produtos | Autenticado | +| GET | `/api/produtos/mais-vendidos` | Top produtos por vendas | Autenticado | +| GET | `/api/produtos/busca` | Buscar produto por nome | Autenticado | +| GET | `/api/produtos/baixo-estoque` | Produtos com estoque baixo | ADMIN | +| GET | `/api/produtos/:id` | Obter produto | Autenticado | +| POST | `/api/produtos` | Criar produto | ADMIN | +| PUT | `/api/produtos/:id` | Atualizar produto | ADMIN | +| DELETE | `/api/produtos/:id` | Deletar produto | ADMIN | + +### Comandas +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/comandas` | Listar/filtrar comandas | Autenticado | +| POST | `/api/comandas` | Criar comanda | GARÇOM/CAIXA/ADMIN | +| POST | `/api/comandas/:id/itens` | Adicionar item | GARÇOM/CAIXA/ADMIN | +| PATCH | `/api/comandas/:id` | Atualizar comanda | GARÇOM/CAIXA/ADMIN | +| POST | `/api/comandas/:id/pagar` | Pagar comanda | CAIXA/ADMIN | +| DELETE | `/api/comandas/itens/:id` | Remover item | CAIXA/ADMIN | + +### Painel Cozinha/Bar +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/painel/itens` | Listar itens do painel | COZINHA/BARMAN/ADMIN | +| PATCH | `/api/painel/itens/:id/status` | Atualizar status | COZINHA/BARMAN/ADMIN | + +### Relatórios +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/relatorios/vendas` | Vendas por período | ADMIN | +| GET | `/api/relatorios/dashboard` | Dados do dashboard | ADMIN | + +### Usuários +| Método | Rota | Descrição | Permissão | +|--------|------|-----------|-----------| +| GET | `/api/usuarios` | Listar usuários | ADMIN | +| POST | `/api/usuarios` | Criar usuário | ADMIN | +| PATCH | `/api/usuarios/:id` | Atualizar usuário | ADMIN | +| PATCH | `/api/usuarios/:id/senha` | Resetar senha | ADMIN | +| DELETE | `/api/usuarios/:id` | Deletar usuário | ADMIN | + +## Roles e Permissões + +| Role | Acesso | +|------|--------| +| **ADMIN** | Acesso total ao sistema | +| **CAIXA** | Comandas, checkout, pagamento | +| **GARÇOM** | Criar/gerenciar comandas | +| **COZINHA** | Painel de preparo (cozinha) | +| **BARMAN** | Painel de preparo (bar) | diff --git a/api.http b/api.http new file mode 100644 index 0000000..1811f8b --- /dev/null +++ b/api.http @@ -0,0 +1,74 @@ +### Login +# @name login +POST http://localhost:3333/api/auth/login +Content-Type: application/json + +{ + "email": "admin@gastrobar.com", + "senha": "123456" +} + +### + +@token = {{login.response.body.token}} + +### Criar Categoria +POST http://localhost:3333/api/categorias +Content-Type: application/json + +{ + "nome": "Bebidas" +} + +### Listar Categorias +GET http://localhost:3333/api/categorias + +### Criar Produto +POST http://localhost:3333/api/produtos +Content-Type: application/json + +{ + "nome": "Cerveja Artesanal", + "descricao": "IPA 500ml", + "preco": 25.50, + "itemCozinha": false, + "categoriaId": "COLOQUE_O_ID_DA_CATEGORIA_AQUI" +} + +### Listar Produtos +GET http://localhost:3333/api/produtos + +### Abrir Comanda +# @name comanda +POST http://localhost:3333/api/comandas +Content-Type: application/json + +{ + "identificador": "Mesa 01", + "nomeCliente": "João Silva" +} + +### + +@comandaId = {{comanda.response.body.id}} + +### Adicionar Item à Comanda +POST http://localhost:3333/api/comandas/{{comandaId}}/itens +Content-Type: application/json + +{ + "produtoId": "COLOQUE_O_ID_DO_PRODUTO_AQUI", + "quantidade": 2, + "observacao": "Sem gelo" +} + +### Painel da Cozinha / Bar (Listar Pendentes) +GET http://localhost:3333/api/painel/itens?status=PENDENTE + +### Atualizar Status do Item +PATCH http://localhost:3333/api/painel/itens/COLOQUE_O_ID_DO_ITEM_AQUI/status +Content-Type: application/json + +{ + "status": "PREPARANDO" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..80d39e3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1980 @@ +{ + "name": "gastrobar-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gastrobar-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/cors": "^8.0.0", + "@fastify/jwt": "^8.0.1", + "@prisma/client": "^5.14.0", + "bcrypt": "^5.1.1", + "dotenv": "^17.4.2", + "fastify": "^4.27.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/node": "^20.12.12", + "prisma": "^5.14.0", + "tsx": "^4.11.0", + "typescript": "^5.4.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", + "integrity": "sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/cors/node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/jwt": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-8.0.1.tgz", + "integrity": "sha512-295bd7V6bDCnZOu8MAQgM6r7V1KILB+kdEq1q6nbHfXCnML569n7NSo3WzeLDG6IAqDl+Rhzi1vjxwaNHhRCBA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.0.0", + "@lukeed/ms": "^2.0.0", + "fast-jwt": "^4.0.0", + "fastify-plugin": "^4.0.0", + "steed": "^1.1.3" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-jwt": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-4.0.5.tgz", + "integrity": "sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.39.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.3.tgz", + "integrity": "sha512-8V8UrSDUkYpi4AXM7Na0G6hctXSaRHBGMuANOotuFdHEFtTdqDTRNfcDczA9WkKODI17o7o10iQvzdMIxXb8eA==", + "license": "MIT" + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a3db58f --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "gastrobar-backend", + "version": "1.0.0", + "description": "Backend para o sistema de PDV e gestão do gastrobar", + "main": "dist/server.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js", + "db:push": "prisma db push", + "db:seed": "tsx prisma/seed.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/cors": "^8.0.0", + "@fastify/jwt": "^8.0.1", + "@prisma/client": "^5.14.0", + "bcrypt": "^5.1.1", + "dotenv": "^17.4.2", + "fastify": "^4.27.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/node": "^20.12.12", + "prisma": "^5.14.0", + "tsx": "^4.11.0", + "typescript": "^5.4.5" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "type": "commonjs" +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..4434f18 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,69 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +generator client { + provider = "prisma-client-js" +} + +model Usuario { + id String @id @default(uuid()) + nome String + email String @unique + senha String + role String @default("GARCOM") // ADMIN, CAIXA, GARCOM, COZINHA, BARMAN + ativo Boolean @default(true) + criadoEm DateTime @default(now()) +} + +model Categoria { + id String @id @default(uuid()) + nome String + produtos Produto[] +} + +model Produto { + id String @id @default(uuid()) + nome String + descricao String? + fotoUrl String? + preco Float + custo Float? + estoqueAtual Int @default(0) + estoqueMinimo Int @default(0) + itemCozinha Boolean + ativo Boolean @default(true) + categoriaId String + categoria Categoria @relation(fields: [categoriaId], references: [id]) + itensPedido ItemPedido[] +} + +model Comanda { + id String @id @default(uuid()) + identificador String + nomeCliente String? + status String @default("ABERTA") // ABERTA, FECHADA, PAGA, CANCELADA + total Float @default(0.0) + desconto Float @default(0.0) + acrescimo Float @default(0.0) + taxaServico Boolean @default(false) + formaPagamento String? // DINHEIRO, CARTAO_CREDITO, CARTAO_DEBITO, PIX + motivoCancelamento String? + criadoEm DateTime @default(now()) + fechadoEm DateTime? + itens ItemPedido[] +} + +model ItemPedido { + id String @id @default(uuid()) + quantidade Int + precoUnitario Float + observacao String? + status String @default("PENDENTE") // PENDENTE, PREPARANDO, PRONTO, ENTREGUE + produtoId String + produto Produto @relation(fields: [produtoId], references: [id]) + comandaId String + comanda Comanda @relation(fields: [comandaId], references: [id]) + criadoEm DateTime @default(now()) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..0e34842 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,34 @@ +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Iniciando o seed...'); + + const passwordHash = await bcrypt.hash('123456', 10); + + const admin = await prisma.usuario.upsert({ + where: { email: 'admin@gastrobar.com' }, + update: {}, + create: { + nome: 'Administrador', + email: 'admin@gastrobar.com', + senha: passwordHash, + role: 'ADMIN', + ativo: true, + }, + }); + + console.log('Usuário admin criado:', admin.email); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..41bad7b --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -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 }); + } +} diff --git a/src/controllers/category.controller.ts b/src/controllers/category.controller.ts new file mode 100644 index 0000000..a7d3b7d --- /dev/null +++ b/src/controllers/category.controller.ts @@ -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 }); + } +} diff --git a/src/controllers/order.controller.ts b/src/controllers/order.controller.ts new file mode 100644 index 0000000..e1c4ad3 --- /dev/null +++ b/src/controllers/order.controller.ts @@ -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 }); + } +} diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts new file mode 100644 index 0000000..6dad0d5 --- /dev/null +++ b/src/controllers/product.controller.ts @@ -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); +} diff --git a/src/controllers/report.controller.ts b/src/controllers/report.controller.ts new file mode 100644 index 0000000..30a5351 --- /dev/null +++ b/src/controllers/report.controller.ts @@ -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); +} diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..dbc16bf --- /dev/null +++ b/src/controllers/user.controller.ts @@ -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 }); + } +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..9b6c4ce --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..58733cb --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -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' }); + } + }; +} diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts new file mode 100644 index 0000000..980691c --- /dev/null +++ b/src/routes/auth.routes.ts @@ -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); +} diff --git a/src/routes/category.routes.ts b/src/routes/category.routes.ts new file mode 100644 index 0000000..eaba45b --- /dev/null +++ b/src/routes/category.routes.ts @@ -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); +} diff --git a/src/routes/order.routes.ts b/src/routes/order.routes.ts new file mode 100644 index 0000000..3f0b103 --- /dev/null +++ b/src/routes/order.routes.ts @@ -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); +} diff --git a/src/routes/product.routes.ts b/src/routes/product.routes.ts new file mode 100644 index 0000000..6311b03 --- /dev/null +++ b/src/routes/product.routes.ts @@ -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); +} diff --git a/src/routes/report.routes.ts b/src/routes/report.routes.ts new file mode 100644 index 0000000..9981b5b --- /dev/null +++ b/src/routes/report.routes.ts @@ -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); +} diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100644 index 0000000..cfe7656 --- /dev/null +++ b/src/routes/user.routes.ts @@ -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); +} diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts new file mode 100644 index 0000000..1531545 --- /dev/null +++ b/src/schemas/auth.schema.ts @@ -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; + +export const changePasswordSchema = z.object({ + senhaAtual: z.string().min(6), + novaSenha: z.string().min(6), +}); + +export type ChangePasswordInput = z.infer; diff --git a/src/schemas/category.schema.ts b/src/schemas/category.schema.ts new file mode 100644 index 0000000..a5a6105 --- /dev/null +++ b/src/schemas/category.schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const categorySchema = z.object({ + nome: z.string().min(2), +}); + +export type CategoryInput = z.infer; diff --git a/src/schemas/order.schema.ts b/src/schemas/order.schema.ts new file mode 100644 index 0000000..ca2e532 --- /dev/null +++ b/src/schemas/order.schema.ts @@ -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; +export type AddItemOrderInput = z.infer; +export type UpdateItemStatusInput = z.infer; +export type UpdateOrderInput = z.infer; +export type PayOrderInput = z.infer; + diff --git a/src/schemas/product.schema.ts b/src/schemas/product.schema.ts new file mode 100644 index 0000000..864aaf5 --- /dev/null +++ b/src/schemas/product.schema.ts @@ -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; diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts new file mode 100644 index 0000000..a684cc9 --- /dev/null +++ b/src/schemas/user.schema.ts @@ -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; +export type UpdateUserInput = z.infer; + +export const resetPasswordSchema = z.object({ + novaSenha: z.string().min(6), +}); + +export type ResetPasswordInput = z.infer; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..6503508 --- /dev/null +++ b/src/server.ts @@ -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(); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..dc8da10 --- /dev/null +++ b/src/services/auth.service.ts @@ -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 }, + }); +} diff --git a/src/services/category.service.ts b/src/services/category.service.ts new file mode 100644 index 0000000..ed820c8 --- /dev/null +++ b/src/services/category.service.ts @@ -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 }, + }); +} diff --git a/src/services/order.service.ts b/src/services/order.service.ts new file mode 100644 index 0000000..343b3a9 --- /dev/null +++ b/src/services/order.service.ts @@ -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 } } }, + }); +} diff --git a/src/services/product.service.ts b/src/services/product.service.ts new file mode 100644 index 0000000..4db8819 --- /dev/null +++ b/src/services/product.service.ts @@ -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) { + 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); +} diff --git a/src/services/report.service.ts b/src/services/report.service.ts new file mode 100644 index 0000000..17dfb0b --- /dev/null +++ b/src/services/report.service.ts @@ -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 = {}; + 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 = {}; + 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 = {}; + 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, + }; +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..452082c --- /dev/null +++ b/src/services/user.service.ts @@ -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 }, + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d77940 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d229f9a --- /dev/null +++ b/web/index.html @@ -0,0 +1,17 @@ + + + + + + + PDV Gastrobar + + + + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..1c49b1c --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,887 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "devDependencies": { + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..909199c --- /dev/null +++ b/web/package.json @@ -0,0 +1,15 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/icons.svg b/web/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/api.ts b/web/src/api.ts new file mode 100644 index 0000000..11021e2 --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,57 @@ +const API_URL = 'http://localhost:3333/api'; + +function getAuthHeaders() { + const token = localStorage.getItem('@gastrobar:token'); + return { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +export const api = { + async post(endpoint: string, body: any) { + const response = await fetch(`${API_URL}${endpoint}`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(body), + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.error || data.message || 'Erro na requisição'); + return data; + }, + + async get(endpoint: string) { + const response = await fetch(`${API_URL}${endpoint}`, { + headers: getAuthHeaders(), + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.error || data.message || 'Erro na requisição'); + return data; + }, + + async patch(endpoint: string, body: any) { + const response = await fetch(`${API_URL}${endpoint}`, { + method: 'PATCH', + headers: getAuthHeaders(), + body: JSON.stringify(body), + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.error || data.message || 'Erro na requisição'); + return data; + }, + + async delete(endpoint: string) { + const response = await fetch(`${API_URL}${endpoint}`, { + method: 'DELETE', + headers: getAuthHeaders(), + }); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || data.message || 'Erro na requisição'); + } + return true; + } +}; diff --git a/web/src/assets/hero.png b/web/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/web/src/assets/hero.png differ diff --git a/web/src/assets/typescript.svg b/web/src/assets/typescript.svg new file mode 100644 index 0000000..6c9d69c --- /dev/null +++ b/web/src/assets/typescript.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/vite.svg b/web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..30622e7 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,66 @@ +import './style.css'; +import { renderLogin } from './views/login'; +import { renderKitchen } from './views/kitchen'; +import { renderOrders } from './views/orders'; +import { renderCheckout } from './views/checkout'; +import { renderMenu } from './views/menu'; +import { renderUsers } from './views/users'; +import { renderDashboard } from './views/dashboard'; + +const app = document.getElementById('app')!; + +export function navigateTo(path: string) { + window.history.pushState({}, '', path); + router(); +} + +(window as any).navigateTo = navigateTo; +(window as any).logout = () => { + localStorage.removeItem('@gastrobar:token'); + localStorage.removeItem('@gastrobar:user'); + navigateTo('/'); +}; + +async function router() { + const path = window.location.pathname; + const token = localStorage.getItem('@gastrobar:token'); + + // Proteção de rotas + if (!token && path !== '/') { + navigateTo('/'); + return; + } + + app.innerHTML = ''; + + switch (path) { + case '/': + if (token) navigateTo('/kitchen'); + else renderLogin(app); + break; + case '/kitchen': + await renderKitchen(app); + break; + case '/orders': + await renderOrders(app); + break; + case '/checkout': + await renderCheckout(app); + break; + case '/menu': + await renderMenu(app); + break; + case '/users': + await renderUsers(app); + break; + case '/dashboard': + await renderDashboard(app); + break; + default: + navigateTo('/kitchen'); + break; + } +} + +window.addEventListener('popstate', router); +router(); diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..7269533 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,1027 @@ +:root { + --bg-primary: #0f111a; + --bg-secondary: #1a1d2d; + --bg-glass: rgba(26, 29, 45, 0.7); + --accent-primary: #ff3366; + --accent-hover: #ff4d79; + --text-primary: #f8f8f2; + --text-secondary: #a9acb6; + --border-color: rgba(255, 255, 255, 0.08); + --success: #00e676; + --warning: #ffb300; + --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5); + + --font-main: 'Inter', sans-serif; + --radius-md: 12px; + --radius-lg: 20px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-main); + background-color: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: hidden; + background-image: + radial-gradient(circle at 15% 50%, rgba(255, 51, 102, 0.05), transparent 25%), + radial-gradient(circle at 85% 30%, rgba(0, 230, 118, 0.05), transparent 25%); +} + +#app { + width: 100%; + flex: 1; + display: flex; + flex-direction: column; +} + +/* Glassmorphism Container */ +.glass-panel { + background: var(--bg-glass); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: 2rem; +} + +/* Typography */ +h1, h2, h3 { + font-weight: 600; + margin-bottom: 1rem; +} + +h1 { + font-size: 2.5rem; + color: #f8f8f2; + /* background: linear-gradient(90deg, #fff, var(--text-secondary)); */ + -webkit-background-clip: text; + /* -webkit-text-fill-color: transparent; */ +} + +/* Buttons */ +.btn { + background-color: var(--accent-primary); + color: #fff; + border: none; + border-radius: var(--radius-md); + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.btn:hover { + background-color: var(--accent-hover); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(255, 51, 102, 0.4); +} + +.btn:active { + transform: translateY(0); +} + +.btn-secondary { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-primary); +} + +.btn-secondary:hover { + background-color: rgba(255, 255, 255, 0.05); + box-shadow: none; +} + +/* Inputs */ +.form-group { + margin-bottom: 1.5rem; + width: 100%; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.form-control { + width: 100%; + background-color: rgba(0, 0, 0, 0.2); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--radius-md); + padding: 0.75rem 1rem; + font-family: var(--font-main); + font-size: 1rem; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.form-control:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(255, 51, 102, 0.2); +} + +/* Navbar */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + background: var(--bg-glass); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-color); +} + +.nav-links { + display: flex; + gap: 1.5rem; +} + +.nav-link { + color: var(--text-secondary); + text-decoration: none; + font-weight: 500; + transition: color 0.3s ease; + cursor: pointer; +} + +.nav-link:hover, .nav-link.active { + color: var(--accent-primary); +} + +/* Layouts */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + width: 100%; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +/* Cards */ +.card { + background: var(--bg-secondary); + border-radius: var(--radius-md); + padding: 1.5rem; + border: 1px solid var(--border-color); + transition: transform 0.3s ease, box-shadow 0.3s ease; + position: relative; + overflow: hidden; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); + border-color: rgba(255, 255, 255, 0.15); +} + +.card-title { + font-size: 1.25rem; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.card-subtitle { + color: var(--text-secondary); + font-size: 0.875rem; + margin-bottom: 1rem; +} + +/* Badges */ +.badge { + padding: 0.25rem 0.75rem; + border-radius: 50px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.badge-warning { + background: rgba(255, 179, 0, 0.15); + color: var(--warning); +} + +.badge-success { + background: rgba(0, 230, 118, 0.15); + color: var(--success); +} + +.badge-info { + background: rgba(0, 176, 255, 0.15); + color: #00b0ff; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fade-in { + animation: fadeIn 0.4s ease forwards; +} + +/* Login View Specific */ +.login-view { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: 100%; +} + +.login-box { + width: 100%; + max-width: 400px; +} + +.error-message { + color: var(--accent-primary); + font-size: 0.875rem; + margin-top: 0.5rem; + display: none; +} + +/* ─── Tabs ─────────────────────────────────────────────── */ +.tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0; +} + +.tab-btn { + background: transparent; + border: none; + color: var(--text-secondary); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 500; + padding: 0.75rem 1.5rem; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: color 0.2s, border-color 0.2s; + margin-bottom: -1px; +} + +.tab-btn:hover { color: var(--text-primary); } +.tab-btn.active { + color: var(--accent-primary); + border-bottom-color: var(--accent-primary); +} + +/* ─── Tables ────────────────────────────────────────────── */ +.table-wrap { + overflow-x: auto; + border-radius: var(--radius-md); + border: 1px solid var(--border-color); +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +thead th { + background: rgba(0,0,0,0.3); + color: var(--text-secondary); + font-weight: 500; + padding: 0.875rem 1.25rem; + text-align: left; + letter-spacing: 0.5px; + text-transform: uppercase; + font-size: 0.75rem; +} + +tbody tr { + border-top: 1px solid var(--border-color); + transition: background 0.2s; +} + +tbody tr:hover { background: rgba(255,255,255,0.03); } + +tbody td { + padding: 1rem 1.25rem; + color: var(--text-primary); +} + +/* ─── Modal ─────────────────────────────────────────────── */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + animation: fadeIn 0.2s ease; +} + +.modal { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 2rem; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0,0,0,0.6); + animation: fadeIn 0.25s ease; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.modal-header h3 { margin: 0; font-size: 1.25rem; } + +.btn-icon { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1.5rem; + line-height: 1; + padding: 0.25rem; + transition: color 0.2s; +} +.btn-icon:hover { color: var(--accent-primary); } + +/* ─── Detalhes Comanda - Collapsible Edit ─────────────── */ +.detalhes-summary { + display: none; +} +.detalhes-edit-toggle { + display: none; +} + +/* ─── Toast ─────────────────────────────────────────────── */ +.toast-container { + position: fixed; + bottom: 2rem; + right: 2rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + z-index: 200; +} + +.toast { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-left: 4px solid var(--accent-primary); + border-radius: var(--radius-md); + padding: 1rem 1.5rem; + font-size: 0.9rem; + box-shadow: var(--shadow-lg); + animation: slideIn 0.3s ease; + min-width: 260px; +} + +.toast.success { border-left-color: var(--success); } +.toast.error { border-left-color: var(--accent-primary); } + +@keyframes slideIn { + from { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); } +} + +/* ─── Checkout / Resumo ──────────────────────────────────── */ +.total-bar { + background: rgba(255, 51, 102, 0.08); + border: 1px solid rgba(255, 51, 102, 0.2); + border-radius: var(--radius-md); + padding: 1.25rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1.5rem; +} + +.total-value { + font-size: 1.75rem; + font-weight: 700; + color: var(--success); +} + +/* ─── Role Badges ────────────────────────────────────────── */ +.badge-admin { background: rgba(255,51,102,0.15); color: #ff3366; } +.badge-caixa { background: rgba(100,108,255,0.15); color: #646cff; } +.badge-garcom { background: rgba(0,176,255,0.15); color: #00b0ff; } +.badge-cozinha { background: rgba(255,179,0,0.15); color: #ffb300; } +.badge-barman { background: rgba(0,230,118,0.15); color: #00e676; } + +/* ─── Status Badges ──────────────────────────────────────── */ +.status-aberta { background:rgba(0,176,255,0.15); color:#00b0ff; } +.status-paga { background:rgba(0,230,118,0.15); color:#00e676; } +.status-cancelada { background:rgba(255,51,102,0.15); color:#ff3366; } +.status-fechada { background:rgba(169,172,182,0.15);color:#a9acb6; } + +/* ─── Page header ────────────────────────────────────────── */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + flex-wrap: wrap; + gap: 1rem; +} + +/* Comandas search bar */ +.comandas-search-bar { + display: flex; + align-items: flex-end; + gap: 0.75rem; + margin-bottom: 1.5rem; + padding: 0.75rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); +} +.comandas-search-field { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + min-width: 130px; +} +.comandas-search-field label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; +} +.comandas-search-field .form-control { + font-size: 0.85rem; + padding: 0.4rem 0.6rem; +} +.comandas-search-actions { + display: flex; + gap: 0.5rem; + align-items: flex-end; +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.btn-danger { + background-color: rgba(255,51,102,0.15); + color: var(--accent-primary); + border: 1px solid rgba(255,51,102,0.3); +} +.btn-danger:hover { + background-color: var(--accent-primary); + color: #fff; + box-shadow: 0 4px 15px rgba(255,51,102,0.4); +} + +.btn-success { + background-color: rgba(0,230,118,0.15); + color: var(--success); + border: 1px solid rgba(0,230,118,0.3); +} +.btn-success:hover { + background-color: var(--success); + color: #000; + box-shadow: 0 4px 15px rgba(0,230,118,0.4); +} + +/* ═══════════════════════════════════════════════════════════ + RESPONSIVIDADE MOBILE + ══════════════════════════════════════════════════════════ */ + +/* ─── Hamburger Menu ────────────────────────────────────── */ +.hamburger { + display: none; + flex-direction: column; + gap: 5px; + cursor: pointer; + background: transparent; + border: none; + padding: 0.5rem; +} + +.hamburger span { + display: block; + width: 24px; + height: 2px; + background: var(--text-primary); + border-radius: 2px; + transition: all 0.3s ease; +} + +.hamburger.open span:nth-child(1) { + transform: translateY(7px) rotate(45deg); +} +.hamburger.open span:nth-child(2) { + opacity: 0; +} +.hamburger.open span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); +} + +/* Drawer mobile */ +.nav-drawer { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.6); + backdrop-filter: blur(4px); + z-index: 50; + animation: fadeIn 0.2s ease; +} + +.nav-drawer-panel { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 280px; + background: var(--bg-secondary); + border-left: 1px solid var(--border-color); + padding: 2rem 1.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + animation: slideInRight 0.25s ease; +} + +@keyframes slideInRight { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} + +.nav-drawer.open { display: block; } + +.nav-drawer .nav-link { + display: block; + padding: 0.875rem 1rem; + border-radius: var(--radius-md); + font-size: 1rem; + transition: background 0.2s, color 0.2s; +} + +.nav-drawer .nav-link:hover, +.nav-drawer .nav-link.active { + background: rgba(255, 51, 102, 0.1); + color: var(--accent-primary); +} + +.nav-drawer-user { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-color); +} + +/* ─── Media Queries ─────────────────────────────────────── */ +@media (max-width: 768px) { + + /* Navbar */ + .navbar { + padding: 1rem 1.25rem; + position: sticky; + top: 0; + z-index: 40; + } + + .nav-links { display: none; } + .hamburger { display: flex; } + + /* Container */ + .container { + padding: 1.25rem; + } + + /* Page Header */ + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + h1 { font-size: 1.75rem; } + + /* Grid → single col */ + .grid { + grid-template-columns: 1fr; + } + + /* Glass Panel */ + .glass-panel { + padding: 1.25rem; + } + + /* Comandas search bar → stack buttons */ + .comandas-search-bar { + flex-wrap: wrap; + } + .comandas-search-actions { + flex-direction: column; + width: 100%; + } + .comandas-search-actions .btn { + width: 100%; + } + + /* Tabs — scroll horizontal */ + .tabs { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + flex-wrap: nowrap; + padding-bottom: 0; + } + .tabs::-webkit-scrollbar { display: none; } + + .tab-btn { + white-space: nowrap; + padding: 0.75rem 1rem; + } + + /* Tabelas → cards empilhados */ + .table-wrap { + border: none; + border-radius: 0; + background: transparent; + } + + table, thead, tbody, th, td, tr { + display: block; + } + + thead tr { + position: absolute; + top: -9999px; + left: -9999px; + } + + tbody tr { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + margin-bottom: 1rem; + padding: 1rem; + } + + tbody td { + padding: 0.35rem 0; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + } + + tbody td::before { + content: attr(data-label); + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 100px; + flex-shrink: 0; + } + + /* Comandas — first row: ident + client + status inline */ + tbody tr:has(.cmd-ident) { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 0.35rem 0.5rem; + padding: 0.75rem; + } + tbody tr:has(.cmd-ident) td { + padding: 0.15rem 0; + } + tbody tr:has(.cmd-ident) td::before { display: none; } + .cmd-ident { + font-size: 0.95rem; + } + .cmd-client { + color: var(--text-secondary); + font-size: 0.8rem; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .cmd-status { + padding-bottom: 0 !important; + border-bottom: none; + margin-bottom: 0; + } + .cmd-total { + font-size: 0.9rem; + justify-content: flex-start !important; + padding-top: 0.35rem !important; + border-top: 1px solid var(--border-color); + } + .cmd-data { + font-size: 0.75rem; + color: var(--text-secondary); + justify-content: flex-end !important; + } + .cmd-acoes { + grid-column: 1 / -1; + display: flex !important; + gap: 0.5rem; + padding-top: 0.35rem !important; + border-top: 1px solid var(--border-color); + } + .cmd-acoes .btn { flex: 1; } + + /* Detalhes Comanda — compact 3-line item cards */ + .modal h4 { margin-bottom: 0.5rem; font-size: 0.9rem; } + #detalhes-itens-tbody tr { + padding: 0.6rem 0.75rem; + margin-bottom: 0.5rem; + gap: 0; + display: grid; + grid-template-columns: 1fr auto; + } + #detalhes-itens-tbody td { + font-size: 0.8rem; + padding: 0.15rem 0; + } + #detalhes-itens-tbody td::before { display: none; } + + /* Line 1 — name spans full */ + #detalhes-itens-tbody td.item-name { + grid-column: 1 / -1; + font-weight: 600; + font-size: 0.875rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + #detalhes-itens-tbody td.item-qty { display: none; } + + /* Line 2 — value + obs */ + #detalhes-itens-tbody td.item-value { + font-weight: 600; + } + #detalhes-itens-tbody td.item-obs { + color: var(--text-secondary); + font-style: italic; + font-size: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + } + + /* Line 3 — status + actions */ + #detalhes-itens-tbody td.item-status { text-align: left; } + #detalhes-itens-tbody td.item-actions { text-align: right; } + #detalhes-itens-tbody td.item-actions .btn { + padding: 0.2rem 0.5rem; + font-size: 0.75rem; + } + #modal-detalhes-comanda .table-wrap { max-height: 250px !important; } + + /* Modal → full screen bottom sheet */ + .modal-overlay { + align-items: flex-end; + padding: 0; + } + + .modal { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); + max-width: 100% !important; + width: 100%; + max-height: 90vh; + overflow-y: auto; + animation: slideUp 0.3s ease; + } + + @keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } + } + + /* Detalhes Comanda - collapsible edit on mobile */ + .detalhes-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + flex-wrap: wrap; + padding: 0.75rem 1rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + margin-bottom: 0.75rem; + } + .detalhes-summary-row { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; + } + .detalhes-summary-label { + font-weight: 700; + font-size: 1rem; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .detalhes-summary-client { + font-size: 0.85rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .detalhes-edit-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + margin-bottom: 0.75rem; + transition: background 0.2s; + } + .detalhes-edit-toggle:hover { background: rgba(255,255,255,0.05); } + .detalhes-edit-toggle .detalhes-edit-arrow { + transition: transform 0.2s; + font-size: 0.8rem; + } + .detalhes-edit-section.expanded .detalhes-edit-toggle .detalhes-edit-arrow { + transform: rotate(180deg); + } + .detalhes-edit-body { + display: none; + animation: fadeIn 0.2s ease; + } + .detalhes-edit-section.expanded .detalhes-edit-body { + display: block; + } + + /* Cards da cozinha */ + .card { + margin-bottom: 0; + } + + /* Filtros da cozinha */ + #items-grid + div, + .page-header > div { + flex-wrap: wrap; + width: 100%; + } + + /* Botões lado a lado em tabelas mobile */ + td > button + button { + margin-left: 0.5rem; + } + + /* Total bar checkout */ + .total-bar { + flex-direction: column; + gap: 0.5rem; + text-align: center; + } + + /* Login */ + .login-box { + max-width: 100%; + margin: 1rem; + } + + /* Form grid de produtos */ + form > div[style*="grid"] { + grid-template-columns: 1fr !important; + } + + /* Toast position */ + .toast-container { + bottom: 1rem; + right: 1rem; + left: 1rem; + } + + .toast { + min-width: unset; + width: 100%; + } +} + +/* ─── Mini Cards de Produtos no Modal ───────────────────── */ +.product-mini-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + max-height: 240px; + overflow-y: auto; + padding: 0.25rem; + margin-top: 0.5rem; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: rgba(0, 0, 0, 0.15); +} + +.product-mini-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: left; +} + +.product-mini-card:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); +} + +.product-mini-card.selected { + background: rgba(255, 51, 102, 0.15); + border-color: var(--accent-primary); + box-shadow: 0 0 8px rgba(255, 51, 102, 0.3); +} + +.product-mini-card-name { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.product-mini-card-price { + font-size: 0.8rem; + color: var(--success); + font-weight: 600; +} + +.product-mini-card-badge { + font-size: 0.65rem; + color: var(--text-secondary); + margin-top: 0.25rem; +} + + +@media (max-width: 480px) { + h1 { font-size: 1.5rem; } + + .btn { + padding: 0.625rem 1rem; + font-size: 0.875rem; + } + + .navbar { + padding: 0.875rem 1rem; + } + + .container { + padding: 1rem; + } +} diff --git a/web/src/utils.ts b/web/src/utils.ts new file mode 100644 index 0000000..154361d --- /dev/null +++ b/web/src/utils.ts @@ -0,0 +1,136 @@ +/** Exibe um toast de notificação na tela */ +export function toast(message: string, type: 'success' | 'error' = 'success') { + let container = document.querySelector('.toast-container'); + if (!container) { + container = document.createElement('div'); + container.className = 'toast-container'; + document.body.appendChild(container); + } + + const el = document.createElement('div'); + el.className = `toast ${type}`; + el.textContent = message; + container.appendChild(el); + + setTimeout(() => el.remove(), 3500); +} + +/** Retorna o usuário logado do localStorage */ +export function getUser(): { id: string; nome: string; email: string; role: string } | null { + const raw = localStorage.getItem('@gastrobar:user'); + return raw ? JSON.parse(raw) : null; +} + +/** Formata valor em R$ */ +export function formatBRL(value: number): string { + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); +} + +/** Badges de role */ +const roleLabels: Record = { + ADMIN: 'Admin', CAIXA: 'Caixa', GARCOM: 'Garçom', COZINHA: 'Cozinha', BARMAN: 'Barman', +}; +export function roleBadge(role: string) { + const cls = `badge badge-${role.toLowerCase()}`; + return `${roleLabels[role] ?? role}`; +} + +/** Badges de status de comanda */ +const statusLabels: Record = { + ABERTA: 'Aberta', FECHADA: 'Fechada', PAGA: 'Paga', CANCELADA: 'Cancelada', +}; +export function statusBadge(status: string) { + const cls = `badge status-${status.toLowerCase()}`; + return `${statusLabels[status] ?? status}`; +} + +/** Navbar padrão com hamburger para mobile */ +export function renderNavbar(activePath: string): string { + const user = getUser(); + const role = user?.role ?? ''; + + const links = [ + { path: '/kitchen', label: '👨‍🍳 Cozinha/Bar', roles: ['ADMIN', 'COZINHA', 'BARMAN'] }, + { path: '/orders', label: '📋 Comandas', roles: ['ADMIN', 'GARCOM', 'CAIXA'] }, + { path: '/checkout',label: '💳 Caixa', roles: ['ADMIN', 'CAIXA'] }, + { path: '/menu', label: '🍔 Cardápio', roles: ['ADMIN'] }, + { path: '/users', label: '👥 Equipe', roles: ['ADMIN'] }, + { path: '/dashboard', label: '📊 Dashboard', roles: ['ADMIN'] }, + ]; + + const visibleLinks = links.filter(l => l.roles.includes(role)); + + const desktopLinks = visibleLinks.map(l => ` + ${l.label} + `).join(''); + + const drawerLinks = visibleLinks.map(l => ` + ${l.label} + `).join(''); + + // Agendar bind do hamburger após o DOM ser inserido + setTimeout(() => { + const btn = document.getElementById('hamburger-btn'); + const drawer = document.getElementById('nav-drawer'); + const overlay = document.getElementById('drawer-overlay'); + + btn?.addEventListener('click', () => { + btn.classList.toggle('open'); + drawer?.classList.toggle('open'); + }); + overlay?.addEventListener('click', () => { + btn?.classList.remove('open'); + drawer?.classList.remove('open'); + }); + }, 0); + + (window as any).closeDrawer = () => { + document.getElementById('hamburger-btn')?.classList.remove('open'); + document.getElementById('nav-drawer')?.classList.remove('open'); + }; + + return ` + + + + + `; +} diff --git a/web/src/views/checkout.ts b/web/src/views/checkout.ts new file mode 100644 index 0000000..75c6ff3 --- /dev/null +++ b/web/src/views/checkout.ts @@ -0,0 +1,175 @@ +import { api } from '../api'; +import { renderNavbar, toast, formatBRL } from '../utils'; + +const FORMAS_PAGAMENTO = [ + { value: 'DINHEIRO', label: '💵 Dinheiro' }, + { value: 'CARTAO_CREDITO', label: '💳 Cartão Crédito' }, + { value: 'CARTAO_DEBITO', label: '💳 Cartão Débito' }, + { value: 'PIX', label: '📱 PIX' }, +]; + +export async function renderCheckout(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/checkout')} +
+ + +
+

Buscar Comanda

+
+ + +
+
+ +
+
+ `; + + document.getElementById('btn-buscar')?.addEventListener('click', searchComanda); + document.getElementById('search-input')?.addEventListener('keydown', (e) => { + if ((e as KeyboardEvent).key === 'Enter') searchComanda(); + }); +} + +function calcularTotalFinal(c: any): number { + let total = c.total || 0; + total -= (c.desconto || 0); + total += (c.acrescimo || 0); + if (c.taxaServico) total += total * 0.10; + return Math.max(total, 0); +} + +async function searchComanda() { + const query = (document.getElementById('search-input') as HTMLInputElement).value.trim(); + const result = document.getElementById('checkout-result')!; + + if (!query) { toast('Digite um identificador para buscar', 'error'); return; } + + result.innerHTML = `

Buscando...

`; + try { + const all = await api.get('/comandas'); + const found: any[] = all.filter((c: any) => + c.identificador.toLowerCase().includes(query.toLowerCase()) && c.status === 'ABERTA' + ); + + if (!found.length) { + result.innerHTML = `

Nenhuma comanda ABERTA encontrada para "${query}".

`; + return; + } + + result.innerHTML = found.map((c: any) => buildComandaCard(c)).join(''); + + document.querySelectorAll('.btn-pay').forEach(btn => { + btn.addEventListener('click', async () => { + const id = (btn as HTMLElement).dataset.id!; + const ident = (btn as HTMLElement).dataset.ident!; + const total = parseFloat((btn as HTMLElement).dataset.total!); + const forma = (document.getElementById(`pagamento-${id}`) as HTMLSelectElement)?.value; + + if (!forma) { + toast('Selecione a forma de pagamento', 'error'); + return; + } + + if (!confirm(`Confirmar pagamento de ${formatBRL(total)} para "${ident}" via ${forma.replace('_', ' ')}?`)) return; + try { + await api.post(`/comandas/${id}/pagar`, { formaPagamento: forma }); + toast(`Comanda "${ident}" fechada com sucesso!`, 'success'); + result.innerHTML = ` +
+

+

Pagamento Realizado!

+

Comanda "${ident}" foi fechada.

+
`; + } catch (err: any) { + toast(err.message, 'error'); + } + }); + }); + } catch (err: any) { + result.innerHTML = `

Erro: ${err.message}

`; + } +} + +function buildComandaCard(c: any): string { + const itens = c.itens ?? []; + const itensHtml = itens.length + ? itens.map((i: any) => ` + + ${i.produto?.nome ?? '—'} + ${i.quantidade} + ${formatBRL(i.precoUnitario)} + ${formatBRL(i.precoUnitario * i.quantidade)} + ${i.status} + + `).join('') + : `Nenhum item ainda`; + + const totalFinal = calcularTotalFinal(c); + const desconto = c.desconto || 0; + const acrescimo = c.acrescimo || 0; + + return ` +
+ + +
+ + + + + + + + + + + ${itensHtml} +
ProdutoQtdUnitárioSubtotalStatus
+
+ +
+
+ Subtotal: + ${formatBRL(c.total || 0)} +
+ ${desconto > 0 ? `
+ Desconto:-${formatBRL(desconto)} +
` : ''} + ${acrescimo > 0 ? `
+ Acréscimo:+${formatBRL(acrescimo)} +
` : ''} + ${c.taxaServico ? `
+ Taxa Serviço (10%):+${formatBRL((c.total - desconto + acrescimo) * 0.10)} +
` : ''} +
+ +
+ Total a Pagar + ${formatBRL(totalFinal)} +
+ + ${c.status === 'ABERTA' ? ` +
+ + +
+ + ` : ''} +
+ `; +} diff --git a/web/src/views/dashboard.ts b/web/src/views/dashboard.ts new file mode 100644 index 0000000..aca24ac --- /dev/null +++ b/web/src/views/dashboard.ts @@ -0,0 +1,200 @@ +import { api } from '../api'; +import { renderNavbar, toast, formatBRL } from '../utils'; + +export async function renderDashboard(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/dashboard')} +
+ + + +
+
+

Vendas Hoje

+

R$ 0,00

+

0 comandas

+
+
+

Vendas Semana

+

R$ 0,00

+

0 comandas

+
+
+

Comandas Abertas

+

0

+
+
+

Estoque Baixo

+

0

+

0 produtos ativos

+
+
+ + +
+ + +
+
+ +
+ +
+ +
+
+
+ `; + + // 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 = ` +
+

Total Vendido

+

${formatBRL(data.resumo.totalVendas)}

+
+
+

Comandas

+

${data.resumo.totalComandas}

+
+
+

Ticket Médio

+

${formatBRL(data.resumo.ticketMedio)}

+
+
+

Descontos

+

${formatBRL(data.resumo.totalDescontos)}

+
+
+

Acréscimos

+

${formatBRL(data.resumo.totalAcrescimos)}

+
+ `; + + // Top produtos + if (data.topProdutos.length > 0) { + document.getElementById('relatorio-top-produtos')!.innerHTML = ` +

🏆 Mais Vendidos

+
+ + + + ${data.topProdutos.map((p: any, i: number) => ` + + + + + + + `).join('')} + +
#ProdutoQtdTotal
${i + 1}º${p.nome}${p.quantidade}${formatBRL(p.total)}
+
+ `; + } else { + document.getElementById('relatorio-top-produtos')!.innerHTML = ''; + } + + // Vendas por dia + if (data.vendasPorDia.length > 0) { + document.getElementById('relatorio-vendas-dia')!.innerHTML = ` +

📅 Vendas por Dia

+
+ + + + ${data.vendasPorDia.map((d: any) => ` + + + + + + `).join('')} + +
DataComandasTotal
${new Date(d.data + 'T12:00:00').toLocaleDateString('pt-BR')}${d.comandas}${formatBRL(d.vendas)}
+
+ `; + } else { + document.getElementById('relatorio-vendas-dia')!.innerHTML = ''; + } + + // Vendas por pagamento + if (data.vendasPorPagamento.length > 0) { + const pagLabels: Record = { + 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 = ` +

💰 Por Forma de Pagamento

+
+ + + + ${data.vendasPorPagamento.map((p: any) => ` + + + + + `).join('')} + +
FormaTotal
${pagLabels[p.forma] ?? p.forma}${formatBRL(p.total)}
+
+ `; + } 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()]); +} diff --git a/web/src/views/kitchen.ts b/web/src/views/kitchen.ts new file mode 100644 index 0000000..071dd4c --- /dev/null +++ b/web/src/views/kitchen.ts @@ -0,0 +1,181 @@ +import { api } from '../api'; +import { renderNavbar, toast } from '../utils'; + +let previousItemIds: Set = new Set(); +let audioCtx: AudioContext | null = null; + +function playNotificationSound() { + try { + if (!audioCtx) audioCtx = new AudioContext(); + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.connect(gain); + gain.connect(audioCtx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(800, audioCtx.currentTime); + osc.frequency.setValueAtTime(1000, audioCtx.currentTime + 0.1); + osc.frequency.setValueAtTime(800, audioCtx.currentTime + 0.2); + gain.gain.setValueAtTime(0.3, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4); + osc.start(audioCtx.currentTime); + osc.stop(audioCtx.currentTime + 0.4); + } catch (e) { + // Audio not supported, silently fail + } +} + +export async function renderKitchen(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/kitchen')} +
+ + +
+ + + + +
+ +
+

Carregando pedidos...

+
+
+ `; + + let currentStatus = 'PENDENTE'; + let currentFilter = ''; + let autoRefreshInterval: ReturnType | null = null; + + const loadItems = async () => { + const grid = document.getElementById('items-grid')!; + grid.innerHTML = '

Carregando...

'; + try { + let url = `/painel/itens?status=${currentStatus}`; + if (currentFilter !== '') url += `&itemCozinha=${currentFilter}`; + const items = await api.get(url); + + const currentIds = new Set(items.map((i: any) => i.id)); + + if (previousItemIds.size > 0) { + const newItems = items.filter((i: any) => !previousItemIds.has(i.id)); + if (newItems.length > 0) { + playNotificationSound(); + toast(`${newItems.length} novo(s) pedido(s) recebido(s)!`, 'success'); + } + } + previousItemIds = currentIds; + + if (!items.length) { + grid.innerHTML = `

+ Nenhum pedido com status ${currentStatus} 🎉 +

`; + return; + } + + const statusActions: Record = { + PENDENTE: { label: '▶ Iniciar Preparo', next: 'PREPARANDO' }, + PREPARANDO: { label: '✔ Marcar Pronto', next: 'PRONTO' }, + PRONTO: { label: '🚀 Marcar Entregue', next: 'ENTREGUE' }, + ENTREGUE: { label: '', next: '' }, + }; + + grid.innerHTML = items.map((item: any) => { + const action = statusActions[item.status]; + return ` +
+
+ + ${item.produto.itemCozinha ? '🍳 Cozinha' : '🍹 Bar'} + + Mesa: ${item.comanda.identificador} +
+

${item.quantidade}× ${item.produto.nome}

+ ${item.observacao ? `

⚠️ ${item.observacao}

` : ''} +

+ Pedido às ${new Date(item.criadoEm).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +

+ ${action.label ? ` + + ` : `

✅ Entregue

`} +
+ `; + }).join(''); + } catch (err: any) { + grid.innerHTML = `

Erro: ${err.message}

`; + } + }; + + const startAutoRefresh = () => { + stopAutoRefresh(); + autoRefreshInterval = setInterval(loadItems, 10000); + }; + + const stopAutoRefresh = () => { + if (autoRefreshInterval) { + clearInterval(autoRefreshInterval); + autoRefreshInterval = null; + } + }; + + // Tab switching + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentStatus = (btn as HTMLElement).dataset.status!; + loadItems(); + }); + }); + + // Filter buttons + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentFilter = (btn as HTMLElement).dataset.filter!; + loadItems(); + }); + }); + + document.getElementById('btn-refresh')?.addEventListener('click', loadItems); + + document.getElementById('auto-refresh-toggle')?.addEventListener('change', (e) => { + if ((e.target as HTMLInputElement).checked) startAutoRefresh(); + else stopAutoRefresh(); + }); + + (window as any).advanceStatus = async (id: string, status: string) => { + try { + await api.patch(`/painel/itens/${id}/status`, { status }); + toast('Status atualizado!', 'success'); + loadItems(); + } catch (err: any) { + toast(err.message, 'error'); + } + }; + + // Cleanup on navigation + const cleanup = () => { + stopAutoRefresh(); + previousItemIds.clear(); + }; + window.addEventListener('popstate', cleanup); + + await loadItems(); + startAutoRefresh(); +} diff --git a/web/src/views/login.ts b/web/src/views/login.ts new file mode 100644 index 0000000..03a3058 --- /dev/null +++ b/web/src/views/login.ts @@ -0,0 +1,58 @@ +import { api } from '../api'; +import { navigateTo } from '../main'; + +export function renderLogin(container: HTMLElement) { + container.innerHTML = ` + + `; + + const form = document.getElementById('login-form'); + const errorEl = document.getElementById('login-error'); + + form?.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = (document.getElementById('email') as HTMLInputElement).value; + const senha = (document.getElementById('password') as HTMLInputElement).value; + + if (errorEl) errorEl.style.display = 'none'; + + try { + const data = await api.post('/auth/login', { email, senha }); + localStorage.setItem('@gastrobar:token', data.token); + localStorage.setItem('@gastrobar:user', JSON.stringify(data.user)); + navigateTo('/kitchen'); + } catch (err: any) { + if (errorEl) { + errorEl.textContent = err.message; + errorEl.style.display = 'block'; + } + } + }); +} diff --git a/web/src/views/menu.ts b/web/src/views/menu.ts new file mode 100644 index 0000000..1007e73 --- /dev/null +++ b/web/src/views/menu.ts @@ -0,0 +1,269 @@ +import { api } from '../api'; +import { renderNavbar, toast, formatBRL } from '../utils'; + +export async function renderMenu(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/menu')} +
+ +
+ + +
+
+
+ + + + + + `; + + let categorias: any[] = []; + + const loadCategorias = async () => { + categorias = await api.get('/categorias'); + const sel = document.getElementById('p-categoria') as HTMLSelectElement; + if (sel) sel.innerHTML = categorias.map((c: any) => ``).join(''); + }; + + const renderTabProdutos = async () => { + const tc = document.getElementById('tab-content')!; + const products = await api.get('/produtos'); + tc.innerHTML = ` + +
+ + + + + + ${products.map((p: any) => ` + + + + + + + + + + `).join('')} + +
NomeCategoriaPreçoEstoqueTipoStatusAções
${p.nome}${p.descricao ? `
${p.descricao}` : ''}
${p.categoria?.nome ?? '—'}${formatBRL(p.preco)} + + ${p.estoqueAtual} ${p.estoqueAtual <= p.estoqueMinimo ? '⚠️' : ''} + + ${p.itemCozinha ? 'Cozinha' : 'Bar'}${p.ativo ? 'Ativo' : 'Inativo'} + + +
+
+ `; + + document.getElementById('btn-novo-produto')?.addEventListener('click', () => { + clearProductForm(); + (document.getElementById('modal-produto') as HTMLElement).style.display = 'flex'; + }); + }; + + const renderTabCategorias = async () => { + const tc = document.getElementById('tab-content')!; + const cats = await api.get('/categorias'); + tc.innerHTML = ` + +
+ + + + ${cats.map((c: any) => ` + + + + + + `).join('')} + +
NomeNº ProdutosAções
${c.nome}${c.produtos?.length ?? 0} + +
+
+ `; + document.getElementById('btn-nova-cat')?.addEventListener('click', () => { + (document.getElementById('modal-categoria') as HTMLElement).style.display = 'flex'; + }); + }; + + // Tabs + document.getElementById('tab-produtos')?.addEventListener('click', async () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.getElementById('tab-produtos')?.classList.add('active'); + await renderTabProdutos(); + }); + document.getElementById('tab-categorias')?.addEventListener('click', async () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.getElementById('tab-categorias')?.classList.add('active'); + await renderTabCategorias(); + }); + + // Form Produto + document.getElementById('form-produto')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const id = (document.getElementById('produto-id') as HTMLInputElement).value; + const payload: any = { + nome: (document.getElementById('p-nome') as HTMLInputElement).value, + categoriaId: (document.getElementById('p-categoria') as HTMLSelectElement).value, + preco: parseFloat((document.getElementById('p-preco') as HTMLInputElement).value), + custo: parseFloat((document.getElementById('p-custo') as HTMLInputElement).value) || undefined, + estoqueAtual: parseInt((document.getElementById('p-estoque') as HTMLInputElement).value), + estoqueMinimo: parseInt((document.getElementById('p-estoque-min') as HTMLInputElement).value), + descricao: (document.getElementById('p-desc') as HTMLInputElement).value || undefined, + itemCozinha: (document.getElementById('p-cozinha') as HTMLInputElement).checked, + ativo: (document.getElementById('p-ativo') as HTMLInputElement).checked, + }; + try { + if (id) await api.patch(`/produtos/${id}`, payload); + else await api.post('/produtos', payload); + toast(id ? 'Produto atualizado!' : 'Produto cadastrado!', 'success'); + (document.getElementById('modal-produto') as HTMLElement).style.display = 'none'; + await renderTabProdutos(); + } catch (err: any) { toast(err.message, 'error'); } + }); + + // Form Categoria + document.getElementById('form-categoria')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const nome = (document.getElementById('cat-nome') as HTMLInputElement).value; + try { + await api.post('/categorias', { nome }); + toast('Categoria criada!', 'success'); + (document.getElementById('modal-categoria') as HTMLElement).style.display = 'none'; + await loadCategorias(); + await renderTabCategorias(); + } catch (err: any) { toast(err.message, 'error'); } + }); + + // Global handlers + (window as any).editProduct = (p: any) => { + (document.getElementById('produto-id') as HTMLInputElement).value = p.id; + (document.getElementById('p-nome') as HTMLInputElement).value = p.nome; + (document.getElementById('p-preco') as HTMLInputElement).value = p.preco; + (document.getElementById('p-custo') as HTMLInputElement).value = p.custo ?? ''; + (document.getElementById('p-estoque') as HTMLInputElement).value = p.estoqueAtual; + (document.getElementById('p-estoque-min') as HTMLInputElement).value = p.estoqueMinimo; + (document.getElementById('p-desc') as HTMLInputElement).value = p.descricao ?? ''; + (document.getElementById('p-cozinha') as HTMLInputElement).checked = p.itemCozinha; + (document.getElementById('p-ativo') as HTMLInputElement).checked = p.ativo; + const sel = document.getElementById('p-categoria') as HTMLSelectElement; + if (sel) sel.value = p.categoriaId; + (document.getElementById('modal-produto-title') as HTMLElement).textContent = 'Editar Produto'; + (document.getElementById('modal-produto') as HTMLElement).style.display = 'flex'; + }; + + (window as any).deleteProduct = async (id: string, nome: string) => { + if (!confirm(`Excluir o produto "${nome}"?`)) return; + try { + await api.patch(`/produtos/${id}`, { ativo: false }); + toast('Produto desativado!', 'success'); + await renderTabProdutos(); + } catch (err: any) { toast(err.message, 'error'); } + }; + + (window as any).deleteCategory = async (id: string, nome: string) => { + if (!confirm(`Excluir a categoria "${nome}"?`)) return; + try { + await api.delete(`/categorias/${id}`); + toast('Categoria excluída!', 'success'); + await renderTabCategorias(); + } catch (err: any) { toast(err.message, 'error'); } + }; + + await loadCategorias(); + await renderTabProdutos(); +} + +function clearProductForm() { + (document.getElementById('produto-id') as HTMLInputElement).value = ''; + (document.getElementById('p-nome') as HTMLInputElement).value = ''; + (document.getElementById('p-preco') as HTMLInputElement).value = ''; + (document.getElementById('p-custo') as HTMLInputElement).value = ''; + (document.getElementById('p-estoque') as HTMLInputElement).value = '0'; + (document.getElementById('p-estoque-min') as HTMLInputElement).value = '0'; + (document.getElementById('p-desc') as HTMLInputElement).value = ''; + (document.getElementById('p-cozinha') as HTMLInputElement).checked = false; + (document.getElementById('p-ativo') as HTMLInputElement).checked = true; + (document.getElementById('modal-produto-title') as HTMLElement).textContent = 'Novo Produto'; +} diff --git a/web/src/views/orders.ts b/web/src/views/orders.ts new file mode 100644 index 0000000..817724d --- /dev/null +++ b/web/src/views/orders.ts @@ -0,0 +1,583 @@ +import { api } from '../api'; +import { renderNavbar, toast, formatBRL, getUser, statusBadge } from '../utils'; + +let localComandas: any[] = []; + +const FORMAS_PAGAMENTO = [ + { value: 'DINHEIRO', label: '💵 Dinheiro' }, + { value: 'CARTAO_CREDITO', label: '💳 Cartão Crédito' }, + { value: 'CARTAO_DEBITO', label: '💳 Cartão Débito' }, + { value: 'PIX', label: '📱 PIX' }, +]; + +export async function renderOrders(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/orders')} +
+ + + + +

Carregando...

+
+ + + + + + + + + + `; + + await loadComandas({ status: 'ABERTA,FECHADA' }); + bindOrderEvents(); +} + +function calcularTotalFinal(c: any): number { + let total = c.total || 0; + total -= (c.desconto || 0); + total += (c.acrescimo || 0); + if (c.taxaServico) total += total * 0.10; + return Math.max(total, 0); +} + +async function loadComandas(params?: { status?: string; dataInicio?: string; dataFim?: string }) { + const list = document.getElementById('comandas-list')!; + try { + const qs = new URLSearchParams(); + if (params?.status) qs.set('status', params.status); + if (params?.dataInicio) qs.set('dataInicio', params.dataInicio); + if (params?.dataFim) qs.set('dataFim', params.dataFim); + const query = qs.toString(); + const comandas = await api.get(`/comandas${query ? '?' + query : ''}`); + localComandas = comandas; + + if (!comandas.length) { + list.innerHTML = `

Nenhuma comanda aberta no momento.

`; + return; + } + list.innerHTML = ` +
+ + + + + + + + + + + + + ${comandas.map((c: any) => ` + + + + + + + + + `).join('')} + +
IdentificadorClienteStatusTotalAberta emAções
${c.identificador}${c.nomeCliente ?? ''}${c.status}${formatBRL(calcularTotalFinal(c))}${new Date(c.criadoEm).toLocaleString('pt-BR')} + + ${c.status === 'ABERTA' ? ` + + ` : ''} +
+
+ `; + } catch (err: any) { + list.innerHTML = `

${err.message}

`; + } +} + +function renderProductCards(products: any[]) { + const grid = document.getElementById('modal-produtos-grid'); + if (!grid) return; + + if (products.length === 0) { + grid.innerHTML = '

Nenhum produto encontrado

'; + return; + } + + grid.innerHTML = products.map((p: any) => ` +
+
+
${p.nome}
+
${formatBRL(p.preco)}
+
+
+ ${p.totalVendido !== undefined ? `Vendidos: ${p.totalVendido}` : (p.categoria?.nome ?? '')} +
+
+ `).join(''); + + const cards = grid.querySelectorAll('.product-mini-card'); + cards.forEach(card => { + card.addEventListener('click', () => { + cards.forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + + const id = (card as HTMLElement).dataset.id!; + const nome = (card as HTMLElement).dataset.nome!; + const preco = parseFloat((card as HTMLElement).dataset.preco!); + + (document.getElementById('selected-produto-id') as HTMLInputElement).value = id; + const preview = document.getElementById('selected-produto-preview')!; + preview.textContent = `Selecionado: ${nome} (${formatBRL(preco)})`; + preview.style.color = 'var(--success)'; + }); + }); +} + +function bindOrderEvents() { + document.getElementById('btn-nova-comanda')?.addEventListener('click', () => { + (document.getElementById('modal-nova-comanda') as HTMLElement).style.display = 'flex'; + }); + document.getElementById('modal-close')?.addEventListener('click', () => { + (document.getElementById('modal-nova-comanda') as HTMLElement).style.display = 'none'; + }); + document.getElementById('modal-item-close')?.addEventListener('click', () => { + (document.getElementById('modal-add-item') as HTMLElement).style.display = 'none'; + }); + document.getElementById('modal-detalhes-close')?.addEventListener('click', () => { + (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'none'; + }); + + document.getElementById('form-busca-comandas')?.addEventListener('submit', (e) => { + e.preventDefault(); + const dataInicio = (document.getElementById('busca-data-inicio') as HTMLInputElement).value; + const dataFim = (document.getElementById('busca-data-fim') as HTMLInputElement).value; + if (!dataInicio && !dataFim) return loadComandas({ status: 'ABERTA,FECHADA' }); + loadComandas({ dataInicio, dataFim }); + }); + + document.getElementById('btn-busca-limpar')?.addEventListener('click', () => { + (document.getElementById('busca-data-inicio') as HTMLInputElement).value = ''; + (document.getElementById('busca-data-fim') as HTMLInputElement).value = ''; + loadComandas({ status: 'ABERTA,FECHADA' }); + }); + + document.getElementById('detalhes-edit-toggle')?.addEventListener('click', () => { + const section = document.querySelector('.detalhes-edit-section'); + section?.classList.toggle('expanded'); + }); + + // Toggle motivo cancelamento visibility + document.getElementById('detalhes-status')?.addEventListener('change', (e) => { + const val = (e.target as HTMLSelectElement).value; + const group = document.getElementById('motivo-cancelamento-group') as HTMLElement; + if (group) group.style.display = val === 'CANCELADA' ? 'block' : 'none'; + }); + + document.getElementById('form-nova-comanda')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const ident = (document.getElementById('identificador') as HTMLInputElement).value; + const nome = (document.getElementById('nomeCliente') as HTMLInputElement).value; + try { + await api.post('/comandas', { identificador: ident, nomeCliente: nome || undefined }); + toast('Comanda aberta com sucesso!', 'success'); + (document.getElementById('modal-nova-comanda') as HTMLElement).style.display = 'none'; + await loadComandas({ status: 'ABERTA,FECHADA' }); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + + document.getElementById('form-editar-comanda')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const id = (document.getElementById('detalhes-comanda-id') as HTMLInputElement).value; + const payload: any = { + identificador: (document.getElementById('detalhes-identificador') as HTMLInputElement).value, + nomeCliente: (document.getElementById('detalhes-cliente') as HTMLInputElement).value || undefined, + status: (document.getElementById('detalhes-status') as HTMLSelectElement).value, + desconto: parseFloat((document.getElementById('detalhes-desconto') as HTMLInputElement).value) || 0, + acrescimo: parseFloat((document.getElementById('detalhes-acrescimo') as HTMLInputElement).value) || 0, + taxaServico: (document.getElementById('detalhes-taxa-servico') as HTMLInputElement).checked, + motivoCancelamento: (document.getElementById('detalhes-motivo-cancelamento') as HTMLInputElement).value || undefined, + }; + + try { + await api.patch(`/comandas/${id}`, payload); + toast('Comanda atualizada!', 'success'); + (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'none'; + await loadComandas({ status: 'ABERTA,FECHADA' }); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + + document.getElementById('form-add-item')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const comandaId = (document.getElementById('current-comanda-id') as HTMLInputElement).value; + const produtoId = (document.getElementById('selected-produto-id') as HTMLInputElement).value; + if (!produtoId) { + toast('Por favor, selecione um produto nos cards acima!', 'error'); + return; + } + const quantidade = parseInt((document.getElementById('qtd') as HTMLInputElement).value); + const observacao = (document.getElementById('obs') as HTMLInputElement).value; + try { + await api.post(`/comandas/${comandaId}/itens`, { produtoId, quantidade, observacao: observacao || undefined }); + toast('Pedido enviado para preparo!', 'success'); + (document.getElementById('modal-add-item') as HTMLElement).style.display = 'none'; + await loadComandas({ status: 'ABERTA,FECHADA' }); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + + const buscaInput = document.getElementById('modal-produto-busca') as HTMLInputElement; + let timer: any; + buscaInput?.addEventListener('input', () => { + clearTimeout(timer); + timer = setTimeout(async () => { + const query = buscaInput.value.trim(); + const gridLabel = document.getElementById('grid-label')!; + try { + if (query.length > 0) { + gridLabel.textContent = 'Resultados da Busca'; + const products = await api.get(`/produtos/busca?q=${encodeURIComponent(query)}`); + renderProductCards(products); + } else { + gridLabel.textContent = 'Produtos Sugeridos (Mais Vendidos)'; + const topProducts = await api.get('/produtos/mais-vendidos'); + renderProductCards(topProducts); + } + } catch (err) { + console.error(err); + } + }, 300); + }); + + (window as any).openAddItem = async (comandaId: string) => { + (document.getElementById('current-comanda-id') as HTMLInputElement).value = comandaId; + (document.getElementById('selected-produto-id') as HTMLInputElement).value = ''; + const buscaInput = document.getElementById('modal-produto-busca') as HTMLInputElement; + if (buscaInput) buscaInput.value = ''; + + const preview = document.getElementById('selected-produto-preview')!; + preview.textContent = 'Nenhum produto selecionado'; + preview.style.color = 'var(--accent-primary)'; + + const gridLabel = document.getElementById('grid-label'); + if (gridLabel) gridLabel.textContent = 'Produtos Sugeridos (Mais Vendidos)'; + + (document.getElementById('modal-add-item') as HTMLElement).style.display = 'flex'; + + try { + const topProducts = await api.get('/produtos/mais-vendidos'); + renderProductCards(topProducts); + } catch (err) { + console.error(err); + } + }; + + (window as any).openDetails = (comandaId: string) => { + const c = localComandas.find((x: any) => x.id === comandaId); + if (!c) return; + + (document.getElementById('detalhes-comanda-id') as HTMLInputElement).value = c.id; + (document.getElementById('detalhes-identificador') as HTMLInputElement).value = c.identificador; + (document.getElementById('detalhes-cliente') as HTMLInputElement).value = c.nomeCliente ?? ''; + (document.getElementById('detalhes-status') as HTMLSelectElement).value = c.status; + (document.getElementById('detalhes-forma-pagamento') as HTMLSelectElement).value = c.formaPagamento ?? ''; + (document.getElementById('detalhes-desconto') as HTMLInputElement).value = c.desconto ?? 0; + (document.getElementById('detalhes-acrescimo') as HTMLInputElement).value = c.acrescimo ?? 0; + (document.getElementById('detalhes-taxa-servico') as HTMLInputElement).checked = c.taxaServico ?? false; + (document.getElementById('detalhes-motivo-cancelamento') as HTMLInputElement).value = c.motivoCancelamento ?? ''; + + // Show/hide motivo cancelamento + const motivoGroup = document.getElementById('motivo-cancelamento-group') as HTMLElement; + if (motivoGroup) motivoGroup.style.display = c.status === 'CANCELADA' ? 'block' : 'none'; + + // Summary labels + document.getElementById('detalhes-label-ident')!.textContent = c.identificador; + document.getElementById('detalhes-label-client')!.textContent = c.nomeCliente ? `· ${c.nomeCliente}` : ''; + document.getElementById('detalhes-label-status')!.innerHTML = statusBadge(c.status); + + // Render itens + const tbody = document.getElementById('detalhes-itens-tbody')!; + const itens = c.itens ?? []; + const user = getUser(); + const canDelete = user && ['ADMIN', 'CAIXA'].includes(user.role) && c.status === 'ABERTA'; + + if (itens.length === 0) { + tbody.innerHTML = `Nenhum item adicionado`; + } else { + tbody.innerHTML = itens.map((i: any) => ` + + ${i.quantidade}x ${i.produto?.nome ?? '—'} + ${i.quantidade} + ${formatBRL(i.precoUnitario * i.quantidade)} + ${i.observacao ?? ''} + ${i.status} + + ${canDelete ? ` + + ` : ''} + + + `).join(''); + } + + if (canDelete) { + tbody.querySelectorAll('.btn-delete-item').forEach(btn => { + btn.addEventListener('click', async () => { + const itemId = (btn as HTMLElement).dataset.id!; + const itemNome = (btn as HTMLElement).dataset.nome!; + if (!confirm(`Remover "${itemNome}" da comanda?`)) return; + try { + await api.delete(`/comandas/itens/${itemId}`); + toast('Item removido!', 'success'); + await loadComandas({ status: 'ABERTA,FECHADA' }); + (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'none'; + (window as any).openDetails(comandaId); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + }); + } + + // Resumo financeiro + const resumoEl = document.getElementById('detalhes-resumo-financeiro')!; + const subtotal = c.total || 0; + const desconto = c.desconto || 0; + const acrescimo = c.acrescimo || 0; + const taxaServico = c.taxaServico ? (subtotal - desconto + acrescimo) * 0.10 : 0; + const totalFinal = Math.max(subtotal - desconto + acrescimo + taxaServico, 0); + + resumoEl.innerHTML = ` +
+
+ Subtotal: + ${formatBRL(subtotal)} +
+ ${desconto > 0 ? `
+ Desconto:-${formatBRL(desconto)} +
` : ''} + ${acrescimo > 0 ? `
+ Acréscimo:+${formatBRL(acrescimo)} +
` : ''} + ${c.taxaServico ? `
+ Taxa Serviço (10%):+${formatBRL(taxaServico)} +
` : ''} + ${c.formaPagamento ? `
+ Pagamento: + ${c.formaPagamento.replace('_', ' ')} +
` : ''} +
+ `; + + document.getElementById('detalhes-total')!.textContent = formatBRL(totalFinal); + + const acoesContainer = document.getElementById('detalhes-acoes-pagamento')!; + acoesContainer.innerHTML = ''; + const canPay = user && ['ADMIN', 'CAIXA'].includes(user.role); + + if (canPay && c.status !== 'PAGA' && c.status !== 'CANCELADA') { + acoesContainer.innerHTML = ` +
+
+ + +
+ +
+ `; + + document.getElementById('btn-detalhes-receber')?.addEventListener('click', async () => { + const forma = (document.getElementById('pagamento-forma') as HTMLSelectElement).value; + if (!confirm(`Confirmar recebimento de ${formatBRL(totalFinal)} para "${c.identificador}"?`)) return; + try { + await api.post(`/comandas/${c.id}/pagar`, { formaPagamento: forma }); + toast('Pagamento registrado!', 'success'); + (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'none'; + await loadComandas({ status: 'ABERTA,FECHADA' }); + } catch (err: any) { + toast(err.message, 'error'); + } + }); + } + + (document.getElementById('modal-detalhes-comanda') as HTMLElement).style.display = 'flex'; + document.querySelector('.detalhes-edit-section')?.classList.remove('expanded'); + }; +} diff --git a/web/src/views/users.ts b/web/src/views/users.ts new file mode 100644 index 0000000..fe54122 --- /dev/null +++ b/web/src/views/users.ts @@ -0,0 +1,156 @@ +import { api } from '../api'; +import { renderNavbar, toast, roleBadge } from '../utils'; + +const ROLE_OPTIONS = ['ADMIN', 'CAIXA', 'GARCOM', 'COZINHA', 'BARMAN']; + +export async function renderUsers(container: HTMLElement) { + container.innerHTML = ` + ${renderNavbar('/users')} +
+ +

Carregando...

+
+ + + + `; + + await loadUsers(); + bindUserEvents(); +} + +async function loadUsers() { + const list = document.getElementById('users-list')!; + try { + const users = await api.get('/usuarios'); + list.innerHTML = ` +
+ + + + + + ${users.map((u: any) => ` + + + + + + + + + `).join('')} + +
NomeE-mailCargoStatusDesdeAções
${u.nome}${u.email}${roleBadge(u.role)}${u.ativo ? 'Ativo' : 'Inativo'}${new Date(u.criadoEm).toLocaleDateString('pt-BR')} + + +
+
+ `; + } catch (err: any) { + list.innerHTML = `

Erro: ${err.message}

`; + } +} + +function bindUserEvents() { + document.getElementById('btn-novo-usuario')?.addEventListener('click', () => { + clearUserForm(); + (document.getElementById('modal-usuario') as HTMLElement).style.display = 'flex'; + (document.getElementById('u-senha') as HTMLInputElement).required = true; + (document.getElementById('senha-group') as HTMLElement).style.display = 'block'; + }); + + document.getElementById('form-usuario')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const id = (document.getElementById('user-id') as HTMLInputElement).value; + const senha = (document.getElementById('u-senha') as HTMLInputElement).value; + const payload: any = { + nome: (document.getElementById('u-nome') as HTMLInputElement).value, + email: (document.getElementById('u-email') as HTMLInputElement).value, + role: (document.getElementById('u-role') as HTMLSelectElement).value, + ativo: (document.getElementById('u-ativo') as HTMLInputElement).checked, + }; + if (!id) payload.senha = senha; + + try { + if (id) await api.patch(`/usuarios/${id}`, payload); + else await api.post('/usuarios', { ...payload, senha }); + toast(id ? 'Usuário atualizado!' : 'Usuário cadastrado!', 'success'); + (document.getElementById('modal-usuario') as HTMLElement).style.display = 'none'; + await loadUsers(); + } catch (err: any) { toast(err.message, 'error'); } + }); + + (window as any).editUser = (u: any) => { + (document.getElementById('user-id') as HTMLInputElement).value = u.id; + (document.getElementById('u-nome') as HTMLInputElement).value = u.nome; + (document.getElementById('u-email') as HTMLInputElement).value = u.email; + (document.getElementById('u-role') as HTMLSelectElement).value = u.role; + (document.getElementById('u-ativo') as HTMLInputElement).checked = u.ativo; + (document.getElementById('u-senha') as HTMLInputElement).required = false; + (document.getElementById('u-senha') as HTMLInputElement).value = ''; + (document.getElementById('senha-group') as HTMLElement).style.display = 'none'; + (document.getElementById('modal-user-title') as HTMLElement).textContent = 'Editar Usuário'; + (document.getElementById('modal-usuario') as HTMLElement).style.display = 'flex'; + }; + + (window as any).toggleUser = async (id: string, nome: string, ativo: boolean) => { + const action = ativo ? 'desativar' : 'ativar'; + if (!confirm(`Deseja ${action} o usuário "${nome}"?`)) return; + try { + await api.patch(`/usuarios/${id}`, { ativo: !ativo }); + toast(`Usuário ${ativo ? 'desativado' : 'ativado'}!`, 'success'); + await loadUsers(); + } catch (err: any) { toast(err.message, 'error'); } + }; +} + +function clearUserForm() { + (document.getElementById('user-id') as HTMLInputElement).value = ''; + (document.getElementById('u-nome') as HTMLInputElement).value = ''; + (document.getElementById('u-email') as HTMLInputElement).value = ''; + (document.getElementById('u-senha') as HTMLInputElement).value = ''; + (document.getElementById('u-role') as HTMLSelectElement).value = 'GARCOM'; + (document.getElementById('u-ativo') as HTMLInputElement).checked = true; + (document.getElementById('modal-user-title') as HTMLElement).textContent = 'Novo Usuário'; +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..206b5e0 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es2023", + "module": "esnext", + "lib": ["ES2023", "DOM"], + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +}