# Plant List Module

**Folder:** `CRUD/plant_list/` · **Main table:** `plant_list` (271 rows — 151 active, 120 archived) · **Reached from:** navbar → Plant Asset List

The master asset register. Every machine the company owns has one row here, keyed by **fleet number** (`fleet_Number`, e.g. `H217`). It carries the machine's identity, specification flags, certification expiry dates, service history and running hours. The hire list, PDI, LOLER and service systems all read from it.

Biggest categories: Excavator (133), Attachment (97), Dumper (22).

> Documented to the format set by [sites.md](sites.md).

---

## How the pieces fit together

```mermaid
flowchart TD
    I[Plant Asset List<br/>plantlist_index.php] --> V[View Machine<br/>plantlist_view.php]
    I --> C[Add Machine<br/>plantlist_create.php]
    I --> U[Edit Machine<br/>plantlist_update.php]
    I --> H[Update Hours<br/>plantlist_update_hrs.php]

    U -.same form, different Back link.-> UHL[plantlist_update_hl.php<br/>from Hire List]
    U -.same form, different Back link.-> UPL[plantlist_update_pl.php<br/>from Plant List on-hire]

    V --> PDI[PDI History<br/>plant_pdi.php]
    V --> W[Works / Repairs<br/>plant_list_works/]

    LU[Upload LOLER Cert<br/>loler_upload.php] --> PL[(plant_list<br/>cert file + expiry)]
    LS[LOLER Cert Search<br/>loler_cert_search.php] --> PL
    AL[LOLER Alerts<br/>plant_loler_alert.php] --> PL

    W --> WD[Works Documents<br/>works_document_upload.php]
```

---

## Plant Asset List — `plantlist_index.php`

![Plant Asset List](../screenshots/plant__asset_list.png)

The asset register as a DataTable: fleet number, model, category, serial, certification dates and service info. Row actions link to view, edit, PDI history and works.

> All output on this page is HTML-escaped via a local `h()` helper — this was tightened during the [July security review](../security-audit-2026-07.md), as the table previously rendered database values raw.

## Add Machine — `plantlist_create.php`

![Add Machine](../screenshots/plant__create.png)

Creates a `plant_list` row. Fleet number is the key the rest of the portal joins on.

**Field groups:** identity (fleet number, model, serial, registration, category / sub-category / weight category, purchase date), **fitted equipment flags** (tracker, 360 cameras, rails, quick hitch, immobiliser, height/slew limiter, rubber tracks, rotate circuit, bio oil, fire extinguisher), **certification** (LOLER and quick-hitch expiry dates), **service** (last serviced date and hours, next service due, DPF cleaned), **hours** (machine hours + date recorded), and PIN codes (master / depot).

## View Machine — `plantlist_view.php`

![View Machine](../screenshots/plant__view.png)

Read-only detail for one machine (`?id_plant_list=<id>`). This is also where **non-manager staff are sent** — drivers and fitters can look up hours, PDI and certificate info but cannot edit.

## Edit Machine — `plantlist_update.php`

![Edit Machine](../screenshots/plant__update.png)

The full edit form (`?id_plant_list=<id>`). Manager-only: any other role is redirected to `plantlist_view.php`.

