dimensionalpulsar commited on
Commit
188bf54
·
verified ·
1 Parent(s): 18a5001

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +10 -0
  2. .gitattributes +1 -0
  3. Dockerfile +32 -0
  4. README.md +18 -5
  5. frontend/.gitignore +24 -0
  6. frontend/README.md +16 -0
  7. frontend/build.log +0 -0
  8. frontend/eslint.config.js +29 -0
  9. frontend/index.html +13 -0
  10. frontend/package-lock.json +0 -0
  11. frontend/package.json +32 -0
  12. frontend/public/favicon.svg +1 -0
  13. frontend/public/icons.svg +24 -0
  14. frontend/public/icons/inventory.png +0 -0
  15. frontend/public/images/burger.png +0 -0
  16. frontend/public/images/cafe.png +0 -0
  17. frontend/public/images/cerveza.png +0 -0
  18. frontend/public/images/limonada.png +0 -0
  19. frontend/public/images/pasta.png +0 -0
  20. frontend/public/images/paste.png +0 -0
  21. frontend/public/images/pizza.png +0 -0
  22. frontend/public/images/salad.png +0 -0
  23. frontend/public/images/tacos.jpg +3 -0
  24. frontend/public/images/tacos.png +0 -0
  25. frontend/src/App.css +184 -0
  26. frontend/src/App.jsx +66 -0
  27. frontend/src/assets/hero.png +0 -0
  28. frontend/src/assets/react.svg +1 -0
  29. frontend/src/assets/vite.svg +1 -0
  30. frontend/src/components/ProtectedRoute.jsx +19 -0
  31. frontend/src/components/admin/CRMManager.jsx +129 -0
  32. frontend/src/components/admin/DashboardOverview.jsx +71 -0
  33. frontend/src/components/admin/FinanceManager.jsx +192 -0
  34. frontend/src/components/admin/InventoryControl.jsx +192 -0
  35. frontend/src/components/admin/MenuEditor.jsx +231 -0
  36. frontend/src/components/admin/Reports.jsx +162 -0
  37. frontend/src/components/admin/SettingsPlus.jsx +115 -0
  38. frontend/src/components/admin/TableManager.jsx +139 -0
  39. frontend/src/components/admin/UserManager.jsx +109 -0
  40. frontend/src/context/AuthContext.jsx +64 -0
  41. frontend/src/firebase/config.js +22 -0
  42. frontend/src/index.css +190 -0
  43. frontend/src/main.jsx +10 -0
  44. frontend/src/pages/AdminDashboard.jsx +94 -0
  45. frontend/src/pages/CustomerMenu.jsx +210 -0
  46. frontend/src/pages/KitchenView.jsx +126 -0
  47. frontend/src/pages/Login.jsx +181 -0
  48. frontend/src/pages/WaiterPOS.jsx +453 -0
  49. frontend/vite.config.js +7 -0
  50. package.json +32 -0
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ 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.
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip 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
 
 
33
  *.zip 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
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine AS build
2
+
3
+ # Set the working directory
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
17
+
18
+ # Second stage: Serve the application with a lightweight HTTP server
19
+ FROM node:22-alpine
20
+ WORKDIR /app
21
+
22
+ # Install 'serve' tool to host static files
23
+ RUN npm install -g serve
24
+
25
+ # Copy only the built files from the build stage
26
+ COPY --from=build /app/dist ./dist
27
+
28
+ # Hugging Face Spaces expect port 7860
29
+ EXPOSE 7860
30
+
31
+ # Command to serve the static site on port 7860
32
+ CMD ["serve", "-s", "dist", "-l", "7860"]
README.md CHANGED
@@ -1,10 +1,23 @@
1
  ---
2
- title: Restaurante
3
- emoji: 🌍
4
- colorFrom: yellow
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Resto OS Premium
3
+ emoji: 🍴
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  ---
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
frontend/.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?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/build.log ADDED
Binary file (17.7 kB). View file
 
frontend/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
+ ])
frontend/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>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "chart.js": "^4.5.1",
14
+ "firebase": "^12.11.0",
15
+ "lucide-react": "^0.577.0",
16
+ "react": "^19.2.4",
17
+ "react-chartjs-2": "^5.3.1",
18
+ "react-dom": "^19.2.4",
19
+ "react-router-dom": "^7.13.1"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.39.4",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^6.0.1",
26
+ "eslint": "^9.39.4",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.4.0",
30
+ "vite": "^6.0.0"
31
+ }
32
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/public/icons/inventory.png ADDED
frontend/public/images/burger.png ADDED
frontend/public/images/cafe.png ADDED
frontend/public/images/cerveza.png ADDED
frontend/public/images/limonada.png ADDED
frontend/public/images/pasta.png ADDED
frontend/public/images/paste.png ADDED
frontend/public/images/pizza.png ADDED
frontend/public/images/salad.png ADDED
frontend/public/images/tacos.jpg ADDED

