// PayrollRunViewer.jsx import React, { useMemo, useState } from "react"; import { saveAs } from "file-saver"; // optional, include if you have file-saver; otherwise CSV download fallback below import Swal from "sweetalert2"; /** * Props: * - payrollRows: array (the payload from payroll.php) * - log: object (meta about the payroll run, e.g. { date_from, date_until, id, created_at }) * - onClose: function */ export default function PayrollRunViewer({ payrollRows = [], log = null, onClose = () => {} }) { const [expandedEmployee, setExpandedEmployee] = useState(null); // safe parser helpers const floatVal = (v) => { if (v === null || typeof v === "undefined" || v === "") return 0; const n = parseFloat((typeof v === "string") ? v.replace(/,/g, "") : v); return isNaN(n) ? 0 : n; }; const rewardTotalFor = (row) => { if (typeof row.total_rewards !== "undefined" && row.total_rewards !== null) return floatVal(row.total_rewards); if (Array.isArray(row.rewards)) { return row.rewards.reduce((s, r) => s + floatVal(r.amount ?? r.applied_amount ?? r.value ?? 0), 0); } return 0; }; const allowancesTotalFor = (row) => { if (typeof row.total_allowances !== "undefined" && row.total_allowances !== null) return floatVal(row.total_allowances); if (Array.isArray(row.allowances)) { return row.allowances.reduce((s, a) => s + floatVal(a.applied_amount ?? a.amount ?? 0), 0); } return 0; }; const computeRowGross = (row) => { // prefer half_month_salary when present (monthly half cut) const half = floatVal(row.half_month_salary || 0); if (half > 0) return half; // otherwise compute: base (basic_salary * total_days) + allowances + incentives + retro_applied + (rewards - deduction_oneoff) const base = floatVal(row.basic_salary) * floatVal(row.total_days); const allowances = allowancesTotalFor(row); const incentives = floatVal(row.total_incentives || 0); const retroApplied = floatVal(row.total_retro_applied || 0); const rewards = rewardTotalFor(row); const deductionOneoff = floatVal(row.deduction_oneoff || 0); return base + allowances + incentives + retroApplied + (rewards - deductionOneoff); }; const computeRowDeductions = (row) => { const phil = floatVal(row.philhealth_employee_share || 0); const sss = floatVal(row.sss_employee_share || 0); const pag = floatVal(row.pagibig_employee_share || 0); // loan deduction prefer actual if exists const loan = floatVal(row.loan_deduction_actual || row.loan_deduction || 0); // include late deduction for monthly payrolls (if backend set it) const late = floatVal(row.late_deduction || 0); // include one-off payroll deduction (already subtracted from gross extras in our gross calc, but show separately) const oneoff = floatVal(row.deduction_oneoff || 0); const total = phil + sss + pag + loan + late + oneoff; return { phil, sss, pag, loan, late, oneoff, total }; }; const computeRowNet = (row) => { const gross = computeRowGross(row); const ded = computeRowDeductions(row); const net = gross - ded.total; return net; }; // memoized totals across the payroll run const totals = useMemo(() => { let grossSum = 0, deductionsSum = 0, netSum = 0; for (const r of payrollRows) { const g = computeRowGross(r); const d = computeRowDeductions(r); const n = g - d.total; grossSum += g; deductionsSum += d.total; netSum += n; } return { grossSum, deductionsSum, netSum, employeesCount: payrollRows.length }; }, [payrollRows]); // eslint-disable-line // CSV exporter (very simple) const downloadCSV = () => { const headers = [ "payroll_id","employee_id","name","position_name","department_name", "gross","deductions","net","total_days","total_rendered_hours" ]; const rows = payrollRows.map(r => { const gross = computeRowGross(r).toFixed(2); const ded = computeRowDeductions(r).total.toFixed(2); const net = (parseFloat(gross) - parseFloat(ded)).toFixed(2); return [ r.payroll_id, r.employee_id, `"${(r.name || "").replace(/"/g,'""')}"`, (r.position_name || ""), (r.department_name || ""), gross, ded, net, r.total_days || 0, r.total_rendered_hours || 0 ].join(","); }); const csvContent = [headers.join(","), ...rows].join("\n"); const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); // try file-saver first try { if (typeof saveAs === "function") { saveAs(blob, `payroll_${log?.date_from || "from"}_${log?.date_until || "until"}.csv`); } else { const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `payroll_${log?.date_from || "from"}_${log?.date_until || "until"}.csv`; link.click(); } } catch (e) { // fallback const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `payroll_${log?.date_from || "from"}_${log?.date_until || "until"}.csv`; link.click(); } }; const copyJsonToClipboard = async () => { try { await navigator.clipboard.writeText(JSON.stringify({ log, payrollRows }, null, 2)); Swal.fire({ icon: "success", title: "Copied", text: "Payroll JSON copied to clipboard", timer: 1200, showConfirmButton: false }); } catch (err) { Swal.fire({ icon: "error", title: "Copy failed", text: String(err) }); } }; return (

