# Contract Form Drag-and-Drop with Custom Fields — 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:** Add drag-and-drop reordering of contract sections and cylinder rows, plus template-driven custom fields at the section, cylinder row, and party detail levels.

**Architecture:** Extend existing contract form with SortableJS for drag-and-drop. Custom field definitions stored in `contract_templates.layout` JSON. Custom field values stored in `contracts.custom_fields` JSON. No new tables.

**Tech Stack:** PHP 8, MySQL, SortableJS 1.15.0 (CDN), vanilla JS

---

## File Map

| File | Role |
|------|------|
| `config/database.php` | Auto-migration for new JSON columns |
| `assets/js/contract.js` | SortableJS init, custom field manager, serialization |
| `assets/css/contract.css` | Drag handle, custom field, popover styles |
| `contracts/create.php` | Section IDs, SortableJS CDN, add-field/add-section buttons |
| `contracts/edit.php` | Same as create, plus load existing custom field values |
| `contracts/save.php` | Accept and store `custom_fields` JSON |
| `contracts/template_save.php` | Accept and store `layout` JSON |
| `contracts/template_load.php` | Return `layout` JSON in response |
| `contracts/print.php` | Render custom fields and reordered sections |
| `contracts/view.php` | Display custom fields in detail view |
| `customers/index.php` | Include layout data in template rendering |

---

### Task 1: Database Migration

**Files:**
- Modify: `config/database.php`

- [ ] **Step 1: Add layout column migration to contract_templates**

In `config/database.php`, after the existing `contract_templates` CREATE TABLE block (around line 316), add:

```php
// Auto-migrate: add layout column to contract_templates
$layoutCol = @mysqli_query($conn, "SHOW COLUMNS FROM contract_templates LIKE 'layout'");
if ($layoutCol && mysqli_num_rows($layoutCol) === 0) {
    @mysqli_query($conn, "ALTER TABLE contract_templates ADD COLUMN layout JSON DEFAULT NULL");
}
```

- [ ] **Step 2: Add custom_fields column migration to contracts**

Right after the layout migration, add:

```php
// Auto-migrate: add custom_fields column to contracts
$cfCol = @mysqli_query($conn, "SHOW COLUMNS FROM contracts LIKE 'custom_fields'");
if ($cfCol && mysqli_num_rows($cfCol) === 0) {
    @mysqli_query($conn, "ALTER TABLE contracts ADD COLUMN custom_fields JSON DEFAULT NULL");
}
```

- [ ] **Step 3: Verify migration runs**

Load any page in GasERP. Check MySQL:
```sql
SHOW COLUMNS FROM contract_templates LIKE 'layout';
SHOW COLUMNS FROM contracts LIKE 'custom_fields';
```
Expected: Both return one row with `JSON` type.

---

### Task 2: SortableJS CDN and Section Reordering in create.php

**Files:**
- Modify: `contracts/create.php`

- [ ] **Step 1: Add SortableJS CDN**

In `contracts/create.php`, after the existing `<script src="../assets/js/contract.js"></script>` line (line 73), add:

```html
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
```

- [ ] **Step 2: Add data-section-id attributes to all form sections**

Each `.contract-form-section` div needs a `data-section-id` attribute. Apply these changes:

| Line | Section | data-section-id |
|------|---------|-----------------|
| ~91 | Logo Upload | `data-section-id="logo"` |
| ~110 | Parties | `data-section-id="parties"` |
| ~144 | Article 1 Cylinder Loan | `data-section-id="cylinders"` |
| ~169 | Article 2 Loan Dates | `data-section-id="dates"` |
| ~181 | Article 3 Cash Repayment | `data-section-id="cash_price"` |
| ~207 | Loan Rules & Restrictions | `data-section-id="conditions"` |
| ~259 | Damage & Replacement Fees | `data-section-id="damage_fees"` |
| ~310 | Article 4 Rental Fee | `data-section-id="rental"` |
| ~344 | Violation Penalties | `data-section-id="penalties"` |
| ~384 | Dispute Resolution | `data-section-id="disputes"` |
| ~426 | Additional Notes | `data-section-id="notes"` |
| ~440 | Signatures | `data-section-id="signatures"` |

Example change for the first section (line ~91):

```php
<!-- Before -->
<div class="contract-form-section">

<!-- After -->
<div class="contract-form-section" data-section-id="logo">
```

- [ ] **Step 3: Add drag handle HTML to each section header**

Each `.section-header` needs a grip handle before its text. Add a `<span class="drag-handle">⠿</span>` as the first child of each `.section-header`.

Example for the first section (line ~92):

```php
<!-- Before -->
<div class="section-header"><span class="material-symbols-rounded">image</span> Company Logo</div>

<!-- After -->
<div class="section-header"><span class="drag-handle" title="Drag to reorder">⠿</span><span class="material-symbols-rounded">image</span> Company Logo</div>
```

Repeat for all 12 section headers.

- [ ] **Step 4: Add a container div around all sections for SortableJS**

Wrap all `.contract-form-section` divs inside a new `<div id="sections-container">` right after the `<form>` opening tag and hidden inputs (after line ~88), and close it before the `<div class="contract-actions">` (before line ~460):

```php
<!-- After hidden inputs -->
<div id="sections-container">

<!-- All contract-form-section divs go here -->

</div>
<!-- Before contract-actions -->
```

- [ ] **Step 5: Initialize SortableJS for sections**

At the bottom of the `<script>` block (before `</script>`), add:

