Inicio - Mi Portal
Exportar contenido
Ruta:
/employee/my-portalComponente:Home.tsxPermiso requerido:my_portal
Dashboard principal del empleado con sistema de fichaje tipo Factorial. Incluye cronómetro en tiempo real, estados visuales y gestión de jornadas abiertas.
Funcionalidades para el empleado
Sección titulada «Funcionalidades para el empleado»1. Fichaje con cronómetro en vivo
Sección titulada «1. Fichaje con cronómetro en vivo»- Fecha actual en español con formato completo
- Timer grande con horas, minutos y segundos en tiempo real
- Estado visual con punto de color y badge:
- 🔴 Trabajando — Verde con animación pulse
- 🟡 En pausa — Amarillo
- 🟢 Jornada completada — Verde
- ⚫ Desconectado — Gris
2. Botones circulares de acción
Sección titulada «2. Botones circulares de acción»Estados del empleado:
| Estado | Botón principal | Acción secundaria |
|---|---|---|
| Desconectado | ▶️ Fichar entrada (verde) | - |
| Trabajando | ⏸️ Iniciar pausa (amarillo) | ⏹️ Fichar salida |
| En pausa | ▶️ Reanudar trabajo (verde) | - |
| Jornada completada | ▶️ Nueva jornada (verde) | - |
Confirmación de salida:
- Diálogo de confirmación antes de fichar salida
- Previene cierres accidentales
- Muestra hora actual
3. Información de jornada
Sección titulada «3. Información de jornada»- Hora de entrada: Se muestra una vez fichado
- Jornadas abiertas: Detección automática de fichajes sin cerrar
- Si hay jornada abierta del día anterior → Diálogo de regularización
- Opción de cerrar automáticamente o corregir manualmente
Detalles técnicos
Sección titulada «Detalles técnicos»Implementación del cronómetro
Hook useTimeEntries
Sección titulada «Hook useTimeEntries»const { currentStatus, // Estado actual: 'working', 'on_break', 'not_clocked', 'day_complete' todayEntries, // Array de fichajes del día isLoadingToday, // Loading state clockMutation, // Mutación para fichar employeeId // ID del empleado} = useTimeEntries(user?.id, currentBusinessId);Tipos de evento:
type EventType = 'clock_in' | 'clock_out' | 'break_start' | 'break_end';Cronómetro en vivo
Sección titulada «Cronómetro en vivo»useEffect(() => { if (status !== "working") { setLiveTime(totalHoursToday); return; }
const baselineHours = totalHoursToday; const baselineMs = Date.now();
const tick = () => { const elapsedMs = Date.now() - baselineMs; setLiveTime(baselineHours + elapsedMs / (1000 * 60 * 60)); };
// Sincronización con segundos completos const msToNextSecond = 1000 - (Date.now() % 1000); const timeoutId = setTimeout(() => { tick(); const intervalId = setInterval(tick, 1000); }, msToNextSecond);
return () => { clearTimeout(timeoutId); clearInterval(intervalId); };}, [status, totalHoursToday]);Características:
- Actualización cada segundo
- Sincronizado con el reloj del sistema
- Reseteo automático en cambios de estado
- Sin drift acumulativo
Formateo del timer
Sección titulada «Formateo del timer»const getTimerParts = (hours: number) => { const totalSeconds = Math.floor(hours * 3600); const h = Math.floor(totalSeconds / 3600); const m = Math.floor((totalSeconds % 3600) / 60); const s = totalSeconds % 60;
return { hours: h.toString().padStart(2, '0'), minutes: m.toString().padStart(2, '0'), seconds: s.toString().padStart(2, '0'), };};Tipografía monoespaciada:
<div style={{ fontVariantNumeric: 'tabular-nums slashed-zero', fontFeatureSettings: '"tnum", "zero"'}}> {timerParts.hours}h {timerParts.minutes}m {timerParts.seconds}s</div>Gestión de jornadas abiertas
Hook useOpenWorkday
Sección titulada «Hook useOpenWorkday»Detecta jornadas que no fueron cerradas correctamente (sin clock_out).
const { data: openWorkday, refetch: refetchOpenWorkday } = useOpenWorkday( employeeId, currentBusinessId);Estructura de jornada abierta:
interface OpenWorkday { date: string; // Fecha de la jornada abierta entries: TimeEntry[]; // Fichajes del día last_clock_in_time: string; // Última entrada sin salida}Diálogo de regularización
Sección titulada «Diálogo de regularización»Si se detecta jornada abierta al intentar clock_in:
if (eventType === 'clock_in' && openWorkday) { setShowOpenWorkdayDialog(true); return;}Componente: OpenWorkdayDialog
- Muestra fecha y hora de la jornada abierta
- Opciones:
- Cerrar automáticamente — Añade
clock_outa la jornada anterior - Corregir manualmente — Redirige a Control Horario para editar
- Cerrar automáticamente — Añade
- Callback
onResolvedinvalida queries para refrescar datos
Tabla de base de datos
time_entries
Sección titulada «time_entries»| Columna | Tipo | Descripción |
|---|---|---|
id | uuid | ID único del fichaje |
employee_id | uuid | Referencia al empleado |
business_id | uuid | Referencia al negocio |
event_timestamp | timestamptz | Momento exacto del fichaje |
event_type | text | ’clock_in’, ‘clock_out’, ‘break_start’, ‘break_end’ |
modality | text | ’on_site’, ‘remote’ |
geolocation | jsonb | Coordenadas GPS (opcional) |
entry_source | text | ’automatic’, ‘manual’, ‘admin’ |
approval_status | text | ’pending’, ‘approved’, ‘rejected’ |
is_superseded | boolean | true si fue reemplazado por corrección |
notes | text | Notas adicionales |
Índices:
idx_time_entries_employee_businessen (employee_id, business_id)idx_time_entries_timestampen (event_timestamp)idx_time_entries_supersededen (is_superseded) WHERE is_superseded = false
Políticas RLS:
- Empleados solo ven sus propios fichajes
- Owners ven todos los fichajes de su negocio
- HR managers pueden ver según permisos
Estados y transiciones
Máquina de estados
Sección titulada «Máquina de estados»type Status = 'not_clocked' | 'working' | 'on_break' | 'day_complete';Transiciones permitidas:
not_clocked ──clock_in──> working │ ┌─────────────┴─────────────┐ │ │ break_start clock_out │ │ v v on_break ──break_end──> working day_completeValidaciones:
- No se puede
clock_outsi hay pausa activa break_endsolo si existebreak_startprevio- Nueva jornada solo después de
day_complete
Configuración de estados
Sección titulada «Configuración de estados»const getStatusConfig = () => { switch (status) { case 'working': return { label: 'Trabajando', dotColor: 'bg-red-500', // Rojo en vivo badgeClass: 'bg-success/20 text-success' }; case 'on_break': return { label: 'En pausa', dotColor: 'bg-amber-500', badgeClass: 'bg-amber-100 text-amber-700' }; case 'day_complete': return { label: 'Jornada completada', dotColor: 'bg-green-500', badgeClass: 'bg-green-100 text-green-700' }; default: return { label: 'Desconectado', dotColor: 'bg-gray-400', badgeClass: 'bg-gray-100 text-gray-600' }; }};Branding y diseño
Sección titulada «Branding y diseño»Botón de fichaje circular
Sección titulada «Botón de fichaje circular»<Button size="icon" className="h-20 w-20 rounded-full bg-[hsl(var(--brand-green))] hover:bg-[hsl(var(--brand-green-dark))] text-[hsl(var(--brand-black))] shadow-lg" onClick={() => handleClock('clock_in')}> <Play className="h-8 w-8" /></Button>Colores Factux:
- Verde:
hsl(68 100% 50%) - Verde oscuro:
hsl(68 100% 45%) - Negro:
hsl(0 0% 10%)
Layout responsivo
Sección titulada «Layout responsivo»Desktop:
- Timer centrado con espacio vertical generoso
- Botones grandes (80x80px)
- Badge y estado prominentes
Mobile:
- Layout vertical compacto
- Botones medianos (64x64px)
- Touch targets de 44px mínimo