+
+
+
+
+
+
+
+
+
diff --git a/db.md b/db.md
new file mode 100644
index 00000000..a889580d
--- /dev/null
+++ b/db.md
@@ -0,0 +1,341 @@
+# Non-EB Claims Module - Database Changes
+
+## 1. New Table: `non_eb_ticket_master`
+
+```sql
+CREATE TABLE `non_eb_ticket_master` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ -- Section 1: Policy & Account
+ `acm_id` INT UNSIGNED DEFAULT NULL,
+ `client_id` INT UNSIGNED DEFAULT NULL,
+ `branch_id` INT UNSIGNED DEFAULT NULL,
+ `client_policy_id` INT UNSIGNED DEFAULT NULL,
+ `policy_type_id` INT UNSIGNED DEFAULT NULL,
+ `policy_no` VARCHAR(100) DEFAULT NULL,
+ `policy_section` VARCHAR(255) DEFAULT NULL,
+ `policy_period` VARCHAR(50) DEFAULT NULL,
+ `insurer_id` INT UNSIGNED DEFAULT NULL,
+ -- Section 2: Loss / Incident
+ `nature_of_loss` VARCHAR(255) DEFAULT NULL,
+ `loss_description` TEXT DEFAULT NULL,
+ `loss_location` VARCHAR(255) DEFAULT NULL,
+ `loss_date` DATE DEFAULT NULL,
+ `loss_estimate` DECIMAL(15,2) DEFAULT NULL,
+ -- Section 3: Intimation & Ref
+ `intimation_recd_date` DATE DEFAULT NULL,
+ `intimated_to_insurer_date` DATE DEFAULT NULL,
+ `nhance_claim_ref_no` VARCHAR(100) DEFAULT NULL,
+ `claim_number` VARCHAR(100) DEFAULT NULL,
+ -- Section 4: Status
+ `claim_status_id` INT UNSIGNED DEFAULT NULL,
+ `surveyor_file_ref_no` VARCHAR(100) DEFAULT NULL,
+ -- Section 5: Surveyor
+ `surveyor_name` VARCHAR(255) DEFAULT NULL,
+ `surveyor_contact_person` VARCHAR(255) DEFAULT NULL,
+ `surveyor_contact_number` VARCHAR(20) DEFAULT NULL,
+ `surveyor_email` VARCHAR(255) DEFAULT NULL,
+ `lor` TEXT DEFAULT NULL,
+ `surveyor_remarks` TEXT DEFAULT NULL,
+ -- Section 6: Insured Contact
+ `insured_contact_name` VARCHAR(255) DEFAULT NULL,
+ `insured_contact_number` VARCHAR(20) DEFAULT NULL,
+ `insured_contact_email` VARCHAR(255) DEFAULT NULL,
+ -- Section 7: Documents
+ `google_drive_links` TEXT DEFAULT NULL,
+ `documents_required` TEXT DEFAULT NULL,
+ `documents_submitted` TEXT DEFAULT NULL,
+ `pending_documents` TEXT DEFAULT NULL,
+ `eta_for_documents` DATE DEFAULT NULL,
+ -- Section 8: Settlement
+ `loss_assessed_value` DECIMAL(15,2) DEFAULT NULL,
+ `settled_amount` DECIMAL(15,2) DEFAULT NULL,
+ `settlement_utr` VARCHAR(100) DEFAULT NULL,
+ -- Asset File (uploaded alternative to manual asset rows)
+ `asset_file` VARCHAR(255) DEFAULT NULL,
+ -- System
+ `priority` TINYINT UNSIGNED DEFAULT NULL,
+ `is_active` TINYINT(1) NOT NULL DEFAULT 1,
+ `created_by` INT UNSIGNED DEFAULT NULL,
+ `updated_by` INT UNSIGNED DEFAULT NULL,
+ `last_updated_by` INT UNSIGNED DEFAULT NULL,
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `idx_claim_status` (`claim_status_id`),
+ KEY `idx_client` (`client_id`),
+ KEY `idx_branch` (`branch_id`),
+ KEY `idx_acm` (`acm_id`),
+ KEY `idx_insurer` (`insurer_id`),
+ KEY `idx_policy_type` (`policy_type_id`),
+ KEY `idx_is_active` (`is_active`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+## 2. New Table: `non_eb_claim_assets`
+
+```sql
+CREATE TABLE `non_eb_claim_assets` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `non_eb_ticket_id` INT UNSIGNED NOT NULL,
+ `asset_id_code` VARCHAR(100) DEFAULT NULL,
+ `serial_no` VARCHAR(100) DEFAULT NULL,
+ `vehicle_no` VARCHAR(50) DEFAULT NULL,
+ `asset_description` VARCHAR(255) DEFAULT NULL,
+ `is_active` TINYINT(1) NOT NULL DEFAULT 1,
+ `created_by` INT UNSIGNED DEFAULT NULL,
+ `updated_by` INT UNSIGNED DEFAULT NULL,
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `idx_ticket` (`non_eb_ticket_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+## 3. Reused Tables (No Changes Needed)
+
+- `ticket_claim_status` - Add Non-EB sub-statuses with `ticket_type` = `policy_type.id`
+- `ticket_history` - Reuse for field change tracking
+- `ticket_notes` - Reuse for notes
+- `ticket_messages` - Reuse for conversation/replies
+- `ticket_mail_template` - Reuse for email templates
+- `ticket_check_list` - Reuse for document checklists
+
+## 4. Status Seed Data (per policy type)
+
+For each Non-EB policy type (from `policy_type` table where `allocg IN ('Non-EB', 'Marine')`), insert these sub-statuses into `ticket_claim_status`:
+
+| Sub Status (claim_status) | Display Name (Status for user) | allowed_status |
+|---|---|---|
+| Claim Intimation - Insured | Claim Intimation Received | [next_id] |
+| Claim Intimation - Insurer | Claim Intimated | [next_id] |
+| Survey Appointment - Awaited | Insurer Pending | [next_id] |
+| Surveyor Appointed | Surveyor Pending | [next_id, lor_awaited_id] |
+| LOR Awaited - Surveyor | Surveyor Pending | [next_id] |
+| Documents Awaited - Insured | Insured Pending | [next_id, partial_docs_id] |
+| Partial Documents Awaited - Insured | Insured Pending | [next_id] |
+| Documents Submitted - Awaiting Loss Assessment | Insurer / Surveyor Pending | [next_id, discrepancy_id] |
+| Loss Assessment - Discrepancy | Insurer / Surveyor Pending | [next_id] |
+| Loss Assessment - Consent Awaited - Insured | Insured Pending | [next_id] |
+| Consent Agreed | Insurer / Surveyor Pending | [approved_id] |
+| Claim Approved | Claim Approved | [settled_id] |
+| Claim Settled | Claim Settled | [] |
+| Claim Closed | Claim Closed | [] |
+| Claim Rejected | Claim Rejected | [] |
+| Claim Withdrawn | Claim Withdrawn | [] |
+| Workshop Pending | Workshop Pending | [next_id] |
+
+> Note: `allowed_status` JSON should contain the IDs of the next valid statuses after insert.
+
+---
+
+## 5. ALTER TABLE Queries (for existing databases)
+
+Run these if the tables already exist and need the new columns:
+
+```sql
+-- Add branch_id column
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `branch_id` INT UNSIGNED DEFAULT NULL AFTER `client_id`;
+ALTER TABLE `non_eb_ticket_master` ADD KEY `idx_branch` (`branch_id`);
+
+-- Add asset_file column
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `asset_file` VARCHAR(255) DEFAULT NULL AFTER `settlement_utr`;
+
+-- Add closure_remark column
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `closure_remark` TEXT DEFAULT NULL AFTER `asset_file`;
+
+-- Add policy date columns
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `policy_start_date` VARCHAR(20) DEFAULT NULL AFTER `policy_period`;
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `policy_end_date` VARCHAR(20) DEFAULT NULL AFTER `policy_start_date`;
+
+-- Optional: Drop legacy columns (only after confirming no existing data needed)
+-- ALTER TABLE `non_eb_ticket_master` DROP COLUMN `policy_period`;
+-- ALTER TABLE `non_eb_ticket_master` DROP COLUMN `policy_section`;
+
+-- Add required_docs column for IR Documents checklist (Claim Files tab)
+ALTER TABLE `non_eb_ticket_master` ADD COLUMN `required_docs` TEXT DEFAULT NULL AFTER `closure_remark`;
+
+-- Add ticket_type flag to claim_files to distinguish EB (1) vs Non-EB (2) records
+ALTER TABLE `claim_files` ADD COLUMN `ticket_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=EB, 2=Non-EB' AFTER `ticket_id`;
+ALTER TABLE `claim_files` ADD KEY `idx_ticket_type` (`ticket_type`);
+```
+
+---
+
+## 6. Trigger: `non_eb_ticket_master_after_update`
+
+Tracks field-level changes into `ticket_history` on every UPDATE.
+
+```sql
+DELIMITER $$
+
+CREATE TRIGGER `non_eb_ticket_master_after_update`
+AFTER UPDATE ON `non_eb_ticket_master`
+FOR EACH ROW
+BEGIN
+
+ -- Claim Status
+ IF (OLD.claim_status_id IS NULL OR OLD.claim_status_id != NEW.claim_status_id) AND NEW.claim_status_id IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'claim_status_id', 'Claim Status', OLD.claim_status_id, NEW.claim_status_id, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Priority
+ IF (OLD.priority IS NULL OR OLD.priority != NEW.priority) AND NEW.priority IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'priority', 'Priority', OLD.priority, NEW.priority, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Account Manager
+ IF (OLD.acm_id IS NULL OR OLD.acm_id != NEW.acm_id) AND NEW.acm_id IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'acm_id', 'Account Manager', OLD.acm_id, NEW.acm_id, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Nature of Loss
+ IF (OLD.nature_of_loss IS NULL OR OLD.nature_of_loss != NEW.nature_of_loss) AND NEW.nature_of_loss IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'nature_of_loss', 'Nature of Loss', OLD.nature_of_loss, NEW.nature_of_loss, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Loss Location
+ IF (OLD.loss_location IS NULL OR OLD.loss_location != NEW.loss_location) AND NEW.loss_location IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'loss_location', 'Loss Location', OLD.loss_location, NEW.loss_location, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Loss Date
+ IF (OLD.loss_date IS NULL OR OLD.loss_date != NEW.loss_date) AND NEW.loss_date IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'loss_date', 'Loss Date', OLD.loss_date, NEW.loss_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Loss Estimate
+ IF (OLD.loss_estimate IS NULL OR OLD.loss_estimate != NEW.loss_estimate) AND NEW.loss_estimate IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'loss_estimate', 'Loss Estimate', OLD.loss_estimate, NEW.loss_estimate, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Loss Description (truncated to 500 chars)
+ IF (OLD.loss_description IS NULL OR OLD.loss_description != NEW.loss_description) AND NEW.loss_description IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'loss_description', 'Loss Description', LEFT(OLD.loss_description, 500), LEFT(NEW.loss_description, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Intimation Recd Date
+ IF (OLD.intimation_recd_date IS NULL OR OLD.intimation_recd_date != NEW.intimation_recd_date) AND NEW.intimation_recd_date IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'intimation_recd_date', 'Intimation Recd Date', OLD.intimation_recd_date, NEW.intimation_recd_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Intimated to Insurer Date
+ IF (OLD.intimated_to_insurer_date IS NULL OR OLD.intimated_to_insurer_date != NEW.intimated_to_insurer_date) AND NEW.intimated_to_insurer_date IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'intimated_to_insurer_date', 'Intimated to Insurer Date', OLD.intimated_to_insurer_date, NEW.intimated_to_insurer_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Nhance Claim Ref No
+ IF (OLD.nhance_claim_ref_no IS NULL OR OLD.nhance_claim_ref_no != NEW.nhance_claim_ref_no) AND NEW.nhance_claim_ref_no IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'nhance_claim_ref_no', 'Nhance Claim Ref No', OLD.nhance_claim_ref_no, NEW.nhance_claim_ref_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Claim Number
+ IF (OLD.claim_number IS NULL OR OLD.claim_number != NEW.claim_number) AND NEW.claim_number IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'claim_number', 'Claim Number', OLD.claim_number, NEW.claim_number, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor File Ref No
+ IF (OLD.surveyor_file_ref_no IS NULL OR OLD.surveyor_file_ref_no != NEW.surveyor_file_ref_no) AND NEW.surveyor_file_ref_no IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_file_ref_no', 'Surveyor File Ref No', OLD.surveyor_file_ref_no, NEW.surveyor_file_ref_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor Name
+ IF (OLD.surveyor_name IS NULL OR OLD.surveyor_name != NEW.surveyor_name) AND NEW.surveyor_name IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_name', 'Surveyor Name', OLD.surveyor_name, NEW.surveyor_name, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor Contact Person
+ IF (OLD.surveyor_contact_person IS NULL OR OLD.surveyor_contact_person != NEW.surveyor_contact_person) AND NEW.surveyor_contact_person IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_contact_person', 'Surveyor Contact Person', OLD.surveyor_contact_person, NEW.surveyor_contact_person, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor Contact Number
+ IF (OLD.surveyor_contact_number IS NULL OR OLD.surveyor_contact_number != NEW.surveyor_contact_number) AND NEW.surveyor_contact_number IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_contact_number', 'Surveyor Contact Number', OLD.surveyor_contact_number, NEW.surveyor_contact_number, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor Email
+ IF (OLD.surveyor_email IS NULL OR OLD.surveyor_email != NEW.surveyor_email) AND NEW.surveyor_email IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_email', 'Surveyor Email', OLD.surveyor_email, NEW.surveyor_email, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- LOR (truncated to 500 chars)
+ IF (OLD.lor IS NULL OR OLD.lor != NEW.lor) AND NEW.lor IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'lor', 'LOR', LEFT(OLD.lor, 500), LEFT(NEW.lor, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Surveyor Remarks (truncated to 500 chars)
+ IF (OLD.surveyor_remarks IS NULL OR OLD.surveyor_remarks != NEW.surveyor_remarks) AND NEW.surveyor_remarks IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'surveyor_remarks', 'Surveyor Remarks', LEFT(OLD.surveyor_remarks, 500), LEFT(NEW.surveyor_remarks, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Loss Assessed Value
+ IF (OLD.loss_assessed_value IS NULL OR OLD.loss_assessed_value != NEW.loss_assessed_value) AND NEW.loss_assessed_value IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'loss_assessed_value', 'Loss Assessed Value', OLD.loss_assessed_value, NEW.loss_assessed_value, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Settled Amount
+ IF (OLD.settled_amount IS NULL OR OLD.settled_amount != NEW.settled_amount) AND NEW.settled_amount IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'settled_amount', 'Settled Amount', OLD.settled_amount, NEW.settled_amount, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Settlement UTR
+ IF (OLD.settlement_utr IS NULL OR OLD.settlement_utr != NEW.settlement_utr) AND NEW.settlement_utr IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'settlement_utr', 'Settlement UTR', OLD.settlement_utr, NEW.settlement_utr, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Closure Remark (truncated to 500 chars)
+ IF (OLD.closure_remark IS NULL OR OLD.closure_remark != NEW.closure_remark) AND NEW.closure_remark IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'closure_remark', 'Closure Remark', LEFT(OLD.closure_remark, 500), LEFT(NEW.closure_remark, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Policy No
+ IF (OLD.policy_no IS NULL OR OLD.policy_no != NEW.policy_no) AND NEW.policy_no IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'policy_no', 'Policy No', OLD.policy_no, NEW.policy_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Policy Start Date
+ IF (OLD.policy_start_date IS NULL OR OLD.policy_start_date != NEW.policy_start_date) AND NEW.policy_start_date IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'policy_start_date', 'Policy Start Date', OLD.policy_start_date, NEW.policy_start_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- Policy Expiry Date
+ IF (OLD.policy_end_date IS NULL OR OLD.policy_end_date != NEW.policy_end_date) AND NEW.policy_end_date IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'policy_end_date', 'Policy Expiry Date', OLD.policy_end_date, NEW.policy_end_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+ -- ETA for Documents
+ IF (OLD.eta_for_documents IS NULL OR OLD.eta_for_documents != NEW.eta_for_documents) AND NEW.eta_for_documents IS NOT NULL THEN
+ INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
+ VALUES (OLD.id, 'eta_for_documents', 'ETA for Documents', OLD.eta_for_documents, NEW.eta_for_documents, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
+ END IF;
+
+END$$
+
+DELIMITER ;
+```
diff --git a/noneb.md b/noneb.md
new file mode 100644
index 00000000..22cbfc8d
--- /dev/null
+++ b/noneb.md
@@ -0,0 +1,257 @@
+# Non-EB Claims Module — Development Summary
+
+## Overview
+Non-EB (Non-Employee Benefits) Claims module for nhance. Handles commercial/property/asset insurance claims (All Risk, Marine, Aviation, Cyber, etc. — 120+ policy types). Mirrors the existing EB Claims (TicketController) architecture.
+
+Built across 4 sessions: 2026-03-24 (prerequisites), 2026-03-26 (form UI + edit sync sessions 2–3), 2026-03-27 (edit read-only + Claim Files tab, session 4).
+
+---
+
+## Files Created
+
+### Controller
+- **`app/Controllers/NonEbClaimController.php`**
+ - Extends BaseController, uses ResponseTrait
+ - Methods: `claimList`, `claimForm`, `createClaim`, `view_claim`, `updateClaim`, `removeClaim`, `mailTemplate`, `crudTemplate`, `crudNote`, `saveReply`, `claimReports`, `getBranchAndPolicyByClientID`, `getVisibleSectionsAjax`, `getMoreInfo`, `uploadFile`, `getClaimFiles`, `removeFile`, `saveIRDocs`
+ - `getBranchAndPolicyByClientID()` returns `branch_data[]` + `policy_data` keyed by `branch_id`, filtered by `pt.allocg IN ('Non-EB','Marine')`
+
+### Models
+- **`app/Models/NonEbClaimAssetModel.php`** — Asset details per claim
+- **`app/Models/NonEbTicketMasterModel.php`** — Main ticket/claim master
+ - `getTicketDataByTicketID()` JOINs `client_branch cb` to expose `cb.branch_name` for read-only display in edit view
+
+### Views
+- **`app/Views/non_eb_claim_search.php`** — Search page with filter sidebar (Policy Type, Insurer, Claim No, Nhance Ref, Client, Status, Date Range). Contains the Policy Type selection modal for "Add New Claim" (survives AJAX list reload). Uses `localStorage` to persist filters.
+- **`app/Views/non_eb_claim_list.php`** — DataTable list with CSV/Excel export, action buttons. Modal HTML removed from here (lives in search view).
+- **`app/Views/non_eb_claim_form.php`** — New claim form with 9 accordion sections. 3-level cascade: Client → Branch → Policy. Status-based section visibility via AJAX. Section 8 (Documents & Attachments) commented out (code preserved).
+- **`app/Views/non_eb_claim_edit.php`** — Edit/view claim. Sections 1 & 2 are fully read-only. 5 tabs: General, Claim Files, Reply, Notes, History. Section 8 commented out.
+- **`app/Views/non_eb_claim_mail_template.php`** — Mail template CRUD with Jodit editor, placeholder insertion, auto-mail toggle.
+- **`app/Views/non_eb_claim_reports.php`** — Reports with filters (Policy Type, ACM, Date Range), AJAX DataTable with export.
+
+### Config Changes
+- **`app/Config/Routes.php`** — Added `/non-eb-claim` route group (18 routes) under `authMVC` filter.
+- **`app/Config/Acl.php`** — Added ACL entry `#^/non-eb-claim#` for HEAD, ADMIN, MANAGER, ACCOUNT_MANAGER roles + CLAIMS team.
+- **`app/Config/Constants.php`** — Added `UPLOAD_EXT_ASSET_FILES` constant.
+
+---
+
+## Routes Reference
+
+```
+/non-eb-claim/list GET|POST claimList
+/non-eb-claim/remove GET removeClaim
+/non-eb-claim/new/(:any) GET claimForm/$1
+/non-eb-claim/create POST createClaim
+/non-eb-claim/update POST updateClaim
+/non-eb-claim/view/(:any) GET view_claim/$1
+/non-eb-claim/mail_template GET mailTemplate
+/non-eb-claim/crud_mail_template/(:any) POST crudTemplate/$1
+/non-eb-claim/note/(:any) POST crudNote/$1
+/non-eb-claim/reply POST saveReply
+/non-eb-claim/reports GET|POST claimReports
+/non-eb-claim/getBranchAndPolicy POST getBranchAndPolicyByClientID
+/non-eb-claim/getVisibleSections POST getVisibleSectionsAjax
+/non-eb-claim/getMoreInfo POST getMoreInfo
+/non-eb-claim/uploadFile POST uploadFile
+/non-eb-claim/getClaimFiles POST getClaimFiles
+/non-eb-claim/removeFile GET removeFile
+/non-eb-claim/saveIRDocs POST saveIRDocs
+```
+
+---
+
+## Section Order (Form & Edit Views)
+
+| # | Section | Notes |
+|---|---|---|
+| 1 | Policy & Account Details | Read-only in edit view (Client, Branch, Policy, Insurer, Policy No, Policy Start/End Date, ACM) |
+| 2 | Insured Contact Details | Read-only in edit view |
+| 3 | Loss / Incident Details | Editable |
+| 4 | Intimation & Claim Reference | Editable |
+| 5 | Status & Tracking | Editable |
+| 6 | Asset Details | Dynamic rows + file upload option |
+| 7 | Surveyor Details | Conditional (status-based) |
+| 8 | Documents & Attachments | Commented out in both form and edit (code preserved) |
+| 9 | Settlement Details | Conditional (status-based) |
+
+**Edit view additionally has:** Claim Files tab (IR Documents checklist + file upload + file list)
+
+---
+
+## DB Schema — Key Columns
+
+### `non_eb_ticket_master`
+| Column | Type | Notes |
+|---|---|---|
+| `branch_id` | INT | Added session 2 |
+| `asset_file` | VARCHAR | Path to uploaded asset file |
+| `policy_start_date` | VARCHAR(20) | Stored as YYYY-MM-DD, displayed as DD-MM-YYYY |
+| `policy_end_date` | VARCHAR(20) | Same format |
+| `required_docs` | TEXT | JSON array for IR Documents checklist |
+| `policy_section` | — | Kept in DB but removed from all UI/model code |
+| `policy_period` | — | Kept in DB but removed from all UI/model code |
+
+### `claim_files`
+| Column | Type | Notes |
+|---|---|---|
+| `ticket_type` | TINYINT(1) DEFAULT 1 | 1=EB, 2=Non-EB. Added to share table without breaking EB records |
+
+### DB Trigger
+`non_eb_ticket_master_after_update` — tracks 22 fields into `ticket_history` on every UPDATE. TEXT fields (loss_description, lor, surveyor_remarks, closure_remark) truncated to 500 chars. Skipped: set-once/bulk fields (client_id, branch_id, insured_contact_*, google_drive_links, documents_*, asset_file, required_docs).
+
+Full DDL in `db.md`.
+
+---
+
+## Key Patterns & Decisions
+
+### UI Style — Bootstrap Card/Collapse Accordion
+All sections use Bootstrap card/collapse pattern (matching `ticket_form_gmc.php`):
+```html
+
+
+
Title
+
+
...
+
+
+
+```
+Custom `toggleAccordion()` and `.accordion`/`.accordion-content` CSS fully removed.
+
+### CSS Classes
+```css
+.readonly-color { background-color: #e0e0e0; color: #666; }
+.readonly-select { pointer-events: none; background-color: #f0f0f0; color: #666; }
+.label-font-size { font-size: 0.875rem; }
+.is-invalid { border-color: #dc3545 !important; }
+.invalid-feedback { display: none; color: #dc3545; font-size: 0.8rem; }
+```
+
+### 3-Level Dropdown Cascade (Create Form Only)
+- Client change → AJAX `non-eb-claim/getBranchAndPolicy` → returns `branch_data[]` + `policy_data{branch_id: [...]}`
+- Branch change → populates policies from stored `noneb_policy_list[branch_id]`, auto-fills insured contact
+- Policy change → auto-fills insurer, policy_no, policy start/end dates (YYYY-MM-DD → dd-mm-yyyy)
+- **Select2 fix**: All append/reset functions do `select2('destroy')` before DOM changes, then `select2()` re-init after. Guard: `.hasClass('select2-hidden-accessible')`
+
+### Edit View — Read-Only Section 1 & 2 (Session 4)
+Sections 1 and 2 are non-editable on the edit page. Strategy:
+- All dropdowns replaced with readonly text inputs showing stored values
+- Hidden inputs for `branch_id` and `acm_id` carry values on form submit
+- No AJAX cascade JS — all client/branch/policy change handlers removed from edit page
+- Policy display: `policy_type_name + policy_no` combined in one text input
+- PHP date formatting at top of edit view: detects YYYY-MM-DD, converts to DD-MM-YYYY for display
+- `validateNonEbForm()` in edit page: removed client/branch/ACM/contact validation (those sections are read-only)
+
+### Form Submission — FormData
+Both create and edit forms use `new FormData()` + `$.ajax({ processData: false, contentType: false })` to support file uploads. `X-Requested-With: XMLHttpRequest` header added for CI4 `isAJAX()` detection.
+
+### Status-Based Section Visibility
+`$statusSectionVisibility` mapping in controller. AJAX endpoint `getVisibleSections` returns which sections show/hide. Surveyor and Settlement sections are conditionally visible.
+
+### Modal Fix (List Page)
+Modal HTML in `non_eb_claim_search.php` (parent), not inside the AJAX-replaced `#claim_list_div`. Reason: `$('#claim_list_div').empty().html(response.html)` destroys inner modals on filter. Uses `new bootstrap.Modal()` for AJAX-safe init.
+
+### URL Construction
+- `base_url` JS var = `'http://localhost/nhance/'` (trailing slash from App.php)
+- All AJAX URLs: `base_url + 'non-eb-claim/...'` (no leading slash — avoids double-slash)
+
+---
+
+## Claim Files Tab (Edit Page — Session 4)
+
+Reuses `claim_files` table with `ticket_type=2` flag. Four controller methods handle Non-EB file operations:
+
+| Method | Route | Purpose |
+|---|---|---|
+| `uploadFile()` | POST `/uploadFile` | URL or physical file upload, inserts with `ticket_type=2` |
+| `getClaimFiles()` | POST `/getClaimFiles` | Fetch files for ticket filtered by `ticket_type=2` |
+| `removeFile()` | GET `/removeFile` | Soft delete (`is_active=0`) by file id |
+| `saveIRDocs()` | POST `/saveIRDocs` | Save IR Documents JSON to `non_eb_ticket_master.required_docs` |
+
+**Tab content:** IR Documents checklist (JSON stored in `required_docs`, with freeze toggle + save button) + file upload form (URL/file toggle) + file list table + edit URL modal.
+
+**JS functions (all prefixed `noneb` to avoid collision with EB claims JS):**
+`nonebLoadClaimFiles`, `nonebCreateFileList`, `nonebAddHTMLInput`, `nonebAddFileUploadHtml`, `nonebRemoveHTMLInput`, `nonebToggleUploadType`, `nonebLoadConfiguration`, `nonebRenderDocumentList`, `nonebCreateDocumentRow`, `nonebAddDocument`, `nonebRemoveDocument`, `nonebUpdateDocName`, `nonebUpdateDocReceived`, `nonebToggleActionFreeze`, `nonebSaveIRDocs`
+
+File downloads reuse the global `downloadClaimFile/(:any)` route (no `ticket_type` filtering needed for download).
+
+**`saveIRDocs` is a separate endpoint** (`non-eb-claim/saveIRDocs`) from the EB equivalent (`ticket/saveIRDocsJson`) — EB updates `ticket_master`, Non-EB updates `non_eb_ticket_master`.
+
+---
+
+## Frontend Validation (`validateNonEbForm()`)
+
+Full JS validation runs before AJAX submit on both create and edit forms.
+
+| Field | Rules |
+|---|---|
+| `client_select` | Required (create only) |
+| `branch_id` | Required (create only) |
+| `acm_id` | Required (create only) |
+| `claim_status_id` | Required |
+| `insured_contact_name` | Required, min 3 chars, `^[a-zA-Z0-9\s_-]+$` (create only) |
+| `insured_contact_number` | Required, numeric, 10–15 digits (create only) |
+| `insured_contact_email` | Optional, email format (create only) |
+| `nature_of_loss` | Required, min 3 chars |
+| `loss_location` | Required |
+| `loss_date` | Required |
+| `loss_estimate` | Optional, numeric |
+| `claim_number` | Optional, `^[a-zA-Z0-9\/_-]+$` |
+| `nhance_claim_ref_no` | Optional, max 100 chars |
+| `surveyor_contact_number` | Optional if section visible, numeric 10–15 digits |
+| `surveyor_email` | Optional if section visible, email format |
+| `loss_assessed_value` | Optional if section visible, numeric |
+| `settled_amount` | Optional if section visible, numeric |
+| `settlement_utr` | Optional if section visible, max 100 chars |
+| Asset section | At least one asset row with data OR valid file upload |
+
+On failure: toastr error, expands collapsed section containing first invalid field, scrolls + focuses it.
+
+---
+
+## Post-Update Redirect (Edit Page)
+After successful update AJAX, 800ms delay then redirect to list:
+```js
+setTimeout(function() { window.location.href = base_url + 'non-eb-claim/list'; }, 800);
+```
+Toast remains visible during the delay.
+
+---
+
+## Bugs Fixed
+
+| Bug | Fix |
+|---|---|
+| Modal not showing on Add click | Moved modal outside AJAX-replaced div, used `new bootstrap.Modal()` |
+| Policies not loading on client select | Fixed double-slash in AJAX URLs (`base_url` already has trailing `/`) |
+| Select2 dropdowns not updating | Added `select2('destroy')` before DOM manipulation, `select2()` re-init after |
+| Policy dates showing as `2-Dec-2026 12:00 am` | Replaced `formatDate()` with inline string split; checks `sp[0].length === 4` to detect YYYY |
+| Branch name missing in edit view `$td` | Added `JOIN client_branch cb` + `cb.branch_name` select to `getTicketDataByTicketID()` |
+| Edit Section 1 dropdowns not pre-selecting | Replaced dropdowns with readonly text inputs — simpler and reliable |
+
+---
+
+## Removed Fields (policy_section & policy_period)
+
+Removed from all UI and code but kept in DB (for historical data).
+
+| Location | Change |
+|---|---|
+| `NonEbTicketMasterModel` allowedFields | Removed both |
+| `claimSearch()` select in controller | Removed `tm.policy_section`, `tm.policy_period` |
+| `getValidationRules()` in controller | Removed both validation rules |
+| `non_eb_claim_edit.php` Section 1 | Removed from view |
+| `non_eb_claim_list.php` | Removed hidden `
` and `
` columns |
+| `db.md` | Commented-out DROP COLUMN statements added for future cleanup |
+
+---
+
+## Pending / TODO
+
+- Run DB ALTER TABLE statements (policy_start_date, policy_end_date, required_docs on `non_eb_ticket_master`; ticket_type on `claim_files`)
+- Run `non_eb_ticket_master_after_update` trigger in DB
+- End-to-end testing: create claim → edit claim → Claim Files tab → IR docs → History tab trigger verification
+- Verify status-based section visibility for all policy types
+- Test filter persistence (localStorage) on search page
+- Review `db.md` for any missing DDL
diff --git a/nonebapi.md b/nonebapi.md
new file mode 100644
index 00000000..62949904
--- /dev/null
+++ b/nonebapi.md
@@ -0,0 +1,465 @@
+# Non-EB Claims — External API Dev Doc & TODO
+
+**Purpose:** Expose four read/write endpoints to external applications (client portals, mobile apps, integrations) for Non-EB Claims.
+
+---
+
+## Endpoints to Build
+
+| # | Method | URL | Purpose |
+|---|---|---|---|
+| 1 | POST | `/api/v1/non-eb-claim/create` | Create a new Non-EB claim |
+| 2 | POST | `/api/v1/non-eb-claim/list` | List claims with optional filter params |
+| 3 | GET | `/api/v1/non-eb-claim/history/{claim_id}` | Get field change history of a claim |
+| 4 | POST | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a file against a required document checklist item |
+
+---
+
+## Architecture Decision — New Controller
+
+Create a **separate API controller**: `app/Controllers/Api/NonEbClaimApiController.php`
+
+**Why separate, not reusing `NonEbClaimController`:**
+- Existing controller uses session-based auth (`get_session_userid()`, `set_session_context()`) — not safe for API
+- API needs token/key auth middleware (no session)
+- API responses must be pure JSON always — no HTML rendering
+- `createClaim()` currently calls `get_session_userid()` via model callbacks (`checkAndADDCreatedByValue`) — needs to be overridden for API context
+- Existing `claimSearch()` reads `$this->request->getPost()` directly — API version should accept clean JSON body
+
+**Reuse:**
+- `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `ClaimFilesModel` — reuse as-is
+- `claimHistory()` logic — copy and adapt (remove priority label mapping that uses internal arrays)
+- `getValidationRules()` — reuse for create, with relaxed `insured_contact_*` rules since those come from external context
+- `formatDatesForClaim()` — reuse as-is
+- `checkDuplicateNonEbClaim()` — reuse as-is
+- `handleAssetFileUpload()` — reuse as-is
+- `saveAssets()` — reuse as-is
+
+---
+
+## TODO Checklist
+
+### Phase 1 — Route Registration
+
+- [ ] Add `/api/v1` route group in `app/Config/Routes.php` (use existing auth filter — no `authMVC`):
+ ```php
+ $routes->group('api/v1', ['filter' => 'your-existing-api-filter'], function($routes) {
+ $routes->group('non-eb-claim', function($routes) {
+ $routes->post('create', 'Api\NonEbClaimApiController::createClaim');
+ $routes->post('list', 'Api\NonEbClaimApiController::listClaims');
+ $routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
+ $routes->post('(:num)/upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc/$1');
+ });
+ });
+ ```
+- [ ] Confirm CI4 namespace resolution for `App\Controllers\Api\NonEbClaimApiController`
+
+---
+
+### Phase 2 — Controller Skeleton
+
+- [ ] Create `app/Controllers/Api/NonEbClaimApiController.php`
+ - Namespace: `App\Controllers\Api`
+ - Extends `BaseController`, uses `ResponseTrait`
+ - Import: `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `TicketClaimStatusModel`, `ClaimFilesModel`
+ - No session calls — `created_by` should be the resolved `api_client_id` (or a fixed system user ID for API)
+ - All responses: `Content-Type: application/json`
+
+---
+
+### Phase 3 — API 1: Create Claim
+
+**POST** `/api/v1/non-eb-claim/create`
+
+**Context:** The user is already logged in and has selected a client policy from the interface. They are triggering a claim with only the loss details — what happened, where, and when. This is a **one-shot create with no edit step**. The controller is responsible for fetching all other mandatory data from the DB and assembling the full record before insert. The caller sends the minimum possible.
+
+---
+
+**What the caller sends (user-facing fields only):**
+
+| Field | Required | Type | Notes |
+|---|---|---|---|
+| `client_policy_id` | Yes | int | Selected in UI before opening the claim form; all other FK fields derived from this |
+| `nature_of_loss` | Yes | string | min 3 chars — what happened |
+| `loss_location` | Yes | string | where it happened |
+| `loss_date` | Yes | string | `DD-MM-YYYY` — when it happened |
+| `loss_description` | No | string | Additional details; required if `asset_file` is sent |
+| `loss_estimate` | No | numeric | Approximate loss value |
+| `claim_number` | No | string | `^[a-zA-Z0-9\/_-]+$` — if already known |
+| `asset_file` | No | file | xlsx/xls/csv/pdf |
+| `asset_id_code[]` | No | array | asset rows |
+| `serial_no[]` | No | array | |
+| `vehicle_no[]` | No | array | |
+| `asset_description[]` | No | array | |
+
+> Surveyor fields, settlement fields, `nhance_claim_ref_no`, `priority` are **not** part of this create flow — they are filled by staff later via the internal web interface.
+
+---
+
+**What the controller fetches and inserts (caller does NOT send these):**
+
+| Field | Source |
+|---|---|
+| `client_id` | `client_policy.client_id` |
+| `branch_id` | `client_policy.client_branch_id` |
+| `policy_type_id` | `client_policy.policy_type_id` |
+| `insurer_id` | `client_policy.insurer_id` |
+| `policy_no` | `client_policy.policy_no` |
+| `acm_id` | `client_rm` where `client_id = cp.client_id AND level = 3 AND is_active = 1` — first result |
+| `claim_status_id` | First row of `ticket_claim_status` where `ticket_type = policy_type_id ORDER BY id ASC` |
+| `insured_contact_name` | Logged-in user's name from auth (resolved via auth middleware) |
+| `insured_contact_number` | Logged-in user's mobile from auth |
+| `insured_contact_email` | Logged-in user's email from auth |
+| `priority` | Hard-coded default: `1` (Low) |
+| `created_by` | API system user ID (explicit — not from session) |
+
+---
+
+**Success Response `200`:**
+```json
+{
+ "status": true,
+ "claim_id": 123,
+ "message": "Non-EB Claim created successfully"
+}
+```
+
+**Error Response `400` — validation failure:**
+```json
+{
+ "status": false,
+ "message": "Input validation failed",
+ "errors": {
+ "nature_of_loss": "Nature of Loss is required",
+ "loss_date": "Loss Date is required"
+ }
+}
+```
+
+**Error Response `404` — policy not found:**
+```json
+{
+ "status": false,
+ "message": "Client policy not found or inactive"
+}
+```
+
+**Error Response `422` — policy type not allowed:**
+```json
+{
+ "status": false,
+ "message": "Only Non-EB or Marine policy types are allowed"
+}
+```
+
+**Conflict Response `409`:**
+```json
+{
+ "status": false,
+ "message": "Duplicate claim found for Client + Loss Date + Policy No combination"
+}
+```
+
+---
+
+**TODOs:**
+- [ ] Validate only the user-facing fields — write a trimmed validation rule set for API (not reusing `getValidationRules()` directly since that includes staff-side fields); required: `client_policy_id`, `nature_of_loss`, `loss_location`, `loss_date`
+- [ ] Fetch `client_policy` row — return `404` if not found or `is_active != 1`
+- [ ] Validate `policy_type.allocg IN ('Non-EB', 'Marine')` — return `422` if EB type sent
+- [ ] Fetch `acm_id` from `client_rm` (level=3, is_active=1) for the derived `client_id` — set null and log warning if none found
+- [ ] Auto-set `claim_status_id` — first `ticket_claim_status` row for derived `policy_type_id ORDER BY id ASC` (same pattern as `initiateClaim` in `EmployeeRestController`)
+- [ ] Resolve insured contact fields from the authenticated user (name, mobile, email) via auth middleware — these are mandatory DB fields but the user does not type them
+- [ ] Handle `created_by` explicitly in insert data — model's `beforeInsert` callback calls `get_session_userid()` which returns null for API; CI4 uses the explicitly passed value
+- [ ] Duplicate check via `checkDuplicateNonEbClaim()` — runs after derivation so `client_id` and `policy_no` are already populated
+- [ ] Date format: `DD-MM-YYYY` → `formatDatesForClaim()` converts to `Y-m-d` for DB
+- [ ] Call `saveAssets()` after insert if asset fields present
+- [ ] Call `putHistoryAfterInsert()` after insert
+- [ ] Skip `sendAutoMailTrigger()` for this initial create — claim is at first status, mail trigger is for status transitions; document this
+- [ ] Handle `multipart/form-data` when `asset_file` is included
+- [ ] Return `claim_id` in response so the caller can reference the created claim
+
+---
+
+### Phase 4 — API 2: List Claims
+
+**POST** `/api/v1/non-eb-claim/list`
+
+**Request Body (JSON):**
+
+| Field | Required | Type | Notes |
+|---|---|---|---|
+| `page` | No | int | Default 1 |
+| `per_page` | No | int | Default 20, max 100 |
+| `policy_type_id` | No | int | Filter by policy type |
+| `claim_status_id` | No | int | Filter by status |
+| `client_id` | No | int | Filter by client |
+| `insurer_id` | No | int | Filter by insurer |
+| `claim_number` | No | string | LIKE search |
+| `nhance_claim_ref_no` | No | string | LIKE search |
+| `date_type` | No | string | `created_date` or `updated_date` |
+| `start_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
+| `end_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
+| `show_closed` | No | bool | Default `false` — if false, excludes Settled/Closed/Rejected/Withdrawn |
+
+**Success Response `200`:**
+```json
+{
+ "status": true,
+ "total": 87,
+ "page": 1,
+ "per_page": 20,
+ "data": [
+ {
+ "id": 123,
+ "claim_number": "CLM/2026/001",
+ "nhance_claim_ref_no": "NEB/2026/00123",
+ "client_name": "ABC Corp",
+ "insurer_name": "New India Assurance",
+ "policy_type_name": "All Risk",
+ "policy_no": "POL/1234/2026",
+ "status": "Claim Intimation - Insured",
+ "status_display": "Claim Intimation - Insured",
+ "loss_date": "15-03-2026",
+ "loss_location": "Chennai",
+ "nature_of_loss": "Fire damage",
+ "loss_estimate": "500000",
+ "insured_contact_name": "John Doe",
+ "insured_contact_number": "9876543210",
+ "acm_name": "Ravi Kumar",
+ "created_date": "01-03-2026",
+ "updated_date": "20-03-2026"
+ }
+ ]
+}
+```
+
+**TODOs:**
+- [ ] Reuse the `claimSearch()` query from existing controller — extract it into a shared private method or duplicate in API controller
+- [ ] Add **pagination**: `LIMIT` + `OFFSET` based on `page` and `per_page`; also run a `COUNT(*)` variant of the same query for `total`
+- [ ] Accept JSON body (`$this->request->getJSON(true)`) not `getPost()` — existing controller uses `getPost()`
+- [ ] Cap `per_page` at 100 to prevent abuse
+- [ ] Strip HTML from `nature_of_loss`, `loss_description` before returning (use existing `convertHtmlToText()` or `esc()`)
+- [ ] Decide which fields to expose — **do not return**: `google_drive_links`, `documents_*`, `required_docs`, `is_active`, `created_by`, `updated_by`
+- [ ] `show_closed: false` (default) mirrors the existing default list behaviour (`whereNotIn` on terminal statuses)
+- [ ] Add `sort_by` param later (optional/phase 2): `loss_date`, `created_at`, `updated_at` with `sort_dir: asc|desc`
+
+---
+
+### Phase 5 — API 3: Claim History
+
+**GET** `/api/v1/non-eb-claim/history/{claim_id}`
+
+**URL Param:** `claim_id` — integer, required
+
+**What is shown vs hidden:**
+
+This endpoint does **not** expose the full raw `ticket_history` table. It follows the same pattern as the EB `claimView` API — only **status progression is shown to the user**, and only statuses that have a `display_name` in `ticket_claim_status` are included. Internal status names, field-level changes (surveyor, ACM, priority etc.), and who made the change are all hidden.
+
+The response is a **chronological status timeline** — oldest to newest — showing when the claim moved through each user-visible stage.
+
+**Filtering logic (mirrors EB `claimView`):**
+1. Fetch all `ticket_history` rows for `ticket_id`
+2. Keep only rows where `field_name = 'claim_status_id'`
+3. For each row, look up `new_value` against `ticket_claim_status` — only include it if that status has a non-null `display_name`
+4. Map to `display_name` for output — never expose raw internal `claim_status` string
+5. Reverse to chronological order (oldest first for timeline display)
+6. Do not expose `modified_by` — who changed it is internal
+
+**URL Param:** `claim_id` — integer, required
+
+**Success Response `200`:**
+```json
+{
+ "status": true,
+ "claim_id": 123,
+ "history": [
+ {
+ "status": "Claim Intimation - Insured",
+ "changed_at": "01-03-2026 10:15 AM"
+ },
+ {
+ "status": "Under Process",
+ "changed_at": "05-03-2026 02:30 PM"
+ },
+ {
+ "status": "Claim Settled",
+ "changed_at": "20-03-2026 04:45 PM"
+ }
+ ]
+}
+```
+
+> If a status does not have a `display_name` in `ticket_claim_status` it is silently skipped — it is an internal-only status not meant for user visibility.
+
+**Error Response `404`:**
+```json
+{
+ "status": false,
+ "message": "Claim not found"
+}
+```
+
+**TODOs:**
+- [ ] Do NOT reuse `claimHistory()` from `NonEbClaimController` as-is — that returns all field changes for internal staff view. Write a separate method that fetches only `claim_status_id` history rows
+- [ ] Fetch all `ticket_history` where `ticket_id = claim_id AND field_name = 'claim_status_id' AND is_active = 1 ORDER BY created_at ASC`
+- [ ] For each row join or look up `ticket_claim_status` on `new_value = id` — filter out rows where `display_name IS NULL`
+- [ ] Verify claim exists in `non_eb_ticket_master` (`is_active = 1`) before querying history — return `404` if not
+- [ ] Format `created_at` as `DD-MM-YYYY HH:MM AM/PM` (matches EB pattern: `date('d-m-Y h:i A', strtotime(...))`)
+- [ ] Output only `status` (display_name) and `changed_at` per row — no `field_name`, no `old_value`, no `modified_by`
+- [ ] Scope check: verify the claim's `client_id` matches the authenticated user's client before returning — prevents users from fetching other clients' claim history
+
+---
+
+### Phase 6 — API 4: Upload Required Document
+
+**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
+
+**URL Param:** `claim_id` — integer, required
+
+**Request Body (`multipart/form-data`):**
+
+| Field | Required | Type | Notes |
+|---|---|---|---|
+| `document_name` | Yes | string | Must exactly match one of the `document_name` values in `required_docs.docs[]` |
+| `file` | Yes | file | Allowed types: same as `UPLOAD_EXT_CLAIM_DOCS` |
+
+**`required_docs` JSON structure (stored in `non_eb_ticket_master.required_docs`):**
+```json
+{
+ "is_action_freeze": false,
+ "docs": [
+ { "document_name": "Invoice Copy", "document_received": false },
+ { "document_name": "Survey Report", "document_received": true }
+ ]
+}
+```
+
+**Behavior:**
+1. Validate claim exists (`non_eb_ticket_master.id = claim_id`, `is_active = 1`) — `404` if not
+2. Fetch `required_docs` JSON from the ticket
+3. If `required_docs` is empty or null — return `422` (no checklist configured for this claim)
+4. If `is_action_freeze = true` — return `423` (checklist is locked, uploads not allowed)
+5. Find the doc entry where `document_name` matches exactly — `404` if not found in list
+6. Upload file → insert row into `claim_files` (`ticket_type = 2`, `file_type = 2`, `ticket_id = claim_id`, `doc_name = document_name`)
+7. Update `required_docs`: set `document_received = true` for the matched doc entry, write back to `non_eb_ticket_master.required_docs`
+8. Return success with the full updated `required_docs` object
+
+**Success Response `200`:**
+```json
+{
+ "status": true,
+ "message": "Document uploaded successfully",
+ "claim_id": 123,
+ "document_name": "Invoice Copy",
+ "required_docs": {
+ "is_action_freeze": false,
+ "docs": [
+ { "document_name": "Invoice Copy", "document_received": true },
+ { "document_name": "Survey Report", "document_received": true }
+ ]
+ }
+}
+```
+
+**Error Response `404` — claim not found:**
+```json
+{ "status": false, "message": "Claim not found" }
+```
+
+**Error Response `422` — no checklist configured:**
+```json
+{ "status": false, "message": "No required documents checklist configured for this claim" }
+```
+
+**Error Response `423` — checklist locked:**
+```json
+{ "status": false, "message": "Document checklist is locked for this claim" }
+```
+
+**Error Response `404` — document_name not in list:**
+```json
+{ "status": false, "message": "Document 'Invoice Copy' not found in required documents list" }
+```
+
+**TODOs:**
+- [ ] Use `claim_files` model for file insert — same structure as existing `uploadFile()` in `NonEbClaimController` (`ticket_type = 2`, `file_type = 2`)
+- [ ] File upload path: `WRITEPATH . 'uploads/claim_files/'` — same as web controller
+- [ ] `created_by`: use API system user ID (same pattern as create endpoint)
+- [ ] `document_name` match is **case-sensitive exact match** — document this for API consumers
+- [ ] Allow re-upload if `document_received` is already `true` — overwrite the previous `claim_files` entry (soft-delete old, insert new) OR just insert new and keep both; decide and document
+- [ ] Do NOT expose the `claim_files.url` file path directly — return download URL via `base_url('downloadClaimFile/') . $file_id` so internal paths are not leaked
+- [ ] Validate file extension against `UPLOAD_EXT_CLAIM_DOCS` constant — reject unsupported types with `415`
+- [ ] The `required_docs` update must be atomic — update the JSON and `claim_files` insert in a DB transaction; roll back file insert if JSON update fails
+
+---
+
+### Phase 7 — Cross-cutting Concerns
+
+- [ ] **Rate limiting**: add a simple request counter per `api_key` per minute in a cache table or Redis. Block at 60 req/min.
+- [ ] **Request logging**: log every API request (api_client_id, endpoint, status_code, ip, timestamp) into an `api_request_log` table for audit
+ ```sql
+ CREATE TABLE api_request_log (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ api_client_id INT,
+ endpoint VARCHAR(100),
+ method VARCHAR(10),
+ status_code SMALLINT,
+ ip_address VARCHAR(45),
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ ```
+- [ ] **CORS headers**: if external app is browser-based, add `Access-Control-Allow-Origin` + preflight `OPTIONS` handling in a filter
+- [ ] **API versioning**: prefix as `/api/v1/non-eb-claim/...` from day one — easier to version-bump later without breaking consumers
+- [ ] **Error envelope**: all errors must follow a consistent shape:
+ ```json
+ { "status": false, "code": 400, "message": "...", "errors": {} }
+ ```
+- [ ] **Postman collection**: create and share a Postman collection with all 4 endpoints, sample request bodies, and expected responses
+- [ ] **DB changes** — add to `db.md`:
+ - `api_request_log` table
+
+---
+
+## Field Reference — What the External App Must Know
+
+### Required IDs (need lookup endpoints or a handshake step)
+These are FK IDs that the external app must send — they need to either be pre-agreed or fetched from lookup endpoints (future phase):
+
+| ID Field | Lookup Source |
+|---|---|
+| `client_id` | `clients` table |
+| `branch_id` | `client_branch` table, filtered by `client_id` |
+| `policy_type_id` | `policy_type` table, `allocg IN ('Non-EB','Marine')` |
+| `client_policy_id` | `client_policy` table, filtered by `branch_id` |
+| `insurer_id` | `insurers` table |
+| `acm_id` | `user_profiles` table, `role = 3` |
+| `claim_status_id` | `ticket_claim_status` table, filtered by `ticket_type = policy_type_id` |
+
+**TODO (future phase):** Build read-only lookup endpoints (`/api/v1/lookup/clients`, `/api/v1/lookup/policy-types`, etc.) so the external app can populate dropdowns without hardcoding IDs.
+
+---
+
+### Claim Status Flow (for external app awareness)
+The trigger-based DB trigger (`non_eb_ticket_master_after_update`) auto-logs status changes into `ticket_history`. History API reflects these changes. External app should not attempt to set arbitrary statuses — only statuses listed in `allowed_status` of the current status are valid transitions.
+
+**TODO:** Document valid status transitions in a separate handshake endpoint response or static doc section.
+
+---
+
+## Files to Create / Modify
+
+| File | Action |
+|---|---|
+| `app/Controllers/Api/NonEbClaimApiController.php` | **Create** |
+| `app/Config/Routes.php` | **Modify** — add `/api/v1` route group |
+| `db.md` | **Modify** — add `api_request_log` DDL |
+
+---
+
+## Out of Scope (this phase)
+
+- Update claim via API — not requested; skip for now
+- Delete/remove claim via API — internal operation only
+- Claim file download via API — separate phase
+- IR Documents (required_docs) CRUD via API — separate phase (upload-required-doc only marks received + stores file; full checklist management is out of scope)
+- Webhook callbacks to external app on status change — separate phase
diff --git a/nonebapidocs.md b/nonebapidocs.md
new file mode 100644
index 00000000..fd65abeb
--- /dev/null
+++ b/nonebapidocs.md
@@ -0,0 +1,447 @@
+# Non-EB Claims API — Developer Reference
+
+**Version:** v1
+**Base URL:** `{{base_url}}/api/v1/non-eb-claim`
+**Content-Type:** `application/json` (except file upload endpoints — see individual notes)
+**All responses are JSON.**
+
+---
+
+## Authentication
+
+Every request must include a valid JWT token in the `Authorization` header.
+
+```
+Authorization: Bearer
+```
+
+- The token is issued at login and identifies the current user.
+- The API reads the user's **name, mobile, and email** from the token automatically — you do not need to send contact details separately.
+- If the token is missing or expired, all endpoints return `401`.
+
+---
+
+## Response Envelope
+
+All responses follow this consistent shape:
+
+**Success:**
+```json
+{
+ "status": true,
+ "code": 200,
+ ...
+}
+```
+
+**Error:**
+```json
+{
+ "status": false,
+ "code": 400,
+ "message": "Human-readable error message",
+ "errors": { "field": "Specific field error" }
+}
+```
+
+> `errors` is only present on `400` validation failures.
+
+---
+
+## Date Format
+
+- **Input:** always send dates as `DD-MM-YYYY` (e.g. `"25-03-2026"`)
+- **Output:** dates in list/history responses are returned as `DD-MM-YYYY`
+- **Timestamps** in history are returned as `DD-MM-YYYY hh:mm AM/PM`
+
+---
+
+## Endpoints
+
+| # | Method | URL | Description |
+|---|---|---|---|
+| 1 | `POST` | `/api/v1/non-eb-claim/create` | Raise a new Non-EB claim |
+| 2 | `POST` | `/api/v1/non-eb-claim/list` | List / search claims |
+| 3 | `GET` | `/api/v1/non-eb-claim/history/{claim_id}` | Status timeline of a claim |
+| 4 | `POST` | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a required document |
+
+---
+
+## 1. Create Claim
+
+**POST** `/api/v1/non-eb-claim/create`
+
+### How it works
+
+The user has already selected a **client policy** on the previous screen. You send only that policy ID plus the loss details. The server derives everything else (client, branch, insurer, policy number, account manager, initial status) automatically from the policy.
+
+The logged-in user's name, mobile, and email are pulled from their JWT token and stored as the insured contact — you do not send them.
+
+### Request
+
+**Content-Type:** `application/json`
+If you are also uploading an asset file, switch to `multipart/form-data` and include all fields as form fields.
+
+| Field | Required | Type | Validation | Notes |
+|---|---|---|---|---|
+| `client_policy_id` | Yes | integer | Must exist and be active | Selected policy from the previous screen |
+| `nature_of_loss` | Yes | string | min 3 chars | What happened |
+| `loss_location` | Yes | string | non-empty | Where it happened |
+| `loss_date` | Yes | string | `DD-MM-YYYY` | When it happened |
+| `loss_description` | No | string | — | Required only if `asset_file` is included |
+| `loss_estimate` | No | numeric | — | Approximate value of loss |
+| `claim_number` | No | string | `[a-zA-Z0-9/_-]` only | If already assigned by insurer |
+| `asset_file` | No | file | xlsx/xls/csv/pdf | Asset register file |
+| `asset_id_code[]` | No | array | — | Asset ID codes (one per row) |
+| `serial_no[]` | No | array | — | Serial numbers (parallel to asset_id_code[]) |
+| `vehicle_no[]` | No | array | — | Vehicle numbers |
+| `asset_description[]` | No | array | — | Asset descriptions |
+
+> **Do not send:** `client_id`, `branch_id`, `insurer_id`, `policy_no`, `acm_id`, `claim_status_id`, `insured_contact_name`, `insured_contact_number`, `insured_contact_email` — these are all derived server-side.
+
+### Example Request (JSON)
+
+```json
+{
+ "client_policy_id": 42,
+ "nature_of_loss": "Fire damage to warehouse",
+ "loss_location": "Chennai",
+ "loss_date": "22-03-2026",
+ "loss_description": "Warehouse section B caught fire due to electrical fault",
+ "loss_estimate": 500000
+}
+```
+
+### Example Request (multipart — with asset file)
+
+```
+POST /api/v1/non-eb-claim/create
+Content-Type: multipart/form-data
+
+client_policy_id = 42
+nature_of_loss = Fire damage to warehouse
+loss_location = Chennai
+loss_date = 22-03-2026
+loss_description = Warehouse section B caught fire
+asset_file =
+asset_id_code[] = AST-001
+serial_no[] = SN-12345
+asset_description[] = Industrial generator
+```
+
+### Success Response `200`
+
+```json
+{
+ "status": true,
+ "code": 200,
+ "claim_id": 123,
+ "message": "Non-EB Claim created successfully"
+}
+```
+
+> Save `claim_id` — you'll need it for history and document upload.
+
+### Error Responses
+
+| Code | Scenario | Sample Message |
+|---|---|---|
+| `400` | Validation failed | `"Input validation failed"` + `errors` object |
+| `400` | Asset file sent without `loss_description` | `"Loss Description is required when uploading an asset file."` |
+| `401` | Missing / expired token | `"Unauthorized"` |
+| `404` | `client_policy_id` not found or inactive | `"Client policy not found or inactive"` |
+| `409` | Duplicate claim (same client + loss date + policy no) | `"Duplicate claim found for Client + Loss Date + Policy No combination"` |
+| `422` | Policy type is EB (not Non-EB / Marine) | `"Only Non-EB or Marine policy types are allowed"` |
+| `422` | No claim status configured for policy type | `"No claim status configured for this policy type"` |
+| `500` | DB insert failed | `"Failed to create claim"` |
+
+### 400 Validation Error Example
+
+```json
+{
+ "status": false,
+ "code": 400,
+ "message": "Input validation failed",
+ "errors": {
+ "nature_of_loss": "Nature of Loss is required",
+ "loss_date": "Loss Date is required"
+ }
+}
+```
+
+---
+
+## 2. List Claims
+
+**POST** `/api/v1/non-eb-claim/list`
+
+### How it works
+
+Send a JSON body with optional filters and pagination params. By default, **closed/settled/rejected/withdrawn claims are excluded**. Pass `"show_closed": true` to include them.
+
+Results are sorted newest first.
+
+### Request
+
+**Content-Type:** `application/json`
+
+| Field | Required | Type | Default | Notes |
+|---|---|---|---|---|
+| `page` | No | integer | `1` | Page number |
+| `per_page` | No | integer | `20` | Max `100` |
+| `client_id` | No | integer | — | Filter by client |
+| `insurer_id` | No | integer | — | Filter by insurer |
+| `policy_type_id` | No | integer | — | Filter by policy type |
+| `claim_status_id` | No | integer | — | Filter by exact status |
+| `claim_number` | No | string | — | Partial match (LIKE) |
+| `nhance_claim_ref_no` | No | string | — | Partial match (LIKE) |
+| `date_type` | No | string | — | `"created_date"` or `"updated_date"` |
+| `start_date` | No | string | — | `DD-MM-YYYY` — used with `date_type` |
+| `end_date` | No | string | — | `DD-MM-YYYY` — used with `date_type` |
+| `show_closed` | No | boolean | `false` | `true` to include settled/closed/rejected/withdrawn |
+
+### Example Request
+
+```json
+{
+ "page": 1,
+ "per_page": 20,
+ "client_id": 5,
+ "date_type": "created_date",
+ "start_date": "01-03-2026",
+ "end_date": "31-03-2026"
+}
+```
+
+### Success Response `200`
+
+```json
+{
+ "status": true,
+ "code": 200,
+ "total": 87,
+ "page": 1,
+ "per_page": 20,
+ "data": [
+ {
+ "id": 123,
+ "claim_number": "CLM/2026/001",
+ "nhance_claim_ref_no": "NEB/2026/00123",
+ "policy_no": "POL/1234/2026",
+ "policy_type_id": 10,
+ "claim_status_id": 3,
+ "status": "Claim Intimation - Insured",
+ "status_display": "Claim Intimation - Insured",
+ "policy_type_name": "All Risk",
+ "client_name": "ABC Corp",
+ "insurer_name": "New India Assurance",
+ "loss_date": "2026-03-22",
+ "loss_location": "Chennai",
+ "nature_of_loss": "Fire damage to warehouse",
+ "loss_estimate": "500000",
+ "insured_contact_name": "Ravi Kumar",
+ "insured_contact_number": "9876543210",
+ "acm_name": "Anand",
+ "created_date": "22-03-2026",
+ "updated_date": "25-03-2026"
+ }
+ ]
+}
+```
+
+> When no results match, `total` is `0` and `data` is `[]`. The response is still `200`.
+
+### Error Responses
+
+| Code | Scenario |
+|---|---|
+| `401` | Missing / expired token |
+
+---
+
+## 3. Claim History
+
+**GET** `/api/v1/non-eb-claim/history/{claim_id}`
+
+### How it works
+
+Returns the **status progression timeline** of a claim — oldest stage first. Only statuses that are configured as user-visible are included. Internal/intermediate statuses used by staff are automatically filtered out.
+
+Each entry shows the status name and when it was reached. Who changed it is not exposed.
+
+### URL Parameter
+
+| Param | Type | Required | Description |
+|---|---|---|---|
+| `claim_id` | integer | Yes | The `id` returned from the create endpoint |
+
+### Example Request
+
+```
+GET /api/v1/non-eb-claim/history/123
+Authorization: Bearer
+```
+
+### Success Response `200`
+
+```json
+{
+ "status": true,
+ "code": 200,
+ "claim_id": 123,
+ "history": [
+ {
+ "status": "Claim Intimation - Insured",
+ "changed_at": "22-03-2026 10:15 AM"
+ },
+ {
+ "status": "Under Process",
+ "changed_at": "24-03-2026 02:30 PM"
+ },
+ {
+ "status": "Claim Settled",
+ "changed_at": "28-03-2026 04:45 PM"
+ }
+ ]
+}
+```
+
+> If no history exists yet, `history` is an empty array `[]`.
+
+### Error Responses
+
+| Code | Scenario |
+|---|---|
+| `401` | Missing / expired token |
+| `404` | Claim not found or inactive |
+
+---
+
+## 4. Upload Required Document
+
+**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
+
+### How it works
+
+Each claim has a **required documents checklist** configured by the staff (e.g. "Invoice Copy", "Survey Report"). This endpoint lets the user upload a file against one of those checklist items.
+
+When a document is uploaded successfully, its `document_received` flag in the checklist is set to `true` and the updated checklist is returned so you can refresh the UI.
+
+The `document_name` you send must **exactly match** (case-sensitive) one of the `document_name` values in the checklist. Use the checklist data to drive your UI — display the exact names as options so the user cannot type the wrong value.
+
+### URL Parameter
+
+| Param | Type | Required | Description |
+|---|---|---|---|
+| `claim_id` | integer | Yes | The claim to upload against |
+
+### Request
+
+**Content-Type:** `multipart/form-data` (always — this is a file upload)
+
+| Field | Required | Type | Notes |
+|---|---|---|---|
+| `document_name` | Yes | string | Must exactly match a `document_name` in the claim's checklist |
+| `file` | Yes | file | Allowed types: pdf, jpg, jpeg, png, doc, docx, xls, xlsx |
+
+### How to get the checklist
+
+The required documents list for a claim is returned when you fetch claim details (or can be shown after claim creation). The structure is:
+
+```json
+{
+ "is_action_freeze": false,
+ "docs": [
+ { "document_name": "Invoice Copy", "document_received": false },
+ { "document_name": "Survey Report", "document_received": true },
+ { "document_name": "Police FIR Copy","document_received": false }
+ ]
+}
+```
+
+- `is_action_freeze: true` means the checklist is locked — the upload endpoint will reject new uploads with `423`.
+- `document_received: true` means the staff has already received this document. You may still re-upload if needed (previous upload is replaced).
+- Show only the document names as upload targets — do not allow freetext entry.
+
+### Example Request
+
+```
+POST /api/v1/non-eb-claim/123/upload-required-doc
+Content-Type: multipart/form-data
+Authorization: Bearer
+
+document_name = Invoice Copy
+file =
+```
+
+### Success Response `200`
+
+```json
+{
+ "status": true,
+ "code": 200,
+ "message": "Document uploaded successfully",
+ "claim_id": 123,
+ "document_name": "Invoice Copy",
+ "download_url": "https://yourdomain.com/downloadClaimFile/456",
+ "required_docs": {
+ "is_action_freeze": false,
+ "docs": [
+ { "document_name": "Invoice Copy", "document_received": true },
+ { "document_name": "Survey Report", "document_received": true },
+ { "document_name": "Police FIR Copy", "document_received": false }
+ ]
+ }
+}
+```
+
+> Use the returned `required_docs` to update the checklist UI immediately without a separate fetch.
+
+### Error Responses
+
+| Code | Scenario | Message |
+|---|---|---|
+| `400` | `document_name` not sent | `"document_name is required"` |
+| `400` | No file sent or invalid file | `"A valid file is required"` |
+| `401` | Missing / expired token | `"Unauthorized"` |
+| `404` | Claim not found | `"Claim not found"` |
+| `404` | `document_name` not in checklist | `"Document 'Invoice Copy' not found in required documents list"` |
+| `415` | Unsupported file type | `"Unsupported file type: bmp"` |
+| `422` | Claim has no checklist configured | `"No required documents checklist configured for this claim"` |
+| `423` | Checklist is locked | `"Document checklist is locked for this claim"` |
+| `500` | Upload or DB failure | `"File upload failed"` / `"Failed to save document. Please try again."` |
+
+---
+
+## Common Error Reference
+
+| HTTP Code | Meaning | When it happens |
+|---|---|---|
+| `200` | OK | Request succeeded |
+| `400` | Bad Request | Validation failed — check `errors` object |
+| `401` | Unauthorized | Token missing, invalid, or expired |
+| `404` | Not Found | Resource doesn't exist or is inactive |
+| `409` | Conflict | Duplicate claim detected |
+| `415` | Unsupported Media Type | File type not allowed |
+| `422` | Unprocessable | Valid request but business rule blocks it |
+| `423` | Locked | Checklist is frozen — no uploads allowed |
+| `500` | Server Error | Something failed on the backend |
+
+---
+
+## Tips for Integration
+
+**Checking `status` field:**
+Always check `response.status === true` before proceeding — do not rely solely on the HTTP status code.
+
+**Pagination:**
+Use `total`, `page`, and `per_page` from the list response to build pagination controls. Total number of pages = `Math.ceil(total / per_page)`.
+
+**Asset rows (create claim):**
+Send parallel arrays. Row 0 of `asset_id_code[]`, `serial_no[]`, `vehicle_no[]`, `asset_description[]` form one asset entry. Leave a value empty string if not applicable for that row.
+
+**Document name matching (upload):**
+Always populate the `document_name` field from the checklist data returned by the server — never let users type it freehand. The match is case-sensitive exact.
+
+**Re-upload:**
+Uploading a document that already has `document_received: true` is allowed. The new file is saved alongside the previous one and `document_received` stays `true`.
diff --git a/public/dev_logs/2026-03-24.md b/public/dev_logs/2026-03-24.md
new file mode 100644
index 00000000..ffc641ff
--- /dev/null
+++ b/public/dev_logs/2026-03-24.md
@@ -0,0 +1,79 @@
+# Dev Log — 2026-03-24
+
+## Feature: Non EB Terms & Non EB Rack Rate in Client Policy List
+
+### Objective
+Introduce "Non EB Terms" and "Non EB Rack Rate" menu items in the client policy table hamburger dropdown. These menus replace the existing "Terms" and "Rack Rate" menus when the policy type's `allocg` field is `Non-EB` or `Marine`.
+
+---
+
+### Files Changed
+
+#### 1. `app/Models/ClientPolicyModel.php`
+- **`getClientPolicyByClientId()`** — Added two new selects:
+ - `policy_type.allocg as allocg` (policy_type table was already joined)
+ - `leads.misc as lead_misc` (leads table was already joined via `client_policy.is_from_lead = leads.id`)
+- **`$allowedFields`** — Added `non_eb_rack_rate_files` to allow updating the new JSON column.
+
+#### 2. `app/Controllers/ClientController.php`
+- **`editClientOnboarding()`** — Added `allocg` and `lead_misc` to the policy data mapping array passed to the view.
+- **Added 3 new methods:**
+ - `getNonEbRackRateFiles()` — GET endpoint, reads `non_eb_rack_rate_files` JSON from `client_policy` table and returns parsed file list.
+ - `uploadNonEbRackRateFile()` — POST endpoint, uploads file to `writable/uploads/non_eb_rack_rate/`, appends file entry to `non_eb_rack_rate_files` JSON column.
+ - `removeNonEbRackRateFile()` — POST endpoint, removes file entry from JSON column and deletes physical file from disk.
+
+#### 3. `app/Config/Routes.php`
+- Added 3 new routes under `client/policy` group:
+ - `GET getNonEbRackRateFiles`
+ - `POST uploadNonEbRackRateFile`
+ - `POST removeNonEbRackRateFile`
+
+#### 4. `app/Config/Constants.php`
+- Added `UPLOAD_EXT_NON_EB_RACK_RATE` constant: `['pdf', 'xls', 'xlsx']`
+
+#### 5. `app/Views/client_policy.php`
+- **Dropdown menus (4 locations: ~lines 530, 808, 2596, 2642):**
+ - Added `allocg` check: `item.allocg === 'Non-EB' || item.allocg === 'Marine'`
+ - If true → shows "Non EB Terms" + "Non EB Rack Rate", hides "Terms" + "Rack Rate"
+ - If false → shows "Terms" + "Rack Rate" (original behavior)
+- **Non EB Terms click handler (`btnNonEbTerms`):**
+ - Parses `lead_misc` JSON from data attribute
+ - If `placement_sheet_id` key exists → opens Google Sheet (`https://docs.google.com/spreadsheets/d/{id}`) in new tab
+ - Otherwise → `alert('No terms found')`
+- **Non EB Rack Rate click handler (`btnNonEbRackRateModal`):**
+ - Opens `#nonEbRackRateModal` using `new bootstrap.Modal()` (BS5 API, stored on `window._nonEbModal`)
+ - Loads existing files via AJAX `getNonEbRackRateFiles`
+- **Upload handler (`#btnUploadNonEbFile`):**
+ - Validates file extension (pdf/xlsx/xls) client-side
+ - Uploads via AJAX `uploadNonEbRackRateFile` with FormData
+ - Appends uploaded file to list on success
+- **Delete handler (`btnRemoveNonEbFile`):**
+ - SweetAlert confirmation
+ - Calls `removeNonEbRackRateFile` via AJAX
+ - Removes file row from DOM on success
+
+#### 6. `app/Views/client_onboarding.php`
+- **Added modal HTML** (`#nonEbRackRateModal`) at line ~457, placed outside tab-pane containers (same level as `autoFetchBranchModal`) to avoid Bootstrap stacking issues.
+- **Scoped CSS** for compact modal: forced `max-width: 420px`, `min-height: auto` on modal-body, reduced padding on header/body/footer.
+- **Close buttons** use `onclick="if(window._nonEbModal) window._nonEbModal.hide();"` — inline JS referencing the global BS5 Modal instance to avoid jQuery binding/timing issues.
+
+#### 7. `app/Controllers/EmployeeRestController.php`
+- **Line ~2257 query** — Added `LEFT JOIN` with `policy_type` table and a `CASE WHEN` select:
+ ```sql
+ CASE WHEN policy_type.allocg IN ('Non-EB', 'Marine') THEN 'Non-EB' ELSE 'EB' END as allocg
+ ```
+
+---
+
+### DDL Required (Manual)
+```sql
+ALTER TABLE `client_policy` ADD COLUMN `non_eb_rack_rate_files` JSON NULL DEFAULT NULL AFTER `wellness_vendor_id`;
+```
+
+---
+
+### Key Technical Decisions
+- **Bootstrap version conflict**: Project loads BS5 via `vendor.min.js` and BS3.4.1 via footer CDN. `$.fn.modal()` uses BS3 (broken), so all modal operations use `new bootstrap.Modal()` (BS5 native API). Modal instance stored on `window._nonEbModal` for close button access.
+- **Modal placement**: Modal HTML must be outside tab-pane containers in `client_onboarding.php` to avoid visibility/stacking issues.
+- **File storage**: Rack rate files stored physically in `writable/uploads/non_eb_rack_rate/` and tracked as JSON array in `client_policy.non_eb_rack_rate_files` column. Each entry: `{name, original_name, type, uploaded_at}`.
+- **Menu visibility**: Driven by `policy_type.allocg` field — `Non-EB`/`Marine` shows Non EB menus, everything else shows standard Terms/Rack Rate menus.
diff --git a/public/dev_logs/2026-03-26.md b/public/dev_logs/2026-03-26.md
new file mode 100644
index 00000000..0c5c6728
--- /dev/null
+++ b/public/dev_logs/2026-03-26.md
@@ -0,0 +1,341 @@
+# Dev Log — 2026-03-26
+
+## Feature: Non-EB Claim Form — UI Overhaul, Validation & Asset File Upload
+
+### Objective
+Continued development of the Non-EB Claims creation form (`non_eb_claim_form.php`). This session covered three major areas: (1) UI restyling to match the GMC ticket form, (2) comprehensive frontend validation with JS-only form submission, and (3) asset section enhancement with file upload support.
+
+---
+
+## 1. UI Restyling — Match `ticket_form_gmc.php` Accordion & Card Style
+
+### What Changed
+Replaced the custom accordion implementation with Bootstrap card/collapse pattern used in the existing GMC ticket form.
+
+### Before
+- Custom CSS classes: `.accordion`, `.accordion-content`, `.arrow`, `.rotate`
+- Custom JS `toggleAccordion(el)` function using `classList.toggle('show')`
+- Section headers: `
Title ▶
`
+- Section body: `
`
+- Labels had no consistent font-size class
+
+### After
+- Bootstrap card/collapse pattern: `
` → `
`
+- Section headers: `
Title
`
+- Section body: `
`
+- Removed `toggleAccordion()` JS function entirely
+- All labels now use `class="label-font-size"` (0.875rem) matching GMC form
+
+### CSS Classes Added (matching `ticket_form_gmc.php`)
+```css
+.readonly-color { background-color: #e0e0e0; color: #666; }
+.readonly-select { pointer-events: none; background-color: #f0f0f0; color: #666; }
+.label-font-size { font-size: 0.875rem; }
+```
+
+### Readonly Fields Styled
+- Insurer, Policy No, Policy Start Date, Policy Expiry Date inputs — added `readonly-color` class for darker background on auto-filled fields
+
+### Section ID Mapping
+| Section | Accordion ID | Collapse ID |
+|---|---|---|
+| Policy & Account Details | accordion1 | collapseOne |
+| Insured Contact Details | accordion2 | collapseTwo |
+| Loss / Incident Details | accordion3 | collapseThree |
+| Intimation & Claim Reference | accordion4 | collapseFour |
+| Status & Tracking | accordion5 | collapseFive |
+| Asset Details | accordion6 | collapseSix |
+| Surveyor Details | section_surveyor | collapseSeven |
+| Documents & Attachments | accordion8 | collapseEight |
+| Settlement Details | section_settlement | collapseNine |
+
+- Surveyor and Settlement sections use `section_surveyor` / `section_settlement` as parent IDs (for JS show/hide toggling based on status)
+
+---
+
+## 2. Frontend Validation & JS-Only Form Submit
+
+### What Changed
+Removed reliance on native HTML5 form submission. Implemented full JavaScript validation matching backend rules in `NonEbClaimController::getValidationRules()`.
+
+### Form Changes
+- Added `onsubmit="return false;"` to `