Spaces:
Running
Running
Upload 47 files
Browse files
src/components/admin/InventoryControl.jsx
CHANGED
|
@@ -79,6 +79,10 @@ export default function InventoryControl() {
|
|
| 79 |
<label style={labelStyle}>Cantidad</label>
|
| 80 |
<input type="number" value={formData.quantity} onChange={e => setFormData({...formData, quantity: e.target.value})} required style={inputStyle} placeholder="50" />
|
| 81 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
<div style={{ flex: 1, minWidth: '80px' }}>
|
| 83 |
<label style={labelStyle}>Unidad</label>
|
| 84 |
<input type="text" value={formData.unit} onChange={e => setFormData({...formData, unit: e.target.value})} style={inputStyle} placeholder="Kg, Lt, Und" />
|
|
|
|
| 79 |
<label style={labelStyle}>Cantidad</label>
|
| 80 |
<input type="number" value={formData.quantity} onChange={e => setFormData({...formData, quantity: e.target.value})} required style={inputStyle} placeholder="50" />
|
| 81 |
</div>
|
| 82 |
+
<div style={{ flex: 1, minWidth: '80px' }}>
|
| 83 |
+
<label style={labelStyle}>Mínimo</label>
|
| 84 |
+
<input type="number" value={formData.minStock} onChange={e => setFormData({...formData, minStock: e.target.value})} style={inputStyle} placeholder="10" />
|
| 85 |
+
</div>
|
| 86 |
<div style={{ flex: 1, minWidth: '80px' }}>
|
| 87 |
<label style={labelStyle}>Unidad</label>
|
| 88 |
<input type="text" value={formData.unit} onChange={e => setFormData({...formData, unit: e.target.value})} style={inputStyle} placeholder="Kg, Lt, Und" />
|
src/components/admin/MenuEditor.jsx
CHANGED
|
@@ -9,7 +9,7 @@ export default function MenuEditor() {
|
|
| 9 |
const [inventory, setInventory] = useState([]);
|
| 10 |
const [formData, setFormData] = useState({
|
| 11 |
name: '', price: '', category: 'Principales', image: '',
|
| 12 |
-
variants: [], ingredients: []
|
| 13 |
});
|
| 14 |
const [editingItem, setEditingItem] = useState(null);
|
| 15 |
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
@@ -55,7 +55,7 @@ export default function MenuEditor() {
|
|
| 55 |
if (!formData.name || !formData.price) return;
|
| 56 |
const newRef = push(ref(db, 'menu'));
|
| 57 |
await set(newRef, { ...formData, price: parseFloat(formData.price), active: true });
|
| 58 |
-
setFormData({ name: '', price: '', category: 'Principales', image: '', variants: [], ingredients: [] });
|
| 59 |
};
|
| 60 |
|
| 61 |
const handleDelete = async (id) => {
|
|
@@ -68,6 +68,12 @@ export default function MenuEditor() {
|
|
| 68 |
else setFormData({ ...formData, ingredients: [...formData.ingredients, newIng] });
|
| 69 |
};
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
const handleSaveEdit = async () => {
|
| 72 |
const { id, ...updates } = editingItem;
|
| 73 |
await update(ref(db, `menu/${id}`), { ...updates, price: parseFloat(updates.price) });
|
|
@@ -146,6 +152,29 @@ export default function MenuEditor() {
|
|
| 146 |
))}
|
| 147 |
</div>
|
| 148 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
</section>
|
| 150 |
|
| 151 |
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
|
@@ -211,6 +240,28 @@ export default function MenuEditor() {
|
|
| 211 |
</div>
|
| 212 |
</div>
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
|
| 215 |
<label style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'var(--primary)', padding: '0.8rem', borderRadius: '8px', cursor: 'pointer', justifyContent: 'center' }}>
|
| 216 |
<Upload size={18} /> {uploading ? 'Subiendo...' : 'Cambiar Imagen'}
|
|
|
|
| 9 |
const [inventory, setInventory] = useState([]);
|
| 10 |
const [formData, setFormData] = useState({
|
| 11 |
name: '', price: '', category: 'Principales', image: '',
|
| 12 |
+
variants: [], ingredients: [], extras: []
|
| 13 |
});
|
| 14 |
const [editingItem, setEditingItem] = useState(null);
|
| 15 |
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
| 55 |
if (!formData.name || !formData.price) return;
|
| 56 |
const newRef = push(ref(db, 'menu'));
|
| 57 |
await set(newRef, { ...formData, price: parseFloat(formData.price), active: true });
|
| 58 |
+
setFormData({ name: '', price: '', category: 'Principales', image: '', variants: [], ingredients: [], extras: [] });
|
| 59 |
};
|
| 60 |
|
| 61 |
const handleDelete = async (id) => {
|
|
|
|
| 68 |
else setFormData({ ...formData, ingredients: [...formData.ingredients, newIng] });
|
| 69 |
};
|
| 70 |
|
| 71 |
+
const addExtraToProduct = (isEditing) => {
|
| 72 |
+
const newExtra = { name: '', price: 0 };
|
| 73 |
+
if (isEditing) setEditingItem({ ...editingItem, extras: [...(editingItem.extras || []), newExtra] });
|
| 74 |
+
else setFormData({ ...formData, extras: [...(formData.extras || []), newExtra] });
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
const handleSaveEdit = async () => {
|
| 78 |
const { id, ...updates } = editingItem;
|
| 79 |
await update(ref(db, `menu/${id}`), { ...updates, price: parseFloat(updates.price) });
|
|
|
|
| 152 |
))}
|
| 153 |
</div>
|
| 154 |
)}
|
| 155 |
+
|
| 156 |
+
<div style={{ marginTop: '1.5rem' }}>
|
| 157 |
+
<button type="button" onClick={() => addExtraToProduct(false)} className="btn-glass" style={{ padding: '0.5rem 1rem', fontSize: '0.85rem' }}>
|
| 158 |
+
<Plus size={16} /> Agregar Extra (Ej. Queso Extra)
|
| 159 |
+
</button>
|
| 160 |
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem', marginTop: '1rem' }}>
|
| 161 |
+
{(formData.extras || []).map((ext, idx) => (
|
| 162 |
+
<div key={idx} className="glass-panel" style={{ padding: '0.5rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
| 163 |
+
<input type="text" placeholder="Extra" value={ext.name} onChange={e => {
|
| 164 |
+
const newExts = [...formData.extras];
|
| 165 |
+
newExts[idx].name = e.target.value;
|
| 166 |
+
setFormData({...formData, extras: newExts});
|
| 167 |
+
}} style={{...inputStyle, padding: '0.4rem', width: '120px'}} />
|
| 168 |
+
<input type="number" placeholder="$" value={ext.price} onChange={e => {
|
| 169 |
+
const newExts = [...formData.extras];
|
| 170 |
+
newExts[idx].price = parseFloat(e.target.value);
|
| 171 |
+
setFormData({...formData, extras: newExts});
|
| 172 |
+
}} style={{...inputStyle, padding: '0.4rem', width: '60px'}} />
|
| 173 |
+
<button type="button" onClick={() => setFormData({...formData, extras: formData.extras.filter((_, i) => i !== idx)})} style={{color: 'var(--primary)', background: 'none', border: 'none'}}><X size={16}/></button>
|
| 174 |
+
</div>
|
| 175 |
+
))}
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
</section>
|
| 179 |
|
| 180 |
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
|
|
|
| 240 |
</div>
|
| 241 |
</div>
|
| 242 |
|
| 243 |
+
<div>
|
| 244 |
+
<label style={labelStyle}>Extras Sugeridos (Opcional)</label>
|
| 245 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
| 246 |
+
{(editingItem.extras || []).map((ext, i) => (
|
| 247 |
+
<div key={i} style={{ display: 'flex', gap: '0.5rem' }}>
|
| 248 |
+
<input type="text" placeholder="Extra" value={ext.name} onChange={e => {
|
| 249 |
+
const exts = [...(editingItem.extras || [])];
|
| 250 |
+
exts[i].name = e.target.value;
|
| 251 |
+
setEditingItem({...editingItem, extras: exts});
|
| 252 |
+
}} style={{...inputStyle, flex: 2}} />
|
| 253 |
+
<input type="number" placeholder="$" value={ext.price} onChange={e => {
|
| 254 |
+
const exts = [...(editingItem.extras || [])];
|
| 255 |
+
exts[i].price = parseFloat(e.target.value);
|
| 256 |
+
setEditingItem({...editingItem, extras: exts});
|
| 257 |
+
}} style={{...inputStyle, flex: 1}} />
|
| 258 |
+
<button onClick={() => setEditingItem({...editingItem, extras: editingItem.extras.filter((_, idx) => idx !== i)})} style={{color: 'var(--primary)'}}><Trash2 size={18} /></button>
|
| 259 |
+
</div>
|
| 260 |
+
))}
|
| 261 |
+
<button onClick={() => addExtraToProduct(true)} className="btn-glass" style={{ padding: '0.5rem', fontSize: '0.8rem' }}><Plus size={14} /> Añadir Extra</button>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
|
| 266 |
<label style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'var(--primary)', padding: '0.8rem', borderRadius: '8px', cursor: 'pointer', justifyContent: 'center' }}>
|
| 267 |
<Upload size={18} /> {uploading ? 'Subiendo...' : 'Cambiar Imagen'}
|
src/components/admin/Reports.jsx
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
| 6 |
LineElement, Title, Tooltip, Legend, BarElement
|
| 7 |
} from 'chart.js';
|
| 8 |
import { Line, Bar } from 'react-chartjs-2';
|
| 9 |
-
import { TrendingUp, Package, Users as UsersIcon, Calendar } from 'lucide-react';
|
|
|
|
| 10 |
|
| 11 |
ChartJS.register(
|
| 12 |
CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, BarElement
|
|
@@ -16,6 +17,9 @@ export default function Reports() {
|
|
| 16 |
const [salesData, setSalesData] = useState([0, 0, 0, 0, 0, 0, 0]);
|
| 17 |
const [topProducts, setTopProducts] = useState({ labels: [], data: [] });
|
| 18 |
const [stats, setStats] = useState({ totalSales: 0, orderCount: 0, avgTicket: 0 });
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
onValue(ref(db, 'orders'), (snapshot) => {
|
|
@@ -24,16 +28,22 @@ export default function Reports() {
|
|
| 24 |
const orders = Object.values(data);
|
| 25 |
const weeklySales = [0, 0, 0, 0, 0, 0, 0];
|
| 26 |
const productCounts = {};
|
|
|
|
| 27 |
let total = 0;
|
|
|
|
| 28 |
|
| 29 |
orders.forEach(order => {
|
| 30 |
if (order.status === 'completed') {
|
| 31 |
total += order.total;
|
| 32 |
-
|
| 33 |
-
const day =
|
| 34 |
-
const index = (day + 6) % 7;
|
| 35 |
weeklySales[index] += order.total;
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
order.items.forEach(item => {
|
| 38 |
productCounts[item.name] = (productCounts[item.name] || 0) + item.qty;
|
| 39 |
});
|
|
@@ -41,6 +51,8 @@ export default function Reports() {
|
|
| 41 |
});
|
| 42 |
|
| 43 |
setSalesData(weeklySales);
|
|
|
|
|
|
|
| 44 |
setStats({
|
| 45 |
totalSales: total,
|
| 46 |
orderCount: orders.filter(o => o.status === 'completed').length,
|
|
@@ -56,9 +68,20 @@ export default function Reports() {
|
|
| 56 |
data: sortedProducts.map(p => p[1])
|
| 57 |
});
|
| 58 |
}
|
|
|
|
|
|
|
|
|
|
| 59 |
});
|
| 60 |
}, []);
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
const lineData = {
|
| 63 |
labels: ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'],
|
| 64 |
datasets: [{
|
|
|
|
| 6 |
LineElement, Title, Tooltip, Legend, BarElement
|
| 7 |
} from 'chart.js';
|
| 8 |
import { Line, Bar } from 'react-chartjs-2';
|
| 9 |
+
import { TrendingUp, Package, Users as UsersIcon, Calendar, DollarSign, Plus, Trash2, Receipt } from 'lucide-react';
|
| 10 |
+
import { push, set, remove } from 'firebase/database';
|
| 11 |
|
| 12 |
ChartJS.register(
|
| 13 |
CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, BarElement
|
|
|
|
| 17 |
const [salesData, setSalesData] = useState([0, 0, 0, 0, 0, 0, 0]);
|
| 18 |
const [topProducts, setTopProducts] = useState({ labels: [], data: [] });
|
| 19 |
const [stats, setStats] = useState({ totalSales: 0, orderCount: 0, avgTicket: 0 });
|
| 20 |
+
const [todaySummary, setTodaySummary] = useState({ Efectivo: 0, Tarjeta: 0, QR: 0 });
|
| 21 |
+
const [expenses, setExpenses] = useState([]);
|
| 22 |
+
const [newExpense, setNewExpense] = useState({ desc: '', amount: '', category: 'Insumos' });
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
onValue(ref(db, 'orders'), (snapshot) => {
|
|
|
|
| 28 |
const orders = Object.values(data);
|
| 29 |
const weeklySales = [0, 0, 0, 0, 0, 0, 0];
|
| 30 |
const productCounts = {};
|
| 31 |
+
const todayPayments = { Efectivo: 0, Tarjeta: 0, QR: 0 };
|
| 32 |
let total = 0;
|
| 33 |
+
const todayStr = new Date().toLocaleDateString();
|
| 34 |
|
| 35 |
orders.forEach(order => {
|
| 36 |
if (order.status === 'completed') {
|
| 37 |
total += order.total;
|
| 38 |
+
const orderDay = new Date(order.timestamp);
|
| 39 |
+
const day = orderDay.getDay();
|
| 40 |
+
const index = (day + 6) % 7;
|
| 41 |
weeklySales[index] += order.total;
|
| 42 |
|
| 43 |
+
if (orderDay.toLocaleDateString() === todayStr) {
|
| 44 |
+
todayPayments[order.paymentMethod || 'Efectivo'] += order.total;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
order.items.forEach(item => {
|
| 48 |
productCounts[item.name] = (productCounts[item.name] || 0) + item.qty;
|
| 49 |
});
|
|
|
|
| 51 |
});
|
| 52 |
|
| 53 |
setSalesData(weeklySales);
|
| 54 |
+
setTodaySummary(todayPayments);
|
| 55 |
+
// ... (rest of the stats logic continues)
|
| 56 |
setStats({
|
| 57 |
totalSales: total,
|
| 58 |
orderCount: orders.filter(o => o.status === 'completed').length,
|
|
|
|
| 68 |
data: sortedProducts.map(p => p[1])
|
| 69 |
});
|
| 70 |
}
|
| 71 |
+
onValue(ref(db, 'expenses'), (snapshot) => {
|
| 72 |
+
const data = snapshot.val();
|
| 73 |
+
setExpenses(data ? Object.keys(data).map(k => ({ id: k, ...data[k] })) : []);
|
| 74 |
});
|
| 75 |
}, []);
|
| 76 |
|
| 77 |
+
const handleAddExpense = async (e) => {
|
| 78 |
+
e.preventDefault();
|
| 79 |
+
if (!newExpense.desc || !newExpense.amount) return;
|
| 80 |
+
const expRef = push(ref(db, 'expenses'));
|
| 81 |
+
await set(expRef, { ...newExpense, amount: parseFloat(newExpense.amount), timestamp: Date.now() });
|
| 82 |
+
setNewExpense({ desc: '', amount: '', category: 'Insumos' });
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
const lineData = {
|
| 86 |
labels: ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'],
|
| 87 |
datasets: [{
|
src/pages/WaiterPOS.jsx
CHANGED
|
@@ -16,6 +16,8 @@ export default function WaiterPOS() {
|
|
| 16 |
const [tip, setTip] = useState(0);
|
| 17 |
const [paymentMethod, setPaymentMethod] = useState('Efectivo');
|
| 18 |
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
|
|
|
|
|
|
|
| 19 |
|
| 20 |
// Fetch Menu
|
| 21 |
useEffect(() => {
|
|
@@ -25,6 +27,11 @@ export default function WaiterPOS() {
|
|
| 25 |
setMenu(Object.keys(data).map(k => ({ id: k, ...data[k] })));
|
| 26 |
}
|
| 27 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}, []);
|
| 29 |
|
| 30 |
const handleLogout = async () => {
|
|
@@ -32,19 +39,29 @@ export default function WaiterPOS() {
|
|
| 32 |
navigate('/login');
|
| 33 |
};
|
| 34 |
|
| 35 |
-
const addToCart = (product) => {
|
| 36 |
if (!activeTable && orderType === 'Mesa') {
|
| 37 |
alert("Selecciona una mesa primero.");
|
| 38 |
return;
|
| 39 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const existing = cart.find(item => item.id === product.id);
|
| 41 |
if (existing) {
|
| 42 |
setCart(cart.map(item => item.id === product.id ? { ...item, qty: item.qty + 1 } : item));
|
| 43 |
} else {
|
| 44 |
-
setCart([...cart, { ...product, qty: 1 }]);
|
| 45 |
}
|
| 46 |
};
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const removeFromCart = (id) => {
|
| 49 |
setCart(cart.filter(item => item.id !== id));
|
| 50 |
};
|
|
@@ -80,7 +97,13 @@ export default function WaiterPOS() {
|
|
| 80 |
// Finalizar y descontar stock
|
| 81 |
await set(ref(db, `orders/${orderRef.key}/status`), 'completed');
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
// Lógica de Descuento de Inventario
|
|
|
|
| 84 |
for (const item of cart) {
|
| 85 |
// Buscamos el producto en el menú para ver su receta (ingredients)
|
| 86 |
// Nota: En un entorno real, esto se haría en el backend para evitar race conditions y asegurar integridad.
|
|
@@ -182,9 +205,9 @@ export default function WaiterPOS() {
|
|
| 182 |
onClick={() => setActiveTable(table)}
|
| 183 |
style={{
|
| 184 |
minWidth: '70px', height: '70px', borderRadius: 'var(--radius-lg)',
|
| 185 |
-
background: activeTable === table ? 'var(--primary)' : 'rgba(255,255,255,0.05)',
|
| 186 |
-
border: activeTable === table ? '2px solid transparent' : '1px solid var(--border-subtle)',
|
| 187 |
-
color: activeTable === table ? '#fff' : 'var(--text-main)',
|
| 188 |
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 189 |
fontWeight: '600', transition: 'all 0.2s', cursor: 'pointer'
|
| 190 |
}}
|
|
@@ -195,9 +218,25 @@ export default function WaiterPOS() {
|
|
| 195 |
</div>
|
| 196 |
)}
|
| 197 |
|
| 198 |
-
<h2 className="text-gradient" style={{ fontSize: '1.25rem', marginBottom: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '1rem' }}>
|
| 200 |
-
{menu
|
|
|
|
|
|
|
| 201 |
<button
|
| 202 |
key={product.id}
|
| 203 |
onClick={() => addToCart(product)}
|
|
@@ -233,6 +272,13 @@ export default function WaiterPOS() {
|
|
| 233 |
<div style={{ flex: 1 }}>
|
| 234 |
<div style={{ fontWeight: '600', fontSize: '0.95rem' }}>{item.name}</div>
|
| 235 |
<div style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>{item.qty} x ${item.price}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
</div>
|
| 237 |
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
| 238 |
<span style={{ fontWeight: '700' }}>${(item.qty * item.price).toFixed(2)}</span>
|
|
|
|
| 16 |
const [tip, setTip] = useState(0);
|
| 17 |
const [paymentMethod, setPaymentMethod] = useState('Efectivo');
|
| 18 |
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
|
| 19 |
+
const [tableStatuses, setTableStatuses] = useState({});
|
| 20 |
+
const [selectedCategory, setSelectedCategory] = useState('Todos');
|
| 21 |
|
| 22 |
// Fetch Menu
|
| 23 |
useEffect(() => {
|
|
|
|
| 27 |
setMenu(Object.keys(data).map(k => ({ id: k, ...data[k] })));
|
| 28 |
}
|
| 29 |
});
|
| 30 |
+
|
| 31 |
+
onValue(ref(db, 'tables'), (snapshot) => {
|
| 32 |
+
const data = snapshot.val();
|
| 33 |
+
if (data) setTableStatuses(data);
|
| 34 |
+
});
|
| 35 |
}, []);
|
| 36 |
|
| 37 |
const handleLogout = async () => {
|
|
|
|
| 39 |
navigate('/login');
|
| 40 |
};
|
| 41 |
|
| 42 |
+
const addToCart = async (product) => {
|
| 43 |
if (!activeTable && orderType === 'Mesa') {
|
| 44 |
alert("Selecciona una mesa primero.");
|
| 45 |
return;
|
| 46 |
}
|
| 47 |
+
|
| 48 |
+
// Mark table as occupied
|
| 49 |
+
if (orderType === 'Mesa' && activeTable) {
|
| 50 |
+
await set(ref(db, `tables/${activeTable}`), 'occupied');
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
const existing = cart.find(item => item.id === product.id);
|
| 54 |
if (existing) {
|
| 55 |
setCart(cart.map(item => item.id === product.id ? { ...item, qty: item.qty + 1 } : item));
|
| 56 |
} else {
|
| 57 |
+
setCart([...cart, { ...product, qty: 1, note: '' }]);
|
| 58 |
}
|
| 59 |
};
|
| 60 |
|
| 61 |
+
const updateCartItemNote = (id, note) => {
|
| 62 |
+
setCart(cart.map(item => item.id === id ? { ...item, note } : item));
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
const removeFromCart = (id) => {
|
| 66 |
setCart(cart.filter(item => item.id !== id));
|
| 67 |
};
|
|
|
|
| 97 |
// Finalizar y descontar stock
|
| 98 |
await set(ref(db, `orders/${orderRef.key}/status`), 'completed');
|
| 99 |
|
| 100 |
+
// Mark table as free
|
| 101 |
+
if (orderType === 'Mesa' && activeTable) {
|
| 102 |
+
await set(ref(db, `tables/${activeTable}`), 'free');
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
// Lógica de Descuento de Inventario
|
| 106 |
+
// ... (existing logic continues)
|
| 107 |
for (const item of cart) {
|
| 108 |
// Buscamos el producto en el menú para ver su receta (ingredients)
|
| 109 |
// Nota: En un entorno real, esto se haría en el backend para evitar race conditions y asegurar integridad.
|
|
|
|
| 205 |
onClick={() => setActiveTable(table)}
|
| 206 |
style={{
|
| 207 |
minWidth: '70px', height: '70px', borderRadius: 'var(--radius-lg)',
|
| 208 |
+
background: activeTable === table ? 'var(--primary)' : (tableStatuses[table] === 'occupied' ? 'rgba(255,90,95,0.2)' : 'rgba(255,255,255,0.05)'),
|
| 209 |
+
border: activeTable === table ? '2px solid transparent' : (tableStatuses[table] === 'occupied' ? '1px solid var(--primary)' : '1px solid var(--border-subtle)'),
|
| 210 |
+
color: activeTable === table ? '#fff' : (tableStatuses[table] === 'occupied' ? 'var(--primary)' : 'var(--text-main)'),
|
| 211 |
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 212 |
fontWeight: '600', transition: 'all 0.2s', cursor: 'pointer'
|
| 213 |
}}
|
|
|
|
| 218 |
</div>
|
| 219 |
)}
|
| 220 |
|
| 221 |
+
<h2 className="text-gradient" style={{ fontSize: '1.25rem', marginBottom: '1rem' }}>Catálogo de Productos</h2>
|
| 222 |
+
|
| 223 |
+
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
|
| 224 |
+
{['Todos', ...new Set(menu.map(p => p.category))].map(cat => (
|
| 225 |
+
<button
|
| 226 |
+
key={cat}
|
| 227 |
+
onClick={() => setSelectedCategory(cat)}
|
| 228 |
+
className={selectedCategory === cat ? 'btn-primary' : 'btn-glass'}
|
| 229 |
+
style={{ padding: '0.4rem 1rem', fontSize: '0.85rem', whiteSpace: 'nowrap' }}
|
| 230 |
+
>
|
| 231 |
+
{cat}
|
| 232 |
+
</button>
|
| 233 |
+
))}
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '1rem' }}>
|
| 237 |
+
{menu
|
| 238 |
+
.filter(p => selectedCategory === 'Todos' || p.category === selectedCategory)
|
| 239 |
+
.map(product => (
|
| 240 |
<button
|
| 241 |
key={product.id}
|
| 242 |
onClick={() => addToCart(product)}
|
|
|
|
| 272 |
<div style={{ flex: 1 }}>
|
| 273 |
<div style={{ fontWeight: '600', fontSize: '0.95rem' }}>{item.name}</div>
|
| 274 |
<div style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>{item.qty} x ${item.price}</div>
|
| 275 |
+
<input
|
| 276 |
+
type="text"
|
| 277 |
+
placeholder="Nota / Extra..."
|
| 278 |
+
value={item.note}
|
| 279 |
+
onChange={(e) => updateCartItemNote(item.id, e.target.value)}
|
| 280 |
+
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' }}
|
| 281 |
+
/>
|
| 282 |
</div>
|
| 283 |
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
| 284 |
<span style={{ fontWeight: '700' }}>${(item.qty * item.price).toFixed(2)}</span>
|