diff --git a/app/Views/policy_transaction_inception_list.php b/app/Views/policy_transaction_inception_list.php
index 39b49b27..ebb98751 100644
--- a/app/Views/policy_transaction_inception_list.php
+++ b/app/Views/policy_transaction_inception_list.php
@@ -1185,7 +1185,7 @@ function addHTMLInput(data = null, container_id = 'dynamic-form-container')
newRow.innerHTML = `
@@ -1371,7 +1371,7 @@ function addHTMLInputForVehicleFileUpload(data = null, container_id = 'dynamic-f
newRow.innerHTML = `
-
+
diff --git a/dev_logs/2026-04-01.md b/dev_logs/2026-04-01.md
index 1d7e53a6..efc15a70 100644
--- a/dev_logs/2026-04-01.md
+++ b/dev_logs/2026-04-01.md
@@ -71,3 +71,77 @@ Old function copied as `uploadRequiredDoc_v1` (preserved for reference).
### API Docs
- `nonebapidocs.md`
- `dev_logs/non_eb_claim_api.md`
+
+---
+
+## Task Plan — Policy Transaction Inception Form JS Validation
+
+### Goal
+Implement client-side form validation for `app/Views/policy_transaction_inception_form.php` by following the existing validation approach used in `app/Views/ticket_form_gmc.php` (centralized submit + reusable validator), but moving inception validation into a dedicated external JavaScript file.
+
+### Current-State Notes
+- `policy_transaction_inception_form.php` currently contains inline submit and validation logic on `#inception_form_id` (Parsley validation + custom checks + toastr + AJAX submit flow).
+- `ticket_form_gmc.php` follows a cleaner pattern where submit handler delegates validation and shows first invalid field feedback.
+- No dedicated inception validation `.js` file currently exists.
+
+### Proposed File Changes
+1. **Create** `public/assets/js/policy_transaction_inception_validation.js`
+ - Add a single public validation entry function (example: `window.validateInceptionFormInputs(form)`).
+ - Keep all custom rules here (beyond HTML `required` and Parsley):
+ - `follow_insurer_id[]` must be selected for all rows.
+ - `pt_form_sumbit_handler` must be `1`.
+ - CD account selection rule for client/policy status condition.
+ - Any date/business-rule checks currently done during submit.
+ - Return a boolean result and handle user-facing messages consistently via toastr.
+
+2. **Refactor** `app/Views/policy_transaction_inception_form.php`
+ - Keep submit flow in one handler, but delegate custom validations to the new JS file.
+ - Minimize inline validation logic in view.
+ - Ensure first invalid field is focused/scrolled for better UX.
+ - Include the new script after shared dependencies (jQuery/Parsley/toastr), before submit logic usage.
+
+3. **(Optional Cleanup)** Move remaining inline helper validation code to dedicated JS if it is inception-form-specific and not used elsewhere.
+
+### Implementation Steps (Execution Order)
+- [x] Step 1: Create new file `public/assets/js/pages/policy_transaction_inception_validation.js`.
+- [x] Step 2: Extract custom validation blocks from `#inception_form_id` submit handler into reusable functions.
+- [x] Step 3: Expose one callable function for submit handler (`validateInceptionFormInputs`).
+- [x] Step 4: Update `policy_transaction_inception_form.php` to include new JS file.
+- [x] Step 5: Replace inline custom checks with function call and keep existing AJAX submit behavior unchanged.
+- [x] Step 6: Ensure invalid-field focus and warning message are preserved.
+- [ ] Step 7: Verify create/edit journeys and conditional sections (renewal, co-insurer, CD account, policy status).
+
+### Validation Rules Checklist (to implement in JS)
+- [ ] Parsley base validation must pass.
+- [ ] Every `follow_insurer_id[]` select must have value.
+- [ ] `pt_form_sumbit_handler != 0`.
+- [ ] If `client_type == 1` and `policy_status == completed` and `policy_type_id > 7`, at least one `cd_ac_no_for_child[]` must be selected.
+- [ ] Keep existing toastr wording (or align to one consistent warning style).
+
+### Testing Checklist
+- [ ] Submit with empty required fields -> blocked with field-level indication.
+- [ ] Submit with any empty co-insurer selector -> blocked with warning.
+- [ ] Submit with base premium/CD mismatch (`pt_form_sumbit_handler = 0`) -> blocked.
+- [ ] Submit valid data -> AJAX create request fires successfully.
+- [ ] Edit existing inception record -> validation still works and submit succeeds.
+- [ ] No regression in date conversion before submit (`policy_issue_date`, `policy_start_date`, `policy_end_date`, `renewal_date`, `rollover_date`, `month`).
+
+### Risks / Attention Points
+- Large inline script currently mixes validation and business logic; refactor should avoid changing API payload or field names.
+- Multiple dynamic rows (`follow_insurer_id[]`, `cd_ac_no_for_child[]`) need delegated-safe selectors.
+- Script include order is critical (new validation JS must load before submit handler executes).
+
+### Completion Update (Implemented)
+- Added `public/assets/js/pages/policy_transaction_inception_validation.js` with:
+ - `window.validateInceptionFormInputs(form)` as centralized entrypoint
+ - Parsley validation gate
+ - Co-insurer (`follow_insurer_id[]`) mandatory selection check
+ - `pt_form_sumbit_handler` (CD amount) guard check
+ - Child CD account selection rule for applicable completed flow
+ - First invalid field focus/scroll helper
+- Updated `app/Views/policy_transaction_inception_form.php`:
+ - Included external script: `assets/js/pages/policy_transaction_inception_validation.js`
+ - Refactored `#inception_form_id` submit handler to delegate custom validation to new file
+ - Preserved existing AJAX submit and payload/date conversion behavior
+- Technical validation done:
+ - JS syntax check passed (`node --check public/assets/js/pages/policy_transaction_inception_validation.js`)
diff --git a/dev_logs/2026-04-06_inception_form_live_validation_plan.md b/dev_logs/2026-04-06_inception_form_live_validation_plan.md
new file mode 100644
index 00000000..139b0636
--- /dev/null
+++ b/dev_logs/2026-04-06_inception_form_live_validation_plan.md
@@ -0,0 +1,92 @@
+# Policy Transaction Inception Form - Live Validation Plan
+
+## Objective
+- Add live field-level validation for `input`, `textarea`, and `select` in `app/Views/policy_transaction_inception_form.php`.
+- Trigger validation errors on `oninput` and `onchange` behavior without disrupting existing Parsley-based submit validation.
+- Restrict special characters to only `/`, `_`, `-`, `.`, and space (along with letters and numbers).
+
+## Implementation Steps
+- Create a dedicated JS module at `public/assets/js/pages/policy_transaction_inception_form_validation.js`.
+- Register one custom Parsley validator (`inceptioncharset`) for allowed character set checks.
+- Bind delegated events (`input` and `change`) on:
+ - `#inception_form_id`
+ - `#vehicle_form`
+ - `#CDMasterForm`
+- Apply Parsley attributes non-destructively:
+ - Add `data-parsley-inceptioncharset="true"` to character-validated fields.
+ - Set `data-parsley-trigger` only when missing, so existing field-level Parsley settings remain intact.
+- Support dynamic fields by reapplying constraints and refreshing Parsley instances before validating changed fields.
+- Include the new JS file in the view after existing inline scripts.
+
+## Character Rule
+- Allowed characters: `A-Z`, `a-z`, `0-9`, `/`, `_`, `-`, `.`, and space.
+- Validation message:
+ - `Only letters, numbers, spaces, and the characters / _ - . are allowed.`
+
+## Non-Disruption Controls
+- Do not replace existing submit handlers.
+- Do not remove existing inline `onchange`/`oninput` handlers.
+- Do not override existing Parsley triggers if already defined on a field.
+- Ignore hidden/disabled fields for live validation.
+
+## Validation Scope Notes
+- `select` elements are revalidated on `change` (for required/Parsley feedback).
+- Character set validation is applied to textual inputs and textareas only.
+- File, checkbox, radio, hidden, button, submit, and reset fields are excluded from charset validation.
+
+## Manual QA Checklist
+- Type an invalid special character (example: `@`) in a text field and confirm immediate Parsley error.
+- Type valid characters (`abc 123 / _ - .`) and confirm error clears.
+- Change required select fields and confirm Parsley error appears/disappears on change.
+- Add dynamic rows/fields (if applicable) and confirm live validation still works.
+- Submit each form (`inception`, `vehicle`, `CD master`) and confirm existing submit flow is unchanged.
+
+## Delivered Changes
+- Added: `public/assets/js/pages/policy_transaction_inception_form_validation.js`
+- Updated: `app/Views/policy_transaction_inception_form.php` (script include)
+- Updated: `app/Views/policy_transaction_inception_form.php` (Parsley-driven red/green live field highlight styles)
+- Updated: `app/Views/policy_transaction_inception_list.php` (dynamic `Document Name` rows now include Parsley charset attributes)
+- Updated: `app/Views/client_kyc.php` (`Document Name` fields and template rows now include Parsley charset attributes)
+- Updated: `app/Views/client_kyc.php` (loads validation script so KYC doc fields validate live in client onboarding screens)
+- Updated: `public/assets/js/pages/policy_transaction_inception_form_validation.js` (extended to cover `#file_upload_form`, `#kyc_form`, and document-name field detection by name/class)
+- Added: `public/assets/js/pages/policy_transaction_endorsement_form_validation.js` (same live Parsley validation flow for endorsement forms and policy-doc upload form)
+- Updated: `app/Views/policy_transaction_endorsement_form.php` (script include for endorsement validation)
+- Updated: `app/Views/policy_transaction_endorsement_form.php` (Parsley-driven red/green live field highlight styles)
+
+## Leads Form Analysis (New Scope)
+- Target file: `app/Views/leads_form.php`
+- Main form identified: `#leads_form_id` (Parsley form)
+- Existing constraints found:
+ - PAN and GST already use dedicated Parsley regex rules.
+ - Several dynamic sections append policy/custom fields into `#dynamic-form-container` and `#appendArea_*`.
+ - Contact fields include `contact_person_mobile` and `contact_person_email` as text inputs.
+- Risk points for charset-only validation:
+ - Email fields must allow `@` and domain characters.
+ - Mobile should stay digits-only and length constrained.
+ - Existing PAN/GST pattern validation must remain untouched.
+
+## Leads Form Plan
+- Create separate JS file:
+ - `public/assets/js/pages/leads_form_validation.js`
+- Add custom Parsley validators for leads page:
+ - Generic charset validator (allow: letters, numbers, `/`, `_`, `-`, `.`, space)
+ - Mobile validator (10 digits)
+ - Email validator (valid email format)
+- Bind delegated `input`/`change` live validation on `#leads_form_id` so dynamic fields are automatically covered.
+- Apply constraints by field type/ID:
+ - `contact_person_mobile` -> mobile validator
+ - `contact_person_email` -> email validator
+ - PAN/GST fields keep their existing `data-parsley-pattern` rules
+ - Other textual fields -> charset validator
+- Add non-intrusive Parsley visual styles (`parsley-error` / `parsley-success`) in `leads_form.php`.
+- Include new JS file in `leads_form.php` after existing page-level script setup.
+
+## Leads Delivered Changes
+- Added: `public/assets/js/pages/leads_form_validation.js`
+- Updated: `app/Views/leads_form.php` (script include for leads live validation)
+- Updated: `app/Views/leads_form.php` (Parsley-driven red/green live field highlight styles)
+
+## New Client Modal Delivered Changes
+- Added: `public/assets/js/pages/new_client_modal_validation.js`
+- Updated: `app/Views/newClientModal.php` (script include for modal live validation)
+- Updated: `app/Views/newClientModal.php` (Parsley-driven red/green live field highlight styles)
diff --git a/public/assets/js/pages/leads_form_validation.js b/public/assets/js/pages/leads_form_validation.js
new file mode 100644
index 00000000..0f470a38
--- /dev/null
+++ b/public/assets/js/pages/leads_form_validation.js
@@ -0,0 +1,247 @@
+(function (getJq) {
+ 'use strict';
+
+ function $(selector, context) {
+ var jq = getJq();
+ if (!jq) {
+ return { length: 0 };
+ }
+ return arguments.length > 1 ? jq(selector, context) : jq(selector);
+ }
+
+ var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ var FORM_SELECTOR = '#leads_form_id';
+ var NS = '.leadsFormValidate';
+
+ var MESSAGES = {
+ text: 'Only letters, numbers, spaces, and the characters / _ - . are allowed.',
+ mobile: 'Mobile number must be exactly 10 digits (numbers only).',
+ email: 'Please enter a valid email address.'
+ };
+
+ function isParsleyReady() {
+ return !!window.Parsley;
+ }
+
+ function isSkippableField(el) {
+ return !el || el.disabled || el.type === 'hidden';
+ }
+
+ function isPanOrGstField(el) {
+ if (!el) {
+ return false;
+ }
+ var id = (el.id || '').toLowerCase();
+ return id === 'pan' || id === 'gst';
+ }
+
+ function isEmailField(el) {
+ if (!el) {
+ return false;
+ }
+ var id = (el.id || '').toLowerCase();
+ var name = (el.name || '').toLowerCase();
+ var type = (el.type || '').toLowerCase();
+ return id === 'contact_person_email' || name === 'contact_person_email' || type === 'email';
+ }
+
+ function isMobileField(el) {
+ if (!el) {
+ return false;
+ }
+ var id = (el.id || '').toLowerCase();
+ var name = (el.name || '').toLowerCase();
+ return id === 'contact_person_mobile' || name === 'contact_person_mobile';
+ }
+
+ function isCharsetCandidate(el) {
+ if (!el || el.tagName === 'SELECT') {
+ return false;
+ }
+ var type = (el.type || '').toLowerCase();
+ if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'submit' || type === 'button' || type === 'reset') {
+ return false;
+ }
+ if (isPanOrGstField(el) || isEmailField(el) || isMobileField(el)) {
+ return false;
+ }
+ return true;
+ }
+
+ function registerValidators() {
+ if (!isParsleyReady() || window.Parsley.__leadsFormValidatorsRegistered) {
+ return;
+ }
+
+ window.Parsley.addValidator('leadscharset', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return TEXT_ALLOWED.test(String(value));
+ },
+ messages: { en: MESSAGES.text }
+ });
+
+ window.Parsley.addValidator('leadsmobile10', {
+ validateString: function (value, req, instance) {
+ var v = String(value || '').replace(/\D/g, '');
+ if (!instance.$element.prop('required') && v.length === 0) {
+ return true;
+ }
+ if (instance.$element.prop('required') && v.length === 0) {
+ return true;
+ }
+ return v.length === 10;
+ },
+ messages: { en: MESSAGES.mobile }
+ });
+
+ window.Parsley.addValidator('leadsemail', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return EMAIL_RE.test(String(value).trim());
+ },
+ messages: { en: MESSAGES.email }
+ });
+
+ window.Parsley.__leadsFormValidatorsRegistered = true;
+ }
+
+ function clearAttrs($el) {
+ ['data-parsley-leadscharset', 'data-parsley-leadsmobile10', 'data-parsley-leadsemail'].forEach(function (a) {
+ $el.removeAttr(a);
+ });
+ }
+
+ function applyConstraints($form) {
+ if (!$form || !$form.length) {
+ return;
+ }
+
+ $form.find('input, textarea, select').each(function () {
+ var el = this;
+ var $el = $(el);
+
+ if (isSkippableField(el) || isPanOrGstField(el)) {
+ return;
+ }
+
+ clearAttrs($el);
+
+ if (isMobileField(el)) {
+ $el.attr('data-parsley-leadsmobile10', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (isEmailField(el)) {
+ $el.attr('data-parsley-leadsemail', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (isCharsetCandidate(el)) {
+ $el.attr('data-parsley-leadscharset', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'change');
+ }
+ });
+ }
+
+ function sanitizeOnInput(el) {
+ if (!el || isSkippableField(el)) {
+ return;
+ }
+ if (isMobileField(el)) {
+ var clean = (el.value || '').replace(/\D/g, '').substring(0, 10);
+ if (el.value !== clean) {
+ el.value = clean;
+ }
+ return;
+ }
+ if (isCharsetCandidate(el)) {
+ var raw = el.value || '';
+ if (!TEXT_ALLOWED.test(raw)) {
+ el.value = raw.replace(/[^A-Za-z0-9/_.\- ]/g, '');
+ }
+ }
+ }
+
+ function refreshParsley($form) {
+ if (!$form || !$form.length || !isParsleyReady()) {
+ return;
+ }
+ try {
+ var instance = $form.parsley();
+ if (instance && typeof instance.refresh === 'function') {
+ instance.refresh();
+ }
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function validateField(field) {
+ if (!field || isSkippableField(field) || !isParsleyReady()) {
+ return;
+ }
+ var $field = $(field);
+ var $form = $field.closest('form');
+ if (!$form.length || typeof $field.parsley !== 'function') {
+ return;
+ }
+
+ applyConstraints($form);
+ refreshParsley($form);
+
+ try {
+ $field.parsley().validate();
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function init() {
+ var $form = $(FORM_SELECTOR);
+ if (!$form.length || !isParsleyReady()) {
+ return;
+ }
+
+ registerValidators();
+ applyConstraints($form);
+ refreshParsley($form);
+
+ $form.off(NS);
+ $form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
+ sanitizeOnInput(this);
+ validateField(this);
+ });
+ $form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+ }
+
+ $(function () {
+ init();
+ });
+
+ window.refreshLeadsFormValidation = function () {
+ init();
+ };
+})(function () {
+ return window.jQuery;
+});
diff --git a/public/assets/js/pages/new_client_modal_validation.js b/public/assets/js/pages/new_client_modal_validation.js
new file mode 100644
index 00000000..fef444e2
--- /dev/null
+++ b/public/assets/js/pages/new_client_modal_validation.js
@@ -0,0 +1,220 @@
+(function (getJq) {
+ 'use strict';
+
+ function $(selector, context) {
+ var jq = getJq();
+ if (!jq) {
+ return { length: 0 };
+ }
+ return arguments.length > 1 ? jq(selector, context) : jq(selector);
+ }
+
+ var FORM_SELECTOR = '#client_form';
+ var NS = '.newClientModalValidate';
+ var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ function isParsleyReady() {
+ return !!window.Parsley;
+ }
+
+ function isSkippable(el) {
+ return !el || el.disabled || el.type === 'hidden';
+ }
+
+ function isPanOrGst(el) {
+ var id = (el.id || '').toLowerCase();
+ return id === 'pan' || id === 'gst';
+ }
+
+ function isMobile(el) {
+ var id = (el.id || '').toLowerCase();
+ var name = (el.name || '').toLowerCase();
+ return id === 'mobile' || id === 'phone' || name === 'mobile' || name === 'phone';
+ }
+
+ function isEmail(el) {
+ var type = (el.type || '').toLowerCase();
+ var id = (el.id || '').toLowerCase();
+ var name = (el.name || '').toLowerCase();
+ return type === 'email' || id === 'email' || id === 'email2' || name === 'email' || name === 'email2';
+ }
+
+ function isCharsetCandidate(el) {
+ if (!el || el.tagName === 'SELECT') {
+ return false;
+ }
+ var type = (el.type || '').toLowerCase();
+ if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'submit' || type === 'button' || type === 'reset') {
+ return false;
+ }
+ if (isPanOrGst(el) || isMobile(el) || isEmail(el)) {
+ return false;
+ }
+ return true;
+ }
+
+ function registerValidators() {
+ if (!isParsleyReady() || window.Parsley.__newClientModalValidatorsRegistered) {
+ return;
+ }
+
+ window.Parsley.addValidator('newclientcharset', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return TEXT_ALLOWED.test(String(value));
+ },
+ messages: { en: 'Only letters, numbers, spaces, and the characters / _ - . are allowed.' }
+ });
+
+ window.Parsley.addValidator('newclientmobile10', {
+ validateString: function (value, req, instance) {
+ var v = String(value || '').replace(/\D/g, '');
+ if (!instance.$element.prop('required') && v.length === 0) {
+ return true;
+ }
+ if (instance.$element.prop('required') && v.length === 0) {
+ return true;
+ }
+ return v.length === 10;
+ },
+ messages: { en: 'Mobile number must be exactly 10 digits (numbers only).' }
+ });
+
+ window.Parsley.addValidator('newclientemail', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return EMAIL_RE.test(String(value).trim());
+ },
+ messages: { en: 'Please enter a valid email address.' }
+ });
+
+ window.Parsley.__newClientModalValidatorsRegistered = true;
+ }
+
+ function clearAttrs($el) {
+ ['data-parsley-newclientcharset', 'data-parsley-newclientmobile10', 'data-parsley-newclientemail'].forEach(function (a) {
+ $el.removeAttr(a);
+ });
+ }
+
+ function applyConstraints($form) {
+ $form.find('input, textarea, select').each(function () {
+ var el = this;
+ var $el = $(el);
+ if (isSkippable(el) || isPanOrGst(el)) {
+ return;
+ }
+
+ clearAttrs($el);
+
+ if (isMobile(el)) {
+ $el.attr('data-parsley-newclientmobile10', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (isEmail(el)) {
+ $el.attr('data-parsley-newclientemail', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (isCharsetCandidate(el)) {
+ $el.attr('data-parsley-newclientcharset', 'true');
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'change');
+ }
+ });
+ }
+
+ function sanitizeOnInput(el) {
+ if (!el || isSkippable(el)) {
+ return;
+ }
+ if (isMobile(el)) {
+ var d = (el.value || '').replace(/\D/g, '').substring(0, 10);
+ if (el.value !== d) {
+ el.value = d;
+ }
+ return;
+ }
+ if (isCharsetCandidate(el)) {
+ var raw = el.value || '';
+ if (!TEXT_ALLOWED.test(raw)) {
+ el.value = raw.replace(/[^A-Za-z0-9/_.\- ]/g, '');
+ }
+ }
+ }
+
+ function refreshParsley($form) {
+ try {
+ var p = $form.parsley();
+ if (p && typeof p.refresh === 'function') {
+ p.refresh();
+ }
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function validateField(el) {
+ if (!el || isSkippable(el) || !isParsleyReady()) {
+ return;
+ }
+ var $f = $(el).closest('form');
+ if (!$f.length) {
+ return;
+ }
+ applyConstraints($f);
+ refreshParsley($f);
+ try {
+ $(el).parsley().validate();
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function init() {
+ var $form = $(FORM_SELECTOR);
+ if (!$form.length || !isParsleyReady()) {
+ return;
+ }
+ registerValidators();
+ applyConstraints($form);
+ refreshParsley($form);
+
+ $form.off(NS);
+ $form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
+ sanitizeOnInput(this);
+ validateField(this);
+ });
+ $form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+ }
+
+ $(function () {
+ init();
+ });
+
+ window.refreshNewClientModalValidation = function () {
+ init();
+ };
+})(function () {
+ return window.jQuery;
+});
diff --git a/public/assets/js/pages/policy_transaction_endorsement_form_validation.js b/public/assets/js/pages/policy_transaction_endorsement_form_validation.js
new file mode 100644
index 00000000..436e65f0
--- /dev/null
+++ b/public/assets/js/pages/policy_transaction_endorsement_form_validation.js
@@ -0,0 +1,166 @@
+(function (getJq) {
+ 'use strict';
+
+ function $(selector, context) {
+ var jq = getJq();
+ if (!jq) {
+ return { length: 0 };
+ }
+ return arguments.length > 1 ? jq(selector, context) : jq(selector);
+ }
+
+ var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
+ var MESSAGE = 'Only letters, numbers, spaces, and the characters / _ - . are allowed.';
+ var FORM_SELECTORS = ['#endorsement_form_id', '#drive_file_upload_form', '#file_upload_form'];
+ var NS = '.endorsementFormValidate';
+
+ function isParsleyReady() {
+ return !!window.Parsley;
+ }
+
+ function isSkippableField(el) {
+ return !el || el.disabled || el.type === 'hidden';
+ }
+
+ function isDocumentNameField(el) {
+ if (!el) {
+ return false;
+ }
+ var name = (el.name || '').toLowerCase();
+ var id = (el.id || '').toLowerCase();
+ return name === 'doc_name[]' ||
+ name === 'other_docs_name[]' ||
+ name === 'docs_name[]' ||
+ id === 'docs_name' ||
+ $(el).hasClass('other-doc-name-field');
+ }
+
+ function isCharsetCandidate(el) {
+ if (!el || el.tagName === 'SELECT') {
+ return false;
+ }
+ var type = (el.type || '').toLowerCase();
+ if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'email' || type === 'url' || type === 'submit' || type === 'button' || type === 'reset') {
+ return false;
+ }
+ return true;
+ }
+
+ function registerValidator() {
+ if (!isParsleyReady() || window.Parsley.__endorsementFormValidatorRegistered) {
+ return;
+ }
+ window.Parsley.addValidator('endorsementcharset', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return TEXT_ALLOWED.test(String(value));
+ },
+ messages: { en: MESSAGE }
+ });
+ window.Parsley.__endorsementFormValidatorRegistered = true;
+ }
+
+ function applyConstraints($form) {
+ if (!$form || !$form.length) {
+ return;
+ }
+ $form.find('input, textarea, select').each(function () {
+ var el = this;
+ var $el = $(el);
+ if (isSkippableField(el)) {
+ return;
+ }
+
+ if (isCharsetCandidate(el) || isDocumentNameField(el)) {
+ if (!$el.attr('data-parsley-endorsementcharset')) {
+ $el.attr('data-parsley-endorsementcharset', 'true');
+ }
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'change');
+ }
+ });
+ }
+
+ function refreshParsley($form) {
+ if (!$form || !$form.length || !isParsleyReady()) {
+ return;
+ }
+ try {
+ var instance = $form.parsley();
+ if (instance && typeof instance.refresh === 'function') {
+ instance.refresh();
+ }
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function validateField(field) {
+ if (!field || isSkippableField(field) || !isParsleyReady()) {
+ return;
+ }
+ var $field = $(field);
+ var $form = $field.closest('form');
+ if (!$form.length || typeof $field.parsley !== 'function') {
+ return;
+ }
+
+ applyConstraints($form);
+ refreshParsley($form);
+
+ try {
+ $field.parsley().validate();
+ } catch (e) {
+ // no-op
+ }
+ }
+
+ function bindForm(selector) {
+ var $form = $(selector);
+ if (!$form.length) {
+ return;
+ }
+
+ registerValidator();
+ applyConstraints($form);
+ refreshParsley($form);
+
+ $form.off(NS);
+ $form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+ $form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+ }
+
+ function init() {
+ if (!isParsleyReady()) {
+ return;
+ }
+ FORM_SELECTORS.forEach(function (selector) {
+ bindForm(selector);
+ });
+ }
+
+ $(function () {
+ init();
+ });
+
+ window.refreshEndorsementFormValidation = function (formSelector) {
+ if (!formSelector || !isParsleyReady()) {
+ return;
+ }
+ bindForm(formSelector);
+ };
+})(function () {
+ return window.jQuery;
+});
diff --git a/public/assets/js/pages/policy_transaction_inception_form_validation.js b/public/assets/js/pages/policy_transaction_inception_form_validation.js
new file mode 100644
index 00000000..634b6b29
--- /dev/null
+++ b/public/assets/js/pages/policy_transaction_inception_form_validation.js
@@ -0,0 +1,185 @@
+(function (getJq) {
+ 'use strict';
+
+ function $(selector, context) {
+ var jq = getJq();
+ if (!jq) {
+ return { length: 0 };
+ }
+ return arguments.length > 1 ? jq(selector, context) : jq(selector);
+ }
+
+ var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
+ var MESSAGE = 'Only letters, numbers, spaces, and the characters / _ - . are allowed.';
+ var FORM_SELECTORS = ['#inception_form_id', '#vehicle_form', '#CDMasterForm', '#file_upload_form', '#kyc_form'];
+ var INCEPTION_NS = '.inceptionFormValidate';
+
+ function isParsleyReady() {
+ return !!window.Parsley;
+ }
+
+ function isSkippableField(el) {
+ if (!el) {
+ return true;
+ }
+ if (el.disabled) {
+ return true;
+ }
+ if (el.type === 'hidden') {
+ return true;
+ }
+ return false;
+ }
+
+ function isCharsetCandidate(el) {
+ if (!el || el.tagName === 'SELECT') {
+ return false;
+ }
+
+ var type = (el.type || '').toLowerCase();
+ if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'email' || type === 'url' || type === 'submit' || type === 'button' || type === 'reset') {
+ return false;
+ }
+ return true;
+ }
+
+ function isDocumentNameField(el) {
+ if (!el) {
+ return false;
+ }
+ var name = (el.name || '').toLowerCase();
+ var id = (el.id || '').toLowerCase();
+ return name === 'doc_name[]' ||
+ name === 'other_docs_name[]' ||
+ name === 'docs_name[]' ||
+ id === 'docs_name' ||
+ $(el).hasClass('other-doc-name-field');
+ }
+
+ function registerValidator() {
+ if (!isParsleyReady() || window.Parsley.__inceptionFormValidatorRegistered) {
+ return;
+ }
+
+ window.Parsley.addValidator('inceptioncharset', {
+ validateString: function (value) {
+ if (!value || String(value).trim() === '') {
+ return true;
+ }
+ return TEXT_ALLOWED.test(String(value));
+ },
+ messages: { en: MESSAGE }
+ });
+
+ window.Parsley.__inceptionFormValidatorRegistered = true;
+ }
+
+ function applyConstraints($form) {
+ if (!$form || !$form.length) {
+ return;
+ }
+
+ $form.find('input, textarea, select').each(function () {
+ var el = this;
+ var $el = $(el);
+
+ if (isSkippableField(el)) {
+ return;
+ }
+
+ if (isCharsetCandidate(el) || isDocumentNameField(el)) {
+ if (!$el.attr('data-parsley-inceptioncharset')) {
+ $el.attr('data-parsley-inceptioncharset', 'true');
+ }
+ if (!$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'input change');
+ }
+ return;
+ }
+
+ if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
+ $el.attr('data-parsley-trigger', 'change');
+ }
+ });
+ }
+
+ function refreshParsleyInstance($form) {
+ if (!$form || !$form.length || !isParsleyReady()) {
+ return;
+ }
+
+ try {
+ var parsleyInstance = $form.parsley();
+ if (parsleyInstance && typeof parsleyInstance.refresh === 'function') {
+ parsleyInstance.refresh();
+ }
+ } catch (e) {
+ // no-op; keep existing form flow unchanged
+ }
+ }
+
+ function validateField(field) {
+ if (!field || isSkippableField(field) || !isParsleyReady()) {
+ return;
+ }
+
+ var $field = $(field);
+ var $form = $field.closest('form');
+ if (!$form.length || typeof $field.parsley !== 'function') {
+ return;
+ }
+
+ // Ensure dynamic fields also receive constraints before validation.
+ applyConstraints($form);
+ refreshParsleyInstance($form);
+
+ try {
+ $field.parsley().validate();
+ } catch (e) {
+ // no-op; avoid interfering with existing page scripts
+ }
+ }
+
+ function bindFormEvents(selector) {
+ var $form = $(selector);
+ if (!$form.length) {
+ return;
+ }
+
+ registerValidator();
+ applyConstraints($form);
+ refreshParsleyInstance($form);
+
+ $form.off(INCEPTION_NS);
+
+ $form.on('input' + INCEPTION_NS, 'input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+
+ $form.on('change' + INCEPTION_NS, 'select, input:not([type=hidden]), textarea', function () {
+ validateField(this);
+ });
+ }
+
+ function init() {
+ if (!isParsleyReady()) {
+ return;
+ }
+ FORM_SELECTORS.forEach(function (selector) {
+ bindFormEvents(selector);
+ });
+ }
+
+ $(function () {
+ init();
+ });
+
+ window.refreshInceptionFormValidation = function (formSelector) {
+ if (!formSelector || !isParsleyReady()) {
+ return;
+ }
+ bindFormEvents(formSelector);
+ };
+})(function () {
+ return window.jQuery;
+});
diff --git a/public/dev_logs/2026-04-06_opd_policy_terms_72_plan.md b/public/dev_logs/2026-04-06_opd_policy_terms_72_plan.md
new file mode 100644
index 00000000..2cb1952c
--- /dev/null
+++ b/public/dev_logs/2026-04-06_opd_policy_terms_72_plan.md
@@ -0,0 +1,157 @@
+# 2026-04-06 — OPD Policy Terms Plan (`policy_type_id = 72`)
+
+## Context
+
+- Policy type: `72`
+- Policy terms label: `OPD Policy Terms`
+- Requested fields/content:
+ - `Mode of Serviceability`
+ - `Eligibility`
+ - `Total Sum Insured limit - INR 15000`
+ - `In Person Doctor Consultation`
+ - `Prescribed Lab test (Pathology & Radiology)`
+ - `Prescribed Pharmacy`
+ - `Dental`
+ - `Vision`
+ - `Vaccination for children & adults`
+- Target view: `app/Views/other_policy_terms.php`
+
+## Problem Identified
+
+- OPD fields were visible in UI but not saved to DB for `policy_type_id = 72`.
+- Root cause: `app/Controllers/ClientController.php` in `otherPolicyTermsFormSubmit()` uses a strict field mapping for policy `72` and did not include the newly added OPD keys.
+- Impact: Submitted payload contained OPD fields, but controller dropped them before `policy_terms` JSON update.
+
+## Fix Plan
+
+1. Update `policy_type_id = 72` save mapping
+ - Add OPD keys to `$data` construction in `otherPolicyTermsFormSubmit()`.
+ - Include defaults where applicable (`total_sum_insured_limit` fallback to `INR 15000`).
+
+2. Keep existing family floater/age mapping unchanged
+ - Preserve current business logic for `family_floater`, `family_floaters`, and `age_ratio`.
+ - Add OPD terms in additive mode only.
+
+3. Validate controller integrity
+ - Run PHP syntax check for `ClientController.php`.
+ - Confirm no changes to unrelated policy type save flow.
+
+## New Requirement Plan - `_display` Checkbox -> `enrollment_display_key` for `other_policy_terms`
+
+### Reference behavior (from `app/Views/policy_gmc_terms.php`)
+
+- Each term row includes a checkbox with `name` ending in `_display`.
+- Only checked display keys are reflected in `enrollment_display_key`.
+- Existing loader flow reads `enrollment_display_key` and restores checkbox states.
+
+### Problem to solve in `other_policy_terms`
+
+- For policy type `72`, OPD fields currently do not have `*_display` checkboxes.
+- In controller, `otherPolicyTermsDisplayKeyConstruct()` currently builds `enrollment_display_key` only from special condition label/input pairs, not from `*_display` checkboxes.
+- Result: checkbox-driven display selection is not persisted/reloaded for OPD terms.
+
+### Implementation plan
+
+1. Add `*_display` checkboxes to OPD rows in `app/Views/other_policy_terms.php`
+ - For each OPD field (`mode_of_serviceability`, `eligibility`, `total_sum_insured_limit`, etc.), add a checkbox input:
+ - `name="_display"`
+ - `id="_display"`
+ - class `unchecked`
+ - default checked
+ - Keep checkbox + label + value input row layout aligned with existing non-72 dynamic term rows.
+
+2. Update `otherPolicyTermsDisplayKeyConstruct()` in `app/Controllers/ClientController.php`
+ - Extend logic to parse all incoming keys ending with `_display`.
+ - For each checked display key, map base key to human-readable label and value from the corresponding base field.
+ - Preserve current special-condition mapping behavior; merge both outputs into one `enrollment_display_key`.
+
+3. Keep save flow backward compatible
+ - Do not alter existing `policy_type_id == 72` family floater and age mapping.
+ - Ensure OPD values and `enrollment_display_key` are both saved in `policy_terms`.
+ - Keep non-72 flow unchanged.
+
+4. Restore checkbox states on load
+ - Reuse existing `processJsonObject`-style behavior in `other_policy_terms` (if missing, add equivalent) to set `*_display` checked state based on `enrollment_display_key`.
+ - Verify for both newly created and previously saved records.
+
+5. Validate end-to-end
+ - Save with mixed checked/unchecked OPD display checkboxes.
+ - Confirm DB JSON includes expected `enrollment_display_key` entries.
+ - Reload and verify display checkbox states are restored.
+
+## Implementation Plan
+
+1. Place OPD terms section immediately after `familyFloaterDiv_others_two`
+ - Keep `familyFloaterDiv_others_two` as the first visible block for policy `72`.
+ - Insert OPD terms container directly below it in the DOM order (not before it).
+ - Ensure the OPD section appears before special conditions.
+
+2. Add OPD-specific policy terms UI block
+ - Add dedicated OPD template/HTML for `policy_type_id = 72`.
+ - Render OPD rows as labeled text inputs with stable keys (`mode_of_serviceability`, `eligibility`, etc.).
+ - Keep field names in snake_case so they serialize cleanly into `policy_terms` JSON.
+
+3. Handle fixed sum insured limit cleanly
+ - Add a separate OPD field key like `total_sum_insured_limit`.
+ - Set default value as `15000` (or `INR 15000` based on UI format) for new entries.
+ - Keep numeric sanitization consistent with existing sum insured input behavior where applicable.
+
+4. Preserve existing `policy_type_id = 72` family floater behavior
+ - Do not remove current family floater section/logic already tied to `policy_type_id == 72`.
+ - Ensure OPD terms are additive and do not break `family_floater`, `family_floaters`, and `age_ratio` handling.
+
+5. Populate saved data on edit
+ - Reuse existing JSON hydration flow (`Object.keys(jsonObject)` loop) so OPD fields auto-populate by `name`.
+ - Confirm keys in UI match keys stored in policy terms JSON exactly.
+
+6. Validate display behavior
+ - Confirm form section opens for `policy_type_id > 5`.
+ - Confirm non-72 cleanup (`if(policy_type_id != 72)`) does not remove OPD fields when policy type is 72.
+ - Confirm OPD fields are not rendered for unrelated policy types.
+ - Confirm visual order is `familyFloaterDiv_others_two` -> OPD terms -> special conditions.
+
+7. QA checklist
+ - Create a new policy terms record for `policy_type_id = 72` with all OPD values.
+ - Reload and verify values repopulate correctly.
+ - Verify submit and autosave payload include OPD keys in `policy_terms`.
+ - Verify no regressions for policy types `6` and `7`.
+
+## Suggested Field Keys
+
+- `mode_of_serviceability`
+- `eligibility`
+- `total_sum_insured_limit`
+- `in_person_doctor_consultation`
+- `prescribed_lab_test_pathology_radiology`
+- `prescribed_pharmacy`
+- `dental`
+- `vision`
+- `vaccination_for_children_and_adults`
+
+## Tasks
+
+- [x] Place OPD section after `familyFloaterDiv_others_two` in UI order.
+- [x] Add `policy_type == 72` OPD HTML block in `appendPolicyTermsHTML()`.
+- [x] Ensure defaults are applied for total sum insured limit.
+- [x] Verify bind/populate works from existing JSON loader.
+- [x] Fix OPD term persistence in `ClientController::otherPolicyTermsFormSubmit()` for `policy_type_id = 72`.
+- [x] Add temporary debug logging for OPD72 save payload in controller.
+- [x] Add `*_display` checkboxes for OPD term rows in `other_policy_terms.php`.
+- [x] Extend `otherPolicyTermsDisplayKeyConstruct()` to include checked `*_display` keys.
+- [x] Ensure `enrollment_display_key` restores OPD checkbox states in `other_policy_terms.php`.
+- [ ] Verify DB `policy_terms.enrollment_display_key` for mixed checked/unchecked OPD fields.
+- [ ] Run manual UI verification for create/edit/submit/autosave.
+- [x] Confirm no regressions for other policy types (code-level condition check and syntax validation completed).
+- [ ] Remove temporary debug logging after verification.
+
+## Verification Notes
+
+- PHP syntax check passed for `app/Views/other_policy_terms.php` (`php -l`).
+- PHP syntax check passed for `app/Controllers/ClientController.php` (`php -l`).
+- Verified `policy_type_id == 72` now renders OPD fields in both create and edit flows.
+- Verified OPD fields are now rendered in a dedicated container placed after `familyFloaterDiv_others_two`.
+- Verified non-`72` cleanup blocks remain scoped (`sumInsuredDiv`/family floater removals are still excluded for `72`).
+- Added temporary controller debug log (`OPD72 save payload`) to confirm persisted key/value mapping during manual test.
+- Added OPD `*_display` checkboxes and controller mapping so checked fields are now included in `enrollment_display_key`.
+- Added `processOtherEnrollmentDisplayKey()` in `other_policy_terms.php` to restore OPD display checkbox states from saved `enrollment_display_key`.
+- Manual browser verification is still required for submit + autosave end-to-end confirmation.