"use client"; import { useState, useEffect, useCallback } from "react"; import { ArrowLeft, Loader2, Check, AlertTriangle, Calculator, Pencil } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { usePayrollSettings } from "@/lib/payroll-settings-hook"; import { calculatePayslip, toPeriod, type TaxJurisdiction, type PayslipCalculation } from "@/lib/payroll"; import { fetchEmployees } from "@/lib/supabase/employees-actions"; import { createPayrollRunWithPayslips, confirmPayrollRun } from "@/lib/supabase/payroll-actions"; import { type EmployeeWithRelations, EMPLOYMENT_TYPE_LABELS, } from "@/lib/supabase/employees-types"; import { formatDateShort } from "@/lib/supabase/payroll-types"; import { PayrollAnomalyAlert } from "@/components/ai/PayrollAnomalyAlert"; import PageSkeleton from "./PageSkeleton"; type Step = "period" | "review" | "confirm"; export default function PayrollNewPage({ onBack, onSuccess, }: { onBack: () => void; onSuccess: () => void; }) { const [step, setStep] = useState("period"); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [employees, setEmployees] = useState([]); const [periodStart, setPeriodStart] = useState(""); const [periodEnd, setPeriodEnd] = useState(""); const [payFrequency, setPayFrequency] = useState<"weekly" | "fortnightly" | "monthly">("monthly"); // Adjusted hours for hourly employees const [adjustedHours, setAdjustedHours] = useState>({}); const { jurisdiction, settings, loading: settingsLoading } = usePayrollSettings(); useEffect(() => { let cancelled = false; fetchEmployees() .then((emps) => { if (!cancelled) setEmployees(emps); }) .catch(() => {}); return () => { cancelled = true; }; }, []); // Calculate payslips for all active employees const payslipCalculations = jurisdiction ? employees .filter((e) => e.status === "active") .map((emp) => { const isHourly = emp.employment_type === "casual" || emp.employment_type === "contractor"; const hours = isHourly ? (adjustedHours[emp.id] ?? 38) : 0; const annualSalary = emp.salary ? Number(emp.salary) : isHourly && emp.hourly_rate ? Number(emp.hourly_rate) * hours * 52 : 0; if (annualSalary <= 0) return null; const calc = calculatePayslip({ grossAnnual: annualSalary, payFrequency, jurisdiction, }); return { employee: emp, hours, hourlyRate: emp.hourly_rate ? Number(emp.hourly_rate) : undefined, calculation: calc, }; }) .filter(Boolean) as { employee: EmployeeWithRelations; hours: number; hourlyRate?: number; calculation: PayslipCalculation; }[] : []; const totals = payslipCalculations.reduce( (acc, p) => ({ gross: acc.gross + p.calculation.grossPay, deductions: acc.deductions + p.calculation.totalDeductions, net: acc.net + p.calculation.netPay, employer: acc.employer + p.calculation.totalEmployerContributions, }), { gross: 0, deductions: 0, net: 0, employer: 0 } ); const handlePeriodSubmit = () => { if (!periodStart || !periodEnd) { setError("Please select both start and end dates"); return; } if (periodEnd < periodStart) { setError("End date must be after start date"); return; } setError(null); setStep("review"); }; const handleConfirm = async () => { if (!jurisdiction || !settings) return; setError(null); setSaving(true); try { const payslips = payslipCalculations.map((p) => ({ employee_id: p.employee.id, gross_pay: p.calculation.grossPay, total_deductions: p.calculation.totalDeductions, net_pay: p.calculation.netPay, employer_contributions: p.calculation.totalEmployerContributions, deduction_lines: p.calculation.deductions, employer_contribution_lines: p.calculation.employerContributions, hours_worked: p.hourlyRate ? p.hours : undefined, hourly_rate: p.hourlyRate, })); const result = await createPayrollRunWithPayslips( { period_start: periodStart, period_end: periodEnd, pay_frequency: payFrequency, employees: payslipCalculations.map((p) => ({ employee_id: p.employee.id })), }, jurisdiction.countryCode, jurisdiction.name, payslips ); if (!result.success) { setError(result.error || "Failed to create payroll run"); return; } onSuccess(); } catch (e: any) { setError(e.message || "An unexpected error occurred"); } finally { setSaving(false); } }; if (settingsLoading) return ; if (!jurisdiction) { return (

Run Payroll

Payroll jurisdiction not configured

Go to onboarding to set up your payroll settings.

); } return (
{/* Header */}

Run Payroll

{jurisdiction.name} · {jurisdiction.currency}

{/* Step indicator */}
{(["period", "review", "confirm"] as Step[]).map((s, i) => (
{["period", "review", "confirm"].indexOf(s) < ["period", "review", "confirm"].indexOf(step) ? : i + 1}
{i < 2 &&
}
))}
{error && (
{error}
)} {/* Step 1: Period selection */} {step === "period" && (

Pay Period

setPeriodStart(e.target.value)} className="mt-1" />
setPeriodEnd(e.target.value)} className="mt-1" min={periodStart} />
{/* Flat rate info for manual jurisdictions */} {settings && !settings.is_natively_supported && settings.flat_income_tax_rate != null && (

Flat income tax rate: {(settings.flat_income_tax_rate * 100).toFixed(1)}%

Employer pension: {((settings.employer_pension_rate || 0) * 100).toFixed(1)}% {settings.tax_free_allowance ? ` · Tax-free allowance: ${jurisdiction.currency} ${(settings.tax_free_allowance).toLocaleString()}` : ""}

)}
)} {/* Step 2: Review */} {step === "review" && (
{/* AI Anomaly Check */} {payslipCalculations.length > 0 && ( ({ employee_id: p.employee.id, employee_name: `${p.employee.first_name} ${p.employee.last_name}`, job_title: p.employee.job_title, gross_pay: p.calculation.grossPay, net_pay: p.calculation.netPay, hours_worked: p.hourlyRate ? p.hours : null, hourly_rate: p.hourlyRate || null, }))} period={`${payFrequency} (${formatDateShort(periodStart)} — ${formatDateShort(periodEnd)})`} /> )} {/* Summary bar */}
{[ { label: "Total Gross", value: `${jurisdiction.currency} ${totals.gross.toFixed(2)}` }, { label: "Total Deductions", value: `${jurisdiction.currency} ${totals.deductions.toFixed(2)}` }, { label: "Total Net", value: `${jurisdiction.currency} ${totals.net.toFixed(2)}` }, { label: "Employer Cost", value: `${jurisdiction.currency} ${totals.employer.toFixed(2)}` }, ].map((s) => (

{s.label}

{s.value}

))}
{/* Employee table */}
{payslipCalculations.map((p) => ( ))}
Employee Type Gross Deductions Net
{p.employee.first_name[0]}{p.employee.last_name[0]}

