Spaces:
Running
Running
Upload 49 files
Browse files- frontend/src/components/admin/DashboardOverview.jsx +5 -27
- frontend/src/components/admin/FinanceManager.jsx +5 -5
- frontend/src/components/admin/InventoryControl.jsx +5 -5
- frontend/src/components/admin/MenuEditor.jsx +5 -5
- frontend/src/components/admin/Reports.jsx +77 -106
- frontend/src/components/admin/TableManager.jsx +21 -38
- frontend/src/components/admin/UserManager.jsx +4 -4
- frontend/src/index.css +24 -25
- frontend/src/pages/AdminDashboard.jsx +7 -13
- frontend/src/pages/CustomerMenu.jsx +1 -1
- frontend/src/pages/KitchenView.jsx +2 -3
- frontend/src/pages/Login.jsx +4 -4
- frontend/src/pages/WaiterPOS.jsx +15 -49
frontend/src/components/admin/DashboardOverview.jsx
CHANGED
|
@@ -18,8 +18,8 @@ export default function DashboardOverview() {
|
|
| 18 |
const cards = [
|
| 19 |
{ title: 'Ventas de Hoy', value: `$${stats.ventasHoy.toLocaleString()}`, icon: <TrendingUp size={24} color="var(--success)" />, bg: 'rgba(32, 201, 151, 0.1)' },
|
| 20 |
{ title: 'Mesas Activas', value: stats.mesasActivas, icon: <Users size={24} color="var(--info)" />, bg: 'rgba(50, 173, 230, 0.1)' },
|
| 21 |
-
{ title: '
|
| 22 |
-
{ title: '
|
| 23 |
];
|
| 24 |
|
| 25 |
return (
|
|
@@ -40,31 +40,9 @@ export default function DashboardOverview() {
|
|
| 40 |
))}
|
| 41 |
</div>
|
| 42 |
|
| 43 |
-
<div
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
<div style={{ height: '10px', width: '100%', background: 'rgba(0,0,0,0.05)', borderRadius: '10px', overflow: 'hidden', display: 'flex' }}>
|
| 47 |
-
<div style={{ width: '20%', background: 'var(--primary)', opacity: 0.3 }}></div>
|
| 48 |
-
<div style={{ width: '40%', background: 'var(--primary)' }}></div>
|
| 49 |
-
<div style={{ width: '15%', background: 'var(--primary)', opacity: 0.5 }}></div>
|
| 50 |
-
<div style={{ width: '25%', background: 'rgba(0,0,0,0.05)' }}></div>
|
| 51 |
-
</div>
|
| 52 |
-
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginTop: '1rem' }}>Punto máximo detectado: 14:00 - 15:30 (Almuerzo)</p>
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
<div className="glass-card">
|
| 56 |
-
<h3 className="text-gradient" style={{ marginBottom: '1rem' }}>Alertas de Suministros (ERP)</h3>
|
| 57 |
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
| 58 |
-
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem' }}>
|
| 59 |
-
<span>Tomate Cherry (Kg)</span>
|
| 60 |
-
<span style={{ color: 'var(--danger)', fontWeight: '700' }}>1.2 (Bajo)</span>
|
| 61 |
-
</div>
|
| 62 |
-
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem' }}>
|
| 63 |
-
<span>Aceite de Oliva (L)</span>
|
| 64 |
-
<span style={{ color: 'var(--warning)', fontWeight: '700' }}>3.0 (Crítico)</span>
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
);
|
|
|
|
| 18 |
const cards = [
|
| 19 |
{ title: 'Ventas de Hoy', value: `$${stats.ventasHoy.toLocaleString()}`, icon: <TrendingUp size={24} color="var(--success)" />, bg: 'rgba(32, 201, 151, 0.1)' },
|
| 20 |
{ title: 'Mesas Activas', value: stats.mesasActivas, icon: <Users size={24} color="var(--info)" />, bg: 'rgba(50, 173, 230, 0.1)' },
|
| 21 |
+
{ title: 'Órdenes', value: '45', icon: <ShoppingBag size={24} color="var(--primary)" />, bg: 'rgba(255, 90, 95, 0.1)' },
|
| 22 |
+
{ title: 'Stock Bajo', value: stats.stockBajo, icon: <AlertCircle size={24} color="var(--warning)" />, bg: 'rgba(245, 166, 35, 0.1)' }
|
| 23 |
];
|
| 24 |
|
| 25 |
return (
|
|
|
|
| 40 |
))}
|
| 41 |
</div>
|
| 42 |
|
| 43 |
+
<div className="glass-card">
|
| 44 |
+
<h3 className="text-gradient">Actividad Reciente</h3>
|
| 45 |
+
<p style={{ color: 'var(--text-muted)', marginTop: '0.5rem' }}>Módulo de notificaciones en tiempo real desde Cocina/Meseros...</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
</div>
|
| 47 |
</div>
|
| 48 |
);
|
frontend/src/components/admin/FinanceManager.jsx
CHANGED
|
@@ -109,7 +109,7 @@ export default function FinanceManager() {
|
|
| 109 |
<div style={{ ...statBoxStyle, background: 'rgba(76,217,100,0.05)', borderColor: 'var(--success)' }}>
|
| 110 |
<span style={{ fontSize: '0.75rem', color: 'var(--success)' }}>Ventas Registradas</span>
|
| 111 |
<div style={{ fontSize: '1.5rem', fontWeight: '900', color: 'var(--success)' }}>$0.00</div>
|
| 112 |
-
<p style={{ fontSize: '0.7rem', opacity: 0.
|
| 113 |
</div>
|
| 114 |
<button onClick={closeCash} className="btn-glass" style={{ color: 'var(--primary)', borderColor: 'var(--primary)', padding: '0.8rem' }}>
|
| 115 |
Cerrar Turno / Corte
|
|
@@ -168,13 +168,13 @@ export default function FinanceManager() {
|
|
| 168 |
</thead>
|
| 169 |
<tbody>
|
| 170 |
{cashSessions.map(sess => (
|
| 171 |
-
<tr key={sess.id} style={{ borderBottom: '1px solid
|
| 172 |
<td style={{ padding: '1rem' }}>{new Date(sess.openedAt).toLocaleDateString()}</td>
|
| 173 |
<td style={{ padding: '1rem' }}>${sess.openingBalance.toFixed(2)}</td>
|
| 174 |
<td style={{ padding: '1rem' }}>{sess.closedAt ? new Date(sess.closedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '-'}</td>
|
| 175 |
<td style={{ padding: '1rem' }}>${(sess.totalSales || 0).toFixed(2)}</td>
|
| 176 |
<td style={{ padding: '1rem' }}>
|
| 177 |
-
<span style={{ padding: '2px 8px', borderRadius: '4px', background: sess.status === 'open' ? 'rgba(76,217,100,0.1)' : 'rgba(
|
| 178 |
{sess.status.toUpperCase()}
|
| 179 |
</span>
|
| 180 |
</td>
|
|
@@ -187,6 +187,6 @@ export default function FinanceManager() {
|
|
| 187 |
);
|
| 188 |
}
|
| 189 |
|
| 190 |
-
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(
|
| 191 |
-
const statBoxStyle = { padding: '1rem', borderRadius: '12px', background: 'rgba(
|
| 192 |
const Plus = ({ size }) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>;
|
|
|
|
| 109 |
<div style={{ ...statBoxStyle, background: 'rgba(76,217,100,0.05)', borderColor: 'var(--success)' }}>
|
| 110 |
<span style={{ fontSize: '0.75rem', color: 'var(--success)' }}>Ventas Registradas</span>
|
| 111 |
<div style={{ fontSize: '1.5rem', fontWeight: '900', color: 'var(--success)' }}>$0.00</div>
|
| 112 |
+
<p style={{ fontSize: '0.7rem', opacity: 0.5, marginTop: '0.2rem' }}>Sincronizado con POS en tiempo real</p>
|
| 113 |
</div>
|
| 114 |
<button onClick={closeCash} className="btn-glass" style={{ color: 'var(--primary)', borderColor: 'var(--primary)', padding: '0.8rem' }}>
|
| 115 |
Cerrar Turno / Corte
|
|
|
|
| 168 |
</thead>
|
| 169 |
<tbody>
|
| 170 |
{cashSessions.map(sess => (
|
| 171 |
+
<tr key={sess.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
|
| 172 |
<td style={{ padding: '1rem' }}>{new Date(sess.openedAt).toLocaleDateString()}</td>
|
| 173 |
<td style={{ padding: '1rem' }}>${sess.openingBalance.toFixed(2)}</td>
|
| 174 |
<td style={{ padding: '1rem' }}>{sess.closedAt ? new Date(sess.closedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '-'}</td>
|
| 175 |
<td style={{ padding: '1rem' }}>${(sess.totalSales || 0).toFixed(2)}</td>
|
| 176 |
<td style={{ padding: '1rem' }}>
|
| 177 |
+
<span style={{ padding: '2px 8px', borderRadius: '4px', background: sess.status === 'open' ? 'rgba(76,217,100,0.1)' : 'rgba(255,255,255,0.05)', color: sess.status === 'open' ? 'var(--success)' : 'var(--text-muted)', fontSize: '0.75rem' }}>
|
| 178 |
{sess.status.toUpperCase()}
|
| 179 |
</span>
|
| 180 |
</td>
|
|
|
|
| 187 |
);
|
| 188 |
}
|
| 189 |
|
| 190 |
+
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none' };
|
| 191 |
+
const statBoxStyle = { padding: '1rem', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column', gap: '0.25rem' };
|
| 192 |
const Plus = ({ size }) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>;
|
frontend/src/components/admin/InventoryControl.jsx
CHANGED
|
@@ -137,11 +137,11 @@ export default function InventoryControl() {
|
|
| 137 |
|
| 138 |
{/* Modal de Edición */}
|
| 139 |
{isModalOpen && (
|
| 140 |
-
<div style={{ position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.
|
| 141 |
<div className="glass-panel animate-slide-up" style={{ maxWidth: '450px', width: '100%', padding: '2.5rem' }}>
|
| 142 |
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
| 143 |
<h2 className="text-gradient" style={{ fontSize: '1.5rem' }}>Editar Insumo</h2>
|
| 144 |
-
<button onClick={() => setIsModalOpen(false)} style={{ background: 'none', border: 'none', color: '
|
| 145 |
</div>
|
| 146 |
|
| 147 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
|
@@ -177,8 +177,8 @@ export default function InventoryControl() {
|
|
| 177 |
|
| 178 |
const inputStyle = {
|
| 179 |
width: '100%', padding: '0.8rem', borderRadius: '8px',
|
| 180 |
-
background: 'rgba(
|
| 181 |
-
color: '
|
| 182 |
};
|
| 183 |
|
| 184 |
const labelStyle = {
|
|
@@ -187,6 +187,6 @@ const labelStyle = {
|
|
| 187 |
|
| 188 |
const actionBtnStyle = {
|
| 189 |
width: '32px', height: '32px', borderRadius: '8px', border: '1px solid var(--border-subtle)',
|
| 190 |
-
background: 'rgba(
|
| 191 |
display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s', fontSize: '0.75rem', fontWeight: '700'
|
| 192 |
};
|
|
|
|
| 137 |
|
| 138 |
{/* Modal de Edición */}
|
| 139 |
{isModalOpen && (
|
| 140 |
+
<div style={{ position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.85)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999, padding: '20px' }}>
|
| 141 |
<div className="glass-panel animate-slide-up" style={{ maxWidth: '450px', width: '100%', padding: '2.5rem' }}>
|
| 142 |
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
| 143 |
<h2 className="text-gradient" style={{ fontSize: '1.5rem' }}>Editar Insumo</h2>
|
| 144 |
+
<button onClick={() => setIsModalOpen(false)} style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer' }}><X size={24} /></button>
|
| 145 |
</div>
|
| 146 |
|
| 147 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
|
|
|
| 177 |
|
| 178 |
const inputStyle = {
|
| 179 |
width: '100%', padding: '0.8rem', borderRadius: '8px',
|
| 180 |
+
background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)',
|
| 181 |
+
color: '#fff', outline: 'none'
|
| 182 |
};
|
| 183 |
|
| 184 |
const labelStyle = {
|
|
|
|
| 187 |
|
| 188 |
const actionBtnStyle = {
|
| 189 |
width: '32px', height: '32px', borderRadius: '8px', border: '1px solid var(--border-subtle)',
|
| 190 |
+
background: 'rgba(255,255,255,0.05)', color: 'var(--text-main)', cursor: 'pointer',
|
| 191 |
display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s', fontSize: '0.75rem', fontWeight: '700'
|
| 192 |
};
|
frontend/src/components/admin/MenuEditor.jsx
CHANGED
|
@@ -139,7 +139,7 @@ export default function MenuEditor() {
|
|
| 139 |
newIngs[idx].qty = parseFloat(e.target.value);
|
| 140 |
setFormData({...formData, ingredients: newIngs});
|
| 141 |
}}
|
| 142 |
-
style={{...inputStyle, padding: '0.4rem', width: '60px'
|
| 143 |
/>
|
| 144 |
<button type="button" onClick={() => setFormData({...formData, ingredients: formData.ingredients.filter((_, i) => i !== idx)})} style={{color: 'var(--primary)', background: 'none', border: 'none'}}><X size={16}/></button>
|
| 145 |
</div>
|
|
@@ -164,7 +164,7 @@ export default function MenuEditor() {
|
|
| 164 |
<span style={{ color: 'var(--success)', fontWeight: '800' }}>${item.price}</span>
|
| 165 |
</div>
|
| 166 |
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
| 167 |
-
<span style={{ fontSize: '0.7rem', color: 'var(--text-muted)', background: 'rgba(
|
| 168 |
{item.ingredients?.length > 0 && <span title="Tiene receta vinculada" style={{ color: 'var(--primary)' }}><ChefHat size={14} /></span>}
|
| 169 |
</div>
|
| 170 |
</div>
|
|
@@ -177,7 +177,7 @@ export default function MenuEditor() {
|
|
| 177 |
<div className="glass-panel animate-scale-in" style={{ maxWidth: '600px', width: '100%', padding: '2rem', maxHeight: '90vh', overflowY: 'auto' }}>
|
| 178 |
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2rem' }}>
|
| 179 |
<h2 className="text-gradient">Editar: {editingItem.name}</h2>
|
| 180 |
-
<button onClick={() => setIsModalOpen(false)} style={{ background: 'none', border: 'none', color: '
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
|
@@ -226,6 +226,6 @@ export default function MenuEditor() {
|
|
| 226 |
);
|
| 227 |
}
|
| 228 |
|
| 229 |
-
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(
|
| 230 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem' };
|
| 231 |
-
const actionBtnStyle = { width: '32px', height: '32px', borderRadius: '8px', border: 'none', background: 'rgba(
|
|
|
|
| 139 |
newIngs[idx].qty = parseFloat(e.target.value);
|
| 140 |
setFormData({...formData, ingredients: newIngs});
|
| 141 |
}}
|
| 142 |
+
style={{...inputStyle, padding: '0.4rem', width: '60px'}}
|
| 143 |
/>
|
| 144 |
<button type="button" onClick={() => setFormData({...formData, ingredients: formData.ingredients.filter((_, i) => i !== idx)})} style={{color: 'var(--primary)', background: 'none', border: 'none'}}><X size={16}/></button>
|
| 145 |
</div>
|
|
|
|
| 164 |
<span style={{ color: 'var(--success)', fontWeight: '800' }}>${item.price}</span>
|
| 165 |
</div>
|
| 166 |
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
| 167 |
+
<span style={{ fontSize: '0.7rem', color: 'var(--text-muted)', background: 'rgba(255,255,255,0.05)', padding: '2px 8px', borderRadius: '4px' }}>{item.category}</span>
|
| 168 |
{item.ingredients?.length > 0 && <span title="Tiene receta vinculada" style={{ color: 'var(--primary)' }}><ChefHat size={14} /></span>}
|
| 169 |
</div>
|
| 170 |
</div>
|
|
|
|
| 177 |
<div className="glass-panel animate-scale-in" style={{ maxWidth: '600px', width: '100%', padding: '2rem', maxHeight: '90vh', overflowY: 'auto' }}>
|
| 178 |
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2rem' }}>
|
| 179 |
<h2 className="text-gradient">Editar: {editingItem.name}</h2>
|
| 180 |
+
<button onClick={() => setIsModalOpen(false)} style={{ background: 'none', border: 'none', color: '#fff' }}><X size={24} /></button>
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
|
|
|
| 226 |
);
|
| 227 |
}
|
| 228 |
|
| 229 |
+
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none' };
|
| 230 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem' };
|
| 231 |
+
const actionBtnStyle = { width: '32px', height: '32px', borderRadius: '8px', border: 'none', background: 'rgba(0,0,0,0.6)', color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' };
|
frontend/src/components/admin/Reports.jsx
CHANGED
|
@@ -6,7 +6,7 @@ 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,
|
| 10 |
|
| 11 |
ChartJS.register(
|
| 12 |
CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, BarElement
|
|
@@ -14,8 +14,6 @@ ChartJS.register(
|
|
| 14 |
|
| 15 |
export default function Reports() {
|
| 16 |
const [salesData, setSalesData] = useState([0, 0, 0, 0, 0, 0, 0]);
|
| 17 |
-
const [hourlyData, setHourlyData] = useState(new Array(24).fill(0));
|
| 18 |
-
const [waiterPerformance, setWaiterPerformance] = useState({ labels: [], data: [] });
|
| 19 |
const [topProducts, setTopProducts] = useState({ labels: [], data: [] });
|
| 20 |
const [stats, setStats] = useState({ totalSales: 0, orderCount: 0, avgTicket: 0 });
|
| 21 |
|
|
@@ -23,139 +21,112 @@ export default function Reports() {
|
|
| 23 |
onValue(ref(db, 'orders'), (snapshot) => {
|
| 24 |
const data = snapshot.val();
|
| 25 |
if (data) {
|
| 26 |
-
const orders = Object.values(data)
|
| 27 |
const weeklySales = [0, 0, 0, 0, 0, 0, 0];
|
| 28 |
-
const hourSales = new Array(24).fill(0);
|
| 29 |
const productCounts = {};
|
| 30 |
-
const waiterSales = {};
|
| 31 |
let total = 0;
|
| 32 |
|
| 33 |
orders.forEach(order => {
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
// Waiters
|
| 47 |
-
const waiter = order.waiter || 'Sistema/Admin';
|
| 48 |
-
waiterSales[waiter] = (waiterSales[waiter] || 0) + order.total;
|
| 49 |
-
|
| 50 |
-
// Products
|
| 51 |
-
order.items.forEach(item => {
|
| 52 |
-
productCounts[item.name] = (productCounts[item.name] || 0) + item.qty;
|
| 53 |
-
});
|
| 54 |
});
|
| 55 |
|
| 56 |
setSalesData(weeklySales);
|
| 57 |
-
setHourlyData(hourSales);
|
| 58 |
setStats({
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
});
|
| 63 |
|
| 64 |
const sortedProducts = Object.entries(productCounts)
|
| 65 |
-
|
| 66 |
-
|
|
|
|
| 67 |
setTopProducts({
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
-
const sortedWaiters = Object.entries(waiterSales)
|
| 73 |
-
.sort((a,b) => b[1] - a[1]);
|
| 74 |
-
setWaiterPerformance({
|
| 75 |
-
labels: sortedWaiters.map(w => w[0].split('@')[0]),
|
| 76 |
-
data: sortedWaiters.map(w => w[1])
|
| 77 |
});
|
| 78 |
}
|
| 79 |
});
|
| 80 |
}, []);
|
| 81 |
|
| 82 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
responsive: true,
|
| 84 |
maintainAspectRatio: false,
|
| 85 |
-
plugins: { legend: { labels: { color: '#9595a8'
|
| 86 |
scales: {
|
| 87 |
-
x: { ticks: { color: '#9595a8'
|
| 88 |
-
y: { ticks: { color: '#9595a8'
|
| 89 |
}
|
| 90 |
};
|
| 91 |
|
| 92 |
return (
|
| 93 |
-
<div className="animate-fade-in"
|
| 94 |
<header style={{ marginBottom: '2.5rem' }}>
|
| 95 |
-
<h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800' }}>
|
| 96 |
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(
|
| 97 |
-
<
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
</div>
|
| 101 |
</header>
|
| 102 |
|
| 103 |
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(
|
| 104 |
-
{
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
{/* Rendimiento Meseros */}
|
| 113 |
-
<ChartCard title="Rendimiento por Mesero" icon={<Award size={18}/>}>
|
| 114 |
-
<Bar data={{
|
| 115 |
-
labels: waiterPerformance.labels,
|
| 116 |
-
datasets: [{ label: 'Ventas Totales ($)', data: waiterPerformance.data, backgroundColor: 'rgba(76,217,100,0.6)', borderRadius: 6 }]
|
| 117 |
-
}} options={chartOptions} />
|
| 118 |
-
</ChartCard>
|
| 119 |
-
|
| 120 |
-
{/* Horas Pico */}
|
| 121 |
-
<ChartCard title="Horas Pico (Densidad de Pedidos)" icon={<Clock size={18}/>}>
|
| 122 |
-
<Line data={{
|
| 123 |
-
labels: Array.from({length: 24}, (_, i) => `${i}:00`),
|
| 124 |
-
datasets: [{ label: 'Pedidos', data: hourlyData, borderColor: '#4dabf7', backgroundColor: 'rgba(77,171,247,0.1)', fill: true, tension: 0.4 }]
|
| 125 |
-
}} options={chartOptions} />
|
| 126 |
-
</ChartCard>
|
| 127 |
-
|
| 128 |
-
{/* Top Productos */}
|
| 129 |
-
<ChartCard title="Productos Estrella" icon={<Package size={18}/>}>
|
| 130 |
-
<Bar data={{
|
| 131 |
-
labels: topProducts.labels,
|
| 132 |
-
datasets: [{ label: 'Unidades', data: topProducts.data, backgroundColor: 'rgba(0,0,0,0.03)', borderColor: 'var(--border-subtle)', borderWidth: 1, borderRadius: 6 }]
|
| 133 |
-
}} options={chartOptions} />
|
| 134 |
-
</ChartCard>
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
function StatCard({ title, value, icon, color }) {
|
| 141 |
-
return (
|
| 142 |
-
<div className="glass-card" style={{ padding: '1.5rem' }}>
|
| 143 |
-
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.85rem', marginBottom: '0.75rem' }}>
|
| 144 |
-
{title} <span style={{ color }}>{icon}</span>
|
| 145 |
-
</div>
|
| 146 |
-
<div style={{ fontSize: '1.75rem', fontWeight: '900' }}>{value}</div>
|
| 147 |
-
</div>
|
| 148 |
-
);
|
| 149 |
-
}
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
<div style={{ flex: 1, position: 'relative' }}>
|
| 158 |
-
{children}
|
| 159 |
</div>
|
| 160 |
</div>
|
| 161 |
);
|
|
|
|
| 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
|
|
|
|
| 14 |
|
| 15 |
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 |
|
|
|
|
| 21 |
onValue(ref(db, 'orders'), (snapshot) => {
|
| 22 |
const data = snapshot.val();
|
| 23 |
if (data) {
|
| 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 |
+
});
|
| 40 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
});
|
| 42 |
|
| 43 |
setSalesData(weeklySales);
|
|
|
|
| 44 |
setStats({
|
| 45 |
+
totalSales: total,
|
| 46 |
+
orderCount: orders.filter(o => o.status === 'completed').length,
|
| 47 |
+
avgTicket: total / (orders.filter(o => o.status === 'completed').length || 1)
|
| 48 |
});
|
| 49 |
|
| 50 |
const sortedProducts = Object.entries(productCounts)
|
| 51 |
+
.sort((a,b) => b[1] - a[1])
|
| 52 |
+
.slice(0, 5);
|
| 53 |
+
|
| 54 |
setTopProducts({
|
| 55 |
+
labels: sortedProducts.map(p => p[0]),
|
| 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: [{
|
| 65 |
+
label: 'Ventas ($)',
|
| 66 |
+
data: salesData,
|
| 67 |
+
borderColor: '#FF5A5F',
|
| 68 |
+
backgroundColor: 'rgba(255, 90, 95, 0.2)',
|
| 69 |
+
fill: true,
|
| 70 |
+
tension: 0.4
|
| 71 |
+
}]
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const barData = {
|
| 75 |
+
labels: topProducts.labels,
|
| 76 |
+
datasets: [{
|
| 77 |
+
label: 'Unidades Vendidas',
|
| 78 |
+
data: topProducts.data,
|
| 79 |
+
backgroundColor: 'rgba(0, 166, 153, 0.7)',
|
| 80 |
+
borderRadius: 8
|
| 81 |
+
}]
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
const options = {
|
| 85 |
responsive: true,
|
| 86 |
maintainAspectRatio: false,
|
| 87 |
+
plugins: { legend: { labels: { color: '#9595a8' } } },
|
| 88 |
scales: {
|
| 89 |
+
x: { ticks: { color: '#9595a8' }, grid: { color: 'rgba(255,255,255,0.05)' } },
|
| 90 |
+
y: { ticks: { color: '#9595a8' }, grid: { color: 'rgba(255,255,255,0.05)' } }
|
| 91 |
}
|
| 92 |
};
|
| 93 |
|
| 94 |
return (
|
| 95 |
+
<div className="animate-fade-in">
|
| 96 |
<header style={{ marginBottom: '2.5rem' }}>
|
| 97 |
+
<h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800' }}>Análisis de Negocio</h2>
|
| 98 |
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem', marginTop: '1.5rem' }}>
|
| 99 |
+
<div className="glass-card" style={{ padding: '1.25rem' }}>
|
| 100 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Ventas Totales <TrendingUp size={16} /></div>
|
| 101 |
+
<div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>${stats.totalSales.toFixed(2)}</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div className="glass-card" style={{ padding: '1.25rem' }}>
|
| 104 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Pedidos <Package size={16} /></div>
|
| 105 |
+
<div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>{stats.orderCount}</div>
|
| 106 |
+
</div>
|
| 107 |
+
<div className="glass-card" style={{ padding: '1.25rem' }}>
|
| 108 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Ticket Promedio <Calendar size={16} /></div>
|
| 109 |
+
<div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>${stats.avgTicket.toFixed(2)}</div>
|
| 110 |
+
</div>
|
| 111 |
</div>
|
| 112 |
</header>
|
| 113 |
|
| 114 |
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem' }}>
|
| 115 |
+
<div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}>
|
| 116 |
+
<h3 style={{ marginBottom: '1.5rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
| 117 |
+
Ventas por Día de la Semana
|
| 118 |
+
</h3>
|
| 119 |
+
<div style={{ flex: 1, position: 'relative' }}>
|
| 120 |
+
<Line data={lineData} options={options} />
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
<div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}>
|
| 125 |
+
<h3 style={{ marginBottom: '1.5rem', fontSize: '1rem' }}>Productos Estrella</h3>
|
| 126 |
+
<div style={{ flex: 1, position: 'relative' }}>
|
| 127 |
+
<Bar data={barData} options={options} />
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
|
|
|
|
|
|
| 130 |
</div>
|
| 131 |
</div>
|
| 132 |
);
|
frontend/src/components/admin/TableManager.jsx
CHANGED
|
@@ -72,7 +72,7 @@ export default function TableManager() {
|
|
| 72 |
<h4 style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '1rem', borderTop: '1px solid var(--border-subtle)', paddingTop: '1.5rem' }}>Listado</h4>
|
| 73 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxHeight: '300px', overflowY: 'auto' }}>
|
| 74 |
{tables.map(t => (
|
| 75 |
-
<div key={t.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.6rem 1rem', background: 'rgba(
|
| 76 |
<span style={{ fontWeight: '700' }}>Mesa {t.number}</span>
|
| 77 |
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
| 78 |
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{t.capacity} px</span>
|
|
@@ -85,45 +85,28 @@ export default function TableManager() {
|
|
| 85 |
</aside>
|
| 86 |
|
| 87 |
{/* Visual Map Area */}
|
| 88 |
-
<section className="glass-panel" style={{ height: '600px', position: 'relative', overflow: 'hidden', background: '
|
| 89 |
-
<div style={{ position: 'absolute', top: '
|
| 90 |
-
<
|
| 91 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: 12, height: 12, background: 'var(--primary)', borderRadius: '2px' }} /> Ocupado</div>
|
| 92 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: 12, height: 12, background: '#fcc419', borderRadius: '2px' }} /> Reservado</div>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
-
<div style={{ width: '100%', height: '100%', padding: '
|
| 96 |
-
{tables.map(t =>
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
borderRadius: t.capacity > 4 ? '16px' : '50%',
|
| 111 |
-
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
| 112 |
-
boxShadow: t.status === 'occupied' ? '0 0 20px rgba(255,90,95,0.05)' : 'none',
|
| 113 |
-
cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
| 114 |
-
position: 'relative'
|
| 115 |
-
}}
|
| 116 |
-
>
|
| 117 |
-
<span style={{ fontSize: '1.4rem', fontWeight: '900', color: statusColor }}>{t.number}</span>
|
| 118 |
-
<span style={{ fontSize: '0.65rem', opacity: 0.6, color: 'var(--text-muted)' }}>{t.capacity} p</span>
|
| 119 |
-
{t.status !== 'available' && (
|
| 120 |
-
<div style={{ position: 'absolute', top: '-5px', right: '-5px', width: 12, height: 12, borderRadius: '50%', background: statusColor, border: '2px solid var(--bg-card)' }} />
|
| 121 |
-
)}
|
| 122 |
-
</div>
|
| 123 |
-
);
|
| 124 |
-
})}
|
| 125 |
{tables.length === 0 && (
|
| 126 |
-
<div style={{ gridColumn: '1 / -1', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.
|
| 127 |
<Grid size={80} />
|
| 128 |
</div>
|
| 129 |
)}
|
|
@@ -135,5 +118,5 @@ export default function TableManager() {
|
|
| 135 |
);
|
| 136 |
}
|
| 137 |
|
| 138 |
-
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(
|
| 139 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
|
|
|
|
| 72 |
<h4 style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '1rem', borderTop: '1px solid var(--border-subtle)', paddingTop: '1.5rem' }}>Listado</h4>
|
| 73 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxHeight: '300px', overflowY: 'auto' }}>
|
| 74 |
{tables.map(t => (
|
| 75 |
+
<div key={t.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.6rem 1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', alignItems: 'center' }}>
|
| 76 |
<span style={{ fontWeight: '700' }}>Mesa {t.number}</span>
|
| 77 |
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
| 78 |
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{t.capacity} px</span>
|
|
|
|
| 85 |
</aside>
|
| 86 |
|
| 87 |
{/* Visual Map Area */}
|
| 88 |
+
<section className="glass-panel" style={{ height: '600px', position: 'relative', overflow: 'hidden', background: 'radial-gradient(circle, rgba(255,255,255,0.02) 1px, transparent 1px)', backgroundSize: '30px 30px', border: '1px solid var(--border-subtle)' }}>
|
| 89 |
+
<div style={{ position: 'absolute', top: '1rem', left: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--text-muted)', fontSize: '0.8rem' }}>
|
| 90 |
+
<MousePointer2 size={14} /> Arrastra para posicionar (Modo Edición)
|
|
|
|
|
|
|
| 91 |
</div>
|
| 92 |
|
| 93 |
+
<div style={{ width: '100%', height: '100%', padding: '3rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: '2rem' }}>
|
| 94 |
+
{tables.map(t => (
|
| 95 |
+
<div key={t.id} style={{
|
| 96 |
+
width: '90px', height: '90px',
|
| 97 |
+
background: 'rgba(255,90,95,0.05)',
|
| 98 |
+
border: '2px solid var(--primary)',
|
| 99 |
+
borderRadius: t.capacity > 4 ? '12px' : '50%',
|
| 100 |
+
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
| 101 |
+
boxShadow: '0 0 15px rgba(255,90,95,0.1)',
|
| 102 |
+
cursor: 'move'
|
| 103 |
+
}}>
|
| 104 |
+
<span style={{ fontSize: '1.2rem', fontWeight: '900', color: 'var(--primary)' }}>{t.number}</span>
|
| 105 |
+
<span style={{ fontSize: '0.65rem', opacity: 0.6 }}>{t.capacity} cap</span>
|
| 106 |
+
</div>
|
| 107 |
+
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
{tables.length === 0 && (
|
| 109 |
+
<div style={{ gridColumn: '1 / -1', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.2 }}>
|
| 110 |
<Grid size={80} />
|
| 111 |
</div>
|
| 112 |
)}
|
|
|
|
| 118 |
);
|
| 119 |
}
|
| 120 |
|
| 121 |
+
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none' };
|
| 122 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
|
frontend/src/components/admin/UserManager.jsx
CHANGED
|
@@ -44,7 +44,7 @@ export default function UserManager() {
|
|
| 44 |
<h3 style={{ fontSize: '1.1rem', fontWeight: '700' }}>{emp.name || 'Sin Nombre'}</h3>
|
| 45 |
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{emp.email}</p>
|
| 46 |
</div>
|
| 47 |
-
<div style={{ background: 'rgba(
|
| 48 |
<Shield size={20} style={{ color: roleColors[emp.role] }} />
|
| 49 |
</div>
|
| 50 |
</div>
|
|
@@ -54,7 +54,7 @@ export default function UserManager() {
|
|
| 54 |
<select
|
| 55 |
value={emp.role}
|
| 56 |
onChange={(e) => handleUpdateRole(emp.uid, e.target.value)}
|
| 57 |
-
style={{...inputStyle, background: 'rgba(0,0,0,0.
|
| 58 |
>
|
| 59 |
<option value="admin">Administrador (Total)</option>
|
| 60 |
<option value="mesero">Mesero (POS)</option>
|
|
@@ -75,7 +75,7 @@ export default function UserManager() {
|
|
| 75 |
))}
|
| 76 |
|
| 77 |
<div className="glass-card" style={{ border: '2px dashed var(--border-subtle)', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center', padding: '2rem', gap: '1rem' }}>
|
| 78 |
-
<div style={{ background: 'rgba(
|
| 79 |
<UserPlus size={32} />
|
| 80 |
</div>
|
| 81 |
<h3 style={{ fontSize: '1rem', fontWeight: '700' }}>Registrar Nuevo Empleado</h3>
|
|
@@ -105,5 +105,5 @@ export default function UserManager() {
|
|
| 105 |
);
|
| 106 |
}
|
| 107 |
|
| 108 |
-
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(
|
| 109 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
|
|
|
|
| 44 |
<h3 style={{ fontSize: '1.1rem', fontWeight: '700' }}>{emp.name || 'Sin Nombre'}</h3>
|
| 45 |
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{emp.email}</p>
|
| 46 |
</div>
|
| 47 |
+
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '0.5rem', borderRadius: '50%' }}>
|
| 48 |
<Shield size={20} style={{ color: roleColors[emp.role] }} />
|
| 49 |
</div>
|
| 50 |
</div>
|
|
|
|
| 54 |
<select
|
| 55 |
value={emp.role}
|
| 56 |
onChange={(e) => handleUpdateRole(emp.uid, e.target.value)}
|
| 57 |
+
style={{...inputStyle, background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border-subtle)'}}
|
| 58 |
>
|
| 59 |
<option value="admin">Administrador (Total)</option>
|
| 60 |
<option value="mesero">Mesero (POS)</option>
|
|
|
|
| 75 |
))}
|
| 76 |
|
| 77 |
<div className="glass-card" style={{ border: '2px dashed var(--border-subtle)', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center', padding: '2rem', gap: '1rem' }}>
|
| 78 |
+
<div style={{ background: 'rgba(255,255,255,0.05)', padding: '1rem', borderRadius: '50%', color: 'var(--text-muted)' }}>
|
| 79 |
<UserPlus size={32} />
|
| 80 |
</div>
|
| 81 |
<h3 style={{ fontSize: '1rem', fontWeight: '700' }}>Registrar Nuevo Empleado</h3>
|
|
|
|
| 105 |
);
|
| 106 |
}
|
| 107 |
|
| 108 |
+
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none' };
|
| 109 |
const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
|
frontend/src/index.css
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap');
|
| 2 |
|
| 3 |
:root {
|
| 4 |
-
/* Premium
|
| 5 |
-
--bg-base: #
|
| 6 |
-
--bg-surface: #
|
| 7 |
-
--bg-glass: rgba(
|
| 8 |
-
--bg-glass-hover: rgba(
|
| 9 |
-
--bg-card:
|
| 10 |
|
| 11 |
/* Brand Accents */
|
| 12 |
--primary: #FF5A5F;
|
| 13 |
--primary-hover: #ff4046;
|
| 14 |
-
--primary-glow: rgba(255, 90, 95, 0.
|
| 15 |
--secondary: #00A699;
|
| 16 |
--secondary-hover: #008f84;
|
| 17 |
|
| 18 |
/* Text */
|
| 19 |
-
--text-main: #
|
| 20 |
-
--text-muted: #
|
| 21 |
-
--text-disabled: #
|
| 22 |
|
| 23 |
/* Status Colors */
|
| 24 |
--success: #20C997;
|
|
@@ -27,12 +27,12 @@
|
|
| 27 |
--info: #32ADE6;
|
| 28 |
|
| 29 |
/* Borders & Shadows */
|
| 30 |
-
--border-subtle: rgba(
|
| 31 |
-
--border-focus: rgba(255, 90, 95, 0.
|
| 32 |
-
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.
|
| 33 |
-
--shadow-md: 0
|
| 34 |
--shadow-glow: 0 0 20px var(--primary-glow);
|
| 35 |
-
--glass-blur: blur(
|
| 36 |
--glass-border: 1px solid var(--border-subtle);
|
| 37 |
|
| 38 |
/* Transitions */
|
|
@@ -61,8 +61,8 @@ body {
|
|
| 61 |
overflow-x: hidden;
|
| 62 |
-webkit-font-smoothing: antialiased;
|
| 63 |
background-image:
|
| 64 |
-
radial-gradient(circle at 10% 20%, rgba(255, 90, 95, 0.
|
| 65 |
-
radial-gradient(circle at 90% 80%, rgba(0, 166, 153, 0.
|
| 66 |
background-attachment: fixed;
|
| 67 |
}
|
| 68 |
|
|
@@ -98,14 +98,13 @@ button {
|
|
| 98 |
border-radius: var(--radius-md);
|
| 99 |
transition: all var(--transition-normal);
|
| 100 |
padding: 1.5rem;
|
| 101 |
-
box-shadow: var(--shadow-sm);
|
| 102 |
}
|
| 103 |
|
| 104 |
.glass-card:hover {
|
| 105 |
transform: translateY(-4px);
|
| 106 |
background: var(--bg-glass-hover);
|
| 107 |
-
border-color: rgba(
|
| 108 |
-
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.
|
| 109 |
}
|
| 110 |
|
| 111 |
/* Premium Buttons */
|
|
@@ -135,8 +134,8 @@ button {
|
|
| 135 |
}
|
| 136 |
|
| 137 |
.btn-glass {
|
| 138 |
-
background: rgba(
|
| 139 |
-
border: 1px solid rgba(
|
| 140 |
color: var(--text-main);
|
| 141 |
padding: 0.75rem 1.75rem;
|
| 142 |
border-radius: var(--radius-pill);
|
|
@@ -146,13 +145,13 @@ button {
|
|
| 146 |
}
|
| 147 |
|
| 148 |
.btn-glass:hover {
|
| 149 |
-
background: rgba(
|
| 150 |
-
border-color: rgba(
|
| 151 |
}
|
| 152 |
|
| 153 |
/* Typography styles */
|
| 154 |
.text-gradient {
|
| 155 |
-
background: linear-gradient(135deg, #
|
| 156 |
-webkit-background-clip: text;
|
| 157 |
-webkit-text-fill-color: transparent;
|
| 158 |
background-clip: text;
|
|
|
|
| 1 |
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap');
|
| 2 |
|
| 3 |
:root {
|
| 4 |
+
/* Premium Dark Theme Core Palette */
|
| 5 |
+
--bg-base: #0a0a0d;
|
| 6 |
+
--bg-surface: #14141a;
|
| 7 |
+
--bg-glass: rgba(20, 20, 26, 0.6);
|
| 8 |
+
--bg-glass-hover: rgba(40, 40, 50, 0.5);
|
| 9 |
+
--bg-card: rgba(30, 30, 40, 0.4);
|
| 10 |
|
| 11 |
/* Brand Accents */
|
| 12 |
--primary: #FF5A5F;
|
| 13 |
--primary-hover: #ff4046;
|
| 14 |
+
--primary-glow: rgba(255, 90, 95, 0.3);
|
| 15 |
--secondary: #00A699;
|
| 16 |
--secondary-hover: #008f84;
|
| 17 |
|
| 18 |
/* Text */
|
| 19 |
+
--text-main: #fcfcfd;
|
| 20 |
+
--text-muted: #9595a8;
|
| 21 |
+
--text-disabled: #555566;
|
| 22 |
|
| 23 |
/* Status Colors */
|
| 24 |
--success: #20C997;
|
|
|
|
| 27 |
--info: #32ADE6;
|
| 28 |
|
| 29 |
/* Borders & Shadows */
|
| 30 |
+
--border-subtle: rgba(255, 255, 255, 0.08);
|
| 31 |
+
--border-focus: rgba(255, 90, 95, 0.5);
|
| 32 |
+
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 33 |
+
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
|
| 34 |
--shadow-glow: 0 0 20px var(--primary-glow);
|
| 35 |
+
--glass-blur: blur(16px);
|
| 36 |
--glass-border: 1px solid var(--border-subtle);
|
| 37 |
|
| 38 |
/* Transitions */
|
|
|
|
| 61 |
overflow-x: hidden;
|
| 62 |
-webkit-font-smoothing: antialiased;
|
| 63 |
background-image:
|
| 64 |
+
radial-gradient(circle at 10% 20%, rgba(255, 90, 95, 0.03) 0%, transparent 40%),
|
| 65 |
+
radial-gradient(circle at 90% 80%, rgba(0, 166, 153, 0.03) 0%, transparent 40%);
|
| 66 |
background-attachment: fixed;
|
| 67 |
}
|
| 68 |
|
|
|
|
| 98 |
border-radius: var(--radius-md);
|
| 99 |
transition: all var(--transition-normal);
|
| 100 |
padding: 1.5rem;
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
.glass-card:hover {
|
| 104 |
transform: translateY(-4px);
|
| 105 |
background: var(--bg-glass-hover);
|
| 106 |
+
border-color: rgba(255, 255, 255, 0.15);
|
| 107 |
+
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
|
| 108 |
}
|
| 109 |
|
| 110 |
/* Premium Buttons */
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
.btn-glass {
|
| 137 |
+
background: rgba(255,255,255,0.05);
|
| 138 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 139 |
color: var(--text-main);
|
| 140 |
padding: 0.75rem 1.75rem;
|
| 141 |
border-radius: var(--radius-pill);
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
.btn-glass:hover {
|
| 148 |
+
background: rgba(255,255,255,0.1);
|
| 149 |
+
border-color: rgba(255,255,255,0.2);
|
| 150 |
}
|
| 151 |
|
| 152 |
/* Typography styles */
|
| 153 |
.text-gradient {
|
| 154 |
+
background: linear-gradient(135deg, #fff 0%, #a5a5b8 100%);
|
| 155 |
-webkit-background-clip: text;
|
| 156 |
-webkit-text-fill-color: transparent;
|
| 157 |
background-clip: text;
|
frontend/src/pages/AdminDashboard.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { useAuth } from '../context/AuthContext';
|
| 3 |
import { useNavigate } from 'react-router-dom';
|
| 4 |
-
import { LayoutDashboard, Utensils, Box, PieChart, LogOut, DollarSign, UtensilsCrossed, Users
|
| 5 |
import DashboardOverview from '../components/admin/DashboardOverview';
|
| 6 |
import MenuEditor from '../components/admin/MenuEditor';
|
| 7 |
import InventoryControl from '../components/admin/InventoryControl';
|
|
@@ -9,8 +9,6 @@ import Reports from '../components/admin/Reports';
|
|
| 9 |
import FinanceManager from '../components/admin/FinanceManager';
|
| 10 |
import UserManager from '../components/admin/UserManager';
|
| 11 |
import TableManager from '../components/admin/TableManager';
|
| 12 |
-
import CRMManager from '../components/admin/CRMManager';
|
| 13 |
-
import SettingsPlus from '../components/admin/SettingsPlus';
|
| 14 |
|
| 15 |
export default function AdminDashboard() {
|
| 16 |
const { logout, currentUser } = useAuth();
|
|
@@ -27,11 +25,9 @@ export default function AdminDashboard() {
|
|
| 27 |
{ id: 'menu', label: 'Menú & Precios', icon: <Utensils size={20} /> },
|
| 28 |
{ id: 'inventory', label: 'Control de Stock', icon: <Box size={20} /> },
|
| 29 |
{ id: 'reports', label: 'Reportes y Analítica', icon: <PieChart size={20} /> },
|
| 30 |
-
{ id: 'cash', label: '
|
| 31 |
-
{ id: 'users', label: '
|
| 32 |
-
{ id: 'tables', label: '
|
| 33 |
-
{ id: 'crm', label: 'CRM & Operaciones', icon: <Truck size={20} /> },
|
| 34 |
-
{ id: 'settings', label: 'Configuración ERP', icon: <Settings size={20} /> },
|
| 35 |
{ id: 'kitchen', label: 'Pantalla Cocina', icon: <UtensilsCrossed size={20} />, action: () => window.open('/kitchen', '_blank') },
|
| 36 |
{ id: 'menu_public', label: 'Carta Digital', icon: <Utensils size={20} />, action: () => window.open('/menu', '_blank') }
|
| 37 |
];
|
|
@@ -40,9 +36,9 @@ export default function AdminDashboard() {
|
|
| 40 |
<div className="app-container">
|
| 41 |
{/* Sidebar */}
|
| 42 |
<aside className="glass-panel" style={{ width: '280px', borderRadius: '0', borderRight: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
|
| 43 |
-
<div style={{ padding: '2rem 1.5rem', borderBottom: '1px solid
|
| 44 |
-
<h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>
|
| 45 |
-
<p style={{ color: 'var(--text-muted)', fontSize: '0.85rem', marginTop: '0.25rem' }}>
|
| 46 |
</div>
|
| 47 |
|
| 48 |
<nav style={{ flex: 1, padding: '1.5rem 1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
@@ -85,8 +81,6 @@ export default function AdminDashboard() {
|
|
| 85 |
{activeTab === 'cash' && <FinanceManager />}
|
| 86 |
{activeTab === 'users' && <UserManager />}
|
| 87 |
{activeTab === 'tables' && <TableManager />}
|
| 88 |
-
{activeTab === 'crm' && <CRMManager />}
|
| 89 |
-
{activeTab === 'settings' && <SettingsPlus />}
|
| 90 |
</div>
|
| 91 |
</main>
|
| 92 |
</div>
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { useAuth } from '../context/AuthContext';
|
| 3 |
import { useNavigate } from 'react-router-dom';
|
| 4 |
+
import { LayoutDashboard, Utensils, Box, PieChart, LogOut, DollarSign, UtensilsCrossed, Users } from 'lucide-react';
|
| 5 |
import DashboardOverview from '../components/admin/DashboardOverview';
|
| 6 |
import MenuEditor from '../components/admin/MenuEditor';
|
| 7 |
import InventoryControl from '../components/admin/InventoryControl';
|
|
|
|
| 9 |
import FinanceManager from '../components/admin/FinanceManager';
|
| 10 |
import UserManager from '../components/admin/UserManager';
|
| 11 |
import TableManager from '../components/admin/TableManager';
|
|
|
|
|
|
|
| 12 |
|
| 13 |
export default function AdminDashboard() {
|
| 14 |
const { logout, currentUser } = useAuth();
|
|
|
|
| 25 |
{ id: 'menu', label: 'Menú & Precios', icon: <Utensils size={20} /> },
|
| 26 |
{ id: 'inventory', label: 'Control de Stock', icon: <Box size={20} /> },
|
| 27 |
{ id: 'reports', label: 'Reportes y Analítica', icon: <PieChart size={20} /> },
|
| 28 |
+
{ id: 'cash', label: 'Caja y Finanzas', icon: <DollarSign size={20} /> },
|
| 29 |
+
{ id: 'users', label: 'Equipo y Roles', icon: <Users size={20} /> },
|
| 30 |
+
{ id: 'tables', label: 'Diseño Salón', icon: <LayoutDashboard size={20} /> },
|
|
|
|
|
|
|
| 31 |
{ id: 'kitchen', label: 'Pantalla Cocina', icon: <UtensilsCrossed size={20} />, action: () => window.open('/kitchen', '_blank') },
|
| 32 |
{ id: 'menu_public', label: 'Carta Digital', icon: <Utensils size={20} />, action: () => window.open('/menu', '_blank') }
|
| 33 |
];
|
|
|
|
| 36 |
<div className="app-container">
|
| 37 |
{/* Sidebar */}
|
| 38 |
<aside className="glass-panel" style={{ width: '280px', borderRadius: '0', borderRight: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
|
| 39 |
+
<div style={{ padding: '2rem 1.5rem', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
| 40 |
+
<h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>Admin OS</h1>
|
| 41 |
+
<p style={{ color: 'var(--text-muted)', fontSize: '0.85rem', marginTop: '0.25rem' }}>{currentUser?.email}</p>
|
| 42 |
</div>
|
| 43 |
|
| 44 |
<nav style={{ flex: 1, padding: '1.5rem 1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
|
|
| 81 |
{activeTab === 'cash' && <FinanceManager />}
|
| 82 |
{activeTab === 'users' && <UserManager />}
|
| 83 |
{activeTab === 'tables' && <TableManager />}
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
</main>
|
| 86 |
</div>
|
frontend/src/pages/CustomerMenu.jsx
CHANGED
|
@@ -8,7 +8,7 @@ export default function CustomerMenu() {
|
|
| 8 |
const [menuItems, setMenuItems] = useState([]);
|
| 9 |
const [categories, setCategories] = useState([]);
|
| 10 |
const [activeCategory, setActiveCategory] = useState('Todos');
|
| 11 |
-
const [menuTheme, setMenuTheme] = useState('
|
| 12 |
const navigate = useNavigate();
|
| 13 |
|
| 14 |
useEffect(() => {
|
|
|
|
| 8 |
const [menuItems, setMenuItems] = useState([]);
|
| 9 |
const [categories, setCategories] = useState([]);
|
| 10 |
const [activeCategory, setActiveCategory] = useState('Todos');
|
| 11 |
+
const [menuTheme, setMenuTheme] = useState('dark'); // 'dark' or 'light'
|
| 12 |
const navigate = useNavigate();
|
| 13 |
|
| 14 |
useEffect(() => {
|
frontend/src/pages/KitchenView.jsx
CHANGED
|
@@ -75,8 +75,7 @@ export default function KitchenView() {
|
|
| 75 |
<div key={order.id} className="glass-card animate-fade-in" style={{
|
| 76 |
padding: '1.5rem',
|
| 77 |
borderLeft: order.status === 'preparing' ? '4px solid var(--warning)' : '4px solid var(--primary)',
|
| 78 |
-
background: order.status === 'preparing' ? 'rgba(255,
|
| 79 |
-
boxShadow: 'var(--shadow-sm)'
|
| 80 |
}}>
|
| 81 |
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1.25rem', alignItems: 'center' }}>
|
| 82 |
<span style={{ fontSize: '1.2rem', fontWeight: '800', color: order.status === 'preparing' ? 'var(--warning)' : 'var(--primary)' }}>
|
|
@@ -89,7 +88,7 @@ export default function KitchenView() {
|
|
| 89 |
|
| 90 |
<div style={{ marginBottom: '1.5rem', minHeight: '80px' }}>
|
| 91 |
{order.items.map((item, idx) => (
|
| 92 |
-
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid
|
| 93 |
<span style={{ fontWeight: '600' }}><span style={{ color: 'var(--primary)', fontWeight: '800' }}>{item.qty}</span> {item.name}</span>
|
| 94 |
</div>
|
| 95 |
))}
|
|
|
|
| 75 |
<div key={order.id} className="glass-card animate-fade-in" style={{
|
| 76 |
padding: '1.5rem',
|
| 77 |
borderLeft: order.status === 'preparing' ? '4px solid var(--warning)' : '4px solid var(--primary)',
|
| 78 |
+
background: order.status === 'preparing' ? 'rgba(255,200,0,0.02)' : 'var(--bg-card)'
|
|
|
|
| 79 |
}}>
|
| 80 |
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1.25rem', alignItems: 'center' }}>
|
| 81 |
<span style={{ fontSize: '1.2rem', fontWeight: '800', color: order.status === 'preparing' ? 'var(--warning)' : 'var(--primary)' }}>
|
|
|
|
| 88 |
|
| 89 |
<div style={{ marginBottom: '1.5rem', minHeight: '80px' }}>
|
| 90 |
{order.items.map((item, idx) => (
|
| 91 |
+
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
| 92 |
<span style={{ fontWeight: '600' }}><span style={{ color: 'var(--primary)', fontWeight: '800' }}>{item.qty}</span> {item.name}</span>
|
| 93 |
</div>
|
| 94 |
))}
|
frontend/src/pages/Login.jsx
CHANGED
|
@@ -50,7 +50,7 @@ export default function Login() {
|
|
| 50 |
|
| 51 |
return (
|
| 52 |
<div className="app-container" style={{
|
| 53 |
-
background: '
|
| 54 |
justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '20px'
|
| 55 |
}}>
|
| 56 |
{/* Icono de Ver Menú Público */}
|
|
@@ -76,7 +76,7 @@ export default function Login() {
|
|
| 76 |
|
| 77 |
{/* Lado Izquierdo: Selección de Rol */}
|
| 78 |
<div style={{
|
| 79 |
-
width: '35%', background: 'rgba(
|
| 80 |
borderRight: '1px solid var(--border-subtle)', padding: '40px 30px',
|
| 81 |
display: 'flex', flexDirection: 'column', gap: '20px'
|
| 82 |
}}>
|
|
@@ -150,14 +150,14 @@ export default function Login() {
|
|
| 150 |
<label style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--text-muted)' }}>Correo Electrónico</label>
|
| 151 |
<input
|
| 152 |
type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
|
| 153 |
-
style={{ width: '100%', padding: '14px', borderRadius: '10px', background: 'rgba(
|
| 154 |
/>
|
| 155 |
</div>
|
| 156 |
<div>
|
| 157 |
<label style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--text-muted)' }}>Contraseña</label>
|
| 158 |
<input
|
| 159 |
type="password" required value={password} onChange={(e) => setPassword(e.target.value)}
|
| 160 |
-
style={{ width: '100%', padding: '14px', borderRadius: '10px', background: 'rgba(
|
| 161 |
/>
|
| 162 |
</div>
|
| 163 |
<button type="submit" className="btn-primary" style={{ height: '55px', fontSize: '1.1rem', marginTop: '10px' }} disabled={loading}>
|
|
|
|
| 50 |
|
| 51 |
return (
|
| 52 |
<div className="app-container" style={{
|
| 53 |
+
background: 'radial-gradient(circle at top right, #1a1a1a, #0a0a0a)',
|
| 54 |
justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '20px'
|
| 55 |
}}>
|
| 56 |
{/* Icono de Ver Menú Público */}
|
|
|
|
| 76 |
|
| 77 |
{/* Lado Izquierdo: Selección de Rol */}
|
| 78 |
<div style={{
|
| 79 |
+
width: '35%', background: 'rgba(255,255,255,0.03)',
|
| 80 |
borderRight: '1px solid var(--border-subtle)', padding: '40px 30px',
|
| 81 |
display: 'flex', flexDirection: 'column', gap: '20px'
|
| 82 |
}}>
|
|
|
|
| 150 |
<label style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--text-muted)' }}>Correo Electrónico</label>
|
| 151 |
<input
|
| 152 |
type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
|
| 153 |
+
style={{ width: '100%', padding: '14px', borderRadius: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff' }}
|
| 154 |
/>
|
| 155 |
</div>
|
| 156 |
<div>
|
| 157 |
<label style={{ display: 'block', marginBottom: '8px', fontSize: '0.9rem', color: 'var(--text-muted)' }}>Contraseña</label>
|
| 158 |
<input
|
| 159 |
type="password" required value={password} onChange={(e) => setPassword(e.target.value)}
|
| 160 |
+
style={{ width: '100%', padding: '14px', borderRadius: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff' }}
|
| 161 |
/>
|
| 162 |
</div>
|
| 163 |
<button type="submit" className="btn-primary" style={{ height: '55px', fontSize: '1.1rem', marginTop: '10px' }} disabled={loading}>
|
frontend/src/pages/WaiterPOS.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|
| 2 |
import { useAuth } from '../context/AuthContext';
|
| 3 |
import { useNavigate } from 'react-router-dom';
|
| 4 |
import { db } from '../firebase/config';
|
| 5 |
-
import { ref, onValue, push, set
|
| 6 |
import { LogOut, Coffee, Send, Trash2, ShoppingBag, Truck, Receipt, CheckCircle, XCircle, CreditCard, Banknote, QrCode } from 'lucide-react';
|
| 7 |
|
| 8 |
export default function WaiterPOS() {
|
|
@@ -16,7 +16,6 @@ export default function WaiterPOS() {
|
|
| 16 |
const [tip, setTip] = useState(0);
|
| 17 |
const [paymentMethod, setPaymentMethod] = useState('Efectivo');
|
| 18 |
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
|
| 19 |
-
const [customerData, setCustomerData] = useState({ name: '', email: '', phone: '', address: '' });
|
| 20 |
|
| 21 |
// Fetch Menu
|
| 22 |
useEffect(() => {
|
|
@@ -61,9 +60,6 @@ export default function WaiterPOS() {
|
|
| 61 |
table: orderType === 'Mesa' ? activeTable : orderType,
|
| 62 |
type: orderType,
|
| 63 |
waiter: currentUser?.email,
|
| 64 |
-
customerName: customerData.name || 'Consumidor Final',
|
| 65 |
-
customerEmail: customerData.email || 'anonimo@rest.os',
|
| 66 |
-
address: customerData.address || '',
|
| 67 |
items: cart,
|
| 68 |
subtotal,
|
| 69 |
discount,
|
|
@@ -79,7 +75,6 @@ export default function WaiterPOS() {
|
|
| 79 |
if (!isCheckoutOpen) {
|
| 80 |
setCart([]);
|
| 81 |
setActiveTable(null);
|
| 82 |
-
setCustomerData({ name: '', email: '', phone: '', address: '' });
|
| 83 |
alert('¡Comanda enviada a cocina exitosamente!');
|
| 84 |
} else {
|
| 85 |
// Finalizar y descontar stock
|
|
@@ -87,16 +82,20 @@ export default function WaiterPOS() {
|
|
| 87 |
|
| 88 |
// Lógica de Descuento de Inventario
|
| 89 |
for (const item of cart) {
|
|
|
|
|
|
|
| 90 |
const productRef = ref(db, `menu/${item.id}`);
|
| 91 |
onValue(productRef, async (snapshot) => {
|
| 92 |
const product = snapshot.val();
|
| 93 |
if (product && product.ingredients) {
|
| 94 |
for (const ing of product.ingredients) {
|
|
|
|
|
|
|
| 95 |
onValue(ref(db, `inventory/${ing.id}`), async (invSnap) => {
|
| 96 |
const invData = invSnap.val();
|
| 97 |
if (invData) {
|
| 98 |
const newQty = invData.quantity - (item.qty * ing.qty);
|
| 99 |
-
await
|
| 100 |
}
|
| 101 |
}, { onlyOnce: true });
|
| 102 |
}
|
|
@@ -107,7 +106,6 @@ export default function WaiterPOS() {
|
|
| 107 |
setIsCheckoutOpen(false);
|
| 108 |
setCart([]);
|
| 109 |
setActiveTable(null);
|
| 110 |
-
setCustomerData({ name: '', email: '', phone: '', address: '' });
|
| 111 |
alert('¡Venta realizada y stock actualizado!');
|
| 112 |
}
|
| 113 |
};
|
|
@@ -141,7 +139,7 @@ export default function WaiterPOS() {
|
|
| 141 |
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
|
| 142 |
{/* Navbar */}
|
| 143 |
<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)' }}>
|
| 144 |
-
<h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>
|
| 145 |
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
| 146 |
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{currentUser?.email}</span>
|
| 147 |
<button className="btn-glass" onClick={() => window.open('/menu', '_blank')} style={{ padding: '0.5rem 1rem' }}>Carta Digital</button>
|
|
@@ -176,35 +174,6 @@ export default function WaiterPOS() {
|
|
| 176 |
</div>
|
| 177 |
</div>
|
| 178 |
|
| 179 |
-
<div className="glass-card" style={{ padding: '1.25rem', marginBottom: '1.5rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem' }}>
|
| 180 |
-
<div style={{ gridColumn: orderType === 'Delivery' ? 'span 2' : 'auto' }}>
|
| 181 |
-
<label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Cliente / Mesa</label>
|
| 182 |
-
<input
|
| 183 |
-
type="text" placeholder="Nombre del Cliente"
|
| 184 |
-
value={customerData.name} onChange={e => setCustomerData({...customerData, name: e.target.value})}
|
| 185 |
-
style={{ ...inputStyle, padding: '0.6rem' }}
|
| 186 |
-
/>
|
| 187 |
-
</div>
|
| 188 |
-
{orderType === 'Delivery' && (
|
| 189 |
-
<div style={{ gridColumn: 'span 2' }}>
|
| 190 |
-
<label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Dirección de Entrega</label>
|
| 191 |
-
<input
|
| 192 |
-
type="text" placeholder="Calle, Número, Referencias"
|
| 193 |
-
value={customerData.address} onChange={e => setCustomerData({...customerData, address: e.target.value})}
|
| 194 |
-
style={{ ...inputStyle, padding: '0.6rem' }}
|
| 195 |
-
/>
|
| 196 |
-
</div>
|
| 197 |
-
)}
|
| 198 |
-
<div>
|
| 199 |
-
<label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Email (Puntos)</label>
|
| 200 |
-
<input
|
| 201 |
-
type="email" placeholder="cliente@email.com"
|
| 202 |
-
value={customerData.email} onChange={e => setCustomerData({...customerData, email: e.target.value})}
|
| 203 |
-
style={{ ...inputStyle, padding: '0.6rem' }}
|
| 204 |
-
/>
|
| 205 |
-
</div>
|
| 206 |
-
</div>
|
| 207 |
-
|
| 208 |
{orderType === 'Mesa' && (
|
| 209 |
<div style={{ display: 'flex', gap: '1rem', overflowX: 'auto', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
|
| 210 |
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(table => (
|
|
@@ -213,12 +182,11 @@ export default function WaiterPOS() {
|
|
| 213 |
onClick={() => setActiveTable(table)}
|
| 214 |
style={{
|
| 215 |
minWidth: '70px', height: '70px', borderRadius: 'var(--radius-lg)',
|
| 216 |
-
background: activeTable === table ? 'var(--primary)' : 'rgba(
|
| 217 |
border: activeTable === table ? '2px solid transparent' : '1px solid var(--border-subtle)',
|
| 218 |
color: activeTable === table ? '#fff' : 'var(--text-main)',
|
| 219 |
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 220 |
-
fontWeight: '600', transition: 'all 0.2s', cursor: 'pointer'
|
| 221 |
-
boxShadow: activeTable === table ? '0 4px 12px var(--primary-glow)' : 'none'
|
| 222 |
}}
|
| 223 |
>
|
| 224 |
{table}
|
|
@@ -261,7 +229,7 @@ export default function WaiterPOS() {
|
|
| 261 |
|
| 262 |
<div style={{ flex: 1, overflowY: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
| 263 |
{cart.map((item) => (
|
| 264 |
-
<div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.75rem', background: 'rgba(
|
| 265 |
<div style={{ flex: 1 }}>
|
| 266 |
<div style={{ fontWeight: '600', fontSize: '0.95rem' }}>{item.name}</div>
|
| 267 |
<div style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>{item.qty} x ${item.price}</div>
|
|
@@ -276,7 +244,7 @@ export default function WaiterPOS() {
|
|
| 276 |
))}
|
| 277 |
</div>
|
| 278 |
|
| 279 |
-
<div style={{ padding: '1.5rem', borderTop: '1px solid var(--border-subtle)', background: 'rgba(0,0,0,0.
|
| 280 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
|
| 281 |
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
| 282 |
<span>Subtotal:</span>
|
|
@@ -325,7 +293,7 @@ export default function WaiterPOS() {
|
|
| 325 |
{isCheckoutOpen && (
|
| 326 |
<div style={{
|
| 327 |
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
| 328 |
-
background: 'rgba(0,0,0,0.
|
| 329 |
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
|
| 330 |
}}>
|
| 331 |
<div className="glass-panel animate-scale-in" style={{ width: '100%', maxWidth: '500px', padding: '2.5rem' }}>
|
|
@@ -359,7 +327,7 @@ export default function WaiterPOS() {
|
|
| 359 |
placeholder="$ 0.00"
|
| 360 |
value={tip}
|
| 361 |
onChange={(e) => setTip(e.target.value)}
|
| 362 |
-
style={{ width: '100%', padding: '0.8rem', background: 'rgba(
|
| 363 |
/>
|
| 364 |
</div>
|
| 365 |
|
|
@@ -405,7 +373,7 @@ export default function WaiterPOS() {
|
|
| 405 |
|
| 406 |
<style>{`
|
| 407 |
.btn-glass {
|
| 408 |
-
background: rgba(
|
| 409 |
border: 1px solid var(--border-subtle);
|
| 410 |
color: var(--text-main);
|
| 411 |
border-radius: var(--radius-md);
|
|
@@ -418,7 +386,7 @@ export default function WaiterPOS() {
|
|
| 418 |
gap: 0.5rem;
|
| 419 |
}
|
| 420 |
.btn-glass:hover {
|
| 421 |
-
background: rgba(
|
| 422 |
transform: translateY(-2px);
|
| 423 |
}
|
| 424 |
.btn-primary {
|
|
@@ -433,12 +401,10 @@ export default function WaiterPOS() {
|
|
| 433 |
align-items: center;
|
| 434 |
justify-content: center;
|
| 435 |
gap: 0.5rem;
|
| 436 |
-
box-shadow: 0 4px 12px var(--primary-glow);
|
| 437 |
}
|
| 438 |
.btn-primary:hover {
|
| 439 |
opacity: 0.9;
|
| 440 |
transform: translateY(-2px);
|
| 441 |
-
box-shadow: 0 6px 16px var(--primary-glow);
|
| 442 |
}
|
| 443 |
.animate-scale-in {
|
| 444 |
animation: scaleIn 0.3s ease-out;
|
|
|
|
| 2 |
import { useAuth } from '../context/AuthContext';
|
| 3 |
import { useNavigate } from 'react-router-dom';
|
| 4 |
import { db } from '../firebase/config';
|
| 5 |
+
import { ref, onValue, push, set } from 'firebase/database';
|
| 6 |
import { LogOut, Coffee, Send, Trash2, ShoppingBag, Truck, Receipt, CheckCircle, XCircle, CreditCard, Banknote, QrCode } from 'lucide-react';
|
| 7 |
|
| 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(() => {
|
|
|
|
| 60 |
table: orderType === 'Mesa' ? activeTable : orderType,
|
| 61 |
type: orderType,
|
| 62 |
waiter: currentUser?.email,
|
|
|
|
|
|
|
|
|
|
| 63 |
items: cart,
|
| 64 |
subtotal,
|
| 65 |
discount,
|
|
|
|
| 75 |
if (!isCheckoutOpen) {
|
| 76 |
setCart([]);
|
| 77 |
setActiveTable(null);
|
|
|
|
| 78 |
alert('¡Comanda enviada a cocina exitosamente!');
|
| 79 |
} else {
|
| 80 |
// Finalizar y descontar stock
|
|
|
|
| 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.
|
| 87 |
const productRef = ref(db, `menu/${item.id}`);
|
| 88 |
onValue(productRef, async (snapshot) => {
|
| 89 |
const product = snapshot.val();
|
| 90 |
if (product && product.ingredients) {
|
| 91 |
for (const ing of product.ingredients) {
|
| 92 |
+
const invRef = ref(db, `inventory/${ing.id}/quantity`);
|
| 93 |
+
// Obtenemos cantidad actual y restamos (qty_producto * qty_ingrediente)
|
| 94 |
onValue(ref(db, `inventory/${ing.id}`), async (invSnap) => {
|
| 95 |
const invData = invSnap.val();
|
| 96 |
if (invData) {
|
| 97 |
const newQty = invData.quantity - (item.qty * ing.qty);
|
| 98 |
+
await set(invRef, newQty);
|
| 99 |
}
|
| 100 |
}, { onlyOnce: true });
|
| 101 |
}
|
|
|
|
| 106 |
setIsCheckoutOpen(false);
|
| 107 |
setCart([]);
|
| 108 |
setActiveTable(null);
|
|
|
|
| 109 |
alert('¡Venta realizada y stock actualizado!');
|
| 110 |
}
|
| 111 |
};
|
|
|
|
| 139 |
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
|
| 140 |
{/* Navbar */}
|
| 141 |
<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)' }}>
|
| 142 |
+
<h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>Terminal POS</h1>
|
| 143 |
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
| 144 |
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{currentUser?.email}</span>
|
| 145 |
<button className="btn-glass" onClick={() => window.open('/menu', '_blank')} style={{ padding: '0.5rem 1rem' }}>Carta Digital</button>
|
|
|
|
| 174 |
</div>
|
| 175 |
</div>
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
{orderType === 'Mesa' && (
|
| 178 |
<div style={{ display: 'flex', gap: '1rem', overflowX: 'auto', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
|
| 179 |
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(table => (
|
|
|
|
| 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 |
}}
|
| 191 |
>
|
| 192 |
{table}
|
|
|
|
| 229 |
|
| 230 |
<div style={{ flex: 1, overflowY: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
| 231 |
{cart.map((item) => (
|
| 232 |
+
<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)' }}>
|
| 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>
|
|
|
|
| 244 |
))}
|
| 245 |
</div>
|
| 246 |
|
| 247 |
+
<div style={{ padding: '1.5rem', borderTop: '1px solid var(--border-subtle)', background: 'rgba(0,0,0,0.2)' }}>
|
| 248 |
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
|
| 249 |
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
| 250 |
<span>Subtotal:</span>
|
|
|
|
| 293 |
{isCheckoutOpen && (
|
| 294 |
<div style={{
|
| 295 |
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
| 296 |
+
background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(10px)',
|
| 297 |
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
|
| 298 |
}}>
|
| 299 |
<div className="glass-panel animate-scale-in" style={{ width: '100%', maxWidth: '500px', padding: '2.5rem' }}>
|
|
|
|
| 327 |
placeholder="$ 0.00"
|
| 328 |
value={tip}
|
| 329 |
onChange={(e) => setTip(e.target.value)}
|
| 330 |
+
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' }}
|
| 331 |
/>
|
| 332 |
</div>
|
| 333 |
|
|
|
|
| 373 |
|
| 374 |
<style>{`
|
| 375 |
.btn-glass {
|
| 376 |
+
background: rgba(255,255,255,0.05);
|
| 377 |
border: 1px solid var(--border-subtle);
|
| 378 |
color: var(--text-main);
|
| 379 |
border-radius: var(--radius-md);
|
|
|
|
| 386 |
gap: 0.5rem;
|
| 387 |
}
|
| 388 |
.btn-glass:hover {
|
| 389 |
+
background: rgba(255,255,255,0.1);
|
| 390 |
transform: translateY(-2px);
|
| 391 |
}
|
| 392 |
.btn-primary {
|
|
|
|
| 401 |
align-items: center;
|
| 402 |
justify-content: center;
|
| 403 |
gap: 0.5rem;
|
|
|
|
| 404 |
}
|
| 405 |
.btn-primary:hover {
|
| 406 |
opacity: 0.9;
|
| 407 |
transform: translateY(-2px);
|
|
|
|
| 408 |
}
|
| 409 |
.animate-scale-in {
|
| 410 |
animation: scaleIn 0.3s ease-out;
|