```javascript
// Initialize section drag-and-drop
var sectionsContainer = document.getElementById('sections-container');
if (sectionsContainer && typeof Sortable !== 'undefined') {
    Sortable.create(sectionsContainer, {
        handle: '.drag-handle',
        animation: 200,
        ghostClass: 'sortable-ghost',
        chosenClass: 'sortable-chosen',
        dragClass: 'sortable-drag'
    });
}
```

- [ ] **Step 6: Add "+ Add Section" button before contract-actions**

Before the `<div class="contract-actions">` block (before line ~460), add:

```php
<button type="button" class="add-cylinder-btn" id="add-custom-section-btn" onclick="openAddSectionModal()">
    <span class="material-symbols-rounded" style="font-size:14px;">add</span> Add Custom Section
</button>
```

- [ ] **Step 7: Test section reordering**

Load the contract create page. Drag sections by the grip handle. Verify:
- Sections move smoothly with animation
- Visual feedback (ghost/chosen classes) during drag
- Section order is maintained in the DOM

---

### Task 3: Cylinder Row Drag-and-Drop

**Files:**
- Modify: `contracts/create.php`
- Modify: `assets/js/contract.js`

- [ ] **Step 1: Add drag handle to cylinder row template in contract.js**

In `assets/js/contract.js`, modify the `renderArticle1()` function to add a drag handle as the first `<td>` in each row. Find the `renderArticle1` function and update the row HTML:

```javascript
function renderArticle1() {
    var tbody = document.getElementById('art1-tbody');
    if (!tbody) return;
    var html = '';
    for (var i = 0; i < cylinders.length; i++) {
        html += '<tr data-index="' + i + '">' +
            '<td class="row-drag-handle" style="cursor:grab;text-align:center;">⠿</td>' +
            '<td><select class="form-control cyl-type-select" onchange="updateCylType(' + i + ',this.value)">' +
            buildTypeOptions(cylinders[i].type) +
            '</select></td>' +
            '<td><input type="number" class="form-control" min="1" value="' + cylinders[i].qty + '" onchange="updateQty(' + i + ',this.value)" style="text-align:center;"></td>' +
            '<td><input type="text" class="form-control" value="' + escHtml(cylinders[i].qty_text) + '" onchange="updateQtyText(' + i + ',this.value)"></td>' +
            '<td><button type="button" onclick="removeRow(' + i + ')" class="remove-row"><span class="material-symbols-rounded">close</span></button></td>' +
            '</tr>';
    }
    tbody.innerHTML = html;
    document.getElementById('art1-total-qty').textContent = getTotalQty();
}
```

- [ ] **Step 2: Initialize SortableJS on art1-tbody**

In `contracts/create.php`, in the `<script>` block at the bottom, add after the sections SortableJS init:

```javascript
// Initialize cylinder row drag-and-drop
var art1Tbody = document.getElementById('art1-tbody');
if (art1Tbody && typeof Sortable !== 'undefined') {
    Sortable.create(art1Tbody, {
        handle: '.row-drag-handle',
        animation: 150,
        onEnd: function(evt) {
            // Reorder cylinders array
            var moved = cylinders.splice(evt.oldIndex, 1)[0];
            cylinders.splice(evt.newIndex, 0, moved);
            renderAll();
        }
    });
}
```

- [ ] **Step 3: Add "+ Column" button above Article 1 table**

In `contracts/create.php`, before the cylinder table (before the `<table class="cylinder-table">` inside the cylinders section), add:

```php
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
    <button type="button" class="add-cylinder-btn" onclick="openAddColumnModal()">
        <span class="material-symbols-rounded" style="font-size:14px;">add</span> Add Column
    </button>
</div>
```

- [ ] **Step 4: Test cylinder row reordering**

Load the contract create page. Add 3 cylinder rows. Drag rows by the grip handle. Verify:
- Rows reorder visually
- Article 1, 3, and 4 tables stay in sync after reorder
- Removing a row after reorder removes the correct data

---

### Task 4: Custom Field Manager (JS)

**Files:**
- Modify: `assets/js/contract.js`

- [ ] **Step 1: Add custom field state object**

At the top of `contract.js`, after the existing variable declarations, add:

```javascript
// Custom fields state
var customFieldState = {
    sections: [],
    cylinderFields: [],
    partyFields: [],
    nextId: 1
};

function generateFieldId(prefix) {
    return prefix + '_' + (customFieldState.nextId++);
}
```

- [ ] **Step 2: Add field type input renderer**

Add this function to `contract.js`:

```javascript
function renderCustomFieldInput(fieldDef, key, value) {
    value = value || '';
    var attrs = 'data-field-key="' + escHtml(key) + '"';
    if (fieldDef.required) attrs += ' required';
    switch (fieldDef.type) {
        case 'number':
            return '<input type="number" class="form-control custom-field-input" ' + attrs + ' value="' + escHtml(value) + '">';
        case 'date':
            return '<input type="date" class="form-control custom-field-input" ' + attrs + ' value="' + escHtml(value) + '">';
        case 'select':
            var opts = '<option value="">-- Select --</option>';
            (fieldDef.options || []).forEach(function(o) {
                opts += '<option value="' + escHtml(o) + '"' + (o === value ? ' selected' : '') + '>' + escHtml(o) + '</option>';
            });
            return '<select class="form-control custom-field-input" ' + attrs + '>' + opts + '</select>';
        case 'checkbox':
            return '<label style="display:flex;align-items:center;gap:6px;"><input type="checkbox" class="custom-field-input" ' + attrs + (value ? ' checked' : '') + '><span>' + escHtml(fieldDef.label) + '</span></label>';
        case 'textarea':
            return '<textarea class="form-control custom-field-input" ' + attrs + ' rows="2">' + escHtml(value) + '</textarea>';
        default:
            return '<input type="text" class="form-control custom-field-input" ' + attrs + ' value="' + escHtml(value) + '">';
    }
}
```

