'use client' /** * @description Payroll management interface for calculating and tracking staff compensation * @audit BUSINESS RULE: Payroll includes base salary, service commissions (10%), and tips (5%) * @audit SECURITY: Requires authenticated admin/manager role via useAuth hook * @audit Validate: Payroll period must have valid start and end dates * @audit AUDIT: Payroll calculations logged through /api/aperture/payroll endpoint */ import { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Calendar, DollarSign, Clock, Users, Calculator, Download, Eye } from 'lucide-react' import { format } from 'date-fns' import { es } from 'date-fns/locale' import { useAuth } from '@/lib/auth/context' interface PayrollRecord { id: string staff_id: string payroll_period_start: string payroll_period_end: string base_salary: number service_commissions: number total_tips: number total_earnings: number hours_worked: number status: string calculated_at?: string paid_at?: string staff?: { id: string display_name: string role: string } } interface PayrollCalculation { base_salary: number service_commissions: number total_tips: number total_earnings: number hours_worked: number } /** * @description Payroll management component with calculation, listing, and reporting features * @returns {JSX.Element} Complete payroll interface with period selection, staff filtering, and calculation modal * @audit BUSINESS RULE: Calculates payroll from completed bookings within the selected period * @audit BUSINESS RULE: Commission is 10% of service revenue, tips are 5% of service revenue * @audit SECURITY: Requires authenticated admin/manager role; staff cannot access payroll * @audit Validate: Ensures period dates are valid before calculation * @audit PERFORMANCE: Auto-sets default period to current month on mount * @audit AUDIT: Payroll records stored and retrievable for financial reporting */ export default function PayrollManagement() { const { user } = useAuth() const [payrollRecords, setPayrollRecords] = useState([]) const [selectedStaff, setSelectedStaff] = useState('') const [periodStart, setPeriodStart] = useState('') const [periodEnd, setPeriodEnd] = useState('') const [loading, setLoading] = useState(false) const [calculating, setCalculating] = useState(false) const [showCalculator, setShowCalculator] = useState(false) const [calculatedPayroll, setCalculatedPayroll] = useState(null) useEffect(() => { // Set default period to current month const now = new Date() const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0) setPeriodStart(format(startOfMonth, 'yyyy-MM-dd')) setPeriodEnd(format(endOfMonth, 'yyyy-MM-dd')) fetchPayrollRecords() }, []) const fetchPayrollRecords = async () => { setLoading(true) try { const params = new URLSearchParams() if (periodStart) params.append('period_start', periodStart) if (periodEnd) params.append('period_end', periodEnd) const response = await fetch(`/api/aperture/payroll?${params}`) const data = await response.json() if (data.success) { setPayrollRecords(data.payroll_records || []) } } catch (error) { console.error('Error fetching payroll records:', error) } finally { setLoading(false) } } const calculatePayroll = async () => { if (!selectedStaff || !periodStart || !periodEnd) { alert('Selecciona un empleado y período') return } setCalculating(true) try { const params = new URLSearchParams({ staff_id: selectedStaff, period_start: periodStart, period_end: periodEnd, action: 'calculate' }) const response = await fetch(`/api/aperture/payroll?${params}`) const data = await response.json() if (data.success) { setCalculatedPayroll(data.payroll) setShowCalculator(true) } else { alert(data.error || 'Error calculando nómina') } } catch (error) { console.error('Error calculating payroll:', error) alert('Error calculando nómina') } finally { setCalculating(false) } } const generatePayrollRecords = async () => { if (!periodStart || !periodEnd) { alert('Selecciona el período de nómina') return } if (!confirm(`¿Generar nóminas para el período ${periodStart} - ${periodEnd}?`)) { return } setLoading(true) try { const response = await fetch('/api/aperture/payroll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ period_start: periodStart, period_end: periodEnd }) }) const data = await response.json() if (data.success) { alert(`Nóminas generadas: ${data.payroll_records.length} registros`) fetchPayrollRecords() } else { alert(data.error || 'Error generando nóminas') } } catch (error) { console.error('Error generating payroll:', error) alert('Error generando nóminas') } finally { setLoading(false) } } const exportPayroll = () => { // Create CSV content const headers = ['Empleado', 'Rol', 'Período Inicio', 'Período Fin', 'Sueldo Base', 'Comisiones', 'Propinas', 'Total', 'Horas', 'Estado'] const csvContent = [ headers.join(','), ...payrollRecords.map(record => [ record.staff?.display_name || 'N/A', record.staff?.role || 'N/A', record.payroll_period_start, record.payroll_period_end, record.base_salary, record.service_commissions, record.total_tips, record.total_earnings, record.hours_worked, record.status ].join(',')) ].join('\n') // Download CSV const blob = new Blob([csvContent], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `nomina-${periodStart}-${periodEnd}.csv` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const getStatusColor = (status: string) => { switch (status) { case 'paid': return 'bg-green-100 text-green-800' case 'calculated': return 'bg-blue-100 text-blue-800' case 'pending': return 'bg-yellow-100 text-yellow-800' default: return 'bg-gray-100 text-gray-800' } } const formatCurrency = (amount: number) => { return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount) } if (!user) return null return (

Sistema de Nómina

Gestión de sueldos, comisiones y propinas

{/* Controls */} Gestión de Nómina
setPeriodStart(e.target.value)} />
setPeriodEnd(e.target.value)} />
{/* Payroll Records Table */} Registros de Nómina {payrollRecords.length} registros encontrados {loading ? (
Cargando registros...
) : payrollRecords.length === 0 ? (
No hay registros de nómina para el período seleccionado
) : ( Empleado Período Sueldo Base Comisiones Propinas Total Horas Estado {payrollRecords.map((record) => (
{record.staff?.display_name}
{record.staff?.role}
{format(new Date(record.payroll_period_start), 'dd/MM', { locale: es })} - {format(new Date(record.payroll_period_end), 'dd/MM', { locale: es })}
{formatCurrency(record.base_salary)} {formatCurrency(record.service_commissions)} {formatCurrency(record.total_tips)} {formatCurrency(record.total_earnings)}
{record.hours_worked.toFixed(1)}h
{record.status === 'paid' ? 'Pagada' : record.status === 'calculated' ? 'Calculada' : record.status === 'pending' ? 'Pendiente' : record.status}
))}
)}
{/* Payroll Calculator Dialog */} Cálculo de Nómina Desglose detallado para el período seleccionado {calculatedPayroll && (
Sueldo Base
{formatCurrency(calculatedPayroll.base_salary)}
Comisiones
{formatCurrency(calculatedPayroll.service_commissions)}
Propinas
{formatCurrency(calculatedPayroll.total_tips)}
Total
{formatCurrency(calculatedPayroll.total_earnings)}
Horas trabajadas: {calculatedPayroll.hours_worked.toFixed(1)} horas
)}
) }