filtro em produtos, bug tela de preparo, nome do user nas comandas e itens

This commit is contained in:
2026-07-23 12:50:26 -03:00
parent 14a36b68b5
commit 14e92c91fd
8 changed files with 407 additions and 31 deletions

237
AGENTS.md Normal file
View 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

View File

@@ -15,6 +15,8 @@ model Usuario {
role String @default("GARCOM") // ADMIN, CAIXA, GARCOM, COZINHA, BARMAN role String @default("GARCOM") // ADMIN, CAIXA, GARCOM, COZINHA, BARMAN
ativo Boolean @default(true) ativo Boolean @default(true)
criadoEm DateTime @default(now()) criadoEm DateTime @default(now())
comandas Comanda[]
itensPedido ItemPedido[]
} }
model Categoria { model Categoria {
@@ -53,6 +55,8 @@ model Comanda {
motivoCancelamento String? motivoCancelamento String?
criadoEm DateTime @default(now()) criadoEm DateTime @default(now())
fechadoEm DateTime? fechadoEm DateTime?
usuarioId String?
usuario Usuario? @relation(fields: [usuarioId], references: [id])
itens ItemPedido[] itens ItemPedido[]
pagamentos Pagamento[] pagamentos Pagamento[]
} }
@@ -67,6 +71,8 @@ model ItemPedido {
produto Produto @relation(fields: [produtoId], references: [id]) produto Produto @relation(fields: [produtoId], references: [id])
comandaId String comandaId String
comanda Comanda @relation(fields: [comandaId], references: [id]) comanda Comanda @relation(fields: [comandaId], references: [id])
usuarioId String?
usuario Usuario? @relation(fields: [usuarioId], references: [id])
criadoEm DateTime @default(now()) criadoEm DateTime @default(now())
} }

View File

