Nóminas
Exportar contenido
Ruta:
/employee/my-portal/payrollsComponente:Payrolls.tsxPermiso requerido:my_portal
Visualiza nóminas enviadas y firmadas, descarga comprobantes PDF y accede al resumen mensual de fichajes con sheet de firma.
Funcionalidades para el empleado
Sección titulada «Funcionalidades para el empleado»1. Selector de año
Sección titulada «1. Selector de año»- Dropdown con últimos 5 años
- Filtrado automático de nóminas por año seleccionado
- Año actual por defecto
2. Tabla de nóminas
Sección titulada «2. Tabla de nóminas»Columnas:
- Período — Mes y año (ej. “Enero 2025”)
- Neto — Salario neto formateado (ej. “2.450,00 €”)
- Estado — Badge verde con “Firmada” o “Pagada”
- Acciones:
- 🕐 Ver resumen de fichajes — Abre AttendanceSignatureSheet
- 📥 Descargar nómina — Descarga PDF
Estados disponibles:
sent— Enviada al empleadosigned— Firmada por el empleado
3. Sheet de resumen de fichajes
Sección titulada «3. Sheet de resumen de fichajes»AttendanceSignatureSheet:
- Desglose día a día del mes
- Total de horas trabajadas
- Total de pausas
- Días trabajados vs días laborables
- Validación de cumplimiento de jornada
- Firma digital del empleado
Generación dinámica:
- Si
time_summaryexiste en payroll → Usa datos almacenados - Si no existe → Genera dinámicamente con
generateTimeSummary - Loading state mientras se genera
Detalles técnicos
Sección titulada «Detalles técnicos»Consulta de nóminas
Query principal
Sección titulada «Query principal»const { data: payrolls, isLoading } = useQuery({ queryKey: ['employee-paid-payrolls', employee?.id, selectedYear], queryFn: async () => { const { data, error } = await supabase .from('payrolls') .select('id, period_month, period_year, net_salary, status, payslip_pdf_url, time_summary') .eq('employee_id', employee?.id) .eq('period_year', parseInt(selectedYear)) .in('status', ['sent', 'signed']) .order('period_month', { ascending: false });
if (error) throw error; return data; }, enabled: !!employee?.id,});Nota: Solo se muestran nóminas con estado sent o signed. Las nóminas en borrador o pendientes NO son visibles para el empleado.
Descarga de PDF
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 downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = url.split('/').pop() || 'payslip.pdf'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(downloadUrl); } catch (error) { console.error('Error downloading file:', error); window.open(url, '_blank'); // Fallback: abrir en nueva pestaña }};Storage en Supabase:
- Bucket:
payslips - Ruta:
{business_id}/{employee_id}/{period_year}-{period_month}.pdf - Acceso: Signed URLs con expiración
Resumen de fichajes (time_summary)
Estructura del resumen
Sección titulada «Estructura del resumen»interface TimeSummary { month: number; year: number; totalHours: number; totalBreaks: number; daysWorked: number; expectedDays: number; dailyBreakdown: DayBreakdown[];}
interface DayBreakdown { date: string; dayOfWeek: number; workedHours: number; breaks: number; entries: TimeEntry[]; isHoliday: boolean; isWeekend: boolean;}Generación dinámica
Sección titulada «Generación dinámica»const handleShowTimeSummary = async (payroll: PayrollWithSummary) => { setSelectedPayroll(payroll);
// Verificar si time_summary ya está almacenado const storedSummary = parseTimeSummary(payroll.time_summary); if (storedSummary) { setDynamicTimeSummary(storedSummary); setShowTimeSummary(true); return; }
// Generar dinámicamente si no existe if (employee?.id && currentBusinessId) { setLoadingSummary(true); try { const summary = await generateTimeSummary( employee.id, currentBusinessId, payroll.period_month, payroll.period_year ); setDynamicTimeSummary(summary); setShowTimeSummary(true); } catch (error) { console.error('Error generating time summary:', error); } finally { setLoadingSummary(false); } }};Función generateTimeSummary:
- Consulta todos los
time_entriesdel mes/año - Agrupa por día
- Calcula horas trabajadas y pausas
- Valida contra jornada contractual
- Detecta festivos y fines de semana
- Genera resumen completo
Tabla de base de datos
payrolls
Sección titulada «payrolls»| Columna | Tipo | Descripción |
|---|---|---|
id | uuid | ID único de la nómina |
employee_id | uuid | Referencia al empleado |
business_id | uuid | Referencia al negocio |
period_month | integer | Mes (1-12) |
period_year | integer | Año |
gross_salary | numeric | Salario bruto |
net_salary | numeric | Salario neto |
status | text | ’draft’, ‘sent’, ‘signed’ |
payslip_pdf_url | text | URL del PDF en Supabase Storage |
time_summary | jsonb | Resumen de fichajes (opcional) |
sent_at | timestamptz | Fecha de envío |
signed_at | timestamptz | Fecha de firma |
Políticas RLS:
- Empleados solo ven sus nóminas con
status IN ('sent', 'signed') - No pueden ver nóminas en borrador
- Owners ven todas las nóminas de su negocio
Formateo de moneda
Sección titulada «Formateo de moneda»import { formatCurrency } from "@/utils/currencyFormatter";
// Uso en componente{formatCurrency(payroll.net_salary || 0)}// Output: "2.450,00 €"Función centralizada:
- Formatea con 2 decimales por defecto
- Separador de miles: punto
- Separador decimal: coma
- Símbolo: €
Empty states
Sección titulada «Empty states»Si no hay nóminas para el año seleccionado:
<Card> <CardContent className="py-12 text-center"> <FileText className="h-16 w-16 mx-auto mb-4 text-muted-foreground/50" /> <h3 className="text-lg font-semibold mb-2">Sin nóminas para {selectedYear}</h3> <p className="text-muted-foreground max-w-md mx-auto"> Aquí aparecerán tus nóminas pagadas. Prueba a seleccionar otro año o espera a que se procese tu próxima nómina. </p> </CardContent></Card>