> **Three variants of this page exist** — `plantlist_update.php`, `plantlist_update_hl.php` (entered from the Hire List) and `plantlist_update_pl.php` (entered from Plant List on-hire). They differ mainly in where Back/Save return to. `_hl` and `_pl` are ~92% identical to each other. See [Known issues](#known-issues).

## Update Hours — `plantlist_update_hrs.php`

![Update Hours](../screenshots/plant__update_hours.png)

A cut-down form for the common job of recording current machine hours without opening the full edit page. Writes `Machine_hours` and `Machine_hours_date_recorded`.

## PDI History — `plant_pdi.php`

Pre-delivery inspection history for a machine. **Takes `?fleet_Number=<fleet>`, not an id** — unlike every other page in this module.

## Delete Machine — `plantlist_delete.php`

Hard delete. Machines are normally **archived** instead (`plant_archive = 'Y'`) so historic hires still resolve — 120 of the 271 rows are archived.

## Backup Plant — `backup_plant.php`

Records a backup/replacement machine against a hire when the primary machine is unavailable.

---

## LOLER & certification

Lifting certificates are legally required and expiry-tracked. Three pages cover it:

### Upload LOLER Certificate — `loler_upload/loler_upload.php`
![LOLER Upload](../screenshots/plant__loler_upload.png)

Drag-and-drop uploader with preview. Pick the machine, certificate type (**Lifting `LC`**, **Quick Hitch `QH`**, **Fork `FK`**) and expiry date; the file is stored in `CRUD/plant_list/LCs/` named `<fleet>_<code>_<ddmmyy>.<ext>`, the old certificate for that machine/type is removed, and `plant_list` is updated with the new filename and expiry.

The machine dropdown flags status inline — **LOLER OVERDUE**, **DUE SOON** (within a month) or **NO LOLER DATE** — so you can see what needs doing while uploading.

### LOLER Certificate Search — `loler_cert_search.php`
![LOLER Search](../screenshots/plant__loler_search.png)

Search stored certificates (`?q=`).

### LOLER Alerts — `plant_loler_alert.php`
Generates the expiry warnings surfaced elsewhere in the portal.

### Legacy upload — `LCs/uploadLC.php`
The original plain uploader, kept on the [Legacy Uploads](../../CRUD/legacy_uploads.php) page. Prefer `loler_upload.php`, which names and files the certificate for you.

---

## Plant Works (repairs / modifications)

Work carried out on a machine — table `plant_list_works` (3 rows), with attachments in `plant_list_works_documents`.

| Page | Purpose |
|---|---|
| `plant_list_works/plantlistworks_index.php` | List of works records |
| `plant_list_works/plantlistworks_create.php` | Raise a works record (`?id_plant_list=<id>`) |
| `plant_list_works/plantlistworks_update.php` | Edit a works record |
| `plant_list_works/plantlistworks_delete.php` | Delete a works record |
| `plant_list_works/plantlist_create.php` | Machine picker used when raising works |

![Plant Works](../screenshots/plant__works_index.png)

Documents attach via `works_document_upload.php` (validated upload), with `works_documents_helper.php` / `works_documents_modal.php` providing the UI.

---

## AJAX / helper endpoints (not user-facing)

| File | Method | Purpose |
|---|---|---|
| `loler_upload/loler_upload.php` | POST `action=getPlants` | Machine list + cert status for the uploader dropdown |
| `plantlist_update.php` | POST `fetch_subcategories` | Sub-category options for the selected category |
| `plant_list_works/works_document_upload.php` | POST | Upload a works document; returns JSON |
| `plant_list_works/works_documents_helper.php` | include | Works document UI + scripts |
| `plant_list_works/works_documents_modal.php` | include | Works document viewer modal |
| `plant_loler_alert.php` | include | LOLER expiry alert generation |

## Database

| Table | Rows | Notes |
|---|---|---|
| `plant_list` | 271 | Master asset register (151 active / 120 archived) |
| `plant_list_audit` | 18,208 | Full row snapshot per change — see note below |
| `plant_list_works` | 3 | Works / repair records |
| `plant_list_works_documents` | 1 | Attachments against a works record |
| `plant_category` / `plant_category_types` / `plant_category_weights` | 242 / 52 / 19 | Category lookup tables |
| `plant_specifications` / `plant_specification_models` | 51 / 39 | Spec sheets (see Plant Specs module) |

Key relationships:
- `hire_list.fleet_number` matches `plant_list.fleet_Number` **by code, not by id**
- `Lifting_Cert_File` / `Hitch_Cert_File` hold filenames inside `CRUD/plant_list/LCs/`
- `plant_archive = 'Y'` hides a machine from pickers without deleting history

> **Audit note:** like `hire_list_audit`, `plant_list_audit` stores **post-change (NEW) snapshots**. The value *before* a change lives in the preceding snapshot, not the row written by that change. `audit_changed_by` comes from `COALESCE(@app_user, USER())`, which `session.php` sets per request.

---

## Known issues

Found during the review pass on 2026-07-21.

| # | Issue | Status |
|---|---|---|
| 1 | `plant_list_works/plantlistworks_index.php` was **completely broken** — a blank line before `<?php` meant `declare(strict_types=1)` was not the first statement, producing a hard fatal on every load. Nothing links to the page, so it went unnoticed. | **Fixed** — leading blank line removed; page now renders. Also made its `display_errors` environment-aware |
| 2 | Local dev left `$_SESSION['role']` unset, so localhost behaved as a *roleless* user and every manager-only page redirected away (e.g. all three plant update pages). Large parts of the portal were untestable locally. | **Fixed** in `CRUD/session.php` — local dev now gets a `manager` identity. Gated by `portal_is_local_request()` (loopback `SERVER_ADDR`), so it can never apply on live |
| 3 | Three near-duplicate edit pages: `plantlist_update.php` (1,360 lines), `_hl` (1,189) and `_pl` (1,184). `_hl` and `_pl` differ by only ~101 lines — roughly 92% identical. A fix applied to one will not reach the others. | **Open** — same clone pattern as `customers_update_ins.php`; worth collapsing into one page with a `return` parameter |
| 4 | `plant_pdi.php` takes `?fleet_Number=` while every other page in the module takes `?id_plant_list=`. Easy to get wrong when linking. | **Open** — cosmetic but inconsistent |
| 5 | `loler_cert_widget.php` (301 lines) is unreferenced — nothing includes it. | **Open** — candidate for deletion |
| 6 | Two different files named `plantlist_create.php` exist (`plant_list/` and `plant_list_works/`) doing different jobs. Confusing when searching. | **Open** — rename candidate |
