Files
CES_SERVE_WEB/web/src/views/kitchen.ts

208 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api } from '../api';
import { renderNavbar, toast } from '../utils';
let previousItemIdsByStatus: Map<string, Set<string>> = new Map();
let audioCtx: AudioContext | null = null;
function playNotificationSound() {
try {
if (!audioCtx) audioCtx = new AudioContext();
if (audioCtx.state === 'suspended') audioCtx.resume();
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')}
<div class="container animate-fade-in">
<div class="page-header">
<h1>Painel de Preparo 👨‍🍳</h1>
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
<label style="color:var(--text-secondary);font-size:0.875rem;">Filtrar:</label>
<button class="btn btn-secondary btn-sm filter-btn active" data-filter="">Todos</button>
<button class="btn btn-secondary btn-sm filter-btn" data-filter="true">🍳 Cozinha</button>
<button class="btn btn-secondary btn-sm filter-btn" data-filter="false">🍹 Bar</button>
<button class="btn btn-secondary btn-sm" id="btn-refresh">🔄 Atualizar</button>
<label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer;color:var(--text-secondary);font-size:0.875rem;">
<input type="checkbox" id="auto-refresh-toggle" checked> Auto-refresh
</label>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" data-status="PENDENTE">🔴 Pendente</button>
<button class="tab-btn" data-status="PREPARANDO">🟡 Preparando</button>
<button class="tab-btn" data-status="PRONTO">🟢 Pronto</button>
<button class="tab-btn" data-status="ENTREGUE">✅ Entregue</button>
</div>
<div class="grid" id="items-grid">
<p>Carregando pedidos...</p>
</div>
</div>
`;
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);
if (previousIds && previousIds.size > 0) {
const newItems = items.filter((i: any) => !previousIds.has(i.id));
if (newItems.length > 0) {
playNotificationSound();
toast(`${newItems.length} novo(s) pedido(s) recebido(s)!`, 'success');
}
}
previousItemIdsByStatus.set(currentStatus, currentIds);
if (!items.length) {
grid.innerHTML = `<p class="card-subtitle" style="grid-column:1/-1;text-align:center;padding:3rem 0;">
Nenhum pedido com status <strong>${currentStatus}</strong> 🎉
</p>`;
return;
}
const statusActions: Record<string, { label: string; next: string }> = {
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 `
<div class="card animate-fade-in">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem;">
<span class="badge ${item.produto.itemCozinha ? 'badge-cozinha' : 'badge-barman'}">
${item.produto.itemCozinha ? '🍳 Cozinha' : '🍹 Bar'}
</span>
<span class="card-subtitle" style="margin:0;">Mesa: <strong>${item.comanda.identificador}</strong></span>
</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: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}
</button>
` : `<p class="card-subtitle" style="text-align:center;">✅ Entregue</p>`}
</div>
`;
}).join('');
} catch (err: any) {
grid.innerHTML = `<p style="color:var(--accent-primary)">Erro: ${err.message}</p>`;
}
};
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();
});
// Event delegation for advance buttons
document.getElementById('items-grid')?.addEventListener('click', async (e) => {
const btn = (e.target as HTMLElement).closest('[data-action="advance"]') as HTMLElement | null;
if (!btn) return;
const id = btn.dataset.itemId!;
const status = btn.dataset.nextStatus!;
try {
await api.patch(`/painel/itens/${id}/status`, { status });
toast('Status atualizado!', 'success');
loadItems();
} catch (err: any) {
toast(err.message, 'error');
}
});
// Pause auto-refresh when tab is hidden
const handleVisibilityChange = () => {
if (document.hidden) {
stopAutoRefresh();
} else {
const checkbox = document.getElementById('auto-refresh-toggle') as HTMLInputElement | null;
if (checkbox?.checked) startAutoRefresh();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// Cleanup on navigation
const cleanup = () => {
stopAutoRefresh();
previousItemIdsByStatus.clear();
document.removeEventListener('visibilitychange', handleVisibilityChange);
audioCtx?.close();
audioCtx = null;
};
window.addEventListener('popstate', cleanup);
await loadItems();
startAutoRefresh();
}