Git LFS Details

  • SHA256: 67c1f17e43c4233a24da28a0638ae4c81c86d1cc383e487cb235fb12dd6c974f
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
frontend/public/images/tacos.png ADDED
frontend/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
+ }
frontend/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;
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/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
+ }
frontend/src/components/admin/CRMManager.jsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue } from 'firebase/database';
4
+ import { Users, Truck, Heart, Star, Phone, MapPin, History, Search } from 'lucide-react';
5
+
6
+ export default function CRMManager() {
7
+ const [customers, setCustomers] = useState([]);
8
+ const [deliveryOrders, setDeliveryOrders] = useState([]);
9
+ const [searchTerm, setSearchTerm] = useState('');
10
+
11
+ useEffect(() => {
12
+ // Analytics for CRM (from Orders)
13
+ onValue(ref(db, 'orders'), (snapshot) => {
14
+ const data = snapshot.val();
15
+ if (data) {
16
+ const orders = Object.values(data);
17
+ const customerMap = {};
18
+ const deliveries = [];
19
+
20
+ orders.forEach(order => {
21
+ if (order.customerEmail) {
22
+ const email = order.customerEmail;
23
+ if (!customerMap[email]) {
24
+ customerMap[email] = {
25
+ email,
26
+ name: order.customerName || 'Cliente',
27
+ totalSpent: 0,
28
+ orderCount: 0,
29
+ lastOrder: 0
30
+ };
31
+ }
32
+ customerMap[email].totalSpent += order.total;
33
+ customerMap[email].orderCount += 1;
34
+ if (order.timestamp > customerMap[email].lastOrder) {
35
+ customerMap[email].lastOrder = order.timestamp;
36
+ }
37
+ }
38
+
39
+ if (order.type === 'delivery') {
40
+ deliveries.push(order);
41
+ }
42
+ });
43
+
44
+ setCustomers(Object.values(customerMap).sort((a,b) => b.totalSpent - a.totalSpent));
45
+ setDeliveryOrders(deliveries.sort((a,b) => b.timestamp - a.timestamp));
46
+ }
47
+ });
48
+ }, []);
49
+
50
+ const filteredCustomers = customers.filter(c =>
51
+ c.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
52
+ c.name.toLowerCase().includes(searchTerm.toLowerCase())
53
+ );
54
+
55
+ return (
56
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
57
+ <header style={{ marginBottom: '2.5rem' }}>
58
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
59
+ <Users size={28} /> CRM & Fidelización
60
+ </h2>
61
+ <p style={{ color: 'var(--text-muted)' }}>Gestión de clientes frecuentes y seguimiento de delivery</p>
62
+ </header>
63
+
64
+ <div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '2rem' }}>
65
+
66
+ {/* Customer List */}
67
+ <section className="glass-card">
68
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
69
+ <h3 style={sectionTitleStyle}><Heart size={18} /> Clientes Frecuentes</h3>
70
+ <div style={{ position: 'relative', width: '200px' }}>
71
+ <Search size={14} style={{ position: 'absolute', left: '10px', top: '12px', color: 'var(--text-muted)' }} />
72
+ <input
73
+ type="text" placeholder="Buscar..."
74
+ value={searchTerm} onChange={e => setSearchTerm(e.target.value)}
75
+ style={{ ...inputStyle, paddingLeft: '30px', padding: '0.4rem 0.5rem 0.4rem 2rem', fontSize: '0.8rem' }}
76
+ />
77
+ </div>
78
+ </div>
79
+
80
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxHeight: '500px', overflowY: 'auto' }}>
81
+ {filteredCustomers.map((c, i) => (
82
+ <div key={c.email} className="glass-panel" style={{ padding: '1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
83
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
84
+ <div style={{ width: '40px', height: '40px', background: 'rgba(0,0,0,0.03)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: '800', color: i < 3 ? 'var(--warning)' : 'var(--text-muted)' }}>
85
+ {i < 3 ? <Star size={18} fill="var(--warning)" /> : i + 1}
86
+ </div>
87
+ <div>
88
+ <div style={{ fontWeight: '700' }}>{c.name}</div>
89
+ <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{c.email}</div>
90
+ </div>
91
+ </div>
92
+ <div style={{ textAlign: 'right' }}>
93
+ <div style={{ color: 'var(--success)', fontWeight: '800' }}>${c.totalSpent.toFixed(2)}</div>
94
+ <div style={{ fontSize: '0.65rem', color: 'var(--text-muted)' }}>{c.orderCount} visitas | {Math.floor(c.totalSpent / 10)} pts</div>
95
+ </div>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ </section>
100
+
101
+ {/* Delivery Activity */}
102
+ <section className="glass-card">
103
+ <h3 style={sectionTitleStyle}><Truck size={18} /> Monitor de Delivery</h3>
104
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
105
+ {deliveryOrders.slice(0, 10).map(order => (
106
+ <div key={order.id} style={{ padding: '1rem', borderLeft: '3px solid var(--primary)', background: 'rgba(0,0,0,0.02)', borderRadius: '0 8px 8px 0' }}>
107
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
108
+ <span style={{ fontWeight: '700', fontSize: '0.9rem' }}>#{order.id.slice(-5)} - {order.customerName}</span>
109
+ <span style={{ fontSize: '0.75rem', padding: '2px 8px', borderRadius: '4px', background: 'rgba(255,90,95,0.1)', color: 'var(--primary)' }}>{order.status.toUpperCase()}</span>
110
+ </div>
111
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem', color: 'var(--text-muted)' }}>
112
+ <MapPin size={12} /> {order.address || 'Local / Para llevar'}
113
+ </div>
114
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
115
+ <History size={12} /> {new Date(order.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
116
+ </div>
117
+ </div>
118
+ ))}
119
+ {deliveryOrders.length === 0 && <p style={{ textAlign: 'center', color: 'var(--text-muted)', paddingTop: '2rem' }}>No hay pedidos delivery recientes</p>}
120
+ </div>
121
+ </section>
122
+
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', outline: 'none' };
129
+ const sectionTitleStyle = { fontSize: '1.2rem', fontWeight: '700', display: 'flex', alignItems: 'center', gap: '0.75rem' };
frontend/src/components/admin/DashboardOverview.jsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 'Ticket Promedio', value: '$275.50', icon: <ShoppingBag size={24} color="var(--primary)" />, bg: 'rgba(255, 90, 95, 0.1)' },
22
+ { title: 'Utilidad Est.', value: '$4,200', 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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
44
+ <div className="glass-card">
45
+ <h3 className="text-gradient" style={{ marginBottom: '1rem' }}>Saturación por Horario (Peak Hours)</h3>
46
+ <div style={{ height: '10px', width: '100%', background: 'rgba(0,0,0,0.05)', borderRadius: '10px', overflow: 'hidden', display: 'flex' }}>
47
+ <div style={{ width: '20%', background: 'var(--primary)', opacity: 0.3 }}></div>
48
+ <div style={{ width: '40%', background: 'var(--primary)' }}></div>
49
+ <div style={{ width: '15%', background: 'var(--primary)', opacity: 0.5 }}></div>
50
+ <div style={{ width: '25%', background: 'rgba(0,0,0,0.05)' }}></div>
51
+ </div>
52
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginTop: '1rem' }}>Punto máximo detectado: 14:00 - 15:30 (Almuerzo)</p>
53
+ </div>
54
+
55
+ <div className="glass-card">
56
+ <h3 className="text-gradient" style={{ marginBottom: '1rem' }}>Alertas de Suministros (ERP)</h3>
57
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
58
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem' }}>
59
+ <span>Tomate Cherry (Kg)</span>
60
+ <span style={{ color: 'var(--danger)', fontWeight: '700' }}>1.2 (Bajo)</span>
61
+ </div>
62
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem' }}>
63
+ <span>Aceite de Oliva (L)</span>
64
+ <span style={{ color: 'var(--warning)', fontWeight: '700' }}>3.0 (Crítico)</span>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
frontend/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.6, marginTop: '0.2rem', color: 'var(--text-muted)' }}>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 var(--border-subtle)' }}>
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(0,0,0,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(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', outline: 'none' };
191
+ const statBoxStyle = { padding: '1rem', borderRadius: '12px', background: 'rgba(0,0,0,0.02)', border: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column', gap: '0.25rem' };
192
+ const Plus = ({ size }) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>;
frontend/src/components/admin/InventoryControl.jsx 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.4)', backdropFilter: 'blur(8px)', 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: 'var(--text-muted)', 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(0,0,0,0.03)', border: '1px solid var(--border-subtle)',
181
+ color: 'var(--text-main)', 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(0,0,0,0.02)', color: 'var(--text-main)', cursor: 'pointer',
191
+ display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s', fontSize: '0.75rem', fontWeight: '700'
192
+ };
frontend/src/components/admin/MenuEditor.jsx 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', background: 'rgba(0,0,0,0.03)'}}
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(0,0,0,0.03)', 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: 'var(--text-muted)' }}><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(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', 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(255,255,255,0.9)', color: 'var(--text-main)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' };
frontend/src/components/admin/Reports.jsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Clock, Award, BarChart3 } 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 [hourlyData, setHourlyData] = useState(new Array(24).fill(0));
18
+ const [waiterPerformance, setWaiterPerformance] = useState({ labels: [], data: [] });
19
+ const [topProducts, setTopProducts] = useState({ labels: [], data: [] });
20
+ const [stats, setStats] = useState({ totalSales: 0, orderCount: 0, avgTicket: 0 });
21
+
22
+ useEffect(() => {
23
+ onValue(ref(db, 'orders'), (snapshot) => {
24
+ const data = snapshot.val();
25
+ if (data) {
26
+ const orders = Object.values(data).filter(o => o.status === 'completed');
27
+ const weeklySales = [0, 0, 0, 0, 0, 0, 0];
28
+ const hourSales = new Array(24).fill(0);
29
+ const productCounts = {};
30
+ const waiterSales = {};
31
+ let total = 0;
32
+
33
+ orders.forEach(order => {
34
+ total += order.total;
35
+
36
+ const date = new Date(order.timestamp);
37
+ // Weekly
38
+ const day = date.getDay();
39
+ const dayIdx = (day + 6) % 7;
40
+ weeklySales[dayIdx] += order.total;
41
+
42
+ // Hourly
43
+ const hour = date.getHours();
44
+ hourSales[hour] += 1; // Count orders per hour for peak density
45
+
46
+ // Waiters
47
+ const waiter = order.waiter || 'Sistema/Admin';
48
+ waiterSales[waiter] = (waiterSales[waiter] || 0) + order.total;
49
+
50
+ // Products
51
+ order.items.forEach(item => {
52
+ productCounts[item.name] = (productCounts[item.name] || 0) + item.qty;
53
+ });
54
+ });
55
+
56
+ setSalesData(weeklySales);
57
+ setHourlyData(hourSales);
58
+ setStats({
59
+ totalSales: total,
60
+ orderCount: orders.length,
61
+ avgTicket: total / (orders.length || 1)
62
+ });
63
+
64
+ const sortedProducts = Object.entries(productCounts)
65
+ .sort((a,b) => b[1] - a[1])
66
+ .slice(0, 5);
67
+ setTopProducts({
68
+ labels: sortedProducts.map(p => p[0]),
69
+ data: sortedProducts.map(p => p[1])
70
+ });
71
+
72
+ const sortedWaiters = Object.entries(waiterSales)
73
+ .sort((a,b) => b[1] - a[1]);
74
+ setWaiterPerformance({
75
+ labels: sortedWaiters.map(w => w[0].split('@')[0]),
76
+ data: sortedWaiters.map(w => w[1])
77
+ });
78
+ }
79
+ });
80
+ }, []);
81
+
82
+ const chartOptions = {
83
+ responsive: true,
84
+ maintainAspectRatio: false,
85
+ plugins: { legend: { labels: { color: '#9595a8', font: { size: 10 } } } },
86
+ scales: {
87
+ x: { ticks: { color: '#9595a8', font: { size: 10 } }, grid: { display: false } },
88
+ y: { ticks: { color: '#9595a8', font: { size: 10 } }, grid: { color: 'rgba(0,0,0,0.05)' } }
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div className="animate-fade-in" style={{ paddingBottom: '3rem' }}>
94
+ <header style={{ marginBottom: '2.5rem' }}>
95
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800' }}>RESTO Analytics PLUS</h2>
96
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem', marginTop: '1.5rem' }}>
97
+ <StatCard title="Ventas Totales" value={`$${stats.totalSales.toFixed(2)}`} icon={<TrendingUp size={18}/>} color="var(--primary)" />
98
+ <StatCard title="Pedidos" value={stats.orderCount} icon={<Package size={18}/>} color="var(--success)" />
99
+ <StatCard title="Ticket Promedio" value={`$${stats.avgTicket.toFixed(2)}`} icon={<BarChart3 size={18}/>} color="#4dabf7" />
100
+ </div>
101
+ </header>
102
+
103
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(450px, 1fr))', gap: '2rem' }}>
104
+ {/* Ventas Semanales */}
105
+ <ChartCard title="Flujo de Ventas Semanal" icon={<Calendar size={18}/>}>
106
+ <Line data={{
107
+ labels: ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'],
108
+ datasets: [{ label: 'Ventas ($)', data: salesData, borderColor: 'var(--primary)', backgroundColor: 'rgba(255,90,95,0.1)', fill: true, tension: 0.4 }]
109
+ }} options={chartOptions} />
110
+ </ChartCard>
111
+
112
+ {/* Rendimiento Meseros */}
113
+ <ChartCard title="Rendimiento por Mesero" icon={<Award size={18}/>}>
114
+ <Bar data={{
115
+ labels: waiterPerformance.labels,
116
+ datasets: [{ label: 'Ventas Totales ($)', data: waiterPerformance.data, backgroundColor: 'rgba(76,217,100,0.6)', borderRadius: 6 }]
117
+ }} options={chartOptions} />
118
+ </ChartCard>
119
+
120
+ {/* Horas Pico */}
121
+ <ChartCard title="Horas Pico (Densidad de Pedidos)" icon={<Clock size={18}/>}>
122
+ <Line data={{
123
+ labels: Array.from({length: 24}, (_, i) => `${i}:00`),
124
+ datasets: [{ label: 'Pedidos', data: hourlyData, borderColor: '#4dabf7', backgroundColor: 'rgba(77,171,247,0.1)', fill: true, tension: 0.4 }]
125
+ }} options={chartOptions} />
126
+ </ChartCard>
127
+
128
+ {/* Top Productos */}
129
+ <ChartCard title="Productos Estrella" icon={<Package size={18}/>}>
130
+ <Bar data={{
131
+ labels: topProducts.labels,
132
+ datasets: [{ label: 'Unidades', data: topProducts.data, backgroundColor: 'rgba(0,0,0,0.03)', borderColor: 'var(--border-subtle)', borderWidth: 1, borderRadius: 6 }]
133
+ }} options={chartOptions} />
134
+ </ChartCard>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function StatCard({ title, value, icon, color }) {
141
+ return (
142
+ <div className="glass-card" style={{ padding: '1.5rem' }}>
143
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.85rem', marginBottom: '0.75rem' }}>
144
+ {title} <span style={{ color }}>{icon}</span>
145
+ </div>
146
+ <div style={{ fontSize: '1.75rem', fontWeight: '900' }}>{value}</div>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ function ChartCard({ title, icon, children }) {
152
+ return (
153
+ <div className="glass-card" style={{ height: '380px', display: 'flex', flexDirection: 'column', padding: '1.5rem' }}>
154
+ <h3 style={{ marginBottom: '1.5rem', fontSize: '1.1rem', fontWeight: '700', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
155
+ {icon} {title}
156
+ </h3>
157
+ <div style={{ flex: 1, position: 'relative' }}>
158
+ {children}
159
+ </div>
160
+ </div>
161
+ );
162
+ }
frontend/src/components/admin/SettingsPlus.jsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { db } from '../../firebase/config';
3
+ import { ref, onValue, set, update } from 'firebase/database';
4
+ import { Settings, Percent, DollarSign, Store, Globe, Bell, Save } from 'lucide-react';
5
+
6
+ export default function SettingsPlus() {
7
+ const [settings, setSettings] = useState({
8
+ restaurantName: 'Resto OS Premium',
9
+ currency: 'USD',
10
+ taxRate: 16,
11
+ serviceCharge: 10,
12
+ language: 'es',
13
+ timezone: 'America/Mexico_City'
14
+ });
15
+ const [saving, setSaving] = useState(false);
16
+
17
+ useEffect(() => {
18
+ onValue(ref(db, 'config/settings'), (snapshot) => {
19
+ if (snapshot.exists()) setSettings(snapshot.val());
20
+ });
21
+ }, []);
22
+
23
+ const handleSave = async () => {
24
+ setSaving(true);
25
+ await set(ref(db, 'config/settings'), settings);
26
+ setTimeout(() => setSaving(false), 800);
27
+ };
28
+
29
+ return (
30
+ <div className="animate-fade-in" style={{ padding: '0 1rem' }}>
31
+ <header style={{ marginBottom: '2.5rem' }}>
32
+ <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
33
+ <Settings size={28} /> Configuración Global
34
+ </h2>
35
+ <p style={{ color: 'var(--text-muted)' }}>Ajustes generales del sistema ERP</p>
36
+ </header>
37
+
38
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem' }}>
39
+
40
+ <div className="glass-card">
41
+ <h3 style={sectionTitleStyle}><Store size={18} /> Información Local</h3>
42
+ <div style={formGrid}>
43
+ <div style={inputGroup}>
44
+ <label style={labelStyle}>Nombre del Establecimiento</label>
45
+ <input type="text" value={settings.restaurantName} onChange={e => setSettings({...settings, restaurantName: e.target.value})} style={inputStyle} />
46
+ </div>
47
+ <div style={inputGroup}>
48
+ <label style={labelStyle}>Moneda Principal</label>
49
+ <select value={settings.currency} onChange={e => setSettings({...settings, currency: e.target.value})} style={inputStyle}>
50
+ <option value="USD">Dólar ($ USD)</option>
51
+ <option value="MXN">Peso ($ MXN)</option>
52
+ <option value="EUR">Euro (€ EUR)</option>
53
+ <option value="COP">Peso ($ COP)</option>
54
+ </select>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <div className="glass-card">
60
+ <h3 style={sectionTitleStyle}><Percent size={18} /> Impuestos y Cargos</h3>
61
+ <div style={formGrid}>
62
+ <div style={inputGroup}>
63
+ <label style={labelStyle}>Impuesto (IVA/VAT) %</label>
64
+ <input type="number" value={settings.taxRate} onChange={e => setSettings({...settings, taxRate: parseFloat(e.target.value)})} style={inputStyle} />
65
+ </div>
66
+ <div style={inputGroup}>
67
+ <label style={labelStyle}>Servicio Sugerido %</label>
68
+ <input type="number" value={settings.serviceCharge} onChange={e => setSettings({...settings, serviceCharge: parseFloat(e.target.value)})} style={inputStyle} />
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div className="glass-card">
74
+ <h3 style={sectionTitleStyle}><Globe size={18} /> Idioma y Región</h3>
75
+ <div style={formGrid}>
76
+ <div style={inputGroup}>
77
+ <label style={labelStyle}>Idioma Interfaz</label>
78
+ <select value={settings.language} onChange={e => setSettings({...settings, language: e.target.value})} style={inputStyle}>
79
+ <option value="es">Español</option>
80
+ <option value="en">English</option>
81
+ </select>
82
+ </div>
83
+ <div style={inputGroup}>
84
+ <label style={labelStyle}>Zona Horaria</label>
85
+ <select value={settings.timezone} onChange={e => setSettings({...settings, timezone: e.target.value})} style={inputStyle}>
86
+ <option value="America/Mexico_City">Mexico City (CST)</option>
87
+ <option value="America/Bogota">Bogotá (EST)</option>
88
+ <option value="Europe/Madrid">Madrid (CET)</option>
89
+ </select>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="glass-card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', border: '1px dashed var(--primary)' }}>
95
+ <button
96
+ onClick={handleSave}
97
+ className="btn-primary"
98
+ style={{ width: '200px', height: '55px', fontSize: '1.1rem' }}
99
+ disabled={saving}
100
+ >
101
+ {saving ? 'Guardando...' : <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Save size={20}/> Guardar Cambios</span>}
102
+ </button>
103
+ <p style={{ marginTop: '1rem', fontSize: '0.8rem', color: 'var(--text-muted)' }}>Los cambios se aplicarán a todos los terminales.</p>
104
+ </div>
105
+
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', outline: 'none' };
112
+ const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
113
+ const sectionTitleStyle = { marginBottom: '1.5rem', fontSize: '1.1rem', fontWeight: '700', display: 'flex', alignItems: 'center', gap: '0.75rem' };
114
+ const formGrid = { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' };
115
+ const inputGroup = { display: 'flex', flexDirection: 'column' };
frontend/src/components/admin/TableManager.jsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(0,0,0,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: 'var(--bg-card)', backgroundSize: '30px 30px', border: '1px solid var(--border-subtle)', backgroundImage: 'radial-gradient(circle, rgba(0,0,0,0.05) 1px, transparent 1px)' }}>
89
+ <div style={{ position: 'absolute', top: '1.5rem', left: '1.5rem', display: 'flex', gap: '1.5rem', fontSize: '0.75rem' }}>
90
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: 12, height: 12, background: 'var(--success)', borderRadius: '2px' }} /> Libre</div>
91
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: 12, height: 12, background: 'var(--primary)', borderRadius: '2px' }} /> Ocupado</div>
92
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: 12, height: 12, background: '#fcc419', borderRadius: '2px' }} /> Reservado</div>
93
+ </div>
94
+
95
+ <div style={{ width: '100%', height: '100%', padding: '5rem 3rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))', gap: '3rem' }}>
96
+ {tables.map(t => {
97
+ const statusColor = t.status === 'occupied' ? 'var(--primary)' : t.status === 'reserved' ? '#fcc419' : 'var(--success)';
98
+ return (
99
+ <div
100
+ key={t.id}
101
+ onClick={async () => {
102
+ const nextStatus = t.status === 'available' ? 'occupied' : t.status === 'occupied' ? 'reserved' : 'available';
103
+ await update(ref(db, `config/tables/${t.id}`), { status: nextStatus });
104
+ }}
105
+ className="animate-scale-in"
106
+ style={{
107
+ width: '100px', height: '100px',
108
+ background: t.status === 'available' ? 'rgba(76,217,100,0.05)' : 'rgba(0,0,0,0.02)',
109
+ border: `2px solid ${statusColor}`,
110
+ borderRadius: t.capacity > 4 ? '16px' : '50%',
111
+ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
112
+ boxShadow: t.status === 'occupied' ? '0 0 20px rgba(255,90,95,0.05)' : 'none',
113
+ cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
114
+ position: 'relative'
115
+ }}
116
+ >
117
+ <span style={{ fontSize: '1.4rem', fontWeight: '900', color: statusColor }}>{t.number}</span>
118
+ <span style={{ fontSize: '0.65rem', opacity: 0.6, color: 'var(--text-muted)' }}>{t.capacity} p</span>
119
+ {t.status !== 'available' && (
120
+ <div style={{ position: 'absolute', top: '-5px', right: '-5px', width: 12, height: 12, borderRadius: '50%', background: statusColor, border: '2px solid var(--bg-card)' }} />
121
+ )}
122
+ </div>
123
+ );
124
+ })}
125
+ {tables.length === 0 && (
126
+ <div style={{ gridColumn: '1 / -1', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.1 }}>
127
+ <Grid size={80} />
128
+ </div>
129
+ )}
130
+ </div>
131
+ </section>
132
+
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', outline: 'none' };
139
+ const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
frontend/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(0,0,0,0.03)', 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.03)', 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(0,0,0,0.03)', 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(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)', outline: 'none' };
109
+ const labelStyle = { display: 'block', fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem' };
frontend/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
+ };
frontend/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;
frontend/src/index.css ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap');
2
+
3
+ :root {
4
+ /* Premium Light Theme Core Palette */
5
+ --bg-base: #f8f9fa;
6
+ --bg-surface: #ffffff;
7
+ --bg-glass: rgba(255, 255, 255, 0.85);
8
+ --bg-glass-hover: rgba(245, 245, 250, 0.9);
9
+ --bg-card: #ffffff;
10
+
11
+ /* Brand Accents */
12
+ --primary: #FF5A5F;
13
+ --primary-hover: #ff4046;
14
+ --primary-glow: rgba(255, 90, 95, 0.15);
15
+ --secondary: #00A699;
16
+ --secondary-hover: #008f84;
17
+
18
+ /* Text */
19
+ --text-main: #1a1a20;
20
+ --text-muted: #6b6b7b;
21
+ --text-disabled: #a5a5b8;
22
+
23
+ /* Status Colors */
24
+ --success: #20C997;
25
+ --warning: #F5A623;
26
+ --danger: #FF3B30;
27
+ --info: #32ADE6;
28
+
29
+ /* Borders & Shadows */
30
+ --border-subtle: rgba(0, 0, 0, 0.08);
31
+ --border-focus: rgba(255, 90, 95, 0.3);
32
+ --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.05);
33
+ --shadow-md: 0 12px 30px rgba(0, 0, 0, 0.08);
34
+ --shadow-glow: 0 0 20px var(--primary-glow);
35
+ --glass-blur: blur(20px);
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.02) 0%, transparent 30%),
65
+ radial-gradient(circle at 90% 80%, rgba(0, 166, 153, 0.02) 0%, transparent 30%);
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
+ box-shadow: var(--shadow-sm);
102
+ }
103
+
104
+ .glass-card:hover {
105
+ transform: translateY(-4px);
106
+ background: var(--bg-glass-hover);
107
+ border-color: rgba(0, 0, 0, 0.1);
108
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.06);
109
+ }
110
+
111
+ /* Premium Buttons */
112
+ .btn-primary {
113
+ background: var(--primary);
114
+ color: #fff;
115
+ padding: 0.75rem 1.75rem;
116
+ border-radius: var(--radius-pill);
117
+ font-weight: 600;
118
+ letter-spacing: 0.5px;
119
+ transition: all var(--transition-fast);
120
+ box-shadow: 0 4px 14px var(--primary-glow);
121
+ display: inline-flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ gap: 0.5rem;
125
+ }
126
+
127
+ .btn-primary:hover {
128
+ background: var(--primary-hover);
129
+ transform: scale(1.02);
130
+ box-shadow: 0 6px 20px var(--primary-glow);
131
+ }
132
+
133
+ .btn-primary:active {
134
+ transform: scale(0.98);
135
+ }
136
+
137
+ .btn-glass {
138
+ background: rgba(0,0,0,0.03);
139
+ border: 1px solid rgba(0,0,0,0.08);
140
+ color: var(--text-main);
141
+ padding: 0.75rem 1.75rem;
142
+ border-radius: var(--radius-pill);
143
+ font-weight: 500;
144
+ transition: all var(--transition-fast);
145
+ backdrop-filter: blur(8px);
146
+ }
147
+
148
+ .btn-glass:hover {
149
+ background: rgba(0,0,0,0.06);
150
+ border-color: rgba(0,0,0,0.12);
151
+ }
152
+
153
+ /* Typography styles */
154
+ .text-gradient {
155
+ background: linear-gradient(135deg, #1a1a20 0%, #6b6b7b 100%);
156
+ -webkit-background-clip: text;
157
+ -webkit-text-fill-color: transparent;
158
+ background-clip: text;
159
+ }
160
+
161
+ .text-gradient-primary {
162
+ background: linear-gradient(135deg, var(--primary) 0%, #ff8a8e 100%);
163
+ -webkit-background-clip: text;
164
+ -webkit-text-fill-color: transparent;
165
+ background-clip: text;
166
+ }
167
+
168
+ /* Layout Utilities */
169
+ .app-container {
170
+ display: flex;
171
+ min-height: 100vh;
172
+ width: 100%;
173
+ }
174
+
175
+ .main-content {
176
+ flex: 1;
177
+ padding: 2rem;
178
+ display: flex;
179
+ flex-direction: column;
180
+ }
181
+
182
+ /* Animations */
183
+ @keyframes fadeIn {
184
+ from { opacity: 0; transform: translateY(10px); }
185
+ to { opacity: 1; transform: translateY(0); }
186
+ }
187
+
188
+ .animate-fade-in {
189
+ animation: fadeIn 0.4s ease-out forwards;
190
+ }
frontend/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
+ )
frontend/src/pages/AdminDashboard.jsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Settings, Save, ShieldCheck, Truck } 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
+ import CRMManager from '../components/admin/CRMManager';
13
+ import SettingsPlus from '../components/admin/SettingsPlus';
14
+
15
+ export default function AdminDashboard() {
16
+ const { logout, currentUser } = useAuth();
17
+ const navigate = useNavigate();
18
+ const [activeTab, setActiveTab] = useState('overview');
19
+
20
+ const handleLogout = async () => {
21
+ await logout();
22
+ navigate('/login');
23
+ };
24
+
25
+ const navItems = [
26
+ { id: 'overview', label: 'Resumen', icon: <LayoutDashboard size={20} /> },
27
+ { id: 'menu', label: 'Menú & Precios', icon: <Utensils size={20} /> },
28
+ { id: 'inventory', label: 'Control de Stock', icon: <Box size={20} /> },
29
+ { id: 'reports', label: 'Reportes y Analítica', icon: <PieChart size={20} /> },
30
+ { id: 'cash', label: 'Finanzas & Caja', icon: <DollarSign size={20} /> },
31
+ { id: 'users', label: 'Gestión de Personal', icon: <Users size={20} /> },
32
+ { id: 'tables', label: 'Arquitectura Salón', icon: <LayoutDashboard size={20} /> },
33
+ { id: 'crm', label: 'CRM & Operaciones', icon: <Truck size={20} /> },
34
+ { id: 'settings', label: 'Configuración ERP', icon: <Settings size={20} /> },
35
+ { id: 'kitchen', label: 'Pantalla Cocina', icon: <UtensilsCrossed size={20} />, action: () => window.open('/kitchen', '_blank') },
36
+ { id: 'menu_public', label: 'Carta Digital', icon: <Utensils size={20} />, action: () => window.open('/menu', '_blank') }
37
+ ];
38
+
39
+ return (
40
+ <div className="app-container">
41
+ {/* Sidebar */}
42
+ <aside className="glass-panel" style={{ width: '280px', borderRadius: '0', borderRight: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
43
+ <div style={{ padding: '2rem 1.5rem', borderBottom: '1px solid var(--border-subtle)' }}>
44
+ <h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>RESTO PLUS</h1>
45
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.85rem', marginTop: '0.25rem' }}>Sistema ERP Inteligente</p>
46
+ </div>
47
+
48
+ <nav style={{ flex: 1, padding: '1.5rem 1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
49
+ {navItems.map(item => (
50
+ <button
51
+ key={item.id}
52
+ onClick={() => {
53
+ if (item.action) item.action();
54
+ else setActiveTab(item.id);
55
+ }}
56
+ style={{
57
+ display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.8rem 1rem', width: '100%',
58
+ borderRadius: 'var(--radius-sm)', transition: 'all var(--transition-fast)',
59
+ background: activeTab === item.id ? 'rgba(255,90,95,0.1)' : 'transparent',
60
+ color: activeTab === item.id ? 'var(--primary)' : 'var(--text-muted)',
61
+ fontWeight: activeTab === item.id ? '600' : '500',
62
+ borderLeft: activeTab === item.id ? '3px solid var(--primary)' : '3px solid transparent'
63
+ }}
64
+ >
65
+ {item.icon}
66
+ {item.label}
67
+ </button>
68
+ ))}
69
+ </nav>
70
+
71
+ <div style={{ padding: '1.5rem 1rem' }}>
72
+ <button onClick={handleLogout} className="btn-glass" style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}>
73
+ <LogOut size={18} /> Salir
74
+ </button>
75
+ </div>
76
+ </aside>
77
+
78
+ {/* Main Content Area */}
79
+ <main className="main-content" style={{ overflowY: 'auto', background: 'var(--bg-base)' }}>
80
+ <div className="animate-fade-in" style={{ width: '100%', maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
81
+ {activeTab === 'overview' && <DashboardOverview />}
82
+ {activeTab === 'menu' && <MenuEditor />}
83
+ {activeTab === 'inventory' && <InventoryControl />}
84
+ {activeTab === 'reports' && <Reports />}
85
+ {activeTab === 'cash' && <FinanceManager />}
86
+ {activeTab === 'users' && <UserManager />}
87
+ {activeTab === 'tables' && <TableManager />}
88
+ {activeTab === 'crm' && <CRMManager />}
89
+ {activeTab === 'settings' && <SettingsPlus />}
90
+ </div>
91
+ </main>
92
+ </div>
93
+ );
94
+ }
frontend/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('light'); // '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
+ }
frontend/src/pages/KitchenView.jsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,160,0,0.05)' : 'var(--bg-card)',
79
+ boxShadow: 'var(--shadow-sm)'
80
+ }}>
81
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1.25rem', alignItems: 'center' }}>
82
+ <span style={{ fontSize: '1.2rem', fontWeight: '800', color: order.status === 'preparing' ? 'var(--warning)' : 'var(--primary)' }}>
83
+ {order.table} {order.type === 'Llevar' && '🛍️'} {order.type === 'Delivery' && '🚀'}
84
+ </span>
85
+ <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>
86
+ {Math.floor((Date.now() - order.timestamp) / 60000)} min atrás
87
+ </span>
88
+ </div>
89
+
90
+ <div style={{ marginBottom: '1.5rem', minHeight: '80px' }}>
91
+ {order.items.map((item, idx) => (
92
+ <div key={idx} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', borderBottom: '1px solid var(--border-subtle)' }}>
93
+ <span style={{ fontWeight: '600' }}><span style={{ color: 'var(--primary)', fontWeight: '800' }}>{item.qty}</span> {item.name}</span>
94
+ </div>
95
+ ))}
96
+ </div>
97
+
98
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
99
+ {order.status !== 'preparing' ? (
100
+ <button
101
+ className="btn-glass"
102
+ onClick={() => updateStatus(order.id, 'preparing')}
103
+ style={{ background: 'rgba(255,160,0,0.1)', color: '#FFB000' }}
104
+ >
105
+ <Flame size={18} /> Preparar
106
+ </button>
107
+ ) : (
108
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.8rem', color: 'var(--warning)', fontWeight: '700' }}>
109
+ ENCENDIDO...
110
+ </div>
111
+ )}
112
+ <button
113
+ className="btn-primary"
114
+ onClick={() => updateStatus(order.id, 'completed')}
115
+ style={{ background: 'var(--success)', color: '#fff' }}
116
+ >
117
+ <CheckCircle size={18} /> Listo
118
+ </button>
119
+ </div>
120
+ </div>
121
+ ))
122
+ )}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
frontend/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: 'var(--bg-base)',
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(0,0,0,0.02)',
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(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)' }}
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(0,0,0,0.03)', border: '1px solid var(--border-subtle)', color: 'var(--text-main)' }}
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
+ }
frontend/src/pages/WaiterPOS.jsx ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const [customerData, setCustomerData] = useState({ name: '', email: '', phone: '', address: '' });
20
+
21
+ // Fetch Menu
22
+ useEffect(() => {
23
+ onValue(ref(db, 'menu'), (snapshot) => {
24
+ const data = snapshot.val();
25
+ if (data) {
26
+ setMenu(Object.keys(data).map(k => ({ id: k, ...data[k] })));
27
+ }
28
+ });
29
+ }, []);
30
+
31
+ const handleLogout = async () => {
32
+ await logout();
33
+ navigate('/login');
34
+ };
35
+
36
+ const addToCart = (product) => {
37
+ if (!activeTable && orderType === 'Mesa') {
38
+ alert("Selecciona una mesa primero.");
39
+ return;
40
+ }
41
+ const existing = cart.find(item => item.id === product.id);
42
+ if (existing) {
43
+ setCart(cart.map(item => item.id === product.id ? { ...item, qty: item.qty + 1 } : item));
44
+ } else {
45
+ setCart([...cart, { ...product, qty: 1 }]);
46
+ }
47
+ };
48
+
49
+ const removeFromCart = (id) => {
50
+ setCart(cart.filter(item => item.id !== id));
51
+ };
52
+
53
+ const subtotal = cart.reduce((acc, item) => acc + (item.price * item.qty), 0);
54
+ const discountAmount = (subtotal * discount) / 100;
55
+ const totalCart = subtotal - discountAmount + Number(tip || 0);
56
+
57
+ const sendOrder = async () => {
58
+ if ((orderType === 'Mesa' && !activeTable) || cart.length === 0) return;
59
+ const orderRef = push(ref(db, 'orders'));
60
+ const orderData = {
61
+ table: orderType === 'Mesa' ? activeTable : orderType,
62
+ type: orderType,
63
+ waiter: currentUser?.email,
64
+ customerName: customerData.name || 'Consumidor Final',
65
+ customerEmail: customerData.email || 'anonimo@rest.os',
66
+ address: customerData.address || '',
67
+ items: cart,
68
+ subtotal,
69
+ discount,
70
+ tip,
71
+ total: totalCart,
72
+ paymentMethod: paymentMethod,
73
+ status: 'pending',
74
+ timestamp: Date.now()
75
+ };
76
+
77
+ await set(orderRef, orderData);
78
+
79
+ if (!isCheckoutOpen) {
80
+ setCart([]);
81
+ setActiveTable(null);
82
+ setCustomerData({ name: '', email: '', phone: '', address: '' });
83
+ alert('¡Comanda enviada a cocina exitosamente!');
84
+ } else {
85
+ // Finalizar y descontar stock
86
+ await set(ref(db, `orders/${orderRef.key}/status`), 'completed');
87
+
88
+ // Lógica de Descuento de Inventario
89
+ for (const item of cart) {
90
+ const productRef = ref(db, `menu/${item.id}`);
91
+ onValue(productRef, async (snapshot) => {
92
+ const product = snapshot.val();
93
+ if (product && product.ingredients) {
94
+ for (const ing of product.ingredients) {
95
+ onValue(ref(db, `inventory/${ing.id}`), async (invSnap) => {
96
+ const invData = invSnap.val();
97
+ if (invData) {
98
+ const newQty = invData.quantity - (item.qty * ing.qty);
99
+ await update(ref(db, `inventory/${ing.id}`), { quantity: newQty });
100
+ }
101
+ }, { onlyOnce: true });
102
+ }
103
+ }
104
+ }, { onlyOnce: true });
105
+ }
106
+
107
+ setIsCheckoutOpen(false);
108
+ setCart([]);
109
+ setActiveTable(null);
110
+ setCustomerData({ name: '', email: '', phone: '', address: '' });
111
+ alert('¡Venta realizada y stock actualizado!');
112
+ }
113
+ };
114
+
115
+ const printTicket = () => {
116
+ const printWindow = window.open('', '_blank');
117
+ printWindow.document.write(`
118
+ <html>
119
+ <head><title>Ticket de Venta</title></head>
120
+ <body style="font-family: monospace; width: 300px; padding: 20px;">
121
+ <h2 style="text-align: center;">RESTAURANT OS</h2>
122
+ <hr/>
123
+ <p>Mesa: ${activeTable || orderType}</p>
124
+ <p>Fecha: ${new Date().toLocaleString()}</p>
125
+ <hr/>
126
+ ${cart.map(item => `<p>${item.qty}x ${item.name.padEnd(20)} $${(item.price * item.qty).toFixed(2)}</p>`).join('')}
127
+ <hr/>
128
+ <p>Subtotal: $${subtotal.toFixed(2)}</p>
129
+ <p>Descuento (${discount}%): -$${discountAmount.toFixed(2)}</p>
130
+ <p>Propina: $${Number(tip || 0).toFixed(2)}</p>
131
+ <h3 style="text-align: right;">Total: $${totalCart.toFixed(2)}</h3>
132
+ <p style="text-align: center;">¡Gracias por su visita!</p>
133
+ </body>
134
+ </html>
135
+ `);
136
+ printWindow.document.close();
137
+ printWindow.print();
138
+ };
139
+
140
+ return (
141
+ <div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
142
+ {/* Navbar */}
143
+ <header className="glass-panel" style={{ padding: '1rem 2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderRadius: '0', borderBottom: '1px solid var(--border-subtle)', background: 'var(--bg-base)' }}>
144
+ <h1 className="text-gradient" style={{ fontSize: '1.5rem', fontWeight: '800' }}>RESTO PLUS POS</h1>
145
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
146
+ <span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{currentUser?.email}</span>
147
+ <button className="btn-glass" onClick={() => window.open('/menu', '_blank')} style={{ padding: '0.5rem 1rem' }}>Carta Digital</button>
148
+ <button className="btn-glass" onClick={() => window.open('/kitchen', '_blank')} style={{ padding: '0.5rem 1rem' }}>Cocina</button>
149
+ <button className="btn-glass" onClick={handleLogout} style={{ padding: '0.5rem 1rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
150
+ <LogOut size={16} /> Salir
151
+ </button>
152
+ </div>
153
+ </header>
154
+
155
+ <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
156
+ {/* Left Column: Tables & Menu */}
157
+ <div style={{ flex: '65%', padding: '1.5rem', overflowY: 'auto', background: 'rgba(255,255,255,0.01)' }}>
158
+
159
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
160
+ <h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>Tipo de Pedido</h2>
161
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
162
+ {[
163
+ { id: 'Mesa', icon: <Coffee size={18} /> },
164
+ { id: 'Llevar', icon: <ShoppingBag size={18} /> },
165
+ { id: 'Delivery', icon: <Truck size={18} /> }
166
+ ].map(type => (
167
+ <button
168
+ key={type.id}
169
+ onClick={() => { setOrderType(type.id); if(type.id !== 'Mesa') setActiveTable(null); }}
170
+ className={orderType === type.id ? 'btn-primary' : 'btn-glass'}
171
+ style={{ padding: '0.5rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}
172
+ >
173
+ {type.icon} {type.id}
174
+ </button>
175
+ ))}
176
+ </div>
177
+ </div>
178
+
179
+ <div className="glass-card" style={{ padding: '1.25rem', marginBottom: '1.5rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem' }}>
180
+ <div style={{ gridColumn: orderType === 'Delivery' ? 'span 2' : 'auto' }}>
181
+ <label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Cliente / Mesa</label>
182
+ <input
183
+ type="text" placeholder="Nombre del Cliente"
184
+ value={customerData.name} onChange={e => setCustomerData({...customerData, name: e.target.value})}
185
+ style={{ ...inputStyle, padding: '0.6rem' }}
186
+ />
187
+ </div>
188
+ {orderType === 'Delivery' && (
189
+ <div style={{ gridColumn: 'span 2' }}>
190
+ <label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Dirección de Entrega</label>
191
+ <input
192
+ type="text" placeholder="Calle, Número, Referencias"
193
+ value={customerData.address} onChange={e => setCustomerData({...customerData, address: e.target.value})}
194
+ style={{ ...inputStyle, padding: '0.6rem' }}
195
+ />
196
+ </div>
197
+ )}
198
+ <div>
199
+ <label style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>Email (Puntos)</label>
200
+ <input
201
+ type="email" placeholder="cliente@email.com"
202
+ value={customerData.email} onChange={e => setCustomerData({...customerData, email: e.target.value})}
203
+ style={{ ...inputStyle, padding: '0.6rem' }}
204
+ />
205
+ </div>
206
+ </div>
207
+
208
+ {orderType === 'Mesa' && (
209
+ <div style={{ display: 'flex', gap: '1rem', overflowX: 'auto', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
210
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(table => (
211
+ <button
212
+ key={table}
213
+ onClick={() => setActiveTable(table)}
214
+ style={{
215
+ minWidth: '70px', height: '70px', borderRadius: 'var(--radius-lg)',
216
+ background: activeTable === table ? 'var(--primary)' : 'rgba(0,0,0,0.03)',
217
+ border: activeTable === table ? '2px solid transparent' : '1px solid var(--border-subtle)',
218
+ color: activeTable === table ? '#fff' : 'var(--text-main)',
219
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
220
+ fontWeight: '600', transition: 'all 0.2s', cursor: 'pointer',
221
+ boxShadow: activeTable === table ? '0 4px 12px var(--primary-glow)' : 'none'
222
+ }}
223
+ >
224
+ {table}
225
+ </button>
226
+ ))}
227
+ </div>
228
+ )}
229
+
230
+ <h2 className="text-gradient" style={{ fontSize: '1.25rem', marginBottom: '1.5rem' }}>Catálogo de Productos</h2>
231
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: '1rem' }}>
232
+ {menu.map(product => (
233
+ <button
234
+ key={product.id}
235
+ onClick={() => addToCart(product)}
236
+ className="glass-card"
237
+ style={{ textAlign: 'left', padding: '1rem', borderColor: 'var(--border-subtle)', position: 'relative' }}
238
+ >
239
+ <p style={{ fontWeight: '600', marginBottom: '0.5rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
240
+ {product.name}
241
+ </p>
242
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
243
+ <span style={{ color: 'var(--success)', fontWeight: '700' }}>${Number(product.price).toFixed(2)}</span>
244
+ <span style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>{product.category}</span>
245
+ </div>
246
+ </button>
247
+ ))}
248
+ </div>
249
+ </div>
250
+
251
+ {/* Right Column: Order Details */}
252
+ <div style={{ flex: '35%', background: 'var(--bg-card)', borderLeft: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column' }}>
253
+ <div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
254
+ <h2 className="text-gradient" style={{ fontSize: '1.25rem' }}>
255
+ {orderType === 'Mesa' ? `Mesa ${activeTable || '?'}` : orderType}
256
+ </h2>
257
+ <button onClick={() => setCart([])} style={{ color: 'var(--danger)', fontSize: '0.8rem', background: 'none', border: 'none', cursor: 'not-allowed' }} disabled={cart.length === 0}>
258
+ Limpiar
259
+ </button>
260
+ </div>
261
+
262
+ <div style={{ flex: 1, overflowY: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
263
+ {cart.map((item) => (
264
+ <div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.75rem', background: 'rgba(0,0,0,0.02)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)' }}>
265
+ <div style={{ flex: 1 }}>
266
+ <div style={{ fontWeight: '600', fontSize: '0.95rem' }}>{item.name}</div>
267
+ <div style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>{item.qty} x ${item.price}</div>
268
+ </div>
269
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
270
+ <span style={{ fontWeight: '700' }}>${(item.qty * item.price).toFixed(2)}</span>
271
+ <button onClick={() => removeFromCart(item.id)} style={{ color: 'var(--danger)', background: 'none', border: 'none', cursor: 'pointer' }}>
272
+ <Trash2 size={16} />
273
+ </button>
274
+ </div>
275
+ </div>
276
+ ))}
277
+ </div>
278
+
279
+ <div style={{ padding: '1.5rem', borderTop: '1px solid var(--border-subtle)', background: 'rgba(0,0,0,0.03)' }}>
280
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
281
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
282
+ <span>Subtotal:</span>
283
+ <span>${subtotal.toFixed(2)}</span>
284
+ </div>
285
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--primary)' }}>
286
+ <span>Descuento ({discount}%):</span>
287
+ <span>-${discountAmount.toFixed(2)}</span>
288
+ </div>
289
+ {tip > 0 && (
290
+ <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--success)' }}>
291
+ <span>Propina:</span>
292
+ <span>+${Number(tip).toFixed(2)}</span>
293
+ </div>
294
+ )}
295
+ </div>
296
+
297
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.4rem', fontWeight: '900', marginBottom: '1.5rem' }}>
298
+ <span>Total:</span>
299
+ <span className="text-gradient-primary">${totalCart.toFixed(2)}</span>
300
+ </div>
301
+
302
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
303
+ <button
304
+ className="btn-glass"
305
+ onClick={sendOrder}
306
+ disabled={cart.length === 0 || (orderType === 'Mesa' && !activeTable)}
307
+ style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
308
+ >
309
+ <Send size={18} /> Comanda
310
+ </button>
311
+ <button
312
+ className="btn-primary"
313
+ onClick={() => setIsCheckoutOpen(true)}
314
+ disabled={cart.length === 0}
315
+ style={{ opacity: (cart.length === 0) ? 0.5 : 1, padding: '0.8rem' }}
316
+ >
317
+ <Receipt size={18} /> Pagar
318
+ </button>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+
324
+ {/* Checkout Modal */}
325
+ {isCheckoutOpen && (
326
+ <div style={{
327
+ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
328
+ background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(10px)',
329
+ display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
330
+ }}>
331
+ <div className="glass-panel animate-scale-in" style={{ width: '100%', maxWidth: '500px', padding: '2.5rem' }}>
332
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
333
+ <h2 className="text-gradient" style={{ fontSize: '1.75rem' }}>Finalizar Venta</h2>
334
+ <button onClick={() => setIsCheckoutOpen(false)} style={{ background: 'none', border: 'none', color: 'var(--text-muted)' }}>
335
+ <XCircle size={28} />
336
+ </button>
337
+ </div>
338
+
339
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
340
+ <div>
341
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Descuento (%)</label>
342
+ <select
343
+ value={discount}
344
+ onChange={(e) => setDiscount(Number(e.target.value))}
345
+ 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' }}
346
+ >
347
+ <option value="0">Sin Descuento</option>
348
+ <option value="5">5% Promo</option>
349
+ <option value="10">10% Cortesía</option>
350
+ <option value="15">15% Empleado</option>
351
+ <option value="50">50% VIP</option>
352
+ </select>
353
+ </div>
354
+
355
+ <div>
356
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Propina (Efectivo/Sugerida)</label>
357
+ <input
358
+ type="number"
359
+ placeholder="$ 0.00"
360
+ value={tip}
361
+ onChange={(e) => setTip(e.target.value)}
362
+ style={{ width: '100%', padding: '0.8rem', background: 'rgba(0,0,0,0.03)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', color: 'var(--text-main)' }}
363
+ />
364
+ </div>
365
+
366
+ <div>
367
+ <label style={{ display: 'block', fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--text-muted)' }}>Método de Pago</label>
368
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.75rem' }}>
369
+ {[
370
+ { id: 'Efectivo', icon: <Banknote size={20} /> },
371
+ { id: 'Tarjeta', icon: <CreditCard size={20} /> },
372
+ { id: 'QR', icon: <QrCode size={20} /> }
373
+ ].map(m => (
374
+ <button
375
+ key={m.id}
376
+ onClick={() => setPaymentMethod(m.id)}
377
+ className={paymentMethod === m.id ? 'btn-primary' : 'btn-glass'}
378
+ style={{ padding: '1rem 0.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', fontSize: '0.8rem' }}
379
+ >
380
+ {m.icon} {m.id}
381
+ </button>
382
+ ))}
383
+ </div>
384
+ </div>
385
+
386
+ <div style={{ background: 'rgba(255,255,255,0.02)', padding: '1.5rem', borderRadius: 'var(--radius-lg)', marginTop: '1rem', border: '1px dashed var(--border-subtle)' }}>
387
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '1.5rem', fontWeight: '900' }}>
388
+ <span>Total a Pagar:</span>
389
+ <span className="text-gradient">${totalCart.toFixed(2)}</span>
390
+ </div>
391
+ </div>
392
+
393
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
394
+ <button className="btn-glass" onClick={printTicket} style={{ padding: '1rem' }}>
395
+ <Receipt size={20} /> Preview Ticket
396
+ </button>
397
+ <button className="btn-primary" onClick={sendOrder} style={{ padding: '1rem' }}>
398
+ <CheckCircle size={20} /> Confirmar Pago
399
+ </button>
400
+ </div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+ )}
405
+
406
+ <style>{`
407
+ .btn-glass {
408
+ background: rgba(0,0,0,0.03);
409
+ border: 1px solid var(--border-subtle);
410
+ color: var(--text-main);
411
+ border-radius: var(--radius-md);
412
+ cursor: pointer;
413
+ font-weight: 600;
414
+ transition: all 0.2s;
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ gap: 0.5rem;
419
+ }
420
+ .btn-glass:hover {
421
+ background: rgba(0,0,0,0.06);
422
+ transform: translateY(-2px);
423
+ }
424
+ .btn-primary {
425
+ background: var(--primary);
426
+ color: white;
427
+ border: none;
428
+ border-radius: var(--radius-md);
429
+ cursor: pointer;
430
+ font-weight: 600;
431
+ transition: all 0.2s;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ gap: 0.5rem;
436
+ box-shadow: 0 4px 12px var(--primary-glow);
437
+ }
438
+ .btn-primary:hover {
439
+ opacity: 0.9;
440
+ transform: translateY(-2px);
441
+ box-shadow: 0 6px 16px var(--primary-glow);
442
+ }
443
+ .animate-scale-in {
444
+ animation: scaleIn 0.3s ease-out;
445
+ }
446
+ @keyframes scaleIn {
447
+ from { transform: scale(0.9); opacity: 0; }
448
+ to { transform: scale(1); opacity: 1; }
449
+ }
450
+ `}</style>
451
+ </div>
452
+ );
453
+ }
frontend/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
+ })
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "chart.js": "^4.5.1",
14
+ "firebase": "^12.11.0",
15
+ "lucide-react": "^0.577.0",
16
+ "react": "^19.2.4",
17
+ "react-chartjs-2": "^5.3.1",
18
+ "react-dom": "^19.2.4",
19
+ "react-router-dom": "^7.13.1"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.39.4",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^6.0.1",
26
+ "eslint": "^9.39.4",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.4.0",
30
+ "vite": "^6.0.0"
31
+ }
32
+ }