> **SUPERSEDED — HISTORICAL RECORD ONLY (archived 2026-07-18).** This document is part of the original "superpowers" wayleave build, whose design diverged from the projects-module patterns it should have mirrored. Do NOT use it as a pattern source for wayleave or any new module. The wayleave module is being realigned to the projects module (meta-driven fields, OpenLayers, shared JS) — see CLAUDE.md.

# Wayleave Module V2 Refactor Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Retrofit the freshly-built wayleave module so it visually and functionally matches the V2 pattern used by `projectedit` (see `html_body_projectedit.php`, `project_edit_v2.js`, `project_edit_v2_fileuploads.js`), addressing every point raised in `2026-04-22-wayleave-module-feedback.md`.

**Architecture:** Re-skin the editor shell from Bootstrap `nav-tabs` to the V2 `project-header-bar` + `horizontal-nav` + `.content-panel` structure. Extend backend loaders so the dropdown/autocomplete fields resolve to human-readable labels, merge the standalone "Land Registry" tab into "Coverage", split dynamic fields into one tab per `agreement_field_sections.section_name`, move file uploads to the V2 modal + card pattern (backed by `fn/file_upload.php` + `fn/serve_file.php`), and rebuild the Journal & Audit Log as a Tabulator view with prev/new values by UNIONing the per-type `*_history` tables with LAG().

**Tech Stack:** PHP 7+ / PDO / PostgreSQL (existing `wayleave.*` schema — see `sql/wayleave_01_core.sql` through `sql/wayleave_05_file_uploads_integration.sql`), vanilla JS + jQuery 3.6, Bootstrap 5, Leaflet 1.9 + Leaflet-Geoman, Tabulator 5, jQuery UI autocomplete wired via `fn/autocomplete.php` and `fn/user_search.php`.

**Codebase Conventions to Respect:**
- No build system. Edit `.css`/`.js` directly; ignore `www/less/` and `www/scss/`.
- No test framework. Every task ends with a manual browser verification step; screenshots are optional.
- All AJAX endpoints live in `www/fn/` and follow the shape `echo json_encode(['success'=>true/false, ...])`.
- SQL files live in the `/sql` subdirectory. New DDL goes in a new file (do not rewrite older files) — older module schemas are off-limits.
- Commit after every task with a Conventional-Commits style message (`feat(wayleave):`, `fix(wayleave):`, `refactor(wayleave):`, `chore(wayleave):`).
- Do **not** touch `www/fn/wayleave_save.php` dynamic-field detection, the coverage polygon endpoints, or the underlying `wayleave.agreement_field_*` schema — they already work.

--- 

## File Structure

### New files

- `sql/wayleave_06_v2_refactor.sql` — adds the `wayleave.teams` lookup, plus any seed data the refactor needs.
- `www/css/wayleave_edit_v2.css` — V2 editor styles (replaces `wayleave_edit.css`).
- `www/js/wayleave_edit_v2.js` — V2 editor behaviour (replaces `wayleave_edit.js`).
- `www/js/wayleave_edit_v2_fileuploads.js` — mirrors `project_edit_v2_fileuploads.js` for the wayleave entity.
- `www/html/html_body_wayleaveedit.php` — V2 body template (replaces `www/html/wayleave_edit.php`).
- `www/fn/wayleave_audit_log_load.php` — prev/new value audit log (UNION of per-type history tables + journal + access log).
- `www/fn/wayleave_file_delete.php` — soft-delete a file attachment (the V2 modal exposes a delete button).
- `www/fn/wayleave_autocomplete.php` — dedicated autocomplete endpoint for agreement-name (parent agreement search) and wayleave-title lookups.

### Modified files

- `www/fn/global_functions.php` — flip the `wayleaveedit` route to use `html_header_nav_v2.php`, new html/js/css filenames, and bump `'version' => ['2']`.
- `www/fn/wayleave_load.php` — join friendly labels for `bd_manager → users.username`, `account_id → accounts.account_name`, `parent_agreement_id → wayleave.agreements.agreement_name`, expose `teams` lookup list, return dynamic-field values keyed by `field_form_id` instead of `field_id`, and include file attachments row list.
- `www/fn/wayleave_save.php` — accept `wayleave_team` as the dropdown's text value (no schema change; still stored in `wayleave.agreements.wayleave_team`) and tolerate the fact that autocomplete fields now arrive as `_name` + `_id` pairs.
- `www/fn/wayleave_premises_load.php`, `www/fn/wayleave_projects_load.php`, `www/fn/wayleave_releases_load.php` — no behaviour change, but confirm shape matches Tabulator `headerFilter`.
- `www/fn/autocomplete.php` — add a `wayleavename` type so the Parent Agreement input can autocomplete.
- `unusedfiles.md` — add the legacy wayleave v1 files (superseded after cutover).

### Deleted / retired at the end

- `www/css/wayleave_edit.css`, `www/js/wayleave_edit.js`, `www/html/wayleave_edit.php` — replaced after a cutover task. Do **not** delete until Task 14 confirms parity.

---

## Task 1: Add the wayleave.teams lookup + seed data

**Why this task:** Feedback item _"Wayleave team field should be a dropdown"_. No existing lookup table exists; `wayleave.agreements.wayleave_team` is a free-text `varchar(100)`. Introduce a lookup table the editor can read to populate the dropdown, while keeping the text column so existing rows and `wayleave_save.php`'s history logging stay valid.

**Files:**
- Create: `sql/wayleave_06_v2_refactor.sql`

- [ ] **Step 1: Create the new SQL file with the teams lookup**

Create `sql/wayleave_06_v2_refactor.sql`:

```sql
-- sql/wayleave_06_v2_refactor.sql
-- V2 refactor support: wayleave teams lookup.
-- Run: psql -U postgres -d netplanner -f sql/wayleave_06_v2_refactor.sql

BEGIN;

CREATE TABLE IF NOT EXISTS wayleave.teams (
    id            serial PRIMARY KEY,
    team_name     varchar(100) NOT NULL UNIQUE,
    display_order integer NOT NULL DEFAULT 0,
    is_active     boolean NOT NULL DEFAULT true,
    created_datetime timestamp NOT NULL DEFAULT now()
);

-- Seed teams from any distinct values already sitting in agreements.wayleave_team,
-- then add a starter set so the dropdown isn't empty on a fresh install.
INSERT INTO wayleave.teams (team_name, display_order)
SELECT DISTINCT wayleave_team, 100
FROM wayleave.agreements
WHERE wayleave_team IS NOT NULL AND wayleave_team <> ''
ON CONFLICT (team_name) DO NOTHING;

INSERT INTO wayleave.teams (team_name, display_order) VALUES
    ('Internal',       10),
    ('External',       20),
    ('Legal',          30),
    ('Property',       40)
ON CONFLICT (team_name) DO NOTHING;

COMMIT;
```

- [ ] **Step 2: Run the migration**

Run: `psql -U postgres -d netplanner -f sql/wayleave_06_v2_refactor.sql`
Expected: `COMMIT` with no errors.

- [ ] **Step 3: Verify the table**

Run: `psql -U postgres -d netplanner -c "SELECT id, team_name, display_order FROM wayleave.teams ORDER BY display_order;"`
Expected: at minimum the 4 seeded rows appear.

- [ ] **Step 4: Commit**

```bash
git add sql/wayleave_06_v2_refactor.sql
git commit -m "feat(wayleave): add wayleave.teams lookup for team dropdown"
```

---

## Task 2: Flip the `wayleaveedit` route to V2 filenames

**Why this task:** Feedback item _"layout of the wayleave module follows the v1 layout … should have been identified using routing in `global_functions.php`"_. Before editing the template we re-point the router to V2 filenames so the new template loads and the old one stays reachable while we build (keep the legacy file until cutover in Task 14).

**Files:**
- Modify: `www/fn/global_functions.php:689-696`

- [ ] **Step 1: Update the route to V2**

Find the current `wayleaveedit` entry around line 689:

```php
'wayleaveedit' => [
    'head_nav' => ['html/html_header_nav.php'],
    'nav_html' => [''],
    'html' => ['html/wayleave_edit.php'],
    'css' => ['css/wayleave_edit.css'],
    'js' => ['js/wayleave_edit.js'],
    'version' => ['1']
],
```

Replace with:

```php
'wayleaveedit' => [
    'head_nav' => ['html/html_header_nav_v2.php'],
    'nav_html' => [''],
    'html' => ['html/html_body_wayleaveedit.php'],
    'css' => ['css/wayleave_edit_v2.css'],
    'js' => ['js/wayleave_edit_v2.js', 'js/wayleave_edit_v2_fileuploads.js'],
    'version' => ['2']
],
```

(The exact surrounding keys match the `projectedit` route at `global_functions.php:673-688`.)

- [ ] **Step 2: Create a placeholder V2 body file so the route still renders**

Create `www/html/html_body_wayleaveedit.php` with a single-line placeholder body so `index.php` does not 500 while the real template is being built:

```php
<?php
$agreement_id = isset($_GET['agreement_id']) ? intval($_GET['agreement_id']) : 0;
?>
<main class="p-0" id="wl-editor-root" data-agreement-id="<?= $agreement_id ?>">
    <div class="alert alert-warning m-3">Wayleave editor V2 template is being built (placeholder).</div>
</main>
```

Create empty placeholder assets so the route does not 404:
- `www/css/wayleave_edit_v2.css` — write a single `/* wayleave_edit_v2.css */` comment line.
- `www/js/wayleave_edit_v2.js` — write a single `// wayleave_edit_v2.js` comment line.
- `www/js/wayleave_edit_v2_fileuploads.js` — write a single `// wayleave_edit_v2_fileuploads.js` comment line.

- [ ] **Step 3: Verify the route loads**

Open `http://localhost/index.php?do=wayleaveedit&agreement_id=1` in the browser.
Expected: the yellow placeholder alert renders, the V2 header nav (with the GeoLynx brand + global search) is visible at the top. Browser DevTools Network tab shows `css/wayleave_edit_v2.css` and `js/wayleave_edit_v2.js` loaded with HTTP 200.

- [ ] **Step 4: Commit**

```bash
git add www/fn/global_functions.php www/html/html_body_wayleaveedit.php www/css/wayleave_edit_v2.css www/js/wayleave_edit_v2.js www/js/wayleave_edit_v2_fileuploads.js
git commit -m "refactor(wayleave): switch wayleaveedit route to v2 filenames"
```

---

## Task 3: Extend `wayleave_load.php` to return labels + teams + form-id-keyed values

**Why this task:** Feedback items _"Dropdown fields do not populate from saved data"_ and _"Fields like BD Manager, Account, Parent ID, Stocklist ID should be text search based on names and populate a hidden field with ID"_. The current loader returns only IDs. Adjust the loader to:
1. JOIN labels for every FK the form shows (`bd_manager → users.username`, `account_id → accounts.account_name`, `parent_agreement_id → wayleave.agreements.agreement_name`).
2. Return the wayleave teams list so the dropdown has options.
3. Return dynamic field values keyed by `field_form_id` — the V2 JS will populate inputs with `$('#'+formId).val(value)` like `project_edit_v2.js` does, which fails today because the values are keyed by numeric `field_id`.
4. Return the file attachments rows directly (so the Attachments tab has its data the moment the page loads, same as `projectedit`).

**Files:**
- Modify: `www/fn/wayleave_load.php`

- [ ] **Step 1: Replace the static agreement query with a label-joined version**

Find the `$sqlA` block (lines 23-33). Replace with:

```php
$sqlA = "SELECT a.*,
                s.description           AS status_description,
                u_bd.username           AS bd_manager_name,
                acc.account_name        AS account_name,
                parent.agreement_name   AS parent_agreement_name
         FROM   wayleave.agreements          a
         LEFT JOIN wayleave.agreement_status s       ON s.id       = a.agreement_status_id
         LEFT JOIN users.users               u_bd    ON u_bd.id    = a.bd_manager
         LEFT JOIN accounts.accounts         acc     ON acc.account_id = a.account_id
         LEFT JOIN wayleave.agreements       parent  ON parent.agreement_id = a.parent_agreement_id
         WHERE  a.agreement_id = :aid AND a.is_deleted = false";
```

- [ ] **Step 2: Add the teams lookup query immediately after the status query**

After the `$statuses` block (around line 36), add:

```php
// Wayleave teams (dropdown options)
$stmt  = $dbh->query("SELECT id, team_name FROM wayleave.teams WHERE is_active = true ORDER BY display_order, team_name");
$teams = $stmt->fetchAll(PDO::FETCH_ASSOC);
```

- [ ] **Step 3: Re-key dynamic values by `field_form_id`**

Replace the `$sqlV` block (lines 65-83) with a version that joins `agreement_fields` to the UNION so the response is keyed by the form-id string:

