# Project: PG7 Payroll HR System

PHP/MySQL/Bootstrap 5/jQuery/DataTables — no framework.

---

## RULE: Always use existing helpers first

Before writing any raw HTML, SQL, or JS for common patterns, **check the helpers below and use the existing one**. Never duplicate what these functions already do.

---

## PHP Helpers — `functions.php`

### UI / Rendering

#### `renderActionButton($action, $label, $icon, $customClass, $buttonClass, $extraAttrs)`
Use for **any action button** in tables or forms. Never write a raw `<button>` without checking this first.
```php
echo renderActionButton('edit',   'Edit',   'bi-pencil', '', 'btn-primary');
echo renderActionButton('delete', '',       'bi-trash',  '', 'btn-danger');
// $action     — sets name="btn-{action}", data-action, data-type
// $label      — optional text label (omit for icon-only)
// $icon       — Bootstrap Icons class (bi-*)
// $customClass — extra CSS classes
// $buttonClass — btn-* color class
// $extraAttrs  — raw HTML attribute string e.g. 'data-id="1" disabled'
```

#### `renderNavButton($href, $label, $pageKey, $currentPage, $icon)`
Use for sidebar/nav link buttons. Automatically adds `active` class.

#### `renderTableLoader($tableContent, $loaderId)`
Wraps content in the standard spinner + table-container shell.
```php
echo renderTableLoader($myTableHtml, 'my-loader');
```

#### `renderComingSoon($title, $icon)`
Use for unfinished module pages. Never build a "coming soon" card manually.

#### `renderTitle()`
Outputs the current page `<title>` string based on `$_GET['p']`. Always use this — never hardcode page titles.

#### `renderDate($dateInput)`
Formats a date as `"Monday, January 6"`. Accepts a DateTime object or string.

#### `hEsc($value)`
HTML-safe escape for output in attributes or text nodes. Alias for `htmlspecialchars`. Use everywhere you output untrusted values.

#### `short_name($name, $max, $ellipsis)`
Truncates a string with ellipsis. Default max = 12 chars.

#### `renderError($logMessage, $httpCode, $context, $msg)`
Use for **all error responses** in API endpoints. Logs to file, sets HTTP code, echoes JSON, and exits.
```php
renderError("Employee not found.", 404, "myEndpoint", "Record not found.");
```
Never `echo json_encode(['error'=>...])` manually.

#### `respond($response)`
`echo json_encode($response); exit;` — use for all successful JSON responses.

---

### Auth / Session

#### `bootApiSession()`
Call at the top of **every API endpoint** instead of `session_start()`. Starts session + enforces login (JSON 401 on failure).

#### `enforceLogin()`
Redirects to login for page (HTML) endpoints if not logged in.

#### `getCsrfToken()`
Returns the session CSRF token. Write it into `<meta name="csrf-token">` on HTML pages.

#### `verifyCsrfToken()`
Validates the CSRF token in POST endpoints. Call before processing any state-changing request.

#### `requireRole(array $allowedRoles, $context)`
RBAC gate. Emits JSON 403 and halts if the session user's role isn't in `$allowedRoles`.
```php
requireRole([ROLE_ADMIN]);
requireRole([ROLE_TK, ROLE_ADMIN]);
```
Role constants: `ROLE_EMP`, `ROLE_SV`, `ROLE_TK`, `ROLE_PO`, `ROLE_ADMIN`

#### `fail($devMsg, $status, $userMsg)`
Throws `AppHttpException` — used inside try/catch blocks in `_save` endpoints for clean transactional error handling.

---

### Input / Validation

#### `getSanitizedQueryParam($key, $default)`
Safely reads a GET param (trimmed, lowercased). Returns `$default` if missing.

#### `getRequiredQueryParam($key, $allowed)`
Like above but throws a 400 error if missing or not in `$allowed` array.

#### `nullIfEmpty($v)`
Returns `null` for empty/blank strings, otherwise trims and returns the value.

#### `requireEnum($value, $allowed, $fieldName)`
Validates a value is in an allowed set. Throws `Exception` on failure.

