filtro em produtos, bug tela de preparo, nome do user nas comandas e itens
This commit is contained in:
237
AGENTS.md
Normal file
237
AGENTS.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Contexto do Projeto - Gastrobar PDV (Café em Saturno)
|
||||
|
||||
## Visão Geral
|
||||
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 (`src/`)
|
||||
- **Runtime:** Node.js + TypeScript (CommonJS)
|
||||
- **Framework:** Fastify v4.27 (porta 3333)
|
||||
- **ORM:** Prisma v5.14 com SQLite (`prisma/dev.db`)
|
||||
- **Validação:** Zod v3.23
|
||||
- **Auth:** JWT (`@fastify/jwt`) + bcrypt
|
||||
- **Dev:** tsx watch (`npm run dev`)
|
||||
|
||||
### Frontend (`web/`)
|
||||
- **Bundler:** Vite v8.1
|
||||
- **Linguagem:** TypeScript vanilla (sem framework, SPA)
|
||||
- **Dev server:** porta 5173
|
||||
- **API URL:** `http://localhost:3333/api`
|
||||
- **Token storage:** `localStorage('@gastrobar:token')` e `localStorage('@gastrobar:user')`
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
app_ces/
|
||||
├── .env # JWT_SECRET
|
||||
├── package.json # Backend dependencies
|
||||
├── tsconfig.json
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Schema do banco (SQLite)
|
||||
│ ├── seed.ts # Seed: admin@gastrobar.com / 123456
|
||||
│ └── dev.db # Banco SQLite (gerado)
|
||||
├── src/ # Backend API
|
||||
│ ├── server.ts # Entrypoint Fastify, porta 3333
|
||||
│ ├── lib/
|
||||
│ │ └── prisma.ts # Instância PrismaClient
|
||||
│ ├── middlewares/
|
||||
│ │ └── auth.middleware.ts # authenticate + requireRoles
|
||||
│ ├── controllers/
|
||||
│ │ ├── auth.controller.ts
|
||||
│ │ ├── category.controller.ts
|
||||
│ │ ├── product.controller.ts
|
||||
│ │ ├── order.controller.ts
|
||||
│ │ ├── user.controller.ts
|
||||
│ │ └── report.controller.ts
|
||||
│ ├── services/
|
||||
│ │ ├── auth.service.ts
|
||||
│ │ ├── category.service.ts
|
||||
│ │ ├── product.service.ts
|
||||
│ │ ├── order.service.ts
|
||||
│ │ ├── user.service.ts
|
||||
│ │ └── report.service.ts
|
||||
│ ├── schemas/ # Validação Zod
|
||||
│ │ ├── auth.schema.ts
|
||||
│ │ ├── user.schema.ts
|
||||
│ │ ├── product.schema.ts
|
||||
│ │ ├── category.schema.ts
|
||||
│ │ └── order.schema.ts
|
||||
│ └── routes/
|
||||
│ ├── auth.routes.ts
|
||||
│ ├── category.routes.ts
|
||||
│ ├── product.routes.ts
|
||||
│ ├── order.routes.ts
|
||||
│ ├── user.routes.ts
|
||||
│ └── report.routes.ts
|
||||
└── web/ # Frontend SPA
|
||||
├── package.json # Vite + TypeScript
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── main.ts # Router SPA + navegação
|
||||
├── api.ts # Cliente HTTP centralizado
|
||||
├── utils.ts # renderNavbar, toast, helpers
|
||||
├── style.css # Dark theme + glassmorphism
|
||||
└── views/
|
||||
├── login.ts
|
||||
├── kitchen.ts # Painel de preparo
|
||||
├── orders.ts # Gerenciamento de comandas
|
||||
├── checkout.ts # Caixa/pagamento
|
||||
├── menu.ts # Cardápio CRUD
|
||||
├── users.ts # Gestão de equipe
|
||||
└── dashboard.ts # KPIs admin
|
||||
```
|
||||
|
||||
## Banco de Dados (Schema Prisma)
|
||||
|
||||
### Models
|
||||
|
||||
**Usuario**
|
||||
- `id` (UUID), `nome`, `email` (unique), `senha` (hash bcrypt), `role` (ADMIN|CAIXA|GARCOM|COZINHA|BARMAN), `ativo`, `criadoEm`
|
||||
- Relations: `comandas` (1:N Comanda), `itensPedido` (1:N ItemPedido)
|
||||
|
||||
**Categoria**
|
||||
- `id` (UUID), `nome`, `produtos` (relation 1:N Produto)
|
||||
|
||||
**Produto**
|
||||
- `id` (UUID), `nome`, `descricao?`, `fotoUrl?`, `preco` (Float), `custo?`, `estoqueAtual`, `estoqueMinimo`, `itemCozinha` (boolean: true=cozinha, false=bar), `ativo`, `categoriaId` (FK)
|
||||
|
||||
**Comanda**
|
||||
- `id` (UUID), `identificador` (mesa/código), `nomeCliente?`, `status` (ABERTA|FECHADA|PAGA|CANCELADA), `total`, `valorPago`, `desconto`, `acrescimo`, `taxaServico` (boolean, +10%), `formaPagamento?`, `motivoCancelamento?`, `criadoEm`, `fechadoEm?`
|
||||
- `usuarioId?` (FK) - Usuário que abriu a comanda
|
||||
- Relations: `itens` (1:N ItemPedido), `pagamentos` (1:N Pagamento), `usuario` (N:1 Usuario)
|
||||
|
||||
**ItemPedido**
|
||||
- `id` (UUID), `quantidade`, `precoUnitario`, `observacao?`, `status` (PENDENTE|PREPARANDO|PRONTO|ENTREGUE), `produtoId` (FK), `comandaId` (FK), `criadoEm`
|
||||
- `usuarioId?` (FK) - Usuário que adicionou o item
|
||||
- Relations: `produto` (N:1 Produto), `comanda` (N:1 Comanda), `usuario` (N:1 Usuario)
|
||||
|
||||
**Pagamento**
|
||||
- `id` (UUID), `comandaId` (FK), `valor`, `formaPagamento` (DINHEIRO|CARTAO_CREDITO|CARTAO_DEBITO|PIX), `observacao?`, `criadoEm`
|
||||
|
||||
## Rotas da API
|
||||
|
||||
### Auth (`/api/auth`)
|
||||
| Método | Rota | Permissão | Descrição |
|
||||
|--------|------|-----------|-----------|
|
||||
| POST | `/auth/login` | Público | Login, retorna JWT + user |
|
||||
| PATCH | `/auth/senha` | Autenticado | Alterar própria senha |
|
||||
|
||||
### Categorias (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/categorias` | Autenticado |
|
||||
| GET | `/categorias/:id` | Autenticado |
|
||||
| POST | `/categorias` | ADMIN |
|
||||
| PUT | `/categorias/:id` | ADMIN |
|
||||
| DELETE | `/categorias/:id` | ADMIN |
|
||||
|
||||
### Produtos (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/produtos` | Autenticado |
|
||||
| GET | `/produtos/mais-vendidos` | Autenticado |
|
||||
| GET | `/produtos/busca` | Autenticado |
|
||||
| GET | `/produtos/baixo-estoque` | ADMIN |
|
||||
| GET | `/produtos/:id` | Autenticado |
|
||||
| POST | `/produtos` | ADMIN |
|
||||
| PUT | `/produtos/:id` | ADMIN |
|
||||
| DELETE | `/produtos/:id` | ADMIN |
|
||||
|
||||
### Comandas (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/comandas` | Autenticado |
|
||||
| GET | `/pagamentos` | Autenticado |
|
||||
| POST | `/comandas` | GARCOM/CAIXA/ADMIN |
|
||||
| POST | `/comandas/:id/itens` | GARCOM/CAIXA/ADMIN |
|
||||
| PATCH | `/comandas/:id` | GARCOM/CAIXA/ADMIN |
|
||||
| POST | `/comandas/:id/pagar` | CAIXA/ADMIN |
|
||||
| POST | `/comandas/:id/pagamentos` | CAIXA/ADMIN |
|
||||
| PATCH | `/comandas/itens/:id/observacao` | GARCOM/CAIXA/ADMIN |
|
||||
| DELETE | `/comandas/itens/:id` | CAIXA/ADMIN |
|
||||
|
||||
### Painel Cozinha/Bar (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/painel/itens` | COZINHA/BARMAN/ADMIN |
|
||||
| PATCH | `/painel/itens/:id/status` | COZINHA/BARMAN/ADMIN |
|
||||
|
||||
### Relatórios (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/relatorios/vendas` | ADMIN |
|
||||
| GET | `/relatorios/dashboard` | ADMIN |
|
||||
|
||||
### Usuários (`/api`)
|
||||
| Método | Rota | Permissão |
|
||||
|--------|------|-----------|
|
||||
| GET | `/usuarios` | ADMIN |
|
||||
| POST | `/usuarios` | ADMIN |
|
||||
| PATCH | `/usuarios/:id` | ADMIN |
|
||||
| PATCH | `/usuarios/:id/senha` | ADMIN |
|
||||
| DELETE | `/usuarios/:id` | ADMIN |
|
||||
|
||||
## Roles e Permissões
|
||||
|
||||
| Role | Descrição | Acesso Principal |
|
||||
|------|-----------|-----------------|
|
||||
| `ADMIN` | Administrador | Acesso total ao sistema |
|
||||
| `CAIXA` | Operador de caixa | Comandas, checkout, pagamento, exclusão de itens |
|
||||
| `GARCOM` | Garçom | Criar/gerenciar comandas, adicionar itens |
|
||||
| `COZINHA` | Cozinheiro | Painel de preparo (itens `itemCozinha=true`) |
|
||||
| `BARMAN` | Bartender | Painel de preparo (itens `itemCozinha=false`) |
|
||||
|
||||
## Lógica de Negócio Importante
|
||||
|
||||
### Fluxo de Comanda
|
||||
1. Criar comanda (identificador = mesa/código) - **registra usuário logado**
|
||||
2. Adicionar itens (baixa estoque automaticamente, atualiza total) - **registra usuário logado**
|
||||
3. Itens aparecem no painel de cozinha/bar com status `PENDENTE`
|
||||
4. Cozinha/bar avança: `PENDENTE → PREPARANDO → PRONTO → ENTREGUE`
|
||||
5. Caixa fecha com pagamento (integral ou parcial)
|
||||
6. Status da comanda: `ABERTA → PAGA`
|
||||
|
||||
### Pagamento
|
||||
- **Integral**: `POST /comandas/:id/pagar` - registra pagamento do restante e fecha comanda
|
||||
- **Parcial**: `POST /comandas/:id/pagamentos` - registra pagamento parcial, pode selecionar itens específicos
|
||||
- **Cálculo do total final**: `total - desconto + acrescimo + (taxaServico ? 10% : 0)`
|
||||
- Formas: `DINHEIRO`, `CARTAO_CREDITO`, `CARTAO_DEBITO`, `PIX`
|
||||
|
||||
### Estoque
|
||||
- Decrementa ao adicionar item na comanda
|
||||
- Incrementa ao remover item da comanda
|
||||
- Validação de estoque insuficiente no backend
|
||||
|
||||
### Frontend
|
||||
- Rotas SPA manuais via `window.history.pushState`
|
||||
- Auto-refresh no painel de cozinha a cada 10 segundos
|
||||
- Notificações sonoras via Web Audio API para novos pedidos
|
||||
- Navbar dinâmica baseada no role do usuário logado
|
||||
|
||||
## Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
npm run dev # Iniciar dev server (tsx watch)
|
||||
npm run build # Compilar TypeScript
|
||||
npm run db:push # Aplicar schema no banco
|
||||
npm run db:seed # Popular banco com dados iniciais
|
||||
|
||||
# Frontend
|
||||
cd web && npm run dev # Iniciar Vite dev server
|
||||
cd web && npm run build # Build de produção
|
||||
```
|
||||
|
||||
## Credenciais Padrão (Seed)
|
||||
- **Email:** admin@gastrobar.com
|
||||
- **Senha:** 123456
|
||||
- **Role:** ADMIN
|
||||
|
||||
## Notas Técnicas
|
||||
- CORS habilitado para qualquer origem (`origin: '*'`)
|
||||
- JWT secret definido em `.env` (fallback: `fallback_secret_change_me`)
|
||||
- Error handler global trata `ZodError` (400) e erros HTTP
|
||||
- Frontend não usa framework UI - renderiza HTML via template literals
|
||||
- Utiliza `event delegation` para botões dinâmicos no painel
|
||||
- Cleanup de listeners ao navegar entre views
|
||||
@@ -15,6 +15,8 @@ model Usuario {
|
||||
role String @default("GARCOM") // ADMIN, CAIXA, GARCOM, COZINHA, BARMAN
|
||||
ativo Boolean @default(true)
|
||||
criadoEm DateTime @default(now())
|
||||
comandas Comanda[]
|
||||
itensPedido ItemPedido[]
|
||||
}
|
||||
|
||||
model Categoria {
|
||||
@@ -53,6 +55,8 @@ model Comanda {
|
||||
motivoCancelamento String?
|
||||
criadoEm DateTime @default(now())
|
||||
fechadoEm DateTime?
|
||||
usuarioId String?
|
||||
usuario Usuario? @relation(fields: [usuarioId], references: [id])
|
||||
itens ItemPedido[]
|
||||
pagamentos Pagamento[]
|
||||
}
|
||||
@@ -67,6 +71,8 @@ model ItemPedido {
|
||||
produto Produto @relation(fields: [produtoId], references: [id])
|
||||
comandaId String
|
||||
comanda Comanda @relation(fields: [comandaId], references: [id])
|
||||
usuarioId String?
|
||||
usuario Usuario? @relation(fields: [usuarioId], references: [id])
|
||||
criadoEm DateTime @default(now())
|
||||
}
|
||||
|
||||
|
||||
@@ -42,16 +42,18 @@ export async function listPaymentsController(request: FastifyRequest, reply: Fas
|
||||
|
||||
export async function createOrderController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const data = createOrderSchema.parse(request.body);
|
||||
const order = await orderService.createOrder(data);
|
||||
const user = request.user as { id: string };
|
||||
const order = await orderService.createOrder(data, user.id);
|
||||
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);
|
||||
const user = request.user as { id: string };
|
||||
|
||||
try {
|
||||
const item = await orderService.addItemToOrder(id, data);
|
||||
const item = await orderService.addItemToOrder(id, data, user.id);
|
||||
return reply.status(201).send(item);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
|
||||
@@ -64,26 +64,35 @@ export async function listOrders(filter?: ListOrdersFilter) {
|
||||
where,
|
||||
orderBy: { criadoEm: 'desc' },
|
||||
include: {
|
||||
usuario: {
|
||||
select: { id: true, nome: true, email: true },
|
||||
},
|
||||
itens: {
|
||||
include: { produto: true }
|
||||
include: {
|
||||
produto: true,
|
||||
usuario: {
|
||||
select: { id: true, nome: true },
|
||||
},
|
||||
}
|
||||
},
|
||||
pagamentos: true,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function createOrder(data: CreateOrderInput) {
|
||||
export async function createOrder(data: CreateOrderInput, usuarioId?: string) {
|
||||
return prisma.comanda.create({
|
||||
data: {
|
||||
identificador: data.identificador,
|
||||
nomeCliente: data.nomeCliente,
|
||||
status: 'ABERTA',
|
||||
total: 0,
|
||||
usuarioId: usuarioId ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function addItemToOrder(comandaId: string, data: AddItemOrderInput) {
|
||||
export async function addItemToOrder(comandaId: string, data: AddItemOrderInput, usuarioId?: string) {
|
||||
const produto = await prisma.produto.findUnique({
|
||||
where: { id: data.produtoId },
|
||||
});
|
||||
@@ -110,6 +119,7 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput)
|
||||
precoUnitario: produto.preco,
|
||||
produtoId: data.produtoId,
|
||||
comandaId: comandaId,
|
||||
usuarioId: usuarioId ?? null,
|
||||
status: 'PENDENTE',
|
||||
},
|
||||
}),
|
||||
@@ -140,6 +150,9 @@ export async function listKitchenBarItems(itemCozinha?: boolean, status?: string
|
||||
include: {
|
||||
produto: true,
|
||||
comanda: true,
|
||||
usuario: {
|
||||
select: { id: true, nome: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -320,8 +333,16 @@ export async function updateOrder(comandaId: string, data: UpdateOrderInput) {
|
||||
where: { id: comandaId },
|
||||
data: updateData,
|
||||
include: {
|
||||
usuario: {
|
||||
select: { id: true, nome: true, email: true },
|
||||
},
|
||||
itens: {
|
||||
include: { produto: true }
|
||||
include: {
|
||||
produto: true,
|
||||
usuario: {
|
||||
select: { id: true, nome: true },
|
||||
},
|
||||
}
|
||||
},
|
||||
pagamentos: true,
|
||||
}
|
||||
@@ -360,6 +381,18 @@ export async function deleteItemFromOrder(itemId: string) {
|
||||
|
||||
return prisma.comanda.findUnique({
|
||||
where: { id: item.comandaId },
|
||||
include: { itens: { include: { produto: true } } },
|
||||
include: {
|
||||
itens: {
|
||||
include: {
|
||||
produto: true,
|
||||
usuario: {
|
||||
select: { id: true, nome: true },
|
||||
},
|
||||
}
|
||||
},
|
||||
usuario: {
|
||||
select: { id: true, nome: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
16
test-api.ts
Normal file
16
test-api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
(async () => {
|
||||
const res = await fetch('http://localhost:3333/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'admin@gastrobar.com', senha: '123456' }),
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('TOKEN:', data.token);
|
||||
|
||||
const res2 = await fetch('http://localhost:3333/api/comandas?status=ABERTA,FECHADA', {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
const data2 = await res2.json();
|
||||
console.log('COMANDAS STATUS:', res2.status);
|
||||
console.log('COMANDAS:', JSON.stringify(data2, null, 2));
|
||||
})();
|
||||
@@ -59,15 +59,19 @@ export async function renderKitchen(container: HTMLElement) {
|
||||
let currentStatus = 'PENDENTE';
|
||||
let currentFilter = '';
|
||||
let autoRefreshInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let requestId = 0;
|
||||
|
||||
const loadItems = async () => {
|
||||
const grid = document.getElementById('items-grid')!;
|
||||
grid.innerHTML = '<p class="card-subtitle">Carregando...</p>';
|
||||
const myRequestId = ++requestId;
|
||||
try {
|
||||
let url = `/painel/itens?status=${currentStatus}`;
|
||||
if (currentFilter !== '') url += `&itemCozinha=${currentFilter}`;
|
||||
const items = await api.get(url);
|
||||
|
||||
if (myRequestId !== requestId) return;
|
||||
|
||||
const currentIds = new Set<string>(items.map((i: any) => i.id));
|
||||
|
||||
const previousIds = previousItemIdsByStatus.get(currentStatus);
|
||||
@@ -106,9 +110,10 @@ export async function renderKitchen(container: HTMLElement) {
|
||||
</div>
|
||||
<h3 class="card-title">${item.quantidade}× ${item.produto.nome}</h3>
|
||||
${item.observacao ? `<p style="color:var(--warning);font-size:0.875rem;margin-bottom:1rem;">⚠️ ${item.observacao}</p>` : ''}
|
||||
<p class="card-subtitle" style="margin-bottom:1.5rem;">
|
||||
<p class="card-subtitle" style="margin-bottom:0.5rem;">
|
||||
Pedido às ${new Date(item.criadoEm).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
${item.usuario ? `<p style="color:var(--text-secondary);font-size:0.8rem;margin-bottom:1rem;">Adicionado por: <strong>${item.usuario.nome}</strong></p>` : '<p style="margin-bottom:1rem;"></p>'}
|
||||
${action.label ? `
|
||||
<button class="btn" style="width:100%;" data-action="advance" data-item-id="${item.id}" data-next-status="${action.next}">
|
||||
${action.label}
|
||||
|
||||
@@ -87,6 +87,10 @@ export async function renderMenu(container: HTMLElement) {
|
||||
`;
|
||||
|
||||
let categorias: any[] = [];
|
||||
let allProducts: any[] = [];
|
||||
let filterCategoria = '';
|
||||
let filterAtivo = '';
|
||||
let filterCozinha = '';
|
||||
|
||||
const loadCategorias = async () => {
|
||||
categorias = await api.get('/categorias');
|
||||
@@ -96,47 +100,109 @@ export async function renderMenu(container: HTMLElement) {
|
||||
|
||||
const renderTabProdutos = async () => {
|
||||
const tc = document.getElementById('tab-content')!;
|
||||
const products = await api.get('/produtos');
|
||||
allProducts = await api.get('/produtos');
|
||||
tc.innerHTML = `
|
||||
<div class="page-header" style="margin-bottom:1rem;">
|
||||
<span class="card-subtitle">${products.length} produto(s) cadastrado(s)</span>
|
||||
<span class="card-subtitle" id="produtos-count"></span>
|
||||
<button class="btn" id="btn-novo-produto">+ Novo Produto</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1.5rem;align-items:flex-end;">
|
||||
<div class="form-group" style="margin:0;width:auto;">
|
||||
<label style="color:var(--text-secondary);font-size:0.8rem;">Categoria</label>
|
||||
<select id="f-categoria" class="form-control" style="padding:0.4rem 0.75rem;">
|
||||
<option value="">Todas</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;width:auto;">
|
||||
<label style="color:var(--text-secondary);font-size:0.8rem;">Status</label>
|
||||
<select id="f-ativo" class="form-control" style="padding:0.4rem 0.75rem;">
|
||||
<option value="">Todos</option>
|
||||
<option value="true">Ativos</option>
|
||||
<option value="false">Inativos</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;width:auto;">
|
||||
<label style="color:var(--text-secondary);font-size:0.8rem;">Tipo</label>
|
||||
<select id="f-cozinha" class="form-control" style="padding:0.4rem 0.75rem;">
|
||||
<option value="">Todos</option>
|
||||
<option value="true">🍳 Cozinha</option>
|
||||
<option value="false">🍹 Bar</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-clear-filters" style="margin-bottom:0;">✕ Limpar</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Nome</th><th>Categoria</th><th>Preço</th><th>Estoque</th><th>Tipo</th><th>Status</th><th>Ações</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${products.map((p: any) => `
|
||||
<tr>
|
||||
<td data-label="Produto"><strong>${p.nome}</strong>${p.descricao ? `<br><small style="color:var(--text-secondary)">${p.descricao}</small>` : ''}</td>
|
||||
<td data-label="Categoria">${p.categoria?.nome ?? '—'}</td>
|
||||
<td data-label="Preço">${formatBRL(p.preco)}</td>
|
||||
<td data-label="Estoque">
|
||||
<span style="color:${p.estoqueAtual <= p.estoqueMinimo ? 'var(--accent-primary)' : 'var(--text-primary)'}">
|
||||
${p.estoqueAtual} ${p.estoqueAtual <= p.estoqueMinimo ? '⚠️' : ''}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Tipo"><span class="badge ${p.itemCozinha ? 'badge-cozinha' : 'badge-barman'}">${p.itemCozinha ? 'Cozinha' : 'Bar'}</span></td>
|
||||
<td data-label="Status"><span class="badge ${p.ativo ? 'badge-success' : 'badge-warning'}">${p.ativo ? 'Ativo' : 'Inativo'}</span></td>
|
||||
<td data-label="Ações" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||
<button class="btn btn-sm btn-secondary" onclick="window.editProduct(${JSON.stringify(p).replace(/"/g, '"')})">✏️</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="window.deleteProduct('${p.id}','${p.nome}')">🗑</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
<tbody id="produtos-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const catSel = document.getElementById('f-categoria') as HTMLSelectElement;
|
||||
catSel.innerHTML = '<option value="">Todas</option>' +
|
||||
categorias.map((c: any) => `<option value="${c.id}">${c.nome}</option>`).join('');
|
||||
catSel.value = filterCategoria;
|
||||
(document.getElementById('f-ativo') as HTMLSelectElement).value = filterAtivo;
|
||||
(document.getElementById('f-cozinha') as HTMLSelectElement).value = filterCozinha;
|
||||
|
||||
catSel.addEventListener('change', () => { filterCategoria = catSel.value; renderProductsTable(); });
|
||||
(document.getElementById('f-ativo') as HTMLSelectElement).addEventListener('change', (e) => { filterAtivo = (e.target as HTMLSelectElement).value; renderProductsTable(); });
|
||||
(document.getElementById('f-cozinha') as HTMLSelectElement).addEventListener('change', (e) => { filterCozinha = (e.target as HTMLSelectElement).value; renderProductsTable(); });
|
||||
document.getElementById('btn-clear-filters')?.addEventListener('click', () => {
|
||||
filterCategoria = '';
|
||||
filterAtivo = '';
|
||||
filterCozinha = '';
|
||||
(document.getElementById('f-categoria') as HTMLSelectElement).value = '';
|
||||
(document.getElementById('f-ativo') as HTMLSelectElement).value = '';
|
||||
(document.getElementById('f-cozinha') as HTMLSelectElement).value = '';
|
||||
renderProductsTable();
|
||||
});
|
||||
|
||||
renderProductsTable();
|
||||
|
||||
document.getElementById('btn-novo-produto')?.addEventListener('click', () => {
|
||||
clearProductForm();
|
||||
(document.getElementById('modal-produto') as HTMLElement).style.display = 'flex';
|
||||
});
|
||||
};
|
||||
|
||||
const renderProductsTable = () => {
|
||||
let filtered = allProducts;
|
||||
if (filterCategoria) filtered = filtered.filter((p: any) => p.categoriaId === filterCategoria);
|
||||
if (filterAtivo !== '') filtered = filtered.filter((p: any) => String(p.ativo) === filterAtivo);
|
||||
if (filterCozinha !== '') filtered = filtered.filter((p: any) => String(p.itemCozinha) === filterCozinha);
|
||||
|
||||
const countEl = document.getElementById('produtos-count');
|
||||
if (countEl) countEl.textContent = `${filtered.length} de ${allProducts.length} produto(s)`;
|
||||
|
||||
const tbody = document.getElementById('produtos-tbody')!;
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:2rem;color:var(--text-secondary);">Nenhum produto encontrado com os filtros selecionados.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = filtered.map((p: any) => `
|
||||
<tr>
|
||||
<td data-label="Produto"><strong>${p.nome}</strong>${p.descricao ? `<br><small style="color:var(--text-secondary)">${p.descricao}</small>` : ''}</td>
|
||||
<td data-label="Categoria">${p.categoria?.nome ?? '—'}</td>
|
||||
<td data-label="Preço">${formatBRL(p.preco)}</td>
|
||||
<td data-label="Estoque">
|
||||
<span style="color:${p.estoqueAtual <= p.estoqueMinimo ? 'var(--accent-primary)' : 'var(--text-primary)'}">
|
||||
${p.estoqueAtual} ${p.estoqueAtual <= p.estoqueMinimo ? '⚠️' : ''}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Tipo"><span class="badge ${p.itemCozinha ? 'badge-cozinha' : 'badge-barman'}">${p.itemCozinha ? 'Cozinha' : 'Bar'}</span></td>
|
||||
<td data-label="Status"><span class="badge ${p.ativo ? 'badge-success' : 'badge-warning'}">${p.ativo ? 'Ativo' : 'Inativo'}</span></td>
|
||||
<td data-label="Ações" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||
<button class="btn btn-sm btn-secondary" onclick="window.editProduct(${JSON.stringify(p).replace(/"/g, '"')})">✏️</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="window.deleteProduct('${p.id}','${p.nome}')">🗑</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const renderTabCategorias = async () => {
|
||||
const tc = document.getElementById('tab-content')!;
|
||||
const cats = await api.get('/categorias');
|
||||
|
||||
@@ -108,6 +108,7 @@ export async function renderOrders(container: HTMLElement) {
|
||||
<span class="detalhes-summary-client" id="detalhes-label-client"></span>
|
||||
</div>
|
||||
<div id="detalhes-label-status"></div>
|
||||
<div id="detalhes-label-usuario" style="font-size:0.8rem;color:var(--text-secondary);margin-top:0.25rem;"></div>
|
||||
</div>
|
||||
|
||||
<div class="detalhes-edit-section">
|
||||
@@ -175,6 +176,7 @@ export async function renderOrders(container: HTMLElement) {
|
||||
<th style="text-align:center;">Qtd</th>
|
||||
<th style="text-align:right;">Subtotal</th>
|
||||
<th>Obs</th>
|
||||
<th>Adicionado por</th>
|
||||
<th>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
@@ -294,6 +296,7 @@ async function loadComandas(params?: { status?: string; dataInicio?: string; dat
|
||||
<tr>
|
||||
<th>Identificador</th>
|
||||
<th>Cliente</th>
|
||||
<th>Aberto por</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Aberta em</th>
|
||||
@@ -305,6 +308,7 @@ async function loadComandas(params?: { status?: string; dataInicio?: string; dat
|
||||
<tr>
|
||||
<td data-label="Mesa" class="cmd-ident"><strong>${c.identificador}</strong></td>
|
||||
<td data-label="Cliente" class="cmd-client">${c.nomeCliente ?? ''}</td>
|
||||
<td data-label="Aberto por" class="cmd-usuario" style="color:var(--text-secondary);font-size:0.85rem;">${c.usuario?.nome ?? '—'}</td>
|
||||
<td data-label="Status" class="cmd-status"><span class="badge status-${c.status.toLowerCase()}">${c.status}</span></td>
|
||||
<td data-label="Total" class="cmd-total"><strong>${formatBRL(calcularTotalFinal(c))}</strong></td>
|
||||
<td data-label="Aberta em" class="cmd-data">${new Date(c.criadoEm).toLocaleString('pt-BR')}</td>
|
||||
@@ -606,6 +610,12 @@ function bindOrderEvents() {
|
||||
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);
|
||||
|
||||
// Exibir quem abriu a comanda
|
||||
const detalhesUsuario = document.getElementById('detalhes-label-usuario');
|
||||
if (detalhesUsuario) {
|
||||
detalhesUsuario.textContent = c.usuario ? `Aberto por: ${c.usuario.nome}` : '';
|
||||
}
|
||||
|
||||
// Render itens
|
||||
const tbody = document.getElementById('detalhes-itens-tbody')!;
|
||||
@@ -614,7 +624,7 @@ function bindOrderEvents() {
|
||||
const canDelete = user && ['ADMIN', 'CAIXA'].includes(user.role) && c.status === 'ABERTA';
|
||||
|
||||
if (itens.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);">Nenhum item adicionado</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-secondary);">Nenhum item adicionado</td></tr>`;
|
||||
} else {
|
||||
tbody.innerHTML = itens.map((i: any) => `
|
||||
<tr>
|
||||
@@ -627,6 +637,7 @@ function bindOrderEvents() {
|
||||
: (i.observacao ?? '')
|
||||
}
|
||||
</td>
|
||||
<td data-label="Adicionado por" class="item-usuario" style="font-size:0.8rem;color:var(--text-secondary);">${i.usuario?.nome ?? '—'}</td>
|
||||
<td data-label="Status" class="item-status"><span class="badge badge-${i.status === 'ENTREGUE' ? 'success' : 'warning'}">${i.status}</span></td>
|
||||
<td data-label="Ações" class="item-actions">
|
||||
${canDelete ? `
|
||||
|
||||
Reference in New Issue
Block a user