CHANGE_CLAIM_MODULE_FRONTEND_VALIDATION

This commit is contained in:
VENKATESHWARAN 2026-04-06 11:52:56 +05:30
parent f438a51af7
commit 201766675b
18 changed files with 1960 additions and 126 deletions

View File

@ -191,6 +191,10 @@ class JobWorker extends AdminController
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\VidalApiController',
],
'VidalGetBenefDetailsV2' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\VidalApiController',
],
'saveVidalAPIData' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\VidalApiController',

View File

@ -1410,7 +1410,7 @@ class TestingController extends BaseController
], 200);
}
/**
/**
* Test Wellness SSO token generation for Medi Assist (MediBuddy).
*
* This uses the token-based authentication details shared by Medi Assist:
@ -1719,36 +1719,18 @@ class TestingController extends BaseController
{
helper('api');
$url = 'https://devapigw.vidalhealthtpa.com/partner-integration/enrollment/enrollment-info';
$url = 'https://devapigw.vidalhealthtpa.com/partner-integration/enrollment/info';
$subscriptionKey = getenv('VIDAL_API_SUBSCRIPTION_KEY');
if (empty($subscriptionKey)) {
return $this->response->setStatusCode(500)->setJSON([
'error' => 'Missing VIDAL_SUBSCRIPTION_KEY/VIDAL_API_SUBSCRIPTION_KEY in env',
'error' => 'Missing VIDAL_API_SUBSCRIPTION_KEY in env',
]);
}
// Allow overriding payload via query/post/json; fall back to existing defaults.
$jsonPayload = $this->request->getJSON(true);
$policyNo = (string) (
($this->request->getGet('policyNo') ?? '')
?: ($this->request->getPost('policyNo') ?? '')
?: ($jsonPayload['policyNo'] ?? '')
?: '000/VZXSY'
);
$startIndex = (int) (
($this->request->getGet('startIndex') ?? null)
?? ($this->request->getPost('startIndex') ?? null)
?? ($jsonPayload['startIndex'] ?? 1)
);
$endIndex = (int) (
($this->request->getGet('endIndex') ?? null)
?? ($this->request->getPost('endIndex') ?? null)
?? ($jsonPayload['endIndex'] ?? 5)
);
$policyNo = '000/VZXSY';
$startIndex = 1;
$endIndex = 5;
$body = [
'policyNo' => $policyNo,
@ -1758,12 +1740,12 @@ class TestingController extends BaseController
$headers = [
'Content-Type: application/json',
'Ocp-Apim-Subscription-Key: ' . $subscriptionKey,
'ocp-apim-subscription-key: ' . $subscriptionKey,
];
$method = "POST";
$rawResponse = call_third_party_api($url, $method, $headers, json_encode($body));
$rawResponse = call_third_party_api($url, $method, $headers, $body);
return $this->response->setStatusCode(200)->setJSON([
'request' => [

View File

@ -3223,6 +3223,42 @@ class TicketController extends BaseController
return $date && $date->format($format) === $value;
}
/**
* Claim upload URL row: allow http(s) with path, query, port; bare host with TLD; localhost; IPv4/IPv6.
* Replaces the old strict regex that rejected ?query= and long TLDs.
*/
private function isClaimUploadUrl(string $s): bool
{
$v = trim($s);
if ($v === '' || strlen($v) > 2048) {
return false;
}
if (! preg_match('#^https?://#i', $v)) {
$v = 'https://' . $v;
}
$parts = parse_url($v);
if ($parts === false || empty($parts['host'])) {
return false;
}
$scheme = strtolower($parts['scheme'] ?? '');
if ($scheme !== 'http' && $scheme !== 'https') {
return false;
}
$host = strtolower($parts['host']);
if ($host === 'localhost') {
return true;
}
$hostForIp = $host;
if (strlen($host) > 2 && $host[0] === '[' && substr($host, -1) === ']') {
$hostForIp = substr($host, 1, -1);
}
if (filter_var($hostForIp, FILTER_VALIDATE_IP)) {
return true;
}
return strpos($host, '.') !== false;
}
public function upload_url()
{
@ -3249,10 +3285,9 @@ class TicketController extends BaseController
],
'url.*' => [
'label' => 'URL',
'rules' => 'if_exist|required|regex_match[/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/]',
'rules' => 'if_exist|required',
'errors' => [
'required' => 'URL is required.',
'regex_match' => 'The URL format is invalid. Example: www.google.com or https://google.com'
'required' => 'URL is required.',
]
],
];
@ -3266,6 +3301,22 @@ class TicketController extends BaseController
]);
}
if (! empty($data['url']) && is_array($data['url'])) {
foreach ($data['url'] as $idx => $singleUrl) {
$singleUrl = (string) $singleUrl;
if (trim($singleUrl) !== '' && ! $this->isClaimUploadUrl($singleUrl)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => [
'url.' . $idx => 'The URL format is invalid. Use https://example.com/path?x=1 or example.com',
],
]);
}
}
}
$get_file_data = $this->request->getFiles('file_upload') ?? [];
$ticket_id = $data['ticket_id_url'];
@ -3840,7 +3891,7 @@ class TicketController extends BaseController
]);
}
// YOUR REQUIRED REGEX RULE
// Same charset as claim upload `docs_name.*` / client `ticketdocname`
if (!preg_match('/^[a-zA-Z0-9_\- ]+$/', $doc['document_name'])) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
@ -3848,7 +3899,7 @@ class TicketController extends BaseController
'message' => 'Input validation failed',
'errors' => [
'required_docs' =>
"Document name '{$doc['document_name']}' can only contain letters, numbers, hyphens and underscores"
"Document Name can only contain letters, numbers, hyphens, and underscores (row " . ($index + 1) . ")"
]
]);
}

View File

@ -533,6 +533,12 @@ class VoloApiController extends BaseController
$dobYmd = $this->normalizeVoloDobToYmd($dobRaw);
}
$dojRaw = $row['DOJ'] ?? $row['doj'] ?? null;
$dojYmd = null;
if ($dojRaw !== null && $dojRaw !== '') {
$dojYmd = $this->normalizeVoloDobToYmd($dojRaw);
}
$rel = trim((string) ($row['relation'] ?? ''));
$mappedRows[] = [
'file_id' => $file_id,
@ -544,6 +550,8 @@ class VoloApiController extends BaseController
'self' => in_array(strtoupper($rel), ['EMPLOYEE', 'SELF'], true) ? 1 : 0,
'tpa_id' => trim((string) ($row['memberId'] ?? '')),
'age' => isset($row['age']) && is_numeric($row['age']) ? (int) $row['age'] : null,
'si' => $row['sumInsured'] ?? null,
'doj' => $dojYmd,
'is_active' => 1,
'created_by' => $file_info[0]['created_by'] ?? null,
];

View File

@ -572,8 +572,12 @@ class EmployeePolicyModel extends Model
employee_polices.tpa_id,
employees.relationship_code as emp_relationship_code,
employees.relationship as emp_relationship,
employees.email_corporate as emp_email_c,
'C' as event_type_data,
employee_polices.date_coverage,
employee_polices.basic_cover_si,
CASE
WHEN emp_endorsement.field_name = 'dob' THEN
DATE_FORMAT(emp_endorsement.old_value, '%d-%b-%Y')
@ -705,6 +709,7 @@ class EmployeePolicyModel extends Model
employee_polices.premium AS old_si_premium,
employee_polices.rata_premimum AS old_rata_premium,
employee_polices.age_band,
employee_polices.date_coverage,
sidata.new_basic_cover_si,
sidata.new_si_premium,
sidata.old_si_premium,

View File

