dimensionalpulsar commited on
Commit
564baf9
·
verified ·
1 Parent(s): 28b0bde

Upload 47 files

Browse files
.dockerignore CHANGED
@@ -2,9 +2,8 @@ node_modules
2
  dist
3
  .git
4
  .gitignore
5
- README.md
6
  build.log
7
  eslint.config.js
8
- package-lock.json (ignore local ones to let HF generate their own or use the one copied)
9
- # Keep package-lock.json if you want identical versions, but usually it's better to let HF install fresh if they have a different arch.
10
- # Actually, keep package-lock.json for stability.
 
2
  dist
3
  .git
4
  .gitignore
 
5
  build.log
6
  eslint.config.js
7
+ package-lock.json
8
+ frontend/
9
+ resto-app/
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  frontend/public/images/tacos.jpg filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  frontend/public/images/tacos.jpg filter=lfs diff=lfs merge=lfs -text
37
+ public/images/tacos.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
Dockerfile CHANGED
@@ -4,13 +4,13 @@ FROM node:22-alpine AS build
4
  WORKDIR /app
5
 
6
  # Copy package files first for better caching
7
- COPY frontend/package*.json ./
8
 
9
  # Install dependencies
10
  RUN npm install
11
 
12
  # Copy the rest of the application
13
- COPY frontend/ .
14
 
15
  # Build the application
16
  RUN npm run build
 
4
  WORKDIR /app
5
 
6
  # Copy package files first for better caching
7
+ COPY package*.json ./
8
 
9
  # Install dependencies
10
  RUN npm install
11
 
12
  # Copy the rest of the application
13
+ COPY . .
14
 
15
  # Build the application
16
  RUN npm run build
README.md CHANGED
@@ -9,15 +9,14 @@ pinned: false
9
 
10
  # Resto OS Premium
11
 
12
- Sistema de gestión de restaurantes premium con carta digital QR y panel pos.
13
 
14
  ## Despliegue en Hugging Face
15
- Este repositorio está configurado para un **Docker Space**.
16
- Hugging Face detectará automáticamente el `Dockerfile` y realizará la construcción de la aplicación React de forma automática.
17
 
18
  ## Características
19
- - Gestión de Menú con Imágenes (Firebase Storage)
20
- - Control de Inventario
21
- - Punto de Venta (POS) para Meseros
22
- - Vista de Cocina en tiempo real
23
- - Carta Digital QR con Temas Dinámicos
 
9
 
10
  # Resto OS Premium
11
 
12
+ Sistema de gestión de restaurantes premium con carta digital QR y panel POS.
13
 
14
  ## Despliegue en Hugging Face
15
+ Este repositorio está configurado para un **Docker Space** (Puerto 7860).
16
+ La estructura ha sido aplanada para facilitar la compilación automática en los servidores de Hugging Face.
17
 
18
  ## Características
19
+ - **Carga de Imágenes**: Gestión directa desde el panel admin con Firebase Storage.
20
+ - **Detección de Sesión**: Acceso rápido si ya estás logueado.
21
+ - **Global Logout**: Botón de salida en todos los módulos.
22
+ - **Diseño Premium**: Glassmorphism y temas dinámicos.
 
build.log ADDED
Binary file (17.7 kB). View file
 
eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
public/favicon.svg ADDED
public/icons.svg ADDED
public/icons/inventory.png ADDED
public/images/burger.png ADDED
public/images/cafe.png ADDED
public/images/cerveza.png ADDED
public/images/limonada.png ADDED
public/images/pasta.png ADDED
public/images/paste.png ADDED
public/images/pizza.png ADDED
public/images/salad.png ADDED
public/images/tacos.jpg ADDED

