From 957c95e59cba024394d203ac95d3739b8502f34f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 23:20:14 +0000 Subject: [PATCH] Skip Finance Management module --- src/App.tsx | 22 +- src/components/DashboardLayout.tsx | 4 +- src/components/restaurant/BillManagement.tsx | 274 +++++++++++++++++++ src/components/restaurant/DigitalMenu.tsx | 213 ++++++++++++++ src/components/restaurant/KitchenDisplay.tsx | 212 ++++++++++++++ src/components/restaurant/TableOrders.tsx | 223 +++++++++++++++ src/pages/dashboard/RestaurantPOS.tsx | 134 +++++++++ 7 files changed, 1074 insertions(+), 8 deletions(-) create mode 100644 src/components/restaurant/BillManagement.tsx create mode 100644 src/components/restaurant/DigitalMenu.tsx create mode 100644 src/components/restaurant/KitchenDisplay.tsx create mode 100644 src/components/restaurant/TableOrders.tsx create mode 100644 src/pages/dashboard/RestaurantPOS.tsx diff --git a/src/App.tsx b/src/App.tsx index b3b5ed1..82a19fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ import Settings from "./pages/dashboard/Settings"; import Invoices from "./pages/dashboard/Invoices"; import InvoiceDetail from "./pages/dashboard/InvoiceDetail"; import HotelManagement from "./pages/dashboard/HotelManagement"; +import RestaurantPOS from "./pages/dashboard/RestaurantPOS"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -240,13 +241,20 @@ const AppRouter = () => ( } /> - - - - - - } /> + + + + + + } /> + + + + + + } /> {/* Catch-all route */} } /> diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 018917c..26eb0dd 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -37,7 +37,8 @@ import { Megaphone, ChevronDown, ChevronRight, - Hotel + Hotel, + UtensilsCrossed } from 'lucide-react'; const DashboardLayout = ({ children }: { children: React.ReactNode }) => { @@ -61,6 +62,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { { icon: Settings, label: 'Admin Panel', path: '/dashboard/admin' }, { icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' }, { icon: Hotel, label: 'Hotel Management', path: '/dashboard/hotel-management' }, + { icon: UtensilsCrossed, label: 'Restaurant POS', path: '/dashboard/restaurant-pos' }, { icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' }, { icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' }, ]; diff --git a/src/components/restaurant/BillManagement.tsx b/src/components/restaurant/BillManagement.tsx new file mode 100644 index 0000000..19f92c3 --- /dev/null +++ b/src/components/restaurant/BillManagement.tsx @@ -0,0 +1,274 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { Receipt, Users, CreditCard, Percent } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { toast } from 'sonner'; + +interface Bill { + id: string; + tableNumber: number; + items: BillItem[]; + subtotal: number; + tip: number; + total: number; + status: 'open' | 'split' | 'paid'; + splits?: number; +} + +interface BillItem { + name: string; + quantity: number; + price: number; +} + +const BillManagement = () => { + const [bills, setBills] = useState([ + { + id: '1', + tableNumber: 5, + items: [ + { name: 'Paella Valenciana', quantity: 2, price: 18.50 }, + { name: 'Gazpacho', quantity: 2, price: 6.50 }, + { name: 'Vino Tinto', quantity: 1, price: 12.00 } + ], + subtotal: 62.00, + tip: 6.20, + total: 68.20, + status: 'open' + }, + { + id: '2', + tableNumber: 3, + items: [ + { name: 'Pulpo a la Gallega', quantity: 1, price: 16.00 } + ], + subtotal: 16.00, + tip: 0, + total: 16.00, + status: 'open' + } + ]); + + const [selectedBill, setSelectedBill] = useState(null); + const [tipPercentage, setTipPercentage] = useState('10'); + const [splitCount, setSplitCount] = useState('2'); + + const calculateTip = (subtotal: number, percentage: string) => { + const percent = parseFloat(percentage) || 0; + return (subtotal * percent) / 100; + }; + + const applySplitBill = () => { + if (!selectedBill) return; + const splits = parseInt(splitCount) || 2; + const amountPerPerson = selectedBill.total / splits; + + setBills(bills.map(bill => + bill.id === selectedBill.id + ? { ...bill, status: 'split', splits } + : bill + )); + + toast.success(`Cuenta dividida en ${splits} partes: €${amountPerPerson.toFixed(2)} por persona`); + }; + + const applyTip = (billId: string) => { + setBills(bills.map(bill => { + if (bill.id === billId) { + const tip = calculateTip(bill.subtotal, tipPercentage); + return { + ...bill, + tip, + total: bill.subtotal + tip + }; + } + return bill; + })); + toast.success(`Propina aplicada: €${calculateTip(selectedBill?.subtotal || 0, tipPercentage).toFixed(2)}`); + }; + + const closeBill = (billId: string) => { + setBills(bills.map(bill => + bill.id === billId ? { ...bill, status: 'paid' } : bill + )); + toast.success('Cuenta cerrada correctamente'); + }; + + return ( +
+
+ {bills.filter(b => b.status !== 'paid').map((bill) => ( + + +
+ Mesa {bill.tableNumber} + + {bill.status === 'split' ? `Dividida (${bill.splits})` : 'Abierta'} + +
+
+ +
+ {bill.items.map((item, idx) => ( +
+ {item.quantity}x {item.name} + €{(item.quantity * item.price).toFixed(2)} +
+ ))} +
+ + + +
+
+ Subtotal: + €{bill.subtotal.toFixed(2)} +
+
+ Propina: + €{bill.tip.toFixed(2)} +
+
+ Total: + €{bill.total.toFixed(2)} +
+
+ + {bill.status === 'split' && ( +
+
Por persona:
+
+ €{(bill.total / (bill.splits || 1)).toFixed(2)} +
+
+ )} + +
+ + + + + + + Dividir Cuenta - Mesa {bill.tableNumber} + +
+
+ + setSplitCount(e.target.value)} + /> +
+
+
Monto por persona:
+
+ €{(bill.total / (parseInt(splitCount) || 2)).toFixed(2)} +
+
+ +
+
+
+ + + + + + + + Agregar Propina - Mesa {bill.tableNumber} + +
+
+ + setTipPercentage(e.target.value)} + /> +
+
+ + + +
+
+
+ Subtotal: + €{bill.subtotal.toFixed(2)} +
+
+ Propina ({tipPercentage}%): + €{calculateTip(bill.subtotal, tipPercentage).toFixed(2)} +
+ +
+ Total: + + €{(bill.subtotal + calculateTip(bill.subtotal, tipPercentage)).toFixed(2)} + +
+
+ +
+
+
+
+ + +
+
+ ))} +
+ + {bills.filter(b => b.status !== 'paid').length === 0 && ( + + + +

No hay cuentas abiertas

+
+
+ )} +
+ ); +}; + +export default BillManagement; diff --git a/src/components/restaurant/DigitalMenu.tsx b/src/components/restaurant/DigitalMenu.tsx new file mode 100644 index 0000000..1a36a5c --- /dev/null +++ b/src/components/restaurant/DigitalMenu.tsx @@ -0,0 +1,213 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { QrCode, Plus, Search, Edit, Trash2, Eye } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { toast } from 'sonner'; + +interface MenuItem { + id: string; + name: string; + description: string; + price: number; + category: string; + image?: string; + available: boolean; + allergens?: string[]; +} + +const DigitalMenu = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [menuItems] = useState([ + { + id: '1', + name: 'Paella Valenciana', + description: 'Arroz tradicional con mariscos y pollo', + price: 18.50, + category: 'Platos Principales', + available: true, + allergens: ['mariscos', 'gluten'] + }, + { + id: '2', + name: 'Gazpacho Andaluz', + description: 'Sopa fría de tomate con verduras', + price: 6.50, + category: 'Entrantes', + available: true, + allergens: [] + }, + { + id: '3', + name: 'Tarta de Santiago', + description: 'Tarta de almendra tradicional gallega', + price: 5.50, + category: 'Postres', + available: true, + allergens: ['frutos secos', 'huevo'] + }, + { + id: '4', + name: 'Pulpo a la Gallega', + description: 'Pulpo cocido con pimentón y aceite de oliva', + price: 16.00, + category: 'Platos Principales', + available: false, + allergens: ['mariscos'] + } + ]); + + const categories = ['all', 'Entrantes', 'Platos Principales', 'Postres', 'Bebidas']; + + const filteredItems = menuItems.filter(item => { + const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.description.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory; + return matchesSearch && matchesCategory; + }); + + const generateQR = (tableNumber: string) => { + toast.success(`Código QR generado para mesa ${tableNumber}`); + }; + + return ( +
+ {/* QR Generation Section */} +
+
+ + +
+ + + + + + + + Agregar Item al Menú + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + {/* Search and Filter */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ +
+ + {/* Menu Items Grid */} +
+ {filteredItems.map((item) => ( + + +
+
+

{item.name}

+

{item.description}

+
+ + {item.available ? 'Disponible' : 'Agotado'} + +
+ + {item.allergens && item.allergens.length > 0 && ( +
+ {item.allergens.map(allergen => ( + + {allergen} + + ))} +
+ )} + +
+ €{item.price.toFixed(2)} +
+ + + +
+
+
+
+ ))} +
+ + {filteredItems.length === 0 && ( +
+ No se encontraron platos con los filtros seleccionados +
+ )} +
+ ); +}; + +export default DigitalMenu; diff --git a/src/components/restaurant/KitchenDisplay.tsx b/src/components/restaurant/KitchenDisplay.tsx new file mode 100644 index 0000000..83e0993 --- /dev/null +++ b/src/components/restaurant/KitchenDisplay.tsx @@ -0,0 +1,212 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Clock, AlertCircle, Check } from 'lucide-react'; +import { toast } from 'sonner'; + +interface KitchenOrder { + id: string; + tableNumber: number; + items: KitchenItem[]; + timestamp: Date; + priority: 'normal' | 'urgent'; + status: 'preparing' | 'ready'; +} + +interface KitchenItem { + name: string; + quantity: number; + notes?: string; + station: string; +} + +const KitchenDisplay = () => { + const [orders, setOrders] = useState([ + { + id: '1', + tableNumber: 5, + items: [ + { name: 'Paella Valenciana', quantity: 2, station: 'Caliente', notes: 'Extra mariscos' }, + { name: 'Gazpacho', quantity: 2, station: 'Frío' } + ], + timestamp: new Date(Date.now() - 15 * 60000), + priority: 'normal', + status: 'preparing' + }, + { + id: '2', + tableNumber: 3, + items: [ + { name: 'Pulpo a la Gallega', quantity: 1, station: 'Caliente', notes: 'Sin pimentón' } + ], + timestamp: new Date(Date.now() - 25 * 60000), + priority: 'urgent', + status: 'preparing' + }, + { + id: '3', + tableNumber: 8, + items: [ + { name: 'Tarta de Santiago', quantity: 3, station: 'Postres' } + ], + timestamp: new Date(Date.now() - 5 * 60000), + priority: 'normal', + status: 'preparing' + } + ]); + + const getTimeSince = (date: Date) => { + const minutes = Math.floor((Date.now() - date.getTime()) / 60000); + return minutes; + }; + + const markAsReady = (orderId: string) => { + setOrders(orders.map(order => + order.id === orderId ? { ...order, status: 'ready' } : order + )); + toast.success('Pedido marcado como listo'); + }; + + const getStationColor = (station: string) => { + switch (station) { + case 'Caliente': return 'bg-orange-500/10 text-orange-500'; + case 'Frío': return 'bg-blue-500/10 text-blue-500'; + case 'Postres': return 'bg-purple-500/10 text-purple-500'; + default: return 'bg-gray-500/10 text-gray-500'; + } + }; + + const preparingOrders = orders.filter(o => o.status === 'preparing'); + const readyOrders = orders.filter(o => o.status === 'ready'); + + return ( +
+
+ {/* Preparing Orders */} +
+
+

En Preparación ({preparingOrders.length})

+ {preparingOrders.length} pedidos +
+ +
+ {preparingOrders.map((order) => { + const timeSince = getTimeSince(order.timestamp); + const isUrgent = timeSince > 20 || order.priority === 'urgent'; + + return ( + + +
+ + Mesa {order.tableNumber} + {isUrgent && ( + + )} + +
+ + {timeSince} min +
+
+
+ + {order.items.map((item, idx) => ( +
+
+
+
+ {item.quantity}x {item.name} +
+ {item.notes && ( +
+ ⚠️ {item.notes} +
+ )} +
+ + {item.station} + +
+
+ ))} + + +
+
+ ); + })} + + {preparingOrders.length === 0 && ( + + + No hay pedidos en preparación + + + )} +
+
+ + {/* Ready Orders */} +
+
+

Listos para Servir ({readyOrders.length})

+ {readyOrders.length} pedidos +
+ +
+ {readyOrders.map((order) => { + const timeSince = getTimeSince(order.timestamp); + + return ( + + +
+ + + Mesa {order.tableNumber} + +
+ + {timeSince} min +
+
+
+ + {order.items.map((item, idx) => ( +
+ + {item.quantity}x {item.name} + + + {item.station} + +
+ ))} +
+
+ ); + })} + + {readyOrders.length === 0 && ( + + + No hay pedidos listos + + + )} +
+
+
+
+ ); +}; + +export default KitchenDisplay; diff --git a/src/components/restaurant/TableOrders.tsx b/src/components/restaurant/TableOrders.tsx new file mode 100644 index 0000000..4cef9a6 --- /dev/null +++ b/src/components/restaurant/TableOrders.tsx @@ -0,0 +1,223 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Clock, User, Plus, Check } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { toast } from 'sonner'; + +interface Order { + id: string; + tableNumber: number; + items: OrderItem[]; + status: 'pending' | 'preparing' | 'ready' | 'served'; + timestamp: Date; + waiter: string; + total: number; +} + +interface OrderItem { + name: string; + quantity: number; + price: number; + notes?: string; +} + +const TableOrders = () => { + const [orders, setOrders] = useState([ + { + id: '1', + tableNumber: 5, + items: [ + { name: 'Paella Valenciana', quantity: 2, price: 18.50 }, + { name: 'Gazpacho', quantity: 2, price: 6.50 } + ], + status: 'preparing', + timestamp: new Date(Date.now() - 15 * 60000), + waiter: 'Carlos M.', + total: 50.00 + }, + { + id: '2', + tableNumber: 3, + items: [ + { name: 'Pulpo a la Gallega', quantity: 1, price: 16.00, notes: 'Sin pimentón' } + ], + status: 'pending', + timestamp: new Date(Date.now() - 5 * 60000), + waiter: 'Ana L.', + total: 16.00 + }, + { + id: '3', + tableNumber: 8, + items: [ + { name: 'Tarta de Santiago', quantity: 3, price: 5.50 } + ], + status: 'ready', + timestamp: new Date(Date.now() - 20 * 60000), + waiter: 'Carlos M.', + total: 16.50 + } + ]); + + const getStatusColor = (status: Order['status']) => { + switch (status) { + case 'pending': return 'secondary'; + case 'preparing': return 'default'; + case 'ready': return 'default'; + case 'served': return 'outline'; + default: return 'secondary'; + } + }; + + const getStatusLabel = (status: Order['status']) => { + switch (status) { + case 'pending': return 'Pendiente'; + case 'preparing': return 'En Cocina'; + case 'ready': return 'Listo'; + case 'served': return 'Servido'; + default: return status; + } + }; + + const updateOrderStatus = (orderId: string, newStatus: Order['status']) => { + setOrders(orders.map(order => + order.id === orderId ? { ...order, status: newStatus } : order + )); + toast.success('Estado actualizado correctamente'); + }; + + const getTimeSince = (date: Date) => { + const minutes = Math.floor((Date.now() - date.getTime()) / 60000); + return `${minutes} min`; + }; + + return ( +
+
+
+ + + + +
+ + + + + + + Nuevo Pedido + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+ {orders.map((order) => ( + + +
+ Mesa {order.tableNumber} + + {getStatusLabel(order.status)} + +
+
+
+ + {order.waiter} +
+
+ + {getTimeSince(order.timestamp)} +
+
+
+ +
+ {order.items.map((item, idx) => ( +
+ + {item.quantity}x {item.name} + {item.notes && ( + + Nota: {item.notes} + + )} + + €{(item.quantity * item.price).toFixed(2)} +
+ ))} +
+ +
+ Total: + €{order.total.toFixed(2)} +
+ +
+ {order.status === 'pending' && ( + + )} + {order.status === 'ready' && ( + + )} +
+
+
+ ))} +
+
+ ); +}; + +export default TableOrders; diff --git a/src/pages/dashboard/RestaurantPOS.tsx b/src/pages/dashboard/RestaurantPOS.tsx new file mode 100644 index 0000000..496d47e --- /dev/null +++ b/src/pages/dashboard/RestaurantPOS.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { UtensilsCrossed, QrCode, ChefHat, Receipt } from 'lucide-react'; +import DigitalMenu from '@/components/restaurant/DigitalMenu'; +import TableOrders from '@/components/restaurant/TableOrders'; +import KitchenDisplay from '@/components/restaurant/KitchenDisplay'; +import BillManagement from '@/components/restaurant/BillManagement'; + +const RestaurantPOS = () => { + const [activeOrders] = useState(12); + const [pendingKitchen] = useState(8); + const [dailyRevenue] = useState(4280); + + return ( +
+
+
+

+ + Restaurant POS +

+

+ Sistema completo de gestión de restaurante +

+
+
+ + {/* Stats Overview */} +
+ + + Pedidos Activos + + + +
{activeOrders}
+

En proceso ahora

+
+
+ + + + En Cocina + + + +
{pendingKitchen}
+

Preparando

+
+
+ + + + Ventas del Día + + + +
${dailyRevenue.toLocaleString()}
+

+12% vs ayer

+
+
+
+ + {/* Main Content */} + + + Menú Digital + Pedidos en Mesa + Cocina + Facturación + + + + + + Menú Digital con QR + + Gestiona tu menú y genera códigos QR para las mesas + + + + + + + + + + + + Sistema de Pedidos en Mesa + + Gestiona pedidos de todas las mesas en tiempo real + + + + + + + + + + + + Display de Cocina + + Vista de pedidos para el equipo de cocina + + + + + + + + + + + + Gestión de Cuentas + + Split bill, propinas y cierre de cuentas + + + + + + + + +
+ ); +}; + +export default RestaurantPOS;