@ -27,9 +27,11 @@
</div>
</div>
<!-- Document List Container -->
<!-- Document List Container (same Document Name rules as claim upload card: ticketdocname) -->
<div class="row mb-3">
<div class="col-md-12">
<form id="ir_documents_form" class="parsley-examples" novalidate
data-parsley-validation-threshold="0">
<!-- Header Row -->
<div class="form-row mb-2">
<div class="col-md-7"><strong>Document Name</strong></div>
@ -39,17 +41,7 @@
<!-- Dynamic Document Rows -->
<div id="document-list-container"></div>
<!-- Add Button -->
<!-- <div class="row mt-3">
<div class="col-md-12 text-right">
<button type="button"
class="btn btn-primary waves-effect waves-light"
onclick="addDocument()">
<i class="mdi mdi-plus"></i> Add Document
</button>
</div>
</div> -->
</form>
</div>
</div>
</div>
@ -72,7 +64,8 @@
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<form class="parsley-examples" id="drive_file_upload_form" method="post"
enctype="multipart/form-data">
enctype="multipart/form-data" novalidate
data-parsley-validation-threshold="0">
<input type="hidden" id="ticket_id_url" name="ticket_id_url">
@ -134,7 +127,8 @@
<!-- edit modal -->
<div class="modal fade" id="edit_url_modal" tabindex="-1" role="dialog" aria-labelledby="editUrlModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<form id="edit_url_form">
<form id="edit_url_form" class="parsley-examples" novalidate
data-parsley-validation-threshold="0">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit URL</h5>
@ -148,12 +142,14 @@
<div class="form-group">
<label for="edit_doc_name">Document Name</label>
<input type="text" class="form-control" id="edit_doc_name" name="doc_name">
<input type="text" class="form-control" id="edit_doc_name" name="doc_name"
data-parsley-ticketdocname-message="Document name can only contain letters, numbers, spaces, hyphens, and underscores.">
</div>
<div class="form-group">
<label for="edit_url_link">URL</label>
<input type="text" class="form-control" id="edit_url_link" name="url">
<input type="text" class="form-control" id="edit_url_link" name="url"
data-parsley-ticketclaimurl-message="Enter a valid web address (e.g. https://example.com/path?x=1 or example.com).">
</div>
</div>
@ -178,10 +174,33 @@
event.preventDefault();
var isValid = $('#drive_file_upload_form').parsley().validate();
if (!isValid) {
console.log('Form is Empty', 'Warning');
return ;
if (typeof window.validateTicketFormInputs === 'function' && !window.validateTicketFormInputs(this)) {
toastr.warning('Form validation failed. Please correct the highlighted fields.', 'Validation');
var $form = $(this);
var $first = $form.find('.parsley-error').first();
if (!$first.length) {
$first = $form.find('input.parsley-error, select.parsley-error, textarea.parsley-error').first();
}
if ($first.length) {
$('html, body').animate({ scrollTop: $first.offset().top - 100 }, 400);
$first.focus();
}
return;
}
var badFileExt = false;
$('#drive_file_upload_form input[type=file]').each(function () {
if (!this.files || !this.files.length) {
return;
}
if (!/\.(pdf|jpe?g|png)$/i.test(this.files[0].name)) {
badFileExt = true;
return false;
}
});
if (badFileExt) {
toastr.warning('Only PDF, JPG, JPEG, and PNG files are allowed.', 'Validation');
return;
}
form_action = '<?php echo base_url() . 'ticket/upload_url' ?>';
@ -252,18 +271,25 @@
function addHTMLInput() {
const container = document.getElementById('dynamic-form-container');
const rowKey = 'u_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
const docsId = 'docs_name_' + rowKey;
const urlId = 'url_name_' + rowKey;
const newRow = document.createElement('div');
newRow.className = 'form-row dynamic-form-row';
newRow.innerHTML = `
<div class="form-group col-md-5">
<label for="file_name">Document Name<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="docs_name" name="docs_name[]" placeholder="Enter file name" required
<label for="${docsId}">Document Name<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="${docsId}" name="docs_name[]" placeholder="Enter file name" required
data-parsley-required-message="Document name is required."
data-parsley-ticketdocname-message="Document name can only contain letters, numbers, spaces, hyphens, and underscores."
value=""
>
</div>
<div class="form-group col-md-5">
<label for="file">URL<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="url_name" name="url[]" required
<label for="${urlId}">URL<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="${urlId}" name="url[]" required
data-parsley-required-message="URL is required."
data-parsley-ticketclaimurl-message="Enter a valid web address (e.g. https://example.com/path?x=1 or example.com)."
value=""
>
</div>
@ -274,7 +300,9 @@
</div>
`;
container.appendChild(newRow);
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
}
function removeHTMLInput(element) {
@ -284,6 +312,9 @@
if (rows.length > 1) {
const row = element.closest('.dynamic-form-row');
row.remove();
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
}
}
@ -319,6 +350,9 @@
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
console.log("Ajax is completed for get url data..!!");
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
}
});
}
@ -330,6 +364,25 @@
$('#edit_url_modal').modal('show'); // Bootstrap modal
}
$('#edit_url_modal').on('shown.bs.modal', function () {
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#edit_url_form');
}
});
$('#edit_url_form').on('submit', function (e) {
e.preventDefault();
if (typeof window.validateTicketFormInputs === 'function' && !window.validateTicketFormInputs(this)) {
toastr.warning('Please correct the highlighted fields.', 'Validation');
var $first = $(this).find('.parsley-error').first();
if ($first.length) {
$first.focus();
}
return false;
}
return false;
});
function create_url_list(data) {
$('#table_bd').empty(); // clear existing rows
@ -402,16 +455,23 @@
function addFileUploadHtml() {
const container = document.getElementById('dynamic-form-container');
const rowKey = 'f_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
const docsId = 'docs_name_' + rowKey;
const fileId = 'file_upload_' + rowKey;
const newRow = document.createElement('div');
newRow.className = 'form-row dynamic-form-row';
newRow.innerHTML = `
<div class="form-group col-md-5">
<label for="file_name">Document Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="docs_name" name="docs_name[]" placeholder="Enter file name" required>
<label for="${docsId}">Document Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="${docsId}" name="docs_name[]" placeholder="Enter file name" required
data-parsley-required-message="Document name is required."
data-parsley-ticketdocname-message="Document name can only contain letters, numbers, spaces, hyphens, and underscores.">
</div>
<div class="form-group col-md-5">
<label for="file">Choose File <span class="text-danger">*</span></label>
<input type="file" class="form-control" id="file_upload" name="file_upload[]" accept=".pdf,.jpg,.jpeg,.png" required>
<label for="${fileId}">Choose File <span class="text-danger">*</span></label>
<input type="file" class="form-control" id="${fileId}" name="file_upload[]" accept=".pdf,.jpg,.jpeg,.png" required
data-parsley-required-message="File is required."
data-parsley-claimfileext-message="Only PDF, JPG, JPEG, and PNG files are allowed.">
</div>
<div class="form-group col-md-2" style="position: relative; top: 28px;">
<a class="btn btn-danger waves-effect waves-light" onclick="removeHTMLInput(this)" style="background-color: #BD0707;">
@ -420,6 +480,9 @@
</div>
`;
container.appendChild(newRow);
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
}
function toggleUploadType(btn) {
@ -446,6 +509,9 @@
// call the function
addHTMLInput();
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
} else {
@ -459,6 +525,9 @@
// call the function
addFileUploadHtml();
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#drive_file_upload_form');
}
}
}
@ -508,6 +577,10 @@
const docRow = createDocumentRow(doc, index);
container.appendChild(docRow);
});
if (typeof window.refreshTicketFormValidationForForm === 'function') {
window.refreshTicketFormValidationForForm('#ir_documents_form');
}
}
// Create a single document row
@ -543,20 +616,35 @@
return row;
}
function escapeHtmlAttr(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/'/g, '&#39;');
}
function createDocumentRow(doc, index) {
const row = document.createElement('div');
row.className = 'form-row align-items-center mb-2';
row.dataset.index = index;
const isLastRow = index === documentConfig.docs.length - 1; // 👉 Check last item
const safeName = escapeHtmlAttr(doc.document_name);
const irId = 'ir_doc_name_' + index;
const reqAttrs = documentConfig.is_action_freeze ? 'disabled' : 'required';
row.innerHTML = `
<div class="col-md-7">
<input type="text" class="form-control"
placeholder="Document Name"
value="${doc.document_name}"
<input type="text" class="form-control"
id="${irId}"
name="ir_document_name[]"
placeholder="Document Name"
value="${safeName}"
onchange="updateDocumentName(${index}, this.value)"
${documentConfig.is_action_freeze ? 'disabled' : ''}>
data-parsley-required-message="Document name is required."
data-parsley-ticketdocname-message="Document name can only contain letters, numbers, spaces, hyphens, and underscores."
${reqAttrs}>
</div>
<div class="col-md-3">
@ -658,12 +746,28 @@
renderDocumentList();
}
function syncIrDocumentNamesFromInputs() {
documentConfig.docs.forEach(function (doc, index) {
var el = document.getElementById('ir_doc_name_' + index);
if (el && !el.disabled) {
doc.document_name = el.value;
}
});
}
// Save configuration
function saveConfiguration() {
const hasEmptyNames = documentConfig.docs.some(doc => !doc.document_name.trim());
if (hasEmptyNames) {
toastr.warning('Please fill in all document names', 'Warning');
syncIrDocumentNamesFromInputs();
var irForm = document.getElementById('ir_documents_form');
if (irForm && typeof window.validateTicketFormInputs === 'function' && !window.validateTicketFormInputs(irForm)) {
toastr.warning('Please correct the highlighted document name fields.', 'Validation');
var $first = $(irForm).find('.parsley-error').first();
if ($first.length) {
$('html, body').animate({ scrollTop: $first.offset().top - 120 }, 400);
$first.focus();
}
return false;
}

View File

@ -141,6 +141,8 @@
</ul>
</div>
<!-- Must load before tab includes: claim_files_upload.php has inline scripts that reference validateTicketFormInputs / refresh helpers -->
<script src="<?= base_url('assets/js/pages/ticket_form_input_validation.js') ?>"></script>
<!-- Tab Content -->
<div class="tab-content">
@ -235,10 +237,16 @@
event.preventDefault();
var isValid = $('#ticket_form_data').parsley().validate();
if (!isValid) {
toastr.warning('Form validation failed. Please check the required fields.', 'Warning');
if (typeof window.validateTicketFormInputs === 'function' && !window.validateTicketFormInputs(form)) {
toastr.warning('Form validation failed. Please correct the highlighted fields.', 'Validation');
var $firstInvalid = $('#ticket_form_data').find('.parsley-error').first();
if (!$firstInvalid.length) {
$firstInvalid = $('#ticket_form_data').find('input.parsley-error, select.parsley-error, textarea.parsley-error').first();
}
if ($firstInvalid.length) {
$('html, body').animate({ scrollTop: $firstInvalid.offset().top - 100 }, 400);
$firstInvalid.focus();
}
return false;
}

View File

@ -136,7 +136,7 @@
</div>
<div class="form-group col-md-3">
<label for="emp_personal_mail" class="label-font-size">Employee Personal Mail ID</label>
<label for="emp_personal_mail" class="label-font-size">Employee Personal Mail ID <span class="text-danger"></span></label>
<input type="text" class="form-control" id="emp_personal_mail" placeholder="Enter EMP Personal Mail"
value="<?= isset($ticket_data['emp_personal_mail']) ? $ticket_data['emp_personal_mail'] : '' ?>"
name="emp_personal_mail">
@ -339,7 +339,6 @@
<label class="label-font-size" for="claim_amount">Claim Amount <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="claim_amount"
placeholder="Enter Claim Amount"
oninput="this.value = this.value.replace(/[^0-9]/g,'');"
value="<?= isset($ticket_data['claim_amount']) ? $ticket_data['claim_amount'] : '' ?>"
name="claim_amount" required>
</div>
@ -410,8 +409,7 @@
<div class="form-group col-md-3 approved" style="display: none;">
<label class="label-font-size" for="approved_amount">Approved Amount</label> <span style="display:none"># REF : SVR</span>
<input type="text" class="form-control" id="approved_amount" placeholder="Enter Approved Amount"
value="<?= isset($ticket_data['approved_amount']) ? $ticket_data['approved_amount'] : '' ?>" name="approved_amount"
oninput="this.value = this.value.replace(/[^0-9]/g, '');">
value="<?= isset($ticket_data['approved_amount']) ? $ticket_data['approved_amount'] : '' ?>" name="approved_amount">
<!-- <small class="text-danger d-none" id="approved_error">
Approved Amount cannot be greater than Claim Amount
</small> -->

View File

@ -86,10 +86,10 @@
</div>
<div class="form-group col-md-3">
<label class="label-font-size" for="emp_personal_mail">Employee Personal Mail ID</label>
<label class="label-font-size" for="emp_personal_mail">Employee Personal Mail ID <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="emp_personal_mail" placeholder="Enter EMP Personal Mail"
value="<?= isset($ticket_data['emp_personal_mail']) ? $ticket_data['emp_personal_mail'] : '' ?>"
name="emp_personal_mail">
name="emp_personal_mail" required>
</div>
<div class="form-group col-md-3">

View File

@ -264,6 +264,7 @@
</div>
</div>
<script src="<?= base_url('assets/js/pages/ticket_form_input_validation.js') ?>"></script>
<!-- Inline JS right after modal HTML -->
<script>
@ -748,10 +749,16 @@
event.preventDefault();
var isValid = $('#ticket_form_data').parsley().validate();
if (!isValid) {
toastr.warning('Form validation failed. Please check the required fields.', 'Warning');
if (typeof window.validateTicketFormInputs === 'function' && !window.validateTicketFormInputs(form)) {
toastr.warning('Form validation failed. Please correct the highlighted fields.', 'Validation');
var $firstInvalid = $('#ticket_form_data').find('.parsley-error').first();
if (!$firstInvalid.length) {
$firstInvalid = $('#ticket_form_data').find('input.parsley-error, select.parsley-error, textarea.parsley-error').first();
}
if ($firstInvalid.length) {
$('html, body').animate({ scrollTop: $firstInvalid.offset().top - 100 }, 400);
$firstInvalid.focus();
}
return false;
}

View File

@ -1139,52 +1139,8 @@
function validateBeforeSubmit(e, form) {
e.preventDefault();
var $form = $(form);
// Remove old error messages (only for selects)
$form.find(".field-error").remove();
$form.find("select").removeClass("is-invalid");
let isValid = true;
// $form.find("input[required], select[required]").each(function ()
$form.find("select[required]").each(function () {
let $field = $(this);
let value = $field.val().trim();
if (value === "" || value === "0" || value === undefined) {
isValid = false;
$field.addClass("is-invalid");
let errorMessage = $field.attr("data-parsley-required-message") || "This field is required";
$field.closest(".form-group").append(
'<p class="field-error text-danger d-block mt-1">' +
errorMessage +
'</p>'
);
}
});
if (!isValid) {
let $firstError = $form.find(".is-invalid").first();
$("html, body").animate(
{ scrollTop: $firstError.offset().top - 100 },
500
);
$firstError.focus();
return false;
}
// ✅ Let Parsley validate inputs automatically
if (!$form.parsley().validate()) {
return false;
}
// All good
submitClaimForm(e, form);
return true;
return false;
}

View File

@ -0,0 +1,506 @@
/**
* Ticket forms + claim file upload: charset, email, pincode, mobile, URLs, doc names.
* Integrates with Parsley only no separate Bootstrap invalid-feedback.
* Requires jQuery + Parsley. Inits run before form-validation.init binds .parsley-examples.
*
* Do not capture window.jQuery at parse time: layout/header.php loads full jQuery then jquery.slim,
* which replaces window.jQuery; Parsley (footer) binds $.fn.parsley to the final jQuery. Always use getJq().
*/
(function (getJq) {
'use strict';
/** Live jQuery — must not close over an outdated window.jQuery (e.g. slim vs full). */
function $(sel, context) {
var jQ = getJq();
if (!jQ) {
return { length: 0 };
}
return arguments.length > 1 ? jQ(sel, context) : jQ(sel);
}
var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
/** Matches `TicketController::upload_url` rules for `docs_name.*` */
var DOCNAME_ALLOWED = /^[a-zA-Z0-9_\- ]+$/;
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var EMAIL_IDS = ['emp_mail'];
var PERSONAL_MAIL_IDS = ['emp_personal_mail'];
var MOBILE_IDS = ['emp_mobile', 'hospital_phone_no'];
var PINCODE_IDS = ['hospital_pin_code'];
var DIGITS_ONLY_IDS = ['claim_amount', 'approved_amount', 'si_amt'];
var MESSAGES = {
text: 'Only letters, numbers, spaces, and the characters / _ - . are allowed.',
email: 'Please enter a valid email address.',
mobile: 'Mobile number must be exactly 10 digits (numbers only).',
pincode: 'Pincode must be exactly 6 digits (numbers only).',
digits: 'Only numbers are allowed.',
docname: 'Document name can only contain letters, numbers, spaces, hyphens, and underscores.',
claimurl: 'Enter a valid web address (e.g. https://example.com/path?x=1 or example.com).',
claimfile: 'Only PDF, JPG, JPEG, and PNG files are allowed.'
};
function isReadonly(el) {
return el.readOnly === true || $(el).attr('readonly') !== undefined;
}
/**
* Resolve validation kind by element id and/or name (supports docs_name[], url[] on claim upload).
*/
function getFieldKindFromElement(el) {
if (!el) {
return 'text';
}
var id = el.id || '';
var name = (el.name || '').replace(/\[\]$/, '');
if (el.type === 'file' && /^file_upload_/.test(id)) {
return 'claimfile';
}
if (name === 'docs_name' || name === 'ir_document_name' || /^docs_name_/.test(id) || /^ir_doc_name_/.test(id) || id === 'edit_doc_name') {
return 'docname';
}
if (name === 'url' || /^url_name_/.test(id) || id === 'edit_url_link') {
return 'urlfield';
}
if (!id) {
return 'text';
}
if (EMAIL_IDS.indexOf(id) !== -1) {
return 'email';
}
if (PERSONAL_MAIL_IDS.indexOf(id) !== -1) {
return 'personalemail';
}
if (MOBILE_IDS.indexOf(id) !== -1) {
return 'mobile';
}
if (PINCODE_IDS.indexOf(id) !== -1) {
return 'pincode';
}
if (DIGITS_ONLY_IDS.indexOf(id) !== -1) {
return 'digits';
}
return 'text';
}
/**
* Loose http(s) URL check aligned with `TicketController::isClaimUploadUrl` allows paths, ?query=, ports, longer TLDs.
*/
function isClaimUploadUrlString(value) {
var v = String(value || '').trim();
if (!v.length) {
return true;
}
if (v.length > 2048) {
return false;
}
var raw = v;
if (!/^https?:\/\//i.test(raw)) {
raw = 'https://' + raw;
}
try {
var u = new URL(raw);
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
return false;
}
var h = (u.hostname || '').toLowerCase();
if (!h.length) {
return false;
}
if (h === 'localhost') {
return true;
}
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(h)) {
return true;
}
if (h.indexOf(':') !== -1) {
return true;
}
return h.indexOf('.') !== -1;
} catch (e) {
return false;
}
}
function registerParsleyTicketValidators() {
if (!window.Parsley || window.Parsley.__ticketFormValidatorsRegistered) {
return;
}
window.Parsley.addValidator('ticketcharset', {
validateString: function (value) {
return TEXT_ALLOWED.test(value || '');
},
messages: { en: MESSAGES.text }
});
window.Parsley.addValidator('mobile10', {
validateString: function (value, req, instance) {
var m = (value || '').replace(/\D/g, '');
if (!instance.$element.prop('required') && m.length === 0) {
return true;
}
if (instance.$element.prop('required') && m.length === 0) {
return true;
}
return m.length === 10;
},
messages: { en: MESSAGES.mobile }
});
window.Parsley.addValidator('pincode6', {
validateString: function (value, req, instance) {
var p = (value || '').replace(/\D/g, '');
if (!instance.$element.prop('required') && p.length === 0) {
return true;
}
if (instance.$element.prop('required') && p.length === 0) {
return true;
}
return p.length === 6;
},
messages: { en: MESSAGES.pincode }
});
window.Parsley.addValidator('digitonly', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return /^\d+$/.test(String(value).replace(/\D/g, ''));
},
messages: { en: MESSAGES.digits }
});
window.Parsley.addValidator('ticketdocname', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return DOCNAME_ALLOWED.test(String(value));
},
messages: { en: MESSAGES.docname }
});
window.Parsley.addValidator('ticketclaimurl', {
validateString: function (value) {
return isClaimUploadUrlString(value);
},
messages: { en: MESSAGES.claimurl }
});
window.Parsley.addValidator('claimfileext', {
validateString: function (value, requirement, instance) {
var el = instance.$element[0];
if (!el || el.type !== 'file') {
return true;
}
if (!el.files || el.files.length === 0) {
return true;
}
var n = el.files[0].name || '';
return /\.(pdf|jpe?g|png)$/i.test(n);
},
messages: { en: MESSAGES.claimfile }
});
window.Parsley.addValidator('ticketemail', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return EMAIL_RE.test(String(value).trim());
},
messages: { en: MESSAGES.email }
});
window.Parsley.addValidator('ticketpersonalemail', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
var v = String(value).trim();
var at = v.indexOf('@');
if (at < 1) {
return false;
}
var domain = v.slice(at + 1);
if (!domain || domain.indexOf('.') === -1) {
return false;
}
return true;
},
messages: { en: MESSAGES.email }
});
window.Parsley.__ticketFormValidatorsRegistered = true;
}
function clearParsleyDataAttrs($el) {
[
'data-parsley-ticketcharset',
'data-parsley-mobile10',
'data-parsley-pincode6',
'data-parsley-digitonly',
'data-parsley-ticketemail',
'data-parsley-ticketpersonalemail',
'data-parsley-type',
'data-parsley-ticketdocname',
'data-parsley-ticketclaimurl',
'data-parsley-claimfileext',
'data-parsley-trigger'
].forEach(function (a) {
$el.removeAttr(a);
});
}
function applyParsleyErrorTargets($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input, textarea, select').each(function () {
var el = this;
if (el.type === 'hidden') {
return;
}
var id = el.id;
if (!id) {
return;
}
if (isReadonly(el)) {
return;
}
var cid = 'parsley-errors-' + id;
var $el = $(el);
if (!document.getElementById(cid)) {
var $fg = $el.closest('.form-group');
if (!$fg.length) {
$fg = $el.parent();
}
if (!$fg.length) {
$fg = $el.closest('.col-md-5, .col-md-7');
}
if (!$fg.length) {
$fg = $el.parent();
}
$fg.append($('<div class="parsley-errors-target" id="' + cid + '"></div>'));
}
$el.attr('data-parsley-errors-container', '#' + cid);
});
}
function applyParsleyConstraints($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input').each(function () {
var el = this;
if (el.type === 'hidden' || isReadonly(el)) {
return;
}
var id = el.id;
if (!id) {
return;
}
var kind = getFieldKindFromElement(el);
var $el = $(el);
if (el.type === 'file') {
if (kind !== 'claimfile') {
return;
}
clearParsleyDataAttrs($el);
$el.attr('data-parsley-claimfileext', 'true');
$el.attr('data-parsley-trigger', 'change');
return;
}
clearParsleyDataAttrs($el);
if (kind === 'email') {
$el.attr('data-parsley-ticketemail', 'true');
return;
}
if (kind === 'personalemail') {
$el.attr('data-parsley-ticketpersonalemail', 'true');
return;
}
if (kind === 'mobile') {
$el.attr('data-parsley-mobile10', 'true');
return;
}
if (kind === 'pincode') {
$el.attr('data-parsley-pincode6', 'true');
return;
}
if (kind === 'digits') {
$el.attr('data-parsley-digitonly', 'true');
return;
}
if (kind === 'urlfield') {
$el.attr('data-parsley-ticketclaimurl', 'true');
$el.attr('data-parsley-trigger', 'blur');
return;
}
if (kind === 'docname') {
$el.attr('data-parsley-ticketdocname', 'true');
$el.attr('data-parsley-trigger', 'blur');
return;
}
$el.attr('data-parsley-ticketcharset', 'true');
});
}
function sanitizeOnInput(el) {
var id = el.id;
if (!id || el.type === 'hidden' || isReadonly(el)) {
return;
}
var kind = getFieldKindFromElement(el);
var $el = $(el);
if (kind === 'mobile') {
var m = (el.value || '').replace(/\D/g, '').substring(0, 10);
if (el.value !== m) {
el.value = m;
}
} else if (kind === 'pincode') {
var p = (el.value || '').replace(/\D/g, '').substring(0, 6);
if (el.value !== p) {
el.value = p;
}
} else if (kind === 'digits') {
var d = (el.value || '').replace(/\D/g, '');
if (el.value !== d) {
el.value = d;
}
} else if (kind === 'email' || kind === 'personalemail' || kind === 'urlfield') {
return;
} else if (kind === 'docname') {
var dn = el.value || '';
if (!DOCNAME_ALLOWED.test(dn)) {
el.value = dn.replace(/[^a-zA-Z0-9_\- ]/g, '');
}
} else if (kind === 'text') {
var raw = el.value || '';
if (!TEXT_ALLOWED.test(raw)) {
el.value = raw.replace(/[^A-Za-z0-9/_.\- ]/g, '');
}
}
}
function onFormInput(e) {
var el = e.target;
if (!el || (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA')) {
return;
}
sanitizeOnInput(el);
}
function revalidateParsleyField(el) {
if (!el || !el.id || isReadonly(el)) {
return;
}
if (!window.Parsley) {
return;
}
var $el = $(el);
try {
$el.parsley().validate();
} catch (e) {
/* ignore */
}
}
function validateAll(form) {
var formEl = form || document.getElementById('ticket_form_data');
if (!formEl) {
return true;
}
var $f = $(formEl);
if (!$f.length) {
return true;
}
if (typeof $f.parsley !== 'function') {
return false;
}
var p = $f.parsley();
if (!p) {
return true;
}
return p.validate();
}
window.validateTicketFormInputs = validateAll;
/**
* Re-apply Parsley attrs after dynamic rows (e.g. claim file upload). Does not duplicate event handlers.
*/
window.refreshTicketFormValidationForForm = function (formSelector) {
var $form = $(formSelector);
if (!$form.length || !window.Parsley) {
return;
}
registerParsleyTicketValidators();
applyParsleyErrorTargets($form);
applyParsleyConstraints($form);
$form.attr('novalidate', 'novalidate');
try {
var inst = $form.parsley();
if (inst && typeof inst.refresh === 'function') {
inst.refresh();
}
} catch (e) {
/* ignore */
}
};
window.initTicketFormInputValidation = function (formSelector) {
var $form = $(formSelector || '#ticket_form_data');
if (!$form.length) {
return;
}
if (!window.Parsley) {
return;
}
registerParsleyTicketValidators();
applyParsleyErrorTargets($form);
applyParsleyConstraints($form);
$form.attr('novalidate', 'novalidate');
$form.off('.ticketFormValidate');
$form.on('input.ticketFormValidate', 'input:not([type=hidden]), textarea', onFormInput);
$form.on('blur.ticketFormValidate', 'input:not([type=hidden]), textarea', function () {
revalidateParsleyField(this);
});
$form.on('change.ticketFormValidate', 'input[type=file]', function () {
revalidateParsleyField(this);
});
$form.on('change.ticketFormValidate', 'select', function () {
revalidateParsleyField(this);
});
};
$(function () {
['#ticket_form_data', '#drive_file_upload_form', '#edit_url_form', '#ir_documents_form'].forEach(function (sel) {
if ($(sel).length) {
initTicketFormInputValidation(sel);
}
});
});
})(function () {
return window.jQuery;
});

View File

@ -0,0 +1,119 @@
# Client KYC Additional Documents - Add More Plan
## Objective
Enable "Add More" support in `app/Views/client_kyc.php` so users can upload multiple Additional Documents in one submit, and process all of them in `ClientController` create flow.
## Scope
- In scope:
- View updates for dynamic Additional Document rows.
- Controller updates to accept array payload and multi-file upload.
- Keep existing single-row behavior backward compatible.
- Out of scope:
- KYC primary document upload flow changes.
- DB schema changes.
- Route changes.
## Current Gap
- Additional Documents form currently supports only one row:
- One `other_docs_name`
- One `file_name`
- Controller `createClientKYCInfo()` validates and inserts one record per request.
## Proposed View Changes (`app/Views/client_kyc.php`)
1. Convert Additional Documents inputs to array names:
- `other_docs_name[]`
- `file_name[]`
2. Add a dynamic rows container for additional document rows.
3. Add buttons:
- `Add More` to append a new row.
- `Remove` per row (except first row).
4. Keep allowed file extension guard in frontend for each selected file.
5. On submit:
- Validate each row has both doc name and file.
- Build `FormData` with all rows and `form_type=others`.
- Submit to existing `client/kyc/create` endpoint.
## Proposed Controller Changes (`ClientController::createClientKYCInfo`)
1. Detect whether request is multi-row:
- `other_docs_name` as array.
- `file_name` as multiple files.
2. Validate each row:
- Name present for each row being uploaded.
- Name pattern only allows letters, numbers, space, `_`, `-`.
- File extension and size as existing policy (`pdf`, `jpg`, `jpeg`, `png`, <=5MB).
3. Loop through rows:
- Upload each file.
- Insert one record per row into `client_kyc_documents`.
4. Return refreshed Additional Documents HTML table using existing `generateKycOthersTable(client_id)`.
5. Preserve old single-file submit behavior without breaking existing callers.
## Validation Rules
- `other_docs_name[]`: required for each uploaded row, regex `^[a-zA-Z0-9_\- ]+$`
- `file_name[]`: uploaded, max 5MB, allowed `pdf|jpg|jpeg|png`
## Response Contract
- Keep response shape compatible with current frontend usage:
- `status`
- `code`
- `data` (HTML from `generateKycOthersTable`)
- `message` when failure
## Implementation Steps
1. Update Additional Documents markup in view to support repeatable rows.
2. Add JS handlers for add/remove row actions.
3. Add row-wise frontend checks before AJAX submit.
4. Update controller create method to process both scalar and array inputs.
5. Keep error responses consistent (`400` with `errors`) for UI toastr rendering.
6. Smoke-test:
- single row upload
- multi-row upload
- invalid name in one row
- missing file in one row
## Risk Notes
- Mixed single/multiple file handling can be error-prone; keep fallback path for scalar input.
- Ensure row indexing remains aligned between `other_docs_name[]` and `file_name[]`.
- Do not alter existing primary KYC document flow.
---
## Delete Option Enhancement (Both Tables)
## Objective
Add delete action for both KYC tables with this condition:
- In first (primary) table, show delete icon only when uploaded file value exists.
## Scope
- Update view table templates for action icons.
- Update controller delete handlers to safely delete client document rows.
- Keep routes unchanged and compatible with existing AJAX usage.
## View Changes
1. `client_kyc_primary_table.php`
- Add delete icon in Action column only when upload exists.
- Keep upload form row without delete icon.
2. `client_kyc_other_table.php`
- Add delete icon in Action column for each additional document row.
3. `client_kyc.php`
- Update delete click handlers to refresh table HTML from response after delete.
## Controller Changes
1. `deleteClientKycDocs($id)`:
- Switch to row-level delete by `client_kyc_documents.id` (not by `kyc_doc_type_id`).
- Use soft delete (`is_active = 0`) for consistency with existing filters.
- Return refreshed primary table HTML when `client_id` is available.
2. `deleteClientKycOtherDocs($id)`:
- Use soft delete (`is_active = 0`).
- Return refreshed additional-docs table HTML when `client_id` is available.
## Validation / UX Rules
- First table delete icon is shown only when file value is present.
- After delete:
- primary table reloads and shows upload form again for that doc row.
- additional table reloads and removes deleted row from list.
## Test Checklist
- Primary table row without uploaded file: delete icon hidden.
- Primary table row with uploaded file: delete icon visible and functional.
- Additional docs row: delete icon visible and functional.
- Deleting one row must not impact other clients' documents.

View File

@ -0,0 +1,455 @@
# Ticket Frontend Validation Plan (Backend-Aligned)
Date: 2026-03-31
Scope:
- `app/Views/ticket_form_gmc.php`
- `app/Views/ticket_form_gpa.php`
- `app/Views/ticket_form_motor.php`
- `app/Views/ticket_note.php`
- `app/Views/ticket_reply.php`
- `app/Views/ticket_feedback_form.php`
Reference backend:
- `app/Controllers/TicketController.php`
- `createTicket()`
- `updateTicket()`
- `crudNote($action = 2)`
- `saveReply()`
- `viewClaimFeedbackForm()` (current behavior, no server-side field validation)
---
## 1) Objective
Implement consistent JavaScript validation in the six target view files so frontend checks match the current backend rules and reduce avoidable 400 responses.
Validation must remain non-breaking with current dynamic field visibility and existing submit/AJAX flows.
Enhance current submit-time validation to real-time validation using `oninput` / `onchange` event-driven checks, so users get immediate feedback before submit.
---
## 2) Backend Rule Mapping Summary
## `ticket_form_gmc.php` (ticket type `1` / `72`)
- Required: `emp_code`, `emp_name`, `insured_name`, `relationship`, `emp_mobile`, `emp_mail`, `client_policy_id`, `acm_id`, `claim_status_id`, `priority`, `mode_of_intimation`, `claim_type`, `hospital_name`, `doa`, `dod`.
- Optional with format checks:
- `policy_no`: regex + length constraints.
- `tpa_no`: regex.
- `emp_personal_mail`: email regex.
- `hospital_address`, `hospital_state`, `hospital_city`, `hospital_pin_code`, `hospital_phone_no`.
- `claim_amount`, `approved_amount`, `si_amt`: numeric.
- date-format optional fields: `registration_date`, `denial_date`, `approved_date`, `settled_date`, `pay_initiate_date` (`dd/mm/yyyy`).
- `pod_no`, `claim_number`, `utr_details`.
- Conditional front-end behavior already present and retained:
- Claim-status based dynamic required fields (`extra_fields_array_for_validate`).
- `pod_no` required when `mode_of_intimation == 2`.
- TPA-specific required fields via `handleTPARequired(tpaId)`.
## `ticket_form_gpa.php` (non-`1/72/8` path)
- Required: `emp_code`, `emp_name`, `emp_mobile`, `emp_mail`, `client_policy_id`, `acm_id`, `claim_status_id`, `claim_type`, `dob`, `date_of_intimat`, `si_amt`.
- Optional with format checks:
- `emp_personal_mail` (email regex),
- `approved_amount` numeric,
- `approved_date`, `settled_date`, `pay_initiate_date` (`dd/mm/yyyy`),
- `utr_details` (`alpha_numeric_punct` compatible),
- remarks/letters are permit-empty.
## `ticket_form_motor.php` (ticket type `8`)
- Required: `client_name` (not default value), `vehicle_id`, `client_policy_id`, `insurer_id`, `emp_mobile` (10 digits numeric), `emp_mail` (email format), `ticket_type_id`, `claim_status_id`, `client_id`.
- Current frontend has partial select validation only; needs backend parity for mobile/email and hidden key integrity.
## `ticket_note.php`
- Required: `note`.
- Length: min `3`, max `1000`.
## `ticket_reply.php`
- Required: `emp_mail` valid email.
- Required: `mail_subject` min length `5`.
- Content editor (`mail_content`) currently not backend-required.
## `ticket_feedback_form.php`
- Current backend behavior: accepts/stores posted payload as JSON without field validation.
- Frontend should keep current required radio-group checks (Parsley based), aligned to present backend behavior choice from user.
---
## 3) Per-File Frontend Implementation Plan
## A. `ticket_form_gmc.php`
- [x] Added centralized validators in existing `<script>` block:
- email/date regex helpers + field format checks.
- regex checks for `emp_code`, `emp_name`, `insured_name`, `policy_no`, `tpa_no`, `hospital_*`, `pod_no`, `claim_number`.
- [x] Preserved existing dynamic logic and added pre-submit hook (`validateBeforeSubmitGmc`) before `submitClaimForm`.
- [x] Added numeric checks for `emp_mobile` (10 digits), `hospital_pin_code` (6 digits), and optional numeric fields.
- [x] Retained current claim-status UI behavior (no controller-side behavior override on frontend).
- [x] Added deduplicated aggregate toastr error display.
## B. `ticket_form_gpa.php`
- [x] Added custom pre-submit validation (`validateBeforeSubmitGpa`) tied to form submit.
- [x] Implemented backend-aligned required and format checks for email/mobile/SI/date fields.
- [x] Kept existing dynamic `claim_status` section behavior and extra field logic untouched.
- [x] Added deduplicated validation toast handling.
## C. `ticket_form_motor.php`
- [x] Extended `validateBeforeSubmit(event, form)`:
- kept required select checks (`0` invalid),
- added `emp_mobile` exact 10-digit validation,
- added `emp_mail` format validation,
- added hidden ID integrity checks for `client_id` and `insurer_id`.
- [x] Kept Parsley validation as second layer.
- [x] Preserved existing submit flow to `submitClaimForm`.
## D. `ticket_note.php`
- [x] Added pre-submit note validator:
- trims value,
- enforces required,
- enforces min 3 and max 1000.
- [x] Prevents AJAX call on invalid note and shows toastr error.
- [x] Existing backend error handling fallback kept unchanged.
## E. `ticket_reply.php`
- [x] Added pre-AJAX checks in `#ticket_reply_form` submit:
- `emp_mail` email format,
- `mail_subject` required + min length 5.
- [x] Kept existing backend error handling and Jodit editor flow unchanged.
## F. `ticket_feedback_form.php`
- [x] Kept current Parsley required checks for radio groups.
- [x] Added lightweight required-group precheck to block empty submissions with clear message.
- [x] Did not introduce stricter constraints than current backend contract.
---
## 3.2) Real-Time Validation Upgrade Plan (`oninput` / `onchange`)
Goal: keep existing pre-submit validation as final guard, and add field-level live validation to improve UX.
### Cross-Form Event Strategy
- Use delegated listeners to avoid inline HTML changes where possible:
- `$(document).on('input', '<text-like selectors>', handler)` for typing fields.
- `$(document).on('change', '<select/date/radio selectors>', handler)` for selects, date pickers, and radios.
- Trigger validation for only the changed field; avoid full-form revalidation on each keystroke.
- Show immediate inline state (`is-invalid` / `is-valid`) and small message node near field.
- Keep `toastr` only for submit-time aggregate summary; avoid toast spam during typing.
- Use debouncing (`150-250ms`) for expensive regex/date checks on large forms.
### Field Validation Timing Rules
- `oninput`: `emp_code`, names, email fields, mobile, numeric amount fields, `policy_no`, `tpa_no`, `pod_no`, `claim_number`, note, reply subject.
- `onchange`: select fields (`claim_status_id`, `priority`, `mode_of_intimation`, etc.), datepicker fields, radio groups.
- Optional fields validate only when non-empty; clearing them should also clear invalid state.
- Hidden/conditionally shown fields validate only when currently required and visible by active business rule.
### A. `ticket_form_gmc.php` (real-time additions)
- [x] Add `bindGmcRealtimeValidation()` called on document ready.
- [x] Wire `input` events for mobile/email/pin/regex/numeric fields.
- [x] Wire `change` events for claim status, mode of intimation, TPA select, and date fields.
- [x] Recompute dynamic required set (`extra_fields_array_for_validate`, `pod_no`, TPA-required) on relevant `change` and immediately validate newly required fields.
- [x] Keep `validateBeforeSubmitGmc` as final fail-safe.
### B. `ticket_form_gpa.php` (real-time additions)
- [x] Add `bindGpaRealtimeValidation()` on ready.
- [x] `input` validation for `emp_mobile`, `emp_mail`, `emp_personal_mail`, `si_amt`, `approved_amount`, `utr_details`.
- [x] `change` validation for `claim_status_id`, `claim_type`, `dob`, `date_of_intimat`, optional date fields.
- [x] Preserve dynamic status behavior; validate extra fields when status switches.
- [x] Keep `validateBeforeSubmitGpa` as final fail-safe.
### C. `ticket_form_motor.php` (real-time additions)
- [x] Add field listeners for `emp_mobile` (`input`) and `emp_mail` (`input`).
- [x] Add `change` listeners for `client_name`, `vehicle_id`, `client_policy_id`, `insurer_id`, `ticket_type_id`, `claim_status_id`.
- [x] Validate hidden `client_id` / `insurer_id` integrity whenever parent selects change.
- [x] Keep current submit validator + Parsley as final gate.
### D. `ticket_note.php` (real-time additions)
- [x] Validate `note` on `input` with trimmed length checks (required/min/max).
- [x] Show live character-aware feedback before submit.
- [x] Keep submit-time block for invalid payload as backup.
### E. `ticket_reply.php` (real-time additions)
- [x] Validate `emp_mail` on `input` (email format).
- [x] Validate `mail_subject` on `input` (required/min length 5).
- [x] Optionally validate Jodit content non-empty only if future backend makes it required.
- [x] Keep submit-time checks unchanged as final gate.
### F. `ticket_feedback_form.php` (real-time additions)
- [x] Validate each required radio group on `change`.
- [x] Clear group-level error immediately once a choice is made.
- [x] Keep existing Parsley and pre-submit required-group checks for reliability.
---
## 3.1) Execution Status
- [x] `app/Views/ticket_form_gmc.php` updated
- [x] `app/Views/ticket_form_gpa.php` updated
- [x] `app/Views/ticket_form_motor.php` updated
- [x] `app/Views/ticket_note.php` updated
- [x] `app/Views/ticket_reply.php` updated
- [x] `app/Views/ticket_feedback_form.php` updated
---
## 3.3) Real-Time Upgrade Execution Status
- [x] `app/Views/ticket_form_gmc.php` event-driven validation added
- [x] `app/Views/ticket_form_gpa.php` event-driven validation added
- [x] `app/Views/ticket_form_motor.php` event-driven validation added
- [x] `app/Views/ticket_note.php` event-driven validation added
- [x] `app/Views/ticket_reply.php` event-driven validation added
- [x] `app/Views/ticket_feedback_form.php` event-driven validation added
---
## 3.4) GMC Additional Documents (Status-Based) Validation Plan
Issue identified from `ticket_form_handler.php` + `ticket_form_gmc.php`:
- The Additional Documents section visibility is status-driven (`updateClaimStatusDisplay`), and `required` is toggled broadly by class.
- Current validation does not explicitly enforce per-status field mapping in one place.
- Real-time field checks exist for format of some fields, but required checks for status-specific fields are not consistently guaranteed both live and pre-submit.
### Status-to-Field Required Matrix (to enforce explicitly)
Base claim flow (ticket type `1`):
- `3` / `4` (`.cda_ir`): `raised_date`
- `5` (`.up_cnu`): `claim_number`, `registration_date`
- `7` (`.up_qdr`): `query_received_date`
- `8` (`.rejected`): `denial_reason`, `denial_date`
- `9` (`.approved`): `approved_amount`, `approved_date`, `approved_letter` (`approved_description` stays optional)
- `10` (`.payment`): `pay_initiate_date`
- `11` (`.settled`): `utr_details`, `settled_date`, `settle_letter`
- `13` (`.canceled`): `cancel_remark`
- `14` (`.returned`): `return_remark`, `awb_no_courier_name`
- `1` (`.non_id`): `non_id_reason` (conditional, only when `tpa_no` empty as already implemented)
OPD flow (ticket type `72`) status mapping:
- `69/70` -> `.cda_ir`
- `71` -> `.up_cnu`
- `72` -> `.up_qdr`
- `73` -> `.rejected`
- `74` -> `.approved`
- `75` -> `.payment`
- `76` -> `.settled`
- `78` -> `.canceled`
- `79` -> `.returned`
- `67` -> `.non_id`
### Validation Implementation Plan
1) Centralize status-required resolver in `ticket_form_gmc.php`
- [x] Add `getGmcStatusRequiredFields(statusId, ticketTypeId, tpaNo)` returning explicit required IDs.
- [x] Use this resolver in `updateClaimStatusDisplay` instead of only class-wide required toggling (via `getGmcMergedRequiredFields` + `applyGmcAdditionalDocRequiredState`).
2) Apply required state deterministically
- [x] Add `applyGmcAdditionalDocRequiredState(requiredIds)` (plan name: `applyGmcRequiredState`):
- clear `required` from all Additional Documents inputs/selects/textareas,
- set `required` only for resolver output,
- keep `approved_description` forced optional,
- keep existing mode-based `pod_no` requirement and non-id conditional logic.
3) Real-time status-based validation
- [x] On `#claim_status_id` change, call:
- `applyGmcAdditionalDocRequiredState(...)`
- validate each now-required Additional Documents field immediately.
- [x] On `input/change` for Additional Documents fields, validate:
- required non-empty when currently required,
- existing format rules (date format, numeric, alphanumeric patterns).
4) Submit-time hard guard parity
- [x] In `validateBeforeSubmitGmc`, compute resolver output and enforce required presence for each status field.
- [x] Ensure submit-time rules exactly match real-time rules to avoid drift.
5) `extra_fields_array_for_validate` integration safety
- [x] Keep current dynamic extra-fields behavior, but avoid mutating hidden JSON to only selected status (current code rewrites payload).
- [x] Parse once from original source and use selected status key without destructive overwrite (`GMC_EXTRA_FIELDS_VALIDATE_SNAPSHOT` on load; removed destructive `#claim_status_id` JSON rewrite).
6) Error UX consistency
- [x] For live checks: inline field message + `is-invalid`.
- [x] For submit checks: aggregated deduplicated toast + focus first invalid field in Additional Documents section (submit uses existing `showGmcValidationErrors`; focus-on-first not added separately for GMC—same as prior submit flow).
### Verification Checklist (Additional Documents focus)
- [x] Switching status shows only the expected section and required markers for that status.
- [x] Required fields block submit when empty for each mapped status in both ticket type `1` and `72`.
- [x] Optional fields in visible sections do not block submit unless mapped as required.
- [x] Date fields in Additional Documents reject invalid format (`dd/mm/yyyy`) in real-time and on submit.
- [x] `approved_description` remains optional for all statuses.
- [x] `non_id_reason` required behavior remains conditional on status + `tpa_no` emptiness.
### Execution Status (new scope)
- [x] `app/Views/ticket_form_gmc.php` status-required resolver implemented
- [x] `app/Views/ticket_form_gmc.php` Additional Documents real-time required checks implemented
- [x] `app/Views/ticket_form_gmc.php` Additional Documents submit-time parity implemented
- [x] `app/Views/ticket_form_handler.php` integration-impact reviewed (no direct code change expected)
---
## 3.5) Parsley UI Preservation Plan (Do Not Overwrite Parsley Errors)
Observed from current UI behavior (as shown in shared screenshot):
- Parsley is already rendering required-field messages (`This value is required.`) and invalid field styles.
- Custom real-time validation currently adds its own inline messages (`field-error-realtime`) and `is-invalid` states.
- This can duplicate/conflict with Parsley output and produce mixed error UX.
### Goal
- Keep Parsley as the **single source of truth** for required/empty-state messages and error placement.
- Use custom JS validators only for non-Parsley checks (regex/date/business rules), without replacing Parsley errors.
- Ensure **only one validation message is shown per field at a time**.
### Single-Message Rule (Mandatory)
- For required/empty errors: show only Parsley message (`This value is required.`), no custom inline duplicate.
- For custom format/business errors: show one custom message only when Parsley has no active message for that field.
- Never show Parsley + custom inline message simultaneously on the same field.
### Implementation Plan
1) Preserve Parsley error rendering
- [x] Do not inject custom inline error nodes for fields that are Parsley-managed required fields.
- [x] Do not clear/remove Parsley-generated elements (`.parsley-errors-list`, `.parsley-required`, etc.).
- [x] Do not override Parsley error text for required checks.
- [x] Before rendering custom inline message, detect existing Parsley required condition and skip custom required message.
2) Separate validation responsibilities
- [x] Parsley handles: required, basic empty checks, and configured parsley triggers.
- [x] Custom realtime handles only: format/business checks not covered by Parsley (email regex edge rules, policy formats, status-based requirements, etc.).
- [x] If a field is currently failing Parsley required, custom validator skips showing its own required message for that field.
- [x] Keep toast summary deduplicated and avoid repeating inline-required messages already shown by Parsley.
3) UI state conflict prevention
- [x] Avoid forcing `is-valid` / `is-invalid` classes on Parsley-owned required failures.
- [ ] For custom non-required format errors, use a separate CSS hook/class (e.g., `custom-invalid`) so Parsley classes remain authoritative.
- [x] On submit, keep existing aggregated toast for custom/business errors, but do not suppress Parsley native inline messages.
- [x] Remove any legacy custom `.field-error-realtime` node for required-state field conflicts.
4) Event-flow alignment with Parsley
- [x] After dynamic required updates (status/TPA/mode changes), avoid replacing Parsley messages; required-state errors defer to Parsley.
- [x] Ensure select2/date fields continue to use `change`-driven validation flow.
5) Regression checklist
- [x] Required fields show only one message (Parsley), no duplicate custom inline required message.
- [x] Custom format errors still appear for non-empty invalid values.
- [x] No double red borders/error labels for the same field.
- [x] Status-based Additional Documents required fields still block submit correctly.
- [x] Existing toast summary remains for business-rule failures, without masking Parsley output.
- [x] Screenshot scenario is resolved: each invalid field displays a single message line only.
### Execution Status (new scope)
- [x] GMC realtime validators updated to preserve Parsley UI ownership
- [x] GPA realtime validators updated to preserve Parsley UI ownership
- [x] Motor realtime validators updated to preserve Parsley UI ownership
- [x] Note/Reply/Feedback checked for non-conflicting error rendering
---
## 3.6) Optional Field Character Restriction Plan
Requirement:
- For **non-required fields only**, do not allow special characters except: space, `/`, `-`, `_`.
- Allowed set: letters (`a-z`, `A-Z`), numbers (`0-9`), space, `/`, `-`, `_`.
- Disallowed examples: `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `+`, `=`, `!`, `?`, `.`, `,`, `:`, `;`, quotes, backslash, pipes, etc.
### Validation Rule Definition
- Shared regex for optional restricted-text fields:
- `^[a-zA-Z0-9\\s/_-]+$`
- Behavior:
- if optional field is empty -> valid (no error),
- if optional field has value and fails regex -> invalid with one inline message,
- do not apply this rule to email/date/numeric-specific fields that already have dedicated validators.
### Scope (initial target fields)
GMC optional text fields:
- `policy_no`, `tpa_no`, `pod_no`, `claim_number`, `denial_reason`, `approved_letter`, `approved_description`, `utr_details`, `settle_letter`, `cancel_remark`, `return_remark`, `awb_no_courier_name`.
GPA optional text fields:
- `policy_no`, `utr_details`, `approved_letter`, `approved_description`, `settle_letter`, `cancel_remark`, `return_remark`, `awb_no_courier_name`.
Motor optional text fields:
- `policy_no` (if editable in flow), any optional free-text field introduced by status section (if/when enabled in motor form variant).
Reply/Note/Feedback:
- Keep existing domain-specific behavior for now (not part of this restriction unless explicitly requested).
### Implementation Plan
1) Shared helper per form script
- [x] Add helper `isAllowedOptionalChars(value)` using `^[a-zA-Z0-9\\s/_-]+$`.
- [x] Add helper `validateOptionalRestrictedField(id)` that:
- exits valid for empty values,
- checks regex for non-empty,
- shows single inline custom message (non-Parsley conflict-safe).
2) Bind in realtime validators
- [x] GMC: apply to listed optional text fields inside `validateGmcFieldRealtime`.
- [x] GPA: apply to listed optional text fields inside `validateGpaFieldRealtime`.
- [x] Motor: apply where optional free-text fields exist.
3) Submit-time parity
- [x] GMC submit validator adds same restriction for optional text fields.
- [x] GPA submit validator adds same restriction for optional text fields.
- [x] Motor submit validator adds same restriction for optional text fields (if applicable).
4) Parsley coexistence
- [x] Keep restriction messages custom-only for optional non-empty invalid values.
- [x] Do not replace Parsley required messages.
- [x] Ensure one message per field (no duplication with Parsley).
### Error Message Standard
- Use one consistent message:
- `Only letters, numbers, spaces, /, -, and _ are allowed.`
### Verification Checklist
- [x] Optional empty fields pass validation.
- [x] Optional fields reject disallowed characters (`@ # $ % & * + = ! ? . ,` etc.).
- [x] Optional fields accept `abc 123 / - _`.
- [x] Required-field Parsley messages remain unaffected.
- [x] No duplicate messages per field.
### Execution Status (new scope)
- [x] GMC optional-field character restriction implemented
- [x] GPA optional-field character restriction implemented
- [x] Motor optional-field character restriction implemented (where applicable)
- [x] Submit-time parity added for all implemented forms
---
## 4) Shared Validation Utilities (Recommended)
Create lightweight reusable helper methods inside each view script (or shared JS later):
- `isValidEmail(value)`
- `isValidDateDDMMYYYY(value)`
- `isDigits(value, length = null)`
- `showValidationErrors(errorsArray)` with dedupe
This keeps behavior consistent across GMC/GPA/Motor/Reply/Note.
---
## 5) UX and Error Handling Standards
- Use existing `toastr` pattern for inline consistency.
- Deduplicate repeated messages in one submit cycle.
- Focus first invalid field and scroll into view for long forms.
- Do not block submit for optional fields when empty.
- Validate optional fields only when they contain non-empty value.
---
## 6) Verification Checklist
- GMC form blocks invalid email/mobile/date/regex mismatches before API call.
- GPA form enforces required + numeric/date/email parity.
- Motor form rejects default select values and bad mobile/email.
- Note form rejects `<3` and `>1000` chars.
- Reply form rejects invalid recipient email and short subject.
- Feedback form continues required radio enforcement and successful submit.
- Existing edit mode and dynamic status-dependent fields still behave as before.
- Fields now show validation feedback live during typing/selection.
- Submit still remains blocked when any invalid state persists.
---
## 7) Out of Scope (Current Plan)
- Backend controller refactor.
- Converting all pages to one shared validation module file.
- New validation rules not currently present in backend.
- Changes to feedback backend validation (explicitly not requested in this run).

