restaurante / src /pages /WaiterPOS.jsx
dimensionalpulsar's picture
Upload 47 files
18f4249 verified
import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
import { db } from '../firebase/config';
import { ref, onValue, push, set } from 'firebase/database';
import { LogOut, Coffee, Send, Trash2, ShoppingBag, Truck, Receipt, CheckCircle, XCircle, CreditCard, Banknote, QrCode } from 'lucide-react';
export default function WaiterPOS() {
const { logout, currentUser } = useAuth();
const navigate = useNavigate();
const [menu, setMenu] = useState([]);
const [activeTable, setActiveTable] = useState(null);
const [cart, setCart] = useState([]);
const [orderType, setOrderType] = useState('Mesa'); // Mesa, Llevar, Delivery
const [discount, setDiscount] = useState(0);
const [tip, setTip] = useState(0);
const [paymentMethod, setPaymentMethod] = useState('Efectivo');
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const [tableStatuses, setTableStatuses] = useState({});
const [selectedCategory, setSelectedCategory] = useState('Todos');
// Fetch Menu
useEffect(() => {
onValue(ref(db, 'menu'), (snapshot) => {
const data = snapshot.val();
if (data) {
setMenu(Object.keys(data).map(k => ({ id: k, ...data[k] })));
}
});
onValue(ref(db, 'tables'), (snapshot) => {
const data = snapshot.val();
if (data) setTableStatuses(data);
});
}, []);
const handleLogout = async () => {
await logout();
navigate('/login');
};
const addToCart = async (product) => {
if (!activeTable && orderType === 'Mesa') {
alert("Selecciona una mesa primero.");
return;
}
// Mark table as occupied
if (orderType === 'Mesa' && activeTable) {
await set(ref(db, `tables/${activeTable}`), 'occupied');
}
const existing = cart.find(item => item.id === product.id);
if (existing) {
setCart(cart.map(item => item.id === product.id ? { ...item, qty: item.qty + 1 } : item));
} else {
setCart([...cart, { ...product, qty: 1, note: '' }]);
}
};
const updateCartItemNote = (id, note) => {
setCart(cart.map(item => item.id === id ? { ...item, note } : item));
};
const removeFromCart = (id) => {
setCart(cart.filter(item => item.id !== id));
};
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.qty), 0);
const discountAmount = (subtotal * discount) / 100;
const totalCart = subtotal - discountAmount + Number(tip || 0);
const sendOrder = async () => {
if ((orderType === 'Mesa' && !activeTable) || cart.length === 0) return;
const orderRef = push(ref(db, 'orders'));
const orderData = {
table: orderType === 'Mesa' ? activeTable : orderType,
type: orderType,
waiter: currentUser?.email,
items: cart,
subtotal,
discount,
tip,
total: totalCart,
paymentMethod: paymentMethod,
status: 'pending',
timestamp: Date.now()
};
await set(orderRef, orderData);
if (!isCheckoutOpen) {
setCart([]);
setActiveTable(null);
alert('¡Comanda enviada a cocina exitosamente!');
} else {
// Finalizar y descontar stock
await set(ref(db, `orders/${orderRef.key}/status`), 'completed');
// Mark table as free
if (orderType === 'Mesa' && activeTable) {
await set(ref(db, `tables/${activeTable}`), 'free');
}
// Lógica de Descuento de Inventario
// ... (existing logic continues)
for (const item of cart) {
// Buscamos el producto en el menú para ver su receta (ingredients)
// Nota: En un entorno real, esto se haría en el backend para evitar race conditions y asegurar integridad.
const productRef = ref(db, `menu/${item.id}`);
onValue(productRef, async (snapshot) => {
const product = snapshot.val();
if (product && product.ingredients) {
for (const ing of product.ingredients) {
const invRef = ref(db, `inventory/${ing.id}/quantity`);
// Obtenemos cantidad actual y restamos (qty_producto * qty_ingrediente)
onValue(ref(db, `inventory/${ing.id}`), async (invSnap) => {
const invData = invSnap.val();
if (invData) {
const newQty = invData.quantity - (item.qty * ing.qty);
await set(invRef, newQty);
}
}, { onlyOnce: true });
}
}
}, { onlyOnce: true });
}
setIsCheckoutOpen(false);
setCart([]);
setActiveTable(null);
alert('¡Venta realizada y stock actualizado!');
}
};
const printTicket = () => {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head><title>Ticket de Venta</title></head>
<body style="font-family: monospace; width: 300px; padding: 20px;">
<h2 style="text-align: center;">RESTAURANT OS</h2>
<hr/>
<p>Mesa: ${activeTable || orderType}</p>
<p>Fecha: ${new Date().toLocaleString()}</p>
<hr/>
${cart.map(item => `<p>${item.qty}x ${item.name.padEnd(20)} $${(item.price * item.qty).toFixed(2)}</p>`).join('')}
<hr/>
<p>Subtotal: $${subtotal.toFixed(2)}</p>
<p>Descuento (${discount}%): -$${discountAmount.toFixed(2)}</p>
<p>Propina: $${Number(tip || 0).toFixed(2)}</p>
<h3 style="text-align: right;">Total: $${totalCart.toFixed(2)}</h3>
<p style="text-align: center;">¡Gracias por su visita!</p>
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
};
return (
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
{/* Navbar */}
<header className="glass-panel" style={{ padding: '1rem 2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderRadius: '0', borderBottom: '1px solid var(--border-subtle)', background: 'var(--bg-base)' }}>
<h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>Terminal POS</h1>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{currentUser?.email}</span>
<button className="btn-glass" onClick={() => window.open('/menu', '_blank')} style={{ padding: '0.5rem 1rem' }}>Carta Digital</button>
<button className="btn-glass" onClick={() => window.open('/kitchen', '_blank')} style={{ padding: '0.5rem 1rem' }}>Cocina</button>
<button className="btn-glass" onClick={handleLogout} style={{ padding: '0.5rem 1rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<LogOut size={16} /> Salir
</button>
</div>
</header>
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Left Column: Tables & Menu */}
<div style={{ flex: '65%', padding: '1.5rem', overflowY: 'auto', background: 'rgba(255,255,255,0.01)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>Tipo de Pedido</h2>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{[
{ id: 'Mesa', icon: <Coffee size={18} /> },
{ id: 'Llevar', icon: <ShoppingBag size={18} /> },
{ id: 'Delivery', icon: <Truck size={18} /> }
].map(type => (
<button
key={type.id}
onClick={() => { setOrderType(type.id); if(type.id !== 'Mesa') setActiveTable(null); }}
className={orderType === type.id ? 'btn-primary' : 'btn-glass'}
style={{ padding: '0.5rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}
>
{type.icon} {type.id}
</button>
))}
</div>
</div>
{orderType === 'Mesa' && (
<div style={{ display: 'flex', gap: '1rem', overflowX: 'auto', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(table => (
<button
key={table}
onClick={() => setActiveTable(table)}
style={{
minWidth: '70px', height: '70px', borderRadius: 'var(--radius-lg)',
background: activeTable === table ? 'var(--primary)' : (tableStatuses[table] === 'occupied' ? 'rgba(255,90,95,0.2)' : 'rgba(255,255,255,0.05)'),
border: activeTable === table ? '2px solid transparent' : (tableStatuses[table] === 'occupied' ? '1px solid var(--primary)' : '1px solid var(--border-subtle)'),
color: activeTable === table ? '#fff' : (tableStatuses[table] === 'occupied' ? 'var(--primary)' : 'var(--text-main)'),
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: '600', transition: 'all 0.2s', cursor: 'pointer'
}}
>
{table}
</button>
))}
</div>
)}
<h2 className="text-gradient" style={{ fontSize: '1.25rem', marginBottom: '1rem' }}>Catálogo de Productos</h2>
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
{['Todos', ...new Set(menu.map(p => p.category))].map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={selectedCategory === cat ? 'btn-primary' : 'btn-glass'}
style={{ padding: '0.4rem 1rem', fontSize: '0.85rem', whiteSpace: 'nowrap' }}
>
{cat}
</button>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '1rem' }}>
{menu
.filter(p => selectedCategory === 'Todos' || p.category === selectedCategory)
.map(product => (
<button
key={product.id}
onClick={() => addToCart(product)}
className="glass-card"
style={{ textAlign: 'left', padding: '1rem', borderColor: 'var(--border-subtle)', position: 'relative' }}
>
<p style={{ fontWeight: '600', marginBottom: '0.5rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{product.name}
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: 'var(--success)', fontWeight: '700' }}>${Number(product.price).toFixed(2)}</span>
<span style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{product.category}</span>
</div>
</button>
))}
</div>
</div>
{/* Right Column: Order Details */}
<div style={{ flex: '35%', background: 'var(--bg-card)', borderLeft: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>
{orderType === 'Mesa' ? `Mesa ${activeTable || '?'}` : orderType}
</h2>
<button onClick={() => setCart([])} style={{ color: 'var(--danger)', fontSize: '0.8rem', background: 'none', border: 'none', cursor: 'not-allowed' }} disabled={cart.length === 0}>
Limpiar
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{cart.map((item) => (
<div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.75rem', background: 'rgba(255,255,255,0.03)', borderRadius: 'var(--radius-md)' }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '600', fontSize: '0.95rem' }}>{item.name}</div>
<div style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>{item.qty} x ${item.price}</div>
<input
type="text"
placeholder="Nota / Extra..."
value={item.note}
onChange={(e) => updateCartItemNote(item.id, e.target.value)}
style={{ width: '100%', marginTop: '0.5rem', padding: '4px 8px', fontSize: '0.75rem', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff' }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<span style={{ fontWeight: '700' }}>${(item.qty * item.price).toFixed(2)}</span>
<button onClick={() => removeFromCart(item.id)} style={{ color: 'var(--danger)', background: 'none', border: 'none', cursor: 'pointer' }}>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
<div style={{ padding: '1.5rem', borderTop: '1px solid var(--border-subtle)', background: 'rgba(0,0,0,0.2)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Subtotal:</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--primary)' }}>
<span>Descuento ({discount}%):</span>
<span>-${discountAmount.toFixed(2)}</span>
</div>
{tip > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--success)' }}>
<span>Propina:</span>
<span>+${Number(tip).toFixed(2)}</span>
</div>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.4rem', fontWeight: '900', marginBottom: '1.5rem' }}>
<span>Total:</span>
<span className="text-gradient-primary">${totalCart.toFixed(2)}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<button
className="btn-glass"
onClick={sendOrder}
disabled={cart.length === 0 || (orderType === 'Mesa' && !activeTable)}
style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
>
<Send size={18} /> Comanda
</button>
<button
className="btn-primary"
onClick={() => setIsCheckoutOpen(true)}
disabled={cart.length === 0}
style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
>
<Receipt size={18} /> Pagar
</button>
</div>
</div>
</div>
</div>
{/* Checkout Modal */}
{isCheckoutOpen && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(10px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
}}>
<div className="glass-panel animate-scale-in" style={{ width: '100%', maxWidth: '500px', padding: '2.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<h2 className="text-gradient" style={{ fontSize: '1.75rem' }}>Finalizar Venta</h2>
<button onClick={() => setIsCheckoutOpen(false)} style={{ background: 'none', border: 'none', color: 'var(--text-muted)' }}>
<XCircle size={28} />
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Descuento (%)</label>
<select
value={discount}
onChange={(e) => setDiscount(Number(e.target.value))}
style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', color: '#fff' }}
>
<option value="0">Sin Descuento</option>
<option value="5">5% Promo</option>
<option value="10">10% Cortesía</option>
<option value="15">15% Empleado</option>
<option value="50">50% VIP</option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Propina (Efectivo/Sugerida)</label>
<input
type="number"
placeholder="$ 0.00"
value={tip}
onChange={(e) => setTip(e.target.value)}
style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', color: '#fff' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Método de Pago</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.75rem' }}>
{[
{ id: 'Efectivo', icon: <Banknote size={20} /> },
{ id: 'Tarjeta', icon: <CreditCard size={20} /> },
{ id: 'QR', icon: <QrCode size={20} /> }
].map(m => (
<button
key={m.id}
onClick={() => setPaymentMethod(m.id)}
className={paymentMethod === m.id ? 'btn-primary' : 'btn-glass'}
style={{ padding: '1rem 0.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', fontSize: '0.8rem' }}
>
{m.icon} {m.id}
</button>
))}
</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.02)', padding: '1.5rem', borderRadius: 'var(--radius-lg)', marginTop: '1rem', border: '1px dashed var(--border-subtle)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.5rem', fontWeight: '900' }}>
<span>Total a Pagar:</span>
<span className="text-gradient">${totalCart.toFixed(2)}</span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
<button className="btn-glass" onClick={printTicket} style={{ padding: '1rem' }}>
<Receipt size={20} /> Preview Ticket
</button>
<button className="btn-primary" onClick={sendOrder} style={{ padding: '1rem' }}>
<CheckCircle size={20} /> Confirmar Pago
</button>
</div>
</div>
</div>
</div>
)}
<style>{`
.btn-glass {
background: rgba(255,255,255,0.05);
border: 1px solid var(--border-subtle);
color: var(--text-main);
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-glass:hover {
background: rgba(255,255,255,0.1);
transform: translateY(-2px);
}
.btn-primary {
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.animate-scale-in {
animation: scaleIn 0.3s ease-out;
}
@keyframes scaleIn {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
`}</style>
</div>
);
}