- [ ] **Step 3: Add custom section renderer**

Add this function to `contract.js`:

```javascript
function renderCustomSection(section, values) {
    values = values || {};
    var html = '<div class="contract-form-section custom-section" data-section-id="' + escHtml(section.id) + '" data-custom-section="' + escHtml(section.id) + '">';
    html += '<div class="section-header"><span class="drag-handle" title="Drag to reorder">⠿</span>';
    html += '<span class="material-symbols-rounded">article</span> ' + escHtml(section.title);
    html += '<button type="button" onclick="removeCustomSection(\'' + escHtml(section.id) + '\')" class="remove-row" style="margin-left:auto;" title="Remove section"><span class="material-symbols-rounded">close</span></button>';
    html += '</div>';
    html += '<div class="section-body">';
    section.fields.forEach(function(field) {
        var key = 'cs_' + section.id + '_' + field.id;
        var val = values[key] || '';
        html += '<div class="form-group custom-field-wrapper" data-field-id="' + escHtml(field.id) + '">';
        html += '<label class="form-label">' + escHtml(field.label);
        if (field.required) html += ' <span style="color:var(--contract-danger);">*</span>';
        html += ' <span class="custom-field-actions"><a href="#" onclick="editCustomField(\'' + escHtml(section.id) + '\',\'' + escHtml(field.id) + '\');return false;" title="Edit">✏️</a>';
        html += ' <a href="#" onclick="removeCustomField(\'' + escHtml(section.id) + '\',\'' + escHtml(field.id) + '\');return false;" title="Delete">🗑️</a></span>';
        html += '</label>';
        html += renderCustomFieldInput(field, key, val);
        html += '</div>';
    });
    html += '<button type="button" class="add-cylinder-btn" onclick="openAddFieldModal(\'' + escHtml(section.id) + '\')"><span class="material-symbols-rounded" style="font-size:13px;">add</span> Add Field</button>';
    html += '</div></div>';
    return html;
}
```

- [ ] **Step 4: Add party custom fields renderer**

Add this function to `contract.js`:

```javascript
function renderPartyCustomFields(fields, values) {
    values = values || {};
    var container = document.getElementById('party-custom-fields');
    if (!container) return;
    var html = '';
    fields.forEach(function(field) {
        var key = 'pf_' + field.id;
        var val = values[key] || '';
        html += '<div class="form-group custom-field-wrapper" data-field-id="' + escHtml(field.id) + '">';
        html += '<label class="form-label">' + escHtml(field.label);
        if (field.required) html += ' <span style="color:var(--contract-danger);">*</span>';
        html += ' <span class="custom-field-actions"><a href="#" onclick="editPartyCustomField(\'' + escHtml(field.id) + '\');return false;" title="Edit">✏️</a>';
        html += ' <a href="#" onclick="removePartyCustomField(\'' + escHtml(field.id) + '\');return false;" title="Delete">🗑️</a></span>';
        html += '</label>';
        html += renderCustomFieldInput(field, key, val);
        html += '</div>';
    });
    html += '<button type="button" class="add-cylinder-btn" onclick="openAddFieldModal(\'party\')"><span class="material-symbols-rounded" style="font-size:13px;">add</span> Add Field</button>';
    container.innerHTML = html;
}
```

- [ ] **Step 5: Add cylinder custom columns renderer**

Add this function to `contract.js`:

```javascript
function renderCylinderCustomColumns(fields, values) {
    values = values || {};
    // Add headers to all three tables
    ['art1', 'art4', 'art6'].forEach(function(tableId) {
        var thead = document.querySelector('#' + tableId + '-tbody').closest('table').querySelector('thead tr');
        if (!thead) return;
        // Remove existing custom headers
        thead.querySelectorAll('.custom-col-header').forEach(function(el) { el.remove(); });
        // Add current custom headers
        fields.forEach(function(field) {
            var th = document.createElement('th');
            th.className = 'custom-col-header';
            th.setAttribute('data-field-id', field.id);
            th.innerHTML = escHtml(field.label) + ' <a href="#" onclick="removeCylinderField(\'' + escHtml(field.id) + '\');return false;" style="font-size:10px;color:var(--contract-danger);" title="Remove">✕</a>';
            // Insert before the last th (action column)
            var lastTh = thead.querySelector('th:last-child');
            thead.insertBefore(th, lastTh);
        });
    });
    // Re-render tables to include custom cells
    renderAll();
}
```

- [ ] **Step 6: Add serializeCustomFields function**

Add this function to `contract.js`:

```javascript
function serializeCustomFields() {
    var data = {};
    // Party custom fields
    document.querySelectorAll('#party-custom-fields .custom-field-input').forEach(function(el) {
        var key = el.getAttribute('data-field-key');
        if (!key) return;
        if (el.type === 'checkbox') data[key] = el.checked ? '1' : '';
        else data[key] = el.value;
    });
    // Custom section fields
    document.querySelectorAll('.custom-section .custom-field-input').forEach(function(el) {
        var key = el.getAttribute('data-field-key');
        if (!key) return;
        if (el.type === 'checkbox') data[key] = el.checked ? '1' : '';
        else data[key] = el.value;
    });
    // Cylinder custom fields
    cylinders.forEach(function(cyl, rowIdx) {
        var row = document.querySelector('#art1-tbody tr[data-index="' + rowIdx + '"]');
        if (!row) return;
        row.querySelectorAll('.custom-field-input').forEach(function(el) {
            var baseKey = el.getAttribute('data-field-key');
            if (!baseKey) return;
            data[baseKey + '_' + rowIdx] = el.type === 'checkbox' ? (el.checked ? '1' : '') : el.value;
        });
    });
    // Embed layout definitions so edit.php can restore the form
    data._layout = {
        section_order: getSectionOrder(),
        custom_sections: customFieldState.sections,
        cylinder_custom_fields: customFieldState.cylinderFields,
        party_custom_fields: customFieldState.partyFields
    };
    return data;
}
```

