Ausencias
Exportar contenido
Ruta:
/employee/my-portal/absencesComponente:Absences.tsxPermiso requerido:my_portal
Gestiona solicitudes de ausencias con calendario anual interactivo, visualiza días disponibles y usados, y consulta festivos de las oficinas.
Funcionalidades para el empleado
Sección titulada «Funcionalidades para el empleado»1. Header con estadísticas
Sección titulada «1. Header con estadísticas»Métricas clave:
- Días disponibles — Saldo actual de vacaciones
- Días usados — Total consumido en el año
- Horas extras de descanso — Banco de horas disponibles
2. Calendario anual
Sección titulada «2. Calendario anual»Vistas disponibles:
- Año — Grid de 12 meses con mini-calendario
- Mes — Calendario mensual completo
- Semana — Vista semanal detallada
- Día — Detalle de eventos del día
Elementos visuales:
- Festivos — Marcados en amarillo 🎉
- Ausencias aprobadas — Código de color por tipo
- Fichajes del año — Icono de reloj 🕐
- Día actual — Ring verde con highlight
3. Solicitud de ausencias
Sección titulada «3. Solicitud de ausencias»Botón ”+ Solicitar ausencia”:
- Sheet lateral con formulario
- Selector de tipo (vacaciones, baja médica, permiso, etc.)
- Fecha inicio y fin
- Contador automático de días
- Campo de motivo/notas
- Upload de documento justificativo (opcional)
4. Historial de solicitudes
Sección titulada «4. Historial de solicitudes»Sheet “Ver solicitudes”:
- Lista de todas las solicitudes
- Badge de estado (pendiente, aprobada, rechazada)
- Filtros por estado y fecha
- Contador de pendientes en badge
Detalles técnicos
Sección titulada «Detalles técnicos»Hook de saldo de ausencias
useEmployeeLeaveBalance
Sección titulada «useEmployeeLeaveBalance»const { totals, isLoading, employeeId } = useEmployeeLeaveBalance();
// totals contiene:{ days_remaining: number; // Días disponibles days_used: number; // Días consumidos days_total: number; // Total anual asignado}Cálculo:
- Lee
employee_contractspara días anuales asignados - Suma días de
absence_requestscon estadoapproved - Resta días usados del total
Calendario con fichajes
Query de fichajes del año
Sección titulada «Query de fichajes del año»const { data: yearTimeEntries } = useQuery({ queryKey: ['employee-year-time-entries', employeeId, currentBusinessId, currentYear], queryFn: async () => { const yearStart = new Date(currentYear, 0, 1); const yearEnd = new Date(currentYear, 11, 31, 23, 59, 59);
const { data, error } = await supabase .from('time_entries') .select('event_timestamp') .eq('employee_id', employeeId) .eq('business_id', currentBusinessId) .eq('is_superseded', false) .gte('event_timestamp', yearStart.toISOString()) .lte('event_timestamp', yearEnd.toISOString());
if (error) throw error; return data || []; }, enabled: !!employeeId && !!currentBusinessId,});Set de días con fichajes:
const daysWithTimeEntries = useMemo(() => { if (!yearTimeEntries) return new Set<string>(); const dates = new Set<string>(); yearTimeEntries.forEach(entry => { const dateStr = format(new Date(entry.event_timestamp), 'yyyy-MM-dd'); dates.add(dateStr); }); return dates;}, [yearTimeEntries]);Indicador visual:
{hasTimeEntry && <Clock className="h-3 w-3 text-blue-600" />}Tipos de ausencia
Códigos de color
Sección titulada «Códigos de color»export const LEAVE_TYPE_COLORS = { vacation: { bg: 'bg-blue-100 dark:bg-blue-950', text: 'text-blue-700 dark:text-blue-400' }, sick_leave: { bg: 'bg-red-100 dark:bg-red-950', text: 'text-red-700 dark:text-red-400' }, personal: { bg: 'bg-purple-100 dark:bg-purple-950', text: 'text-purple-700 dark:text-purple-400' }, maternity: { bg: 'bg-pink-100 dark:bg-pink-950', text: 'text-pink-700 dark:text-pink-400' }, unpaid: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-400' }, holiday: { bg: 'bg-amber-100 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-400' }};Labels en español
Sección titulada «Labels en español»export const getAbsenceTypeLabel = (type: string) => { const labels: Record<string, string> = { vacation: 'Vacaciones', sick_leave: 'Baja médica', personal: 'Permiso personal', maternity: 'Maternidad/Paternidad', unpaid: 'Sin sueldo', study_leave: 'Permiso de estudios', marriage: 'Matrimonio', death: 'Defunción familiar', moving: 'Mudanza', other: 'Otro' }; return labels[type] || type;};Festivos de las oficinas
Hook usePublicHolidays
Sección titulada «Hook usePublicHolidays»const { data: holidays = [], isLoading } = usePublicHolidays(currentBusinessId);Estructura:
interface PublicHoliday { holiday_date: string; // 'YYYY-MM-DD' name: string; // 'Día de Reyes' is_national: boolean; // true si es nacional office_id: string; // ID de la oficina}Toggle de visualización:
<Switch checked={showHolidays} onCheckedChange={setShowHolidays}/>Tabla de ausencias
absence_requests
Sección titulada «absence_requests»| Columna | Tipo | Descripción |
|---|---|---|
id | uuid | ID de la solicitud |
employee_id | uuid | Empleado solicitante |
business_id | uuid | Negocio |
absence_type | text | Tipo de ausencia |
start_date | date | Fecha inicio |
end_date | date | Fecha fin |
total_days | numeric | Total de días |
status | text | ’pending’, ‘approved’, ‘rejected’ |
reason | text | Motivo/notas |
document_url | text | URL del justificante |
requested_at | timestamptz | Fecha de solicitud |
Botones de acción
Sección titulada «Botones de acción»Solicitar ausencia
Sección titulada «Solicitar ausencia»<AbsenceRequestSheet employeeId={employeeId} />Componente interno:
- Validación de fechas
- Cálculo automático de días
- Descuenta festivos y fines de semana
- Verifica saldo disponible
- Upload de documentos a Supabase Storage
Ver solicitudes
Sección titulada «Ver solicitudes»<EmployeeAbsenceRequestsSheet open={isRequestsSheetOpen} onOpenChange={setIsRequestsSheetOpen} employeeId={employeeId}/>Badge de pendientes:
{pendingRequestsCount > 0 && ( <Badge className="absolute -top-1 -right-1 h-5 w-5"> {pendingRequestsCount} </Badge>)}Leyenda de colores
Sección titulada «Leyenda de colores»Al final del calendario se muestra una leyenda dinámica con solo los tipos de ausencia presentes:
<div className="flex flex-wrap gap-3 mt-6 pt-4 border-t"> {activeLegendTypes.map(type => { const colors = LEAVE_TYPE_COLORS[type]; const label = getAbsenceTypeLabel(type); return ( <div key={type} className="flex items-center gap-2"> <div className={cn('w-3 h-3 rounded-full', colors.bg)} /> <span className="text-sm text-muted-foreground">{label}</span> </div> ); })}</div>