// 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 (
Employees: {totals.employeesCount} • Generated: {log?.created_at ? new Date(log.created_at).toLocaleString() : "—"}
| 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)
|