feat: sistema GastroBar PDV completo

- Backend: Fastify + Prisma/SQLite com autenticacao JWT
- CRUD completo: usuarios, categorias, produtos, comandas
- Sistema de comandas com itens, status, pagamento
- Controle de estoque (entrada/saida automatica)
- Relatorios e dashboard com KPIs
- Notificacoes sonoras na cozinha
- Frontend: SPA vanilla TS com tema dark glassmorphism
- Mobile-first com bottom-sheet e cards compactos
- Busca de comandas por periodo
- Modal de detalhes com secao editavel colapsavel
This commit is contained in:
2026-07-21 12:47:15 -03:00
commit 402ac776ac
55 changed files with 7403 additions and 0 deletions

156
web/src/views/users.ts Normal file
View File

@@ -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')}
<div class="container animate-fade-in">
<div class="page-header">
<h1>Equipe 👥</h1>
<button class="btn" id="btn-novo-usuario"> Novo Usuário</button>
</div>
<div id="users-list"><p class="card-subtitle">Carregando...</p></div>
</div>
<!-- Modal Usuário -->
<div id="modal-usuario" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3 id="modal-user-title">Novo Usuário</h3>
<button class="btn-icon" onclick="document.getElementById('modal-usuario').style.display='none'">✕</button>
</div>
<div class="modal-body">
<form id="form-usuario">
<input type="hidden" id="user-id">
<div class="form-group">
<label>Nome Completo *</label>
<input type="text" id="u-nome" class="form-control" required>
</div>
<div class="form-group">
<label>E-mail *</label>
<input type="email" id="u-email" class="form-control" required>
</div>
<div class="form-group" id="senha-group">
<label>Senha *</label>
<input type="password" id="u-senha" class="form-control" placeholder="Mínimo 6 caracteres">
</div>
<div class="form-group">
<label>Cargo (Role) *</label>
<select id="u-role" class="form-control" required>
${ROLE_OPTIONS.map(r => `<option value="${r}">${r}</option>`).join('')}
</select>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.5rem;">
<input type="checkbox" id="u-ativo" checked>
<label for="u-ativo" style="cursor:pointer;color:var(--text-primary);">Usuário Ativo</label>
</div>
<button type="submit" class="btn" style="width:100%;">Salvar</button>
</form>
</div>
</div>
</div>
`;
await loadUsers();
bindUserEvents();
}
async function loadUsers() {
const list = document.getElementById('users-list')!;
try {
const users = await api.get('/usuarios');
list.innerHTML = `
<div class="table-wrap">
<table>
<thead>
<tr><th>Nome</th><th>E-mail</th><th>Cargo</th><th>Status</th><th>Desde</th><th>Ações</th></tr>
</thead>
<tbody>
${users.map((u: any) => `
<tr>
<td data-label="Nome"><strong>${u.nome}</strong></td>
<td data-label="E-mail" style="color:var(--text-secondary)">${u.email}</td>
<td data-label="Cargo">${roleBadge(u.role)}</td>
<td data-label="Status"><span class="badge ${u.ativo ? 'badge-success' : 'badge-warning'}">${u.ativo ? 'Ativo' : 'Inativo'}</span></td>
<td data-label="Desde" style="color:var(--text-secondary)">${new Date(u.criadoEm).toLocaleDateString('pt-BR')}</td>
<td data-label="Ações" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<button class="btn btn-sm btn-secondary" onclick="window.editUser(${JSON.stringify(u).replace(/"/g, '&quot;')})">✏️ Editar</button>
<button class="btn btn-sm btn-danger" onclick="window.toggleUser('${u.id}','${u.nome}',${u.ativo})">
${u.ativo ? '🚫 Desativar' : '✅ Ativar'}
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} catch (err: any) {
list.innerHTML = `<p style="color:var(--accent-primary)">Erro: ${err.message}</p>`;
}
}
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';
}