- [ ] **Step 7: Add custom field CRUD functions**

Add these functions to `contract.js`:

```javascript
function addCustomSection(title) {
    var id = generateFieldId('cs');
    customFieldState.sections.push({ id: id, title: title, fields: [] });
    var container = document.getElementById('sections-container');
    var signaturesSection = container.querySelector('[data-section-id="signatures"]');
    var html = renderCustomSection({ id: id, title: title, fields: [] }, {});
    if (signaturesSection) {
        signaturesSection.insertAdjacentHTML('beforebegin', html);
    } else {
        container.insertAdjacentHTML('beforeend', html);
    }
    // Re-init SortableJS to include new section
    reinitSectionSortable();
}

function removeCustomSection(sectionId) {
    customFieldState.sections = customFieldState.sections.filter(function(s) { return s.id !== sectionId; });
    var el = document.querySelector('.custom-section[data-section-id="' + sectionId + '"]');
    if (el) el.remove();
}

function addFieldToSection(sectionId, fieldDef) {
    var section = customFieldState.sections.find(function(s) { return s.id === sectionId; });
    if (!section) return;
    fieldDef.id = generateFieldId('f');
    section.fields.push(fieldDef);
    // Re-render the section
    var el = document.querySelector('.custom-section[data-section-id="' + sectionId + '"]');
    if (el) {
        var values = {};
        el.querySelectorAll('.custom-field-input').forEach(function(inp) {
            var k = inp.getAttribute('data-field-key');
            if (k) values[k] = inp.type === 'checkbox' ? (inp.checked ? '1' : '') : inp.value;
        });
        el.outerHTML = renderCustomSection(section, values);
    }
    reinitSectionSortable();
}

function addCylinderCustomField(fieldDef) {
    fieldDef.id = generateFieldId('cf');
    customFieldState.cylinderFields.push(fieldDef);
    renderCylinderCustomColumns(customFieldState.cylinderFields, {});
}

function removeCylinderField(fieldId) {
    customFieldState.cylinderFields = customFieldState.cylinderFields.filter(function(f) { return f.id !== fieldId; });
    // Remove column headers and re-render
    document.querySelectorAll('.custom-col-header[data-field-id="' + fieldId + '"]').forEach(function(el) { el.remove(); });
    document.querySelectorAll('.custom-col-cell[data-field-id="' + fieldId + '"]').forEach(function(el) { el.remove(); });
    renderAll();
}

function addPartyCustomField(fieldDef) {
    fieldDef.id = generateFieldId('pf');
    customFieldState.partyFields.push(fieldDef);
    renderPartyCustomFields(customFieldState.partyFields, {});
}

function removePartyCustomField(fieldId) {
    customFieldState.partyFields = customFieldState.partyFields.filter(function(f) { return f.id !== fieldId; });
    renderPartyCustomFields(customFieldState.partyFields, {});
}

function reinitSectionSortable() {
    var sectionsContainer = document.getElementById('sections-container');
    if (sectionsContainer && typeof Sortable !== 'undefined') {
        // Destroy existing instance if possible
        var existing = Sortable.get(sectionsContainer);
        if (existing) existing.destroy();
        Sortable.create(sectionsContainer, {
            handle: '.drag-handle',
            animation: 200,
            ghostClass: 'sortable-ghost',
            chosenClass: 'sortable-chosen',
            dragClass: 'sortable-drag'
        });
    }
}
```

- [ ] **Step 8: Extend serializeConditions to include custom fields**

In the existing `serializeConditions()` function in `contract.js`, before setting `conditions-json` value, add:

```javascript
// Serialize custom fields (includes _layout with section order and field definitions)
var cfData = serializeCustomFields();
var cfInput = document.getElementById('custom-fields-json');
if (cfInput) cfInput.value = JSON.stringify(cfData);
```

And add this helper function:

```javascript
function getSectionOrder() {
    var order = [];
    document.querySelectorAll('#sections-container > .contract-form-section').forEach(function(el) {
        order.push(el.getAttribute('data-section-id'));
    });
    return order;
}
```

- [ ] **Step 9: Test custom field functions in browser console**

Load the contract create page. In the browser console, run:
```javascript
addCustomSection('Insurance Details');
addFieldToSection(customFieldState.sections[0].id, {label: 'Policy Number', type: 'text', required: true});
addCylinderCustomField({label: 'Serial Number', type: 'text'});
addPartyCustomField({label: 'ID Card', type: 'text'});
```
Verify sections and fields render correctly.

---

### Task 5: Add Field / Add Section Modals in create.php

**Files:**
- Modify: `contracts/create.php`

- [ ] **Step 1: Add hidden inputs for custom fields and layout**

After the existing hidden inputs (after line ~88), add:

```html
<input type="hidden" name="custom_fields" id="custom-fields-json">
```

- [ ] **Step 2: Add party custom fields container**

Inside the Parties section (after the form-grid divs, before the closing `</div>` of the section-body around line ~139), add:

