Control Horario
Exportar contenido
Ruta:
/employee/my-portal/time-controlComponente:TimeControl.tsxPermiso requerido:my_portal
Vista mensual/semanal del registro de fichajes con estadísticas en tiempo real, opción de añadir fichajes manuales y sistema de solicitud de correcciones a RRHH.
Funcionalidades para el empleado
Sección titulada «Funcionalidades para el empleado»1. Estadísticas del período
Sección titulada «1. Estadísticas del período»Métricas dinámicas (mes o semana):
- Total horas — Suma de horas trabajadas
- Media diaria — Promedio por día trabajado
- Pausas — Total de tiempo en descansos
2. Tabla de fichajes
Sección titulada «2. Tabla de fichajes»Columnas:
- Checkbox — Selección múltiple
- Día — Fecha con día de la semana
- Ubicación — Oficina, Remoto, Con ubicación, Sin definir
- Horas trabajadas — Total del día
- Tiempo de descanso — Suma de pausas
- Estado — Badge (Pendiente, Aprobado, Rechazado, En curso)
Acciones por fila:
- Click en fila → Abre detalle del día
- Doble click → Abre edición (si permitido)
3. Vista mensual vs semanal
Sección titulada «3. Vista mensual vs semanal»Toggle de navegación:
- Botones Mes / Semana
- Navegación con ← →
- Botón “Hoy” para volver al día actual
- Scroll automático al día actual
4. Añadir fichajes manuales
Sección titulada «4. Añadir fichajes manuales»Botón ”+ Añadir fichaje”:
- Selección de fecha (una o múltiples con checkbox)
- Sheet con editor de sesiones de trabajo
- Validación de horarios
- Solo días sin fichajes
Restricciones:
- No se puede añadir si ya hay fichajes ese día
- No se puede añadir si hay fichaje en curso
- Fichajes manuales quedan en estado
pending
5. Solicitar correcciones
Sección titulada «5. Solicitar correcciones»Botón “Ver solicitudes”:
- Lista de solicitudes de corrección enviadas
- Badge con contador de pendientes
- Historial completo con estados
Sheet de corrección desde detalle del día:
- Muestra fichajes actuales
- Campo de motivo de corrección
- Descripción de cambios solicitados
- Envío a RRHH para aprobación
Detalles técnicos
Sección titulada «Detalles técnicos»Procesamiento de fichajes diarios
Cálculo de horas y pausas
Sección titulada «Cálculo de horas y pausas»const dailyData = useMemo(() => { if (!monthlyEntries) return {};
const grouped: Record<string, { entries: TimeEntry[], totalHours: number, totalBreaks: number, location: string, status: 'pendiente' | 'aprobado' | 'rechazado' | 'en_curso', canEdit: boolean }> = {};
monthlyEntries.forEach(entry => { const dateKey = format(new Date(entry.event_timestamp), 'yyyy-MM-dd'); if (!grouped[dateKey]) { grouped[dateKey] = { entries: [], totalHours: 0, totalBreaks: 0, location: getLocationFromEntry(entry), status: 'pendiente', canEdit: true }; } grouped[dateKey].entries.push(entry); });
// Calcular horas usando parseTimeEntriesToSessions Object.keys(grouped).forEach(dateKey => { const sessions = parseTimeEntriesToSessions(grouped[dateKey].entries); const totalHours = sessions.reduce((sum, session) => { return sum + session.ranges .filter(r => r.type === 'work') .reduce((acc, r) => acc + (r.duration || 0), 0); }, 0);
grouped[dateKey].totalHours = totalHours; });
return grouped;}, [monthlyEntries]);Función parseTimeEntriesToSessions:
- Agrupa eventos en sesiones lógicas
- Calcula rangos de trabajo y pausa
- Valida secuencias de eventos
- Detecta jornadas en curso
Detección de ubicación
getLocationFromEntry
Sección titulada «getLocationFromEntry»const getLocationFromEntry = (entry: TimeEntry): string => { // 1. Intentar extraer de notes [LOCATION:...] if (entry.notes) { const locationMatch = entry.notes.match(/\[LOCATION:(.+?)\]/); if (locationMatch) return locationMatch[1]; }
// 2. Check geolocation manual marker if (entry.geolocation?.manual) { return entry.geolocation.location || 'Con ubicación'; }
// 3. Si modality es remote if (entry.modality === 'remote') return 'Remoto';
// 4. Si on_site con geolocalización real if (entry.modality === 'on_site' && entry.geolocation?.lat) { return 'Con ubicación'; }
// 5. Si on_site manual sin geo if (entry.modality === 'on_site' && entry.entry_source === 'manual') { return 'Oficina'; }
// Default return 'Sin definir';};Tipos de ubicación:
- Oficina — Fichaje manual on-site sin GPS
- Remoto — modality = ‘remote’
- Con ubicación — Tiene coordenadas GPS
- Sin definir — Fichajes automáticos sin datos
Estados de aprobación
Estados del fichaje
Sección titulada «Estados del fichaje»type ApprovalStatus = 'pending' | 'approved' | 'rejected';Lógica de estado:
const approvalStatus = entry.approval_status;if (approvalStatus === 'approved') { dayData.status = 'aprobado'; dayData.canEdit = false; // No editable} else if (approvalStatus === 'rejected') { dayData.status = 'rechazado'; dayData.canEdit = true; // Puede corregir} else { dayData.status = 'pendiente'; dayData.canEdit = true; // Puede editar}Fichaje en curso:
const hasActiveClockIn = entries.some(entry => { if (entry.event_type === 'clock_in') { const hasClockOut = entries.some(e => e.event_type === 'clock_out' && new Date(e.event_timestamp) > new Date(entry.event_timestamp) ); return !hasClockOut; } return false;});
if (hasActiveClockIn) { dayData.status = 'en_curso'; dayData.canEdit = false;}Añadir fichajes manuales
Validación previa
Sección titulada «Validación previa»const canAddTimeEntry = useMemo(() => { if (selectedDays.size === 0) return true;
for (const dateKey of selectedDays) { const dayData = dailyData[dateKey]; if (dayData && dayData.entries.length > 0) { return false; // Ya tiene fichajes } } return true;}, [selectedDays, dailyData]);Conversión de slots a eventos
Sección titulada «Conversión de slots a eventos»const convertSlotsToEvents = async (date: string, sessions: any[], empId: string, busId: string) => { const entries: TimeEntry[] = [];
for (const session of sessions) { const workSlots = session.slots.filter((s: any) => s.type === 'work'); const breakSlots = session.slots.filter((s: any) => s.type === 'break');
// 1. clock_in al inicio de primer slot de trabajo const firstSlot = workSlots[0]; const clockInTime = new Date(`${date}T${firstSlot.startTime}:00`); entries.push({ event_type: 'clock_in', event_timestamp: clockInTime.toISOString(), modality: session.location === 'Remoto' ? 'remote' : 'on_site', entry_source: 'manual', approval_status: 'pending', notes: `[LOCATION:${session.location}]`, ... });
// 2. break_start y break_end para cada pausa for (const brk of breakSlots) { // Agregar break_start y break_end }
// 3. clock_out al final del último slot de trabajo const lastSlot = workSlots[workSlots.length - 1]; const clockOutTime = new Date(`${date}T${lastSlot.endTime}:00`); entries.push({ event_type: 'clock_out', event_timestamp: clockOutTime.toISOString(), ... }); }
// Validar secuencia antes de insertar if (!validateEventSequence(entries)) { throw new Error('Secuencia inválida de eventos'); }
return entries;};Sistema de correcciones
Hook useEmployeeCorrectionRequests
Sección titulada «Hook useEmployeeCorrectionRequests»const { createRequest, isCreating, requests } = useEmployeeCorrectionRequests( employeeId, currentBusinessId);Estructura de solicitud:
interface CorrectionRequest { id: string; employee_id: string; request_date: string; reason: string; requested_changes: string; status: 'pending' | 'approved' | 'rejected'; created_at: string; reviewed_at: string | null;}Crear solicitud:
createRequest({ requestDate: '2025-01-15', reason: 'Olvidé fichar la salida', requestedChanges: 'Solicito añadir clock_out a las 18:00',});Tabla: time_correction_requests
Sheets y modales
Sección titulada «Sheets y modales»DayTimeEntriesSheet
Sección titulada «DayTimeEntriesSheet»Ver detalle del día:
- Timeline visual de fichajes
- Total de horas y pausas
- Sesiones de trabajo separadas
- Botón “Editar” (si permitido)
- Botón “Solicitar corrección”
EditDayEntriesSheet
Sección titulada «EditDayEntriesSheet»Editar fichajes:
- Editor de sesiones con horarios
- Añadir/eliminar sesiones
- Modificar pausas
- Validación en tiempo real
- Guardar → Reemplaza fichajes existentes
CorrectionDayEntriesSheet
Sección titulada «CorrectionDayEntriesSheet»Solicitar corrección:
- Vista read-only de fichajes actuales
- Campo de motivo (requerido)
- Campo de cambios solicitados
- Envío a cola de RRHH
AddTimeEntrySheet
Sección titulada «AddTimeEntrySheet»Añadir fichaje manual:
- Lista de fechas seleccionadas
- Editor de sesiones por fecha
- Selector de ubicación
- Validación de horarios
- Múltiples sesiones por día
Configuración de jornada
Sección titulada «Configuración de jornada»Card “Mi Jornada”:
- Muestra horario contractual del empleado
- Entrada y salida esperadas
- Días laborables con badges
- Fuente:
work_schedulestable
<Card> <CardContent> <div className="flex items-center gap-4"> <div>Entrada: {schedule.start_time}</div> <div>Salida: {schedule.end_time}</div> </div> <div className="flex gap-1"> {schedules.map(s => ( <Badge>{dayNames[s.day_of_week]}</Badge> ))} </div> </CardContent></Card>