Skip Finance Management module
This commit is contained in:
22
src/App.tsx
22
src/App.tsx
@@ -33,6 +33,7 @@ import Settings from "./pages/dashboard/Settings";
|
|||||||
import Invoices from "./pages/dashboard/Invoices";
|
import Invoices from "./pages/dashboard/Invoices";
|
||||||
import InvoiceDetail from "./pages/dashboard/InvoiceDetail";
|
import InvoiceDetail from "./pages/dashboard/InvoiceDetail";
|
||||||
import HotelManagement from "./pages/dashboard/HotelManagement";
|
import HotelManagement from "./pages/dashboard/HotelManagement";
|
||||||
|
import RestaurantPOS from "./pages/dashboard/RestaurantPOS";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -240,13 +241,20 @@ const AppRouter = () => (
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/dashboard/hotel-management" element={
|
<Route path="/dashboard/hotel-management" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<HotelManagement />
|
<HotelManagement />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/dashboard/restaurant-pos" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<RestaurantPOS />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* Catch-all route */}
|
{/* Catch-all route */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ import {
|
|||||||
Megaphone,
|
Megaphone,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Hotel
|
Hotel,
|
||||||
|
UtensilsCrossed
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
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: Settings, label: 'Admin Panel', path: '/dashboard/admin' },
|
||||||
{ icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' },
|
{ icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' },
|
||||||
{ icon: Hotel, label: 'Hotel Management', path: '/dashboard/hotel-management' },
|
{ 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: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
||||||
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
||||||
];
|
];
|
||||||
|
|||||||
274
src/components/restaurant/BillManagement.tsx
Normal file
274
src/components/restaurant/BillManagement.tsx
Normal file
@@ -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<Bill[]>([
|
||||||
|
{
|
||||||
|
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<Bill | null>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{bills.filter(b => b.status !== 'paid').map((bill) => (
|
||||||
|
<Card key={bill.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg">Mesa {bill.tableNumber}</CardTitle>
|
||||||
|
<Badge variant={bill.status === 'split' ? 'default' : 'secondary'}>
|
||||||
|
{bill.status === 'split' ? `Dividida (${bill.splits})` : 'Abierta'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{bill.items.map((item, idx) => (
|
||||||
|
<div key={idx} className="flex justify-between text-sm">
|
||||||
|
<span>{item.quantity}x {item.name}</span>
|
||||||
|
<span>€{(item.quantity * item.price).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span>€{bill.subtotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-muted-foreground">
|
||||||
|
<span>Propina:</span>
|
||||||
|
<span>€{bill.tip.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between font-bold text-base pt-1">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span className="text-primary">€{bill.total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bill.status === 'split' && (
|
||||||
|
<div className="bg-primary/10 p-2 rounded text-sm">
|
||||||
|
<div className="font-medium">Por persona:</div>
|
||||||
|
<div className="text-lg font-bold text-primary">
|
||||||
|
€{(bill.total / (bill.splits || 1)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={() => setSelectedBill(bill)}
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
Dividir
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Dividir Cuenta - Mesa {bill.tableNumber}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="split-count">Número de Personas</Label>
|
||||||
|
<Input
|
||||||
|
id="split-count"
|
||||||
|
type="number"
|
||||||
|
min="2"
|
||||||
|
value={splitCount}
|
||||||
|
onChange={(e) => setSplitCount(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-muted rounded-lg">
|
||||||
|
<div className="text-sm text-muted-foreground">Monto por persona:</div>
|
||||||
|
<div className="text-2xl font-bold text-primary">
|
||||||
|
€{(bill.total / (parseInt(splitCount) || 2)).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" onClick={applySplitBill}>
|
||||||
|
Aplicar División
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={() => setSelectedBill(bill)}
|
||||||
|
>
|
||||||
|
<Percent className="h-4 w-4" />
|
||||||
|
Propina
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Agregar Propina - Mesa {bill.tableNumber}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="tip-percentage">Porcentaje de Propina</Label>
|
||||||
|
<Input
|
||||||
|
id="tip-percentage"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={tipPercentage}
|
||||||
|
onChange={(e) => setTipPercentage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setTipPercentage('5')}>5%</Button>
|
||||||
|
<Button variant="outline" onClick={() => setTipPercentage('10')}>10%</Button>
|
||||||
|
<Button variant="outline" onClick={() => setTipPercentage('15')}>15%</Button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-muted rounded-lg">
|
||||||
|
<div className="flex justify-between text-sm mb-2">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span>€{bill.subtotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm mb-2">
|
||||||
|
<span>Propina ({tipPercentage}%):</span>
|
||||||
|
<span>€{calculateTip(bill.subtotal, tipPercentage).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-bold">Total:</span>
|
||||||
|
<span className="text-xl font-bold text-primary">
|
||||||
|
€{(bill.subtotal + calculateTip(bill.subtotal, tipPercentage)).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" onClick={() => applyTip(bill.id)}>
|
||||||
|
Aplicar Propina
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full gap-2"
|
||||||
|
onClick={() => closeBill(bill.id)}
|
||||||
|
>
|
||||||
|
<CreditCard className="h-4 w-4" />
|
||||||
|
Cerrar Cuenta
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bills.filter(b => b.status !== 'paid').length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
<Receipt className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>No hay cuentas abiertas</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BillManagement;
|
||||||
213
src/components/restaurant/DigitalMenu.tsx
Normal file
213
src/components/restaurant/DigitalMenu.tsx
Normal file
@@ -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<MenuItem[]>([
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* QR Generation Section */}
|
||||||
|
<div className="flex flex-wrap gap-4 items-end">
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<Label htmlFor="table">Número de Mesa</Label>
|
||||||
|
<Input id="table" type="number" placeholder="Ej: 5" />
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => generateQR('5')} className="gap-2">
|
||||||
|
<QrCode className="h-4 w-4" />
|
||||||
|
Generar QR
|
||||||
|
</Button>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Nuevo Item
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Agregar Item al Menú</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="item-name">Nombre del Plato</Label>
|
||||||
|
<Input id="item-name" placeholder="Ej: Paella Valenciana" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="item-desc">Descripción</Label>
|
||||||
|
<Input id="item-desc" placeholder="Descripción del plato" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="item-price">Precio (€)</Label>
|
||||||
|
<Input id="item-price" type="number" step="0.01" placeholder="18.50" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="item-category">Categoría</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger id="item-category">
|
||||||
|
<SelectValue placeholder="Seleccionar" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.slice(1).map(cat => (
|
||||||
|
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full">Agregar Item</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="flex-1 min-w-[200px] relative">
|
||||||
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar platos..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat} value={cat}>
|
||||||
|
{cat === 'all' ? 'Todas las categorías' : cat}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu Items Grid */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredItems.map((item) => (
|
||||||
|
<Card key={item.id} className={!item.available ? 'opacity-60' : ''}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-lg">{item.name}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={item.available ? 'default' : 'secondary'}>
|
||||||
|
{item.available ? 'Disponible' : 'Agotado'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.allergens && item.allergens.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 my-2">
|
||||||
|
{item.allergens.map(allergen => (
|
||||||
|
<Badge key={allergen} variant="outline" className="text-xs">
|
||||||
|
{allergen}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-4">
|
||||||
|
<span className="text-xl font-bold text-primary">€{item.price.toFixed(2)}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredItems.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
No se encontraron platos con los filtros seleccionados
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DigitalMenu;
|
||||||
212
src/components/restaurant/KitchenDisplay.tsx
Normal file
212
src/components/restaurant/KitchenDisplay.tsx
Normal file
@@ -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<KitchenOrder[]>([
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
{/* Preparing Orders */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">En Preparación ({preparingOrders.length})</h3>
|
||||||
|
<Badge variant="secondary">{preparingOrders.length} pedidos</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{preparingOrders.map((order) => {
|
||||||
|
const timeSince = getTimeSince(order.timestamp);
|
||||||
|
const isUrgent = timeSince > 20 || order.priority === 'urgent';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={order.id} className={isUrgent ? 'border-orange-500' : ''}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
Mesa {order.tableNumber}
|
||||||
|
{isUrgent && (
|
||||||
|
<AlertCircle className="h-5 w-5 text-orange-500" />
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{timeSince} min
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{order.items.map((item, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">
|
||||||
|
{item.quantity}x {item.name}
|
||||||
|
</div>
|
||||||
|
{item.notes && (
|
||||||
|
<div className="text-sm text-orange-500 font-medium mt-1">
|
||||||
|
⚠️ {item.notes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge className={getStationColor(item.station)} variant="secondary">
|
||||||
|
{item.station}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full gap-2 mt-3"
|
||||||
|
onClick={() => markAsReady(order.id)}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Marcar como Listo
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{preparingOrders.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
No hay pedidos en preparación
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ready Orders */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">Listos para Servir ({readyOrders.length})</h3>
|
||||||
|
<Badge variant="default">{readyOrders.length} pedidos</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{readyOrders.map((order) => {
|
||||||
|
const timeSince = getTimeSince(order.timestamp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={order.id} className="border-green-500 bg-green-500/5">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Check className="h-5 w-5 text-green-500" />
|
||||||
|
Mesa {order.tableNumber}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{timeSince} min
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{order.items.map((item, idx) => (
|
||||||
|
<div key={idx} className="flex justify-between">
|
||||||
|
<span className="font-medium">
|
||||||
|
{item.quantity}x {item.name}
|
||||||
|
</span>
|
||||||
|
<Badge className={getStationColor(item.station)} variant="secondary">
|
||||||
|
{item.station}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{readyOrders.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
No hay pedidos listos
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KitchenDisplay;
|
||||||
223
src/components/restaurant/TableOrders.tsx
Normal file
223
src/components/restaurant/TableOrders.tsx
Normal file
@@ -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<Order[]>([
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm">Todas</Button>
|
||||||
|
<Button variant="ghost" size="sm">Pendientes</Button>
|
||||||
|
<Button variant="ghost" size="sm">En Cocina</Button>
|
||||||
|
<Button variant="ghost" size="sm">Listas</Button>
|
||||||
|
</div>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Nuevo Pedido
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Nuevo Pedido</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="order-table">Mesa</Label>
|
||||||
|
<Input id="order-table" type="number" placeholder="Número de mesa" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="order-waiter">Mesero</Label>
|
||||||
|
<Input id="order-waiter" placeholder="Nombre del mesero" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="order-item">Plato</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger id="order-item">
|
||||||
|
<SelectValue placeholder="Seleccionar plato" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="paella">Paella Valenciana - €18.50</SelectItem>
|
||||||
|
<SelectItem value="gazpacho">Gazpacho - €6.50</SelectItem>
|
||||||
|
<SelectItem value="pulpo">Pulpo a la Gallega - €16.00</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="order-quantity">Cantidad</Label>
|
||||||
|
<Input id="order-quantity" type="number" min="1" defaultValue="1" />
|
||||||
|
</div>
|
||||||
|
<Button className="w-full">Crear Pedido</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{orders.map((order) => (
|
||||||
|
<Card key={order.id}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg">Mesa {order.tableNumber}</CardTitle>
|
||||||
|
<Badge variant={getStatusColor(order.status)}>
|
||||||
|
{getStatusLabel(order.status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 text-sm text-muted-foreground mt-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
{order.waiter}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{getTimeSince(order.timestamp)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{order.items.map((item, idx) => (
|
||||||
|
<div key={idx} className="flex justify-between text-sm">
|
||||||
|
<span>
|
||||||
|
{item.quantity}x {item.name}
|
||||||
|
{item.notes && (
|
||||||
|
<span className="text-xs text-muted-foreground block">
|
||||||
|
Nota: {item.notes}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">€{(item.quantity * item.price).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 border-t flex justify-between items-center">
|
||||||
|
<span className="font-bold">Total:</span>
|
||||||
|
<span className="font-bold text-lg">€{order.total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
{order.status === 'pending' && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => updateOrderStatus(order.id, 'preparing')}
|
||||||
|
>
|
||||||
|
Enviar a Cocina
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{order.status === 'ready' && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={() => updateOrderStatus(order.id, 'served')}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Marcar Servido
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableOrders;
|
||||||
134
src/pages/dashboard/RestaurantPOS.tsx
Normal file
134
src/pages/dashboard/RestaurantPOS.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||||
|
<UtensilsCrossed className="h-8 w-8 text-primary" />
|
||||||
|
Restaurant POS
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Sistema completo de gestión de restaurante
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Overview */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Pedidos Activos</CardTitle>
|
||||||
|
<Receipt className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{activeOrders}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">En proceso ahora</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">En Cocina</CardTitle>
|
||||||
|
<ChefHat className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{pendingKitchen}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Preparando</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Ventas del Día</CardTitle>
|
||||||
|
<QrCode className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">${dailyRevenue.toLocaleString()}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">+12% vs ayer</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<Tabs defaultValue="menu" className="space-y-4">
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="menu">Menú Digital</TabsTrigger>
|
||||||
|
<TabsTrigger value="orders">Pedidos en Mesa</TabsTrigger>
|
||||||
|
<TabsTrigger value="kitchen">Cocina</TabsTrigger>
|
||||||
|
<TabsTrigger value="bills">Facturación</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="menu" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Menú Digital con QR</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Gestiona tu menú y genera códigos QR para las mesas
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DigitalMenu />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="orders" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sistema de Pedidos en Mesa</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Gestiona pedidos de todas las mesas en tiempo real
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TableOrders />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="kitchen" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Display de Cocina</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Vista de pedidos para el equipo de cocina
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<KitchenDisplay />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="bills" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Gestión de Cuentas</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Split bill, propinas y cierre de cuentas
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<BillManagement />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RestaurantPOS;
|
||||||
Reference in New Issue
Block a user