372 lines
14 KiB
TypeScript
372 lines
14 KiB
TypeScript
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;
|