```html
<div id="party-custom-fields"></div>
<button type="button" class="add-cylinder-btn" onclick="openAddFieldModal('party')" style="margin-top:8px;">
    <span class="material-symbols-rounded" style="font-size:13px;">add</span> Add Field
</button>
```

- [ ] **Step 3: Add the Add Field modal HTML**

Before the closing `</form>` tag (before line ~466), add:

```html
<!-- Add Custom Field Modal -->
<div id="add-field-modal" class="modal-overlay" style="display:none;">
    <div class="modal" style="max-width:420px;">
        <div class="modal-box">
            <div class="modal-header">
                <h3><span class="material-symbols-rounded" style="font-size:16px;">add</span> Add Custom Field</h3>
                <button type="button" class="modal-close" onclick="closeAddFieldModal()">&times;</button>
            </div>
            <div class="modal-body">
                <input type="hidden" id="add-field-target">
                <div class="form-group">
                    <label class="form-label">Field Label *</label>
                    <input type="text" id="add-field-label" class="form-control" placeholder="e.g. Serial Number" required>
                </div>
                <div class="form-group">
                    <label class="form-label">Field Type</label>
                    <select id="add-field-type" class="form-control" onchange="toggleFieldOptions()">
                        <option value="text">Text</option>
                        <option value="number">Number</option>
                        <option value="date">Date</option>
                        <option value="select">Dropdown (Select)</option>
                        <option value="checkbox">Checkbox</option>
                        <option value="textarea">Text Area</option>
                    </select>
                </div>
                <div class="form-group" id="add-field-options-group" style="display:none;">
                    <label class="form-label">Options (comma-separated)</label>
                    <input type="text" id="add-field-options" class="form-control" placeholder="e.g. Red, Blue, Green">
                </div>
                <div class="form-group">
                    <label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">
                        <input type="checkbox" id="add-field-required" style="width:15px;height:15px;">
                        Required field
                    </label>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" onclick="closeAddFieldModal()">Cancel</button>
                <button type="button" class="btn btn-primary" style="background:var(--contract-primary);color:#fff;" onclick="confirmAddField()">Add Field</button>
            </div>
        </div>
    </div>
</div>
```

- [ ] **Step 4: Add the Add Section modal HTML**

After the Add Field modal, add:

```html
<!-- Add Custom Section Modal -->
<div id="add-section-modal" class="modal-overlay" style="display:none;">
    <div class="modal" style="max-width:400px;">
        <div class="modal-box">
            <div class="modal-header">
                <h3><span class="material-symbols-rounded" style="font-size:16px;">add</span> Add Custom Section</h3>
                <button type="button" class="modal-close" onclick="closeAddSectionModal()">&times;</button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label class="form-label">Section Title *</label>
                    <input type="text" id="add-section-title" class="form-control" placeholder="e.g. Insurance Details" required>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" onclick="closeAddSectionModal()">Cancel</button>
                <button type="button" class="btn btn-primary" style="background:var(--contract-primary);color:#fff;" onclick="confirmAddSection()">Add Section</button>
            </div>
        </div>
    </div>
</div>
```

- [ ] **Step 5: Add the Add Column modal HTML**

After the Add Section modal, add:

```html
<!-- Add Cylinder Column Modal -->
<div id="add-column-modal" class="modal-overlay" style="display:none;">
    <div class="modal" style="max-width:400px;">
        <div class="modal-box">
            <div class="modal-header">
                <h3><span class="material-symbols-rounded" style="font-size:16px;">add</span> Add Cylinder Column</h3>
                <button type="button" class="modal-close" onclick="closeAddColumnModal()">&times;</button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label class="form-label">Column Name *</label>
                    <input type="text" id="add-column-label" class="form-control" placeholder="e.g. Serial Number" required>
                </div>
                <div class="form-group">
                    <label class="form-label">Field Type</label>
                    <select id="add-column-type" class="form-control">
                        <option value="text">Text</option>
                        <option value="number">Number</option>
                        <option value="date">Date</option>
                    </select>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" onclick="closeAddColumnModal()">Cancel</button>
                <button type="button" class="btn btn-primary" style="background:var(--contract-primary);color:#fff;" onclick="confirmAddColumn()">Add Column</button>
            </div>
        </div>
    </div>
</div>
```

- [ ] **Step 6: Add modal JS functions**

In the `<script>` block at the bottom of `create.php`, add:

```javascript
// Add Field Modal
function openAddFieldModal(target) {
    document.getElementById('add-field-target').value = target;
    document.getElementById('add-field-label').value = '';
    document.getElementById('add-field-type').value = 'text';
    document.getElementById('add-field-options').value = '';
    document.getElementById('add-field-required').checked = false;
    document.getElementById('add-field-options-group').style.display = 'none';
    document.getElementById('add-field-modal').style.display = '';
}
function closeAddFieldModal() {
    document.getElementById('add-field-modal').style.display = 'none';
}
function toggleFieldOptions() {
    var show = document.getElementById('add-field-type').value === 'select';
    document.getElementById('add-field-options-group').style.display = show ? '' : 'none';
}
function confirmAddField() {
    var target = document.getElementById('add-field-target').value;
    var label = document.getElementById('add-field-label').value.trim();
    if (!label) { alert('Field label is required'); return; }
    var fieldDef = {
        label: label,
        type: document.getElementById('add-field-type').value,
        required: document.getElementById('add-field-required').checked
    };
    if (fieldDef.type === 'select') {
        fieldDef.options = document.getElementById('add-field-options').value.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
    }
    if (target === 'party') {
        addPartyCustomField(fieldDef);
    } else {
        addFieldToSection(target, fieldDef);
    }
    closeAddFieldModal();
}

// Add Section Modal
function openAddSectionModal() {
    document.getElementById('add-section-title').value = '';
    document.getElementById('add-section-modal').style.display = '';
}
function closeAddSectionModal() {
    document.getElementById('add-section-modal').style.display = 'none';
}
function confirmAddSection() {
    var title = document.getElementById('add-section-title').value.trim();
    if (!title) { alert('Section title is required'); return; }
    addCustomSection(title);
    closeAddSectionModal();
}

// Add Column Modal
function openAddColumnModal() {
    document.getElementById('add-column-label').value = '';
    document.getElementById('add-column-type').value = 'text';
    document.getElementById('add-column-modal').style.display = '';
}
function closeAddColumnModal() {
    document.getElementById('add-column-modal').style.display = 'none';
}
function confirmAddColumn() {
    var label = document.getElementById('add-column-label').value.trim();
    if (!label) { alert('Column name is required'); return; }
    addCylinderCustomField({ label: label, type: document.getElementById('add-column-type').value });
    closeAddColumnModal();
}
```