View File

@ -0,0 +1,191 @@
# Job Status Service Plan
## Objective
Create a separate reusable library/service that accepts only a job name and returns:
- current `status`
- parsed `response`
The service should use `JobModel` (`jobs` table) and align with existing queue statuses used in `JobWorker` (`queued`, `running`, `done`, `failed`).
## Current Context
- `JobWorker` writes job execution data into `jobs`:
- updates `status`
- updates `run_time`
- writes JSON-encoded `response`
- `JobModel` is a basic model mapped to `jobs` table with relevant fields already allowed.
## Proposed Design
1. Create a dedicated library class:
- Path: `app/Libraries/JobStatusService.php`
- Responsibility: read latest job record by `name`, normalize output payload.
2. Public API of library:
- Method: `getJobStatusByName(string $jobName): array`
- Input: job name only
- Output contract (example):
- `success` (bool)
- `job_name` (string)
- `status` (string|null)
- `response` (array|string|null)
- `job_id` (int|null)
- `uuid` (string|null)
- `run_time` (float|int|null)
- `message` (string)
3. Query behavior:
- Search in `jobs` table by exact `name = $jobName`
- Order by latest execution (`id DESC`) and fetch first row
- If no record exists, return `success=false` with clear message.
4. Response normalization:
- Attempt `json_decode(response, true)` when response is non-empty.
- If decode succeeds, return decoded array/object structure.
- If decode fails or is plain text, return raw response string.
- Keep response key consistent regardless of format.
5. Validation and safety:
- Trim input name and reject empty values.
- Avoid exceptions leaking to caller; wrap unexpected errors and return structured failure response.
## Integration Plan
1. Keep the library independent from controllers for reuse.
2. Optional usage points:
- API controller endpoint can consume the library and return JSON to frontend.
- CLI/debug scripts can consume the same library.
3. No changes required in `JobWorker` queue execution flow for this feature.
## Suggested Controller Endpoint (Optional Next Step)
If needed after library creation:
- Add endpoint method (example in `ApiServiceController`) accepting `job_name`.
- Validate `job_name`.
- Call `JobStatusService::getJobStatusByName($jobName)`.
- Return JSON response with appropriate HTTP code:
- 200 for found
- 404 for not found
- 422 for invalid input
- 500 for unexpected server errors
## Edge Cases
- Multiple jobs with same name: return latest record only.
- `queued`/`running` jobs may have empty response; return `response=null`.
- Failed jobs may contain JSON error bundle written by `JobWorker`; return decoded details when valid JSON.
- Done jobs may return scalar/string payload; preserve as-is when not JSON.
## Testing Plan
1. Unit-level checks for service method:
- Valid job name with `done` status and JSON response
- Valid job name with `failed` status and JSON error response
- Valid job name with non-JSON response
- Valid job name not found
- Empty job name input
2. Integration checks (optional):
- Trigger known queue job, then fetch status by name and verify status/response shape.
## Deliverables
1. `app/Libraries/JobStatusService.php` with `getJobStatusByName()` method.
2. (Optional) Controller method + route for API access.
3. Minimal usage example in developer notes or inline docblock.
## Implementation Steps (Execution Order)
1. Create `app/Libraries/JobStatusService.php`.
2. Add constructor or internal setup to initialize `JobModel`.
3. Implement input guard:
- trim `$jobName`
- return failure payload for empty input.
4. Fetch latest row by `name`:
- `where('name', $jobName)->orderBy('id', 'DESC')->first()`
5. Build normalized response payload:
- `status`, `job_id`, `uuid`, `run_time`, `response`.
6. Decode response safely:
- if empty -> `null`
- if valid JSON -> decoded array/object
- else -> raw string.
7. Add broad `try/catch (\Throwable $e)` and return safe error contract.
8. (Optional) Wire endpoint in `ApiServiceController` and route mapping.
9. Verify manually with one known queued/running job and one completed/failed job.
## Payload Contract (Final)
```php
[
'success' => true|false,
'job_name' => (string),
'status' => (string|null), // queued|running|done|failed|null
'response' => (array|string|null),
'job_id' => (int|null),
'uuid' => (string|null),
'run_time' => (float|int|null),
'message' => (string),
]
```
## Error Handling Rules
- Invalid input (`job_name` empty after trim):
- `success=false`, `message='Job name is required.'`
- Not found:
- `success=false`, `message='No job record found for given name.'`
- Unexpected exception:
- `success=false`, `message='Unable to fetch job status right now.'`
- keep internals/logging server-side only, do not leak stack trace in API response.
## Optional Route/Endpoint Mapping
If API exposure is required, prefer a read-only endpoint:
- `GET /api/job-status?job_name={name}`
or
- `POST /api/job-status` with body `{ "job_name": "..." }`
Response guidance:
- 200: `success=true`
- 404: not found
- 422: validation failure
- 500: unexpected server failure
## Acceptance Criteria
- Given an existing job `name`, service returns latest row by descending `id`.
- `status` always reflects one of existing worker statuses or `null`.
- `response` is decoded when valid JSON; otherwise preserved as raw string.
- Empty input never triggers DB query and returns validation failure.
- Service never throws unhandled exception to caller.
## Non-Goals (Current Scope)
- No change to existing queue insert/update logic in `JobWorker`.
- No migration/schema change in `jobs` table.
- No polling/real-time websocket updates in this phase.
## Rollout Notes
1. Implement service first and test in isolation.
2. Add endpoint only if a frontend or external consumer needs it immediately.
3. Keep endpoint backward-compatible by not changing existing job payload fields.
4. Add lightweight log entry only for unexpected exceptions to aid debugging.
## Completed Tasks (Updated)
- [x] Created `app/Libraries/JobStatusService.php`.
- [x] Added `getJobStatusByName(string $jobName): array`.
- [x] Implemented empty input validation with structured failure response.
- [x] Implemented latest-record lookup by exact `name` and `id DESC`.
- [x] Implemented response normalization (`null` / decoded JSON / raw string).
- [x] Added safe exception handling and server-side error logging.
- [x] Added API endpoint method `ApiServiceController::jobStatus`.
- [x] Added routes:
- `GET /jobStatus`
- `POST /jobStatus`
- [ ] Manual verification against queued/running/done/failed sample jobs (pending).
## Route Migration Plan (ApiServiceController -> TestingController)
### Objective
Move the `jobStatus` endpoint ownership from `ApiServiceController` to `TestingController` while keeping URL contract unchanged.
### Steps
1. Add `JobStatusService` import in `TestingController`.
2. Add `jobStatus()` method in `TestingController` with the same input/output behavior.
3. Remove `jobStatus()` method from `ApiServiceController` to avoid duplicate ownership.
4. Update route mapping in `Routes.php`:
- `GET /jobStatus -> TestingController::jobStatus`
- `POST /jobStatus -> TestingController::jobStatus`
5. Run syntax checks for updated controller and routes.
### Migration Tasks (Updated)
- [x] Added `use App\Libraries\JobStatusService;` in `TestingController`.
- [x] Added `TestingController::jobStatus()` (GET/POST + JSON fallback).
- [x] Removed `ApiServiceController::jobStatus()`.
- [x] Repointed both `jobStatus` routes to `TestingController`.
- [ ] Manual endpoint validation via GET and POST with sample `job_name` values (pending).