@@ -42,16 +42,18 @@ export async function listPaymentsController(request: FastifyRequest, reply: Fas
export async function createOrderController(request: FastifyRequest, reply: FastifyReply) { export async function createOrderController(request: FastifyRequest, reply: FastifyReply) {
const data = createOrderSchema.parse(request.body); 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); return reply.status(201).send(order);
} }
export async function addItemToOrderController(request: FastifyRequest, reply: FastifyReply) { export async function addItemToOrderController(request: FastifyRequest, reply: FastifyReply) {
const { id } = z.object({ id: z.string().uuid() }).parse(request.params); const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
const data = addItemOrderSchema.parse(request.body); const data = addItemOrderSchema.parse(request.body);
const user = request.user as { id: string };
try { try {
const item = await orderService.addItemToOrder(id, data); const item = await orderService.addItemToOrder(id, data, user.id);
return reply.status(201).send(item); return reply.status(201).send(item);
} catch (error: any) { } catch (error: any) {
return reply.status(400).send({ error: error.message }); return reply.status(400).send({ error: error.message });

View File

@@ -64,26 +64,35 @@ export async function listOrders(filter?: ListOrdersFilter) {
where, where,
orderBy: { criadoEm: 'desc' }, orderBy: { criadoEm: 'desc' },
include: { include: {
usuario: {
select: { id: true, nome: true, email: true },
},
itens: { itens: {
include: { produto: true } include: {
produto: true,
usuario: {
select: { id: true, nome: true },
},
}
}, },
pagamentos: true, pagamentos: true,
} }
}); });
} }
export async function createOrder(data: CreateOrderInput) { export async function createOrder(data: CreateOrderInput, usuarioId?: string) {
return prisma.comanda.create({ return prisma.comanda.create({
data: { data: {
identificador: data.identificador, identificador: data.identificador,
nomeCliente: data.nomeCliente, nomeCliente: data.nomeCliente,
status: 'ABERTA', status: 'ABERTA',
total: 0, 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({ const produto = await prisma.produto.findUnique({
where: { id: data.produtoId }, where: { id: data.produtoId },
}); });
@@ -110,6 +119,7 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput)
precoUnitario: produto.preco, precoUnitario: produto.preco,
produtoId: data.produtoId, produtoId: data.produtoId,
comandaId: comandaId, comandaId: comandaId,
usuarioId: usuarioId ?? null,
status: 'PENDENTE', status: 'PENDENTE',
}, },
}), }),
@@ -140,6 +150,9 @@ export async function listKitchenBarItems(itemCozinha?: boolean, status?: string
include: { include: {
produto: true, produto: true,
comanda: true, comanda: true,
usuario: {
select: { id: true, nome: true },
},
}, },
}); });
} }
@@ -320,8 +333,16 @@ export async function updateOrder(comandaId: string, data: UpdateOrderInput) {
where: { id: comandaId }, where: { id: comandaId },
data: updateData, data: updateData,
include: { include: {
usuario: {
select: { id: true, nome: true, email: true },
},
itens: { itens: {
include: { produto: true } include: {
produto: true,
usuario: {
select: { id: true, nome: true },
},
}
}, },
pagamentos: true, pagamentos: true,
} }
@@ -360,6 +381,18 @@ export async function deleteItemFromOrder(itemId: string) {
return prisma.comanda.findUnique({ return prisma.comanda.findUnique({
where: { id: item.comandaId }, 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
View 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));
})();

View File

@@ -59,15 +59,19 @@ export async function renderKitchen(container: HTMLElement) {
let currentStatus = 'PENDENTE'; let currentStatus = 'PENDENTE';
let currentFilter = ''; let currentFilter = '';
let autoRefreshInterval: ReturnType<typeof setInterval> | null = null; let autoRefreshInterval: ReturnType<typeof setInterval> | null = null;
let requestId = 0;
const loadItems = async () => { const loadItems = async () => {
const grid = document.getElementById('items-grid')!; const grid = document.getElementById('items-grid')!;
grid.innerHTML = '<p class="card-subtitle">Carregando...</p>'; grid.innerHTML = '<p class="card-subtitle">Carregando...</p>';
const myRequestId = ++requestId;
try { try {
let url = `/painel/itens?status=${currentStatus}`; let url = `/painel/itens?status=${currentStatus}`;
if (currentFilter !== '') url += `&itemCozinha=${currentFilter}`; if (currentFilter !== '') url += `&itemCozinha=${currentFilter}`;
const items = await api.get(url); const items = await api.get(url);
if (myRequestId !== requestId) return;
const currentIds = new Set<string>(items.map((i: any) => i.id)); const currentIds = new Set<string>(items.map((i: any) => i.id));
const previousIds = previousItemIdsByStatus.get(currentStatus); const previousIds = previousItemIdsByStatus.get(currentStatus);
@@ -106,9 +110,10 @@ export async function renderKitchen(container: HTMLElement) {
</div> </div>
<h3 class="card-title">${item.quantidade}× ${item.produto.nome}</h3> <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>` : ''} ${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' })} Pedido às ${new Date(item.criadoEm).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
</p> </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 ? ` ${action.label ? `
<button class="btn" style="width:100%;" data-action="advance" data-item-id="${item.id}" data-next-status="${action.next}"> <button class="btn" style="width:100%;" data-action="advance" data-item-id="${item.id}" data-next-status="${action.next}">
${action.label} ${action.label}

View File

@@ -87,6 +87,10 @@ export async function renderMenu(container: HTMLElement) {
`; `;
let categorias: any[] = []; let categorias: any[] = [];
let allProducts: any[] = [];
let filterCategoria = '';
let filterAtivo = '';
let filterCozinha = '';
const loadCategorias = async () => { const loadCategorias = async () => {
categorias = await api.get('/categorias'); categorias = await api.get('/categorias');
@@ -96,19 +100,90 @@ export async function renderMenu(container: HTMLElement) {
const renderTabProdutos = async () => { const renderTabProdutos = async () => {
const tc = document.getElementById('tab-content')!; const tc = document.getElementById('tab-content')!;
const products = await api.get('/produtos'); allProducts = await api.get('/produtos');
tc.innerHTML = ` tc.innerHTML = `
<div class="page-header" style="margin-bottom:1rem;"> <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> <button class="btn" id="btn-novo-produto"> Novo Produto</button>
</div> </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"> <div class="table-wrap">
<table> <table>
<thead> <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> <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> </thead>
<tbody> <tbody id="produtos-tbody"></tbody>
${products.map((p: any) => ` </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> <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="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="Categoria">${p.categoria?.nome ?? '—'}</td>
@@ -125,16 +200,7 @@ export async function renderMenu(container: HTMLElement) {
<button class="btn btn-sm btn-danger" onclick="window.deleteProduct('${p.id}','${p.nome}')">🗑</button> <button class="btn btn-sm btn-danger" onclick="window.deleteProduct('${p.id}','${p.nome}')">🗑</button>
</td> </td>
</tr> </tr>
`).join('')} `).join('');
</tbody>
</table>
</div>
`;
document.getElementById('btn-novo-produto')?.addEventListener('click', () => {
clearProductForm();
(document.getElementById('modal-produto') as HTMLElement).style.display = 'flex';
});
}; };
const renderTabCategorias = async () => { const renderTabCategorias = async () => {

View File

@@ -108,6 +108,7 @@ export async function renderOrders(container: HTMLElement) {
<span class="detalhes-summary-client" id="detalhes-label-client"></span> <span class="detalhes-summary-client" id="detalhes-label-client"></span>
</div> </div>
<div id="detalhes-label-status"></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>
<div class="detalhes-edit-section"> <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:center;">Qtd</th>
<th style="text-align:right;">Subtotal</th> <th style="text-align:right;">Subtotal</th>
<th>Obs</th> <th>Obs</th>
<th>Adicionado por</th>
<th>Status</th> <th>Status</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
@@ -294,6 +296,7 @@ async function loadComandas(params?: { status?: string; dataInicio?: string; dat
<tr> <tr>
<th>Identificador</th> <th>Identificador</th>
<th>Cliente</th> <th>Cliente</th>
<th>Aberto por</th>
<th>Status</th> <th>Status</th>
<th>Total</th> <th>Total</th>
<th>Aberta em</th> <th>Aberta em</th>
@@ -305,6 +308,7 @@ async function loadComandas(params?: { status?: string; dataInicio?: string; dat
<tr> <tr>
<td data-label="Mesa" class="cmd-ident"><strong>${c.identificador}</strong></td> <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="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="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="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> <td data-label="Aberta em" class="cmd-data">${new Date(c.criadoEm).toLocaleString('pt-BR')}</td>
@@ -607,6 +611,12 @@ function bindOrderEvents() {
document.getElementById('detalhes-label-client')!.textContent = c.nomeCliente ? `· ${c.nomeCliente}` : ''; document.getElementById('detalhes-label-client')!.textContent = c.nomeCliente ? `· ${c.nomeCliente}` : '';
document.getElementById('detalhes-label-status')!.innerHTML = statusBadge(c.status); 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 // Render itens
const tbody = document.getElementById('detalhes-itens-tbody')!; const tbody = document.getElementById('detalhes-itens-tbody')!;
const itens = c.itens ?? []; const itens = c.itens ?? [];
@@ -614,7 +624,7 @@ function bindOrderEvents() {
const canDelete = user && ['ADMIN', 'CAIXA'].includes(user.role) && c.status === 'ABERTA'; const canDelete = user && ['ADMIN', 'CAIXA'].includes(user.role) && c.status === 'ABERTA';
if (itens.length === 0) { 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 { } else {
tbody.innerHTML = itens.map((i: any) => ` tbody.innerHTML = itens.map((i: any) => `
<tr> <tr>
@@ -627,6 +637,7 @@ function bindOrderEvents() {
: (i.observacao ?? '') : (i.observacao ?? '')
} }
</td> </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="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"> <td data-label="Ações" class="item-actions">
${canDelete ? ` ${canDelete ? `