Payroll: {log ? `${new Date(log.date_from).toLocaleDateString()} → ${new Date(log.date_until).toLocaleDateString()}` : "Payroll Run"}

Employees: {totals.employeesCount} • Generated: {log?.created_at ? new Date(log.created_at).toLocaleString() : "—"}

{/* Totals */}
Total Gross
₱{totals.grossSum.toFixed(2)}
Total Deductions
₱{totals.deductionsSum.toFixed(2)}
Total Net
₱{totals.netSum.toFixed(2)}
{/* Employee cards */}
{payrollRows.map((row) => { const gross = computeRowGross(row); const deductions = computeRowDeductions(row); const net = gross - deductions.total; const isExpanded = expandedEmployee === row.employee_id; return (
{row.name}
ID: {row.employee_id}
{row.employee_type || row.salary_type || ""}
{row.position_name} • {row.department_name}
Gross
₱{gross.toFixed(2)}
Deductions
₱{deductions.total.toFixed(2)}
Net
₱{net.toFixed(2)}
Total Days: {row.total_days}
Rendered Hours: {row.total_rendered_hours}
Incentive: ₱{floatVal(row.total_incentives || 0).toFixed(2)}
{/* Expanded details */} {isExpanded && (
{/* Deductions breakdown */}
SSS
₱{deductions.sss.toFixed(2)}
PhilHealth
₱{deductions.phil.toFixed(2)}
Pag-IBIG
₱{deductions.pag.toFixed(2)}
{/* Loan & one-off */}
Loan Deduction
₱{deductions.loan.toFixed(2)}
One-off Deduction
₱{deductions.oneoff.toFixed(2)}
Late Deduction
₱{deductions.late.toFixed(2)}
{/* Attendance table */}
Attendance Records
{Array.isArray(row.attendance_records) && row.attendance_records.length > 0 ? (
{row.attendance_records.map((a, i) => ( ))}
Date Rendered Hrs Credited Days Net Work Mins Schedule
{a.attendance_date} {floatVal(a.total_rendered_hours).toFixed(2)} {a.days_credited} {a.net_work_minutes} {a.schedule ? (
{a.schedule.shift_name}
{a.schedule.start_time} → {a.schedule.end_time} ({a.schedule.total_minutes} mins)
) : No schedule}
) : (
No attendance records
)}
{/* Loans detail */}
Loans
{Array.isArray(row.loans) && row.loans.length > 0 ? (
{row.loans.map((ln) => (
Loan #{ln.loan_id} {ln.loan_reference_no ? `(${ln.loan_reference_no})` : ""}
Balance: ₱{parseFloat(ln.balance || 0).toFixed(2)}
{ln.description}
{Array.isArray(ln.journal_entries) && ln.journal_entries.length > 0 && (
Journal entries ({ln.journal_entries.length})
{ln.journal_entries.map((je) => (
{je.entry_date} • {je.origin}
₱{floatVal(je.amount).toFixed(2)}
))}
)}
))}
) : (
No loans
)}
)}
); })}
); }