# Hire List Module

**Folder:** `CRUD/hire_list/` · **Main table:** `hire_list` · **Reached from:** navbar → Hire List

The spine of the portal. A **hire** links a machine ([plant_list](plant-list.md)) to a customer ([customers](customers.md)) at a site ([sites](sites.md)) for a period of time, and almost everything else in the system hangs off it — PODs, timesheets, purchase orders, invoices, off-hires and site moves.

Nearly every page takes **`?id_hire_list=<id>`**.

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

---

## The hire lifecycle

```mermaid
flowchart LR
    N[Need<br/>need/] --> HL[Hire List<br/>hire_list.php]
    HL --> DEL[Delivered<br/>delivery_actions/delivered.php]
    DEL --> ON[On hire]
    ON --> TC[To Collect<br/>to_collect.php]
    TC --> COL[Collected<br/>delivery_actions/collected.php]
    COL --> OH[Off Hire<br/>hire_offhire/offhire.php]
    OH -.mistake.-> CAN[Cancel Off-Hire<br/>hire_offhire_cancel/]
    ON --> SM[Site Move<br/>CRUD/site_move/]
    ON --> CH[Chargeable Works<br/>chargeable/]
    YL[Yard Ready List<br/>yard_list.php] --> HL
    LR[Lorry Runs<br/>lorry_runs/] --> DEL
```

## The list pages

Five views onto the same `hire_list` table, each for a different job:

| Page | Purpose |
|---|---|
| `hire_list.php` | **The main view** — all current hires, delivery status, PO/POD links |
| `plantlist_onhire.php` | Where every machine currently is (machine-centric view) |
| `to_collect.php` | Machines awaiting collection |
| `yard_list.php` | Machines checked and ready to go out |
| `xh_list.php` | Cross-hire — kit hired **in** from other companies |

![Hire List](../screenshots/hire__hire_list.png)

![Yard Ready List](../screenshots/hire__yard_list.png)

The Yard Ready List groups available excavators by weight class, using `CRUD/functions/PlantData.php`.

## Editing a hire — `hire_updates/`

![Update Hire](../screenshots/hire__update.png)

The main edit form is `hire_updates/update.php` (`?id_hire_list=<id>`). Several variants exist for entering the form from different places or for different job types:

| Page | Entered from / used for |
|---|---|
| `update.php` | The main Hire List |
| `update_pl.php` | The Plant List on-hire view |
| `update_move_sd.php` | Self-drive site move |
| `update_move_op.php` | Operated site move |
| `update_move_standing.php` | Standing (idle) site move |
| `update_move_XH.php` | Cross-hire site move |
| `tocollect_update.php` | The To Collect view |
| `history_modal_update.php` | The history modal |