#### `requireDate($date, $fieldName)`
Validates and normalizes a `Y-m-d` date string. Throws on invalid format.

#### `isValidDateYmd($value)`
Returns `bool` — validates a `YYYY-MM-DD` string.

#### `isValidTimeHHMMSS($t)`
Returns `bool` — validates a `HH:MM:SS` string.

#### `getRowIndicesFromPost($prefix)`
Extracts numeric row indices from POST keys like `prefix_1`, `prefix_2`, etc.

---

### Type Coercion

#### `toFloatOrZero($v)` / `toIntOrZero($v)` / `toStrOrNull($v)`
Safe type coercions. Use instead of casting with `(float)`, `(int)`, etc. when input could be null or empty.

#### `nullIfEmpty($v)`
Trims and returns `null` for empty strings.

#### `safeDate($val)`
Converts any date value to `Y-m-d` or returns empty string.

---

### Formatting / Display

#### `money($n)`
`number_format($n, 2)` — formats a number as currency string with 2 decimal places.

#### `num2($n)`
`number_format($n, 1)` — 1 decimal place.

#### `numFmt($n, $decimals, $point)`
Like `number_format` but no thousands separator. Default 2 decimals.

#### `dowLabel($dow)`
Returns abbreviated weekday name from day-of-week integer (0=Sun…6=Sat). e.g. `"Mon"`.

#### `formatDateDisplay($value)`
`"2025-01-15"` → `"01/15/2025"`.

#### `formatTimeDisplay($value)`
Returns `H:i:s` display string.

#### `formatDateTimeDisplay($value)`
Converts datetime to Manila timezone and formats as `"m/d/Y h:i A"`.

#### `minutesToHms($minutes)` / `minutesToHm($minutes)`
Converts total minutes to `HH:MM:SS` or `HH:MM`. Supports negatives.

#### `secondsToHms($seconds)`
Converts total seconds to `HH:MM:SS`. Supports negatives.

#### `getCurrentTimestampInTimezone($timezone)`
Returns current datetime string in Manila time (default).

---

### Employee / Name

#### `formatPersonName($row, $mode)`
Builds a display name from an employee row `{f_name, m_name, l_name, suffix}`.
- `"last_first"` (default) → `"LastName, FirstName"`
- `"full"` → `"FirstName MiddleName LastName Suffix"`

#### `short_name($name, $max, $ellipsis)`
Truncates long names for compact display.

#### `userTitle($userType)`
Returns the human-readable role label for a `pu_type` value.

---

### Database

#### `executeQuery($conn, $query, $types, $params, $throwOnError)`
Wrapper for all DB queries. Returns rows array (SELECT), affected rows (INSERT/UPDATE/DELETE), or `false`.
```php
$rows = executeQuery($conn, "SELECT * FROM employees WHERE id = ?", "i", [$id], true);
```
Never write raw `$conn->prepare()` / `$stmt->bind_param()` chains — always use this.

#### `getSysSetting($conn, $key, $default)`
Reads a value from `payroll_sys_config` key-value table.

#### `getTableColumns($conn, $table)`
Returns an associative array of column names for a table.

#### `normalizeResult($rows)`
Normalizes a `mysqli_result` object or array to a plain array.

#### `asRows($result)`
Safe cast: returns `[]` if `$result` is not an array.

---

### Employee Dropdown Options

#### `listDesignations($conn)`
Returns `<option>` HTML for all distinct designations.

#### `listDepartments($conn, $selected)`
Returns `<option>` HTML for active departments with optional pre-selected value.

#### `listEmployeeOptions($conn, $mode, $includeInactive)`
Returns `<option>` HTML for employees.
- `$mode`: `'id'` (value=employee_id), `'name'` (value=name), `'id_name'` (value="id - name")

---

### Color / CSS

#### `normalizeHexColor($value)`
Normalizes a color input to a valid `#rrggbb` / `#rgb` / `rgb()` CSS value.

---

### Attendance / DTR (use for timekeeping features)

