dimensionalpulsar commited on
Commit
18f4249
·
verified ·
1 Parent(s): 446200c

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
- // Simple day mapping (last 7 days logic would be better but this is for demo)
33
- const day = new Date(order.timestamp).getDay(); // 0-6
34
- const index = (day + 6) % 7; // Map Mon-Sun
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: '1.5rem' }}>Catálogo de Productos</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '1rem' }}>
200
- {menu.map(product => (
 
 
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>