> These eight files total **~10,800 lines** and overlap heavily — see [Known issues](#known-issues).

**Business rule:** moving a hire to a non-operated job type (Self drive, Site move, Sale) **clears the driver name**. Only *Operated* and *Operator only* carry a driver. Enforced server-side in `CRUD/functions/hire_rules.php` because the forms only *hide* the driver field, and a hidden input still posts its value. The previous driver stays recoverable from `hire_list_audit`.

## Delivery actions — `delivery_actions/`

Small GET handlers that advance a hire's status, linked as icons from the list pages:

| Handler | Effect |
|---|---|
| `delivered.php` | Mark delivered |
| `collected.php` | Mark collected |
| `confirmed.php`, `pl_delivered.php`, `pl_collected.php`, `pl_confirmed.php` | Unreferenced variants — see Known issues |

> These are guarded by `block_cross_site_request()` (added in the [security review](../security-audit-2026-07.md)) because they perform state changes on a plain GET.

## Off hire — `hire_offhire/` and `hire_offhire_cancel/`

![Off Hire](../screenshots/hire__offhire.png)

`offhire.php` (and `offhire_pl.php` from the plant view) end a hire and set the off-hire date. `op_only/offhire_oponly.php` handles operator-only hires. Off-hires can be reversed from `hire_offhire_cancel/`.

Off-hire validates that the off-hire date is **not before the on-hire date**, rejecting with a message rather than saving.

## Need — `need/`

![Need](../screenshots/hire__need.png)

Records kit that is **needed** but not yet allocated.

| Page | Purpose |
|---|---|
| `need/need.php` | Raise a single requirement |
| `need/need_multi.php` | Raise several at once |
| `need/need_update.php` | Edit a requirement (`?id_hire_list=`) |
| `need/create_from_hirelist.php` | Create a need from an existing hire |

## Operator Only — `op_only/`

![Operator Only](../screenshots/hire__operator_only.png)

Hires where only an operator is supplied. `operator_only.php` lists them, `update_oponly.php` edits, `offhire_oponly.php` off-hires. Operator hours feed the operated-invoice calculation.

## Chargeable works — `chargeable/`

![Chargeable](../screenshots/hire__chargeable.png)

Extra chargeable items raised against a hire (damage, extra kit, callouts) — standard create/read/update/delete set.

## Lorry runs — `lorry_runs/`

![Lorry Runs](../screenshots/hire__lorry_runs.png)

Daily transport planning: which lorry and driver moves which machine.

| File | Type | Purpose |
|---|---|---|
| `lorry_runs.php` | Page | The daily run planner |
| `lorry_runs_weeklyplanner.php` | Page | Week view |
| `lorry_runs_weeklyplanner_load.php` / `_save.php` | API | Load/save the week plan |
| `get_drivers.php` | API | Driver list for the dropdowns (client-side) |
| `lorry_load_jobs.php` | API | Jobs available to load onto a run |
| `lorry_run_create.php` / `lorry_run_handler.php` | API | Create and manage runs |

![Weekly Planner](../screenshots/hire__weekly_planner.png)

## LOLER sending — `send_loler/`

Sends a machine's LOLER certificate to a customer contact.

| File | Purpose |
|---|---|
| `loler_send_modal.php` | The send dialog (contact picker) |
| `fetch_loler_emails.php` | Contact email lookup (JSON) |
| `loler_make_outlook_eml.php` | Builds an Outlook `.eml` (POST only — a GET correctly returns 405) |
| `loler_send_whatsapp.php` | WhatsApp share link |

## Shared internals — `internal_function/`

| File | Purpose |
|---|---|
| `documents_panel.php` | The PO / delivery-note / POD tab panel on a hire |
| `additions_module.php` | Bucket and attachment selection |
| `excavator_availability.php` | Availability logic used by the yard list |
| `update_XHOH_status.php` | Cross-hire off-hire status endpoint |

## Database

Main table **`hire_list`** — see [database-schema.md](../database-schema.md). Key columns and links:

- `fleet_number` → `plant_list.fleet_Number` (**by code, not id**)
- `customer_acc` / `customer_acc_id` → `customers`
- `id_site_address` → `site_address`
- `purchase_order` → the PO document is found **by filename**: `/CRUD/customers/POs/<purchase_order>.pdf`
- `job_type_name` — Operated, Self drive, Site move, Operator only, Sale
- `off_hire_number` — `'On hire'` while live; set on off-hire
- `drivers_name` — only populated for Operated / Operator only

> **Audit:** `hire_list_audit` stores a **full row snapshot per change**, recording **NEW** values. The value *before* a change lives in the preceding snapshot. `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 | **`yard_list.php` was completely broken** outside production. `CRUD/functions/PlantData.php` had **10 SQL references hardcoded to the live database name** (`hickeyhub_hickeyplant.plant_list`, `.hire_list`, `.site_address`, `.contact_list`), so on local/test it fatalled with *"table doesn't exist"*. It only worked on live by coincidence of the schema being named that. | **Fixed** — schema prefixes removed so queries use the connection's own database. No behaviour change on live (unqualified names resolve to the same schema); local/test now work. No other file hardcodes a DB name |
| 2 | `lorry_runs.php` emitted 4 warnings per load — `Undefined variable $drivers` then `foreach() argument must be of type array\|object, null given`, twice. The server-side driver loops were **dead code**: `loadDrivers()` replaces the select's `innerHTML` wholesale from `get_drivers.php`. | **Fixed** — dead loops removed |
| 3 | `op_only/offhire_oponly.php` emitted 3 warnings — `$options2`, `$options`, `$opcus` undefined. They are stale names; `functions_loader.php` provides `$opsfleetno`, `$customerOptions` etc. The sibling `offhire.php` renders the same three fields as plain readonly inputs. | **Fixed** — undefined echoes removed (display-only fields, zero behaviour change) |
| 4 | **Large-scale duplication — the worst in the portal.** `hire_delete/`: `delete.php`, `delete_pl.php`, `delete_tc.php` are 169 lines each and differ by **exactly 6 lines — all of them the return URL**. `hire_offhire_cancel/`: three files of 156 lines differing by 10. `hire_updates/`: 8 files totalling ~10,800 lines, where `update_move_sd` vs `update_move_standing` differ by only 77 lines (~93% identical) and `sd` vs `op` by 101. | **Open** — a `?return=` parameter would collapse the delete and off-hire-cancel families to one file each. The `update_move_*` family is a bigger job |
| 5 | Six `delivery_actions/` handlers exist but only `delivered.php` and `collected.php` are linked. `confirmed.php`, `pl_delivered.php`, `pl_collected.php`, `pl_confirmed.php` are unreferenced. | **Open** — likely superseded; verify then delete |
| 6 | Other unreferenced files: `hire_updates/tocollect_update.php` (1,279 lines), `hire_updates/history_modal_update.php` (1,063), `hire_delete/delete_pl.php`, `chargeable/chargeable_read.php`, `lorry_runs/lorry_run_confirm.php`, `send_loler/send_loler_email.php`. | **Open** — several are large and recently edited, so confirm before removing |
