Ir al contenido

Nóminas

Ruta: /employee/my-portal/payrolls Componente: Payrolls.tsx Permiso requerido: my_portal

Visualiza nóminas enviadas y firmadas, descarga comprobantes PDF y accede al resumen mensual de fichajes con sheet de firma.


  • Dropdown con últimos 5 años
  • Filtrado automático de nóminas por año seleccionado
  • Año actual por defecto

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 empleado
  • signed — Firmada por el empleado

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_summary existe en payroll → Usa datos almacenados
  • Si no existe → Genera dinámicamente con generateTimeSummary
  • Loading state mientras se genera

Consulta de nóminas
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
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)
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;
}
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:

  1. Consulta todos los time_entries del mes/año
  2. Agrupa por día
  3. Calcula horas trabajadas y pausas
  4. Valida contra jornada contractual
  5. Detecta festivos y fines de semana
  6. Genera resumen completo
Tabla de base de datos
ColumnaTipoDescripción
iduuidID único de la nómina
employee_iduuidReferencia al empleado
business_iduuidReferencia al negocio
period_monthintegerMes (1-12)
period_yearintegerAño
gross_salarynumericSalario bruto
net_salarynumericSalario neto
statustext’draft’, ‘sent’, ‘signed’
payslip_pdf_urltextURL del PDF en Supabase Storage
time_summaryjsonbResumen de fichajes (opcional)
sent_attimestamptzFecha de envío
signed_attimestamptzFecha 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

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: €

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>