```php
$sqlV = "
    WITH v AS (
        SELECT agreement_id, field_id, field_value::text AS field_value, 'text'    AS dtype FROM wayleave.agreement_field_values_text
        UNION ALL
        SELECT agreement_id, field_id, field_value::text,                 'int'     FROM wayleave.agreement_field_values_int
        UNION ALL
        SELECT agreement_id, field_id, field_value::text,                 'numeric' FROM wayleave.agreement_field_values_numeric
        UNION ALL
        SELECT agreement_id, field_id, field_value::text,                 'date'    FROM wayleave.agreement_field_values_date
        UNION ALL
        SELECT agreement_id, field_id, field_value::text,                 'boolean' FROM wayleave.agreement_field_values_boolean
    )
    SELECT f.field_form_id, v.field_id, v.field_value, v.dtype
    FROM   v
    JOIN   wayleave.agreement_fields f ON f.field_id = v.field_id
    WHERE  v.agreement_id = :aid";
$stmt = $dbh->prepare($sqlV);
$stmt->bindValue(':aid', $agreement_id, PDO::PARAM_INT);
$stmt->execute();
$valRows = $stmt->fetchAll(PDO::FETCH_ASSOC);

$values       = [];  // field_form_id => value      (for V2 DOM population)
$valuesByFid  = [];  // field_id      => value      (legacy)
foreach ($valRows as $v) {
    $values[$v['field_form_id']] = $v['field_value'];
    $valuesByFid[$v['field_id']] = $v['field_value'];
}
```

- [ ] **Step 4: Add a file attachments query**

After the journal query block (around line 111), add:

```php
// File attachments — shared wayleave.vw_file_uploads view filters entity='wayleave'
$sqlFiles = "SELECT file_id, file_name, file_description, file_category, file_type,
                    file_size, file_upload_datetime, username
             FROM wayleave.vw_file_uploads
             WHERE agreement_id = :aid
             ORDER BY file_upload_datetime DESC";
$stmt = $dbh->prepare($sqlFiles);
$stmt->bindValue(':aid', $agreement_id, PDO::PARAM_INT);
$stmt->execute();
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
```

- [ ] **Step 5: Include the new payload in the response**

Replace the final `echo json_encode([...])` with:

```php
echo json_encode([
    'success'         => true,
    'agreement'       => $agreement,
    'statuses'        => $statuses,
    'teams'           => $teams,
    'fields'          => $fields,
    'values'          => $values,
    'values_by_fid'   => $valuesByFid,
    'counts'          => $counts,
    'journal'         => $journal,
    'files'           => $files,
]);
```

- [ ] **Step 6: Smoke-test with curl**

Run (from a bash/WSL shell with a logged-in session cookie — or use the browser DevTools Network tab to POST the request and copy the response):

```bash
curl -s -X POST -b "PHPSESSID=<your-session>" -d "agreement_id=1" http://localhost/fn/wayleave_load.php | python -m json.tool | head -40
```

Expected: the JSON includes `teams` (a non-empty array), `agreement.bd_manager_name` (null or a username string), `agreement.account_name`, `agreement.parent_agreement_name`, `values` as an object keyed by `field_form_id` strings, and a `files` array.

- [ ] **Step 7: Commit**

```bash
git add www/fn/wayleave_load.php
git commit -m "feat(wayleave): return teams/labels/file-attachments in wayleave_load"
```

---

## Task 4: Extend autocomplete endpoints for wayleave agreements

**Why this task:** Feedback items _"Fields like … Parent ID, Stocklist ID should be text search based on names"_. `fn/autocomplete.php` already covers `companyname`, `accountname`, `projectname`, `stocklistname`, and `usernames` (via `fn/user_search.php`). Add a new `wayleavename` branch so the Parent Agreement input can autocomplete from existing agreements.

**Files:**
- Modify: `www/fn/autocomplete.php:33-51`

- [ ] **Step 1: Add the `wayleavename` branch to the switch**

Find the `switch ($type) {` block and add this case just before `default:`:

```php
case 'wayleavename':
    $q = "SELECT 'Wayleave Agreement' AS type,
                 agreement_id         AS value,
                 agreement_name       AS label
          FROM   wayleave.agreements
          WHERE  is_deleted = false
            AND  lower(agreement_name) LIKE lower(:search_term)
          ORDER  BY agreement_name
          LIMIT  15";
    break;
```

- [ ] **Step 2: Wire the V2 jQuery-UI binding in `main.js`**

In `www/js/main.js`, find the `$("input[data-autocomplete='stocklistname']").autocomplete({...})` block (starts around line 401) and copy it, changing the selector and source only. Add the copy immediately after the `stocklistname` block:

```js
$("input[data-autocomplete='wayleavename']").autocomplete({
    source: function(request, response) {
        var autocompleteType = $(this.element).data('autocomplete');
        $.ajax({
            url: "fn/autocomplete.php",
            data: { term: request.term, type: autocompleteType },
            success: function(data) {
                if (typeof data === "string") { data = JSON.parse(data); }
                response($.map(data, function(item) {
                    return { label: item.label, value: item.value };
                }));
            }
        });
    },
    minLength: 0,
    select: function(event, ui) {
        $(this).val(ui.item.label);
        var targetInputSelector = $(this).data('autocomplete-target');
        $('#'+targetInputSelector).val(ui.item.value);
        return false;
    }
}).on('click', function() {
    $(this).autocomplete('search', $(this).val() || '');
});
```

- [ ] **Step 3: Smoke-test in the browser**

Open `?do=wayleave` (list page). In the browser console, paste:

```js
$.post('fn/autocomplete.php', { term: '', type: 'wayleavename' }, null, 'json')
 .done(console.log);
```

Actually, `autocomplete.php` reads `$_GET`, so run this instead:

```js
$.get('fn/autocomplete.php', { term: '', type: 'wayleavename' }, null, 'json')
 .done(console.log);
```

Expected: an array of up to 15 agreements, each with `{type, value, label}`.

- [ ] **Step 4: Commit**

```bash
git add www/fn/autocomplete.php www/js/main.js
git commit -m "feat(wayleave): add wayleavename autocomplete type"
```

---

## Task 5: Build the V2 HTML body template

**Why this task:** Feedback items _"layout of the wayleave module follows the v1 layout"_, _"Fields with their own category should get a new tab"_, and _"Title linking … should be part of the coverage tab and not its own separate tab"_. Replace the Bootstrap `nav-tabs` shell with the V2 `project-header-bar` + `horizontal-nav` + `.content-panel` structure used by `projectedit`. Define one tab per `agreement_field_sections` section (Commercial / Legal / Delivery, etc.); tab buttons for them are emitted by the JS at runtime once the field metadata loads. Also drop the standalone `Land Registry` tab — that content moves inside `Coverage` in a later task.

**Files:**
- Modify: `www/html/html_body_wayleaveedit.php`

- [ ] **Step 1: Replace the placeholder with the V2 skeleton**

Overwrite `www/html/html_body_wayleaveedit.php` with:

```php
<?php
$agreement_id = isset($_GET['agreement_id']) ? intval($_GET['agreement_id']) : 0;
?>

<!-- Wayleave Agreement Header Bar (mirrors project-header-bar in html_body_projectedit.php) -->
<div class="row project-header-bar" id="wl-editor-root" data-agreement-id="<?= $agreement_id ?>">
    <div class="project-info col-12 col-md-6">
        <div>
            <div class="project-title">
                <h1>Wayleave: <span id="wl-editor-title">Loading</span></h1>
                <span id="wl-editor-status-badge" class="badge bg-primary">Loading</span>
            </div>
            <div class="project-stats">
                Resolved Premises: <span id="wl-hdr-premise-count">0</span> ·
                Polygons: <span id="wl-hdr-polygon-count">0</span> ·
                Files: <span id="wl-hdr-file-count">0</span>
            </div>
        </div>
    </div>
    <div class="project-actions col-12 col-md-6 d-flex justify-content-end">
        <span id="wl-save-indicator" class="small text-muted align-self-center me-2"></span>
        <a href="?do=wayleave" class="btn btn-xs btn-outline-secondary">
            <i class="bi bi-x-circle me-1"></i> Exit Editor
        </a>
    </div>
</div>

<!-- Horizontal Navigation (mirrors .horizontal-nav in html_body_projectedit.php). Dynamic section tabs are injected by JS. -->
<nav class="horizontal-nav" id="wl-nav-container">
    <button class="nav-tab active" data-nav="main"     data-tab-target="#wl-tab-main"><i class="bi bi-card-list"></i> Main Details</button>
    <!-- Per-section tabs (e.g. Commercial / Legal / Delivery) are inserted here by wayleave_edit_v2.js -->
    <button class="nav-tab"        data-nav="coverage" data-tab-target="#wl-tab-coverage">
        <i class="bi bi-bounding-box"></i> Coverage <span class="badge bg-secondary ms-1" id="wl-coverage-pending-badge" style="display:none"></span>
    </button>
    <button class="nav-tab"        data-nav="premises" data-tab-target="#wl-tab-premises">
        <i class="bi bi-house"></i> Premises <span class="badge bg-light text-dark ms-1" id="wl-premise-count-badge"></span>
    </button>
    <button class="nav-tab"        data-nav="projects" data-tab-target="#wl-tab-projects"><i class="bi bi-diagram-3"></i> Projects</button>
    <button class="nav-tab"        data-nav="releases" data-tab-target="#wl-tab-releases"><i class="bi bi-box-arrow-up-right"></i> Releases</button>
    <button class="nav-tab"        data-nav="map"      data-tab-target="#wl-tab-map"><i class="bi bi-map"></i> Map</button>
    <button class="nav-tab"        data-nav="files"    data-tab-target="#wl-tab-files"><i class="bi bi-paperclip"></i> Attachments</button>
    <button class="nav-tab"        data-nav="log"      data-tab-target="#wl-tab-log"><i class="bi bi-journal-text"></i> Journal &amp; Audit Log</button>
</nav>

<main class="p-0">
    <form id="wl-main-form" autocomplete="off">
        <input type="hidden" name="agreement_id" value="<?= $agreement_id ?>">

        <div id="wl-tab-content">

            <!-- Main Details (always-on static fields + first-section dynamic fields) -->
            <div class="content-panel p-3" id="wl-tab-main">
                <h6>Main Details</h6>
                <div class="row g-3" id="wl-main-static-fields"></div>
                <div class="mt-3">
                    <button type="submit" class="btn btn-sm btn-success" id="wl-main-save">Save</button>
                </div>
            </div>

            <!-- One content-panel per dynamic-field section — injected by JS into #wl-section-panels -->
            <div id="wl-section-panels"></div>

            <!-- Coverage -->
            <div class="content-panel p-3 d-none" id="wl-tab-coverage">
                <div class="row">
                    <div class="col-lg-7">
                        <h6>Polygons</h6>
                        <p class="text-muted small">Draw boundary polygons. Each change triggers a spatial join; results appear in the approval panel.</p>
                        <div id="wl-coverage-map" style="height: 480px; border: 1px solid #ced4da; border-radius: 4px;"></div>
                        <div class="mt-2"><small class="text-muted" id="wl-polygon-summary"></small></div>
                    </div>
                    <div class="col-lg-5">
                        <h6>Pending Approval</h6>
                        <div id="wl-pending-pane">
                            <div class="mb-3">
                                <strong>Pending add</strong>
                                <span class="badge bg-primary" id="wl-pending-add-count">0</span>
                                <div class="wl-pending-list" id="wl-pending-add-list"></div>
                                <div class="mt-1">
                                    <button type="button" class="btn btn-sm btn-success" data-approve="pending_add">Approve all adds</button>
                                    <button type="button" class="btn btn-sm btn-outline-danger" data-reject="pending_add">Reject all adds</button>
                                </div>
                            </div>
                            <div>
                                <strong>Pending removal</strong>
                                <span class="badge bg-warning text-dark" id="wl-pending-remove-count">0</span>
                                <div class="wl-pending-list" id="wl-pending-remove-list"></div>
                                <div class="mt-1">
                                    <button type="button" class="btn btn-sm btn-success" data-approve="pending_remove">Approve all removes</button>
                                    <button type="button" class="btn btn-sm btn-outline-secondary" data-reject="pending_remove">Keep all in</button>
                                </div>
                            </div>
                        </div>

                        <hr>
                        <h6>Direct UPRNs</h6>
                        <div class="input-group input-group-sm mb-2">
                            <input type="text" id="wl-direct-uprn-input" class="form-control" placeholder="Enter UPRN(s), comma or space separated">
                            <button type="button" class="btn btn-outline-primary" id="wl-direct-uprn-add">Add</button>
                        </div>
                        <div id="wl-direct-uprn-list" class="small"></div>

                        <hr>
                        <h6>Attached Stocklists</h6>
                        <div class="input-group input-group-sm mb-2">
                            <div class="position-relative flex-grow-1">
                                <input type="text" id="wl-stocklist-search" class="form-control"
                                       placeholder="Stocklist name"
                                       data-autocomplete="stocklistname"
                                       data-autocomplete-target="wl-stocklist-id">
                                <input type="hidden" id="wl-stocklist-id">
                            </div>
                            <button type="button" class="btn btn-outline-primary" id="wl-stocklist-attach">Attach</button>
                        </div>
                        <div id="wl-stocklist-list" class="small"></div>

                        <hr>
                        <h6>Linked Titles (Land Registry)</h6>
                        <p class="text-muted small mb-2">Titles referenced by this agreement for cross-checking against Land Registry ownership.</p>
                        <div class="input-group input-group-sm mb-2">
                            <input type="text" id="wl-landreg-title" class="form-control" placeholder="Title number">
                            <select id="wl-landreg-tenure" class="form-select" style="max-width: 140px;">
                                <option value="">Tenure</option>
                                <option>Freehold</option>
                                <option>Leasehold</option>
                            </select>
                            <button id="wl-landreg-add" type="button" class="btn btn-outline-primary">Add</button>
                        </div>
                        <div id="wl-landreg-list"></div>
                    </div>
                </div>
            </div>

            <!-- Premises -->
            <div class="content-panel p-3 d-none" id="wl-tab-premises">
                <div class="d-flex align-items-center mb-2">
                    <h5 class="mb-0">Resolved Premises <span class="small" id="wl-premises-count"></span></h5>
                    <button type="button" class="btn btn-sm btn-outline-secondary ms-auto" id="wl-premises-refresh">Refresh</button>
                </div>
                <div id="wl-premises-table"></div>
            </div>

            <!-- Projects -->
            <div class="content-panel p-3 d-none" id="wl-tab-projects">
                <h5>Overlapping Projects</h5>
                <p class="text-muted small">Projects that share at least one UPRN with this agreement's resolved premise list.</p>
                <div id="wl-projects-table"></div>
            </div>

            <!-- Releases -->
            <div class="content-panel p-3 d-none" id="wl-tab-releases">
                <h5>Premise Releases</h5>
                <div id="wl-releases-table"></div>
            </div>

            <!-- Map (read-only view) -->
            <div class="content-panel d-none" id="wl-tab-map">
                <div class="p-0" style="height: calc(100vh - 125px) !important; overflow:hidden;">
                    <div id="wl-view-map" class="w-100 h-100"></div>
                </div>
            </div>

            <!-- Attachments (V2 modal pattern) -->
            <div class="content-panel p-3 d-none" id="wl-tab-files">
                <h5>Attachments</h5>
                <div class="card shadow-sm mb-4">
                    <div class="card-body p-2">
                        <div class="d-flex justify-content-between align-items-center mb-1 pb-2 border-bottom">
                            <h6 class="card-title mb-0 fw-semibold">Wayleave Attachments</h6>
                            <button type="button" class="btn btn-primary btn-sm" onclick="openWayleaveUploadModal()">
                                <i class="fas fa-plus me-2"></i>Add Attachment
                            </button>
                        </div>
                        <div id="wl-attachments-container">
                            <div class="alert alert-info">Attachments will appear here once uploaded.</div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Journal & Audit Log -->
            <div class="content-panel p-3 d-none" id="wl-tab-log">
                <h5>Wayleave Journal &amp; Audit Log</h5>
                <div id="wl-journal-entry" class="mb-2">
                    <div class="row">
                        <div class="col-md-12">
                            <div class="mb-3">
                                <label for="wl-journal-text" class="form-label">Enter A Journal Entry</label>
                                <textarea class="form-control" id="wl-journal-text" rows="3" placeholder="Type something…"></textarea>
                            </div>
                            <button type="button" id="wl-journal-submit" class="btn btn-xs btn-success">Save Journal Entry</button>
                        </div>
                    </div>
                </div>
                <div id="wl-audit-log"></div>
            </div>

        </div>
    </form>

    <!-- Upload Modal (mirrors projectedit uploadModal) -->
    <div class="modal fade" id="wlUploadModal" tabindex="-1">
        <div class="modal-dialog modal-dialog-centered">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="wlUploadModalTitle">Add New Attachment</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div class="mb-3">
                        <label for="wlFileInputUpload" class="form-label fw-semibold">File *</label>
                        <input type="file" class="form-control" id="wlFileInputUpload">
                        <div class="form-text">Maximum file size: 20MB • PDF, DOC, DOCX, XLS, XLSX, ZIP, PNG, JPG, GIF</div>
                    </div>
                    <div class="mb-3">
                        <label for="wlFileDescription" class="form-label fw-semibold">Description *</label>
                        <textarea class="form-control" id="wlFileDescription" rows="3" placeholder="Provide a description of this attachment…"></textarea>
                    </div>
                    <div class="mb-3">
                        <label for="wlFileCategory" class="form-label fw-semibold">Category</label>
                        <select class="form-select" id="wlFileCategory">
                            <option value="">Select a category (optional)</option>
                            <option value="Wayleave">Wayleave</option>
                            <option value="Legal">Legal</option>
                            <option value="Planning">Planning</option>
                            <option value="Other">Other</option>
                        </select>
                    </div>
                    <div class="d-none bg-light rounded p-3" id="wlUploadProgress">
                        <div class="progress mb-2" style="height: 8px;">
                            <div class="progress-bar progress-bar-geolynx" id="wlProgressFill" style="width: 0%"></div>
                        </div>
                        <div class="small text-secondary" id="wlProgressText">Uploading… 0%</div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" onclick="wayleaveUploadFile()">
                        <i class="fas fa-upload me-2"></i>Upload
                    </button>
                </div>
            </div>
        </div>
    </div>
</main>
```