- `attDeriveScheduleForEmployeeDate($conn, $employeeId, $workDate)` — full schedule derivation
- `attBuildEmployeeDtrSeedValues($conn, $employeeId, $workDate)` — builds DTR row seed
- `attEnsureEmployeeDtrRange($conn, $employeeId, $cutoffStart, $cutoffEnd)` — upsert DTR rows for a date range
- `attEnsureEmployeeDtrRangeForUiRefresh(...)` — same but does NOT overwrite manual edits
- `attFetchTimecardRowByEmployeeDate($conn, $employeeId, $workDate)` — fetch timecard
- `attResolvePayrollConsistentLateAndUndertime(...)` — computes late/undertime minutes
- `attComputeLateMinutesForDerivedSchedule($schedule, $firstClockInTs)` — late minutes from schedule
- `attNormalizeEmployeeDtrOtHours($totalOtMinutes)` — normalizes OT (enforces ≥30 min minimum)
- `attFetchBreakMinutesForTimesheet($conn, $timesheetId)` — total break minutes from break rules

---

## JS Helpers — `js/app.js`

### Button State (always use for action buttons)

#### `showProcessing($btn)`
Disables button, shows spinner. Call on AJAX start.

#### `showSuccess($btn, originalText, noLabel)`
Shows success state (checkmark + green). Call on `res.success === true`.

#### `showFormError({ msg, btn, originalText, resMessage, revert, delay })`
Shows warning state + error message. Call on `res.success === false`.

#### `showAjaxError($btn)`
Shows danger state for network/server errors. Call in `$.ajax error:`.

#### `showCompleted($btn, originalText, $msg, delay)`
Resets button back to idle state after a delay. Call in `$.ajax complete:`.

#### `setBtnIconState($btn, state)`
Directly sets icon-mode button state: `"idle"` | `"processing"` | `"success"`.

#### `toggleAllButtons(disabled, exceptBtn)`
Disables/enables all non-modal buttons on the page. Always pair with AJAX start/complete.

---

### AJAX Form Wiring (use instead of writing $.ajax manually)

#### `wireAjaxFormButton(selector, getConfig)`
Handles the full "click → AJAX → UI feedback" pattern. Use for all save/submit buttons.
```js
wireAjaxFormButton('[name="btn-save"]', ($btn) => ({
    url: 'my_save.php',
    formSelector: '#myForm',
    contextParam: p,
    doReloadTable: true,
    onSuccess: (res, $btn, $form) => { /* extra success logic */ }
}));
```
Key config options: `url`, `formSelector`, `data`, `method`, `contextParam`, `extraData`, `doReloadTable`, `reloadTarget`, `onSuccess`, `onAfterSuccess`, `onError`, `formErrorRevert`, `useFormData`, `closeModalOnSuccess`, `closeModalDelay`

- `closeModalOnSuccess` — auto-hide the modal after the success state shows. Pass `true` to hide the modal containing the clicked button, or a selector string (e.g. `".modal-set-password"`) to target a specific modal. Default `false`.
- `closeModalDelay` — ms to wait after success before auto-closing. Default `1200`.

#### `wireModalOpener(selector, getConfig)`
Handles "click → show modal with loader → optional AJAX prefetch" pattern.
```js
wireModalOpener('[data-action="edit"]', ($btn) => ({
    modalSelector: '#myModal',
    ajax: { url: 'fetch.php', data: { id: $btn.data('id') }, onSuccess: (res, $btn, $modal) => { /* populate */ } }
}));
```

---

### Table Rendering (JS-side)

#### `renderBtn(action, label, icon, customClass, buttonClass, extraAttrs)`
JS equivalent of PHP `renderActionButton()`. Use inside `populateXxxTable()` functions.
```js
renderBtn('edit',   'Edit',   'bi-pencil', '',        'btn-white')
renderBtn('delete', 'Delete', 'bi-trash3', 'my-del',  'btn-danger')
```