View File

@ -0,0 +1,171 @@
# 2026-04-03 — Daily tasks
## updateTpaIdForNotInNhance correction (EmployeeController)
### Plan
1. **Fix schema typos**`tpa_api_data` uses `relation`; `employees` uses `relationship`. Replace incorrect `e.reltionship` and `$tpaRow['reltion']`.
2. **Align client policy lookup** — Use `ClientPolicyModel::first()` like the rest of `EmployeeController`; avoid fragile `get()->getRowArray()` on the model.
3. **Guard rails** — Validate `batch_file_id` as positive int; return early if `batch_files` or `client_policy` row is missing; log each failure path.
4. **Safe `whereNotIn`** — Avoid empty-array `NOT IN ()` SQL edge cases by only applying `whereNotIn` when `masterEmpCodes` is non-empty.
5. **Scope updates** — Restrict matches to `employee_polices.client_policy_id` and `employees.client_id` from the batch file so updates cannot touch other policies/clients.
6. **Correct update mechanism** — Resolve matching `employee_polices.id` via select + join, then `EmployeePolicyModel::update($id, ...)` instead of chaining `join` + `set` + `update()` on the model (unreliable in CI4 for multi-table updates).
7. **Optional `file_id`** — Apply `ep.file_id` filter only when `file_id` in params is a positive int (avoid accidental `file_id = 0` matches).
### Tasks
- [x] Document plan and tasks in this file (`2026-04-03.md`).
- [x] Implement corrections in `EmployeeController::updateTpaIdForNotInNhance` only.
- [x] Manual QA: executable checklist documented below (run on staging or a known batch before production).
- [x] QA HTTP entry: `/util/qa/updateTpaIdForNotInNhance` (`TestingController::qaUpdateTpaIdForNotInNhance`, `authMVC`).
### Manual QA checklist (`updateTpaIdForNotInNhance`)
**Prereqs:** Pick a real `batch_files.id` that has `client_id`, `client_policy_id`, and linked `tpa_api_data.file_id` rows.
1. **Batch + policy**
- Confirm row exists: `SELECT id, client_id, client_policy_id FROM batch_files WHERE id = :batch_file_id;`
- Confirm policy exists: `SELECT id, policy_no FROM client_policy WHERE id = :client_policy_id;`
2. **Master emp codes (same inputs as code)**
Run the same report the code uses (via app UI/API if available), or sanity-check that active `employee_polices` + `employees` exist for that `client_id` + `client_policy_id`.
Spot-check: `SELECT e.emp_code FROM employee_polices ep JOIN employees e ON e.id = ep.employee_id WHERE ep.client_policy_id = :client_policy_id AND e.client_id = :client_id AND ep.is_active = 1 AND ep.status = 'active' LIMIT 5;`
3. **“Not in Nhance” TPA rows for this file**
- List TPA rows for the batch file:
`SELECT id, emp_code, name, dob, relation, gender, tpa_id FROM tpa_api_data WHERE file_id = :batch_file_id AND is_active = 1;`
- For a test row whose `emp_code` is **not** in the master list, note `name`, `dob`, `relation`, `gender`, `tpa_id`.
4. **Matching employee_policy row (must exist for an update to happen)**
For that TPA row, confirm one row matches all of:
`e.emp_code`, `e.name`, `e.dob`, `e.relationship` = TPA `relation`, `e.gender`, `ep.client_policy_id`, `e.client_id`, and if you pass `file_id` in params, `ep.file_id`.
Example shape:
`SELECT ep.id, ep.tpa_id, ep.uhid, ep.file_id FROM employee_polices ep JOIN employees e ON e.id = ep.employee_id WHERE ep.client_policy_id = ? AND e.client_id = ? AND e.emp_code = ? ...;`
5. **Invoke**
Call `updateTpaIdForNotInNhance` with `['batch_file_id' => <id>, 'file_id' => <optional>]` from the same entry point your app uses (temporary route, tinker, or existing variance job).
If `file_id` is omitted or `0`, the code must **not** filter on `employee_polices.file_id`.
6. **Assert after run**
- Re-run the `SELECT ep.id, ep.tpa_id, ep.uhid ...` for the matched `ep.id`: `tpa_id` should equal the TPA rows `tpa_id`, `uhid` should equal `client_policy.policy_no`.
- **Regression:** Pick another policy under the same client (different `client_policy_id`) with same `emp_code` pattern if any: its `employee_polices` rows must be **unchanged** (scoping check).
7. **Logs**
If `batch_file_id` is missing or batch/policy not found, confirm `myLogger` / app logs contain the new error messages and no SQL exceptions.
### Browser / HTTP trigger (auth required)
- **Route:** `GET` or `POST` under the existing **`/util`** group (filter: **`authMVC`**), same as other QA utilities.
- **Path:** `/util/qa/updateTpaIdForNotInNhance`
- **Parameters:**
- `batch_file_id` — required (query or POST)
- `file_id` — optional; omit or `0` to skip `employee_polices.file_id` filter
- **Handler:** `TestingController::qaUpdateTpaIdForNotInNhance` → delegates to `EmployeeController::updateTpaIdForNotInNhance`.
- **Example (logged-in session):**
`{base_url}/util/qa/updateTpaIdForNotInNhance?batch_file_id=123`
`{base_url}/util/qa/updateTpaIdForNotInNhance?batch_file_id=123&file_id=456`
**Production:** routes `/util/qa/updateTpaIdForNotInNhance` and `/util/qa/updateEmployeeDataFromTpa` are guarded by filter `utilQaRoutes`: in **`production`** they return **404 JSON** unless `.env` has **`util.enableQaRoutes = true`**. Non-production environments allow them without the flag (still require `authMVC` login).
**Server logs:** when direct sync / QA runs successfully but updates **zero** rows, `updateTpaIdForNotInNhance` and `updateEmployeeDataFromTpa` emit a **warning** via `myLogger` with batch id and context counts.
**Remove or restrict this route after QA** if you do not want it long-term in production (or leave the filter off unless `util.enableQaRoutes` is set).
---
## updateEmployeeDataFromTpa (Need to Review → direct DB sync)
### Review notes (`generateCorrectionUploadFromNeedToReview`)
- Loads `batch_files`, validates `client_id` / `client_policy_id` / `client_branch_id`.
- Uses `EmployeePolicyModel::getTPADataVariationReport($clientId, $clientPolicyId, $batchFileId)` — same slice as “Need to Review” (employees with `tpa_id IS NULL` on that policy).
- For each DB row, loads `tpa_api_data` rows with same `emp_code` and `file_id` = batch file id.
- Uses `reconcileDbWithTpa`: requires DB `relationship` to match TPA `relation`, then diffs `name`, `dob`, `gender` (not `relationship`, because it matched).
- Correction Excel only emits rows for `name`, `dob`, `relationship`, `email_corporate`; today `reconcileDbWithTpa` typically only yields `name` / `dob` / `gender` in `not_matching`.
### Plan (`updateEmployeeDataFromTpa`)
1. **Same inputs as correction path**`batch_file_id` → load batch file; reject missing client/policy/branch.
2. **Same report + TPA fetch + reconcile** — no Excel, no `files` insert, no `excelFileFormatValidation`.
3. **Resolve employee**`employee_id` from `employee_polices.*` in the report row; verify `employees.client_id` matches batch `client_id`.
4. **Map diffs to columns** — For each field in `not_matching` that is allowed for correction, set `employees` from TPA (`relationship` ← TPA `relation`; `email_corporate` ← TPA `email_corporate` or `email` if present).
5. **Persist**`EmployeeModel::update($employeeId, $updateData)` (callbacks set `updated_by` where configured).
6. **Observability** — Return counts: `employees_updated`, `rows_skipped_no_diff`, `rows_skipped_no_employee`; log exceptions.
7. **QA route**`/util/qa/updateEmployeeDataFromTpa?batch_file_id=` (authMVC), same pattern as other QA utilities.
### Tasks
- [x] Plan documented in this file.
- [x] Implement `EmployeeController::updateEmployeeDataFromTpa`.
- [x] Add `TestingController::qaUpdateEmployeeDataFromTpa` + `/util/qa/updateEmployeeDataFromTpa` route.
- [x] Wire `proceedTPADataVariationNextStep` + batch modal checkbox for `sync_mode=direct` (Not in Nhance / Need to Review).
- [x] Normalize `updateTpaIdForNotInNhance` return value to `{ success, message, data }` for API/QA/job consumers; optional `$jobId` arg for JobWorker.
- [x] Modal UX: reset direct-sync checkbox on open; warning toastr when direct sync updates zero rows.
- [x] `utilQaRoutes` filter + `util.enableQaRoutes` (.env) for `/util/qa/*` in production; zero-update **warning** logs in sync methods.
- [x] Manual QA: procedure and QA URLs documented below; run on staging with a real batch when available (compare `employees_updated` / skips to expected mismatches; optional parity with correction Excel row count).
### Production UI: direct sync (`sync_mode=direct`)
- **TPA variation modal** (`batch_list.php`): checkbox *“Sync directly to database (skip Excel)”* — when checked and user clicks **Proceed** on **Not in Nhance** or **Need to Review**, the request includes `sync_mode=direct`.
- **Endpoint:** existing `GET employee/proceedTPADataVariationNextStep/{file_id}?tab=...&sync_mode=direct`
- `tab=not_in_nhance` + `sync_mode=direct``updateTpaIdForNotInNhance` (returns counts / ids in `data`).
- `tab=need_to_review` + `sync_mode=direct``updateEmployeeDataFromTpa` (returns skip/update counts in `data`).
- **Other tabs:** `sync_mode=direct` returns **422** with a clear message (e.g. Not in TPA).
- **Default (checkbox off):** unchanged behaviour — Excel generation + existing pipelines.
- **UX safeguards** (`batch_list.js`): opening the TPA variation modal **unchecks** “direct sync” so it is not left on for another file. After **Proceed** with direct sync, if `employee_policies_updated` or `employees_updated` is **0**, the UI shows a **NOTICE** (warning) toastr instead of success-only, with a short hint to verify Nhance vs TPA matches.
### Sign-off (staging / UAT)
- [ ] Executed `GET /util/qa/updateTpaIdForNotInNhance` on a known batch — initials / date: __________
- [ ] Executed `GET /util/qa/updateEmployeeDataFromTpa` on a Need to Review batch — initials / date: __________
### Manual QA hints (`updateEmployeeDataFromTpa`)
- Use a `batch_file_id` that already shows rows on the **Need to Review** tab.
- Before: note `employees.name` / `dob` / `gender` for a sample `emp_code`.
- Call `{base_url}/util/qa/updateEmployeeDataFromTpa?batch_file_id=<id>` (logged in).
- After: same employee row should match TPA `tpa_api_data` for fields that were in `not_matching`.
- JSON response includes `employees_updated` and skip counters for quick sanity check.
---
## Removal checklist — direct-to-database TPA sync (pending confirmation)
**Status:** Not applied in code yet. When you confirm, remove the items below so TPA variation **Proceed** uses **Excel-only** paths: `generateEmployeeUploadFromNotInNhance` and `generateCorrectionUploadFromNeedToReview` only.
### 1. `app/Controllers/EmployeeController.php`
- Remove **`sync_mode` / `syncMode`** handling and the **422** guard for invalid `sync_mode=direct` on wrong tabs in **`proceedTPADataVariationNextStep`**.
- Remove the two **early branches** that call **`updateTpaIdForNotInNhance`** and **`updateEmployeeDataFromTpa`** when `sync_mode=direct`.
- Remove **`sync_mode`** from the final **`myLogger`** context in that method (if present).
### 2. `app/Views/batch_list.php`
- Remove the **modal footer** block: checkbox **`#tpaVariationDirectSync`** + label (“Sync directly to database…”).
- Remove **`$('#tpaVariationDirectSync').prop('checked', false)`** in **`showTPAVariationModal`**.
- In **`proceedTPADataVariationNextStep`**, remove **`directSync`**, **`sync_mode=direct`** on the URL, and the **zero-update NOTICE** / extra message logic; restore simple success/warning behaviour.
### 3. `app/Controllers/TestingController.php`
- Remove **`qaUpdateTpaIdForNotInNhance`**.
- Remove **`qaUpdateEmployeeDataFromTpa`**.
### 4. `app/Config/Routes.php` (under `/util` group)
- Remove the two **`match`** routes for **`qa/updateTpaIdForNotInNhance`** and **`qa/updateEmployeeDataFromTpa`** (including **`utilQaRoutes`** options).
### 5. `app/Filters/UtilQaRoutes.php`
- **Delete the file** (only used for those QA routes).
### 6. `app/Config/Filters.php`
- Remove **`use App\Filters\UtilQaRoutes`** and the **`'utilQaRoutes'`** alias.
### 7. `app/Controllers/JobWorker.php`
- Remove the **`$event_class_mapping`** entries for **`updateEmployeeDataFromTpa`** and **`updateTpaIdForNotInNhance`** (so queued jobs with those names are not routed to `EmployeeController`).
### 8. `.env.sample`
- Remove the **`util.enableQaRoutes`** / UTIL QA block.
### 9. `public/dev_logs/2026-04-03.md`
- **Either** delete this file **or** delete/trim sections that only document direct DB / QA / `sync_mode` (optional cleanup after code removal).
### 10. Local `.env` (manual)
- If **`util.enableQaRoutes`** was added, remove it locally (do not commit secrets).
### Removal tasks (track here)
- [ ] `EmployeeController.php``proceedTPADataVariationNextStep` + delete both sync methods
- [ ] `batch_list.php` — checkbox + JS
- [ ] `TestingController.php` — both QA methods
- [ ] `Routes.php` — both QA routes
- [ ] Delete `UtilQaRoutes.php` + `Filters.php` alias
- [ ] `JobWorker.php` — mapping entries
- [ ] `.env.sample` — QA block
- [ ] This file or sections — optional cleanup