- [ ] **Step 2: Verify the skeleton renders**

Open `?do=wayleaveedit&agreement_id=1`. The page should now show the V2 header bar, horizontal nav with 8 static tabs, and the Main Details panel (empty form). Nothing will be wired up yet — clicks will do nothing.

- [ ] **Step 3: Commit**

```bash
git add www/html/html_body_wayleaveedit.php
git commit -m "feat(wayleave): v2 html shell for wayleave editor"
```

---

## Task 6: Port V2 styles and add wayleave-specific polish

**Why this task:** Feedback items _"File Upload layout/design doesn't follow the same pattern as v2 layout"_ and _"Journal Log layout/design doesn't follow the same pattern as v2 layout"_. The V2 styles (`project-header-bar`, `.horizontal-nav`, `.nav-tab`, `.content-panel`, `.file-item card`, `.upload-zone`) already live in `www/css/custom.css`, so the new CSS only needs wayleave-specific additions (source-tag colours, pending-list styling) plus the content-panel show/hide helper.

**Files:**
- Modify: `www/css/wayleave_edit_v2.css`

- [ ] **Step 1: Replace the placeholder CSS with the full V2 styles**

Overwrite `www/css/wayleave_edit_v2.css` with:

```css
/* www/css/wayleave_edit_v2.css
   Wayleave editor V2. Shared V2 classes (project-header-bar, horizontal-nav,
   nav-tab, content-panel, upload-zone, file-item) live in www/css/custom.css. */

/* Content panel show/hide (mirrors projectedit JS behaviour which toggles .d-none) */
.content-panel { display: block; }
.content-panel.d-none { display: none; }

/* Pending UPRN lists inside the Coverage tab */
.wl-pending-list {
    max-height: 180px;
    overflow-y: auto;
    border: 1px solid #e9ecef;
    border-radius: 4px;
    padding: 6px;
    margin-top: 4px;
    font-size: 0.85rem;
    background: #fbfbfd;
}
.wl-pending-row { display: flex; justify-content: space-between; gap: 6px; padding: 2px 0; }
.wl-pending-row .u  { font-family: monospace; }
.wl-pending-row .a  { color: #6c757d; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Source-of-data badges */
.wl-source-tag {
    display: inline-block; padding: 0 6px; border-radius: 8px;
    font-size: 0.75rem; background: #e9ecef; color: #495057; margin-right: 4px;
}
.wl-source-tag.polygon   { background: #cfe2ff; color: #084298; }
.wl-source-tag.direct    { background: #d1e7dd; color: #0f5132; }
.wl-source-tag.stocklist { background: #fff3cd; color: #664d03; }

.wl-field-spacer { height: 8px; grid-column: 1 / -1; }

/* Leaflet map containers */
#wl-coverage-map, #wl-view-map { background: #f8f9fa; }
.leaflet-pm-toolbar { font-size: 12px; }

/* Audit-log table sizing (Tabulator container) */
#wl-audit-log-table { min-height: 500px; }

/* Autocomplete clear-button positioning (reuses the projectedit pattern) */
.wl-autocomplete-wrap { position: relative; }
.wl-autocomplete-wrap .wl-clear-btn {
    position: absolute; top: 50%; right: 0; transform: translateY(-50%);
    padding: 0 0.5rem; display: block; z-index: 10;
    background: transparent; border: 0;
}
```

- [ ] **Step 2: Verify the styles load**

Hard-refresh `?do=wayleaveedit&agreement_id=1` (Ctrl+F5). Confirm in DevTools → Elements that `.content-panel.d-none { display: none }` is applied to the hidden panels, and that the horizontal nav picks up the shared `.horizontal-nav` / `.nav-tab` styles from `custom.css`.

- [ ] **Step 3: Commit**

```bash
git add www/css/wayleave_edit_v2.css
git commit -m "style(wayleave): v2 editor stylesheet"
```

---

## Task 7: Build the V2 JS — core shell, nav, static-field rendering, main-form save

**Why this task:** Feedback items _"Dropdown fields do not populate from saved data"_, _"Wayleave team field should be a dropdown"_, _"Fields like BD Manager, Account, Parent ID … should be text search based on names"_. This task rebuilds the core editor JS so it follows the V2 `$('#'+key).val(value)` pattern used by `project_edit_v2.js`, renders static fields with the right control types (autocomplete vs dropdown vs date), and wires the Save button end-to-end.

**Files:**
- Modify: `www/js/wayleave_edit_v2.js`

- [ ] **Step 1: Write the core shell (state, nav, renderMain)**

Overwrite `www/js/wayleave_edit_v2.js` with:

```js
// www/js/wayleave_edit_v2.js
// Wayleave editor — V2 shell. Mirrors the patterns in project_edit_v2.js:
// horizontal-nav tabs toggle .content-panel visibility, static fields are
// rendered per-section, dynamic fields get one content-panel per
// agreement_field_sections section, autocomplete fields follow the
// data-autocomplete / data-autocomplete-target contract wired in main.js.

(function () {
    var state = {
        agreementId: null,
        agreement:   null,
        statuses:    [],
        teams:       [],
        fields:      [],
        values:      {},   // keyed by field_form_id
        counts:      {},
        journal:     [],
        files:       [],
        loaded: {
            main: false, coverage: false, premises: false,
            files: false, projects: false, releases: false,
            map: false, log: false
        }
    };
    window.WL = { state: state };

    function escapeHtml(s) {
        if (s === null || s === undefined) return '';
        return String(s).replace(/[&<>"']/g, function (c) {
            return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
        });
    }
    WL.escapeHtml = escapeHtml;

    function toast(msg, type) {
        $('#wl-save-indicator').text(msg).css('color', type === 'err' ? '#dc3545' : '#198754');
        clearTimeout(WL._tt);
        WL._tt = setTimeout(function () { $('#wl-save-indicator').text(''); }, 3000);
    }
    WL.toast = toast;

    // ---- Nav handling (V2 horizontal-nav) ----
    function wireNav() {
        $('#wl-nav-container').on('click', '.nav-tab', function () {
            var $btn = $(this);
            var target = $btn.data('tab-target');
            $('#wl-nav-container .nav-tab').removeClass('active');
            $btn.addClass('active');
            $('#wl-tab-content > .content-panel, #wl-section-panels > .content-panel').addClass('d-none');
            $(target).removeClass('d-none');
            lazyLoadForTab(target);
        });
    }

    function lazyLoadForTab(target) {
        switch (target) {
            case '#wl-tab-coverage': if (WL.loadCoverage) WL.loadCoverage(); break;
            case '#wl-tab-premises': if (WL.loadPremises) WL.loadPremises(true); break;
            case '#wl-tab-projects': if (WL.loadProjects) WL.loadProjects(); break;
            case '#wl-tab-releases': if (WL.loadReleases) WL.loadReleases(); break;
            case '#wl-tab-map':      if (WL.loadMap)      WL.loadMap();      break;
            case '#wl-tab-files':    if (WL.loadFiles)    WL.loadFiles();    break;
            case '#wl-tab-log':      if (WL.loadAuditLog) WL.loadAuditLog(); break;
        }
    }

    // ---- Initial load ----
    WL.loadMain = function () {
        return $.post('fn/wayleave_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) { alert('Load failed: '+(resp&&resp.error||'unknown')); return; }
                state.agreement = resp.agreement;
                state.statuses  = resp.statuses;
                state.teams     = resp.teams  || [];
                state.fields    = resp.fields;
                state.values    = resp.values || {};
                state.counts    = resp.counts;
                state.journal   = resp.journal;
                state.files     = resp.files  || [];
                renderHeader();
                renderStaticFields();
                renderSectionTabsAndPanels();
                populateAllValues();
                rebindAutocompleteForNewInputs();
                updateBadges();
                state.loaded.main = true;
            });
    };

    function renderHeader() {
        var a = state.agreement;
        $('#wl-editor-title').text(a.agreement_name || 'Agreement #'+a.agreement_id);
        $('#wl-editor-status-badge').text(a.status_description || '—');
        $('#wl-hdr-premise-count').text(state.counts.direct_uprn_count ? '~'+state.counts.direct_uprn_count : '0');
        $('#wl-hdr-polygon-count').text(state.counts.polygon_count || 0);
        $('#wl-hdr-file-count').text(state.counts.file_count || 0);
    }

    function updateBadges() {
        var pending = (+state.counts.pending_add_count||0) + (+state.counts.pending_remove_count||0);
        if (pending > 0) $('#wl-coverage-pending-badge').text(pending).show();
        else             $('#wl-coverage-pending-badge').hide();
    }
    WL.updateBadges = updateBadges;

    // ---- Static fields (always on main tab) ----
    function renderStaticFields() {
        var a        = state.agreement;
        var statuses = state.statuses;
        var teams    = state.teams;

        var statusOpts = '<option value="">—</option>' + statuses.map(function (s) {
            return '<option value="'+s.id+'"'+(String(s.id)===String(a.agreement_status_id||'')?' selected':'')+'>'+escapeHtml(s.description)+'</option>';
        }).join('');

        var teamOpts = '<option value="">—</option>' + teams.map(function (t) {
            return '<option value="'+escapeHtml(t.team_name)+'"'+(String(t.team_name)===String(a.wayleave_team||'')?' selected':'')+'>'+escapeHtml(t.team_name)+'</option>';
        }).join('');

        var typeOpts = ['','SDU','MDU','Estate','Building','Route'].map(function (v) {
            return '<option value="'+escapeHtml(v)+'"'+(v===(a.agreement_type||'')?' selected':'')+'>'+(v||'—')+'</option>';
        }).join('');

        var html = ''
          + col(3, textField ('agreement_name',      'Agreement Name',    a.agreement_name))
          + col(3, textField ('agreement_reference', 'Reference',         a.agreement_reference))
          + col(3, selectField('agreement_status_id','Status',            statusOpts))
          + col(3, selectField('agreement_type',     'Type',              typeOpts))
          + col(3, selectField('wayleave_team',      'Wayleave Team',     teamOpts))
          + col(3, autocompleteField('bd_manager_name', 'bd_manager',           'BD Manager',        'usernames',    a.bd_manager_name))
          + col(3, autocompleteField('account_name',    'account_id',           'Account',           'accountname',  a.account_name))
          + col(3, autocompleteField('parent_agreement_name', 'parent_agreement_id','Parent Agreement','wayleavename',  a.parent_agreement_name))
          + col(3, dateField ('ecd_date',    'ECD Date',    a.ecd_date))
          + col(3, dateField ('signed_date', 'Signed Date', a.signed_date));
        $('#wl-main-static-fields').html(html);
    }

    function col(n, inner)   { return '<div class="col-md-'+n+'">'+inner+'</div>'; }
    function label(id, text) { return '<label class="form-label small mb-1" for="'+id+'">'+escapeHtml(text)+'</label>'; }

    function textField(id, lbl, val) {
        return label(id, lbl) +
            '<input type="text" id="'+id+'" name="'+id+'" class="form-control" value="'+escapeHtml(val||'')+'">';
    }
    function dateField(id, lbl, val) {
        var d = val ? String(val).substring(0,10) : '';
        return label(id, lbl) +
            '<input type="date" id="'+id+'" name="'+id+'" class="form-control" value="'+escapeHtml(d)+'">';
    }
    function selectField(id, lbl, optsHtml) {
        return label(id, lbl) +
            '<select id="'+id+'" name="'+id+'" class="form-select">'+optsHtml+'</select>';
    }
    function autocompleteField(nameInputId, hiddenInputId, lbl, acType, nameValue) {
        return label(nameInputId, lbl) +
            '<div class="wl-autocomplete-wrap">' +
                '<input type="text" id="'+nameInputId+'" class="form-control pe-4" ' +
                    'data-autocomplete="'+acType+'" data-autocomplete-target="'+hiddenInputId+'" ' +
                    'value="'+escapeHtml(nameValue||'')+'">' +
                '<button type="button" class="wl-clear-btn" data-clear-target="'+hiddenInputId+'" data-clear-label="'+nameInputId+'" title="Clear"><i class="bi bi-x-circle text-muted"></i></button>' +
            '</div>' +
            '<input type="hidden" id="'+hiddenInputId+'" name="'+hiddenInputId+'">';
    }
    WL.col = col;
    WL.label = label;
    WL.textField = textField;
    WL.dateField = dateField;
    WL.selectField = selectField;
    WL.autocompleteField = autocompleteField;

    // ---- Section tabs + dynamic field panels ----
    function renderSectionTabsAndPanels() {
        var bySection = {};
        state.fields.forEach(function (f) {
            var key = f.section_name || 'General';
            (bySection[key] = bySection[key] || []).push(f);
        });

        // Build nav buttons between Main and Coverage
        var $mainBtn     = $('#wl-nav-container .nav-tab[data-tab-target="#wl-tab-main"]');
        $('#wl-nav-container .nav-tab[data-dynamic-section]').remove();
        var $insertAfter = $mainBtn;
        Object.keys(bySection).forEach(function (sec) {
            var safeId = 'wl-tab-section-' + sec.toLowerCase().replace(/[^a-z0-9]+/g,'-');
            var $btn = $('<button type="button" class="nav-tab" data-dynamic-section="1">'
                +'<i class="bi bi-tag"></i> '+escapeHtml(sec)+'</button>');
            $btn.attr('data-tab-target', '#'+safeId);
            $insertAfter.after($btn);
            $insertAfter = $btn;

            var $panel = $('<div class="content-panel p-3 d-none" id="'+safeId+'"></div>');
            $panel.append('<h6>'+escapeHtml(sec)+'</h6>');
            var $grid = $('<div class="row g-3"></div>');
            bySection[sec].forEach(function (f) {
                var col = $('<div class="col-md-4"></div>');
                col.append(renderDynamicField(f));
                $grid.append(col);
                if (f.field_spacer_after) $grid.append('<div class="wl-field-spacer"></div>');
            });
            $panel.append($grid);
            $panel.append(
                '<div class="mt-3">' +
                    '<button type="submit" class="btn btn-sm btn-success wl-section-save">Save</button>' +
                '</div>'
            );
            $('#wl-section-panels').append($panel);
        });
    }

    function renderDynamicField(f) {
        var id   = f.field_form_id;
        var lbl  = label(id, f.field_name);
        var opts = f.dropdown_options || [];

        if (opts.length) {
            var html = '<select id="'+id+'" name="'+id+'" class="form-select"><option value="">—</option>';
            opts.forEach(function (o) { html += '<option value="'+escapeHtml(o.value)+'">'+escapeHtml(o.label)+'</option>'; });
            html += '</select>';
            return lbl + html;
        }
        switch (f.field_input_type) {
            case 'textarea':
                return lbl + '<textarea id="'+id+'" name="'+id+'" rows="2" class="form-control"></textarea>';
            case 'date':
                return lbl + '<input type="date" id="'+id+'" name="'+id+'" class="form-control">';
            case 'checkbox':
                return lbl + '<div class="form-check"><input type="checkbox" id="'+id+'" name="'+id+'" value="1" class="form-check-input"></div>';
            case 'number':
                return lbl + '<input type="number" step="any" id="'+id+'" name="'+id+'" class="form-control">';
            default:
                return lbl + '<input type="text" id="'+id+'" name="'+id+'" class="form-control">';
        }
    }

    // Populate every field from state.values + state.agreement using the V2 pattern.
    function populateAllValues() {
        // Static columns: copied directly from the agreement row (includes
        // joined *_name fields for the autocomplete text inputs).
        $.each(state.agreement, function (key, value) {
            if (value === null || value === undefined) return;
            var $el = $('#'+key);
            if (!$el.length) return;
            if ($el.is(':checkbox')) { $el.prop('checked', value==='t'||value===true||value==='1'); }
            else                     { $el.val(String(value).substring(0, $el.is('input[type=date]') ? 10 : 5000)); }
        });
        // Dynamic values keyed by field_form_id
        Object.keys(state.values).forEach(function (formId) {
            var v   = state.values[formId];
            var $el = $('#'+formId);
            if (!$el.length) return;
            if ($el.is(':checkbox')) { $el.prop('checked', v==='t'||v===true||v==='1'); }
            else if ($el.is('input[type=date]')) { $el.val(v ? String(v).substring(0,10) : ''); }
            else                     { $el.val(v); }
        });
    }

    // Re-run the jQuery UI autocomplete setup so newly-rendered inputs are wired.
    function rebindAutocompleteForNewInputs() {
        if (typeof bindAutocompleteInputs === 'function') {
            bindAutocompleteInputs();  // Provided in Task 8 via main.js refactor.
            return;
        }
        // Fallback: trigger a synthetic focus on each input — main.js already binds on DOMReady,
        // so fresh inputs added afterwards won't pick up handlers. Use the dedicated wl-bind
        // delegated initialiser below instead.
        $('input[data-autocomplete]').not('.wl-autocomplete-bound').each(function () {
            var $el    = $(this);
            var acType = $el.data('autocomplete');
            $el.addClass('wl-autocomplete-bound').autocomplete({
                source: function (request, response) {
                    $.getJSON('fn/autocomplete.php', { term: request.term, type: acType }, function (data) {
                        response($.map(data, function (item) { return { label: item.label, value: item.value }; }));
                    });
                },
                minLength: 0,
                select: function (event, ui) {
                    $(this).val(ui.item.label);
                    $('#'+$(this).data('autocomplete-target')).val(ui.item.value);
                    return false;
                }
            }).on('click', function () { $(this).autocomplete('search', $(this).val() || ''); });
        });
    }

    // Clear-button handler for autocomplete fields (both label + hidden id)
    $(document).on('click', '.wl-clear-btn', function () {
        $('#'+$(this).data('clear-target')).val('');
        $('#'+$(this).data('clear-label')).val('');
    });

    // ---- Save (main + section tabs submit the same form) ----
    function wireSave() {
        $('#wl-main-form').on('submit', function (e) {
            e.preventDefault();
            submitMainForm();
        });
        $(document).on('click', '#wl-main-save, .wl-section-save', function (e) {
            e.preventDefault();
            submitMainForm();
        });
    }

    function submitMainForm() {
        var data = $('#wl-main-form').serialize();
        $.post('fn/wayleave_save.php', data, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) { toast('Save failed: '+(resp&&resp.error||'unknown'), 'err'); return; }
                var n = (resp.changed_static.length + resp.changed_dynamic.length);
                toast(n>0 ? 'Saved '+n+' change(s)' : 'No changes');
                if (n>0) WL.loadMain();
            })
            .fail(function (xhr) { toast('Save failed: HTTP '+xhr.status, 'err'); });
    }

    $(function () {
        state.agreementId = parseInt($('#wl-editor-root').attr('data-agreement-id'), 10);
        if (!state.agreementId) { alert('Missing agreement_id'); return; }
        wireNav();
        wireSave();
        WL.loadMain();
    });
})();
```

- [ ] **Step 2: Smoke-test the Main tab**

Open `?do=wayleaveedit&agreement_id=1` and verify:
- The header bar shows the agreement name + status badge (from the loaded row).
- **Dropdowns populate the saved value**: `agreement_status_id`, `agreement_type`, `wayleave_team` all show the persisted value when the row had one. (This is the fix for the _"Dropdown fields do not populate from saved data"_ bug.)
- **Autocomplete text fields show the joined label**: `bd_manager_name` shows the BD manager's username (not the integer id); `account_name` shows the account name; `parent_agreement_name` shows the parent agreement's name.
- Dynamic-section tabs appear in the horizontal nav (Commercial / Legal / Delivery) and clicking each shows its fields.
- Dynamic field inputs populate from saved values.
- Clicking Save writes changes: edit `agreement_reference`, click Save, reload the page — the new reference is persisted.

- [ ] **Step 3: Commit**

```bash
git add www/js/wayleave_edit_v2.js
git commit -m "feat(wayleave): v2 editor core shell with section tabs + autocomplete"
```

---

## Task 8: Re-bind jQuery UI autocomplete after dynamic render

**Why this task:** `www/js/main.js` calls `$("input[data-autocomplete='usernames']").autocomplete(...)` at DOMReady. The wayleave editor builds its inputs *after* DOMReady via `renderStaticFields()`, so those inputs never get wired. Export the binder from `main.js` so the wayleave shell can re-run it, and tighten the fallback used in Task 7 step 1.

**Files:**
- Modify: `www/js/main.js`

- [ ] **Step 1: Wrap the autocomplete bindings in a reusable function**

At the top of `main.js` (around line 200, before the first `$("input[data-autocomplete='…']").autocomplete` block), add a function declaration that contains the existing six bindings and expose it globally:

```js
// Shared autocomplete initialiser — safe to call multiple times because every
// selector is scoped to `input[data-autocomplete='…']:not(.ac-bound)` below
// and each branch adds the `.ac-bound` class on wire-up.
window.bindAutocompleteInputs = function () {
    // The existing blocks below have been modified to use
    //   $("input[data-autocomplete='xxx']:not(.ac-bound)").addClass('ac-bound').autocomplete({...})
    // instead of
    //   $("input[data-autocomplete='xxx']").autocomplete({...})
};
```

Then update each of the six existing blocks (usernames / companyname / accountname / projectname / stocklistname / strategyanalysistabels — and the `wayleavename` block added in Task 4) so their opening selector reads:

```js
$("input[data-autocomplete='usernames']:not(.ac-bound)").addClass('ac-bound').autocomplete({...});
```

Move all six blocks inside the `window.bindAutocompleteInputs` function body.

At the bottom of the file (or at the original call site inside the DOMReady handler), call `bindAutocompleteInputs();` once so DOMReady-present inputs are still wired.