#### `renderEmptyRow(colspan, icon, label, hint)`
Renders a full-width "no data" table row. Use in every `populateXxxTable()` when rows array is empty.
```js
$tb.append(renderEmptyRow(6, 'bi-inbox', 'No records found.'));
```

#### `renderStatusBadge(status)`
Returns a Bootstrap badge `<span>` styled by status string (`approved`, `pending`, `rejected`, `active`, `paused`, `closed`, `cancelled`, `inactive`).

---

### Table Reload

#### `reloadTable(p, view, new_id, callback)`
Central dispatcher — reloads the correct table for the given page key. Always use this, never call the specific `reloadXxxTable()` functions directly from feature code.

---

### URL / Hash

#### `getUrlParameter(name)`
Reads a query string parameter from the current URL.

#### `getHashParams()`
Returns `URLSearchParams` from the current URL hash.

#### `setHashParam(key, value)`
Sets a hash param without adding a history entry. Use to persist tab/employee selection.

#### `withHashParam(href, key, value)`
Returns a modified hash string — use to build `href` attributes.

---

### Employee Panel

#### `loadEmployee(employee_id)`
Loads employee data via AJAX and populates the right panel. Handles abort of in-flight requests.

#### `setActiveEmployeeRow(employee_id)`
Marks the correct `<tr#view-employee>` as active.

#### `setEmployeeHeader(emp)`
Populates `#HeaderId` and `#HeaderName` from an employee object.

#### `autoSelectEmployeeOnLoad(p)`
Called after table reload — auto-selects the employee from hash or first row.

---

### DTR Helpers

#### `populateDTRForm(dtr)`
Populates `#frmDtr` inputs from a DTR rows array.

#### `computeDtrCutoffSummary(rows)`
Computes late/undertime/grace/deductible totals from DTR rows array.

#### `renderDtrCutoffSummary(rows)`
Computes and renders the DTR summary panel in `#frmDtr`.

#### `runWithoutDirtyTracking(callback, formsToClear)`
Wraps programmatic form population so it doesn't trigger the unsaved-changes guard.

---

### Pending Approvals

#### `populatePendingOtFields($modal, data)`
Populates the Pending OT approval modal table.

#### `populatePendingLoaFields($modal, data)`
Populates the Pending LOA approval modal table.

#### `populatePendingDedFields($modal, data)`
Populates the Pending Deduction approval modal table.

#### `buildPendingOtRemHtml(rem, canApprove, tooltipText)`
Renders the REM cell content with optional tooltip icon for pending OT rows.

---

### Other Utilities

#### `escapeHtml(str)`
HTML-escapes a value for safe inline use. Use for all dynamic values in template literals.

#### `fmtMoney(v)`
Formats a number with 2 decimal places and locale thousands separator.

#### `apprvFlagToStatus(flag)`
Converts `0/1/2` approval flags to `"pending"` / `"approved"` / `"rejected"`.

#### `isDateInRange(date, start, end)`
Returns `bool` — checks if a date falls within a range (inclusive, time-normalized).

#### `appendExtraToFormData(fd, extraData)`
Appends extra fields (object or query string) to a `FormData` instance.

#### `appendToHash(isSuccess)`
Appends `#=success` or `#=fail` to the current URL after form submission.

#### `resetModal($modal)`
Resets all forms inside a modal and clears dynamic state.

#### `refreshPendingOtBadge()` / `refreshPendingLoaBadge()` / `refreshPendingDedBadge()`
Refreshes the topbar notification badge counts. Call after approvals/rejections.

#### `setFormDirty(formSelector)` / `clearFormDirty(formSelector)` / `confirmUnsavedFormChanges(options)`
Unsaved-changes guard system. Use `runWithoutDirtyTracking()` when populating forms programmatically.

---

## Conventions

- Bootstrap 5 classes for all layout and UI
- Bootstrap Icons (`bi bi-*`) for all icons
- DataTables for all data tables
- jQuery for all JS interactions
- All API endpoints must call `bootApiSession()` at the top
- All POST endpoints must call `verifyCsrfToken()` before processing