Git LFS Details

  • SHA256: 67c1f17e43c4233a24da28a0638ae4c81c86d1cc383e487cb235fb12dd6c974f
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
public/images/tacos.png ADDED
src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
src/App.jsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3
+ import { AuthProvider, useAuth } from './context/AuthContext';
4
+ import ProtectedRoute from './components/ProtectedRoute';
5
+
6
+ // Páginas
7
+ import Login from './pages/Login';
8
+ import AdminDashboard from './pages/AdminDashboard';
9
+ import WaiterPOS from './pages/WaiterPOS';
10
+ import CustomerMenu from './pages/CustomerMenu';
11
+ import KitchenView from './pages/KitchenView';
12
+
13
+ // Router base para redirigir dependiendo del rol al loguearse
14
+ function RootRedirect() {
15
+ const { currentUser, userRole, loading } = useAuth();
16
+ if (loading) return <div className="app-container" style={{justifyContent:'center', alignItems:'center'}}>Cargando...</div>;
17
+ if (!currentUser) return <Navigate to="/login" replace />;
18
+ if (userRole === 'admin') return <Navigate to="/admin" replace />;
19
+ if (userRole === 'mesero') return <Navigate to="/pos" replace />;
20
+ return <Navigate to="/login" replace />; // Default
21
+ }
22
+
23
+ function App() {
24
+ return (
25
+ <Router>
26
+ <AuthProvider>
27
+ <Routes>
28
+ <Route path="/" element={<RootRedirect />} />
29
+ <Route path="/login" element={<Login />} />
30
+ <Route path="/menu" element={<CustomerMenu />} />
31
+
32
+ {/* Rutas Privadas */}
33
+ <Route
34
+ path="/admin/*"
35
+ element={
36
+ <ProtectedRoute allowedRoles={['admin']}>
37
+ <AdminDashboard />
38
+ </ProtectedRoute>
39
+ }
40
+ />
41
+ <Route
42
+ path="/pos/*"
43
+ element={
44
+ <ProtectedRoute allowedRoles={['admin', 'mesero']}>
45
+ <WaiterPOS />
46
+ </ProtectedRoute>
47
+ }
48
+ />
49
+ <Route
50
+ path="/kitchen"
51
+ element={
52
+ <ProtectedRoute allowedRoles={['admin', 'mesero']}>
53
+ <KitchenView />
54
+ </ProtectedRoute>
55
+ }
56
+ />
57
+
58
+ {/* Fallback */}
59
+ <Route path="*" element={<Navigate to="/" replace />} />
60
+ </Routes>
61
+ </AuthProvider>
62
+ </Router>
63
+ );
64
+ }
65
+
66
+ export default App;
src/assets/hero.png ADDED
src/assets/react.svg ADDED
src/assets/vite.svg ADDED
src/components/ProtectedRoute.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Navigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ export default function ProtectedRoute({ children, allowedRoles }) {
6
+ const { currentUser, userRole, loading } = useAuth();
7
+
8
+ if (loading) return <div className="app-container" style={{justifyContent:'center', alignItems:'center'}}>Cargando...</div>;
9
+
10
+ if (!currentUser) {
11
+ return <Navigate to="/login" replace />;
12
+ }
13
+
14
+ if (allowedRoles && !allowedRoles.includes(userRole)) {
15
+ return <Navigate to="/unauthorized" replace />;
16
+ }
17
+
18
+ return children;
19
+ }
src/components/admin/DashboardOverview.jsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue } from 'firebase/database';
4
+ import { TrendingUp, Users, ShoppingBag, AlertCircle } from 'lucide-react';
5
+
6
+ export default function DashboardOverview() {
7
+ const [stats, setStats] = useState({ ventasHoy: 0, mesasActivas: 0, stockBajo: 0 });
8
+
9
+ useEffect(() => {
10
+ // Aquí iría la escucha de Firebase para stats en tiempo real
11
+ setStats({
12
+ ventasHoy: 12500,
13
+ mesasActivas: 4,
14
+ stockBajo: 2
15
+ });
16
+ }, []);
17
+
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 (
26
+ <div className="animate-fade-in">
27
+ <h2 className="text-gradient" style={{ fontSize: '2rem', marginBottom: '1.5rem' }}>Resumen General</h2>
28
+
29
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem', marginBottom: '3rem' }}>
30
+ {cards.map((card, i) => (
31
+ <div key={i} className="glass-card" style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
32
+ <div style={{ width: '56px', height: '56px', borderRadius: '50%', background: card.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
33
+ {card.icon}
34
+ </div>
35
+ <div>
36
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginBottom: '0.25rem' }}>{card.title}</p>
37
+ <h3 style={{ fontSize: '1.5rem', fontWeight: '700' }}>{card.value}</h3>
38
+ </div>
39
+ </div>
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
+ );
49
+ }
src/components/admin/FinanceManager.jsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue, push, set } from 'firebase/database';
4
+ import { DollarSign, ArrowUpCircle, ArrowDownCircle, Save, Calendar, Clock, History } from 'lucide-react';
5
+
6
+ export default function FinanceManager() {
7
+ const [cashSessions, setCashSessions] = useState([]);
8
+ const [expenses, setExpenses] = useState([]);
9
+ const [activeSession, setActiveSession] = useState(null);
10
+ const [openingBalance, setOpeningBalance] = useState('');
11
+ const [expenseData, setExpenseData] = useState({ concept: '', amount: '', category: 'Varios' });
12
+
13
+ useEffect(() => {
14
+ // Fetch sessions
15
+ onValue(ref(db, 'finance/sessions'), (snapshot) => {
16
+ const data = snapshot.val();
17
+ if (data) {
18
+ const list = Object.keys(data).map(id => ({ id, ...data[id] })).sort((a,b) => b.openedAt - a.openedAt);
19
+ setCashSessions(list);
20
+ const active = list.find(s => s.status === 'open');
21
+ setActiveSession(active || null);
22
+ }
23
+ });
24
+
25
+ // Fetch expenses
26
+ onValue(ref(db, 'finance/expenses'), (snapshot) => {
27
+ const data = snapshot.val();
28
+ setExpenses(data ? Object.keys(data).map(id => ({ id, ...data[id] })).sort((a,b) => b.timestamp - a.timestamp) : []);
29
+ });
30
+ }, []);
31
+
32
+ const openCash = async () => {
33
+ if (!openingBalance) return;
34
+ const newSessionRef = push(ref(db, 'finance/sessions'));
35
+ await set(newSessionRef, {
36
+ openedAt: Date.now(),
37
+ openingBalance: parseFloat(openingBalance),
38
+ status: 'open',
39
+ totalSales: 0,
40
+ totalExpenses: 0
41
+ });
42
+ setOpeningBalance('');
43
+ };
44
+
45
+ const closeCash = async () => {
46
+ if (!activeSession) return;
47
+ if (window.confirm('¿Deseas cerrar el turno de caja actual?')) {
48
+ await set(ref(db, `finance/sessions/${activeSession.id}/status`), 'closed');
49
+ await set(ref(db, `finance/sessions/${activeSession.id}/closedAt`), Date.now());
50
+ }
51
+ };
52
+
53
+ const addExpense = async (e) => {
54
+ e.preventDefault();
55
+ if (!expenseData.concept || !expenseData.amount) return;
56
+ const expenseRef = push(ref(db, 'finance/expenses'));
57
+ await set(expenseRef, {
58
+ ...expenseData,
59
+ amount: parseFloat(expenseData.amount),
60
+ timestamp: Date.now(),
61
+ sessionId: activeSession?.id || 'none'
62
+ });
63
+ setExpenseData({ concept: '', amount: '', category: 'Varios' });
64
+ };
65
+
66
+ return (
67
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
68
+ <header style={{ marginBottom: '2.5rem' }}>
69
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
70
+ <DollarSign size={28} /> Control Financiero
71
+ </h2>
72
+ <p style={{ color: 'var(--text-muted)' }}>Arqueo de caja, egresos y flujo de efectivo semanal</p>
73
+ </header>
74
+
75
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', gap: '2rem' }}>
76
+
77
+ {/* Cash Arqueo Module */}
78
+ <div className="glass-card" style={{ border: activeSession ? '1px solid var(--success)' : '1px solid var(--border-subtle)' }}>
79
+ <h3 style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
80
+ <Clock size={20} /> Turno de Caja (Arqueo)
81
+ </h3>
82
+
83
+ {!activeSession ? (
84
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
85
+ <p style={{ fontSize: '0.9rem', color: 'var(--text-muted)' }}>Caja cerrada actualmente. Ingresa el fondo de apertura para iniciar.</p>
86
+ <input
87
+ type="number"
88
+ placeholder="Monto de Apertura ($)"
89
+ value={openingBalance}
90
+ onChange={(e) => setOpeningBalance(e.target.value)}
91
+ style={inputStyle}
92
+ />
93
+ <button onClick={openCash} className="btn-primary" style={{ padding: '0.8rem' }}>
94
+ Abrir Turno
95
+ </button>
96
+ </div>
97
+ ) : (
98
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
99
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
100
+ <div style={statBoxStyle}>
101
+ <span style={{ fontSize: '0.75rem', opacity: 0.6 }}>Apertura</span>
102
+ <div style={{ fontSize: '1.2rem', fontWeight: '800' }}>${activeSession.openingBalance.toFixed(2)}</div>
103
+ </div>
104
+ <div style={statBoxStyle}>
105
+ <span style={{ fontSize: '0.75rem', opacity: 0.6 }}>Hora</span>
106
+ <div style={{ fontSize: '1rem', fontWeight: '600' }}>{new Date(activeSession.openedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
107
+ </div>
108
+ </div>
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
116
+ </button>
117
+ </div>
118
+ )}
119
+ </div>
120
+
121
+ {/* Expenses Module */}
122
+ <div className="glass-card">
123
+ <h3 style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
124
+ <ArrowDownCircle size={20} /> Registro de Egresos
125
+ </h3>
126
+ <form onSubmit={addExpense} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
127
+ <input type="text" placeholder="Concepto (ej. Pago luz, Propina extra)" value={expenseData.concept} onChange={e => setExpenseData({...expenseData, concept: e.target.value})} style={inputStyle} />
128
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
129
+ <input type="number" placeholder="Monto ($)" value={expenseData.amount} onChange={e => setExpenseData({...expenseData, amount: e.target.value})} style={inputStyle} />
130
+ <select value={expenseData.category} onChange={e => setExpenseData({...expenseData, category: e.target.value})} style={inputStyle}>
131
+ <option value="Varios">Varios</option>
132
+ <option value="Compras">Compras</option>
133
+ <option value="Servicios">Servicios</option>
134
+ <option value="Sueldos">Sueldos</option>
135
+ </select>
136
+ </div>
137
+ <button type="submit" className="btn-glass" style={{ padding: '0.8rem' }}>
138
+ <Plus size={18} /> Registrar Egreso
139
+ </button>
140
+ </form>
141
+
142
+ <div style={{ marginTop: '2rem', maxHeight: '200px', overflowY: 'auto' }}>
143
+ <h4 style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem', borderBottom: '1px solid var(--border-subtle)', paddingBottom: '0.5rem' }}>Últimos Gastos</h4>
144
+ {expenses.map(exp => (
145
+ <div key={exp.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', fontSize: '0.9rem' }}>
146
+ <span>{exp.concept}</span>
147
+ <span style={{ color: 'var(--primary)', fontWeight: '700' }}>-${exp.amount.toFixed(2)}</span>
148
+ </div>
149
+ ))}
150
+ {expenses.length === 0 && <p style={{ fontSize: '0.85rem', color: 'var(--text-muted)', textAlign: 'center' }}>No hay gastos registrados</p>}
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="glass-panel" style={{ marginTop: '3rem', padding: '1.5rem' }}>
156
+ <h3 style={{ fontSize: '1.1rem', marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
157
+ <History size={18} /> Historial de Turnos Recientes
158
+ </h3>
159
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.9rem' }}>
160
+ <thead>
161
+ <tr style={{ textAlign: 'left', borderBottom: '1px solid var(--border-subtle)', opacity: 0.6 }}>
162
+ <th style={{ padding: '1rem' }}>Fecha</th>
163
+ <th style={{ padding: '1rem' }}>Apertura</th>
164
+ <th style={{ padding: '1rem' }}>Cierre</th>
165
+ <th style={{ padding: '1rem' }}>Ventas</th>
166
+ <th style={{ padding: '1rem' }}>Estado</th>
167
+ </tr>
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>
181
+ </tr>
182
+ ))}
183
+ </tbody>
184
+ </table>
185
+ </div>
186
+ </div>
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>;
src/components/admin/InventoryControl.jsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue, push, set, remove, update } from 'firebase/database';
4
+ import { Plus, Trash2, Edit3, AlertTriangle, X, Save } from 'lucide-react';
5
+
6
+ export default function InventoryControl() {
7
+ const [items, setItems] = useState([]);
8
+ const [formData, setFormData] = useState({ name: '', quantity: '', unit: '', minStock: '' });
9
+ const [editingItem, setEditingItem] = useState(null);
10
+ const [isModalOpen, setIsModalOpen] = useState(false);
11
+
12
+ useEffect(() => {
13
+ const inventoryRef = ref(db, 'inventory');
14
+ onValue(inventoryRef, (snapshot) => {
15
+ const data = snapshot.val();
16
+ if (data) {
17
+ const productList = Object.keys(data).map(key => ({ id: key, ...data[key] }));
18
+ setItems(productList);
19
+ } else {
20
+ setItems([]);
21
+ }
22
+ });
23
+ }, []);
24
+
25
+ const handleAddItem = async (e) => {
26
+ e.preventDefault();
27
+ if (!formData.name || !formData.quantity) return;
28
+
29
+ const inventoryRef = ref(db, 'inventory');
30
+ const newItemRef = push(inventoryRef);
31
+ await set(newItemRef, {
32
+ name: formData.name,
33
+ quantity: Number(formData.quantity),
34
+ unit: formData.unit || 'Und',
35
+ minStock: Number(formData.minStock) || 10
36
+ });
37
+ setFormData({ name: '', quantity: '', unit: '', minStock: '' });
38
+ };
39
+
40
+ const handleDelete = async (id) => {
41
+ if(window.confirm('¿Seguro que deseas eliminar este insumo?')){
42
+ await remove(ref(db, `inventory/${id}`));
43
+ }
44
+ };
45
+
46
+ const openEdit = (item) => {
47
+ setEditingItem({ ...item });
48
+ setIsModalOpen(true);
49
+ };
50
+
51
+ const handleSaveEdit = async () => {
52
+ if (!editingItem.name) return;
53
+ const { id, ...updates } = editingItem;
54
+ await update(ref(db, `inventory/${id}`), {
55
+ ...updates,
56
+ quantity: Number(updates.quantity),
57
+ minStock: Number(updates.minStock)
58
+ });
59
+ setEditingItem(null);
60
+ setIsModalOpen(false);
61
+ };
62
+
63
+ const handleAddStock = async (id, currentQty) => {
64
+ await set(ref(db, `inventory/${id}/quantity`), currentQty + 10);
65
+ }
66
+
67
+ return (
68
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
69
+ <h2 className="text-gradient" style={{ fontSize: '2rem', marginBottom: '1.5rem', fontWeight: '800' }}>Control de Stock</h2>
70
+
71
+ <div className="glass-card" style={{ marginBottom: '2rem', padding: '1.5rem' }}>
72
+ <h3 style={{ marginBottom: '1.25rem', fontSize: '1.1rem' }}>Registrar Nuevo Insumo</h3>
73
+ <form onSubmit={handleAddItem} style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
74
+ <div style={{ flex: 2, minWidth: '150px' }}>
75
+ <label style={labelStyle}>Nombre del Insumo</label>
76
+ <input type="text" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} required style={inputStyle} placeholder="Tomate rojo" />
77
+ </div>
78
+ <div style={{ flex: 1, minWidth: '80px' }}>
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" />
85
+ </div>
86
+ <button type="submit" className="btn-primary" style={{ padding: '0.8rem 1.5rem' }}>
87
+ <Plus size={20} />
88
+ </button>
89
+ </form>
90
+ </div>
91
+
92
+ <div className="glass-panel" style={{ overflow: 'hidden' }}>
93
+ <table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left' }}>
94
+ <thead>
95
+ <tr style={{ borderBottom: '1px solid var(--border-subtle)', background: 'rgba(255,255,255,0.02)' }}>
96
+ <th style={{ padding: '1.25rem', color: 'var(--text-muted)', fontWeight: '500' }}>Insumo</th>
97
+ <th style={{ padding: '1.25rem', color: 'var(--text-muted)', fontWeight: '500' }}>Stock Actual</th>
98
+ <th style={{ padding: '1.25rem', color: 'var(--text-muted)', fontWeight: '500' }}>Estado</th>
99
+ <th style={{ padding: '1.25rem', color: 'var(--text-muted)', fontWeight: '500', textAlign: 'right' }}>Acciones</th>
100
+ </tr>
101
+ </thead>
102
+ <tbody>
103
+ {items.length === 0 ? (
104
+ <tr><td colSpan="4" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}>Sin insumos registrados</td></tr>
105
+ ) : (
106
+ items.map((item) => {
107
+ const isLow = item.quantity <= (item.minStock || 10);
108
+ return (
109
+ <tr key={item.id} style={{ borderBottom: '1px solid var(--border-subtle)', transition: 'background 0.2s' }} className="table-row-hover">
110
+ <td style={{ padding: '1.25rem', fontWeight: '600' }}>{item.name}</td>
111
+ <td style={{ padding: '1.25rem', fontWeight: '700', color: isLow ? 'var(--primary)' : 'var(--text-main)', fontSize: '1.1rem' }}>
112
+ {item.quantity} <span style={{ fontSize: '0.8rem', fontWeight: '400', color: 'var(--text-muted)' }}>{item.unit}</span>
113
+ </td>
114
+ <td style={{ padding: '1.25rem' }}>
115
+ {isLow ? (
116
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem', color: 'var(--primary)', fontSize: '0.8rem', background: 'rgba(255,107,107,0.1)', padding: '0.25rem 0.75rem', borderRadius: '12px', fontWeight: '600' }}>
117
+ <AlertTriangle size={14} /> RELLENAR
118
+ </span>
119
+ ) : (
120
+ <span style={{ color: 'var(--success)', fontSize: '0.8rem', fontWeight: '600', background: 'rgba(76,217,100,0.1)', padding: '0.25rem 0.75rem', borderRadius: '12px' }}>ÓPTIMO</span>
121
+ )}
122
+ </td>
123
+ <td style={{ padding: '1.25rem', textAlign: 'right' }}>
124
+ <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
125
+ <button onClick={() => handleAddStock(item.id, item.quantity)} title="Sumar 10" style={actionBtnStyle}>+10</button>
126
+ <button onClick={() => openEdit(item)} title="Editar" style={actionBtnStyle}><Edit3 size={16} /></button>
127
+ <button onClick={() => handleDelete(item.id)} title="Eliminar" style={{...actionBtnStyle, color: 'var(--primary)'}}><Trash2 size={16} /></button>
128
+ </div>
129
+ </td>
130
+ </tr>
131
+ )
132
+ })
133
+ )}
134
+ </tbody>
135
+ </table>
136
+ </div>
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' }}>
148
+ <div>
149
+ <label style={labelStyle}>Nombre del Insumo</label>
150
+ <input type="text" value={editingItem.name} onChange={e => setEditingItem({...editingItem, name: e.target.value})} style={inputStyle} />
151
+ </div>
152
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
153
+ <div>
154
+ <label style={labelStyle}>Stock</label>
155
+ <input type="number" value={editingItem.quantity} onChange={e => setEditingItem({...editingItem, quantity: e.target.value})} style={inputStyle} />
156
+ </div>
157
+ <div>
158
+ <label style={labelStyle}>Min. Stock</label>
159
+ <input type="number" value={editingItem.minStock || 10} onChange={e => setEditingItem({...editingItem, minStock: e.target.value})} style={inputStyle} />
160
+ </div>
161
+ </div>
162
+ <div>
163
+ <label style={labelStyle}>Unidad</label>
164
+ <input type="text" value={editingItem.unit} onChange={e => setEditingItem({...editingItem, unit: e.target.value})} style={inputStyle} />
165
+ </div>
166
+
167
+ <button onClick={handleSaveEdit} className="btn-primary" style={{ height: '50px', marginTop: '1rem', display: 'flex', gap: '0.75rem', alignItems: 'center', justifyContent: 'center' }}>
168
+ <Save size={18} /> Guardar Cambios
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }
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 = {
185
+ display: 'block', marginBottom: '0.5rem', fontSize: '0.85rem', color: 'var(--text-muted)'
186
+ };
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
+ };
src/components/admin/MenuEditor.jsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db, storage } from '../../firebase/config';
3
+ import { ref, onValue, push, set, remove, update } from 'firebase/database';
4
+ import { ref as sRef, uploadBytes, getDownloadURL } from 'firebase/storage';
5
+ import { Plus, Trash2, Edit3, X, Image as ImageIcon, Save, Palette, Upload, Loader2, ListTree, ChefHat } from 'lucide-react';
6
+
7
+ export default function MenuEditor() {
8
+ const [products, setProducts] = useState([]);
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);
16
+ const [menuTheme, setMenuTheme] = useState('dark');
17
+ const [uploading, setUploading] = useState(false);
18
+
19
+ useEffect(() => {
20
+ onValue(ref(db, 'menu'), (snapshot) => {
21
+ const data = snapshot.val();
22
+ setProducts(data ? Object.keys(data).map(id => ({ id, ...data[id] })) : []);
23
+ });
24
+
25
+ onValue(ref(db, 'inventory'), (snapshot) => {
26
+ const data = snapshot.val();
27
+ setInventory(data ? Object.keys(data).map(id => ({ id, ...data[id] })) : []);
28
+ });
29
+
30
+ onValue(ref(db, 'config/menuTheme'), (snapshot) => {
31
+ if (snapshot.exists()) setMenuTheme(snapshot.val());
32
+ });
33
+ }, []);
34
+
35
+ const handleToggleTheme = async () => {
36
+ await set(ref(db, 'config/menuTheme'), menuTheme === 'dark' ? 'light' : 'dark');
37
+ };
38
+
39
+ const handleImageUpload = async (e, isEditing = false) => {
40
+ const file = e.target.files[0];
41
+ if (!file || !file.type.startsWith('image/')) return;
42
+ setUploading(true);
43
+ try {
44
+ const storagePath = `menu/${Date.now()}_${file.name}`;
45
+ const fileRef = sRef(storage, storagePath);
46
+ await uploadBytes(fileRef, file);
47
+ const url = await getDownloadURL(fileRef);
48
+ if (isEditing) setEditingItem(prev => ({ ...prev, image: url }));
49
+ else setFormData(prev => ({ ...prev, image: url }));
50
+ } catch (error) { console.error(error); } finally { setUploading(false); }
51
+ };
52
+
53
+ const handleAddProduct = async (e) => {
54
+ e.preventDefault();
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) => {
62
+ if(window.confirm('¿Eliminar producto?')) await remove(ref(db, `menu/${id}`));
63
+ };
64
+
65
+ const addIngredientToProduct = (isEditing) => {
66
+ const newIng = { id: '', qty: 1 };
67
+ if (isEditing) setEditingItem({ ...editingItem, ingredients: [...(editingItem.ingredients || []), newIng] });
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) });
74
+ setIsModalOpen(false);
75
+ };
76
+
77
+ return (
78
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
79
+ <header style={{ marginBottom: '2.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
80
+ <div>
81
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
82
+ <ChefHat size={28} /> Carta & Recetas
83
+ </h2>
84
+ <p style={{ color: 'var(--text-muted)' }}>Gestión de productos y consumo de insumos</p>
85
+ </div>
86
+ <button onClick={handleToggleTheme} className="btn-glass" style={{ padding: '0.8rem 1.25rem' }}>
87
+ Tema QR: {menuTheme.toUpperCase()}
88
+ </button>
89
+ </header>
90
+
91
+ <section className="glass-card" style={{ marginBottom: '3rem', padding: '1.5rem' }}>
92
+ <h3 style={{ marginBottom: '1.5rem', fontSize: '1.1rem' }}>Agregar Platillo</h3>
93
+ <form onSubmit={handleAddProduct} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.25rem', alignItems: 'end' }}>
94
+ <div>
95
+ <label style={labelStyle}>Nombre</label>
96
+ <input type="text" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} style={inputStyle} />
97
+ </div>
98
+ <div>
99
+ <label style={labelStyle}>Precio Base ($)</label>
100
+ <input type="number" value={formData.price} onChange={e => setFormData({...formData, price: e.target.value})} style={inputStyle} />
101
+ </div>
102
+ <div>
103
+ <label style={labelStyle}>Categoría</label>
104
+ <select value={formData.category} onChange={e => setFormData({...formData, category: e.target.value})} style={inputStyle}>
105
+ {['Entradas', 'Bebidas', 'Fuertes', 'Postres', 'Pizzas', 'Combos'].map(c => <option key={c} value={c}>{c}</option>)}
106
+ </select>
107
+ </div>
108
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
109
+ <button type="button" onClick={() => addIngredientToProduct(false)} className="btn-glass" style={{ flex: 1, padding: '0.8rem' }}>
110
+ <ListTree size={18} /> Insumos
111
+ </button>
112
+ <button type="submit" className="btn-primary" style={{ padding: '0.8rem 1.5rem', opacity: uploading ? 0.5 : 1 }}>
113
+ <Plus size={20} />
114
+ </button>
115
+ </div>
116
+ </form>
117
+
118
+ {formData.ingredients.length > 0 && (
119
+ <div style={{ marginTop: '1.5rem', display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
120
+ {formData.ingredients.map((ing, idx) => (
121
+ <div key={idx} className="glass-panel" style={{ padding: '0.5rem 1rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
122
+ <select
123
+ value={ing.id}
124
+ onChange={e => {
125
+ const newIngs = [...formData.ingredients];
126
+ newIngs[idx].id = e.target.value;
127
+ setFormData({...formData, ingredients: newIngs});
128
+ }}
129
+ style={{...inputStyle, padding: '0.4rem', width: 'auto'}}
130
+ >
131
+ <option value="">Seleccionar Insumo</option>
132
+ {inventory.map(i => <option key={i.id} value={i.id}>{i.name}</option>)}
133
+ </select>
134
+ <input
135
+ type="number"
136
+ value={ing.qty}
137
+ onChange={e => {
138
+ const newIngs = [...formData.ingredients];
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>
146
+ ))}
147
+ </div>
148
+ )}
149
+ </section>
150
+
151
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1.5rem' }}>
152
+ {products.map(item => (
153
+ <div key={item.id} className="glass-card" style={{ padding: '0', overflow: 'hidden' }}>
154
+ <div style={{ position: 'relative', height: '160px', background: 'rgba(255,255,255,0.05)' }}>
155
+ {item.image && <img src={item.image} alt={item.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
156
+ <div style={{ position: 'absolute', top: '10px', right: '10px', display: 'flex', gap: '5px' }}>
157
+ <button onClick={() => { setEditingItem({...item}); setIsModalOpen(true); }} style={actionBtnStyle}><Edit3 size={16} /></button>
158
+ <button onClick={() => handleDelete(item.id)} style={{...actionBtnStyle, color: 'var(--primary)'}}><Trash2 size={16} /></button>
159
+ </div>
160
+ </div>
161
+ <div style={{ padding: '1.25rem' }}>
162
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
163
+ <h4 style={{ fontWeight: '700' }}>{item.name}</h4>
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>
171
+ </div>
172
+ ))}
173
+ </div>
174
+
175
+ {isModalOpen && editingItem && (
176
+ <div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.85)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, padding: '20px' }}>
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' }}>
184
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
185
+ <div><label style={labelStyle}>Nombre</label><input type="text" value={editingItem.name} onChange={e => setEditingItem({...editingItem, name: e.target.value})} style={inputStyle} /></div>
186
+ <div><label style={labelStyle}>Precio</label><input type="number" value={editingItem.price} onChange={e => setEditingItem({...editingItem, price: e.target.value})} style={inputStyle} /></div>
187
+ </div>
188
+
189
+ <div>
190
+ <label style={labelStyle}>Receta (Consumo de insumos por venta)</label>
191
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
192
+ {(editingItem.ingredients || []).map((ing, i) => (
193
+ <div key={i} style={{ display: 'flex', gap: '0.5rem' }}>
194
+ <select value={ing.id} onChange={e => {
195
+ const ings = [...editingItem.ingredients];
196
+ ings[i].id = e.target.value;
197
+ setEditingItem({...editingItem, ingredients: ings});
198
+ }} style={{...inputStyle, flex: 2}}>
199
+ <option value="">Seleccionar Insumo</option>
200
+ {inventory.map(inv => <option key={inv.id} value={inv.id}>{inv.name}</option>)}
201
+ </select>
202
+ <input type="number" value={ing.qty} onChange={e => {
203
+ const ings = [...editingItem.ingredients];
204
+ ings[i].qty = parseFloat(e.target.value);
205
+ setEditingItem({...editingItem, ingredients: ings});
206
+ }} style={{...inputStyle, flex: 1}} />
207
+ <button onClick={() => setEditingItem({...editingItem, ingredients: editingItem.ingredients.filter((_, idx) => idx !== i)})} style={{color: 'var(--primary)'}}><Trash2 size={18} /></button>
208
+ </div>
209
+ ))}
210
+ <button onClick={() => addIngredientToProduct(true)} className="btn-glass" style={{ padding: '0.5rem', fontSize: '0.8rem' }}><Plus size={14} /> Añadir Insumo</button>
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'}
217
+ <input type="file" hidden onChange={e => handleImageUpload(e, true)} accept="image/*" />
218
+ </label>
219
+ <button onClick={handleSaveEdit} className="btn-primary" style={{ flex: 1, padding: '0.8rem' }}><Save size={18} /> Guardar</button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )}
225
+ </div>
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' };
src/components/admin/Reports.jsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue } from 'firebase/database';
4
+ import {
5
+ Chart as ChartJS, CategoryScale, LinearScale, PointElement,
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
13
+ );
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
+
20
+ useEffect(() => {
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
+ );
133
+ }
src/components/admin/TableManager.jsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue, set, update, push, remove } from 'firebase/database';
4
+ import { Layout, Plus, Trash2, Map as MapIcon, Grid, Save, MousePointer2 } from 'lucide-react';
5
+
6
+ export default function TableManager() {
7
+ const [tables, setTables] = useState([]);
8
+ const [newTable, setNewTable] = useState({ number: '', capacity: 4 });
9
+
10
+ useEffect(() => {
11
+ onValue(ref(db, 'config/tables'), (snapshot) => {
12
+ const data = snapshot.val();
13
+ if (data) {
14
+ setTables(Object.keys(data).map(id => ({ id, ...data[id] })).sort((a,b) => a.number - b.number));
15
+ } else {
16
+ setTables([]);
17
+ }
18
+ });
19
+ }, []);
20
+
21
+ const addTable = async (e) => {
22
+ e.preventDefault();
23
+ if (!newTable.number) return;
24
+ const tableRef = push(ref(db, 'config/tables'));
25
+ await set(tableRef, {
26
+ number: parseInt(newTable.number),
27
+ capacity: parseInt(newTable.capacity),
28
+ status: 'available',
29
+ x: 0,
30
+ y: 0
31
+ });
32
+ setNewTable({ number: '', capacity: 4 });
33
+ };
34
+
35
+ const deleteTable = async (id) => {
36
+ if (window.confirm('¿Eliminar esta mesa del mapa?')) {
37
+ await remove(ref(db, `config/tables/${id}`));
38
+ }
39
+ };
40
+
41
+ return (
42
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
43
+ <header style={{ marginBottom: '2.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
44
+ <div>
45
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
46
+ <MapIcon size={28} /> Diseño de Salón
47
+ </h2>
48
+ <p style={{ color: 'var(--text-muted)' }}>Configura la distribución física de tu restaurante</p>
49
+ </div>
50
+ </header>
51
+
52
+ <div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '2rem' }}>
53
+
54
+ {/* Sidebar: Add Tables */}
55
+ <aside className="glass-card" style={{ alignSelf: 'start' }}>
56
+ <h3 style={{ marginBottom: '1.5rem', fontSize: '1.1rem' }}>Añadir Mesas</h3>
57
+ <form onSubmit={addTable} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
58
+ <div>
59
+ <label style={labelStyle}>Número de Mesa</label>
60
+ <input type="number" value={newTable.number} onChange={e => setNewTable({...newTable, number: e.target.value})} style={inputStyle} placeholder="Ej. 10" />
61
+ </div>
62
+ <div>
63
+ <label style={labelStyle}>Capacidad (Personas)</label>
64
+ <input type="number" value={newTable.capacity} onChange={e => setNewTable({...newTable, capacity: e.target.value})} style={inputStyle} placeholder="Ej. 4" />
65
+ </div>
66
+ <button type="submit" className="btn-primary" style={{ padding: '0.8rem' }}>
67
+ <Plus size={18} /> Crear Mesa
68
+ </button>
69
+ </form>
70
+
71
+ <div style={{ marginTop: '2.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>
79
+ <button onClick={() => deleteTable(t.id)} style={{ color: 'var(--primary)', background: 'none', border: 'none' }}><Trash2 size={14} /></button>
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </div>
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
+ )}
113
+ </div>
114
+ </section>
115
+
116
+ </div>
117
+ </div>
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' };
src/components/admin/UserManager.jsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue, set, update } from 'firebase/database';
4
+ import { Users, Shield, UserPlus, Trash2, CheckCircle, Activity, ShieldCheck, UserCog } from 'lucide-react';
5
+
6
+ export default function UserManager() {
7
+ const [employees, setEmployees] = useState([]);
8
+ const [newEmployee, setNewEmployee] = useState({ email: '', role: 'mesero', name: '' });
9
+
10
+ useEffect(() => {
11
+ onValue(ref(db, 'users'), (snapshot) => {
12
+ const data = snapshot.val();
13
+ if (data) {
14
+ setEmployees(Object.keys(data).map(uid => ({ uid, ...data[uid] })));
15
+ }
16
+ });
17
+ }, []);
18
+
19
+ const handleUpdateRole = async (uid, newRole) => {
20
+ await update(ref(db, `users/${uid}`), { role: newRole });
21
+ };
22
+
23
+ const roleColors = {
24
+ admin: 'var(--primary)',
25
+ mesero: 'var(--success)',
26
+ cocina: 'var(--warning)',
27
+ cajero: '#50ADE6'
28
+ };
29
+
30
+ return (
31
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
32
+ <header style={{ marginBottom: '2.5rem' }}>
33
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
34
+ <UserCog size={28} /> Equipo y Roles
35
+ </h2>
36
+ <p style={{ color: 'var(--text-muted)' }}>Control de acceso y permisos para empleados</p>
37
+ </header>
38
+
39
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.5rem' }}>
40
+ {employees.map(emp => (
41
+ <div key={emp.uid} className="glass-card" style={{ padding: '1.5rem', borderLeft: `4px solid ${roleColors[emp.role] || '#fff'}` }}>
42
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '1rem' }}>
43
+ <div>
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>
51
+
52
+ <div style={{ marginBottom: '1.5rem' }}>
53
+ <label style={labelStyle}>Rol Asignado</label>
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>
61
+ <option value="cocina">Cocina (KDS)</option>
62
+ <option value="cajero">Cajero (Caja/POS)</option>
63
+ </select>
64
+ </div>
65
+
66
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
67
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.7rem', color: 'var(--success)' }}>
68
+ <Activity size={12} /> Activo
69
+ </div>
70
+ <button style={{ background: 'none', border: 'none', color: 'var(--primary)', cursor: 'pointer' }} title="Revocar Acceso">
71
+ <Trash2 size={16} />
72
+ </button>
73
+ </div>
74
+ </div>
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>
82
+ <p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>El empleado debe registrarse primero en la app para aparecer aquí.</p>
83
+ </div>
84
+ </div>
85
+
86
+ <div className="glass-panel" style={{ marginTop: '3rem', padding: '2rem' }}>
87
+ <h3 style={{ fontSize: '1.1rem', marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
88
+ <ShieldCheck size={18} /> Permisos por Rol
89
+ </h3>
90
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
91
+ {[
92
+ { role: 'Admin', rights: 'Control total de inventario, finanzas y menú.' },
93
+ { role: 'Mesero', rights: 'Toma de pedidos, cierre de cuentas y mapa de mesas.' },
94
+ { role: 'Cocina', rights: 'Visualización de tickets y cambio de estado de preparación.' },
95
+ { role: 'Cajero', rights: 'Gestión de arqueo de caja y cobros de POS.' }
96
+ ].map(r => (
97
+ <div key={r.role}>
98
+ <h4 style={{ fontSize: '0.9rem', fontWeight: '800', marginBottom: '0.5rem' }}>{r.role}</h4>
99
+ <p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', lineHeight: '1.4' }}>{r.rights}</p>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ </div>
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' };
src/context/AuthContext.jsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+ import { auth, db } from '../firebase/config';
3
+ import { onAuthStateChanged, signInWithEmailAndPassword, signOut } from 'firebase/auth';
4
+ import { ref, get } from 'firebase/database';
5
+
6
+ export const AuthContext = createContext();
7
+
8
+ export const useAuth = () => {
9
+ return useContext(AuthContext);
10
+ };
11
+
12
+ export const AuthProvider = ({ children }) => {
13
+ const [currentUser, setCurrentUser] = useState(null);
14
+ const [userRole, setUserRole] = useState(null);
15
+ const [loading, setLoading] = useState(true);
16
+
17
+ const login = async (email, password) => {
18
+ return signInWithEmailAndPassword(auth, email, password);
19
+ };
20
+
21
+ const logout = () => {
22
+ return signOut(auth);
23
+ };
24
+
25
+ useEffect(() => {
26
+ const unsubscribe = onAuthStateChanged(auth, async (user) => {
27
+ if (user) {
28
+ setCurrentUser(user);
29
+ // Obtener rol del usuario desde DB
30
+ try {
31
+ const userRef = ref(db, `users/${user.uid}`);
32
+ const snapshot = await get(userRef);
33
+ if (snapshot.exists()) {
34
+ setUserRole(snapshot.val().role);
35
+ } else {
36
+ console.log("Rol no definido para este usuario = asume default");
37
+ setUserRole('comensal');
38
+ }
39
+ } catch (error) {
40
+ console.error("Error al obtener el rol del usuario", error);
41
+ }
42
+ } else {
43
+ setCurrentUser(null);
44
+ setUserRole(null);
45
+ }
46
+ setLoading(false);
47
+ });
48
+
49
+ return unsubscribe;
50
+ }, []);
51
+
52
+ const value = {
53
+ currentUser,
54
+ userRole,
55
+ login,
56
+ logout
57
+ };
58
+
59
+ return (
60
+ <AuthContext.Provider value={value}>
61
+ {!loading && children}
62
+ </AuthContext.Provider>
63
+ );
64
+ };
src/firebase/config.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeApp } from "firebase/app";
2
+ import { getAuth } from "firebase/auth";
3
+ import { getDatabase } from "firebase/database";
4
+ import { getStorage } from "firebase/storage";
5
+
6
+ const firebaseConfig = {
7
+ apiKey: "AIzaSyBm2Tvsz7SHtWEWO-97p3gvqC76iqt05VM",
8
+ authDomain: "rest-fc379.firebaseapp.com",
9
+ databaseURL: "https://rest-fc379-default-rtdb.firebaseio.com",
10
+ projectId: "rest-fc379",
11
+ storageBucket: "rest-fc379.firebasestorage.app",
12
+ messagingSenderId: "119970678093",
13
+ appId: "1:119970678093:web:16fb097882249fdce0e304",
14
+ measurementId: "G-ZDEW4Q04NN"
15
+ };
16
+
17
+ const app = initializeApp(firebaseConfig);
18
+ export const auth = getAuth(app);
19
+ export const db = getDatabase(app);
20
+ export const storage = getStorage(app);
21
+
22
+ export default app;
src/index.css ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
25
+ --warning: #F5A623;
26
+ --danger: #FF3B30;
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 */
39
+ --transition-fast: 0.15s ease;
40
+ --transition-normal: 0.3s cubic-bezier(0.25, 1, 0.5, 1);
41
+ --transition-slow: 0.5s cubic-bezier(0.25, 1, 0.5, 1);
42
+
43
+ /* Radii */
44
+ --radius-sm: 8px;
45
+ --radius-md: 16px;
46
+ --radius-lg: 24px;
47
+ --radius-pill: 9999px;
48
+ }
49
+
50
+ * {
51
+ margin: 0;
52
+ padding: 0;
53
+ box-sizing: border-box;
54
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
55
+ }
56
+
57
+ body {
58
+ background-color: var(--bg-base);
59
+ color: var(--text-main);
60
+ min-height: 100vh;
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
+
69
+ a {
70
+ color: inherit;
71
+ text-decoration: none;
72
+ }
73
+
74
+ button {
75
+ background: none;
76
+ border: none;
77
+ color: inherit;
78
+ cursor: pointer;
79
+ font-family: inherit;
80
+ }
81
+
82
+ /* Base Glassmorphic Container */
83
+ .glass-panel {
84
+ background: var(--bg-glass);
85
+ backdrop-filter: var(--glass-blur);
86
+ -webkit-backdrop-filter: var(--glass-blur);
87
+ border: var(--glass-border);
88
+ border-radius: var(--radius-md);
89
+ box-shadow: var(--shadow-md);
90
+ }
91
+
92
+ /* Glass Card with Hover */
93
+ .glass-card {
94
+ background: var(--bg-card);
95
+ backdrop-filter: blur(12px);
96
+ -webkit-backdrop-filter: blur(12px);
97
+ border: var(--glass-border);
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 */
111
+ .btn-primary {
112
+ background: var(--primary);
113
+ color: #fff;
114
+ padding: 0.75rem 1.75rem;
115
+ border-radius: var(--radius-pill);
116
+ font-weight: 600;
117
+ letter-spacing: 0.5px;
118
+ transition: all var(--transition-fast);
119
+ box-shadow: 0 4px 14px var(--primary-glow);
120
+ display: inline-flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .btn-primary:hover {
127
+ background: var(--primary-hover);
128
+ transform: scale(1.02);
129
+ box-shadow: 0 6px 20px var(--primary-glow);
130
+ }
131
+
132
+ .btn-primary:active {
133
+ transform: scale(0.98);
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);
142
+ font-weight: 500;
143
+ transition: all var(--transition-fast);
144
+ backdrop-filter: blur(8px);
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;
158
+ }
159
+
160
+ .text-gradient-primary {
161
+ background: linear-gradient(135deg, var(--primary) 0%, #ff8a8e 100%);
162
+ -webkit-background-clip: text;
163
+ -webkit-text-fill-color: transparent;
164
+ background-clip: text;
165
+ }
166
+
167
+ /* Layout Utilities */
168
+ .app-container {
169
+ display: flex;
170
+ min-height: 100vh;
171
+ width: 100%;
172
+ }
173
+
174
+ .main-content {
175
+ flex: 1;
176
+ padding: 2rem;
177
+ display: flex;
178
+ flex-direction: column;
179
+ }
180
+
181
+ /* Animations */
182
+ @keyframes fadeIn {
183
+ from { opacity: 0; transform: translateY(10px); }
184
+ to { opacity: 1; transform: translateY(0); }
185
+ }
186
+
187
+ .animate-fade-in {
188
+ animation: fadeIn 0.4s ease-out forwards;
189
+ }
src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
src/pages/AdminDashboard.jsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
8
+ 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
+
13
+ export default function AdminDashboard() {
14
+ const { logout, currentUser } = useAuth();
15
+ const navigate = useNavigate();
16
+ const [activeTab, setActiveTab] = useState('overview');
17
+
18
+ const handleLogout = async () => {
19
+ await logout();
20
+ navigate('/login');
21
+ };
22
+
23
+ const navItems = [
24
+ { id: 'overview', label: 'Resumen', icon: <LayoutDashboard size={20} /> },
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
+ ];
34
+
35
+ return (
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' }}>
45
+ {navItems.map(item => (
46
+ <button
47
+ key={item.id}
48
+ onClick={() => {
49
+ if (item.action) item.action();
50
+ else setActiveTab(item.id);
51
+ }}
52
+ style={{
53
+ display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.8rem 1rem', width: '100%',
54
+ borderRadius: 'var(--radius-sm)', transition: 'all var(--transition-fast)',
55
+ background: activeTab === item.id ? 'rgba(255,90,95,0.1)' : 'transparent',
56
+ color: activeTab === item.id ? 'var(--primary)' : 'var(--text-muted)',
57
+ fontWeight: activeTab === item.id ? '600' : '500',
58
+ borderLeft: activeTab === item.id ? '3px solid var(--primary)' : '3px solid transparent'
59
+ }}
60
+ >
61
+ {item.icon}
62
+ {item.label}
63
+ </button>
64
+ ))}
65
+ </nav>
66
+
67
+ <div style={{ padding: '1.5rem 1rem' }}>
68
+ <button onClick={handleLogout} className="btn-glass" style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}>
69
+ <LogOut size={18} /> Salir
70
+ </button>
71
+ </div>
72
+ </aside>
73
+
74
+ {/* Main Content Area */}
75
+ <main className="main-content" style={{ overflowY: 'auto', background: 'var(--bg-base)' }}>
76
+ <div className="animate-fade-in" style={{ width: '100%', maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
77
+ {activeTab === 'overview' && <DashboardOverview />}
78
+ {activeTab === 'menu' && <MenuEditor />}
79
+ {activeTab === 'inventory' && <InventoryControl />}
80
+ {activeTab === 'reports' && <Reports />}
81
+ {activeTab === 'cash' && <FinanceManager />}
82
+ {activeTab === 'users' && <UserManager />}
83
+ {activeTab === 'tables' && <TableManager />}
84
+ </div>
85
+ </main>
86
+ </div>
87
+ );
88
+ }
src/pages/CustomerMenu.jsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { db } from '../firebase/config';
4
+ import { ref, onValue } from 'firebase/database';
5
+ import { Utensils, Star, Clock, MapPin, ChefHat, Instagram, Facebook, ArrowRight } from 'lucide-react';
6
+
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('dark'); // 'dark' or 'light'
12
+ const navigate = useNavigate();
13
+
14
+ useEffect(() => {
15
+ // Escuchar el menú
16
+ const menuRef = ref(db, 'menu');
17
+ onValue(menuRef, (snapshot) => {
18
+ const data = snapshot.val();
19
+ if (data) {
20
+ const list = Object.keys(data).map(id => ({ id, ...data[id] })).filter(item => item.active !== false);
21
+ setMenuItems(list);
22
+
23
+ const cats = [...new Set(list.map(item => item.category))];
24
+ setCategories(['Todos', ...cats]);
25
+ }
26
+ });
27
+
28
+ // Escuchar el tema configurado por el admin
29
+ const themeRef = ref(db, 'config/menuTheme');
30
+ onValue(themeRef, (snapshot) => {
31
+ if (snapshot.exists()) {
32
+ setMenuTheme(snapshot.val());
33
+ }
34
+ });
35
+ }, []);
36
+
37
+ const filteredItems = activeCategory === 'Todos'
38
+ ? menuItems
39
+ : menuItems.filter(item => item.category === activeCategory);
40
+
41
+ const isDark = menuTheme === 'dark';
42
+
43
+ const themeColors = {
44
+ bg: isDark ? '#0a0a0a' : '#ffffff',
45
+ text: isDark ? '#ffffff' : '#1a1a1a',
46
+ muted: isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)',
47
+ card: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)',
48
+ border: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
49
+ accent: '#FF6B6B'
50
+ };
51
+
52
+ return (
53
+ <div style={{
54
+ minHeight: '100vh',
55
+ background: themeColors.bg,
56
+ color: themeColors.text,
57
+ fontFamily: "'Outfit', sans-serif",
58
+ transition: 'all 0.5s ease'
59
+ }}>
60
+
61
+ {/* Hero Section */}
62
+ <section style={{
63
+ height: '60vh', position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center',
64
+ background: `linear-gradient(rgba(0,0,0,${isDark ? 0.7 : 0.4}), rgba(0,0,0,${isDark ? 0.7 : 0.4})), url('https://images.unsplash.com/photo-1514362545857-3bc16c4c7d1b?auto=format&fit=crop&q=80&w=2070')`,
65
+ backgroundSize: 'cover', backgroundPosition: 'center'
66
+ }}>
67
+ <div style={{ textAlign: 'center', padding: '0 20px' }} className="animate-slide-up">
68
+ <h1 style={{ fontSize: '4rem', fontWeight: '900', marginBottom: '1rem', letterSpacing: '-2px' }}>Gourmet Experience</h1>
69
+ <p style={{ fontSize: '1.25rem', opacity: 0.9, maxWidth: '600px', margin: '0 auto' }}>Descubre una fusión de sabores únicos, preparados con pasión y los ingredientes más frescos.</p>
70
+ <div style={{ marginTop: '2rem', display: 'flex', gap: '1rem', justifyContent: 'center' }}>
71
+ <button onClick={() => navigate('/login')} className="btn-primary" style={{ padding: '12px 30px', borderRadius: '30px' }}>
72
+ Reservar Mesa <ArrowRight size={18} style={{ marginLeft: '10px' }} />
73
+ </button>
74
+ </div>
75
+ </div>
76
+
77
+ {/* Badges */}
78
+ <div style={{
79
+ position: 'absolute', bottom: '-40px', left: '50%', transform: 'translateX(-50%)',
80
+ display: 'flex', gap: '30px', width: '100%', justifyContent: 'center', maxWidth: '800px'
81
+ }}>
82
+ {[
83
+ { icon: <Star size={20} />, text: "4.9 Estrellas" },
84
+ { icon: <Clock size={20} />, text: "30-45 min" },
85
+ { icon: <MapPin size={20} />, text: "Centro Histórico" }
86
+ ].map((badge, i) => (
87
+ <div key={i} className="glass-card" style={{
88
+ padding: '15px 25px', borderRadius: '15px', display: 'flex', alignItems: 'center', gap: '10px',
89
+ background: isDark ? 'rgba(30,30,30,0.8)' : 'rgba(255,255,255,0.9)',
90
+ color: isDark ? '#fff' : '#1a1a1a', border: `1px solid ${themeColors.border}`,
91
+ boxShadow: '0 10px 30px rgba(0,0,0,0.1)'
92
+ }}>
93
+ <span style={{ color: themeColors.accent }}>{badge.icon}</span>
94
+ <span style={{ fontWeight: '600', fontSize: '0.9rem' }}>{badge.text}</span>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </section>
99
+
100
+ {/* Menu Section */}
101
+ <section style={{ maxWidth: '1200px', margin: '100px auto', padding: '0 20px' }}>
102
+ <div style={{ textAlign: 'center', marginBottom: '60px' }}>
103
+ <h2 style={{ fontSize: '2.5rem', fontWeight: '800', marginBottom: '1rem' }}>Nuestra Carta</h2>
104
+ <div style={{ width: '60px', height: '4px', background: themeColors.accent, margin: '0 auto', borderRadius: '2px' }}></div>
105
+ </div>
106
+
107
+ {/* Category Tabs */}
108
+ <div style={{
109
+ display: 'flex', justifyContent: 'center', gap: '10px', marginBottom: '50px',
110
+ overflowX: 'auto', padding: '10px 0', scrollbarWidth: 'none'
111
+ }}>
112
+ {categories.map(cat => (
113
+ <button
114
+ key={cat}
115
+ onClick={() => setActiveCategory(cat)}
116
+ style={{
117
+ padding: '12px 25px', borderRadius: '30px', border: 'none', cursor: 'pointer',
118
+ background: activeCategory === cat ? themeColors.accent : themeColors.card,
119
+ color: activeCategory === cat ? '#fff' : themeColors.text,
120
+ fontWeight: '600', transition: 'all 0.3s ease', whiteSpace: 'nowrap',
121
+ border: `1px solid ${activeCategory === cat ? themeColors.accent : themeColors.border}`
122
+ }}
123
+ >
124
+ {cat}
125
+ </button>
126
+ ))}
127
+ </div>
128
+
129
+ {/* Menu Grid */}
130
+ <div style={{
131
+ display: 'grid',
132
+ gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
133
+ gap: '30px'
134
+ }}>
135
+ {filteredItems.map(item => (
136
+ <div key={item.id} className="glass-card" style={{
137
+ padding: '0', overflow: 'hidden', border: `1px solid ${themeColors.border}`,
138
+ background: themeColors.card, position: 'relative', transition: 'transform 0.3s ease'
139
+ }} onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-10px)'} onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}>
140
+
141
+ {/* Dish Image */}
142
+ <div style={{ height: '240px', overflow: 'hidden', position: 'relative' }}>
143
+ <img
144
+ src={item.image || 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&q=80&w=1760'}
145
+ alt={item.name}
146
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
147
+ />
148
+ <div style={{
149
+ position: 'absolute', top: '15px', right: '15px',
150
+ background: 'rgba(0,0,0,0.6)', color: '#fff', padding: '5px 15px',
151
+ borderRadius: '20px', fontWeight: '800', fontSize: '1.1rem', backdropFilter: 'blur(5px)'
152
+ }}>
153
+ ${item.price}
154
+ </div>
155
+ </div>
156
+
157
+ {/* Dish Details */}
158
+ <div style={{ padding: '25px' }}>
159
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' }}>
160
+ <span style={{ fontSize: '0.75rem', textTransform: 'uppercase', color: themeColors.accent, fontWeight: '800', letterSpacing: '1px' }}>{item.category}</span>
161
+ </div>
162
+ <h3 style={{ fontSize: '1.4rem', fontWeight: '700', marginBottom: '10px' }}>{item.name}</h3>
163
+ <p style={{ color: themeColors.muted, fontSize: '0.95rem', lineHeight: '1.6' }}>Preparado con ingredientes frescos de la estación y el toque secreto de nuestro chef.</p>
164
+ <div style={{ marginTop: '20px', display: 'flex', gap: '15px' }}>
165
+ <span style={{ fontSize: '0.85rem', display: 'flex', alignItems: 'center', gap: '5px', color: themeColors.muted }}>
166
+ <ChefHat size={16} /> Especialidad
167
+ </span>
168
+ <span style={{ fontSize: '0.85rem', display: 'flex', alignItems: 'center', gap: '5px', color: themeColors.muted }}>
169
+ <Clock size={16} /> 20 min
170
+ </span>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ </section>
177
+
178
+ {/* Footer */}
179
+ <footer style={{
180
+ padding: '80px 20px', borderTop: `1px solid ${themeColors.border}`,
181
+ background: isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.02)', textAlign: 'center'
182
+ }}>
183
+ <div style={{ maxWidth: '600px', margin: '0 auto' }}>
184
+ <h2 style={{ fontSize: '1.8rem', fontWeight: '900', marginBottom: '1.5rem' }}>Restaurant OS</h2>
185
+ <p style={{ color: themeColors.muted, marginBottom: '30px' }}>Siguenos en nuestras redes sociales para estar al tanto de nuestras promociones y eventos especiales.</p>
186
+ <div style={{ display: 'flex', justifyContent: 'center', gap: '20px' }}>
187
+ <button onClick={() => {}} style={{ background: 'none', border: 'none', color: themeColors.text, cursor: 'pointer' }}><Instagram size={24} /></button>
188
+ <button onClick={() => {}} style={{ background: 'none', border: 'none', color: themeColors.text, cursor: 'pointer' }}><Facebook size={24} /></button>
189
+ </div>
190
+ <div style={{ marginTop: '50px', fontSize: '0.85rem', color: themeColors.muted }}>
191
+ © {new Date().getFullYear()} Gourmet Experience. Todos los derechos reservados.
192
+ </div>
193
+ </div>
194
+ </footer>
195
+
196
+ {/* Admin Quick Back (Floating) */}
197
+ <button
198
+ onClick={() => navigate('/login')}
199
+ style={{
200
+ position: 'fixed', bottom: '30px', right: '30px', width: '50px', height: '50px', borderRadius: '50%',
201
+ background: themeColors.accent, color: '#fff', border: 'none', cursor: 'pointer',
202
+ boxShadow: '0 10px 20px rgba(0,0,0,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100
203
+ }}
204
+ title="Admin Login"
205
+ >
206
+ <Utensils size={24} />
207
+ </button>
208
+ </div>
209
+ );
210
+ }
src/pages/KitchenView.jsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { db } from '../firebase/config';
3
+ import { ref, onValue, update } from 'firebase/database';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import { CheckCircle, Clock, UtensilsCrossed, LogOut, ExternalLink, PlayCircle, Flame } from 'lucide-react';
7
+
8
+ export default function KitchenView() {
9
+ const { logout } = useAuth();
10
+ const navigate = useNavigate();
11
+ const [orders, setOrders] = useState([]);
12
+ const lastOrderCount = useRef(0);
13
+
14
+ useEffect(() => {
15
+ const ordersRef = ref(db, 'orders');
16
+ onValue(ordersRef, (snapshot) => {
17
+ const data = snapshot.val();
18
+ if (data) {
19
+ const orderList = Object.keys(data).map(k => ({ id: k, ...data[k] }))
20
+ .filter(o => o.status !== 'completed')
21
+ .sort((a, b) => b.timestamp - a.timestamp);
22
+
23
+ // Alerta de sonido si hay nuevas órdenes
24
+ if (orderList.length > lastOrderCount.current) {
25
+ playNotificationSound();
26
+ }
27
+ lastOrderCount.current = orderList.length;
28
+ setOrders(orderList);
29
+ } else {
30
+ setOrders([]);
31
+ }
32
+ });
33
+ }, []);
34
+
35
+ const playNotificationSound = () => {
36
+ const audio = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3');
37
+ audio.play().catch(e => console.log("User interaction required for audio"));
38
+ };
39
+
40
+ const updateStatus = async (id, status) => {
41
+ await update(ref(db, `orders/${id}`), { status });
42
+ };
43
+
44
+ const handleLogout = async () => {
45
+ await logout();
46
+ navigate('/login');
47
+ };
48
+
49
+ return (
50
+ <div className="app-container" style={{ flexDirection: 'column', background: 'var(--bg-base)', height: '100vh', overflow: 'hidden' }}>
51
+ <header className="glass-panel" style={{ padding: '1rem 2.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderRadius: '0', borderBottom: '1px solid var(--border-subtle)' }}>
52
+ <h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
53
+ <UtensilsCrossed size={24} /> KDS - Centro de Cocina
54
+ </h1>
55
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
56
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--success)', fontSize: '0.9rem' }}>
57
+ <div style={{ width: '8px', height: '8px', background: 'var(--success)', borderRadius: '50%', boxShadow: '0 0 10px var(--success)' }}></div>
58
+ En Vivo
59
+ </div>
60
+ <button className="btn-glass" onClick={handleLogout} style={{ padding: '0.5rem 1rem', color: 'var(--primary)' }}>
61
+ <LogOut size={16} /> Salir
62
+ </button>
63
+ </div>
64
+ </header>
65
+
66
+ <div className="main-content" style={{ padding: '2rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '1.5rem', overflowY: 'auto' }}>
67
+ {orders.length === 0 ? (
68
+ <div style={{ gridColumn: '1 / -1', textAlign: 'center', paddingTop: '10vh', color: 'var(--text-muted)' }}>
69
+ <UtensilsCrossed size={60} style={{ opacity: 0.1, marginBottom: '1rem' }} />
70
+ <h2 style={{ fontSize: '1.5rem' }}>Cocina Despejada</h2>
71
+ <p>No hay pedidos en espera.</p>
72
+ </div>
73
+ ) : (
74
+ orders.map((order) => (
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)' }}>
82
+ {order.table} {order.type === 'Llevar' && '🛍️'} {order.type === 'Delivery' && '🚀'}
83
+ </span>
84
+ <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>
85
+ {Math.floor((Date.now() - order.timestamp) / 60000)} min atrás
86
+ </span>
87
+ </div>
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
+ ))}
95
+ </div>
96
+
97
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
98
+ {order.status !== 'preparing' ? (
99
+ <button
100
+ className="btn-glass"
101
+ onClick={() => updateStatus(order.id, 'preparing')}
102
+ style={{ background: 'rgba(255,160,0,0.1)', color: '#FFB000' }}
103
+ >
104
+ <Flame size={18} /> Preparar
105
+ </button>
106
+ ) : (
107
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.8rem', color: 'var(--warning)', fontWeight: '700' }}>
108
+ ENCENDIDO...
109
+ </div>
110
+ )}
111
+ <button
112
+ className="btn-primary"
113
+ onClick={() => updateStatus(order.id, 'completed')}
114
+ style={{ background: 'var(--success)', color: '#fff' }}
115
+ >
116
+ <CheckCircle size={18} /> Listo
117
+ </button>
118
+ </div>
119
+ </div>
120
+ ))
121
+ )}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
src/pages/Login.jsx ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { User, ShieldCheck, Utensils, ArrowRightCircle } from 'lucide-react';
5
+
6
+ export default function Login() {
7
+ const [email, setEmail] = useState('admin@rest-os.com');
8
+ const [password, setPassword] = useState('admin-password123');
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState('');
11
+ const [loginMode, setLoginMode] = useState('admin'); // 'admin' or 'mesero'
12
+
13
+ const { login, currentUser } = useAuth();
14
+ const navigate = useNavigate();
15
+
16
+ const handleSubmit = async (e) => {
17
+ e.preventDefault();
18
+ setLoading(true);
19
+ setError('');
20
+
21
+ // Si ya hay una sesión activa, simplemente navegar
22
+ if (currentUser && currentUser.email === email) {
23
+ const targetRole = email.includes('admin') ? 'admin' : 'mesero';
24
+ navigate(targetRole === 'admin' ? '/admin' : '/pos');
25
+ setLoading(false);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ await login(email, password);
31
+ // navigation is handled by RootRedirect in App.jsx
32
+ } catch (err) {
33
+ setError('Credenciales inválidas. Por favor intente de nuevo.');
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ const setDemo = (role) => {
40
+ if (role === 'admin') {
41
+ setEmail('admin@rest-os.com');
42
+ setPassword('admin-password123');
43
+ setLoginMode('admin');
44
+ } else {
45
+ setEmail('mesero@rest-os.com');
46
+ setPassword('mesero-password123');
47
+ setLoginMode('mesero');
48
+ }
49
+ };
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 */}
57
+ <button
58
+ onClick={() => navigate('/menu')}
59
+ className="glass-card animate-fade-in"
60
+ style={{
61
+ position: 'absolute', top: '30px', right: '30px', padding: '12px 24px',
62
+ display: 'flex', alignItems: 'center', gap: '10px', color: 'var(--primary)',
63
+ cursor: 'pointer', border: '1px solid rgba(255,107,107,0.3)',
64
+ zIndex: 10
65
+ }}
66
+ >
67
+ <Utensils size={20} />
68
+ <span style={{ fontWeight: '600' }}>Ver Carta Digital</span>
69
+ <ArrowRightCircle size={18} />
70
+ </button>
71
+
72
+ <div className="glass-panel animate-slide-up" style={{
73
+ maxWidth: '900px', width: '100%', padding: '0', display: 'flex',
74
+ overflow: 'hidden', boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)'
75
+ }}>
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
+ }}>
83
+ <h2 style={{ fontSize: '1.4rem', fontWeight: '800', marginBottom: '10px' }}>Bienvenido</h2>
84
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginBottom: '20px' }}>Seleccione su rol para ingresar al sistema</p>
85
+
86
+ <button
87
+ onClick={() => { setDemo('admin'); setError(''); }}
88
+ style={{
89
+ padding: '20px', borderRadius: 'var(--radius-sm)', border: '1px solid',
90
+ borderColor: loginMode === 'admin' ? 'var(--primary)' : 'transparent',
91
+ background: loginMode === 'admin' ? 'rgba(255,107,107,0.1)' : 'rgba(255,255,255,0.02)',
92
+ display: 'flex', alignItems: 'center', gap: '15px', textAlign: 'left',
93
+ transition: 'all 0.3s ease', cursor: 'pointer'
94
+ }}
95
+ >
96
+ <div style={{
97
+ padding: '10px', borderRadius: '10px',
98
+ background: loginMode === 'admin' ? 'var(--primary)' : 'rgba(255,255,255,0.05)',
99
+ color: loginMode === 'admin' ? '#fff' : 'var(--text-muted)'
100
+ }}>
101
+ <ShieldCheck size={24} />
102
+ </div>
103
+ <div>
104
+ <div style={{ fontWeight: '700', color: loginMode === 'admin' ? 'var(--text-main)' : 'var(--text-muted)' }}>Administrador</div>
105
+ <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Gestión y Reportes</div>
106
+ </div>
107
+ </button>
108
+
109
+ <button
110
+ onClick={() => { setDemo('mesero'); setError(''); }}
111
+ style={{
112
+ padding: '20px', borderRadius: 'var(--radius-sm)', border: '1px solid',
113
+ borderColor: loginMode === 'mesero' ? 'var(--primary)' : 'transparent',
114
+ background: loginMode === 'mesero' ? 'rgba(255,107,107,0.1)' : 'rgba(255,255,255,0.02)',
115
+ display: 'flex', alignItems: 'center', gap: '15px', textAlign: 'left',
116
+ transition: 'all 0.3s ease', cursor: 'pointer'
117
+ }}
118
+ >
119
+ <div style={{
120
+ padding: '10px', borderRadius: '10px',
121
+ background: loginMode === 'mesero' ? 'var(--primary)' : 'rgba(255,255,255,0.05)',
122
+ color: loginMode === 'mesero' ? '#fff' : 'var(--text-muted)'
123
+ }}>
124
+ <User size={24} />
125
+ </div>
126
+ <div>
127
+ <div style={{ fontWeight: '700', color: loginMode === 'mesero' ? 'var(--text-main)' : 'var(--text-muted)' }}>Mesero / POS</div>
128
+ <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Pedidos y Control</div>
129
+ </div>
130
+ </button>
131
+ </div>
132
+
133
+ {/* Lado Derecho: Formulario de Login */}
134
+ <div style={{ flex: 1, padding: '60px' }}>
135
+ <div style={{ marginBottom: '40px' }}>
136
+ <h1 className="text-gradient" style={{ fontSize: '2.5rem', fontWeight: '900', marginBottom: '10px' }}>
137
+ {loginMode === 'admin' ? 'Dashboard Admin' : 'Terminal Meseros'}
138
+ </h1>
139
+ <p style={{ color: 'var(--text-muted)' }}>Ingrese sus credenciales para continuar</p>
140
+ </div>
141
+
142
+ {error && (
143
+ <div className="glass-card" style={{ padding: '10px 20px', background: 'rgba(255,90,95,0.1)', border: '1px solid var(--primary)', borderRadius: 'var(--radius-sm)', marginBottom: '20px', color: 'var(--primary)', fontSize: '0.9rem' }}>
144
+ {error}
145
+ </div>
146
+ )}
147
+
148
+ <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
149
+ <div>
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}>
164
+ {loading ? 'Validando...' : 'Iniciar Sesión'}
165
+ </button>
166
+ </form>
167
+
168
+ {/* Botón Demo rápido */}
169
+ <div style={{ marginTop: '30px', textAlign: 'center' }}>
170
+ <button
171
+ onClick={() => setDemo(loginMode)}
172
+ style={{ background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', textDecoration: 'underline', fontSize: '0.85rem' }}
173
+ >
174
+ Usar cuenta de prueba para {loginMode === 'admin' ? 'Admin' : 'Mesero'}
175
+ </button>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
src/pages/WaiterPOS.jsx ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 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 } 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() {
9
+ const { logout, currentUser } = useAuth();
10
+ const navigate = useNavigate();
11
+ const [menu, setMenu] = useState([]);
12
+ const [activeTable, setActiveTable] = useState(null);
13
+ const [cart, setCart] = useState([]);
14
+ const [orderType, setOrderType] = useState('Mesa'); // Mesa, Llevar, Delivery
15
+ const [discount, setDiscount] = useState(0);
16
+ const [tip, setTip] = useState(0);
17
+ const [paymentMethod, setPaymentMethod] = useState('Efectivo');
18
+ const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
19
+
20
+ // Fetch Menu
21
+ useEffect(() => {
22
+ onValue(ref(db, 'menu'), (snapshot) => {
23
+ const data = snapshot.val();
24
+ if (data) {
25
+ setMenu(Object.keys(data).map(k => ({ id: k, ...data[k] })));
26
+ }
27
+ });
28
+ }, []);
29
+
30
+ const handleLogout = async () => {
31
+ await logout();
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
+ };
51
+
52
+ const subtotal = cart.reduce((acc, item) => acc + (item.price * item.qty), 0);
53
+ const discountAmount = (subtotal * discount) / 100;
54
+ const totalCart = subtotal - discountAmount + Number(tip || 0);
55
+
56
+ const sendOrder = async () => {
57
+ if ((orderType === 'Mesa' && !activeTable) || cart.length === 0) return;
58
+ const orderRef = push(ref(db, 'orders'));
59
+ const orderData = {
60
+ table: orderType === 'Mesa' ? activeTable : orderType,
61
+ type: orderType,
62
+ waiter: currentUser?.email,
63
+ items: cart,
64
+ subtotal,
65
+ discount,
66
+ tip,
67
+ total: totalCart,
68
+ paymentMethod: paymentMethod,
69
+ status: 'pending',
70
+ timestamp: Date.now()
71
+ };
72
+
73
+ await set(orderRef, orderData);
74
+
75
+ if (!isCheckoutOpen) {
76
+ setCart([]);
77
+ setActiveTable(null);
78
+ alert('¡Comanda enviada a cocina exitosamente!');
79
+ } else {
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.
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
+ }
102
+ }
103
+ }, { onlyOnce: true });
104
+ }
105
+
106
+ setIsCheckoutOpen(false);
107
+ setCart([]);
108
+ setActiveTable(null);
109
+ alert('¡Venta realizada y stock actualizado!');
110
+ }
111
+ };
112
+
113
+ const printTicket = () => {
114
+ const printWindow = window.open('', '_blank');
115
+ printWindow.document.write(`
116
+ <html>
117
+ <head><title>Ticket de Venta</title></head>
118
+ <body style="font-family: monospace; width: 300px; padding: 20px;">
119
+ <h2 style="text-align: center;">RESTAURANT OS</h2>
120
+ <hr/>
121
+ <p>Mesa: ${activeTable || orderType}</p>
122
+ <p>Fecha: ${new Date().toLocaleString()}</p>
123
+ <hr/>
124
+ ${cart.map(item => `<p>${item.qty}x ${item.name.padEnd(20)} $${(item.price * item.qty).toFixed(2)}</p>`).join('')}
125
+ <hr/>
126
+ <p>Subtotal: $${subtotal.toFixed(2)}</p>
127
+ <p>Descuento (${discount}%): -$${discountAmount.toFixed(2)}</p>
128
+ <p>Propina: $${Number(tip || 0).toFixed(2)}</p>
129
+ <h3 style="text-align: right;">Total: $${totalCart.toFixed(2)}</h3>
130
+ <p style="text-align: center;">¡Gracias por su visita!</p>
131
+ </body>
132
+ </html>
133
+ `);
134
+ printWindow.document.close();
135
+ printWindow.print();
136
+ };
137
+
138
+ return (
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>
146
+ <button className="btn-glass" onClick={() => window.open('/kitchen', '_blank')} style={{ padding: '0.5rem 1rem' }}>Cocina</button>
147
+ <button className="btn-glass" onClick={handleLogout} style={{ padding: '0.5rem 1rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
148
+ <LogOut size={16} /> Salir
149
+ </button>
150
+ </div>
151
+ </header>
152
+
153
+ <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
154
+ {/* Left Column: Tables & Menu */}
155
+ <div style={{ flex: '65%', padding: '1.5rem', overflowY: 'auto', background: 'rgba(255,255,255,0.01)' }}>
156
+
157
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
158
+ <h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>Tipo de Pedido</h2>
159
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
160
+ {[
161
+ { id: 'Mesa', icon: <Coffee size={18} /> },
162
+ { id: 'Llevar', icon: <ShoppingBag size={18} /> },
163
+ { id: 'Delivery', icon: <Truck size={18} /> }
164
+ ].map(type => (
165
+ <button
166
+ key={type.id}
167
+ onClick={() => { setOrderType(type.id); if(type.id !== 'Mesa') setActiveTable(null); }}
168
+ className={orderType === type.id ? 'btn-primary' : 'btn-glass'}
169
+ style={{ padding: '0.5rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}
170
+ >
171
+ {type.icon} {type.id}
172
+ </button>
173
+ ))}
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 => (
180
+ <button
181
+ key={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}
193
+ </button>
194
+ ))}
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)}
204
+ className="glass-card"
205
+ style={{ textAlign: 'left', padding: '1rem', borderColor: 'var(--border-subtle)', position: 'relative' }}
206
+ >
207
+ <p style={{ fontWeight: '600', marginBottom: '0.5rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
208
+ {product.name}
209
+ </p>
210
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
211
+ <span style={{ color: 'var(--success)', fontWeight: '700' }}>${Number(product.price).toFixed(2)}</span>
212
+ <span style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{product.category}</span>
213
+ </div>
214
+ </button>
215
+ ))}
216
+ </div>
217
+ </div>
218
+
219
+ {/* Right Column: Order Details */}
220
+ <div style={{ flex: '35%', background: 'var(--bg-card)', borderLeft: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
221
+ <div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
222
+ <h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>
223
+ {orderType === 'Mesa' ? `Mesa ${activeTable || '?'}` : orderType}
224
+ </h2>
225
+ <button onClick={() => setCart([])} style={{ color: 'var(--danger)', fontSize: '0.8rem', background: 'none', border: 'none', cursor: 'not-allowed' }} disabled={cart.length === 0}>
226
+ Limpiar
227
+ </button>
228
+ </div>
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>
236
+ </div>
237
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
238
+ <span style={{ fontWeight: '700' }}>${(item.qty * item.price).toFixed(2)}</span>
239
+ <button onClick={() => removeFromCart(item.id)} style={{ color: 'var(--danger)', background: 'none', border: 'none', cursor: 'pointer' }}>
240
+ <Trash2 size={16} />
241
+ </button>
242
+ </div>
243
+ </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>
251
+ <span>${subtotal.toFixed(2)}</span>
252
+ </div>
253
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--primary)' }}>
254
+ <span>Descuento ({discount}%):</span>
255
+ <span>-${discountAmount.toFixed(2)}</span>
256
+ </div>
257
+ {tip > 0 && (
258
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--success)' }}>
259
+ <span>Propina:</span>
260
+ <span>+${Number(tip).toFixed(2)}</span>
261
+ </div>
262
+ )}
263
+ </div>
264
+
265
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.4rem', fontWeight: '900', marginBottom: '1.5rem' }}>
266
+ <span>Total:</span>
267
+ <span className="text-gradient-primary">${totalCart.toFixed(2)}</span>
268
+ </div>
269
+
270
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
271
+ <button
272
+ className="btn-glass"
273
+ onClick={sendOrder}
274
+ disabled={cart.length === 0 || (orderType === 'Mesa' && !activeTable)}
275
+ style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
276
+ >
277
+ <Send size={18} /> Comanda
278
+ </button>
279
+ <button
280
+ className="btn-primary"
281
+ onClick={() => setIsCheckoutOpen(true)}
282
+ disabled={cart.length === 0}
283
+ style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
284
+ >
285
+ <Receipt size={18} /> Pagar
286
+ </button>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ {/* Checkout Modal */}
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' }}>
300
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
301
+ <h2 className="text-gradient" style={{ fontSize: '1.75rem' }}>Finalizar Venta</h2>
302
+ <button onClick={() => setIsCheckoutOpen(false)} style={{ background: 'none', border: 'none', color: 'var(--text-muted)' }}>
303
+ <XCircle size={28} />
304
+ </button>
305
+ </div>
306
+
307
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
308
+ <div>
309
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Descuento (%)</label>
310
+ <select
311
+ value={discount}
312
+ onChange={(e) => setDiscount(Number(e.target.value))}
313
+ 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' }}
314
+ >
315
+ <option value="0">Sin Descuento</option>
316
+ <option value="5">5% Promo</option>
317
+ <option value="10">10% Cortesía</option>
318
+ <option value="15">15% Empleado</option>
319
+ <option value="50">50% VIP</option>
320
+ </select>
321
+ </div>
322
+
323
+ <div>
324
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Propina (Efectivo/Sugerida)</label>
325
+ <input
326
+ type="number"
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
+
334
+ <div>
335
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Método de Pago</label>
336
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.75rem' }}>
337
+ {[
338
+ { id: 'Efectivo', icon: <Banknote size={20} /> },
339
+ { id: 'Tarjeta', icon: <CreditCard size={20} /> },
340
+ { id: 'QR', icon: <QrCode size={20} /> }
341
+ ].map(m => (
342
+ <button
343
+ key={m.id}
344
+ onClick={() => setPaymentMethod(m.id)}
345
+ className={paymentMethod === m.id ? 'btn-primary' : 'btn-glass'}
346
+ style={{ padding: '1rem 0.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', fontSize: '0.8rem' }}
347
+ >
348
+ {m.icon} {m.id}
349
+ </button>
350
+ ))}
351
+ </div>
352
+ </div>
353
+
354
+ <div style={{ background: 'rgba(255,255,255,0.02)', padding: '1.5rem', borderRadius: 'var(--radius-lg)', marginTop: '1rem', border: '1px dashed var(--border-subtle)' }}>
355
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.5rem', fontWeight: '900' }}>
356
+ <span>Total a Pagar:</span>
357
+ <span className="text-gradient">${totalCart.toFixed(2)}</span>
358
+ </div>
359
+ </div>
360
+
361
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
362
+ <button className="btn-glass" onClick={printTicket} style={{ padding: '1rem' }}>
363
+ <Receipt size={20} /> Preview Ticket
364
+ </button>
365
+ <button className="btn-primary" onClick={sendOrder} style={{ padding: '1rem' }}>
366
+ <CheckCircle size={20} /> Confirmar Pago
367
+ </button>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ )}
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);
380
+ cursor: pointer;
381
+ font-weight: 600;
382
+ transition: all 0.2s;
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
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 {
393
+ background: var(--primary);
394
+ color: white;
395
+ border: none;
396
+ border-radius: var(--radius-md);
397
+ cursor: pointer;
398
+ font-weight: 600;
399
+ transition: all 0.2s;
400
+ display: flex;
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;
411
+ }
412
+ @keyframes scaleIn {
413
+ from { transform: scale(0.9); opacity: 0; }
414
+ to { transform: scale(1); opacity: 1; }
415
+ }
416
+ `}</style>
417
+ </div>
418
+ );
419
+ }
vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })