208 lines
8.3 KiB
TypeScript
208 lines
8.3 KiB
TypeScript
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();
|
||
}
|