- [ ] **Step 2: Update the wayleave shell fallback to prefer the shared binder**

In `www/js/wayleave_edit_v2.js`, the `rebindAutocompleteForNewInputs` function (from Task 7) already checks `typeof bindAutocompleteInputs === 'function'`. Confirm the call site (`WL.loadMain`'s `.done` callback) executes after the static + dynamic fields are in the DOM.

- [ ] **Step 3: Smoke-test the autocompletes**

Reload `?do=wayleaveedit&agreement_id=1`. Click each of the four autocomplete fields (`bd_manager_name`, `account_name`, `parent_agreement_name`, and the stocklist search inside the Coverage tab — test this one after Task 11). Expected: with an empty term the dropdown shows the first 15 matches from the respective endpoint. Selecting an item populates both the visible label input and the hidden id input. The jQuery UI suggestion box is visible above other content (no z-index issue).

- [ ] **Step 4: Commit**

```bash
git add www/js/main.js
git commit -m "refactor: expose bindAutocompleteInputs for dynamic re-init"
```

---

## Task 9: Rebuild the Attachments tab to match the V2 upload modal + card pattern

**Why this task:** Feedback item _"File Upload layout/design doesn't follow the same pattern as v2 layout"_. Replace the minimal upload form with the full V2 modal + `file-item card` pattern used by `projectedit` (see `html_body_projectedit.php:596-823` and `project_edit_v2_fileuploads.js`). Reuse the existing shared `fn/file_upload.php` and `fn/serve_file.php` — the Phase 0 plan already integrated wayleave with the shared uploads pipeline (see `sql/wayleave_05_file_uploads_integration.sql`).

**Files:**
- Modify: `www/js/wayleave_edit_v2_fileuploads.js`
- Modify: `www/js/wayleave_edit_v2.js` (add `WL.loadFiles` + render)

- [ ] **Step 1: Write the V2 upload/download/delete module**

Overwrite `www/js/wayleave_edit_v2_fileuploads.js` with:

```js
// www/js/wayleave_edit_v2_fileuploads.js
// Mirrors project_edit_v2_fileuploads.js but targeted at the wayleave entity.
// Shared upload endpoint: fn/file_upload.php
// Shared download endpoint: fn/serve_file.php

let wlUploadModalInstance;

document.addEventListener('DOMContentLoaded', function () {
    var el = document.getElementById('wlUploadModal');
    if (el) { wlUploadModalInstance = new bootstrap.Modal(el); }
});

function openWayleaveUploadModal() {
    document.getElementById('wlUploadModalTitle').textContent = 'Add New Attachment';
    document.getElementById('wlFileInputUpload').value   = '';
    document.getElementById('wlFileDescription').value   = '';
    document.getElementById('wlFileCategory').value      = '';
    document.getElementById('wlUploadProgress').classList.add('d-none');
    wlUploadModalInstance.show();
}

function wayleaveUploadFile() {
    var fileInput   = document.getElementById('wlFileInputUpload');
    var description = document.getElementById('wlFileDescription').value;
    var category    = document.getElementById('wlFileCategory').value;
    var agreementId = (window.WL && WL.state && WL.state.agreementId) || 0;

    if (!fileInput.files.length) { alert('Please select a file to upload'); return; }
    if (!description.trim())     { alert('Please provide a description'); return; }
    if (!agreementId)            { alert('Missing agreement id');         return; }

    var fd = new FormData();
    fd.append('file',              fileInput.files[0]);
    fd.append('file_description',  description);
    fd.append('file_category',     category);
    fd.append('entity',            'wayleave');
    fd.append('wayleave',          agreementId);
    fd.append('project',           '');
    fd.append('stocklist',         '');
    fd.append('account',           '');
    fd.append('opportunity',       '');

    var progressDiv  = document.getElementById('wlUploadProgress');
    var progressFill = document.getElementById('wlProgressFill');
    var progressText = document.getElementById('wlProgressText');
    progressDiv.classList.remove('d-none');

    var xhr = new XMLHttpRequest();
    xhr.upload.addEventListener('progress', function (e) {
        if (e.lengthComputable) {
            var pct = Math.round((e.loaded / e.total) * 100);
            progressFill.style.width = pct + '%';
            progressText.textContent = 'Uploading… ' + pct + '%';
        }
    });
    xhr.addEventListener('load', function () {
        progressDiv.classList.add('d-none');
        progressFill.style.width = '0%';
        try {
            var resp = JSON.parse(xhr.responseText);
            if (xhr.status === 200 && resp.success) {
                wlUploadModalInstance.hide();
                if (window.WL && WL.loadFiles) WL.loadFiles();
                if (window.WL && WL.loadAuditLog) WL.loadAuditLog();
                if (window.WL && WL.loadMain)    WL.loadMain();  // refresh file count in header
            } else {
                alert('Upload failed: ' + (resp.error || resp.message || 'unknown'));
            }
        } catch (e) {
            alert('Invalid response from server');
        }
    });
    xhr.addEventListener('error', function () {
        alert('Upload failed - network error');
        progressDiv.classList.add('d-none');
    });
    xhr.open('POST', 'fn/file_upload.php');
    xhr.send(fd);
}

async function wayleaveDownloadFile(fileId) {
    var agreementId = (window.WL && WL.state && WL.state.agreementId) || 0;
    try {
        var response = await fetch('fn/serve_file.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: 'file_id=' + encodeURIComponent(fileId)
                 + '&entity=wayleave'
                 + '&entity_id=' + encodeURIComponent(agreementId)
        });
        if (!response.ok) throw new Error('HTTP ' + response.status);
        var cd = response.headers.get('Content-Disposition') || '';
        var m  = cd.match(/filename="(.+)"/);
        var filename = m ? m[1] : 'download';
        var blob = await response.blob();
        var url  = window.URL.createObjectURL(blob);
        var a    = document.createElement('a');
        a.href = url; a.download = filename;
        document.body.appendChild(a); a.click();
        window.URL.revokeObjectURL(url); document.body.removeChild(a);
    } catch (err) {
        alert('Failed to download file: ' + err.message);
    }
}

async function wayleaveDeleteFile(fileId) {
    if (!confirm('Delete this attachment?')) return;
    var agreementId = (window.WL && WL.state && WL.state.agreementId) || 0;
    var resp = await $.post('fn/wayleave_file_delete.php', {
        file_id: fileId, agreement_id: agreementId
    }, null, 'json');
    if (!resp || !resp.success) { alert('Delete failed: '+(resp && resp.error || 'unknown')); return; }
    if (window.WL && WL.loadFiles) WL.loadFiles();
    if (window.WL && WL.loadMain)  WL.loadMain();
}

// File icon class mapping (mirrors getFileTypeInfo in project_edit_v2.js)
function wlFileTypeInfo(fileType) {
    var t = (fileType || '').toLowerCase();
    if (t.indexOf('pdf')   !== -1) return { class: 'pdf',   icon: 'fa-file-pdf' };
    if (t.indexOf('excel') !== -1 || t.indexOf('sheet') !== -1 || t === 'xlsx' || t === 'xls') return { class: 'excel', icon: 'fa-file-excel' };
    if (t.indexOf('word')  !== -1 || t === 'doc' || t === 'docx') return { class: 'doc',   icon: 'fa-file-word' };
    if (t.indexOf('image') !== -1 || t === 'png' || t === 'jpg' || t === 'jpeg' || t === 'gif') return { class: 'image', icon: 'fa-file-image' };
    if (t.indexOf('zip')   !== -1) return { class: 'zip',   icon: 'fa-file-archive' };
    return { class: 'default', icon: 'fa-file' };
}
window.wlFileTypeInfo = wlFileTypeInfo;
```

- [ ] **Step 2: Add `WL.loadFiles` in the core JS**

Append the following to `www/js/wayleave_edit_v2.js` (before the final closing `})();` of the core IIFE, or in a new IIFE block at the bottom of the file):

```js
// ---- Attachments tab ----
(function () {
    if (!window.WL) return;
    var state = WL.state;

    WL.loadFiles = function () {
        return $.post('fn/wayleave_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                state.files = resp.files || [];
                renderAttachments();
            });
    };

    function renderAttachments() {
        var $c = $('#wl-attachments-container');
        if (!state.files.length) {
            $c.html('<div class="alert alert-info">No attachments uploaded yet.</div>');
            return;
        }
        var html = '';
        state.files.forEach(function (f) {
            var info     = window.wlFileTypeInfo ? wlFileTypeInfo(f.file_type) : { class: 'default', icon: 'fa-file' };
            var iconType = 'fas ' + info.icon;
            html +=
              '<div class="file-item card border mb-2">' +
                '<div class="card-body">' +
                  '<div class="row g-3">' +
                    '<div class="col-auto"><div class="file-icon '+info.class+' text-center"><i class="'+iconType+'"></i></div></div>' +
                    '<div class="col">' +
                      '<h6 class="mb-2 fw-semibold">'+WL.escapeHtml(f.file_name||'')+'</h6>' +
                      '<p class="text-secondary small mb-2">'+WL.escapeHtml(f.file_description||'')+'</p>' +
                      '<div class="d-flex gap-3 flex-wrap small text-muted">' +
                        '<span><i class="fas fa-tags me-1"></i>'+WL.escapeHtml(f.file_category||'')+'</span>' +
                        '<span><i class="fas fa-file me-1"></i>'+WL.escapeHtml(f.file_size||'')+'</span>' +
                        '<span><i class="fas fa-calendar me-1"></i>Uploaded: '+WL.escapeHtml((f.file_upload_datetime||'').substring(0,16))+'</span>' +
                        '<span><i class="fas fa-user me-1"></i>'+WL.escapeHtml(f.username||'')+'</span>' +
                      '</div>' +
                    '</div>' +
                    '<div class="col-12 col-md-auto">' +
                      '<div class="d-flex gap-2 justify-content-end">' +
                        '<button type="button" class="btn btn-outline-success btn-sm" onclick="wayleaveDownloadFile(\''+WL.escapeHtml(f.file_id)+'\')"><i class="fas fa-download"></i> Download</button>' +
                        '<button type="button" class="btn btn-outline-danger btn-sm" onclick="wayleaveDeleteFile(\''+WL.escapeHtml(f.file_id)+'\')"><i class="fas fa-trash"></i></button>' +
                      '</div>' +
                    '</div>' +
                  '</div>' +
                '</div>' +
              '</div>';
        });
        $c.html(html);
    }
})();
```

- [ ] **Step 3: Create the soft-delete endpoint**

Create `www/fn/wayleave_file_delete.php`:

```php
<?php
// www/fn/wayleave_file_delete.php — soft-delete a file attachment for a wayleave.
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once __DIR__ . '/global_vars.php';
require_once __DIR__ . '/global_functions.php';
require_once __DIR__ . '/db.php';

include 'login_check.php';
$check = loginCheck('func');
if ($check == false) { echo 'no access'; exit; }
$userID = $_SESSION['id'];

$file_id      = isset($_POST['file_id'])      ? intval($_POST['file_id'])      : 0;
$agreement_id = isset($_POST['agreement_id']) ? intval($_POST['agreement_id']) : 0;
if ($file_id < 1 || $agreement_id < 1) { echo json_encode(['success'=>false,'error'=>'bad params']); exit; }

$dbh = new PDO("pgsql:host=$hostname;port=5432;dbname=$dbname;user=$username;password=$password");
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

try {
    $dbh->beginTransaction();
    $stmt = $dbh->prepare("UPDATE wayleave.agreement_file_uploads
                           SET file_is_deleted = true, modified_user = :u, modified_datetime = now()
                           WHERE file_id = :fid AND agreement_id = :aid AND file_is_deleted = false");
    $stmt->execute([':u'=>$userID, ':fid'=>$file_id, ':aid'=>$agreement_id]);
    $n = $stmt->rowCount();

    if ($n > 0) {
        $dbh->prepare("INSERT INTO wayleave.agreement_journal (agreement_id, user_id, log_text, is_system)
                       VALUES (:a,:u,:t,true)")
            ->execute([':a'=>$agreement_id, ':u'=>$userID, ':t'=>'Attachment removed (file_id='.$file_id.')']);
    }
    $dbh->commit();
    echo json_encode(['success'=>true, 'deleted'=>$n]);
} catch (Exception $e) {
    if ($dbh->inTransaction()) $dbh->rollBack();
    echo json_encode(['success'=>false,'error'=>$e->getMessage()]);
}
```

Note: the wayleave file attachment join table name in `sql/wayleave_05_file_uploads_integration.sql` is `wayleave.agreement_file_uploads` — confirm the column is `file_is_deleted` and the PK is `(file_id, agreement_id)` before running. If the actual column is named differently, adjust the `UPDATE` clause.

- [ ] **Step 4: Smoke-test uploads end-to-end**

Open `?do=wayleaveedit&agreement_id=1`, click the **Attachments** tab:
- Click **Add Attachment**. Pick a small PDF, type a description, pick category "Wayleave", click **Upload**. Expected: progress bar fills, modal closes, the file card appears in the list, and the header file count increments.
- Click **Download** on the card. Expected: the PDF downloads with its original filename.
- Click the delete button. Expected: card disappears, header count decrements, a system journal entry is written (visible later in Task 10's audit log).

- [ ] **Step 5: Commit**

```bash
git add www/js/wayleave_edit_v2_fileuploads.js www/js/wayleave_edit_v2.js www/fn/wayleave_file_delete.php
git commit -m "feat(wayleave): v2 attachments tab with modal upload + card list"
```

---

## Task 10: Build the Journal & Audit Log tab with prev/new values

**Why this task:** Feedback item _"Journal Log does not include previous/new values as per v2 layouts"_. The V2 pattern (see `project_edit_v2.js:5905-5998`) is a Tabulator with columns **Record Date / Username / Field/Action / Previous Value / New Value** filterable via `headerFilter: "input"`, populated by a server-side UNION of all the per-type history tables with `LAG()` over `(agreement_id, field_id ORDER BY history_datetime)` plus journal entries.

**Files:**
- Create: `www/fn/wayleave_audit_log_load.php`
- Modify: `www/js/wayleave_edit_v2.js` (add `WL.loadAuditLog`)

- [ ] **Step 1: Create the server-side UNION query**

Create `www/fn/wayleave_audit_log_load.php`:

```php
<?php
// www/fn/wayleave_audit_log_load.php — prev/new value audit log for a wayleave agreement.
// Mirrors the projects.project_load.php pattern: UNION across per-type history tables
// with LAG() to derive previous_value, plus journal entries as free-text rows.
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once __DIR__ . '/global_vars.php';
require_once __DIR__ . '/db.php';

include 'login_check.php';
$check = loginCheck('func');
if ($check == false) { echo 'no access'; exit; }

$agreement_id = isset($_POST['agreement_id']) ? intval($_POST['agreement_id']) : 0;
if ($agreement_id < 1) { echo json_encode(['success'=>false,'error'=>'missing agreement_id']); exit; }

$dbh = new PDO("pgsql:host=$hostname;port=5432;dbname=$dbname;user=$username;password=$password");
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$sql = "
WITH a AS (
    /* Static-column history: agreements_history already stores history_old_value + history_new_value
       as text, one row per changed column. */
    SELECT
        h.agreement_id,
        h.history_field                                         AS field_form_id,
        h.history_field                                         AS field_name,
        'text'                                                  AS field_data_type,
        to_char(h.history_datetime, 'YYYY-MM-DD HH24:MI')       AS record_date,
        h.history_datetime                                      AS record_datetime,
        u.username                                              AS username,
        h.history_new_value                                     AS text_value,
        NULL::int    AS int_value,    NULL::numeric AS numeric_value,
        NULL::date   AS date_value,   NULL::boolean AS boolean_value,
        h.history_old_value                                     AS previous_text_value,
        NULL::int    AS previous_int_value,    NULL::numeric AS previous_numeric_value,
        NULL::date   AS previous_date_value,   NULL::boolean AS previous_boolean_value
    FROM wayleave.agreements_history h
    LEFT JOIN users.users u ON u.id = h.history_user
    WHERE h.agreement_id = :aid1 AND h.history_action = 'UPDATE' AND h.history_field IS NOT NULL

    UNION ALL

    /* Dynamic-field text history */
    SELECT
        vt.agreement_id,
        f.field_form_id, f.field_name, 'text' AS field_data_type,
        to_char(vt.history_datetime, 'YYYY-MM-DD HH24:MI'),
        vt.history_datetime, u.username,
        vt.field_value, NULL, NULL, NULL, NULL,
        lag(vt.field_value) OVER (PARTITION BY vt.agreement_id, vt.field_id ORDER BY vt.history_datetime),
        NULL, NULL, NULL, NULL
    FROM wayleave.agreement_field_values_text_history vt
    JOIN wayleave.agreement_fields f ON f.field_id = vt.field_id
    LEFT JOIN users.users u ON u.id = vt.history_user
    WHERE vt.agreement_id = :aid2

    UNION ALL

    /* int history */
    SELECT
        vt.agreement_id,
        f.field_form_id, f.field_name, 'int',
        to_char(vt.history_datetime, 'YYYY-MM-DD HH24:MI'),
        vt.history_datetime, u.username,
        NULL, vt.field_value, NULL, NULL, NULL,
        NULL,
        lag(vt.field_value) OVER (PARTITION BY vt.agreement_id, vt.field_id ORDER BY vt.history_datetime),
        NULL, NULL, NULL
    FROM wayleave.agreement_field_values_int_history vt
    JOIN wayleave.agreement_fields f ON f.field_id = vt.field_id
    LEFT JOIN users.users u ON u.id = vt.history_user
    WHERE vt.agreement_id = :aid3

    UNION ALL

    /* numeric history */
    SELECT
        vt.agreement_id,
        f.field_form_id, f.field_name, 'numeric',
        to_char(vt.history_datetime, 'YYYY-MM-DD HH24:MI'),
        vt.history_datetime, u.username,
        NULL, NULL, vt.field_value, NULL, NULL,
        NULL, NULL,
        lag(vt.field_value) OVER (PARTITION BY vt.agreement_id, vt.field_id ORDER BY vt.history_datetime),
        NULL, NULL
    FROM wayleave.agreement_field_values_numeric_history vt
    JOIN wayleave.agreement_fields f ON f.field_id = vt.field_id
    LEFT JOIN users.users u ON u.id = vt.history_user
    WHERE vt.agreement_id = :aid4

    UNION ALL

    /* date history */
    SELECT
        vt.agreement_id,
        f.field_form_id, f.field_name, 'date',
        to_char(vt.history_datetime, 'YYYY-MM-DD HH24:MI'),
        vt.history_datetime, u.username,
        NULL, NULL, NULL, vt.field_value, NULL,
        NULL, NULL, NULL,
        lag(vt.field_value) OVER (PARTITION BY vt.agreement_id, vt.field_id ORDER BY vt.history_datetime),
        NULL
    FROM wayleave.agreement_field_values_date_history vt
    JOIN wayleave.agreement_fields f ON f.field_id = vt.field_id
    LEFT JOIN users.users u ON u.id = vt.history_user
    WHERE vt.agreement_id = :aid5

    UNION ALL

    /* boolean history */
    SELECT
        vt.agreement_id,
        f.field_form_id, f.field_name, 'boolean',
        to_char(vt.history_datetime, 'YYYY-MM-DD HH24:MI'),
        vt.history_datetime, u.username,
        NULL, NULL, NULL, NULL, vt.field_value,
        NULL, NULL, NULL, NULL,
        lag(vt.field_value) OVER (PARTITION BY vt.agreement_id, vt.field_id ORDER BY vt.history_datetime)
    FROM wayleave.agreement_field_values_boolean_history vt
    JOIN wayleave.agreement_fields f ON f.field_id = vt.field_id
    LEFT JOIN users.users u ON u.id = vt.history_user
    WHERE vt.agreement_id = :aid6

    UNION ALL

    /* Journal entries */
    SELECT
        j.agreement_id,
        'journal_entry', 'Journal Entry', 'text',
        to_char(j.log_datetime, 'YYYY-MM-DD HH24:MI'),
        j.log_datetime, u.username,
        j.log_text, NULL, NULL, NULL, NULL,
        NULL, NULL, NULL, NULL, NULL
    FROM wayleave.agreement_journal j
    LEFT JOIN users.users u ON u.id = j.user_id
    WHERE j.agreement_id = :aid7
)
SELECT * FROM a
WHERE  agreement_id = :aid8
ORDER  BY record_datetime DESC
LIMIT  2000";

try {
    $stmt = $dbh->prepare($sql);
    for ($i = 1; $i <= 8; $i++) { $stmt->bindValue(':aid'.$i, $agreement_id, PDO::PARAM_INT); }
    $stmt->execute();
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    echo json_encode(['success'=>true, 'rows'=>$rows]);
} catch (PDOException $e) {
    echo json_encode(['success'=>false,'error'=>$e->getMessage()]);
}
```

- [ ] **Step 2: Hit the endpoint and verify the shape**

After changing at least one static column and one dynamic field, run (from browser DevTools):

```js
$.post('fn/wayleave_audit_log_load.php', { agreement_id: 1 }, null, 'json').done(console.log);
```

Expected: `rows` array contains entries with both `text_value` + `previous_text_value` populated where fields were edited; journal entries appear as their own rows; rows are ordered newest first.

- [ ] **Step 3: Render the audit log in the editor**

Append the following IIFE to the bottom of `www/js/wayleave_edit_v2.js`:

```js
// ---- Journal & Audit Log tab ----
(function () {
    if (!window.WL) return;
    var state = WL.state;
    var auditTable = null;

    WL.loadAuditLog = function () {
        return $.post('fn/wayleave_audit_log_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                renderAuditLog(resp.rows || []);
            });
    };

    function renderAuditLog(rows) {
        $('#wl-audit-log').empty().append('<div id="wl-audit-log-table"></div>');
        auditTable = new Tabulator('#wl-audit-log-table', {
            data:              rows,
            layout:            'fitDataStretch',
            pagination:        true,
            paginationSize:    25,
            paginationSizeSelector: [25, 50, 100],
            paginationCounter: 'rows',
            height:            '65vh',
            placeholder:       'No audit entries yet.',
            columns: [
                { title: 'Record Date', field: 'record_date',   width: 140, headerSort: true, headerFilter: 'input' },
                { title: 'Username',    field: 'username',      width: 160, headerSort: true, headerFilter: 'input' },
                { title: 'Field/Action',field: 'field_name',    width: 220, headerSort: true, headerFilter: 'input' },
                {
                    title: 'Previous Value', field: 'previous_value',
                    minWidth: 180, headerSort: false, headerFilter: 'input',
                    formatter: function (cell) { return formatPrevNew(cell, 'prev'); }
                },
                {
                    title: 'New Value',      field: 'new_value',
                    minWidth: 180, headerSort: false, headerFilter: 'input',
                    formatter: function (cell) { return formatPrevNew(cell, 'new'); }
                }
            ]
        });
    }

    function formatPrevNew(cell, which) {
        var item = cell.getData();
        var v  = '', pv = '';
        switch (item.field_data_type) {
            case 'text':    v = item.text_value    || ''; pv = item.previous_text_value    || ''; break;
            case 'int':     v = item.int_value;           pv = item.previous_int_value;           break;
            case 'numeric': v = item.numeric_value;       pv = item.previous_numeric_value;       break;
            case 'date':    v = item.date_value    || ''; pv = item.previous_date_value    || ''; break;
            case 'boolean': v = item.boolean_value;       pv = item.previous_boolean_value;       break;
        }
        if (v  === null || v  === undefined) v  = '';
        if (pv === null || pv === undefined) pv = '';
        return WL.escapeHtml(String(which === 'prev' ? pv : v));
    }

    // Journal submit
    $(document).on('click', '#wl-journal-submit', function () {
        var t = $.trim($('#wl-journal-text').val());
        if (!t) return;
        $.post('fn/wayleave_journal_save.php', {
            agreement_id: state.agreementId, journal_text: t
        }, null, 'json').done(function (resp) {
            if (!resp || !resp.success) { WL.toast('Journal save failed', 'err'); return; }
            $('#wl-journal-text').val('');
            WL.loadAuditLog();
        });
    });
})();
```

- [ ] **Step 4: Smoke-test in the browser**

1. Edit the Agreement Reference, Save.
2. Edit a dynamic text field, Save.
3. Write "hello" in the Journal entry, click Save Journal Entry.
4. Click **Journal & Audit Log** tab.

Expected: the Tabulator shows three rows with the username, field name, previous value, and new value populated correctly; each column has a filter input in the header; changing pagination size works; the free-text journal entry appears with `field_name = "Journal Entry"` and the text in the New Value column (Previous Value empty).

- [ ] **Step 5: Commit**

```bash
git add www/fn/wayleave_audit_log_load.php www/js/wayleave_edit_v2.js
git commit -m "feat(wayleave): audit log with prev/new values (v2 pattern)"
```

---

## Task 11: Port the Coverage tab (map + pending + direct UPRNs + stocklists + titles)

**Why this task:** Feedback items _"Title linking … should be part of the coverage tab and not its own separate tab, it should also autocomplete to give users feedback"_ and _"Fields like … Stocklist ID should be text search based on names"_. Port the existing coverage logic from `wayleave_edit.js` into the V2 shell, swap the numeric stocklist input for an autocomplete, and add the title-linking sub-panel inside the Coverage tab with a title-number autocomplete over `landregistry.ccod`.

**Files:**
- Modify: `www/js/wayleave_edit_v2.js` (add `WL.loadCoverage`)
- Modify: `www/fn/autocomplete.php` (add `landregtitle` type)

- [ ] **Step 1: Add the `landregtitle` autocomplete type**

In `www/fn/autocomplete.php`, add another case to the switch:

```php
case 'landregtitle':
    $q = "SELECT 'Title' AS type,
                 title_number AS value,
                 title_number || ' · ' || COALESCE(proprietor_name_1,'') AS label
          FROM   landregistry.ccod
          WHERE  title_number ILIKE :search_term
          ORDER  BY title_number
          LIMIT  15";
    break;
```

Also add the jQuery-UI binding for `landregtitle` inside the `bindAutocompleteInputs` function in `www/js/main.js` (same shape as the other blocks).

- [ ] **Step 2: Port the coverage IIFE into the V2 JS**

Append the following to `www/js/wayleave_edit_v2.js` (near the bottom, above any closing IIFE):

```js
// ---- Coverage tab ----
(function () {
    if (!window.WL) return;
    var state = WL.state;
    var map = null, polyLayer = null, polygonsById = {};
    var initialized = false;

    function initMap() {
        map = L.map('wl-coverage-map').setView([54.5, -2.0], 6);
        L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            { attribution: '&copy; OpenStreetMap', maxZoom: 19 }).addTo(map);
        polyLayer = L.featureGroup().addTo(map);
        map.pm.addControls({
            position: 'topleft',
            drawPolygon: true, drawMarker: false, drawPolyline: false,
            drawRectangle: true, drawCircle: false, drawCircleMarker: false, drawText: false,
            editMode: true, dragMode: false, cutPolygon: false, removalMode: true
        });
        map.on('pm:create', function (e) {
            var gj = e.layer.toGeoJSON();
            savePolygon('create', 0, gj.geometry, null, e.layer);
        });
        map.on('pm:remove', function (e) {
            var id = e.layer._wlPolygonId;
            if (id) savePolygon('delete', id, null, null, null);
        });
    }
    function bindLayerEvents(layer, polygonId) {
        layer._wlPolygonId = polygonId;
        polygonsById[polygonId] = layer;
        layer.on('pm:edit', function () {
            var gj = layer.toGeoJSON();
            savePolygon('update', polygonId, gj.geometry, null, layer);
        });
    }
    function savePolygon(action, polygonId, geom, lbl, newLayer) {
        var payload = { agreement_id: state.agreementId, action: action };
        if (polygonId) payload.polygon_id = polygonId;
        if (geom)      payload.geojson    = JSON.stringify(geom);
        if (lbl)       payload.label      = lbl;
        $.post('fn/wayleave_coverage_polygon_save.php', payload, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) { WL.toast('Polygon save failed: '+(resp&&resp.error||''), 'err'); return; }
                WL.toast('Polygon '+action+': '+resp.uprn_in_boundary+' UPRN(s) in boundary');
                if (action === 'create' && newLayer) bindLayerEvents(newLayer, resp.polygon_id);
                refreshPending();
                refreshPolygonSummary();
            });
    }
    function loadPolygons() {
        $.post('fn/wayleave_map_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                polyLayer.clearLayers(); polygonsById = {};
                (resp.polygons.features || []).forEach(function (f) {
                    var layer = L.geoJSON(f).getLayers()[0];
                    polyLayer.addLayer(layer);
                    bindLayerEvents(layer, f.properties.id);
                });
                if (polyLayer.getLayers().length) map.fitBounds(polyLayer.getBounds(), { padding: [20,20] });
                refreshPolygonSummary();
            });
    }
    function refreshPolygonSummary() {
        $('#wl-polygon-summary').text(Object.keys(polygonsById).length+' polygon(s) saved');
    }
    function refreshPending() {
        $.post('fn/wayleave_coverage_polygon_uprns_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                $('#wl-pending-add-count').text(resp.pending_add.length);
                $('#wl-pending-remove-count').text(resp.pending_remove.length);
                $('#wl-pending-add-list').html(renderPendingList(resp.pending_add));
                $('#wl-pending-remove-list').html(renderPendingList(resp.pending_remove));
                var total = resp.pending_add.length + resp.pending_remove.length;
                if (total > 0) $('#wl-coverage-pending-badge').text(total).show();
                else           $('#wl-coverage-pending-badge').hide();
            });
    }
    function renderPendingList(arr) {
        if (!arr.length) return '<em class="text-muted">None</em>';
        return arr.map(function (r) {
            return '<div class="wl-pending-row"><span class="u">'+WL.escapeHtml(r.uprn)+'</span>'
                 + '<span class="a">'+WL.escapeHtml(r.address_full||'')+'</span></div>';
        }).join('');
    }

    function wireApprove() {
        $('#wl-pending-pane').on('click', '[data-approve], [data-reject]', function () {
            var $btn = $(this);
            var isApprove = $btn.is('[data-approve]');
            var stateName = $btn.attr('data-approve') || $btn.attr('data-reject');
            var listSel = stateName === 'pending_add' ? '#wl-pending-add-list' : '#wl-pending-remove-list';
            var rows = [];
            $(listSel).find('.wl-pending-row .u').each(function () { rows.push({ uprn: $(this).text(), state: stateName }); });
            if (!rows.length) return;
            $.post('fn/wayleave_coverage_polygon_uprns_approve.php', {
                agreement_id: state.agreementId,
                action:       isApprove ? 'approve' : 'reject',
                uprns:        JSON.stringify(rows)
            }, null, 'json').done(function (resp) {
                if (!resp || !resp.success) { WL.toast('Approval failed', 'err'); return; }
                WL.toast('Approved '+resp.approved+', rejected '+resp.rejected);
                refreshPending();
                if (WL.loadPremises) WL.loadPremises(true);
            });
        });
    }

    function loadDirect() {
        $.post('fn/wayleave_premises_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                var direct = (resp.premises || []).filter(function (p) { return p.sources && p.sources.indexOf('direct') !== -1; });
                if (!direct.length) { $('#wl-direct-uprn-list').html('<em class="text-muted">No direct UPRNs.</em>'); return; }
                $('#wl-direct-uprn-list').html(direct.map(function (p) {
                    return '<div class="wl-pending-row">'
                         + '<span class="u">'+WL.escapeHtml(p.uprn)+'</span>'
                         + '<span class="a">'+WL.escapeHtml(p.address_full||'')+'</span>'
                         + '<button type="button" class="btn btn-sm btn-link text-danger p-0 wl-direct-remove" data-uprn="'+WL.escapeHtml(p.uprn)+'">remove</button>'
                         + '</div>';
                }).join(''));
            });
    }
    function wireDirect() {
        $('#wl-direct-uprn-add').on('click', function () {
            var raw = $.trim($('#wl-direct-uprn-input').val());
            if (!raw) return;
            var uprns = raw.split(/[\s,]+/).filter(Boolean);
            $.post('fn/wayleave_coverage_uprn_save.php', {
                agreement_id: state.agreementId, action: 'add', uprns: JSON.stringify(uprns)
            }, null, 'json').done(function (resp) {
                if (!resp || !resp.success) { WL.toast('Add failed', 'err'); return; }
                WL.toast('Added '+resp.count+' UPRN(s)');
                $('#wl-direct-uprn-input').val(''); loadDirect();
                if (WL.loadPremises) WL.loadPremises(true);
            });
        });
        $('#wl-direct-uprn-list').on('click', '.wl-direct-remove', function () {
            var u = $(this).data('uprn');
            $.post('fn/wayleave_coverage_uprn_save.php', {
                agreement_id: state.agreementId, action: 'remove', uprns: JSON.stringify([u])
            }, null, 'json').done(function (resp) {
                if (resp && resp.success) { loadDirect(); if (WL.loadPremises) WL.loadPremises(true); }
            });
        });
    }

    // Stocklist — autocomplete-by-name (feedback fix)
    function loadStocklists() {
        $.post('fn/wayleave_premises_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (pr) {
                if (!pr || !pr.success) return;
                var ids = {};
                (pr.premises || []).forEach(function (p) { (p.source_ids || []).forEach(function (id) { ids[id] = true; }); });
                var list = Object.keys(ids);
                if (!list.length) { $('#wl-stocklist-list').html('<em class="text-muted">No stocklists attached.</em>'); return; }
                $('#wl-stocklist-list').html(list.map(function (id) {
                    return '<div class="wl-pending-row"><span class="u">#'+WL.escapeHtml(id)+'</span><span class="a"></span>'
                         + '<button type="button" class="btn btn-sm btn-link text-danger p-0 wl-stocklist-detach" data-id="'+WL.escapeHtml(id)+'">detach</button></div>';
                }).join(''));
            });
    }
    function wireStocklists() {
        $('#wl-stocklist-attach').on('click', function () {
            var id = parseInt($('#wl-stocklist-id').val(), 10);
            if (!id) { alert('Pick a stocklist from the suggestions'); return; }
            $.post('fn/wayleave_coverage_stocklist_save.php', {
                agreement_id: state.agreementId, action: 'attach', stocklist_id: id
            }, null, 'json').done(function (resp) {
                if (!resp || !resp.success) { WL.toast('Attach failed', 'err'); return; }
                $('#wl-stocklist-search').val(''); $('#wl-stocklist-id').val('');
                loadStocklists();
                if (WL.loadPremises) WL.loadPremises(true);
            });
        });
        $('#wl-stocklist-list').on('click', '.wl-stocklist-detach', function () {
            var id = parseInt($(this).data('id'), 10);
            $.post('fn/wayleave_coverage_stocklist_save.php', {
                agreement_id: state.agreementId, action: 'detach', stocklist_id: id
            }, null, 'json').done(function (resp) {
                if (resp && resp.success) { loadStocklists(); if (WL.loadPremises) WL.loadPremises(true); }
            });
        });
    }

    // Land Registry titles — now inside Coverage (feedback fix)
    WL.loadLandreg = function () {
        $.post('fn/wayleave_landreg_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                if (!resp.titles.length) { $('#wl-landreg-list').html('<em class="text-muted">No titles linked.</em>'); return; }
                var html = '<table class="table table-sm"><thead><tr><th>Title</th><th>Tenure</th><th>Added</th><th></th></tr></thead><tbody>';
                resp.titles.forEach(function (t) {
                    html += '<tr><td>'+WL.escapeHtml(t.title_number)+'</td>'
                          + '<td>'+WL.escapeHtml(t.tenure||'')+'</td>'
                          + '<td>'+WL.escapeHtml((t.added_datetime||'').substring(0,16))+'</td>'
                          + '<td><button type="button" class="btn btn-sm btn-link text-danger p-0 wl-landreg-remove" data-title="'+WL.escapeHtml(t.title_number)+'">remove</button></td></tr>';
                });
                html += '</tbody></table>';
                $('#wl-landreg-list').html(html);
            });
    };
    $(document).on('click', '#wl-landreg-add', function () {
        var title  = $.trim($('#wl-landreg-title').val());
        var tenure = $('#wl-landreg-tenure').val();
        if (!title) return;
        $.post('fn/wayleave_landreg_save.php', {
            agreement_id: state.agreementId, action: 'add', title_number: title, tenure: tenure
        }, null, 'json').done(function (resp) {
            if (!resp || !resp.success) { WL.toast('Add failed', 'err'); return; }
            $('#wl-landreg-title').val(''); $('#wl-landreg-tenure').val('');
            WL.loadLandreg();
        });
    });
    $(document).on('click', '.wl-landreg-remove', function () {
        var t = $(this).data('title');
        $.post('fn/wayleave_landreg_save.php', {
            agreement_id: state.agreementId, action: 'remove', title_number: t
        }, null, 'json').done(function (resp) { if (resp && resp.success) WL.loadLandreg(); });
    });

    // Turn the title-number input into an autocomplete (feedback fix)
    function upgradeTitleInput() {
        var $in = $('#wl-landreg-title');
        if (!$in.length || $in.data('ac-upgraded')) return;
        $in.data('ac-upgraded', true).attr('data-autocomplete', 'landregtitle');
        if (typeof bindAutocompleteInputs === 'function') bindAutocompleteInputs();
    }

    WL.loadCoverage = function () {
        if (!initialized) { initMap(); wireApprove(); wireDirect(); wireStocklists(); initialized = true; upgradeTitleInput(); }
        setTimeout(function () { if (map) map.invalidateSize(); }, 150);
        loadPolygons();
        refreshPending();
        loadDirect();
        loadStocklists();
        WL.loadLandreg();
        WL.state.loaded.coverage = true;
    };
})();
```

- [ ] **Step 3: Smoke-test the Coverage tab**

Open `?do=wayleaveedit&agreement_id=1` → Coverage tab:
- Leaflet map renders with the existing polygons (if any).
- Pending add/remove lists populate.
- **Stocklist autocomplete**: typing in the stocklist search shows suggestions from existing stocklists; selecting one populates the hidden id; clicking Attach links it.
- **Title autocomplete**: typing a partial title number in the Linked Titles panel shows suggestions from `landregistry.ccod`; clicking Add links it. Removing works.

- [ ] **Step 4: Commit**

```bash
git add www/fn/autocomplete.php www/js/main.js www/js/wayleave_edit_v2.js
git commit -m "feat(wayleave): port coverage tab; move titles into coverage; add autocompletes"
```

---

## Task 12: Premises, Projects, Releases tabs — V2 filterable Tabulators

**Why this task:** Feedback item _"Premise/project lists (tabulator) should be filterable as per v2 layouts"_. Add `headerFilter: "input"` to the tabulator column defs and port the premises/projects/releases logic into the V2 JS.

**Files:**
- Modify: `www/js/wayleave_edit_v2.js` (add `WL.loadPremises`, `WL.loadProjects`, `WL.loadReleases`)

- [ ] **Step 1: Append the three tab modules**

Append to `www/js/wayleave_edit_v2.js`:

```js
// ---- Premises / Projects / Releases ----
(function () {
    if (!window.WL) return;
    var state = WL.state;
    var premisesTable = null, projectsTable = null, releasesTable = null;

    function sourceTags(sources) {
        return (sources || []).map(function (s) {
            return '<span class="wl-source-tag '+WL.escapeHtml(s)+'">'+WL.escapeHtml(s)+'</span>';
        }).join('');
    }

    WL.loadPremises = function (force) {
        return $.post('fn/wayleave_premises_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                $('#wl-premises-count').text(resp.count ? '('+resp.count+')' : '');
                $('#wl-premise-count-badge').text(resp.count || '');
                $('#wl-hdr-premise-count').text(resp.count || 0);
                if (!premisesTable) {
                    premisesTable = new Tabulator('#wl-premises-table', {
                        data:              resp.premises,
                        layout:            'fitColumns',
                        pagination:        'local',
                        paginationSize:    50,
                        placeholder:       'No resolved premises',
                        columns: [
                            { title: 'UPRN',    field: 'uprn',         width: 140, headerSort: true, headerFilter: 'input' },
                            { title: 'Address', field: 'address_full',             headerSort: true, headerFilter: 'input' },
                            { title: 'Sources', field: 'sources',      width: 220, headerSort: false,
                              formatter: function (c) { return sourceTags(c.getValue()); } }
                        ]
                    });
                } else { premisesTable.replaceData(resp.premises); }
                state.loaded.premises = true;
            });
    };

    WL.loadProjects = function () {
        return $.post('fn/wayleave_projects_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                if (!projectsTable) {
                    projectsTable = new Tabulator('#wl-projects-table', {
                        data:         resp.projects,
                        layout:       'fitColumns',
                        placeholder:  'No projects overlap with this agreement',
                        columns: [
                            { title: 'Project', field: 'project_name', widthGrow: 3,
                              headerSort: true, headerFilter: 'input',
                              formatter: function (c) {
                                  var d = c.getRow().getData();
                                  return '<a href="?do=projectedit&project='+d.project_id+'">'+WL.escapeHtml(d.project_name||'')+'</a>';
                              } },
                            { title: 'Status',  field: 'status',         widthGrow: 1, headerSort: true, headerFilter: 'input' },
                            { title: 'Overlap', field: 'overlap_count', widthGrow: 1, headerSort: true, hozAlign: 'right', headerFilter: 'input' }
                        ]
                    });
                } else { projectsTable.replaceData(resp.projects); }
            });
    };

    WL.loadReleases = function () {
        return $.post('fn/wayleave_releases_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                if (!releasesTable) {
                    releasesTable = new Tabulator('#wl-releases-table', {
                        data:         resp.releases,
                        layout:       'fitColumns',
                        placeholder:  'No releases',
                        columns: [
                            { title: 'UPRN',      field: 'uprn',         width: 140, headerSort: true, headerFilter: 'input' },
                            { title: 'Address',   field: 'address_full',             headerSort: true, headerFilter: 'input' },
                            { title: 'Release',   field: 'release_date', width: 130, headerSort: true, headerFilter: 'input', editor: 'input' },
                            { title: 'Sales Ref', field: 'sales_ref',    width: 140, headerSort: true, headerFilter: 'input', editor: 'input' },
                            { title: 'PIC Ref',   field: 'pic_ref',      width: 140, headerSort: true, headerFilter: 'input', editor: 'input' },
                            { title: 'Notes',     field: 'notes',                  headerSort: true, headerFilter: 'input', editor: 'input' },
                            { title: '', width: 90, headerSort: false,
                              formatter: function () { return '<button type="button" class="btn btn-sm btn-outline-danger wl-release-del">delete</button>'; },
                              cellClick: function (e, cell) {
                                  if (!$(e.target).hasClass('wl-release-del')) return;
                                  var d = cell.getRow().getData();
                                  $.post('fn/wayleave_releases_save.php', {
                                      agreement_id: state.agreementId, action: 'delete', uprn: d.uprn
                                  }, null, 'json').done(function (resp) { if (resp && resp.success) WL.loadReleases(); });
                              } }
                        ],
                        cellEdited: function (cell) {
                            var d = cell.getRow().getData();
                            $.post('fn/wayleave_releases_save.php', {
                                agreement_id: state.agreementId, action: 'upsert',
                                uprn:         d.uprn,
                                release_date: d.release_date || '',
                                sales_ref:    d.sales_ref   || '',
                                pic_ref:      d.pic_ref     || '',
                                notes:        d.notes       || ''
                            }, null, 'json').done(function (resp) {
                                if (!resp || !resp.success) WL.toast('Release save failed', 'err');
                                else WL.toast('Release saved');
                            });
                        }
                    });
                } else { releasesTable.replaceData(resp.releases); }
            });
    };

    $(document).on('click', '#wl-premises-refresh', function () { WL.loadPremises(true); });
})();
```

- [ ] **Step 2: Smoke-test the tabulators**

Open the editor and switch between the **Premises**, **Projects**, and **Releases** tabs. Expected: each column shows a filter input in its header; typing filters the visible rows live; the Releases tab still supports inline editing (click a cell, edit, blur → toast "Release saved"); a Refresh button re-fetches premises.

- [ ] **Step 3: Commit**

```bash
git add www/js/wayleave_edit_v2.js
git commit -m "feat(wayleave): filterable Tabulators for premises/projects/releases"
```

---

## Task 13: Port the read-only map tab

**Why this task:** Parity with the prior version's Map tab. The V2 shell's `#wl-tab-map` needs the Leaflet view-map JS that renders polygons and premise points, ported from the legacy editor.

**Files:**
- Modify: `www/js/wayleave_edit_v2.js`

- [ ] **Step 1: Append the map module**

Append:

```js
// ---- Read-only view map ----
(function () {
    if (!window.WL) return;
    var state = WL.state;
    var viewMap = null, viewPolyLayer = null, viewPointLayer = null;

    WL.loadMap = function () {
        if (!viewMap) {
            viewMap = L.map('wl-view-map').setView([54.5, -2.0], 6);
            L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                { attribution: '&copy; OpenStreetMap', maxZoom: 19 }).addTo(viewMap);
            viewPolyLayer  = L.featureGroup().addTo(viewMap);
            viewPointLayer = L.featureGroup().addTo(viewMap);
        }
        setTimeout(function () { viewMap.invalidateSize(); }, 150);
        $.post('fn/wayleave_map_load.php', { agreement_id: state.agreementId }, null, 'json')
            .done(function (resp) {
                if (!resp || !resp.success) return;
                viewPolyLayer.clearLayers(); viewPointLayer.clearLayers();
                L.geoJSON(resp.polygons, { style: { color: '#0d6efd', weight: 2, fillOpacity: 0.1 } })
                 .eachLayer(function (l) { viewPolyLayer.addLayer(l); });
                (resp.premises.features || []).forEach(function (f) {
                    var c = f.geometry.coordinates;
                    var p = f.properties;
                    var color = p.released ? '#198754'
                              : (p.sources.indexOf('polygon') !== -1 ? '#0d6efd'
                              : (p.sources.indexOf('direct')  !== -1 ? '#20c997' : '#ffc107'));
                    var m = L.circleMarker([c[1], c[0]], { radius: 4, color: color, fillColor: color, fillOpacity: 0.7, weight: 1 });
                    m.bindPopup('<strong>UPRN:</strong> '+WL.escapeHtml(p.uprn)+'<br>'+WL.escapeHtml(p.address||'')
                        + '<br>Sources: '+(p.sources||[]).join(', ')
                        + (p.released ? '<br><em>Released</em>' : ''));
                    viewPointLayer.addLayer(m);
                });
                var bounds = viewPolyLayer.getBounds();
                if (!bounds.isValid()) bounds = viewPointLayer.getBounds();
                if (bounds.isValid()) viewMap.fitBounds(bounds, { padding: [20,20] });
            });
    };
})();
```

- [ ] **Step 2: Smoke-test**

Open the **Map** tab. Expected: polygons render in blue, premise points in status-coloured circles; popup shows UPRN/address/sources on click; map fits to data.

- [ ] **Step 3: Commit**

```bash
git add www/js/wayleave_edit_v2.js
git commit -m "feat(wayleave): v2 read-only map tab"
```

---

## Task 14: Cutover — retire legacy v1 files

**Why this task:** Until all previous tasks are verified working, the legacy `wayleave_edit.*` triplet stayed in place as a safety net. Now that V2 is live and every feedback item is green, retire the legacy files so the tree stays clean. Follow memory note: SQL files live in `/sql`, and CLAUDE.md's unused-files list is `unusedfiles.md`.

**Files:**
- Delete: `www/html/wayleave_edit.php`, `www/js/wayleave_edit.js`, `www/css/wayleave_edit.css`
- Modify: `unusedfiles.md`

- [ ] **Step 1: Run the full feedback checklist one more time**

Before deleting anything, walk through the feedback file `docs/superpowers/plans/2026-04-22-wayleave-module-feedback.md` and tick each item in the browser:

1. V2 layout (header bar + horizontal-nav + content-panel) — ✅ visible on the page.
2. V2 file upload modal + file-item cards — ✅ visible on the Attachments tab.
3. V2 journal log with prev/new columns and header filters — ✅ visible on the Journal & Audit Log tab.
4. Title linking inside Coverage, not its own tab — ✅.
5. Title autocomplete — ✅ suggestions appear from `landregistry.ccod`.
6. Dropdowns populate from saved values (Status / Type / Team) — ✅.
7. Text-search + hidden-id for BD Manager / Account / Parent / Stocklist — ✅ (four autocompletes work).
8. One tab per `agreement_field_sections.section_name` — ✅.
9. Tabulator header filters on Premises / Projects / Releases — ✅.
10. Wayleave Team is a dropdown backed by `wayleave.teams` — ✅.
11. Audit log rows include previous + new values for both static columns and every dynamic-field type — ✅.

If any item fails, **stop and go back to the task that implements it**. Do not delete files with open feedback.

- [ ] **Step 2: Delete the legacy files**

```bash
git rm www/html/wayleave_edit.php www/js/wayleave_edit.js www/css/wayleave_edit.css
```

- [ ] **Step 3: Update `unusedfiles.md`**

Open `unusedfiles.md` and append (or update the existing wayleave section if one exists):

```
## Wayleave — superseded by V2 (2026-04-22 refactor)
- www/html/wayleave_edit.php       → replaced by www/html/html_body_wayleaveedit.php
- www/js/wayleave_edit.js          → replaced by www/js/wayleave_edit_v2.js (+ wayleave_edit_v2_fileuploads.js)
- www/css/wayleave_edit.css        → replaced by www/css/wayleave_edit_v2.css
```

- [ ] **Step 4: Final verification**

Browse to every wayleave route once more to confirm nothing references the deleted files:
- `?do=wayleave` (list) — should still work (unchanged route).
- `?do=wayleaveedit&agreement_id=1` — V2 editor renders.
- `?do=wayleavefreehold`, `?do=wayleavetitle` — unchanged, should still work.

Open DevTools Console. Expected: no 404s for `wayleave_edit.{js,css,php}`.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "chore(wayleave): retire v1 editor files after v2 cutover"
```

---

## Self-Review Checklist

### Spec coverage (against `2026-04-22-wayleave-module-feedback.md`)

| Feedback item | Addressed by |
|---|---|
| Layout follows v1 — should be v2 | Tasks 2 (router flip) + 5 (V2 skeleton) + 6 (styles) |
| File Upload layout doesn't match v2 | Task 9 |
| Journal Log layout doesn't match v2 | Task 10 |
| Title linking should be on Coverage tab with autocomplete | Tasks 5 (markup) + 11 (behaviour + `landregtitle` autocomplete) |
| Dropdown fields do not populate from saved data | Task 3 (values keyed by form_id) + Task 7 (`populateAllValues`) |
| BD Manager / Account / Parent ID / Stocklist should be name-search + hidden id | Tasks 3 (label joins) + 4 (wayleavename autocomplete) + 7 (`autocompleteField`) + 11 (stocklist autocomplete) |
| Fields with their own category should get a new tab | Task 7 (`renderSectionTabsAndPanels`) |
| Premise/project lists should be filterable | Task 12 (`headerFilter: 'input'`) |
| Wayleave team should be a dropdown | Task 1 (teams lookup) + Task 3 (return list) + Task 7 (render as `<select>`) |
| Journal Log should include previous/new values | Task 10 |

### Placeholder scan

No "TBD", "implement later", "similar to Task N", or empty "tests for the above" remain. Every step shows the code to write or the exact command to run.

### Type consistency

- `state.values` is keyed by `field_form_id` (string) in every task that touches it (Tasks 3, 7, 10).
- Autocomplete fields use the same `{label, value}` contract in every task (Tasks 4, 7, 8, 11).
- Tabulator `headerFilter: 'input'` spelled consistently in Tasks 10 and 12.
- `wayleave.teams.team_name` is referenced as `t.team_name` everywhere (Tasks 1, 3, 7).
- Autocomplete `data-autocomplete-target` names match the hidden-input IDs (`account_id`, `parent_agreement_id`, `bd_manager`, `wl-stocklist-id`) used by `wayleave_save.php`'s existing allow-list.

---

## Execution Handoff

Plan complete and saved to `docs/superpowers/plans/2026-04-22-wayleave-module-v2-refactor.md`. Two execution options:

**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.

**2. Inline Execution** — Execute tasks in this session using `superpowers:executing-plans`, batch execution with checkpoints.

Which approach?