- [ ] **Step 7: Update form submit handler to serialize custom fields**

In the existing `document.addEventListener('DOMContentLoaded', ...)` block, update:

```javascript
document.addEventListener('DOMContentLoaded', function() {
    var form = document.querySelector('form.contract-form');
    if (form) form.addEventListener('submit', function() {
        serializeConditions();
        // Custom fields and layout are serialized inside serializeConditions
    });
});
```

- [ ] **Step 8: Test the full create flow**

1. Load contract create page
2. Click "+ Add Custom Section", enter "Insurance Details"
3. Click "+ Add Field" in the new section, add a text field "Policy Number"
4. Click "+ Add Column" in Article 1, add "Serial Number"
5. Click "+ Add Field" in Parties section, add "ID Card Number"
6. Drag a section to reorder
7. Submit the form
8. Check the POST data includes `custom_fields` and `layout` JSON

---

### Task 6: Save Custom Fields in save.php

**Files:**
- Modify: `contracts/save.php`

- [ ] **Step 1: Accept custom_fields and layout POST data**

In `contracts/save.php`, after the CSRF check (around line 26), add:

```php
// Custom fields (includes _layout with section order and field definitions)
$custom_fields_json = trim($_POST['custom_fields'] ?? '');
```

- [ ] **Step 2: Add auto-migration for new columns**

After the existing logo/signature column migration (around line 19), add:

```php
// Auto-migrate: custom_fields column
$cfCol = @mysqli_query($conn, "SHOW COLUMNS FROM contracts LIKE 'custom_fields'");
if (mysqli_num_rows($cfCol) === 0) {
    @mysqli_query($conn, "ALTER TABLE contracts ADD COLUMN custom_fields JSON DEFAULT NULL");
}
```

- [ ] **Step 3: Include custom_fields in INSERT query**

Find the INSERT INTO contracts query (around line 147 in the original). Add `custom_fields` to the column list and values. The column list should include `custom_fields` and the value should be the escaped JSON:

```php
$cfEsc = $custom_fields_json ? "'" . $esc($custom_fields_json) . "'" : "NULL";
```

Add `custom_fields` to the INSERT column list and `$cfEsc` to the VALUES.

- [ ] **Step 4: Include custom_fields in UPDATE query**

Find the UPDATE contracts query. Add:

```php
custom_fields=$cfEsc
```

to the SET clause.

- [ ] **Step 5: Test save**

Create a contract with custom fields. Check the database:
```sql
SELECT custom_fields FROM contracts ORDER BY id DESC LIMIT 1;
```
Expected: JSON with custom field values.

---

### Task 7: Template Save/Load with Layout

**Files:**
- Modify: `contracts/template_save.php`
- Modify: `contracts/template_load.php`

- [ ] **Step 1: Add layout migration to template_save.php**

After the existing logo/signature migration in `template_save.php` (around line 47), add:

```php
// Auto-migrate: layout column
$layoutCol = @mysqli_query($conn, "SHOW COLUMNS FROM contract_templates LIKE 'layout'");
if (mysqli_num_rows($layoutCol) === 0) {
    @mysqli_query($conn, "ALTER TABLE contract_templates ADD COLUMN layout JSON DEFAULT NULL");
}
```

- [ ] **Step 2: Accept layout POST data**

After the CSRF check, add:

```php
$layout = trim($_POST['layout'] ?? '');
```

- [ ] **Step 3: Include layout in template INSERT and UPDATE**

In the UPDATE query (around line 154), add to the SET clause:

```php
layout='" . $esc($layout) . "'
```

In the INSERT query (around line 179), add `layout` to the column list and `'" . $esc($layout) . "'` to the values.

- [ ] **Step 4: Return layout in template_load.php**

In `template_load.php`, after decoding JSON fields (around line 31), add:

```php
$template['layout'] = json_decode($template['layout'] ?? '{}', true) ?: [];
```

- [ ] **Step 5: Test template save/load**

1. Create a contract with custom sections and fields
2. Click "Save as Template"
3. Create a new contract, select the saved template
4. Verify custom sections and fields appear

---

### Task 8: Apply Layout on Template Load (JS)

**Files:**
- Modify: `assets/js/contract.js`

- [ ] **Step 1: Extend applyTemplate to handle layout**

In `contract.js`, modify the `applyTemplate` function. After the existing conditions/accessories/disputes handling, add:

```javascript
// Apply layout (section order + custom fields)
var layout = tpl.layout || {};
if (typeof layout === 'string') try { layout = JSON.parse(layout); } catch(e) { layout = {}; }

// Apply section order
if (layout.section_order && layout.section_order.length > 0) {
    var container = document.getElementById('sections-container');
    layout.section_order.forEach(function(sectionId) {
        var section = container.querySelector('[data-section-id="' + sectionId + '"]');
        if (section) container.appendChild(section);
    });
    reinitSectionSortable();
}

// Apply custom sections
customFieldState.sections = layout.custom_sections || [];
customFieldState.cylinderFields = layout.cylinder_custom_fields || [];
customFieldState.partyFields = layout.party_custom_fields || [];

// Find max ID to avoid conflicts
var allIds = [];
customFieldState.sections.forEach(function(s) {
    allIds.push(s.id);
    s.fields.forEach(function(f) { allIds.push(f.id); });
});
customFieldState.cylinderFields.forEach(function(f) { allIds.push(f.id); });
customFieldState.partyFields.forEach(function(f) { allIds.push(f.id); });
customFieldState.nextId = allIds.length + 1;

// Remove existing custom sections
document.querySelectorAll('.custom-section').forEach(function(el) { el.remove(); });

// Render custom sections
customFieldState.sections.forEach(function(section) {
    var container = document.getElementById('sections-container');
    var signaturesSection = container.querySelector('[data-section-id="signatures"]');
    var html = renderCustomSection(section, {});
    if (signaturesSection) {
        signaturesSection.insertAdjacentHTML('beforebegin', html);
    } else {
        container.insertAdjacentHTML('beforeend', html);
    }
});

// Render party custom fields
renderPartyCustomFields(customFieldState.partyFields, {});

// Render cylinder custom columns
renderCylinderCustomColumns(customFieldState.cylinderFields, {});
```

- [ ] **Step 2: Test template application**

1. Create a contract with custom sections, cylinder columns, and party fields
2. Save as template
3. Create a new blank contract
4. Select the saved template
5. Verify: sections reorder, custom sections appear, cylinder columns appear, party fields appear

---

### Task 9: Edit.php — Load Custom Fields

**Files:**
- Modify: `contracts/edit.php`

- [ ] **Step 1: Load custom_fields in PHP**

At the top of `edit.php`, after loading the contract (around line 12), add:

```php
$custom_fields = json_decode($contract['custom_fields'] ?? '{}', true) ?: [];
// Layout definitions are embedded in custom_fields._layout by the save flow
$layout = $custom_fields['_layout'] ?? ['section_order' => [], 'custom_sections' => [], 'cylinder_custom_fields' => [], 'party_custom_fields' => []];
```

- [ ] **Step 2: Add SortableJS CDN and data-section-id attributes**

Apply the same changes as Task 2 Steps 1-4 to `edit.php` (SortableJS CDN, data-section-id on sections, drag handles, sections-container wrapper).

- [ ] **Step 3: Add hidden inputs for custom_fields and layout**

Same as Task 5 Step 1.

- [ ] **Step 4: Add party custom fields container and add-field buttons**

Same as Task 5 Step 2.

- [ ] **Step 5: Add modal HTML**

Same as Task 5 Steps 3-5 (copy the three modal HTML blocks).

- [ ] **Step 6: Initialize with existing data**

In the `<script>` block at the bottom of `edit.php`, add:

```javascript
// Initialize custom fields from contract data
var existingLayout = <?= json_encode($layout) ?>;
var existingValues = <?= json_encode($custom_fields) ?>;

customFieldState.sections = existingLayout.custom_sections || [];
customFieldState.cylinderFields = existingLayout.cylinder_custom_fields || [];
customFieldState.partyFields = existingLayout.party_custom_fields || [];

// Apply section order
if (existingLayout.section_order && existingLayout.section_order.length > 0) {
    var container = document.getElementById('sections-container');
    existingLayout.section_order.forEach(function(sectionId) {
        var section = container.querySelector('[data-section-id="' + sectionId + '"]');
        if (section) container.appendChild(section);
    });
}

// Render custom sections
customFieldState.sections.forEach(function(section) {
    var container = document.getElementById('sections-container');
    var signaturesSection = container.querySelector('[data-section-id="signatures"]');
    var html = renderCustomSection(section, existingValues);
    if (signaturesSection) {
        signaturesSection.insertAdjacentHTML('beforebegin', html);
    } else {
        container.insertAdjacentHTML('beforeend', html);
    }
});

// Render party custom fields
renderPartyCustomFields(customFieldState.partyFields, existingValues);

// Render cylinder custom columns
renderCylinderCustomColumns(customFieldState.cylinderFields, existingValues);
```

- [ ] **Step 7: Add modal JS functions**

Same as Task 5 Step 6 (copy modal functions).

- [ ] **Step 8: Update form submit handler**

Same as Task 5 Step 7.

- [ ] **Step 9: Test edit flow**

1. Create a contract with custom fields, save it
2. Edit the contract
3. Verify: section order preserved, custom sections show values, cylinder columns show values, party fields show values
4. Modify a custom field value, save
5. Edit again, verify updated value persists

---

### Task 10: Print View with Custom Fields

**Files:**
- Modify: `contracts/print.php`

- [ ] **Step 1: Load layout and custom_fields in print.php**

At the top of `print.php`, after loading the contract, add:

```php
$custom_fields = json_decode($contract['custom_fields'] ?? '{}', true) ?: [];
$layout = ['section_order' => [], 'custom_sections' => [], 'cylinder_custom_fields' => [], 'party_custom_fields' => []];
$tplId = intval($contract['template_id'] ?? 0);
if ($tplId > 0) {
    $tplQ = mysqli_query($conn, "SELECT layout FROM contract_templates WHERE id=$tplId");
    if ($tplQ && mysqli_num_rows($tplQ) > 0) {
        $tplRow = mysqli_fetch_assoc($tplQ);
        $layout = json_decode($tplRow['layout'] ?? '{}', true) ?: $layout;
    }
}
```

