Refactor Restaurant POS module
This commit is contained in:
371
src/components/hotel/StaffManagement.tsx
Normal file
371
src/components/hotel/StaffManagement.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
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, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, Users, Clock, Calendar, Phone, Mail, Shield } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface HotelStaff {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
department: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: 'active' | 'inactive' | 'on_leave';
|
||||
shift: string;
|
||||
hiredDate: Date;
|
||||
hourlyRate: number;
|
||||
accessLevel: string;
|
||||
}
|
||||
|
||||
const StaffManagement = () => {
|
||||
const [staff, setStaff] = useState<HotelStaff[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'María González',
|
||||
role: 'Recepcionista',
|
||||
department: 'Recepción',
|
||||
email: 'maria@hotel.com',
|
||||
phone: '+34 600 111 222',
|
||||
status: 'active',
|
||||
shift: 'Mañana',
|
||||
hiredDate: new Date('2022-05-10'),
|
||||
hourlyRate: 13.00,
|
||||
accessLevel: 'Alto'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Pedro Sánchez',
|
||||
role: 'Conserje',
|
||||
department: 'Recepción',
|
||||
email: 'pedro@hotel.com',
|
||||
phone: '+34 600 222 333',
|
||||
status: 'active',
|
||||
shift: 'Tarde',
|
||||
hiredDate: new Date('2023-01-15'),
|
||||
hourlyRate: 12.00,
|
||||
accessLevel: 'Medio'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Carmen Ruiz',
|
||||
role: 'Supervisora',
|
||||
department: 'Limpieza',
|
||||
email: 'carmen@hotel.com',
|
||||
phone: '+34 600 333 444',
|
||||
status: 'active',
|
||||
shift: 'Mañana',
|
||||
hiredDate: new Date('2021-08-20'),
|
||||
hourlyRate: 14.50,
|
||||
accessLevel: 'Alto'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'José Martín',
|
||||
role: 'Camarero',
|
||||
department: 'Limpieza',
|
||||
email: 'jose@hotel.com',
|
||||
phone: '+34 600 444 555',
|
||||
status: 'active',
|
||||
shift: 'Mañana',
|
||||
hiredDate: new Date('2023-06-01'),
|
||||
hourlyRate: 11.00,
|
||||
accessLevel: 'Bajo'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Isabel Torres',
|
||||
role: 'Técnico',
|
||||
department: 'Mantenimiento',
|
||||
email: 'isabel@hotel.com',
|
||||
phone: '+34 600 555 666',
|
||||
status: 'on_leave',
|
||||
shift: 'Completo',
|
||||
hiredDate: new Date('2022-11-12'),
|
||||
hourlyRate: 15.00,
|
||||
accessLevel: 'Alto'
|
||||
}
|
||||
]);
|
||||
|
||||
const roles = [
|
||||
'Recepcionista', 'Conserje', 'Supervisor', 'Camarero',
|
||||
'Técnico de Mantenimiento', 'Gerente', 'Botones'
|
||||
];
|
||||
const departments = ['Recepción', 'Limpieza', 'Mantenimiento', 'Seguridad', 'Administración'];
|
||||
const shifts = ['Mañana', 'Tarde', 'Noche', 'Completo'];
|
||||
const accessLevels = ['Bajo', 'Medio', 'Alto'];
|
||||
|
||||
const activeStaff = staff.filter(s => s.status === 'active').length;
|
||||
const onLeave = staff.filter(s => s.status === 'on_leave').length;
|
||||
|
||||
const getStatusColor = (status: HotelStaff['status']) => {
|
||||
switch (status) {
|
||||
case 'active': return 'default';
|
||||
case 'inactive': return 'secondary';
|
||||
case 'on_leave': return 'outline';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: HotelStaff['status']) => {
|
||||
switch (status) {
|
||||
case 'active': return 'Activo';
|
||||
case 'inactive': return 'Inactivo';
|
||||
case 'on_leave': return 'De Permiso';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = (staffId: string, newStatus: HotelStaff['status']) => {
|
||||
setStaff(staff.map(s =>
|
||||
s.id === staffId ? { ...s, status: newStatus } : s
|
||||
));
|
||||
toast.success('Estado actualizado');
|
||||
};
|
||||
|
||||
const getMonthsEmployed = (hiredDate: Date) => {
|
||||
const months = Math.floor((Date.now() - hiredDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
|
||||
return months;
|
||||
};
|
||||
|
||||
const staffByDepartment = departments.map(dept => ({
|
||||
department: dept,
|
||||
count: staff.filter(s => s.department === dept).length
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Personal</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{staff.length}</div>
|
||||
<p className="text-xs text-muted-foreground">empleados totales</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Activos</CardTitle>
|
||||
<Users className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-500">{activeStaff}</div>
|
||||
<p className="text-xs text-muted-foreground">trabajando hoy</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">De Permiso</CardTitle>
|
||||
<Clock className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-500">{onLeave}</div>
|
||||
<p className="text-xs text-muted-foreground">ausentes hoy</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Departamentos</CardTitle>
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{departments.length}</div>
|
||||
<p className="text-xs text-muted-foreground">áreas operativas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Department Distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Departamento</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{staffByDepartment.map(dept => (
|
||||
<div key={dept.department} className="text-center p-3 bg-muted rounded-lg">
|
||||
<div className="text-2xl font-bold text-primary">{dept.count}</div>
|
||||
<div className="text-xs text-muted-foreground">{dept.department}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Equipo del Hotel</h3>
|
||||
<p className="text-sm text-muted-foreground">Gestiona tu personal hotelero</p>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Nuevo Empleado
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Nuevo Empleado</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-name">Nombre Completo</Label>
|
||||
<Input id="staff-name" placeholder="Ej: María González" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-role">Puesto</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-role">
|
||||
<SelectValue placeholder="Seleccionar puesto" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map(role => (
|
||||
<SelectItem key={role} value={role}>{role}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-department">Departamento</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-department">
|
||||
<SelectValue placeholder="Departamento" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map(dept => (
|
||||
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-email">Email</Label>
|
||||
<Input id="staff-email" type="email" placeholder="email@hotel.com" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-phone">Teléfono</Label>
|
||||
<Input id="staff-phone" placeholder="+34 600 123 456" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-shift">Turno</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-shift">
|
||||
<SelectValue placeholder="Turno" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shifts.map(shift => (
|
||||
<SelectItem key={shift} value={shift}>{shift}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-access">Nivel de Acceso</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-access">
|
||||
<SelectValue placeholder="Acceso" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{accessLevels.map(level => (
|
||||
<SelectItem key={level} value={level}>{level}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-rate">Tarifa/Hora (€)</Label>
|
||||
<Input id="staff-rate" type="number" step="0.50" placeholder="13.00" />
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full">Agregar Empleado</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Staff List */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{staff.map((member) => (
|
||||
<Card key={member.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg">{member.name}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{member.role}</p>
|
||||
<Badge variant="outline" className="mt-1">{member.department}</Badge>
|
||||
</div>
|
||||
<Badge variant={getStatusColor(member.status)}>
|
||||
{getStatusLabel(member.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs truncate">{member.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs">{member.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Turno: {member.shift}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Acceso: {member.accessLevel}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{getMonthsEmployed(member.hiredDate)} meses en el hotel</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs text-muted-foreground">Tarifa por Hora</div>
|
||||
<div className="text-xl font-bold text-primary">€{member.hourlyRate.toFixed(2)}</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={member.status}
|
||||
onValueChange={(value) => updateStatus(member.id, value as HotelStaff['status'])}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Activo</SelectItem>
|
||||
<SelectItem value="on_leave">De Permiso</SelectItem>
|
||||
<SelectItem value="inactive">Inactivo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaffManagement;
|
||||
364
src/components/restaurant/InventoryManagement.tsx
Normal file
364
src/components/restaurant/InventoryManagement.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
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, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, Package, AlertTriangle, TrendingUp, Search } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
minStock: number;
|
||||
supplier: string;
|
||||
lastRestocked: Date;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
const InventoryManagement = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
const [inventory, setInventory] = useState<InventoryItem[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Arroz Bomba',
|
||||
category: 'Ingredientes',
|
||||
quantity: 25,
|
||||
unit: 'kg',
|
||||
minStock: 10,
|
||||
supplier: 'Distribuciones García',
|
||||
lastRestocked: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),
|
||||
cost: 3.50
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Gambas Frescas',
|
||||
category: 'Mariscos',
|
||||
quantity: 5,
|
||||
unit: 'kg',
|
||||
minStock: 8,
|
||||
supplier: 'Pescadería El Mar',
|
||||
lastRestocked: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
|
||||
cost: 18.00
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Vino Tinto Reserva',
|
||||
category: 'Bebidas',
|
||||
quantity: 45,
|
||||
unit: 'botellas',
|
||||
minStock: 20,
|
||||
supplier: 'Bodegas Rioja',
|
||||
lastRestocked: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000),
|
||||
cost: 8.50
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Aceite de Oliva Virgen Extra',
|
||||
category: 'Ingredientes',
|
||||
quantity: 8,
|
||||
unit: 'litros',
|
||||
minStock: 5,
|
||||
supplier: 'Aceites del Sur',
|
||||
lastRestocked: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
|
||||
cost: 12.00
|
||||
}
|
||||
]);
|
||||
|
||||
const categories = ['all', 'Ingredientes', 'Mariscos', 'Bebidas', 'Carnes', 'Verduras'];
|
||||
|
||||
const lowStockItems = inventory.filter(item => item.quantity <= item.minStock);
|
||||
|
||||
const filteredInventory = inventory.filter(item => {
|
||||
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.supplier.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
const addStock = (itemId: string, quantity: number) => {
|
||||
setInventory(inventory.map(item =>
|
||||
item.id === itemId
|
||||
? { ...item, quantity: item.quantity + quantity, lastRestocked: new Date() }
|
||||
: item
|
||||
));
|
||||
toast.success('Stock actualizado correctamente');
|
||||
};
|
||||
|
||||
const getDaysAgo = (date: Date) => {
|
||||
const days = Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
return days === 0 ? 'Hoy' : days === 1 ? 'Ayer' : `Hace ${days} días`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Items</CardTitle>
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{inventory.length}</div>
|
||||
<p className="text-xs text-muted-foreground">productos en inventario</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Stock Bajo</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-500">{lowStockItems.length}</div>
|
||||
<p className="text-xs text-muted-foreground">items necesitan reposición</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Valor Total</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
€{inventory.reduce((sum, item) => sum + (item.quantity * item.cost), 0).toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">valor del inventario</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Categorías</CardTitle>
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{categories.length - 1}</div>
|
||||
<p className="text-xs text-muted-foreground">categorías activas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Low Stock Alert */}
|
||||
{lowStockItems.length > 0 && (
|
||||
<Card className="border-orange-500 bg-orange-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-orange-700">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Alertas de Stock Bajo
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{lowStockItems.map(item => (
|
||||
<div key={item.id} className="flex justify-between items-center p-2 bg-white rounded">
|
||||
<div>
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="text-sm text-muted-foreground ml-2">
|
||||
Stock: {item.quantity} {item.unit}
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => addStock(item.id, item.minStock)}>
|
||||
Reponer
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Filters and Actions */}
|
||||
<div className="flex flex-wrap gap-4 items-end">
|
||||
<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 productos o proveedores..."
|
||||
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>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Nuevo Item
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Item al Inventario</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="item-name">Nombre del Producto</Label>
|
||||
<Input id="item-name" placeholder="Ej: Arroz Bomba" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="item-quantity">Cantidad</Label>
|
||||
<Input id="item-quantity" type="number" placeholder="25" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="item-unit">Unidad</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="item-unit">
|
||||
<SelectValue placeholder="Unidad" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="kg">Kilogramos</SelectItem>
|
||||
<SelectItem value="litros">Litros</SelectItem>
|
||||
<SelectItem value="unidades">Unidades</SelectItem>
|
||||
<SelectItem value="botellas">Botellas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="item-category">Categoría</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="item-category">
|
||||
<SelectValue placeholder="Seleccionar categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.slice(1).map(cat => (
|
||||
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="item-min">Stock Mínimo</Label>
|
||||
<Input id="item-min" type="number" placeholder="10" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="item-cost">Costo Unitario (€)</Label>
|
||||
<Input id="item-cost" type="number" step="0.01" placeholder="3.50" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="item-supplier">Proveedor</Label>
|
||||
<Input id="item-supplier" placeholder="Nombre del proveedor" />
|
||||
</div>
|
||||
<Button className="w-full">Agregar al Inventario</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Inventory List */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredInventory.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold">{item.name}</h4>
|
||||
<Badge variant="outline" className="mt-1">{item.category}</Badge>
|
||||
</div>
|
||||
{item.quantity <= item.minStock && (
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Stock actual:</span>
|
||||
<span className="font-medium">
|
||||
{item.quantity} {item.unit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Stock mínimo:</span>
|
||||
<span>{item.minStock} {item.unit}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Costo:</span>
|
||||
<span>€{item.cost.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Valor total:</span>
|
||||
<span className="font-medium">€{(item.quantity * item.cost).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Proveedor:</span>
|
||||
<span className="text-xs">{item.supplier}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Última reposición:</span>
|
||||
<span className="text-xs">{getDaysAgo(item.lastRestocked)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
Reponer
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reponer Stock - {item.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Cantidad a agregar ({item.unit})</Label>
|
||||
<Input type="number" defaultValue={item.minStock} />
|
||||
</div>
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<div className="text-sm text-muted-foreground">Nuevo stock será:</div>
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{item.quantity + item.minStock} {item.unit}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => addStock(item.id, item.minStock)}
|
||||
>
|
||||
Confirmar Reposición
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredInventory.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No se encontraron productos
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InventoryManagement;
|
||||
290
src/components/restaurant/POSTerminal.tsx
Normal file
290
src/components/restaurant/POSTerminal.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Search, Plus, Minus, Trash2, CreditCard, DollarSign, Printer } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface CartItem extends MenuItem {
|
||||
quantity: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
const POSTerminal = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [selectedTable, setSelectedTable] = useState<number | null>(null);
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ id: '1', name: 'Paella Valenciana', price: 18.50, category: 'Platos Principales' },
|
||||
{ id: '2', name: 'Gazpacho Andaluz', price: 6.50, category: 'Entrantes' },
|
||||
{ id: '3', name: 'Pulpo a la Gallega', price: 16.00, category: 'Platos Principales' },
|
||||
{ id: '4', name: 'Tarta de Santiago', price: 5.50, category: 'Postres' },
|
||||
{ id: '5', name: 'Vino Tinto Reserva', price: 12.00, category: 'Bebidas' },
|
||||
{ id: '6', name: 'Ensalada Mixta', price: 7.50, category: 'Entrantes' },
|
||||
{ id: '7', name: 'Café Espresso', price: 2.00, category: 'Bebidas' },
|
||||
{ id: '8', name: 'Crema Catalana', price: 4.50, category: 'Postres' }
|
||||
];
|
||||
|
||||
const categories = ['all', 'Entrantes', 'Platos Principales', 'Postres', 'Bebidas'];
|
||||
|
||||
const filteredItems = menuItems.filter(item => {
|
||||
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
const addToCart = (item: MenuItem) => {
|
||||
const existingItem = cart.find(i => i.id === item.id);
|
||||
if (existingItem) {
|
||||
setCart(cart.map(i =>
|
||||
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
||||
));
|
||||
} else {
|
||||
setCart([...cart, { ...item, quantity: 1 }]);
|
||||
}
|
||||
toast.success(`${item.name} agregado`);
|
||||
};
|
||||
|
||||
const updateQuantity = (itemId: string, delta: number) => {
|
||||
setCart(cart.map(item => {
|
||||
if (item.id === itemId) {
|
||||
const newQuantity = item.quantity + delta;
|
||||
return newQuantity > 0 ? { ...item, quantity: newQuantity } : item;
|
||||
}
|
||||
return item;
|
||||
}).filter(item => item.quantity > 0));
|
||||
};
|
||||
|
||||
const removeFromCart = (itemId: string) => {
|
||||
setCart(cart.filter(item => item.id !== itemId));
|
||||
};
|
||||
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const tax = subtotal * 0.10; // 10% IVA
|
||||
const total = subtotal + tax;
|
||||
|
||||
const processPayment = (method: 'cash' | 'card') => {
|
||||
if (!selectedTable) {
|
||||
toast.error('Por favor selecciona una mesa');
|
||||
return;
|
||||
}
|
||||
if (cart.length === 0) {
|
||||
toast.error('El carrito está vacío');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Pago procesado - Mesa ${selectedTable}`);
|
||||
setCart([]);
|
||||
setSelectedTable(null);
|
||||
};
|
||||
|
||||
const printReceipt = () => {
|
||||
toast.success('Imprimiendo recibo...');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-3 gap-6 h-[calc(100vh-300px)]">
|
||||
{/* Left: Menu Items */}
|
||||
<div className="lg:col-span-2 space-y-4 overflow-y-auto">
|
||||
{/* Table Selection */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm font-medium self-center">Mesa:</span>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map(num => (
|
||||
<Button
|
||||
key={num}
|
||||
size="sm"
|
||||
variant={selectedTable === num ? 'default' : 'outline'}
|
||||
onClick={() => setSelectedTable(num)}
|
||||
>
|
||||
{num}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Search and Categories */}
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar productos..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map(cat => (
|
||||
<Button
|
||||
key={cat}
|
||||
size="sm"
|
||||
variant={selectedCategory === cat ? 'default' : 'outline'}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
>
|
||||
{cat === 'all' ? 'Todo' : cat}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{filteredItems.map(item => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="cursor-pointer hover:border-primary transition-colors"
|
||||
onClick={() => addToCart(item)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm leading-tight">{item.name}</h4>
|
||||
<Badge variant="outline" className="text-xs">{item.category}</Badge>
|
||||
<div className="text-lg font-bold text-primary">
|
||||
€{item.price.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Cart and Checkout */}
|
||||
<div className="space-y-4">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex justify-between items-center">
|
||||
<span>Pedido Actual</span>
|
||||
{selectedTable && (
|
||||
<Badge>Mesa {selectedTable}</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 flex flex-col space-y-4">
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{cart.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Carrito vacío
|
||||
</div>
|
||||
) : (
|
||||
cart.map(item => (
|
||||
<div key={item.id} className="p-2 border rounded-lg space-y-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{item.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
€{item.price.toFixed(2)} × {item.quantity}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
€{(item.price * item.quantity).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-7 w-7"
|
||||
onClick={() => updateQuantity(item.id, -1)}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-7 w-7"
|
||||
onClick={() => updateQuantity(item.id, 1)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 ml-auto"
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
<span>€{subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">IVA (10%):</span>
|
||||
<span>€{tax.toFixed(2)}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total:</span>
|
||||
<span className="text-primary">€{total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Buttons */}
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="w-full gap-2"
|
||||
size="lg"
|
||||
onClick={() => processPayment('card')}
|
||||
disabled={cart.length === 0 || !selectedTable}
|
||||
>
|
||||
<CreditCard className="h-5 w-5" />
|
||||
Pagar con Tarjeta
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full gap-2"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={() => processPayment('cash')}
|
||||
disabled={cart.length === 0 || !selectedTable}
|
||||
>
|
||||
<DollarSign className="h-5 w-5" />
|
||||
Pagar en Efectivo
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={printReceipt}
|
||||
disabled={cart.length === 0}
|
||||
>
|
||||
<Printer className="h-4 w-4" />
|
||||
Imprimir Recibo
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default POSTerminal;
|
||||
292
src/components/restaurant/StaffManagement.tsx
Normal file
292
src/components/restaurant/StaffManagement.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
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, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, Users, Clock, Calendar, Phone, Mail } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface Staff {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: 'active' | 'inactive' | 'on_leave';
|
||||
shift: string;
|
||||
hiredDate: Date;
|
||||
hourlyRate: number;
|
||||
}
|
||||
|
||||
const StaffManagement = () => {
|
||||
const [staff, setStaff] = useState<Staff[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Carlos Martínez',
|
||||
role: 'Mesero',
|
||||
email: 'carlos@restaurant.com',
|
||||
phone: '+34 600 123 456',
|
||||
status: 'active',
|
||||
shift: 'Mañana',
|
||||
hiredDate: new Date('2023-01-15'),
|
||||
hourlyRate: 12.50
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Ana López',
|
||||
role: 'Mesera',
|
||||
email: 'ana@restaurant.com',
|
||||
phone: '+34 600 234 567',
|
||||
status: 'active',
|
||||
shift: 'Tarde',
|
||||
hiredDate: new Date('2023-03-20'),
|
||||
hourlyRate: 12.50
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Miguel Rodríguez',
|
||||
role: 'Chef',
|
||||
email: 'miguel@restaurant.com',
|
||||
phone: '+34 600 345 678',
|
||||
status: 'active',
|
||||
shift: 'Completo',
|
||||
hiredDate: new Date('2022-06-10'),
|
||||
hourlyRate: 18.00
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Laura García',
|
||||
role: 'Ayudante de Cocina',
|
||||
email: 'laura@restaurant.com',
|
||||
phone: '+34 600 456 789',
|
||||
status: 'on_leave',
|
||||
shift: 'Mañana',
|
||||
hiredDate: new Date('2023-09-05'),
|
||||
hourlyRate: 10.50
|
||||
}
|
||||
]);
|
||||
|
||||
const roles = ['Mesero', 'Chef', 'Ayudante de Cocina', 'Bartender', 'Host', 'Gerente'];
|
||||
const shifts = ['Mañana', 'Tarde', 'Noche', 'Completo'];
|
||||
|
||||
const activeStaff = staff.filter(s => s.status === 'active').length;
|
||||
const onLeave = staff.filter(s => s.status === 'on_leave').length;
|
||||
|
||||
const getStatusColor = (status: Staff['status']) => {
|
||||
switch (status) {
|
||||
case 'active': return 'default';
|
||||
case 'inactive': return 'secondary';
|
||||
case 'on_leave': return 'outline';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: Staff['status']) => {
|
||||
switch (status) {
|
||||
case 'active': return 'Activo';
|
||||
case 'inactive': return 'Inactivo';
|
||||
case 'on_leave': return 'De Permiso';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = (staffId: string, newStatus: Staff['status']) => {
|
||||
setStaff(staff.map(s =>
|
||||
s.id === staffId ? { ...s, status: newStatus } : s
|
||||
));
|
||||
toast.success('Estado actualizado');
|
||||
};
|
||||
|
||||
const getMonthsEmployed = (hiredDate: Date) => {
|
||||
const months = Math.floor((Date.now() - hiredDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
|
||||
return months;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Personal</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{staff.length}</div>
|
||||
<p className="text-xs text-muted-foreground">empleados totales</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Activos</CardTitle>
|
||||
<Users className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-500">{activeStaff}</div>
|
||||
<p className="text-xs text-muted-foreground">trabajando hoy</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">De Permiso</CardTitle>
|
||||
<Clock className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-500">{onLeave}</div>
|
||||
<p className="text-xs text-muted-foreground">ausentes hoy</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Costo Hora</CardTitle>
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
€{staff.reduce((sum, s) => sum + s.hourlyRate, 0).toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">por hora total</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Equipo de Trabajo</h3>
|
||||
<p className="text-sm text-muted-foreground">Gestiona tu personal del restaurante</p>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Nuevo Empleado
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Nuevo Empleado</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-name">Nombre Completo</Label>
|
||||
<Input id="staff-name" placeholder="Ej: Carlos Martínez" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-role">Puesto</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-role">
|
||||
<SelectValue placeholder="Seleccionar puesto" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map(role => (
|
||||
<SelectItem key={role} value={role}>{role}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-email">Email</Label>
|
||||
<Input id="staff-email" type="email" placeholder="email@ejemplo.com" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-phone">Teléfono</Label>
|
||||
<Input id="staff-phone" placeholder="+34 600 123 456" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="staff-shift">Turno</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="staff-shift">
|
||||
<SelectValue placeholder="Turno" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shifts.map(shift => (
|
||||
<SelectItem key={shift} value={shift}>{shift}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="staff-rate">Tarifa por Hora (€)</Label>
|
||||
<Input id="staff-rate" type="number" step="0.50" placeholder="12.50" />
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full">Agregar Empleado</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Staff List */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{staff.map((member) => (
|
||||
<Card key={member.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{member.name}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{member.role}</p>
|
||||
</div>
|
||||
<Badge variant={getStatusColor(member.status)}>
|
||||
{getStatusLabel(member.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs">{member.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs">{member.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Turno: {member.shift}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{getMonthsEmployed(member.hiredDate)} meses en la empresa</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs text-muted-foreground">Tarifa por Hora</div>
|
||||
<div className="text-xl font-bold text-primary">€{member.hourlyRate.toFixed(2)}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={member.status}
|
||||
onValueChange={(value) => updateStatus(member.id, value as Staff['status'])}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">Activo</SelectItem>
|
||||
<SelectItem value="on_leave">De Permiso</SelectItem>
|
||||
<SelectItem value="inactive">Inactivo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaffManagement;
|
||||
262
src/components/restaurant/TableConfiguration.tsx
Normal file
262
src/components/restaurant/TableConfiguration.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, Edit, Trash2, Grid3x3 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface Table {
|
||||
id: string;
|
||||
number: number;
|
||||
seats: number;
|
||||
section: string;
|
||||
status: 'available' | 'occupied' | 'reserved' | 'maintenance';
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
const TableConfiguration = () => {
|
||||
const [tables, setTables] = useState<Table[]>([
|
||||
{ id: '1', number: 1, seats: 4, section: 'Interior', status: 'available', position: { x: 50, y: 50 } },
|
||||
{ id: '2', number: 2, seats: 2, section: 'Interior', status: 'occupied', position: { x: 200, y: 50 } },
|
||||
{ id: '3', number: 3, seats: 6, section: 'Terraza', status: 'available', position: { x: 50, y: 200 } },
|
||||
{ id: '4', number: 4, seats: 4, section: 'Terraza', status: 'reserved', position: { x: 200, y: 200 } },
|
||||
{ id: '5', number: 5, seats: 8, section: 'VIP', status: 'available', position: { x: 350, y: 125 } }
|
||||
]);
|
||||
|
||||
const [editingTable, setEditingTable] = useState<Table | null>(null);
|
||||
|
||||
const getStatusColor = (status: Table['status']) => {
|
||||
switch (status) {
|
||||
case 'available': return 'bg-green-500';
|
||||
case 'occupied': return 'bg-red-500';
|
||||
case 'reserved': return 'bg-yellow-500';
|
||||
case 'maintenance': return 'bg-gray-500';
|
||||
default: return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: Table['status']) => {
|
||||
switch (status) {
|
||||
case 'available': return 'Disponible';
|
||||
case 'occupied': return 'Ocupada';
|
||||
case 'reserved': return 'Reservada';
|
||||
case 'maintenance': return 'Mantenimiento';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const addTable = () => {
|
||||
const newTable: Table = {
|
||||
id: String(tables.length + 1),
|
||||
number: tables.length + 1,
|
||||
seats: 4,
|
||||
section: 'Interior',
|
||||
status: 'available',
|
||||
position: { x: 100, y: 100 }
|
||||
};
|
||||
setTables([...tables, newTable]);
|
||||
toast.success('Mesa agregada correctamente');
|
||||
};
|
||||
|
||||
const updateTable = (updatedTable: Table) => {
|
||||
setTables(tables.map(t => t.id === updatedTable.id ? updatedTable : t));
|
||||
setEditingTable(null);
|
||||
toast.success('Mesa actualizada correctamente');
|
||||
};
|
||||
|
||||
const deleteTable = (tableId: string) => {
|
||||
setTables(tables.filter(t => t.id !== tableId));
|
||||
toast.success('Mesa eliminada correctamente');
|
||||
};
|
||||
|
||||
const changeStatus = (tableId: string, newStatus: Table['status']) => {
|
||||
setTables(tables.map(t =>
|
||||
t.id === tableId ? { ...t, status: newStatus } : t
|
||||
));
|
||||
toast.success('Estado actualizado');
|
||||
};
|
||||
|
||||
const sections = ['Interior', 'Terraza', 'VIP', 'Bar'];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Layout del Restaurante</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tables.length} mesas configuradas
|
||||
</p>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Nueva Mesa
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Nueva Mesa</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="table-number">Número de Mesa</Label>
|
||||
<Input id="table-number" type="number" defaultValue={tables.length + 1} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="table-seats">Capacidad (personas)</Label>
|
||||
<Input id="table-seats" type="number" defaultValue={4} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="table-section">Sección</Label>
|
||||
<Select defaultValue="Interior">
|
||||
<SelectTrigger id="table-section">
|
||||
<SelectValue placeholder="Seleccionar sección" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sections.map(section => (
|
||||
<SelectItem key={section} value={section}>{section}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={addTable} className="w-full">Agregar Mesa</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Visual Layout */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="relative bg-muted/30 rounded-lg" style={{ height: '400px' }}>
|
||||
<div className="absolute top-2 left-2 flex gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-green-500"></div>
|
||||
<span>Disponible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-red-500"></div>
|
||||
<span>Ocupada</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-yellow-500"></div>
|
||||
<span>Reservada</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tables.map((table) => (
|
||||
<div
|
||||
key={table.id}
|
||||
className="absolute cursor-pointer hover:scale-110 transition-transform"
|
||||
style={{
|
||||
left: `${table.position.x}px`,
|
||||
top: `${table.position.y}px`
|
||||
}}
|
||||
>
|
||||
<div className={`${getStatusColor(table.status)} text-white rounded-lg p-3 shadow-lg`}>
|
||||
<div className="text-center">
|
||||
<div className="font-bold">#{table.number}</div>
|
||||
<div className="text-xs">{table.seats} personas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tables List */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{tables.map((table) => (
|
||||
<Card key={table.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-semibold text-lg">Mesa #{table.number}</h4>
|
||||
<p className="text-sm text-muted-foreground">{table.section}</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="icon" variant="ghost" onClick={() => setEditingTable(table)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar Mesa #{table.number}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Capacidad</Label>
|
||||
<Input type="number" defaultValue={table.seats} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Sección</Label>
|
||||
<Select defaultValue={table.section}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sections.map(section => (
|
||||
<SelectItem key={section} value={section}>{section}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Estado</Label>
|
||||
<Select
|
||||
defaultValue={table.status}
|
||||
onValueChange={(value) => changeStatus(table.id, value as Table['status'])}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="available">Disponible</SelectItem>
|
||||
<SelectItem value="occupied">Ocupada</SelectItem>
|
||||
<SelectItem value="reserved">Reservada</SelectItem>
|
||||
<SelectItem value="maintenance">Mantenimiento</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button className="w-full">Guardar Cambios</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => deleteTable(table.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Capacidad:</span>
|
||||
<span className="font-medium">{table.seats} personas</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Estado:</span>
|
||||
<span className={`font-medium ${getStatusColor(table.status)} text-white px-2 py-0.5 rounded text-xs`}>
|
||||
{getStatusLabel(table.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableConfiguration;
|
||||
Reference in New Issue
Block a user