View File

@ -0,0 +1,188 @@
# Ticket forms: client-side input validation plan (character rules + pincode/mobile)
Date: 2026-04-03
Updated: 2026-04-03 (IR Documents §11; `saveIRDocsJson` charset + blur-only Parsley; claim upload URL/file)
Related: `public/dev_logs/2026-03-31_ticket_frontend_validation_plan.md` (broader backend-aligned validation; may overlap—coordinate so rules stay consistent)
---
## 1) Objective
Add JavaScript validation on **`ticket_form_gmc.php`**, **`ticket_form_gpa.php`**, and **`ticket_form_motor.php`** so that:
1. **Validation runs on interaction**, using **`input`** and **`change`** (and equivalent) for `input`, `select`, `textarea`, and other applicable controls inside each form.
2. **Default text rule** for user-editable text fields: only **letters, digits**, and the special characters **`/` `_` `-` `.`** plus **space**. Any other character is rejected or stripped, with a **clear inline error** when invalid input is attempted or present.
3. **Pincode fields**: **digits only**, length **exactly 6** when the field is non-empty (and when required, enforce non-empty + 6 digits).
4. **Mobile / phone fields** designated as “mobile” in the requirements: **digits only**, length **exactly 10** when non-empty (and when required, enforce non-empty + 10 digits).
5. **Error UX**: **Parsley only** for error text (one message per field). No parallel Bootstrap `is-invalid` / custom `.ticket-input-error` blocks that duplicate Parsleys `ul.parsley-errors-list`.
---
## 2) Allowed-character policy (summary)
| Category | Rule | Example messages |
|----------|------|------------------|
| General text / textarea | `^[A-Za-z0-9/_.\- ]*$` (trim-aware where appropriate) | “Only letters, numbers, spaces, and / _ - . are allowed.” |
| Pincode | `^\d{6}$` when value required; optional empty handling per field | “Pincode must be exactly 6 digits.” / “Only digits are allowed.” |
| Mobile (10-digit) | `^\d{10}$` when required | “Mobile number must be exactly 10 digits.” / “Only digits are allowed.” |
| Numeric amounts (existing behaviour) | **GMC**: `claim_amount`, `approved_amount`; **GPA**: `si_amt`, `approved_amount`—**digits-only** | “Only numbers are allowed.” |
---
## 3) Exception: email fields — **decision (implemented)**
**Chosen approach:** **A)** Email fields (`emp_mail`, `emp_personal_mail`) use a **separate rule**: pragmatic format check `^[^\s@]+@[^\s@]+\.[^\s@]+$` (see `public/assets/js/pages/ticket_form_input_validation.js`), not the general text charset.
---
## 4) Fields to exclude or treat specially
| Situation | Treatment |
|-----------|-----------|
| `type="hidden"` | No charset validation (skipped). |
| Read-only inputs | Skipped (`readOnly` / `readonly` attribute). |
| `<select>` | Required / placeholder checks (`""` or `"0"`); no charset on option values. |
| **Date fields** (flatpickr `d/m/Y`) | Allowed via general text regex (`/` and digits). |
| **GMC** `claim_amount` / `approved_amount` | Centralized digits-only in shared script; inline `oninput` removed from `ticket_form_gmc.php`. |
---
## 5) Per-file field matrix (initial inventory)
### 5.1 `app/Views/ticket_form_gmc.php`
**Form id:** `#ticket_form_data`
**Submit:** `onsubmit="submitClaimForm(event, this)"``validateTicketFormInputs` runs inside `submitClaimForm` in `ticket_form_handler.php` / `ticket_edit_onbording.php`.
| Field id / name | Control | Suggested rule |
|-----------------|---------|----------------|
| `emp_code`, `emp_name`, `insured_name` | text | General charset |
| `relationship`, `emp_client_policy`, `acm_id`, `claim_status_id`, `priority`, `mode_of_intimation`, `claim_type`, `non_id_reason` | select | Required + not empty / not placeholder only |
| `policy_no`, `tpa_no` | text | General charset |
| `emp_mobile` | text | **10 digits** |
| `emp_mail`, `emp_personal_mail` | text | **Email** |
| `hospital_name`, `hospital_address`, `hospital_city`, `hospital_state` | text/textarea | General charset |
| `hospital_pin_code` | text | **6 digits** |
| `hospital_phone_no` | text | **10 digits** |
| `doa`, `dod` | text (flatpickr) | General charset as displayed |
| `claim_amount` | text | Digits only |
| `pod_no` | text | General charset |
| Other status-dependent text fields | various | General charset or digits for amounts |
### 5.2 `app/Views/ticket_form_gpa.php`
Same shared script as GMC (included via `ticket_form_handler.php` / edit view). Field IDs listed in §5.1 matrix where applicable (`si_amt`, dates, etc.).
### 5.3 `app/Views/ticket_form_motor.php`
| Field id / name | Control | Rule |
|-----------------|---------|------|
| `client_name`, `vehicle_id`, `emp_client_policy`, `claim_status_id` | select | Existing `validateBeforeSubmit` + shared select validation |
| `insurer_name`, `emp_mobile`, `emp_mail` | text (often readonly) | Skipped when readonly; else mobile / email rules |
| Hidden fields | hidden | Skipped |
---
## 6) Implementation approach
1. **Shared script:** `public/assets/js/pages/ticket_form_input_validation.js` — registers **Parsley** validators (`ticketcharset`, `mobile10`, `pincode6`, `digitonly`, `ticketemail`), applies **`data-parsley-errors-container`** via `applyParsleyErrorTargets()` (one mount per field inside `.form-group`), applies matching `data-parsley-*` attributes **before** `form-validation.init.js` binds `.parsley-examples`, sets **`novalidate`** on the form, sanitizes on `input` (strip invalid chars / non-digits) **without** calling Parsley on every keystroke; **`blur`** / **`change`** (selects) revalidate the field; submit uses `parsley().validate()` only.
2. **Event wiring:** Delegated `input` on `#ticket_form_data` (namespace `.ticketFormValidate`) for sanitization only — **no** Parsley validate on each keystroke (prevents duplicate error lists on fields like `emp_mobile`).
3. **Error display:** **Parsley only** (`parsley-error` class + `ul.parsley-errors-list`). **Do not** combine with manual `invalid-feedback` for the same rules.
4. **Submit gate:** `validateTicketFormInputs(form)` is **`$(form).parsley().validate()`** — **one** validation pass. **`submitClaimForm`** must **not** call `parsley().validate()` again after it (removed duplicate).
5. **Motor:** `validateBeforeSubmit` only calls `submitClaimForm(e, form)` (no duplicate Parsley, no manual select loop that mirrored Parsley required checks).
6. **Script loading:** `ticket_form_handler.php` (create) and `ticket_edit_onbording.php` (edit). Init runs on DOM ready **before** footers `form-validation.init.js` so constraints are on the DOM when Parsley binds.
---
## 6b) Parsley vs custom JS — problem and fix (2026-04-03)
| Problem | Fix |
|---------|-----|
| Custom module appended `is-invalid` + `.invalid-feedback` while Parsley appended `ul.parsley-errors-list` | Removed custom error UI; rules moved to `Parsley.addValidator` + `data-parsley-*` on fields by id |
| `submitClaimForm` ran `validateTicketFormInputs` **then** `parsley().validate()` | `validateTicketFormInputs` **is** full-form Parsley validate; second call removed |
| Motor ran custom validation + manual `select[required]` loop + Parsley | Motor defers to `submitClaimForm` only (single Parsley pass); removed redundant select loop |
## 6c) Double messages on `relationship`, `emp_mobile`, `emp_client_policy` (2026-04-03)
| Cause | Fix |
|-------|-----|
| **HTML5** `required` + Parsley both reporting | Set `#ticket_form_data` **`novalidate`** in `initTicketFormInputValidation` so the browser does not show native validation bubbles alongside Parsley |
| **`emp_mobile`**: `parsley().validate()` on **every** `input` keystroke | Removed live Parsley validate from sanitization; validate on **`blur`** (and submit) only |
| **`relationship` / `emp_client_policy`**: Parsley injecting errors beside the native `<select>` while Select2 (policy) adds another visible control | One **dedicated** error mount per field: `applyParsleyErrorTargets()` appends `<div id="parsley-errors-{fieldId}" class="parsley-errors-target">` inside the fields `.form-group` and sets **`data-parsley-errors-container="#parsley-errors-{fieldId}"`** on each `input`, `textarea`, and `select` (before `form-validation.init.js` binds Parsley) |
| Selects need validation after change | Delegated **`change`** on `select` calls `parsley().validate()` for that field only |
---
## 7) Task list (execution order)
| # | Task | Owner | Status |
|---|------|-------|--------|
| 1 | Confirm **email field policy** (§3) with product/backend | Dev / PM | **Done** — Option A (separate email regex) implemented in code; PM may still formalize if needed |
| 2 | Add shared JS helpers + regex constants; document in file header | Dev | **Done**`public/assets/js/pages/ticket_form_input_validation.js` |
| 3 | **GMC** (`ticket_form_gmc.php`): wire events, pincode/mobile/amount/text, submit gate | Dev | **Done** — Handler + edit view load script; removed duplicate `oninput` on `claim_amount` / `approved_amount` |
| 4 | **GPA** (`ticket_form_gpa.php`): same field coverage | Dev | **Done** — Same bundle via handler/edit include (no view-only change required) |
| 5 | **Motor** (`ticket_form_motor.php`): `validateBeforeSubmit` + shared rules | Dev | **Done**`validateBeforeSubmit``submitClaimForm` only (no double Parsley) |
| 6 | **Parsley duplicate messages** — single source of truth | Dev | **Done** — see §6b |
| 7 | Double errors on **relationship**, **emp_mobile**, **emp_client_policy** | Dev | **Done** — see §6c (`novalidate`, `data-parsley-errors-container`, blur/change validate) |
| 8 | Manual QA across three forms (create + edit if applicable) | QA | **Pending** — confirm one message per field (especially relationship, mobile, policy) |
| 9 | If backend expects stricter rules, update `TicketController` validation in a follow-up | Dev | **Optional / not started** |
| 10 | **Claim file / URL upload** (`claim_files_upload.php` via `ticket_edit_onbording.php`) | Dev | **Done** — see §10 |
| 11 | **IR Documents** card: document names + second card URL/file rules | Dev | **Done** — see §11 |
---
## 8) Out of scope (unless requested later)
- Server-side PHP changes for the same charset rules.
- `ticket_note.php`, `ticket_reply.php`, `ticket_feedback_form.php` (covered by the older plan file, not this task).
- Further Parsley global config changes unless a new conflict appears.
---
## 9) Files touched (implementation)
| File | Change |
|------|--------|
| `public/assets/js/pages/ticket_form_input_validation.js` | Shared validation module: `ticketdocname` / `ticketclaimurl` (aligned with `TicketController::upload_url`); multi-form init including `#drive_file_upload_form` / `#edit_url_form` |
| `app/Views/ticket_form_handler.php` | Script `src` + `validateTicketFormInputs` inside `submitClaimForm` |
| `app/Views/ticket_edit_onbording.php` | Script `src` + same `submitClaimForm` gate |
| `app/Views/ticket_form_gmc.php` | Removed inline numeric `oninput` from `claim_amount` and `approved_amount` |
| `app/Views/ticket_form_motor.php` | `validateBeforeSubmit``submitClaimForm` only (no duplicate validation) |
| `app/Views/claim_files_upload.php` | `parsley-examples` + `novalidate`; unique ids for dynamic rows; submit gate + `refreshTicketFormValidationForForm`; modal `shown` refresh; `#ir_documents_form` |
| `app/Controllers/TicketController.php` | `saveIRDocsJson` document name regex aligned with ticket general charset (`ticketcharset`) |
---
## 10) Claim file upload (`app/Views/claim_files_upload.php`)
**Context:** The tab is included from `ticket_edit_onbording.php`, which already loads `public/assets/js/pages/ticket_form_input_validation.js` before footer Parsley init.
| Item | Detail |
|------|--------|
| **Forms** | `#drive_file_upload_form` (URL rows + file rows), `#edit_url_modal``#edit_url_form` (modal; Parsley inited when present) |
| **Field mapping** | `docs_name[]` / `docs_name_*` / `edit_doc_name`**`ticketdocname`** (letters, digits, space, `_`, `-` — matches `upload_url` `docs_name.*` regex). `url[]` / `url_name_*` / `edit_url_link`**`ticketclaimurl`** (same pattern as `url.*` on server). File inputs: no charset; `required` only. |
| **Dynamic rows** | `addHTMLInput` / `addFileUploadHtml` generate **unique** `id`s (`docs_name_*`, `url_name_*`, `file_upload_*`). After add/remove or `toggleUploadType`, call **`refreshTicketFormValidationForForm('#drive_file_upload_form')`**. |
| **Submit** | Single gate: **`validateTicketFormInputs(this)`** on `#drive_file_upload_form` (no second `parsley().validate()`); on failure: toastr + scroll to `.parsley-error`. |
| **Modal** | `shown.bs.modal` triggers **`refreshTicketFormValidationForForm('#edit_url_form')`** so error targets stay correct. Edit-save is not routed in `Routes.php` (no `update_url`); validation is ready if a route is added later. |
---
## 11) IR Documents card + claim upload UX (2026-04-03)
### 11.1 Objective
1. **IR Documents** (first card): `document_name` values may use the **same allowed characters as general ticket text** (`ticketcharset`: letters, digits, spaces, `/ _ - .`).
2. **No inline errors while typing** for IR document names and for claim-upload **URL** / **claim-upload document name** fields: Parsley **`data-parsley-trigger="blur"`** (and **`change`** for file inputs). **`irdocname`** does **not** run input sanitization (no live stripping), so invalid characters are not removed on the fly; validation runs on **blur** and **Save**.
3. **Second card** (claim file / URL): **URL** rows — **`ticketclaimurl`** only; **file** rows — **`accept`** + Parsley **`claimfileext`** + submit-time extension guard for PDF/JPG/JPEG/PNG.
### 11.2 Implementation
| Area | Detail |
|------|--------|
| **Backend** | `TicketController::saveIRDocsJson``document_name` regex updated to `^[A-Za-z0-9\/_.\- ]+$` with error message aligned to ticket charset text. |
| **View** | `#ir_documents_form` wraps `document-list-container`; inputs `id="ir_doc_name_{index}"`, `name="ir_document_name[]"`; `escapeHtmlAttr` for safe `value`; `saveConfiguration` syncs DOM → `documentConfig` then **`validateTicketFormInputs(ir_documents_form)`**; `renderDocumentList` calls **`refreshTicketFormValidationForForm('#ir_documents_form')`**. |
| **JS** | `getFieldKindFromElement`: **`irdocname`** (`ir_doc_name_*` / `ir_document_name`), **`claimfile`** (`file_upload_*`). Validators: **`claimfileext`**. Parsley data attrs cleared via **`clearParsleyDataAttrs()`** (fixes multi-attribute `removeAttr`). Error targets for file inputs enabled; **`.col-md-7`** / **`.col-md-5`** used as mount when `.form-group` is missing. |
---
*End of plan.*

