# Payroll deduction breakdown and UI revision Status: implementation complete and automated checks passing; authenticated browser review remains pending. Scope: the deduction panel in `frontend/src/payrollPage/payrollComponents/deductionTemplate.jsx` and its surrounding Payroll Details view. This document is the design blueprint. Track implementation in the [separate execution checklist](PAYROLL-DEDUCTION-BREAKDOWN-CHECKLIST.md). ## 0. Discovery and context inspection - `PayrollPage.jsx` supplies the selected payroll to `payrollList.jsx` and `payrollLayoutDropdown.jsx`; the latter renders `IncomeTaxEditor`, `ContributionTypeSelector`, and `DeductionTemplate`. The detail dialog already has a scrollable body. - `deductionTemplate.jsx` shows employee SSS, Pag-IBIG, PhilHealth, employer shares, `LoanDetailsMultiApply`, a separate `loan_deduction_actual` line, and a total built from the three employee shares, loan, and `ca_deduction`. It does not visibly break down tax, attendance penalties, or one-off deductions. - `LoanDetailsMultiApply.jsx` distinguishes saved cutoff drafts from posted loans and shows individual loan amounts where the API supplies them. Drafts do not affect a loan balance or journal until finalization. - `backend/payroll/payroll.php` exposes employee share fields, `gov_deduction_total_cached`, `income_tax_withheld`, loan amounts, draft summary, `total_deductions`, and `net_salary`. Its `total_deductions` expression is government employee share + withholding tax + applied loan amount. The live response normalizes `has_loan_draft` and `loan_draft_amount`; the finalized response sets those draft fields to false and zero. - `backend/payroll/finalize_payroll_from_live_cache.php` subtracts late, absence, undertime, unpaid leave, government contributions, tax, and the applied loan once when calculating final net. Employer shares are company cost and never reduce employee net pay. Cash advance and one-off fields do not appear in that final-net function, so the revised UI shows them as recorded information outside payroll-deduction and attendance totals. - `deductionBreakdown.js` now preserves the API total as authoritative and keeps a zero-value draft separate from an applied loan amount. Its focused tests cover aggregate reconciliation and the zero-draft case. - Existing PHP endpoints, tenant authorization, payroll roles, loan draft audit, and override handlers remain the sources of truth. The per-loan draft edit and revert actions described below reuse these existing workflows; no direct loan-balance or journal mutation is introduced. ### Display contract to validate before coding | Group | Fields or source | Display rule | | --- | --- | --- | | Government employee share | Three `*_employee_share_applied_cached` fields, with existing fallbacks | Show each amount, schedule/mode, and subtotal. Compare with `gov_deduction_total_cached`; flag a mismatch for investigation rather than silently changing payroll. | | Withholding tax | `income_tax_withheld`, `income_tax_mode` | Show actual withheld amount and mode; keep the existing editor. | | Loan applied or posted | `loan_applied_amount`, `loan_deduction_actual`, loan detail rows | Label with payroll state. Show the amount that belongs in the payroll total exactly once. | | Loan draft | `has_loan_draft`, `loan_draft_amount`, per-loan `draft_amount` | Show separately as a proposed amount before finalization. A zero or absent draft must not erase an existing applied amount. Never label a draft as posted. | | Attendance adjustments | `late_deduction`, `absent_deduction`, `undertime_deduction`, `unpaid_leave_deduction` | Show four lines and their own subtotal. Reconcile them to final net separately from API `total_deductions`; do not subtract them twice. | | Cash advance and one-off | `ca_deduction`, `deduction_oneoff` and compatible aliases | Show when populated, but only include in a net-pay total after confirming the actual backend calculation and API data for this payroll state. Mark unconfirmed amounts as informational. | | Employer share | Three `*_employer_share_applied_cached` fields | Keep in a distinct company-cost section, outside every employee deduction total. | ## 1. User flow and use cases - **Actor:** payroll reviewer with existing access to Payroll Details. **Precondition:** a live preview or finalized payroll row is selected. - **Happy path:** open Payroll Details, scan a summary, expand or read government, tax, loan, attendance, and other line items, then compare the displayed totals with the payroll net amount. Existing authorized override and loan-editor actions remain available. - **Alternatives:** zero amounts remain intelligible; missing data is shown as unavailable rather than fabricated; saved draft, skipped loan, and posted loan have distinct labels; a discrepancy between detail and authoritative payroll totals is visible without silently rewriting figures. A payroll fetch failure uses the existing parent error and retry behavior. - **Acceptance:** every supported deduction category has a named row; the displayed payroll-deduction subtotal reconciles to `total_deductions` or shows a discrepancy; attendance is shown separately; employer cost and proposed loan drafts are never mixed into a posted employee subtotal; mobile rows have no horizontal clipping; finalized records expose no enabled edit action. ## 2. Database architecture and ERD | Change | Table | Relationship and relevant columns | Keys, indexes, rollback, compatibility | | --- | --- | --- | --- | | ALTER | None | Display reads existing payroll data. | No migration or rollback required. | | CREATE | None | Display reads existing payroll data. | No migration or rollback required. | | READ | `payroll_live_cache`, `payroll_finalized` | One row per employee, tenant, and cutoff/batch; existing payroll API shapes the view. | Use existing primary/tenant-period lookup indexes. No extra query is planned, so there is no new index or downtime requirement. | | READ | `payroll_loan_deduction_drafts`, `loans` | A draft belongs to a source payroll preview and loan in a company/brand/period; posted data belongs to finalized payroll. | Existing unique draft scope and tenant/period/loan indexes in the 2026-09-24 migration are sufficient for this view. | Data flow: `payroll_live_cache` or `payroll_finalized` -> `backend/payroll/payroll.php` -> selected payroll object -> `DeductionTemplate`; loan details are attached to that payroll object. Revert the frontend changes to roll back the UI. No production data or schema will be changed. ## 3. RESTful PHP API plan | Method and route | Purpose | Payload / response | Errors and controls | | --- | --- | --- | --- | | Existing payroll read route used by `PayrollPage.jsx` (`backend/payroll/payroll.php`) | Supply current or finalized payroll deduction fields | Existing scoped query parameters; existing payroll JSON row. Confirm required fields are present for both modes before UI integration. | Preserve current auth, tenant scope, error format, and cache behavior. If a field is missing, do not infer a financial amount from a different field. | | Existing loan draft summary route (`GET /api/loan_api/get_loan_summary`) | Supply editor data in `PayrollLoanEditor` | Employee and cutoff scope; existing loan state, draft amount, balance, and projected balance. The deduction panel currently reads `payroll.loans` supplied by the payroll row; confirm both sources agree before relying on per-loan draft amounts. | Preserve current validation and authorization. | | Existing draft mutation routes (`POST /api/loan_api/update_loan_summary`, `POST /api/loan_api/skip_loan_deduction_draft`) | Save an edited cutoff draft or record an audited skip from the loan card | Selected loan ID, employee/period context, positive amount, and a required skip reason. Both return the existing cache-refresh payload. | Existing payroll guard, tenant context, open-period checks, optimistic version checks when supplied, and finalization restrictions remain authoritative. No route or schema contract changes. | No new endpoint is planned. The detail view reuses the existing loan-draft mutation routes described above. If discovery in checklist item 2 shows a required amount is absent, document the precise field and extend only the existing read response with a backward-compatible nullable field. Use the existing standardized JSON error behavior; do not introduce a second payroll total calculation in PHP merely for presentation. ## 4. React frontend flow and UX - Keep `DeductionTemplate` inside `payrollLayoutDropdown`. Present a compact summary at the top, then clearly separated employee deductions, attendance adjustments, loan detail, and employer company cost. - Proposed information order: ```text Deduction breakdown Live / Finalized Payroll deductions (API total) amount Government contributions subtotal SSS / Pag-IBIG / PhilHealth applied employee share each Withholding tax amount + mode Loan applied to payroll amount + state Per-loan cards draft / skipped / posted details Attendance adjustments subtotal Late / absence / undertime / unpaid leave amount each Other recorded amounts cash advance / one-off, if supplied Employer contributions (company cost) separate subtotal and detail ``` Keep the existing tax editor and contribution settings above the panel. Place company cost after employee-impact sections so a reviewer can scan employee figures without mistaking employer expense for a deduction. Keep the per-loan editor action close to each loan card. - Label totals by what they actually include: API payroll deductions (government + tax + applied loan), attendance adjustments, and company cost. A combined employee-impact total is allowed only after it reconciles with the authoritative net formula. Proposed loan drafts stay outside posted/applied totals. - Use `total_deductions` as the authoritative payroll-deduction figure and the line-item sum as a reconciliation check. Do not replace the API value with a locally calculated sum when they disagree. Attendance adjustments are additional reductions in the net-pay formula, outside API `total_deductions`. Show cash advance and one-off values as recorded information until their payroll effect is confirmed. - Use the implemented pure breakdown mapper for field fallback, currency rounding, and reconciliation metadata. It never sums duplicate aliases or a loan-detail subtotal on top of an aggregate loan amount. - Use existing prop and `onSaved`/`payroll:refresh` synchronization. After an override or loan edit, refresh the affected payroll row and recompute the breakdown from the new row. Avoid stale local copies and unnecessary network requests. - Mobile first: label and amount stay paired; rows wrap instead of clipping, while dense per-loan detail may collapse into cards. Use semantic headings and groups, visible focus states, descriptive button labels, and sufficient contrast. - Existing override dialogs need keyboard access, Escape handling, focus trap, initial focus, and focus return to the trigger. Preserve disabled controls for finalized payroll. Use the parent loading/error states where available; show a clear empty state for missing loan details. - The panel displays a single employee and cutoff, so virtualization, pagination, and debounced search are unnecessary. Avoid memoization unless measurement shows a rendering problem. ## 5. Environment, flags, and dependencies No new `.env` variable, feature flag, or package is expected. Use the installed React, Lucide, Tailwind, Vitest, and Testing Library stack. The change must remain compatible with existing live and finalized payroll responses and can be reverted by reverting frontend files. ## 6. Audit and compliance Opening or expanding the breakdown is read-only and creates no audit event. Employee/employer contribution overrides and loan draft edits retain their current authenticated API and immutable audit behavior. The UI must never call a loan posting, journal, or finalization mutation while displaying details. A discrepancy indication should include the compared field names for support, without exposing sensitive employee data in browser logs. ## 7. Testing and QA - Unit-test the mapper with applied live loans, finalized loans, zero drafts, recorded fields, and mismatched aggregate fixtures. Assert that employer shares and draft proposals never enter employee totals and that no alias is counted twice. - Component-test visible line items, section headings, scoped totals, and the absence of the legacy total. Use the repository's Vitest and Testing Library. - Regression-check the existing loan editor, override save/refresh, tax editor, and finalized read-only journal view. Review a real payroll API payload without changing payroll data. - Run targeted tests, ESLint on changed files, and the frontend build. Manually inspect mobile and desktop viewports and compare displayed numbers with `total_deductions` and `net_salary` for one live and one finalized row. Record any known backend mismatch instead of hiding it. ## 8. Applied-loan detail repair — employee 10844 The supplied database export contains live payroll row `32931` for employee `10844`, covering `2026-08-25` through `2026-09-10`. Its three payroll loan columns all contain `3904.44`. The cached `loans_json` contains five applied rows totaling that amount: `944.40`, `500.04`, `960.00`, `1000.00`, and `500.00`. The defect was in `LoanDetailsMultiApply.jsx`: it rendered only loans with a positive current balance or an active open-ended state. Four applied fixed-term rows had a zero cached balance, so the panel hid them even though their amounts were included in payroll. The revised component retains every loan with a positive amount applied to the current payroll and presents these groups: - **Included in this payroll**: the aggregate applied amount, which is part of employee deductions. - **Saved draft changes**: proposals that do not change the included amount until they are applied by the payroll workflow. - **Per-loan cards**: scheduled amount, included or posted amount, remaining balance, and original loan amount. No database data, table, API route, or loan state is changed. The fix reads the existing `loans_json` payload returned by `GET /api/payroll`. ## 9. Per-loan live-payroll edit and revert This is a medium-scope extension of the detail view. It changes the existing period-scoped loan-draft workflow only; it does not change a finalized payroll, loan balance, or journal entry directly. ### Confirmed behavior - **Edit** opens a per-loan dialog for a live payroll and saves a positive draft deduction through the existing `POST /api/loan_api/update_loan_summary` route. The payload is limited to the selected loan, employee, and displayed cutoff dates. - **Revert** first checks existing loan journals for the selected cutoff. A preview-only amount uses `POST /api/loan_api/skip_loan_deduction_draft`, which records an audited skip. A posted, unfinalized payroll credit uses `POST /api/loan_journal_entry_api/undo_unfinalized_payroll_deduction`, which reverses the journal, restores the fixed-term loan balance, and refreshes the live preview. Both paths require a reason. - A closed or cancelled loan still cannot be saved as a new payroll deduction. It can be reverted when a stale live-cache amount needs correction: the skip route records an omission only and deliberately bypasses the add-deduction balance/status validation. - Finalized payrolls expose neither action. Server-side session, payroll-guard, tenant, date, and loan validation continue to decide authorization; hidden or disabled client controls do not grant permission. - Both successful actions dispatch the existing `payroll:refresh` event so the parent reloads authoritative payroll data. The component does not calculate or persist a replacement aggregate locally. - The detail dialog also applies the returned server refresh totals and skipped state immediately. This prevents a second revert click from sending an outdated draft version while the parent refresh is still in flight. ### UX and validation - Each loan card with an included or scheduled amount has **Edit draft** and **Revert this cutoff** actions while the payroll is live. The card remains the source of the scheduled, included, and remaining-balance context needed to make the decision. - Open-ended loans have a dedicated badge and use **Status: Ongoing** instead of a currency balance, so reviewers do not mistake them for fixed-term loans that are paid off. - The parent loan-deduction total stays visible while individual loan rows are collapsed by default. Expanding the rows shows only the per-cutoff amount, applied amount, and balance or status. - Attendance-related adjustments are shown only when at least one adjustment has a non-zero amount; zero-value rows do not add noise to the payroll review. - The edit dialog has a labeled currency input, displays the selected loan and cutoff, requires an amount greater than zero, prevents duplicate saves while the request is pending, and returns focus to its trigger when closed. - Revert uses a confirmation dialog with a required reason and explains that it changes the live preview for this cutoff only. This makes the financial consequence reviewable and provides an audit reason without offering an unsafe delete operation. - The existing API response and parent refresh determine the post-save state. Network and validation errors remain visible in the dialog rather than being converted into a local optimistic balance. ### Data, API, and rollout impact | Area | Change | Compatibility and risk control | | --- | --- | --- | | Database | None. Existing `payroll_loan_deduction_drafts` records and audit behavior are reused. | No migration, backfill, lock, or rollback operation is required. | | API | Existing `update_loan_summary` and `skip_loan_deduction_draft` routes are called from the detail view. | Reuse current authenticated validation and tenant scope. No endpoint contract changes. | | Posted journal undo | Existing `read_journal_entries` and `undo_unfinalized_payroll_deduction` routes distinguish and reverse an unfinalized posted payroll credit. | The endpoint rejects journals linked to finalized payroll; finalized corrections remain a payroll-batch void operation. | | Frontend | `DeductionTemplate` owns modal/request state; `LoanDetailsMultiApply` renders optional, callback-based actions. | Keep finalized records read-only, refresh from server after mutation, and retain existing editor for broader loan administration. | | Audit | Edit and skip routes retain their existing reason/audit records. | Revert is modeled as an auditable skip, never as deletion of historical loan activity. | ## 10. Final handoff format When all items in the [execution checklist](PAYROLL-DEDUCTION-BREAKDOWN-CHECKLIST.md) are `[x]`, provide one concise report: files changed, database migrations executed (expected: none), endpoints added or modified (expected: none; existing draft routes are reused), deduction components and data flow, exact verification commands/results, and any documented mismatch that still needs a backend decision.