{p.employee.first_name} {p.employee.last_name}

{p.employee.job_title || ""}

{EMPLOYMENT_TYPE_LABELS[p.employee.employment_type]} {p.hourlyRate && (
setAdjustedHours((prev) => ({ ...prev, [p.employee.id]: parseFloat(e.target.value) || 0 }))} className="w-16 h-7 text-xs px-2" /> hrs
)}
{jurisdiction.currency} {p.calculation.grossPay.toFixed(2)} {jurisdiction.currency} {p.calculation.totalDeductions.toFixed(2)} {jurisdiction.currency} {p.calculation.netPay.toFixed(2)}
{/* Actions */}
)} {/* Step 3: Confirm */} {step === "confirm" && (

Confirm Payroll Run

{payslipCalculations.length} employees · {jurisdiction.currency} {totals.net.toFixed(2)} total net pay

{formatDateShort(periodStart)} — {formatDateShort(periodEnd)} · {payFrequency}

{/* Deduction breakdown for first employee (preview) */} {payslipCalculations.length > 0 && (

Sample deduction breakdown ({payslipCalculations[0].employee.first_name} {payslipCalculations[0].employee.last_name})

{payslipCalculations[0].calculation.deductions.map((d) => (
{d.name} {jurisdiction.currency} {d.amount.toFixed(2)}
))}
Net Pay {jurisdiction.currency} {payslipCalculations[0].calculation.netPay.toFixed(2)}
)}
)}
); }