Contratos
Exportar contenido
Ruta:
/employee/my-portal/contractsComponente:MyPortalContracts.tsxPermiso requerido:my_portal
Visualiza todos tus contratos (activos y anteriores) con información completa de jornada, salario, período y documentación adjunta.
Funcionalidades para el empleado
Sección titulada «Funcionalidades para el empleado»1. Grid de contratos
Sección titulada «1. Grid de contratos»Tarjetas 2x2 responsive:
- Puesto y tipo de contrato
- Badge “Activo” o “Anterior”
- Fechas de inicio y fin
- Icono si tiene documento adjunto
2. Sheet de detalle
Sección titulada «2. Sheet de detalle»Al hacer clic en un contrato se abre un sheet lateral con:
Información principal:
- Puesto de trabajo
- Tipo de contrato (temporal, indefinido, prácticas, etc.)
- Estado (activo/anterior)
Período:
- Fecha de inicio (formato completo)
- Fecha de fin (si aplica)
Jornada:
- Horas semanales
- Días laborables (Lunes, Martes, etc.)
- Horario entrada/salida (desde
notesparseado)
Remuneración:
- Salario bruto (mensual/anual/horario)
- Tipo de salario
- Pagos al año (si es anual)
Documento:
- Botón de descarga del PDF del contrato
Detalles técnicos
Sección titulada «Detalles técnicos»Consulta de contratos
Query de contratos
Sección titulada «Query de contratos»const { data: contracts, isLoading } = useQuery({ queryKey: ["my-contracts", currentEmployee?.id], queryFn: async () => { if (!currentEmployee?.id) return [];
const { data, error } = await supabase .from("employee_contracts") .select("*") .eq("employee_id", currentEmployee.id) .order("start_date", { ascending: false });
if (error) throw error; return data as EmployeeContract[]; }, enabled: !!currentEmployee?.id,});Ordenamiento: Contratos más recientes primero.
Detección de contrato activo
Función isContractActive
Sección titulada «Función isContractActive»export const isContractActive = (contract: EmployeeContract): boolean => { const today = new Date(); const startDate = new Date(contract.start_date);
// Si tiene fecha de fin, verificar que hoy esté en el rango if (contract.end_date) { const endDate = new Date(contract.end_date); return today >= startDate && today <= endDate; }
// Sin fecha de fin = indefinido, verificar solo que haya empezado return today >= startDate;};Badge:
<Badge variant={isActive ? "default" : "secondary"}> {isActive ? "Activo" : "Anterior"}</Badge>Parsing de schedule desde notes
Estructura almacenada
Sección titulada «Estructura almacenada»Los datos de horario se guardan en notes con formato:
[SCHEDULE:{"working_days":["mon","tue","wed","thu","fri"],"salary_type":"monthly","payments_per_year":12,"hours_unit":"week","is_active":true}]Función de parsing
Sección titulada «Función de parsing»const parseScheduleFromNotes = (notes: string | null) => { if (!notes) return null;
try { const scheduleMatch = notes.match(/\[SCHEDULE:\s*({[^}]+})\]/); if (scheduleMatch) { return JSON.parse(scheduleMatch[1]); } } catch (e) { // Si falla el parsing, retornar null } return null;};Limpieza de notes
Sección titulada «Limpieza de notes»const cleanNotes = (notes: string | null): string | null => { if (!notes) return null;
// Eliminar el schedule JSON completo let cleaned = notes.replace(/\[SCHEDULE:[^\]]+\]/g, "").trim();
// Eliminar fragmentos JSON residuales cleaned = cleaned.replace(/^[,:\[\]{}"\\s]+/g, "").trim(); cleaned = cleaned.replace(/[,:\[\]{}"\\s]+$/g, "").trim();
// Eliminar líneas que sean solo JSON cleaned = cleaned.split('\n') .map(line => line.trim()) .filter(line => { if (/^[,:\[\]{}"\\s]+$/.test(line)) return false; if (/^[,:]\\s*["']?\\w+["']?\\s*:\\s*/.test(line)) return false; return true; }) .join('\n') .trim();
if (!cleaned || /^[,:\[\]{}"\\s]+$/.test(cleaned)) { return null; }
return cleaned;};Uso:
const schedule = parseScheduleFromNotes(selectedContract.notes);const realNotes = cleanNotes(selectedContract.notes);
// Mostrar jornada desde schedule{schedule?.working_days && ( <p>{getWorkingDaysLabel(schedule.working_days)}</p>)}Tipos de contrato
CONTRACT_TYPE_OPTIONS
Sección titulada «CONTRACT_TYPE_OPTIONS»export const CONTRACT_TYPE_OPTIONS = [ { value: 'indefinido', label: 'Indefinido' }, { value: 'temporal', label: 'Temporal' }, { value: 'formacion', label: 'Formación' }, { value: 'practicas', label: 'Prácticas' }, { value: 'obra_servicio', label: 'Obra o servicio' }, { value: 'interinidad', label: 'Interinidad' }, { value: 'eventual', label: 'Eventual por circunstancias de la producción' }, { value: 'relevo', label: 'Relevo' }, { value: 'sustitucion', label: 'Sustitución' }, { value: 'jubilacion_parcial', label: 'Jubilación parcial' },];Label helper:
const getContractTypeLabel = (type: string) => { return CONTRACT_TYPE_OPTIONS.find((opt) => opt.value === type)?.label || type;};Tabla de base de datos
employee_contracts
Sección titulada «employee_contracts»| Columna | Tipo | Descripción |
|---|---|---|
id | uuid | ID único del contrato |
employee_id | uuid | Referencia al empleado |
business_id | uuid | Referencia al negocio |
contract_type | text | Tipo de contrato |
position | text | Puesto de trabajo |
start_date | date | Fecha de inicio |
end_date | date | Fecha de fin (null si indefinido) |
salary | numeric | Salario bruto |
working_hours | numeric | Horas semanales |
contract_document_url | text | URL del PDF del contrato |
notes | text | Notas adicionales + schedule parseado |
Políticas RLS:
- Empleados solo ven sus propios contratos
- Owners ven todos los contratos de empleados de su negocio
Descarga de documentos
Sección titulada «Descarga de documentos»Handler de descarga
Sección titulada «Handler de descarga»const handleDownload = async (url: string) => { try { const response = await fetch(url); const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = `contrato-${selectedContract?.id || 'documento'}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); } catch (error) { console.error("Error al descargar el documento:", error); window.open(url, "_blank"); // Fallback }};Storage:
- Bucket:
contracts - Ruta:
{business_id}/{employee_id}/{contract_id}.pdf
Empty state
Sección titulada «Empty state»Si el empleado no tiene contratos registrados:
<Card> <CardContent className="py-12 text-center"> <ScrollText className="h-12 w-12 mx-auto text-muted-foreground mb-4" /> <p className="text-muted-foreground">No tienes contratos registrados</p> <p className="text-sm text-muted-foreground mt-2"> Tus contratos aparecerán aquí cuando estén disponibles </p> </CardContent></Card>