Ir al contenido

Ausencias

Ruta: /employee/my-portal/absences Componente: Absences.tsx Permiso requerido: my_portal

Gestiona solicitudes de ausencias con calendario anual interactivo, visualiza días disponibles y usados, y consulta festivos de las oficinas.


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

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

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)

Sheet “Ver solicitudes”:

  • Lista de todas las solicitudes
  • Badge de estado (pendiente, aprobada, rechazada)
  • Filtros por estado y fecha
  • Contador de pendientes en badge

Hook de saldo de ausencias
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_contracts para días anuales asignados
  • Suma días de absence_requests con estado approved
  • Resta días usados del total
Calendario con fichajes
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
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'
}
};
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
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
ColumnaTipoDescripción
iduuidID de la solicitud
employee_iduuidEmpleado solicitante
business_iduuidNegocio
absence_typetextTipo de ausencia
start_datedateFecha inicio
end_datedateFecha fin
total_daysnumericTotal de días
statustext’pending’, ‘approved’, ‘rejected’
reasontextMotivo/notas
document_urltextURL del justificante
requested_attimestamptzFecha de solicitud

<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
<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>
)}

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>