Ir al contenido

Contratos

Ruta: /employee/my-portal/contracts Componente: MyPortalContracts.tsx Permiso requerido: my_portal

Visualiza todos tus contratos (activos y anteriores) con información completa de jornada, salario, período y documentación adjunta.


Tarjetas 2x2 responsive:

  • Puesto y tipo de contrato
  • Badge “Activo” o “Anterior”
  • Fechas de inicio y fin
  • Icono si tiene documento adjunto

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 notes parseado)

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

Consulta 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
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

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}]
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;
};
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
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
ColumnaTipoDescripción
iduuidID único del contrato
employee_iduuidReferencia al empleado
business_iduuidReferencia al negocio
contract_typetextTipo de contrato
positiontextPuesto de trabajo
start_datedateFecha de inicio
end_datedateFecha de fin (null si indefinido)
salarynumericSalario bruto
working_hoursnumericHoras semanales
contract_document_urltextURL del PDF del contrato
notestextNotas adicionales + schedule parseado

Políticas RLS:

  • Empleados solo ven sus propios contratos
  • Owners ven todos los contratos de empleados de su negocio

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

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>