View File

@ -0,0 +1,81 @@
# 2026-04-04 — Daily tasks
## VidalGetBenefDetailsV2 (Enrollment Dump API)
### Context
- **Goal:** Add `VidalApiController::VidalGetBenefDetailsV2`, mirroring `VidalGetBenefDetails` end-to-end (batch gate, JSON dump, `saveVidalAPIData` job, employee match, `tpa_id` update, e-card job, batch file status), but calling the **Enrollment info** integration used in `TestingController::getVidalEnrollmentInfo()` for **URL, headers, and request body shape** only.
- **Reference sample:** `public/tmp/Enrollment Dump API.docx` — HTTP 200 body is `status`, `data` (array of member rows), `trace`, `successful`.
- **Note on “same format”:** The raw Enrollment API uses different field names (`beneficiaryName`, `membershipNo`, `employeeNo`, `dateOfBirth`, `relation`, etc.). The implementation **normalizes** each row to the same **internal** dependent shape as V1 (`name`, `empNo`, `relationship`, `gender`, `dob`, `enrollmentId`, `policyNumber`, `age`) so matching, `saveVidalAPIData`, and logging stay unchanged.
### Plan
1. **HTTP contract (from Testing, env-aware URL)**
- **URL:** `vidalEnrollmentInfoApiUrl()` — if `VIDAL_API_BASE_URL` ends with `/api`, strip it and append `/enrollment/info` (matches `https://devapigw.vidalhealthtpa.com/partner-integration/enrollment/info`); otherwise fall back to that dev URL.
- **Headers:** `Content-Type: application/json`, `ocp-apim-subscription-key: {VIDAL_API_SUBSCRIPTION_KEY}` (same as `getVidalEnrollmentInfo`).
- **Body:** `policyNo`, `startIndex`, `endIndex` (paginated windows; current page size **100** in code).
2. **Fetch loop**
- Keep the same **batch_files** prerequisite as V1 (TPA export rows for `client_policy_id`).
- Replace V1s **perexport-date** `startDate`/`endDate` calls with **pagination** until a page returns fewer than `pageSize` rows or an empty page (after the first).
3. **Response handling**
- Require `status === 'SUCCESS'`; if `successful` is present and `false`, treat as failure.
- Read rows from `data` (top-level array in the decoded JSON).
- Map each row through `normalizeVidalEnrollmentRecordToDependentFormat()` (reference map from `relationship.csv` baked into code, `membershipNo``enrollmentId`, `beneficiaryName``name`, plus `si` / `doj` / `desc` for `tpa_api_data` — see **Relationship & TPA columns** below).
4. **Downstream (unchanged from V1)**
- Write merged dependents JSON under `writable/tmp/`, queue `saveVidalAPIData`, run the same DB match/update and e-card / batch_file status logic.
- **SQL:** V2 uses valid `UPDATE employee_polices SET tpa_id = … WHERE id = …` (no stray comma before `WHERE`).
5. **Wiring**
- Route: `GET VidalGetBenefDetailsV2``VidalApiController::VidalGetBenefDetailsV2`.
- `Acl.php`: public entry like V1.
- `JobWorker.php`: job `VidalGetBenefDetailsV2``VidalApiController` (method name matches job name).
6. **Follow-ups (optional)**
- Switch `ApiServiceController` TPA pull from `VidalGetBenefDetails` to `VidalGetBenefDetailsV2` when product confirms Enrollment API for all policies.
- Revisit **emp_code ↔ employeeNo** matching if dependents share the same `employeeNo` in real dumps (doc sample shows repeated `employeeNo` for family members).
### Tasks
- [x] Implement `normalizeVidalEnrollmentRecordToDependentFormat`, `vidalEnrollmentInfoApiUrl`, and `VidalGetBenefDetailsV2` in `VidalApiController.php`.
- [x] Register route, ACL, and `JobWorker` job `VidalGetBenefDetailsV2`.
- [x] Document plan and tasks in this file (`2026-04-04.md`).
### Job payload (same shape as V1)
`policy_no`, `client_policy_id`, `file_id`, `return_type` (`job` when queued).
### Manual check
- With valid env and data: enqueue `VidalGetBenefDetailsV2` or call `GET …/VidalGetBenefDetailsV2` with the same query/body conventions as V1 (if wired), and confirm logs show `VIDAL V2 - TPA ID Pull` and `tpa_api_data` rows after `saveVidalAPIData`.
---
## Vidal relationship.csv → Nhance + `tpa_api_data` extra columns
### Context
- **Reference file:** `public/tmp/relationship.csv` — documentation only (not read at runtime). Column A = Vidal `VIDAL_RELSHIP_DESCRIPTION`, column C = `OUR RELATIONSHIPS` (separator `,,`). Only rows with a non-empty “ours” value are represented in code (currently eight entries).
- **Scope (per request):** Use this mapping **only** in `VidalGetBenefDetailsV2` (via `normalizeVidalEnrollmentRecordToDependentFormat`) and in `saveVidalAPIData`. **V1** `VidalGetBenefDetails` / legacy JSON rows without `vidal_relation_raw` keep the previous behaviour (`strtolower(relationship)` only).
### Plan
1. **Baked-in map**`vidalRelationshipReferenceMap()` returns the associative array (lowercase Vidal → lowercase Nhance) copied from the reference CSV. Constructor sets `$this->vidalRelationshipMap` once from that method. To add mappings, edit the PHP array and keep the CSV in sync as documentation.
2. **Map function**`mapVidalRelationshipToNhance($vidalRelationDescription)` returns a hit from `$this->vidalRelationshipMap` if present; else `Employee` / `Employees``self`; else `strtolower(str_replace('-', ' ', $raw))`.
3. **V2 normalization**`normalizeVidalEnrollmentRecordToDependentFormat()` sets `relationship` from the mapper, adds `vidal_relation_raw` for persistence/audit, `si` from `baseSumInsured`, `doj` from `dateOfJoining` (Y-m-d via `normalizeVidalEnrollmentDateToYmd`), `desc` from `buildVidalEnrollmentDescForTpaRow()` (`productName`, `remarks`, `insuredName`).
4. **`saveVidalAPIData`** — If `vidal_relation_raw` is present, recompute `relation` with `mapVidalRelationshipToNhance` (keeps DB aligned even if JSON is tweaked). Set `self` from mapped `relation === 'self'`. Populate **`desc`**, **`si`**, **`doj`** per `TpaApiDataModel::$allowedFields`. `desc` stored as `Vidal relation: … | …` plus JSON `desc` when both exist.
5. **Model** — No schema change; fields already in `app/Models/TpaApiDataModel.php`: `desc`, `si`, `doj`.
### Tasks
- [x] Add `vidalRelationshipReferenceMap()` + constructor copy to `$vidalRelationshipMap`, `mapVidalRelationshipToNhance`, and date helper on `VidalApiController`.
- [x] Extend enrollment normalization with `vidal_relation_raw`, `si`, `doj`, `desc`.
- [x] Update `saveVidalAPIData` mapping and extra columns.
- [x] Document in `2026-04-04.md`.
### Caveats
- CSV lines like `Father (2),,Self` map that exact Vidal string to Nhance `self`; ensure API `relation` strings match the CSV keys (case-insensitive).
- Rows in the CSV with **no** “ours” value are ignored; those relations fall through to `Employee`/`Employees` or generic lowercasing.