- [ ] **Step 2: Add custom cylinder columns to print table**

In the cylinder loan table in `print.php`, after the existing `<th>` headers, add:

```php
<?php foreach (($layout['cylinder_custom_fields'] ?? []) as $cf): ?>
    <th><?= htmlspecialchars($cf['label']) ?></th>
<?php endforeach; ?>
```

And in each `<tr>` for cylinder data, add corresponding `<td>`:

```php
<?php foreach (($layout['cylinder_custom_fields'] ?? []) as $cf): ?>
    <td><?= htmlspecialchars($custom_fields[$cf['id'] . '_' . $i] ?? '') ?></td>
<?php endforeach; ?>
```

- [ ] **Step 3: Add party custom fields to print**

After the existing party details section in the print view, add:

```php
<?php if (!empty($layout['party_custom_fields'])): ?>
    <?php foreach ($layout['party_custom_fields'] as $pf): ?>
        <p><strong><?= htmlspecialchars($pf['label']) ?>:</strong> <?= htmlspecialchars($custom_fields['pf_' . $pf['id']] ?? '') ?></p>
    <?php endforeach; ?>
<?php endif; ?>
```

- [ ] **Step 4: Render custom sections in print**

Before the signatures section in the print view, add:

```php
<?php foreach (($layout['custom_sections'] ?? []) as $cs): ?>
    <div style="margin-top:20px;">
        <h3><?= htmlspecialchars($cs['title']) ?></h3>
        <?php foreach ($cs['fields'] as $f): ?>
            <p><strong><?= htmlspecialchars($f['label']) ?>:</strong> <?= htmlspecialchars($custom_fields['cs_' . $cs['id'] . '_' . $f['id']] ?? '') ?></p>
        <?php endforeach; ?>
    </div>
<?php endforeach; ?>
```

- [ ] **Step 5: Test print**

1. Create a contract with custom fields
2. Open print view
3. Verify custom sections, cylinder columns, and party fields appear in print output

---

### Task 11: View Page Custom Fields

**Files:**
- Modify: `contracts/view.php`

- [ ] **Step 1: Load layout and custom_fields**

Same as Task 10 Step 1.

- [ ] **Step 2: Display custom fields in view**

In the contract detail view, after the existing sections, add the custom sections, cylinder custom columns, and party custom fields using the same pattern as the print view (read-only display).

- [ ] **Step 3: Test view page**

Open an existing contract with custom fields. Verify all custom data displays correctly.

---

### Task 12: CSS Styles

**Files:**
- Modify: `assets/css/contract.css`

- [ ] **Step 1: Add drag handle styles**

Add to `contract.css`:

```css
/* Drag Handle */
.drag-handle {
    cursor: grab;
    font-size: 18px;
    color: var(--contract-text-muted);
    margin-right: 8px;
    user-select: none;
    opacity: 0.5;
    transition: opacity 0.2s;
}
.drag-handle:hover {
    opacity: 1;
    color: var(--contract-primary);
}
.section-header {
    cursor: default;
}

/* Row drag handle */
.row-drag-handle {
    cursor: grab;
    color: var(--contract-text-muted);
    user-select: none;
    opacity: 0.4;
    transition: opacity 0.2s;
}
.row-drag-handle:hover {
    opacity: 1;
}

/* SortableJS classes */
.sortable-ghost {
    opacity: 0.4;
    background: var(--contract-primary-light);
    border: 2px dashed var(--contract-primary);
    border-radius: var(--contract-radius);
}
.sortable-chosen {
    box-shadow: var(--contract-shadow-lg);
}
.sortable-drag {
    opacity: 0.9;
}
```

- [ ] **Step 2: Add custom field styles**

Add to `contract.css`:

```css
/* Custom Fields */
.custom-field-wrapper {
    position: relative;
}
.custom-field-actions {
    display: none;
    font-size: 11px;
    margin-left: 6px;
}
.custom-field-wrapper:hover .custom-field-actions {
    display: inline;
}
.custom-field-actions a {
    text-decoration: none;
    margin: 0 2px;
}

/* Custom Section */
.custom-section {
    border: 1px dashed var(--contract-border);
    border-left: 3px solid var(--contract-primary);
}
.custom-section .section-header {
    background: var(--contract-primary-light);
}

/* Custom column in cylinder table */
.custom-col-header {
    font-size: 11px;
    white-space: nowrap;
}
.custom-col-cell input,
.custom-col-cell select {
    font-size: 11px;
    padding: 3px 6px;
}
```

- [ ] **Step 3: Verify visual appearance**

Load the contract form. Check:
- Drag handles are visible but subtle (low opacity)
- Drag handles highlight on hover
- Ghost/chosen effects during drag
- Custom sections have distinct visual styling
- Custom field edit/delete icons appear on hover

---

### Task 13: Update customers/index.php Template Loading

**Files:**
- Modify: `customers/index.php`

- [ ] **Step 1: Include layout in template data**

In `customers/index.php`, where templates are loaded (around line 56-64), add `layout` to the decoded fields:

```php
$row['layout'] = json_decode($row['layout'] ?? '{}', true) ?: [];
```

This ensures the layout data is available when templates are rendered on the customer page contract tab.

- [ ] **Step 2: Verify template cards show layout info**

Load the customers page, go to the cylinder contract tab. Select a template that has custom fields. Verify the form updates correctly.
