Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
sanjeev.p 2026-04-08 11:53:32 +05:30
commit 24fe2b79c4
47 changed files with 6208 additions and 596 deletions

View File

@ -276,7 +276,7 @@ class Acl
// ===================== INTERNAL TEST =====================
'#^/test#' => [
'roles' => [ADMIN_ROLE_ID],
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
'teams' => []
],

View File

@ -887,6 +887,8 @@ $routes->get('HealthIndiaGetBenefDetails','HealthIndiaApiController::HealthIndia
$routes->post("ecardRequest", "ApiServiceController::ecardRequest");
$routes->post("getTPAID", "ApiServiceController::getTPAID");
$routes->post("sendDataToTPA", "ApiServiceController::sendDataToTPA");
$routes->get("jobStatus", "TestingController::jobStatus");
$routes->post("jobStatus", "TestingController::jobStatus");
$routes->get('fileDownload','MediAssistApiController::fileDownload');
@ -928,6 +930,8 @@ $routes->group('test', function($routes) {
$routes->get('testingquerys','TestingController::testingquerys');
$routes->get('thzReminderCrone','ThzController::getOpenTicketsOlderThan24HoursAndAssignNextLevel');
$routes->get('chartbrewDashboardDemo','TestingController::chartbrewDashboardDemo');
$routes->get('getVidalEnrollmentInfo','TestingController::getVidalEnrollmentInfo');
// $routes->get('createDefaultMailTempalteInDB','TestingController::createDefaultMailTempalteInCrossDB');
// $routes->get('createDefaultMailTempalteInSameDB','TestingController::createDefaultMailTempalteInSameDB');
});

View File

@ -350,6 +350,15 @@ class ApiServiceController extends BaseController
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Action Triggered','data' => [] ]);
}else if ($tpa_id == $this->icici_primary_key) // ICICI Lombard (EWA)
{
$file_id = $fileModel->insert($data);
log_message('error', "ICICI - Files table inserted successfully, File id : {$file_id}");
$r = Jobs::addJob(['job_name' => 'getEnrollmentBatchStatus', 'payload' => ['client_policy_id' => $policy_id, 'file_id' => $file_id, 'client_policy_id' => $policy_id, 'return_type' => 'job']]);
log_message('error', "ICICI - getEnrollmentBatchStatus job pushed successfully.");
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Action Triggered','data' => [] ]);
}else{
return $this->respond(['status' => false, 'code' => 200,'message' => 'TPA not found ','data' => [] ]);
}
@ -553,7 +562,6 @@ class ApiServiceController extends BaseController
]);
}
// public function getWellnessUrl()
// {
@ -725,7 +733,98 @@ class ApiServiceController extends BaseController
// }
public function sendDataToTPA()
{
$requested_data = $this->request->getPost() ?? [];
log_message('error', "getTPAID payloads :" . json_encode($requested_data));
$tpa_id = $requested_data['tpa_id'] ?? null;
$policy_no = $requested_data['policy_no'] ?? null;
$client_id = $requested_data['client_id'] ?? null;
$branch_id = $requested_data['client_branch_id'] ?? null;
$policy_id = $requested_data['client_policy_id'] ?? null;
$event = $requested_data['event'] ?? null;
$event_mapping = [
'inception' => 'A',
'missed_inception' => 'A',
'addition' => 'A',
'dependent_addition' => 'A',
'deletion' => 'D',
'correction' => 'M',
'si_enhancement' => 'M'
];
$fileModel = new BatchFileModel();
$filesData = $fileModel
->join('client_policy', 'client_policy.id = batch_files.client_policy_id')
->where('client_policy.tpa_id', $tpa_id)
->where('batch_files.is_active', 1)
->where('batch_files.event_type', 'api')
->where('batch_files.icici_status_flag !=', 'COMPLETED')
->countAllResults();
if($filesData > 0){
log_message('error', "TPA initiation is in progress.");
return $this->respond(['status' => false, 'code' => 200,'message' => 'TPA initiation is in progress.','data' => [] ]);
}
$employeePolicyModel = new EmployeePolicyModel();
$employeePolicyData = $employeePolicyModel
->select('
employees.*,
employee_polices.id as emp_policy_id,
employee_polices.client_policy_id,
')
->join('employees', 'employees.id = employee_polices.employee_id')
->where('employee_polices.is_active', 1)
->where('employee_polices.status', 'active')
->where('employees.is_active', 1)
->where('employees.emp_status', 'active')
->where('employee_polices.tpa_id IS NULL')
->where('employee_polices.client_policy_id', $policy_id)
->countAllResults();
if($employeePolicyData == 0){
log_message('error', "No Employee Policy found with null TPA ID. TPA ID is already updated.");
return $this->respond(['status' => false, 'code' => 200,'message' => 'No employee to upload','data' => [] ]);
}
$data = [
'file_name' => "API - Employee data push",
'event_type' => $event,
'actions' => "push",
'insurer_or_tpa' => "tpa",
'batch_code' => generate_random_string(4),
'status' => "inprogress",
'client_id' => $client_id,
'client_policy_id'=> $policy_id,
'client_branch_id'=> $branch_id,
'created_by'=> get_session_userid(),
];
if ($tpa_id == $this->icici_primary_key) // MediAssist
{
$file_id = $fileModel->insert($data);
log_message('error', "Files table inserted successfully, File id : {$file_id}");
$requested_data['file_id'] = $file_id;
$requested_data['return_type'] = 'job';
$requested_data['insurer_or_tpa'] = 'tpa';
$requested_data['flag_status'] = $event_mapping[$event] ?? "A";
$ICICILombardController = new ICICILombardController();
$apiResponse = $ICICILombardController->ICICIPushEmployeeDetails($requested_data);
// $r = Jobs::addJob(['job_name' => 'ICICIPushEmployeeDetails', 'payload' => $requested_data]);
// log_message('error', "ICICIPushEmployeeDetails job pushed successfully.");
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Action Triggered','data' => $apiResponse ]);
}
}

View File

@ -5307,6 +5307,18 @@ class ClientController extends AdminController
}
$data['family_floaters']['elders_count'] = $this->request->getPost("elder_member_count") ? $this->request->getPost("elder_member_count") : 0;
// OPD policy terms fields for policy_type_id = 72
$data['mode_of_serviceability'] = $policy_terms['mode_of_serviceability'] ?? '';
$data['eligibility'] = $policy_terms['eligibility'] ?? '';
$data['total_sum_insured_limit'] = $policy_terms['total_sum_insured_limit'] ?? 'INR 15000';
$data['in_person_doctor_consultation'] = $policy_terms['in_person_doctor_consultation'] ?? '';
$data['prescribed_lab_test_pathology_radiology'] = $policy_terms['prescribed_lab_test_pathology_radiology'] ?? '';
$data['prescribed_pharmacy'] = $policy_terms['prescribed_pharmacy'] ?? '';
$data['dental'] = $policy_terms['dental'] ?? '';
$data['vision'] = $policy_terms['vision'] ?? '';
$data['vaccination_for_children_and_adults'] = $policy_terms['vaccination_for_children_and_adults'] ?? '';
$data['special_condition_label'] = $policy_terms['special_condition_label'] ?? [];
$data['special_condition_input'] = $policy_terms["special_condition_input"] ?? [];
@ -5364,6 +5376,7 @@ class ClientController extends AdminController
$values = $data['special_condition_input'] ?? [];
$special_conditions = [];
$display_fields = [];
foreach ($labels as $i => $label) {
$value = $values[$i] ?? '';
@ -5376,7 +5389,39 @@ class ClientController extends AdminController
$special_conditions[$label] = $value;
}
return $special_conditions;
$labelMap = [
'mode_of_serviceability' => 'Mode Of Serviceability',
'eligibility' => 'Eligibility',
'total_sum_insured_limit' => 'Total Sum Insured Limit',
'in_person_doctor_consultation' => 'In Person Doctor Consultation',
'prescribed_lab_test_pathology_radiology' => 'Prescribed Lab Test Pathology Radiology',
'prescribed_pharmacy' => 'Prescribed Pharmacy',
'dental' => 'Dental',
'vision' => 'Vision',
'vaccination_for_children_and_adults' => 'Vaccination For Children And Adults',
];
foreach ($data as $key => $value) {
if (!str_ends_with($key, '_display')) {
continue;
}
if (empty($value)) {
continue;
}
$baseKey = substr($key, 0, -8);
$baseValue = $data[$baseKey] ?? '';
if (trim((string)$baseValue) === '') {
continue;
}
$label = $labelMap[$baseKey] ?? ucwords(str_replace('_', ' ', $baseKey));
$display_fields[$label] = $baseValue;
}
return array_merge($display_fields, $special_conditions);
}
public function checkPolicyType($policy_type_id, $client_branch_id, $client_id)

View File

@ -2601,6 +2601,7 @@ class EmployeeController extends AdminController
$employeeRest = new EmployeeRestController();
$tpa_api_service_status = $employeeRest->checkTpaApiEnable($client_policy_id, 'getTPAID', 'internel');
$tpa_push_api_service_status = $employeeRest->checkTpaApiEnable($client_policy_id, 'sendDataToTPA', 'internel');
$slab_details = $this->policiesModel->getPolicySlabRatesForEmpOnboard($client_policy_id,$policy_details['client_id']);
@ -2620,7 +2621,7 @@ class EmployeeController extends AdminController
$message = isset($message) ? ($message . ' The policy does not have a CD account number.') : null;
}
return $this->respond(['dataStatus' => true, 'code' => 200, 'message' => $message, 'si_enhancement' => $si_enhancement_true_or_false, 'insurer_multi_event' => $insurer_details['is_multi_event'], 'tpa_api_service_status' => $tpa_api_service_status], 200);
return $this->respond(['dataStatus' => true, 'code' => 200, 'message' => $message, 'si_enhancement' => $si_enhancement_true_or_false, 'insurer_multi_event' => $insurer_details['is_multi_event'], 'tpa_api_service_status' => $tpa_api_service_status, 'tpa_push_api_service_status' => $tpa_push_api_service_status], 200);
}
@ -4074,39 +4075,33 @@ class EmployeeController extends AdminController
// d($not_in_nhance);die();
if (!empty($not_in_tpa) || !empty($not_in_nhance) || !empty($emp_data_wo_tpa_id)) {
if ($type === 'download') {
$response = [
'not_in_tpa' => $not_in_tpa,
'not_in_nhance' => $not_in_nhance,
'mismatch_data' => $emp_data_wo_tpa_id,
];
if ($type === 'internal') {
return $response;
}else if ($type === 'download') {
$this->exportVariationReportExcel($not_in_tpa, $not_in_nhance, $emp_data_wo_tpa_id);
} else {
$response = [
'not_in_tpa' => $not_in_tpa,
'not_in_nhance' => $not_in_nhance,
'mismatch_data' => $emp_data_wo_tpa_id,
];
return $this->respond(
[
'status' => true,
'code' => 200,
'message' => '',
'data' => $response,
],
200
);
return $this->respond(['status' => true,'code' => 200,'message' => '','data' => $response,],200);
}
} else {
if ($type === 'internal') {
return [];
}
if ($type === 'download') {
return false;
}
return $this->respond(
[
'status' => false,
'code' => 202,
'message' => 'No data found',
'data' => [],
],
200
);
return $this->respond(['status' => false,'code' => 202,'message' => 'No data found','data' => [],],200);
}
}
@ -4161,14 +4156,15 @@ class EmployeeController extends AdminController
} elseif ($tab === 'need_to_review') {
// For the "Need to Review" tab, generate a Correction Excel
// using the same overall pipeline as the Not in Nhance implementation.
$generationResult = $this->generateCorrectionUploadFromNeedToReview((int) $file_id, $file);
// $generationResult = $this->generateCorrectionUploadFromNeedToReview((int) $file_id, $file);
$generationResult = $this->updateEmployeeDataFromTpa([$file_id]);
if (!$generationResult['status']) {
return $this->respond(
[
'status' => false,
'code' => 422,
'message' => $generationResult['message'] ?? 'Unable to generate correction upload file from Need to Review data.',
'message' => $generationResult['message'] ?? 'Unable to process the data from Need to Review tab.',
'data' => $generationResult['data'] ?? [],
],
200
@ -4178,12 +4174,13 @@ class EmployeeController extends AdminController
$this->myLogger->logme(
'error',
'TPA variation review completed and proceed to next clicked',
[
'file_id' => $file_id,
'user_id' => get_session_userid(),
'tab' => $tab,
]
'TPA variation review completed and proceed to next clicked' . json_encode(
[
'file_id' => $file_id,
'user_id' => get_session_userid(),
'tab' => $tab,
]
)
);
return $this->respond(
@ -4193,7 +4190,7 @@ class EmployeeController extends AdminController
'message' => $tab === 'not_in_nhance'
? 'Employee upload file generated from Not in Nhance data and queued for processing.'
: ($tab === 'need_to_review'
? 'Correction upload file generated from Need to Review data and queued for processing.'
? 'Review data update successfully.'
: 'Proceed to next step recorded successfully.'),
'data' => [],
],
@ -4246,13 +4243,7 @@ class EmployeeController extends AdminController
$TpaApiDataModel = new TpaApiDataModel();
// Get master emp codes from Nhance for this client & policy
$masterEmpRows = $this->employeePolicyModel->getTPADataVariationReport(
$clientId,
$clientPolicyId,
$batchFileId,
[],
true
);
$masterEmpRows = $this->employeePolicyModel->getTPADataVariationReport($clientId,$clientPolicyId,$batchFileId,[],true);
if (!is_array($masterEmpRows)) {
$masterEmpRows = [];
}
@ -4450,7 +4441,7 @@ class EmployeeController extends AdminController
// Run the same format validation used for manual uploads.
$validationResult = $empServiceController->excelFileFormatValidation(['file_id' => $newFileId]);
$validationResult = $empServiceController->excelFileFormatValidation(['file_id' => $newFileId, 'batch_file_id' => $batchFileId]);
if (isset($validationResult['error_summary']) && count($validationResult['error_summary'])) {
return [
@ -4465,6 +4456,7 @@ class EmployeeController extends AdminController
'message' => 'Employee upload file generated from Not in Nhance data and queued for processing.',
'data' => ['file_id' => $newFileId],
];
} catch (\Throwable $e) {
$this->myLogger->logme(
'error',
@ -4763,6 +4755,352 @@ class EmployeeController extends AdminController
}
}
/**
* Apply TPA values to `employees` for rows in the same "Need to Review" set as
* {@see generateCorrectionUploadFromNeedToReview}: same `getTPADataVariationReport` slice,
* same `reconcileDbWithTpa` matching, then persist mismatched fields from TPA instead of
* generating a correction Excel.
*
* Updatable fields mirror the correction Excel allow-list where they exist on TPA rows:
* `name`, `dob`, `gender`, `relationship` (from TPA `relation`), `email_corporate` (if present on TPA).
* Note: {@see reconcileDbWithTpa} only pairs rows when DB `relationship` matches TPA `relation`,
* so `relationship` rarely appears in `not_matching`; name/dob/gender are the usual diffs.
*
* @param array $params Expects `batch_file_id` (int, required).
*
* @return array{success:bool,message:string,data:array}
*/
public function updateEmployeeDataFromTpa(array $params)
{
try {
$batchFileId = (int) ($params['batch_file_id'] ?? 0);
if ($batchFileId <= 0) {
return [
'success' => false,
'message' => 'batch_file_id is required and must be a positive integer.',
'data' => [],
];
}
$batchFile = $this->batchFileModel->find($batchFileId);
if (!$batchFile) {
return [
'success' => false,
'message' => 'Batch file not found.',
'data' => ['batch_file_id' => $batchFileId],
];
}
$clientId = (int) ($batchFile['client_id'] ?? 0);
$clientPolicyId = (int) ($batchFile['client_policy_id'] ?? 0);
$clientBranchId = (int) ($batchFile['client_branch_id'] ?? 0);
if (!$clientId || !$clientPolicyId || !$clientBranchId) {
return [
'success' => false,
'message' => 'Incomplete batch file information. Client / policy / branch missing.',
'data' => [],
];
}
$TpaApiDataModel = new TpaApiDataModel();
$dbRows = $this->employeePolicyModel->getTPADataVariationReport(
$clientId,
$clientPolicyId,
$batchFileId
);
if (!is_array($dbRows) || $dbRows === []) {
return [
'success' => false,
'message' => 'No employee data found for this TPA variation batch.',
'data' => [],
];
}
$allowedFields = ['name', 'dob', 'relationship', 'email_corporate', 'gender'];
$employeeModel = new EmployeeModel();
$employeesUpdated = 0;
$rowsSkippedNoDiff = 0;
$rowsSkippedNoEmployee = 0;
foreach ($dbRows as $dbRow) {
$tpaRows = $TpaApiDataModel->select('*')
->where('emp_code', $dbRow['emp_code'] ?? '')
->where('file_id', $batchFileId)
->where('is_active', 1)
->findAll();
if ($tpaRows === []) {
continue;
}
$match = $this->reconcileDbWithTpa($dbRow, $tpaRows);
if (($match['status'] ?? '') !== 'matched') {
continue;
}
$tpaRecord = $match['tpa_record'] ?? [];
$notMatching = $match['not_matching'] ?? [];
if (!is_array($notMatching) || $notMatching === []) {
$rowsSkippedNoDiff++;
continue;
}
$employeeId = (int) ($dbRow['employee_id'] ?? 0);
if ($employeeId <= 0) {
$rowsSkippedNoEmployee++;
continue;
}
$employeeRow = $employeeModel->find($employeeId);
if (
!$employeeRow
|| (int) ($employeeRow['client_id'] ?? 0) !== $clientId
) {
$rowsSkippedNoEmployee++;
continue;
}
$updateData = [];
foreach ($notMatching as $field) {
if (!in_array($field, $allowedFields, true)) {
continue;
}
if ($field === 'relationship') {
$val = $tpaRecord['relation'] ?? null;
if ($val !== null && $val !== '') {
$updateData['relationship'] = $val;
}
continue;
}
if ($field === 'email_corporate') {
$val = $tpaRecord['email_corporate'] ?? $tpaRecord['email'] ?? null;
if ($val !== null && $val !== '') {
$updateData['email_corporate'] = $val;
}
continue;
}
$val = $tpaRecord[$field] ?? null;
if ($val !== null && $val !== '') {
$updateData[$field] = $val;
}
}
if ($updateData === []) {
continue;
}
if ($employeeModel->update($employeeId, $updateData)) {
$employeesUpdated++;
}
}
if ($employeesUpdated === 0) {
$this->myLogger->logme(
'error',
'updateEmployeeDataFromTpa: zero employee rows updated',
[
'batch_file_id' => $batchFileId,
'rows_skipped_no_diff' => $rowsSkippedNoDiff,
'rows_skipped_no_employee' => $rowsSkippedNoEmployee,
]
);
}
return [
'success' => true,
'message' => 'TPA-aligned employee updates applied where mismatches were reconciled.',
'data' => [
'batch_file_id' => $batchFileId,
'employees_updated' => $employeesUpdated,
'rows_skipped_no_diff' => $rowsSkippedNoDiff,
'rows_skipped_no_employee' => $rowsSkippedNoEmployee,
],
];
} catch (\Throwable $e) {
$this->myLogger->logme(
'error',
'updateEmployeeDataFromTpa failed: ' . json_encode([
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], JSON_PRETTY_PRINT)
);
return [
'success' => false,
'message' => 'Unable to process the data from Need to Review tab.',
'data' => [],
];
}
}
/**
* @param array $params batch_file_id (required), file_id (optional, >0 filters employee_polices.file_id)
* @param mixed $jobId Optional queue job id when invoked from JobWorker (ignored).
*
* @return array{success:bool,message:string,data:array}
*/
public function updateTpaIdForNotInNhance($params, $jobId = null): array
{
try {
if (!is_array($params)) {
return [
'success' => false,
'message' => 'Invalid parameters.',
'data' => [],
];
}
$TpaApiDataModel = new TpaApiDataModel();
$fileId = (int) ($params['file_id'] ?? 0);
$batchFileId = (int) ($params['batch_file_id'] ?? 0);
if ($batchFileId <= 0) {
$this->myLogger->logme('error', 'Batch file ID missing in updateTpaIdForNotInNhance', ['params' => $params]);
return [
'success' => false,
'message' => 'Batch file ID is required.',
'data' => [],
];
}
$batchFileData = db_connect()->table('batch_files')->where('id', $batchFileId)->get()->getRowArray();
if (empty($batchFileData) || empty($batchFileData['client_policy_id'])) {
$this->myLogger->logme('error', 'Batch file not found or missing client_policy_id in updateTpaIdForNotInNhance', ['batch_file_id' => $batchFileId]);
return [
'success' => false,
'message' => 'Batch file not found or missing client policy.',
'data' => [],
];
}
$client_policy_data = $this->clientPolicyModel->where('id', $batchFileData['client_policy_id'])->first();
if (empty($client_policy_data)) {
$this->myLogger->logme('error', 'Client policy not found in updateTpaIdForNotInNhance', ['client_policy_id' => $batchFileData['client_policy_id']]);
return [
'success' => false,
'message' => 'Client policy not found.',
'data' => [],
];
}
// Get master emp codes from Nhance for this client & policy
$masterEmpRows = $this->employeePolicyModel->getTPADataVariationReport(
$batchFileData['client_id'],
$batchFileData['client_policy_id'],
$batchFileId,
[],
true
);
if (!is_array($masterEmpRows)) {
$masterEmpRows = [];
}
$masterEmpCodes = array_values(array_filter(
array_unique(array_column($masterEmpRows, 'emp_code')),
static fn ($code) => $code !== null && $code !== ''
));
// Fetch Not in Nhance rows for this batch file
$notInNhanceQuery = $TpaApiDataModel->select('*')
->where('is_active', 1)
->where('file_id', $batchFileId);
if ($masterEmpCodes !== []) {
$notInNhanceQuery->whereNotIn('emp_code', $masterEmpCodes);
}
$notInNhance = $notInNhanceQuery->findAll();
$uhidValue = $client_policy_data['policy_no'] ?? null;
$empPolicyIds = [];
foreach ($notInNhance as $tpaRow) {
$builder = $this->employeePolicyModel
->select('employee_polices.id')
->join('employees e', 'e.id = employee_polices.employee_id')
->where('e.name', $tpaRow['name'] ?? '')
->where('e.emp_code', $tpaRow['emp_code'] ?? '')
->where('e.dob', $tpaRow['dob'] ?? null)
->where('e.relationship', $tpaRow['relation'] ?? null)
->where('e.gender', $tpaRow['gender'] ?? null)
->where('employee_polices.client_policy_id', (int) $batchFileData['client_policy_id'])
->where('e.client_id', (int) $batchFileData['client_id']);
if ($fileId > 0) {
$builder->where('employee_polices.file_id', $fileId);
}
$empPolicyRow = $builder->first();
if (empty($empPolicyRow['id'])) {
continue;
}
$epId = (int) $empPolicyRow['id'];
if ($this->employeePolicyModel->update($epId, [
'tpa_id' => $tpaRow['tpa_id'] ?? null,
'uhid' => $uhidValue,
])) {
$empPolicyIds[] = $epId;
}
}
if ($empPolicyIds === []) {
$this->myLogger->logme(
'warning',
'updateTpaIdForNotInNhance: zero employee_policy rows updated',
[
'batch_file_id' => $batchFileId,
'not_in_nhance_count' => count($notInNhance),
]
);
}
return [
'success' => true,
'message' => 'TPA id / UHID applied on matching employee policies for Not in Nhance rows.',
'data' => [
'employee_policies_updated' => count($empPolicyIds),
'employee_policy_ids' => $empPolicyIds,
],
];
} catch (\Throwable $e) {
$this->myLogger->logme(
'error',
'updateTpaIdForNotInNhance exception: ' . $e->getMessage(),
['params' => $params, 'trace' => $e->getTraceAsString()]
);
return [
'success' => false,
'message' => 'Unexpected error while updating TPA id from Not in Nhance data.',
'data' => [],
];
}
}
// not in use once all functionality workes well in this funciton then remvoe this function
function compareDbWithTpa(array $db, array $tpaRows): array

View File

@ -784,6 +784,8 @@ class EmployeeServiceController extends AdminController
//get file name
// check_dob_diff('4-APr-1990');die();
$file_id = $params['file_id'];
$batch_file_id = $params['batch_file_id'] ?? null;
$file = $this->fileModel->find((int)$file_id);
// dd($file);
$return = [];
@ -1063,7 +1065,7 @@ class EmployeeServiceController extends AdminController
//proceed next data level validation in JOB queue
$job_details = new Jobs();
$r = Jobs::addJob(['job_name' => 'excelFileDataValidation','payload' => ['file_id' => $file_id]]);
$r = Jobs::addJob(['job_name' => 'excelFileDataValidation','payload' => ['file_id' => $file_id, 'batch_file_id' => $batch_file_id]]);
// $jobWorker = new JobWorker();
// JobWorker::processJob($r);
@ -1077,6 +1079,8 @@ class EmployeeServiceController extends AdminController
helper('excel_util_helper');
//get file name
$file_id = $params['file_id'];
$batch_file_id = $params['batch_file_id'] ?? null;
$file = $this->fileModel->find((int)$file_id);
// dd($file);
$return = [];
@ -1307,13 +1311,13 @@ class EmployeeServiceController extends AdminController
//proceed next data level validation in JOB queue
$job_details = new Jobs();
$r = Jobs::addJob(['job_name' => 'employeesCorrectionProcess','payload' => ['file_id' => $file_id]]);
$r = Jobs::addJob(['job_name' => 'employeesCorrectionProcess','payload' => ['file_id' => $file_id, 'batch_file_id' => $batch_file_id]]);
}
else if ($file['action'] == 'inception' || $file['action'] == 'missed_inception' || $file['action'] == 'addition' || $file['action'] == 'dependent_addition')//inception OR addition OR dependent addition
{
//proceed next data level validation in JOB queue
// $job_details = new Jobs();
$r = Jobs::addJob(['job_name' => 'employeesOnboardPreprocess','payload' => ['file_id' => $file_id]]);
$r = Jobs::addJob(['job_name' => 'employeesOnboardPreprocess','payload' => ['file_id' => $file_id, 'batch_file_id' => $batch_file_id]]);
// $jobWorker = new JobWorker();
// JobWorker::processJob($r);
}
@ -1333,166 +1337,183 @@ class EmployeeServiceController extends AdminController
{
helper('excel_util_helper');
// dd($params);
if(isset($params['file_id']))//handle data from excel to inception
if (isset($params['file_id'])) //handle data from excel to inception
{
//get file name
$file_id = $params['file_id'];
$file = $this->fileModel->find((int)$file_id);
// dd($file);
$return = [];
if(!isset($file))
{
//file not found in DB
return array('status' => false, 'msg' => 'file not found in DB');
}
$file_name_with_path = WRITEPATH."/uploads/excel/".$file['file_name'];
//check physical file
if(!file_exists($file_name_with_path))
{
//file not found update status and reason
$message = "Physical file not found";
// echo $message;
$this->myLogger->logme('error',($message . ' for file id ' . $file_id));
$this->fileModel->where('id', $file_id)->set(['status' => 'failed','reason' => json_encode(['error_summary' => array_count_values([5]),'error_data' => $message])])->update();
return array('error_summary' => [5], 'error_data' => $message);
}
//get file name
$file_id = $params['file_id'];
$file = $this->fileModel->find((int)$file_id);
// dd($file);
$return = [];
if (!isset($file)) {
//file not found in DB
return array('status' => false, 'msg' => 'file not found in DB');
}
$file_name_with_path = WRITEPATH . "/uploads/excel/" . $file['file_name'];
$columns_to_check = [];
if($file['action'] == 'inception'){ $columns_to_check = $this->inception_excel_columns; }
if($file['action'] == 'addition'){ $columns_to_check = $this->inception_excel_columns; }
if($file['action'] == 'dependent_addition'){ $columns_to_check = $this->inception_excel_columns; }
if($file['action'] == 'deletion'){ $columns_to_check = $this->deletion_excel_columns; }
if($file['action'] == 'correction'){ $columns_to_check = $this->correction_excel_columns; }
if($file['action'] == 'si_enhancement'){ $columns_to_check = $this->si_enhance_excel_columns; }
if($file['action'] == 'missed_inception'){ $columns_to_check = $this->inception_excel_columns; }
//check physical file
if (!file_exists($file_name_with_path)) {
//file not found update status and reason
$message = "Physical file not found";
// echo $message;
$this->myLogger->logme('error', ($message . ' for file id ' . $file_id));
$this->fileModel->where('id', $file_id)->set(['status' => 'failed', 'reason' => json_encode(['error_summary' => array_count_values([5]), 'error_data' => $message])])->update();
return array('error_summary' => [5], 'error_data' => $message);
}
$current_column_action = null;
if($file['action'] == 'inception'){ $current_column_action = 'I'; }
else if($file['action'] == 'addition'){ $current_column_action = 'A'; }
else if($file['action'] == 'dependent_addition'){ $current_column_action = 'DA'; }
else if($file['action'] == 'deletion'){ $current_column_action = 'D'; }
else if($file['action'] == 'correction'){ $current_column_action = 'C'; }
else if($file['action'] == 'si_enhancement'){ $current_column_action = 'SI'; }
else if($file['action'] == 'enrollment'){ $current_column_action = 'I'; }
else if($file['action'] == 'missed_inception'){ $current_column_action = 'MI'; }
$columns_to_check = [];
if ($file['action'] == 'inception') {
$columns_to_check = $this->inception_excel_columns;
}
if ($file['action'] == 'addition') {
$columns_to_check = $this->inception_excel_columns;
}
if ($file['action'] == 'dependent_addition') {
$columns_to_check = $this->inception_excel_columns;
}
if ($file['action'] == 'deletion') {
$columns_to_check = $this->deletion_excel_columns;
}
if ($file['action'] == 'correction') {
$columns_to_check = $this->correction_excel_columns;
}
if ($file['action'] == 'si_enhancement') {
$columns_to_check = $this->si_enhance_excel_columns;
}
if ($file['action'] == 'missed_inception') {
$columns_to_check = $this->inception_excel_columns;
}
// get policy and rack details
$policy_terms = $this->clientPolicyModel->getPolicyDetails($file['client_id'],$file['policy_id']);
$policy_terms = (array) $policy_terms[0];// convert obj to array
// $policy_terms = json_decode($policy_terms[0]->policy_terms);
// dd($policy_terms);
//get excel data
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
$sheet = $spreadsheet->getActiveSheet();
$highestRowAndColumn = $sheet->getHighestRowAndColumn();
$allowedHighestColumn = end($columns_to_check);
// dd($allowedHighestColumn);
$excel_data = $sheet->rangeToArray('A1:' . $allowedHighestColumn['col_cell_name'] . $highestRowAndColumn['row']);
unset($excel_data[0]);
$current_column_action = null;
if ($file['action'] == 'inception') {
$current_column_action = 'I';
} else if ($file['action'] == 'addition') {
$current_column_action = 'A';
} else if ($file['action'] == 'dependent_addition') {
$current_column_action = 'DA';
} else if ($file['action'] == 'deletion') {
$current_column_action = 'D';
} else if ($file['action'] == 'correction') {
$current_column_action = 'C';
} else if ($file['action'] == 'si_enhancement') {
$current_column_action = 'SI';
} else if ($file['action'] == 'enrollment') {
$current_column_action = 'I';
} else if ($file['action'] == 'missed_inception') {
$current_column_action = 'MI';
}
// get policy and rack details
$policy_terms = $this->clientPolicyModel->getPolicyDetails($file['client_id'], $file['policy_id']);
$policy_terms = (array) $policy_terms[0]; // convert obj to array
// $policy_terms = json_decode($policy_terms[0]->policy_terms);
//get policy slab rates
$slab_details = $this->policiesModel->getPolicySlabRatesForEmpOnboard($file['policy_id'],$file['client_id']);
// dd($slab_details);
//get existing units in the current branch
$existing_units = $this->clientBranchModel->getExisitingUnits(client_id: $file['client_id'],client_branch_id: $file['client_branch_id']);
// dd($policy_terms);
//get excel data
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
$sheet = $spreadsheet->getActiveSheet();
$employee_data_group_by_family = data_group_by_family($excel_data, 'excel', '', $current_column_action);
// dd($employee_data_group_by_family);
$highestRowAndColumn = $sheet->getHighestRowAndColumn();
$allowedHighestColumn = end($columns_to_check);
// dd($allowedHighestColumn);
$excel_data = $sheet->rangeToArray('A1:' . $allowedHighestColumn['col_cell_name'] . $highestRowAndColumn['row']);
unset($excel_data[0]);
// dd($excel_data);
$employee_insert_count = 0;
foreach ($employee_data_group_by_family as $emp_id => $family)
{
//get policy slab rates
$slab_details = $this->policiesModel->getPolicySlabRatesForEmpOnboard($file['policy_id'], $file['client_id']);
// dd($slab_details);
//get existing units in the current branch
$existing_units = $this->clientBranchModel->getExisitingUnits(client_id: $file['client_id'], client_branch_id: $file['client_branch_id']);
//if action is DA then get all familiy members,transfrom them into excel array, addd data source as db or excel
if($file['action'] == 'dependent_addition')
{
$existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(emp_code: $emp_id,client_id: $file['client_id'],client_policy_id: $file ['policy_id'],emp_status: ['active'],policy_status:['active'],client_branch_id: [ $file['client_branch_id'] ]);
// dd($existing_famility_details);
//transsform familiy details from db columns to exxcel row with column indexs for checking remaining functionalitites
$existing_famility_details = transform_db_data_to_excel($existing_famility_details,$file);
// Kint::dump($existing_famility_details);
$family = array_merge($family,$existing_famility_details);
$family = data_group_by_family($family, 'excel', 1)[ $emp_id ];// reason to call this again is bring self to first index of the array
// dd($family);
$self = current(array_filter($family, fn($r) => strtolower($r[5] ?? '') === 'self'));
$premium = (int)($self['temp']['rata_premimum'] ?? 0);
foreach ($family as &$r) if (strtolower($r[5] ?? '') !== 'self') $r['self_rata_premium'] = $premium;
}
$employee_data_group_by_family = data_group_by_family($excel_data, 'excel', '', $current_column_action);
// dd($employee_data_group_by_family);
// Kint::dump($family);
$data = calculate_premium_new(family_data:$family,policy_terms: $policy_terms,slab_details : $slab_details,fileArr: $file, existing_units:$existing_units);
// dd($data);
$employee_data_group_by_family[$emp_id] = $data;
$res = $this->employeesOnboardProcess(['familiy_data' => $data,'file' => $file]);
$employee_insert_count = 0;
foreach ($employee_data_group_by_family as $emp_id => $family) {
if($res > 0){
$employee_insert_count++;
}
}
//if action is DA then get all familiy members,transfrom them into excel array, addd data source as db or excel
if ($file['action'] == 'dependent_addition') {
$existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(emp_code: $emp_id, client_id: $file['client_id'], client_policy_id: $file['policy_id'], emp_status: ['active'], policy_status: ['active'], client_branch_id: [$file['client_branch_id']]);
// dd($existing_famility_details);
//transsform familiy details from db columns to exxcel row with column indexs for checking remaining functionalitites
$existing_famility_details = transform_db_data_to_excel($existing_famility_details, $file);
// Kint::dump($existing_famility_details);
$family = array_merge($family, $existing_famility_details);
$family = data_group_by_family($family, 'excel', 1)[$emp_id]; // reason to call this again is bring self to first index of the array
// dd($family);
$self = current(array_filter($family, fn($r) => strtolower($r[5] ?? '') === 'self'));
$premium = (int)($self['temp']['rata_premimum'] ?? 0);
foreach ($family as &$r) if (strtolower($r[5] ?? '') !== 'self') $r['self_rata_premium'] = $premium;
}
if($employee_insert_count > 0){
$this->fileModel->where('id', $file_id)->set(['status' => 'success','reason' => ''])->update();
$this->myLogger->logme("error",'{file_id} uploaded success',['file_id' => $file_id]);
}else{
$reason = json_encode([
'error_summary' => [5 => 1],
'error_data' => "Rack rate configuration issue: Please check the slab rates configuration"
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->fileModel->where('id', $file_id)->set(['status' => 'failed','reason' => $reason])->update();
$this->myLogger->logme("error",'{file_id} uploaded failed with reason: Rack rate configuration issue: Please check the slab rates configuration',['file_id' => $file_id]);
// Kint::dump($family);
$data = calculate_premium_new(family_data: $family, policy_terms: $policy_terms, slab_details: $slab_details, fileArr: $file, existing_units: $existing_units);
// dd($data);
$employee_data_group_by_family[$emp_id] = $data;
// $res = $this->employeesOnboardProcess(['familiy_data' => $data, 'file' => $file]);
}
// if ($res > 0) {
// $employee_insert_count++;
// }
}
$this->setPullNotification($this->getFileMetaDataByFileId($file_id,'success'));
// if ($employee_insert_count > 0) {
// $this->fileModel->where('id', $file_id)->set(['status' => 'success', 'reason' => ''])->update();
// $this->myLogger->logme("error", '{file_id} uploaded success', ['file_id' => $file_id]);
// } else {
// $reason = json_encode([
// 'error_summary' => [5 => 1],
// 'error_data' => "Rack rate configuration issue: Please check the slab rates configuration"
// ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// $this->fileModel->where('id', $file_id)->set(['status' => 'failed', 'reason' => $reason])->update();
// $this->myLogger->logme("error", '{file_id} uploaded failed with reason: Rack rate configuration issue: Please check the slab rates configuration', ['file_id' => $file_id]);
// }
}
else if(isset($params['client_policy_id']))//handle data from enrollment to inception
{
// $this->setPullNotification($this->getFileMetaDataByFileId($file_id, 'success'));
} else if (isset($params['client_policy_id'])) //handle data from enrollment to inception
{
$client_policy_id = $params['client_policy_id'];
//get client id
$client_id = ($this->clientPolicyModel->select('client_id')->find((int)$client_policy_id))['client_id'];
// dd($client_id);
// get policy and rack details
$policy_terms = $this->clientPolicyModel->getPolicyDetails($client_id,$client_policy_id);
$policy_terms = (array) $policy_terms[0];// convert obj to array
$policy_terms = $this->clientPolicyModel->getPolicyDetails($client_id, $client_policy_id);
$policy_terms = (array) $policy_terms[0]; // convert obj to array
//get policy slab rates
$slab_details = $this->policiesModel->getPolicySlabRatesForEmpOnboard($client_policy_id,$client_id);
$slab_details = $this->policiesModel->getPolicySlabRatesForEmpOnboard($client_policy_id, $client_id);
$existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id,client_policy_id: $client_policy_id,emp_status: ['enrolled'],policy_status:['enrolled']);
$existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id, client_policy_id: $client_policy_id, emp_status: ['enrolled'], policy_status: ['enrolled']);
// $file = ['id' => null,'client_id' => $client_id,'policy_id' => $client_policy_id,'action' => 'inception'];
$file = ['id' => null,'client_id' => $client_id,'policy_id' => $client_policy_id,'action' => 'inception','created_by' => get_session_userid(),'client_branch_id' => $params['client_branch_id']];
$file = ['id' => null, 'client_id' => $client_id, 'policy_id' => $client_policy_id, 'action' => 'inception', 'created_by' => get_session_userid(), 'client_branch_id' => $params['client_branch_id']];
//get existing units in the current branch
$existing_units = $this->clientBranchModel->getExisitingUnits(client_id: $file['client_id'],client_branch_id: $file['client_branch_id']);
$existing_units = $this->clientBranchModel->getExisitingUnits(client_id: $file['client_id'], client_branch_id: $file['client_branch_id']);
// dd($this->employeeModel->getLastQuery());
// Kint::dump($existing_famility_details);die();
$employee_data_group_by_family = data_group_by_family($existing_famility_details,$data_source = 'db');
// dd($employee_data_group_by_family);
foreach ($employee_data_group_by_family as $emp_id => $family)
{
$employee_data_group_by_family = data_group_by_family($existing_famility_details, $data_source = 'db');
// dd($employee_data_group_by_family);
foreach ($employee_data_group_by_family as $emp_id => $family) {
$transformed_famility_details = transform_db_data_to_excel($family);
// $data = calculate_premium_new($transformed_famility_details,$policy_terms,$slab_details,$file);
$data = calculate_premium_new(family_data:$transformed_famility_details,policy_terms: $policy_terms,slab_details : $slab_details,fileArr: $file, existing_units:$existing_units);
// dd($data);
$employee_data_group_by_family[$emp_id] = $data;
$this->employeesOnboardProcess(['familiy_data' => $data,'file' => $file]);
}
// $data = calculate_premium_new($transformed_famility_details,$policy_terms,$slab_details,$file);
$data = calculate_premium_new(family_data: $transformed_famility_details, policy_terms: $policy_terms, slab_details: $slab_details, fileArr: $file, existing_units: $existing_units);
// dd($data);
$employee_data_group_by_family[$emp_id] = $data;
$this->employeesOnboardProcess(['familiy_data' => $data, 'file' => $file]);
}
// dd($employee_data_group_by_family);
}
// dd($employee_data_group_by_family);
}
if(count($employee_data_group_by_family ?? []) && isset($params['batch_file_id'])){
$r = Jobs::addJob(['job_name' => 'updateEmployeeDataFromTpa','payload' => ['file_id' => $file_id, 'batch_file_id' => $params['batch_file_id']]]);
$this->myLogger->logme("info", 'Batch file id {batch_file_id} processed successfully', ['batch_file_id' => $params['batch_file_id']]);
}
// die();
return (count($employee_data_group_by_family));
}
//deletion of emp
@ -1652,6 +1673,7 @@ class EmployeeServiceController extends AdminController
helper('excel_util_helper');
//get file name
$file_id = $params['file_id'];
$batch_file_id = $params['batch_file_id'] ?? null;
$file = $this->fileModel->find((int)$file_id);
// dd($file);
@ -1718,6 +1740,12 @@ class EmployeeServiceController extends AdminController
}
if(!empty($batch_file_id)){
$r = Jobs::addJob(['job_name' => 'updateEmployeeDataFromTpa','payload' => ['file_id' => $file_id, 'batch_file_id' => $batch_file_id]]);
$this->myLogger->logme("error", 'Batch file id {batch_file_id} processed successfully', ['batch_file_id' => $batch_file_id]);
}
$this->fileModel->where('id', $file_id)->set(['status' => 'success','reason' => ''])->update();
$this->myLogger->logme("error",'{file_id} uploaded success',['file_id' => $file_id]);
//set success msg to pull notifications

File diff suppressed because it is too large Load Diff

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',
@ -254,6 +258,26 @@ class JobWorker extends AdminController
'ICICIPushEmployeeDetails' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\ICICILombardController',
],
'getEnrollmentBatchStatus' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\ICICILombardController',
],
'fetchUHIDDetails' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\ICICILombardController',
],
'saveICICILombardAPIData' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\ICICILombardController',
],
'updateEmployeeDataFromTpa' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\EmployeeController',
],
'updateTpaIdForNotInNhance' => [
'type' => 'CC', // Handler Category
'handler' => 'App\Controllers\EmployeeController',
]
];

View File

@ -506,7 +506,7 @@ class LeadsController extends BaseController
'errors' => ['required' => 'Claim Year is required for all entries.', 'regex_match' => 'Year must be in format YYYY-YYYY.'],
];
$rules['first_policy_type_.*'] = [
'rules' => 'required|regex_match[/^[a-zA-Z0-9_-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
'errors' => ['required' => 'Policy Type is required in Claim History.',
'regex_match' => 'Policy Type only letters, numbers, space, hyphens and underscores are allowed',
],
@ -517,7 +517,7 @@ class LeadsController extends BaseController
'regex_match' => 'Date of Loss must be inValid format.'],
];
$rules['first_cause_of_loss.*'] = [
'rules' => 'required|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
'errors' => [
'required' => 'Cause of Loss is required.',
'regex_match' => 'Cause of Loss only letters, numbers, space, hyphens and underscores are allowed',
@ -6802,67 +6802,79 @@ class LeadsController extends BaseController
public function handleMemberDataGPATotalSumInsurerFromExcel($params)
{
$lead_id = $params['lead_id'];
$lead_data = $this->leadsModel->find((int) $lead_id);
// dd($lead_data);
$file_name_with_path = WRITEPATH . "/uploads/lead_files/" . $lead_data['file_name'];
// dd($file_name_with_path);
try {
$lead_id = $params['lead_id'];
$lead_data = $this->leadsModel->find((int) $lead_id);
if (! $lead_data) {
return ['status' => 'failed', 'message' => 'Opportunity data not found'];
}
if ($lead_data['file_name']) {
//check physical file
if (! file_exists($file_name_with_path)) {
$message = "Lead Physcial file not found";
$this->myLogger->logme('error', ($message . ' for file ' . $file_name_with_path));
return ['status' => 'failed', 'message' => 'no physical file'];
if (! $lead_data) {
return ['status' => 'failed', 'message' => 'Opportunity data not found'];
}
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
$file_name_with_path = WRITEPATH . "/uploads/lead_files/" . $lead_data['file_name'];
//get members data
$members_sheet = $spreadsheet->getSheet(0);
$highestRowAndColumn = $members_sheet->getHighestRowAndColumn();
// dd($highestRowAndColumn);
if ($lead_data['file_name']) {
$uncleaned_members = $members_sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']);
// dd($uncleaned_members);
//check physical file
if (! file_exists($file_name_with_path)) {
$members = ExcelSanitizeHelper::sanitizeArrayData($uncleaned_members);
// dd($members);
// Check column headings
$members_heading = $members[0];
$available_col = [];
$lower_headers = array_map('strtolower', $members_heading);
foreach ($lower_headers as $index => $header) {
if (preg_match('/^(sa\s*-\s*option|proposed\s+sum\s+insured)\s+\d+$/i', $header)) {
$column_name = $members_heading[$index];
$sum = 0;
for ($i = 1; $i < count($members); $i++) {
$cell_raw = $members[$i][$index] ?? '';
$cell_clean = preg_replace('/[^0-9.\-]/', '', $cell_raw); // remove non-numeric chars
if ($cell_clean !== '' && is_numeric($cell_clean)) {
$sum += (float) $cell_clean;
}
}
$available_col[] = $sum;
$message = "Lead Physcial file not found";
$this->myLogger->logme('error', ($message . ' for file ' . $file_name_with_path));
return ['status' => 'failed', 'message' => 'no physical file'];
}
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
//get members data
$members_sheet = $spreadsheet->getSheet(0);
$highestRowAndColumn = $members_sheet->getHighestRowAndColumn();
// Read raw values and skip formula evaluation to avoid Calculation exceptions from malformed formulas.
$uncleaned_members = $members_sheet->rangeToArray(
'A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row'],
null,
false,
true,
false
);
$members = ExcelSanitizeHelper::sanitizeArrayData($uncleaned_members);
// Check column headings
$members_heading = $members[0];
$available_col = [];
$lower_headers = array_map('strtolower', $members_heading);
foreach ($lower_headers as $index => $header) {
if (preg_match('/^(sa\s*-\s*option|proposed\s+sum\s+insured)\s+\d+$/i', $header)) {
$sum = 0;
for ($i = 1; $i < count($members); $i++) {
$cell_raw = $members[$i][$index] ?? '';
$cell_clean = preg_replace('/[^0-9.\-]/', '', $cell_raw); // remove non-numeric chars
if ($cell_clean !== '' && is_numeric($cell_clean)) {
$sum += (float) $cell_clean;
}
}
$available_col[] = $sum;
}
}
return $available_col;
}
// dd($available_col);
return $available_col;
}
return [];
} catch (\Throwable $e) {
log_message('error', 'Exception in handleMemberDataGPATotalSumInsurerFromExcel: {message}', [
'message' => $e->getMessage(),
]);
log_message('error', 'Trace: {trace}', [
'trace' => $e->getTraceAsString(),
]);
return [];
return [];
}
}
public function generateDemographyDataTable($param = [])

View File

@ -420,7 +420,7 @@ class MasterController extends AdminController
'state' => [
'label' => 'State',
'rules' => 'required|regex_match[/^[a-zA-Z\s\-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-\_\.\s]+$/]',
'errors' => [
'required' => 'State is required.',
'regex_match' => 'State name can only contain letters, spaces, and hyphens.'
@ -661,7 +661,7 @@ class MasterController extends AdminController
'state' => [
'label' => 'State',
'rules' => 'required|regex_match[/^[a-zA-Z\s\-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-\_\.\s]+$/]',
'errors' => [
'required' => 'State is required.',
'regex_match' => 'State name can only contain letters, spaces, and hyphens.'
@ -1057,7 +1057,7 @@ class MasterController extends AdminController
'state' => [
'label' => 'State',
'rules' => 'required|regex_match[/^[a-zA-Z\s\-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-\_\.\s]+$/]',
'errors' => [
'required' => 'State is required.',
'regex_match' => 'State name can only contain letters, spaces, and hyphens.'
@ -1370,7 +1370,7 @@ class MasterController extends AdminController
'state' => [
'label' => 'State',
'rules' => 'required|regex_match[/^[a-zA-Z\s\-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-\_\.\s]+$/]',
'errors' => [
'required' => 'State is required.',
'regex_match' => 'State name can only contain letters, spaces, and hyphens.'
@ -3590,7 +3590,7 @@ class MasterController extends AdminController
],
'state' => [
'label' => 'State',
'rules' => 'required|regex_match[/^[a-zA-Z\s\-]+$/]',
'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-\_\.\s]+$/]',
'errors' => [
'required' => 'State is required.',
'regex_match' => 'State name can only contain letters, spaces, and hyphens.'

View File

@ -845,6 +845,7 @@ class MediAssistApiController extends BaseController
tm.id,
tm.tpa_no as memberId,
tm.tpa_claim_push_reference_no as claimRefNo,
tm.doa,
cp.policy_no as policyNo,
cp.policy_start_date as startDate,
cp.policy_end_date as endDate,

View File

@ -10,6 +10,7 @@ use App\Models\BatchFileModel;
use App\Models\InsurerBranchModel;
use App\Models\RFQModel;
use App\Models\EmployeeModel;
use App\Libraries\JobStatusService;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\API\ResponseTrait;
use Dompdf\Dompdf;
@ -48,6 +49,110 @@ class TestingController extends BaseController
]);
}
/**
* Get latest job status by job name.
* Supports:
* - GET -> ?job_name=...
* - POST -> job_name in form/json payload
*/
public function jobStatus()
{
$jobName = trim((string) ($this->request->getGet('job_name') ?? ''));
if ($jobName === '') {
$jobName = trim((string) ($this->request->getPost('job_name') ?? ''));
}
if ($jobName === '') {
$jsonPayload = $this->request->getJSON(true);
if (is_array($jsonPayload)) {
$jobName = trim((string) ($jsonPayload['job_name'] ?? ''));
}
}
$service = new JobStatusService();
$result = $service->getJobStatusByName($jobName);
$httpCode = 200;
if (($result['success'] ?? false) !== true) {
if ($jobName === '') {
$httpCode = 422;
} elseif (($result['message'] ?? '') === 'No job record found for given name.') {
$httpCode = 404;
} else {
$httpCode = 500;
}
}
return $this->response->setStatusCode($httpCode)->setJSON($result);
}
/**
* QA-only: invokes EmployeeController::updateTpaIdForNotInNhance.
* Routed under /util with authMVC must be logged into the admin app.
*
* GET or POST: batch_file_id (required), file_id (optional, omit or 0 to skip file_id filter).
*/
public function qaUpdateTpaIdForNotInNhance()
{
$batchFileId = $this->request->getGet('batch_file_id');
if ($batchFileId === null || $batchFileId === '') {
$batchFileId = $this->request->getPost('batch_file_id');
}
$fileId = $this->request->getGet('file_id');
if ($fileId === null || $fileId === '') {
$fileId = $this->request->getPost('file_id');
}
if ($batchFileId === null || $batchFileId === '') {
return $this->response->setStatusCode(422)->setJSON([
'success' => false,
'message' => 'batch_file_id is required (query string or POST).',
]);
}
$params = [
'batch_file_id' => (int) $batchFileId,
'file_id' => ($fileId !== null && $fileId !== '') ? (int) $fileId : 0,
];
$employeeController = new EmployeeController();
$employeeController->initController($this->request, $this->response, service('logger'));
$result = $employeeController->updateTpaIdForNotInNhance($params);
return $this->response->setJSON(array_merge($result, ['params' => $params]));
}
/**
* QA-only: invokes EmployeeController::updateEmployeeDataFromTpa (Need to Review DB updates, no Excel).
* Routed under /util with authMVC.
*
* GET or POST: batch_file_id (required).
*/
public function qaUpdateEmployeeDataFromTpa()
{
$batchFileId = $this->request->getGet('batch_file_id');
if ($batchFileId === null || $batchFileId === '') {
$batchFileId = $this->request->getPost('batch_file_id');
}
if ($batchFileId === null || $batchFileId === '') {
return $this->response->setStatusCode(422)->setJSON([
'success' => false,
'message' => 'batch_file_id is required (query string or POST).',
]);
}
$employeeController = new EmployeeController();
$employeeController->initController($this->request, $this->response, service('logger'));
$result = $employeeController->updateEmployeeDataFromTpa([
'batch_file_id' => (int) $batchFileId,
]);
return $this->response->setJSON($result);
}
public function testcli()
{
echo "hi";
@ -1305,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:
@ -1609,4 +1714,48 @@ class TestingController extends BaseController
],
];
}
public function getVidalEnrollmentInfo()
{
helper('api');
$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_API_SUBSCRIPTION_KEY in env',
]);
}
$policyNo = '000/VZXSY';
$startIndex = 1;
$endIndex = 5;
$body = [
'policyNo' => $policyNo,
'startIndex' => $startIndex,
'endIndex' => $endIndex,
];
$headers = [
'Content-Type: application/json',
'ocp-apim-subscription-key: ' . $subscriptionKey,
];
$method = "POST";
$rawResponse = call_third_party_api($url, $method, $headers, $body);
return $this->response->setStatusCode(200)->setJSON([
'request' => [
'url' => $url,
'method' => $method,
'headers' => $headers,
'body' => $body,
],
'raw_response' => $rawResponse,
]);
}
}

View File

@ -576,7 +576,7 @@ class TicketController extends BaseController
->where('tm.is_active', 1)
->whereNotIn('claim_status_id', [11, 12, 13, 22, 24, 32, 34, 42, 44, 47, 53, 58, 65])
->orderBy('tm.id', 'DESC')
->limit(100);
->limit(500);
$data = $query->get()->getResultArray();
@ -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

@ -19,6 +19,14 @@ class VidalApiController extends BaseController
protected $claim_type_array;
protected $ticketController;
/**
* Vidal `relation` (lowercase) Nhance relation (lowercase).
* Populated once from {@see self::vidalRelationshipReferenceMap()} (values match public/tmp/relationship.csv; that file is reference only, not read at runtime).
*
* @var array<string, string>
*/
protected array $vidalRelationshipMap = [];
public function __construct()
{
$this->db = \Config\Database::connect();
@ -26,6 +34,8 @@ class VidalApiController extends BaseController
$this->ticketController = new TicketController();
$this->claim_type_array = $this->ticketController->claimType;
$this->vidalRelationshipMap = self::vidalRelationshipReferenceMap();
}
@ -455,6 +465,7 @@ class VidalApiController extends BaseController
tm.tpa_no as memberId,
tm.tpa_claim_push_reference_no as claimRefNo,
tm.tpa_claim_id as claimID,
tm.doa,
cp.policy_no as policyNo,
cp.policy_start_date as startDate,
cp.policy_end_date as endDate,
@ -1022,6 +1033,522 @@ class VidalApiController extends BaseController
}
}
public function VidalGetBenefDetailsV2($requestData = null)
{
$requestData = is_array($requestData) ? $requestData : [];
$function_calling_type = $requestData['return_type'] ?? 'job';
try {
helper('api');
$url = $this->vidalEnrollmentInfoApiUrl();
$method = 'POST';
$subscriptionKey = getenv('VIDAL_API_SUBSCRIPTION_KEY');
if (empty($subscriptionKey)) {
log_message('error', 'VIDAL V2 - TPA ID Pull | VIDAL_API_SUBSCRIPTION_KEY missing');
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'VIDAL_API_SUBSCRIPTION_KEY required'];
}
return $this->respond(['status' => false, 'message' => 'VIDAL_API_SUBSCRIPTION_KEY required']);
}
$headers = [
'Content-Type: application/json',
'ocp-apim-subscription-key: ' . $subscriptionKey,
];
$policyNo = $requestData['policy_no'] ?? null;
$client_policy_id = $requestData['client_policy_id'] ?? null;
if (empty($policyNo)) {
log_message('error', 'VIDAL V2 - TPA ID Pull | policy_no missing in request');
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'policy_no required'];
}
return $this->respond(['status' => false, 'message' => 'policy_no required']);
}
if (empty($client_policy_id)) {
log_message('error', 'VIDAL V2 - TPA ID Pull | client_policy_id missing in request');
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'client_policy_id required'];
}
return $this->respond(['status' => false, 'message' => 'client_policy_id required']);
}
log_message('error', "VIDAL V2 - TPA ID Pull | called for policy_no: {$policyNo} , client_policy_id: {$client_policy_id}");
$batchFiles = $this->db->table('batch_files f')
->select('f.created_at')
->where('f.client_policy_id', $client_policy_id)
->where('f.insurer_or_tpa', 'tpa')
->where('f.actions', 'export')
->get()
->getResultArray();
if (empty($batchFiles)) {
log_message('error', 'VIDAL V2 - TPA ID Pull FAILED | batchFiles is empty for this tpa id pull request');
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'batchFiles not found'];
}
return $this->respond(['status' => false, 'message' => 'batchFiles not found']);
}
$pageSize = 100;
$startIndex = 1;
$allBenef = [];
while (true) {
$endIndex = $startIndex + $pageSize - 1;
$body = [
'policyNo' => $policyNo,
'startIndex' => $startIndex,
'endIndex' => $endIndex,
];
log_message('error', 'VIDAL V2 - TPA ID Pull | API params ' . json_encode([$url, $method, $headers, $body]));
$response = call_third_party_api($url, $method, $headers, $body);
if ($response['status'] !== true) {
if (isset($requestData['file_id']) && !empty($requestData['file_id'])) {
$file_model = new BatchFileModel();
$file_model->where('id', $requestData['file_id'])->set('status', 'failed-8')->update();
log_message('error', "VIDAL V2 - TPA ID Pull | Files table status updated for the file id : {$requestData['file_id']}");
} else {
log_message('error', 'VIDAL V2 - TPA ID Pull | Failed to update file table status.');
}
log_message('error', 'VIDAL V2 - TPA ID Pull API FAILED | API failed: ' . json_encode($response));
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'API call failed', 'data' => $response];
}
return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => $response]);
}
$apiRoot = $response['data'] ?? [];
if (($apiRoot['status'] ?? '') !== 'SUCCESS'
|| (array_key_exists('successful', $apiRoot) && $apiRoot['successful'] === false)) {
log_message('error', 'VIDAL V2 - TPA ID Pull API FAILED | envelope: ' . json_encode($apiRoot));
if (isset($requestData['file_id']) && !empty($requestData['file_id'])) {
$file_model = new BatchFileModel();
$file_model->where('id', $requestData['file_id'])->set('status', 'failed-8')->update();
}
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'API call failed', 'data' => $apiRoot];
}
return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => $apiRoot]);
}
$chunk = $apiRoot['data'] ?? null;
if (!is_array($chunk)) {
log_message('error', 'VIDAL V2 - TPA ID Pull FAILED | data is not an array: ' . json_encode($apiRoot));
break;
}
if (count($chunk) === 0) {
if ($startIndex === 1) {
log_message('error', "VIDAL V2 - TPA ID Pull FAILED | empty data on first page");
if (isset($requestData['file_id']) && !empty($requestData['file_id'])) {
$file_model = new BatchFileModel();
$file_model->where('id', $requestData['file_id'])->set('status', 'failed-8')->update();
}
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'No enrollment records returned', 'data' => $apiRoot];
}
return $this->respond(['status' => false, 'message' => 'No enrollment records returned', 'data' => $apiRoot]);
}
break;
}
foreach ($chunk as $rec) {
if (is_array($rec)) {
$allBenef[] = $this->normalizeVidalEnrollmentRecordToDependentFormat($rec);
}
}
log_message('error', 'VIDAL V2 - TPA ID Pull | Fetched ' . count($chunk) . ' records (page startIndex=' . $startIndex . ')');
if (count($chunk) < $pageSize) {
break;
}
$startIndex += $pageSize;
}
$json = json_encode($allBenef, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$filePath = WRITEPATH . 'tmp/' . time() . '_' . $requestData['file_id'] . '.json';
file_put_contents($filePath, $json);
Jobs::addJob(['job_name' => 'saveVidalAPIData', 'payload' => ['file_id' => $requestData['file_id'], 'json_file_path' => $filePath]]);
$employeePolicyModel = new EmployeePolicyModel();
$employeePolicyData = $employeePolicyModel
->select('
employees.*,
employee_polices.id as emp_policy_id,
employee_polices.client_policy_id,
')
->join('employees', 'employees.id = employee_polices.employee_id')
->where('employee_polices.is_active', 1)
->where('employee_polices.status', 'active')
->where('employees.is_active', 1)
->where('employees.emp_status', 'active')
->where('employee_polices.tpa_id IS NULL')
->where('employee_polices.client_policy_id', $client_policy_id)
->findAll();
$batch_file_success = 'success';
$updated = 0;
$totalCount = count($employeePolicyData);
$employee_policy_ids = [];
foreach ($employeePolicyData as $policy_data) {
$hasMatchForThisPolicy = false;
foreach ($allBenef as $row) {
if (
strtolower(trim($policy_data['name'] ?? '')) === strtolower(trim($row['name'] ?? '')) &&
($policy_data['emp_code'] ?? '') === ($row['empNo'] ?? '') &&
strtolower(trim($policy_data['relationship'] ?? '')) === strtolower(trim(str_replace('-', ' ', $row['relationship'] ?? ''))) &&
($policy_data['gender'] ?? '') === ($row['gender'] ?? '') &&
($policy_data['dob'] ?? '') === (change_date_format($row['dob'], 'Y-m-d H:i:s') ?? '')
) {
$hasMatchForThisPolicy = true;
$sql = 'UPDATE employee_polices
SET tpa_id = :tpa_id:
WHERE id = :emp_policy_id:';
$this->db->query($sql, ['tpa_id' => $row['enrollmentId'], 'emp_policy_id' => $policy_data['emp_policy_id']]);
if (strtolower(trim($policy_data['relationship'])) === 'self') {
$employee_policy_ids[] = $policy_data['emp_policy_id'];
}
if ($this->db->affectedRows() > 0) {
$updated++;
log_message('error', "VIDAL V2 - TPA ID Pull | Updated tpa_id={$row['enrollmentId']} for emp_code={$row['empNo']} policy={$row['policyNumber']}");
} else {
log_message('error', "VIDAL V2 - TPA ID Pull | No update (already set or not matched) for emp_code={$row['empNo']} policy={$row['policyNumber']}");
}
}
}
if (!$hasMatchForThisPolicy) {
$nhanceSideData = [
'name' => $policy_data['name'] ?? null,
'emp_code' => $policy_data['emp_code'] ?? null,
'relationship' => $policy_data['relationship'] ?? null,
'gender' => $policy_data['gender'] ?? null,
'dob' => $policy_data['dob'] ?? null,
];
$batch_file_success = 'partially success';
log_message(
'error',
'VIDAL V2 - TPA ID Pull | No match for Nhance = ' . json_encode($nhanceSideData)
);
}
}
if (!empty($employee_policy_ids)) {
log_message('error', 'sendMailForDownloadingECard JOB PUSHED (V2).');
Jobs::addJob(['job_name' => 'sendMailForDownloadingECard', 'payload' => ['ids' => $employee_policy_ids, 'client_policy_id' => $client_policy_id]]);
}
if (isset($requestData['file_id']) && !empty($requestData['file_id'])) {
$file_model = new BatchFileModel();
$file_model->where('id', $requestData['file_id'])->set('status', $batch_file_success)->update();
log_message('error', "VIDAL V2 - Files table status updated for the file id : {$requestData['file_id']}");
} else {
log_message('error', 'VIDAL V2 - Failed to update file table status.');
}
log_message('error', "VIDAL V2 - TPA ID Pull SUCCESS | completed. Total fetched={$totalCount}, updated={$updated}");
if ($function_calling_type === 'job') {
return [
'status' => true,
'message' => 'Updated successfully',
'total_fetched' => $totalCount,
'total_updated' => $updated,
];
}
return $this->respond([
'status' => true,
'message' => 'Updated successfully',
'total_fetched' => $totalCount,
'total_updated' => $updated,
]);
} catch (\Throwable $th) {
if (isset($requestData['file_id']) && !empty($requestData['file_id'])) {
$file_model = new BatchFileModel();
$file_model->where('id', $requestData['file_id'])->set('status', 'failed-8')->update();
log_message('error', "VIDAL V2 - Files table status updated for the file id : {$requestData['file_id']}");
} else {
log_message('error', 'VIDAL V2 - Failed to update file table status.');
}
$errorData = [
'message' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine(),
'code' => $th->getCode(),
'trace' => $th->getTraceAsString(),
'trace_array' => $th->getTrace(),
'function' => $th->getTrace()[0]['function'] ?? null,
'class' => $th->getTrace()[0]['class'] ?? null,
];
log_message('error', 'VIDAL V2 - Exception thrown while calling GetBenefDetailsV2 API: ' . json_encode($errorData));
if ($function_calling_type === 'job') {
return ['status' => false, 'message' => 'API call failed', 'data' => $errorData];
}
return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => $errorData]);
}
}
/**
* Nhance relationship => list of Vidal `relation` labels (any case). Edit the grouped list only; the flat map is built here.
* Reference: public/tmp/relationship.csv (documentation only).
*
* @return array<string, string> lowercase Vidal label (and hyphen/space variant) => Nhance relation key
*/
private static function vidalRelationshipReferenceMap(): array
{
$relationshipGrouped = [
'self' => [
'SELF',
'EMPLOYER',
'EMPLOYEE',
'EMPLOYEES',
],
'spouse' => [
'SPOUSE',
'PARTNER',
'HUSBAND',
'HUSBAND (2)',
'HUSBAND (3)',
'HUSBAND (4)',
'HUSBAND (5)',
'WIFE',
'WIFE (2)',
'WIFE (3)',
'WIFE (4)',
'WIFE (5)',
],
'father' => [
'FATHER',
'FATHER (2)',
'FATHER (3)',
'FATHER (4)',
'FATHER (5)',
],
'mother' => [
'MOTHER',
'MOTHER (2)',
'MOTHER (3)',
'MOTHER (4)',
'MOTHER (5)',
],
'son' => [
'SON',
'SON (2)',
'SON (3)',
'SON (4)',
'SON (5)',
],
'daughter' => [
'DAUGHTER',
'DAUGHTER (2)',
'DAUGHTER (3)',
'DAUGHTER (4)',
'DAUGHTER (5)',
],
'father in law' => [
'FATHER-IN-LAW',
'FATHER-IN-LAW (2)',
],
'mother in law' => [
'MOTHER-IN-LAW',
'MOTHER-IN-LAW (2)',
],
];
return self::flattenVidalRelationshipGroupedToLookupMap($relationshipGrouped);
}
/**
* @param array<string, list<string>> $grouped
*
* @return array<string, string>
*/
private static function flattenVidalRelationshipGroupedToLookupMap(array $grouped): array
{
$map = [];
foreach ($grouped as $nhanceRelation => $vidalLabels) {
foreach ($vidalLabels as $label) {
$label = trim((string) $label);
if ($label === '') {
continue;
}
$variants = [
strtolower($label),
strtolower(str_replace('-', ' ', $label)),
];
foreach (array_unique($variants) as $key) {
$key = trim(preg_replace('/\s+/', ' ', $key));
if ($key === '') {
continue;
}
$map[$key] = $nhanceRelation;
}
}
}
return $map;
}
/**
* Map Vidal enrollment `relation` text to Nhance `employees.relationship` / `tpa_api_data.relation` style (lowercase).
*/
private function mapVidalRelationshipToNhance(?string $vidalRelationDescription): string
{
$raw = trim((string) $vidalRelationDescription);
if ($raw === '') {
return '';
}
foreach (self::vidalRelationLookupKeyVariants($raw) as $key) {
if (isset($this->vidalRelationshipMap[$key])) {
return $this->vidalRelationshipMap[$key];
}
}
if (strcasecmp($raw, 'Employee') === 0 || strcasecmp($raw, 'Employees') === 0) {
return 'self';
}
return strtolower(str_replace('-', ' ', $raw));
}
/**
* Keys must stay in sync with {@see self::flattenVidalRelationshipGroupedToLookupMap()}.
*
* @return list<string>
*/
private static function vidalRelationLookupKeyVariants(string $raw): array
{
$base = strtolower(trim($raw));
$withHyphensAsSpaces = strtolower(str_replace('-', ' ', $base));
$collapsed = trim(preg_replace('/\s+/', ' ', $withHyphensAsSpaces));
return array_values(array_unique(array_filter([$base, $collapsed])));
}
private function normalizeVidalEnrollmentDateToYmd($value): ?string
{
if ($value === null || $value === '') {
return null;
}
if (is_numeric($value)) {
return null;
}
$s = trim((string) $value);
$ts = strtotime(str_replace('/', '-', $s));
return $ts ? date('Y-m-d', $ts) : null;
}
/**
* Map Enrollment Dump API row (see public/tmp/Enrollment Dump API.docx) to the dependent
* shape used by VidalGetBenefDetailsV2 matching and saveVidalAPIData.
*/
private function normalizeVidalEnrollmentRecordToDependentFormat(array $row): array
{
$rel = trim((string) ($row['relation'] ?? ''));
$relationship = $this->mapVidalRelationshipToNhance($rel);
$si = $row['baseSumInsured'] ?? null;
$si = $si !== null && $si !== '' ? trim((string) $si) : null;
return [
'name' => trim((string) ($row['beneficiaryName'] ?? '')),
'empNo' => trim((string) ($row['employeeNo'] ?? '')),
'relationship' => $relationship,
'vidal_relation_raw' => $rel,
'gender' => $row['gender'] ?? '',
'dob' => $row['dateOfBirth'] ?? null,
'enrollmentId' => trim((string) ($row['membershipNo'] ?? '')),
'policyNumber' => trim((string) ($row['policyNumber'] ?? '')),
'age' => $row['age'] ?? null,
'si' => $si,
'doj' => $this->normalizeVidalEnrollmentDateToYmd($row['dateOfJoining'] ?? null),
'desc' => $this->buildVidalEnrollmentDescForTpaRow($row),
];
}
/**
* Fills tpa_api_data.desc from enrollment fields (product / remarks / insured name).
*/
private function buildVidalEnrollmentDescForTpaRow(array $row): ?string
{
$chunks = array_filter([
trim((string) ($row['productName'] ?? '')),
trim((string) ($row['remarks'] ?? '')),
trim((string) ($row['insuredName'] ?? '')),
], static fn ($v) => $v !== '');
if ($chunks === []) {
return null;
}
return substr(implode(' | ', $chunks), 0, 65000);
}
private function vidalEnrollmentInfoApiUrl(): string
{
$base = rtrim((string) getenv('VIDAL_API_BASE_URL'), '/');
if ($base !== '' && preg_match('#/api$#', $base)) {
return preg_replace('#/api$#', '', $base) . '/enrollment/info';
}
return 'https://devapigw.vidalhealthtpa.com/partner-integration/enrollment/info';
}
public function saveVidalAPIData($array)
{
$file_id = $array['file_id'];
@ -1045,20 +1572,49 @@ class VidalApiController extends BaseController
foreach ($records as $row) {
$rawVidalRel = trim((string) ($row['vidal_relation_raw'] ?? ''));
if ($rawVidalRel !== '') {
$relation = $this->mapVidalRelationshipToNhance($rawVidalRel);
} else {
$relation = trim(strtolower((string) ($row['relationship'] ?? '')));
}
$si = $row['si'] ?? null;
$si = $si !== null && $si !== '' ? trim((string) $si) : null;
$doj = null;
if (!empty($row['doj'])) {
$dojRaw = $row['doj'];
if (is_string($dojRaw) && preg_match('/^\d{4}-\d{2}-\d{2}/', $dojRaw)) {
$doj = substr($dojRaw, 0, 10);
} else {
$doj = $this->normalizeVidalEnrollmentDateToYmd($dojRaw);
}
}
$descPieces = [];
if ($rawVidalRel !== '') {
$descPieces[] = 'Vidal relation: ' . $rawVidalRel;
}
$jsonDesc = trim((string) ($row['desc'] ?? ''));
if ($jsonDesc !== '') {
$descPieces[] = $jsonDesc;
}
$desc = $descPieces !== [] ? substr(implode(' | ', $descPieces), 0, 65000) : null;
$mappedRows[] = [
'file_id' => $file_id, // ← pass from controller
'emp_code' => trim($row['empNo'] ?? ''),
'name' => trim($row['name'] ?? ''),
'dob' => !empty($row['dob'] ?? null) ? date('Y-m-d', strtotime(str_replace('/', '-', $row['dob']))) : null,
'relation' => trim(strtolower($row['relationship'] ?? '')),
'gender' => format_gender_v2($row['gender'] ?? null),
'self' => strtolower($row['relationship'] ?? '') === 'self' ? 1 : 0,
'tpa_id' => trim($row['enrollmentId'] ?? null),
'age' => is_numeric($row['age'] ?? null) ? (int) $row['age'] : null,
'file_id' => $file_id,
'emp_code' => trim($row['empNo'] ?? ''),
'name' => trim($row['name'] ?? ''),
'dob' => !empty($row['dob'] ?? null) ? date('Y-m-d', strtotime(str_replace('/', '-', $row['dob']))) : null,
'relation' => $relation,
'gender' => format_gender_v2($row['gender'] ?? null),
'self' => $relation === 'self' ? 1 : 0,
'tpa_id' => trim($row['enrollmentId'] ?? null),
'age' => is_numeric($row['age'] ?? null) ? (int) $row['age'] : null,
'desc' => $desc,
'si' => $si,
'doj' => $doj,
'is_active' => 1,
'created_by' => $file_info[0]['created_by'] ?? null,
];

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

@ -0,0 +1,100 @@
<?php
namespace App\Libraries;
use App\Models\JobModel;
class JobStatusService
{
protected JobModel $jobModel;
public function __construct(?JobModel $jobModel = null)
{
$this->jobModel = $jobModel ?? new JobModel();
}
public function getJobStatusByName(string $jobName): array
{
$jobName = trim($jobName);
if ($jobName === '') {
return $this->buildResponse(false, $jobName, 'Job name is required.');
}
try {
$job = $this->jobModel
->where('name', $jobName)
->orderBy('id', 'DESC')
->first();
if (!$job) {
return $this->buildResponse(false, $jobName, 'No job record found for given name.');
}
return [
'success' => true,
'job_name' => $jobName,
'status' => $job['status'] ?? null,
'response' => $this->normalizeResponse($job['response'] ?? null),
'job_id' => isset($job['id']) ? (int) $job['id'] : null,
'uuid' => $job['uuid'] ?? null,
'run_time' => $this->normalizeRunTime($job['run_time'] ?? null),
'message' => 'Job status fetched successfully.',
];
} catch (\Throwable $e) {
log_message('error', 'JobStatusService failed for job "{job}": {message}', [
'job' => $jobName,
'message' => $e->getMessage(),
]);
return $this->buildResponse(false, $jobName, 'Unable to fetch job status right now.');
}
}
protected function normalizeResponse($rawResponse)
{
// dd($rawResponse);
if ($rawResponse === null) {
return null;
}
if (is_string($rawResponse)) {
$trimmed = trim($rawResponse);
if ($trimmed === '') {
return null;
}
$decoded = json_decode($trimmed, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
return $rawResponse;
}
return $rawResponse;
}
protected function normalizeRunTime($runTime)
{
if ($runTime === null || $runTime === '') {
return null;
}
return is_numeric($runTime) ? (float) $runTime : null;
}
protected function buildResponse(bool $success, string $jobName, string $message): array
{
return [
'success' => $success,
'job_name' => $jobName,
'status' => null,
'response' => null,
'job_id' => null,
'uuid' => null,
'run_time' => null,
'message' => $message,
];
}
}

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

@ -43,7 +43,7 @@
<div class="form-row additional-doc-row" data-row-index="0">
<div class="form-group col-md-4">
<label>Document Name<span class="text-danger">*</span></label>
<input type="text" class="form-control other-doc-name-field" placeholder="Document Name" name="other_docs_name[]" required>
<input type="text" class="form-control other-doc-name-field" placeholder="Document Name" name="other_docs_name[]" data-parsley-inceptioncharset="true" data-parsley-trigger="input change" required>
</div>
<div class="form-group col-md-4">
<label>File<span class="text-danger">*</span></label>
@ -92,6 +92,7 @@
</div>
<!-- end -->
<script src="<?= base_url('public/assets/js/pages/policy_transaction_inception_form_validation.js') ?>"></script>
<script>
var kycPrimaryKey = $('#client_id_kyc').val();
@ -101,7 +102,7 @@
<div class="form-row additional-doc-row" data-row-index="${index}">
<div class="form-group col-md-4">
<label>Document Name<span class="text-danger">*</span></label>
<input type="text" class="form-control other-doc-name-field" placeholder="Document Name" name="other_docs_name[]" required>
<input type="text" class="form-control other-doc-name-field" placeholder="Document Name" name="other_docs_name[]" data-parsley-inceptioncharset="true" data-parsley-trigger="input change" required>
</div>
<div class="form-group col-md-4">
<label>File<span class="text-danger">*</span></label>

View File

@ -176,6 +176,7 @@ var client_id_param = 0;
var client_branch_id_param = 0;
var client_policy_param = 0;
var tpa_api_sevice = 0;
var tpa_push_api_sevice = 0;
$('#file_upload').hide();
@ -1018,11 +1019,18 @@ function checkPolicyTermsAndRackRatesHasDefiend(event)
$('#event_type_data').val(response.insurer_multi_event);
console.log('response.tpa_api_service_status', response.tpa_api_service_status);
console.log('response.tpa_push_api_service_status', response.tpa_push_api_service_status);
console.log('tpa_api_sevice 1', tpa_api_sevice);
if(response.tpa_api_service_status){
tpa_api_sevice = 1;
}else{
tpa_api_sevice = 0;
}
if(response.tpa_push_api_service_status){
tpa_push_api_sevice = 1;
}else{
tpa_push_api_sevice = 0;
}
$('.fetch_tpa').hide();
$('#action_type').val('').change();

View File

@ -503,6 +503,9 @@
<button id="emp_form_submit_button_2" type="submit" class="btn btn-primary waves-effect waves-light justify-content-end">Download</button>
</div>
</div>
<div class="form-group col-md-4 push_tpa" style="margin-top: 18px;">
<a id="fetch_tpa_btn" class="btn btn-primary waves-effect waves-light justify-content-end" onclick="sendDataToTPA(this)">Upload Employees to TPA</a>
</div>
</div>
<div class="form-row" id="onboard_div" style="display:none;">
<a href="#"><span id="on_board_btn_txt" onclick="initiateWellnessOnboard(this)"></span></a>
@ -928,7 +931,7 @@
var insurer_or_tpa = $('#insurer_or_tpa').val()
var action_type = $('#action_type').val()
console.log({tpa_api_sevice, selectedVal, insurer_or_tpa, action_type});
console.log({tpa_api_sevice, selectedVal, insurer_or_tpa, action_type, tpa_push_api_sevice});
if (tpa_api_sevice == 0 || action_type == 'export' || insurer_or_tpa == "insurer" || (selectedVal == 'correction' || selectedVal == 'deletion' || selectedVal == 'si_enhancement')) {
$('.fetch_tpa').hide();
@ -936,6 +939,12 @@
$('.fetch_tpa').show();
}
if (tpa_push_api_sevice == 0 || action_type == 'import' || insurer_or_tpa == "insurer") {
$('.push_tpa').hide();
} else {
$('.push_tpa').show();
}
if (selectedVal === 'correction') {
$('#policy').prop('required', false);
$('#policy_danger').hide();
@ -944,8 +953,6 @@
$('#policy_danger').show();
}
console.log('tpa_api_sevice', tpa_api_sevice);
console.log('tpa_api_sevice type', typeof tpa_api_sevice);
});
$('#action_type').on('change', function() {
@ -974,6 +981,12 @@
} else {
$('.fetch_tpa').show();
}
if (tpa_push_api_sevice == 0 || action_type == 'import' || insurer_or_tpa == "insurer") {
$('.push_tpa').hide();
} else {
$('.push_tpa').show();
}
});
$('#insurer_or_tpa').on('change', function() {
@ -984,7 +997,7 @@
var action_type = $('#action_type').val()
var event_string = $('#event_type').val();
console.log({tpa_api_sevice, insurer_or_tpa, policy_value, event_type_data, action_type, event_string});
console.log({tpa_api_sevice, insurer_or_tpa, policy_value, event_type_data, action_type, event_string, tpa_push_api_sevice});
if (tpa_api_sevice == 0 || action_type == 'export' || insurer_or_tpa == "insurer" || (event_string == 'correction' || event_string == 'deletion' || event_string == 'si_enhancement')) {
$('.fetch_tpa').hide();
@ -992,6 +1005,12 @@
$('.fetch_tpa').show();
}
if (tpa_push_api_sevice == 0 || action_type == 'import' || insurer_or_tpa == "insurer") {
$('.push_tpa').hide();
} else {
$('.push_tpa').show();
}
// if(insurer_or_tpa == 'tpa' || event_type_data == 0){
if(insurer_or_tpa == 'tpa'){
@ -1034,7 +1053,7 @@
var event_string = $('#event_type').val();
var event_type_data = $('#event_type_data').val();
console.log({tpa_api_sevice, action_type, policy_value, insurer_or_tpa, event_string, event_type_data})
console.log({tpa_api_sevice, action_type, policy_value, insurer_or_tpa, event_string, event_type_data, tpa_push_api_sevice})
if (tpa_api_sevice == 0 || action_type == 'export' || insurer_or_tpa == "insurer" || (event_string == 'correction' || event_string == 'deletion' || event_string == 'si_enhancement')) {
$('.fetch_tpa').hide();
@ -1042,6 +1061,12 @@
$('.fetch_tpa').show();
}
if (tpa_push_api_sevice == 0 || action_type == 'import' || insurer_or_tpa == "insurer") {
$('.push_tpa').hide();
} else {
$('.push_tpa').show();
}
if(action_type == 'import'){
$("#event_type").removeAttr("multiple");
@ -1342,6 +1367,69 @@
}
function sendDataToTPA(){
let user_confirm = confirm('Are you sure you want to initiate the TPA Employee Push? This may take a while.');
if(!user_confirm)
{
return false;
}
let event = $('#event_type').val();
let client_id = $('#client').val();
let branch_id = $('#client_branch_id').val();
let policy_id = $('#policy').val();
let tpa_id = $('#policy option:selected').data('tid');
let policy_no = $('#policy option:selected').data('pno');
let checks = [
{val: client_id, msg: 'Please select the client'},
{val: branch_id, msg: 'Please select the client branch'},
{val: policy_id, msg: 'Please select the policy'},
{val: event, msg: 'Please select the event'},
{val: tpa_id, msg: 'TPA id empty'},
{val: policy_no, msg: 'Policy No empty'},
];
for (let c of checks) {
if (!c.val || c.val == 0) {
toastr.warning(c.msg, 'Warning');
return;
}
}
let url = '<?= base_url('sendDataToTPA') ?>';
// Data to send in the AJAX request
let requestData = {
policy_no: policy_no,
tpa_id: tpa_id,
client_id: client_id,
client_branch_id: branch_id,
client_policy_id: policy_id,
event: event,
};
// Send AJAX request
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
console.log('Data fetched successfully:', response);
if (response.status == true) {
toastr.success(response.message || 'Initiating successafully', 'Success');
} else {
toastr.error(response.message || 'Unable to fetch data', 'Error');
}
}, function(xhr, status, error) {
console.error('Error fetching data:', error);
console.error(xhr.responseText);
toastr.error('An error occurred while pushing.', 'Error');
});
}
</script>
<script>

View File

@ -101,6 +101,21 @@
border-radius: 10px !important;
border-width: 1px !important;
}
/* Live field highlight driven by Parsley state */
input.parsley-error,
textarea.parsley-error,
select.parsley-error {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.12) !important;
}
input.parsley-success,
textarea.parsley-success,
select.parsley-success {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.08) !important;
}
</style>
<?php
$isLeadEditEb = isset($lead_edit_data) && ! empty($lead_edit_data);
@ -110,6 +125,7 @@
var pageSubTitle = '<?= $ebSubtitle ?>';
var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="tooltip" data-placement="top" title="Back"><i class="mdi mdi-arrow-left" style="font-size: 17px;"></i></a>';
</script>
<script src="<?= base_url('public/assets/js/pages/leads_form_validation.js') ?>"></script>
<div class="container-fluid-min">
<div class="row" id="leads_form">
@ -706,6 +722,9 @@
if (res.status == true) {
$('#appendArea_' + dataIncrement).append(res.data.html);
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
let policy_type_id = res.data.policy_type_id;
let lead_type = res.data.lead_type;
@ -1076,6 +1095,9 @@
if (response.status == true) {
console.log(response.message, 'SUCCESS');
$('#appendArea_' + dataIncrement).append(response.data);
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
console.log("Policy Type ID : ", policy_type_id);
if (policy_type_id == 1 || policy_type_id == 6 || policy_type_id == 7) {
@ -1275,6 +1297,9 @@
`;
container.appendChild(newRow);
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
//append mutli file html
addFileField(increment);

View File

@ -460,6 +460,15 @@ if (isset($selected_lead_type)) {
let shortNameTimer;
$('#client_name').on('input', function() {
let lead_type = $('#lead_type').val();
let exixting_client = $('#existing_client').is(':checked');
console.log(`Lead Type: ${lead_type}, Existing Client: ${exixting_client}`);
if(lead_type == 1 || exixting_client == true){
return false;
}
clearTimeout(shortNameTimer);
shortNameTimer = setTimeout(generateShortName, 200);
});

View File

@ -1,4 +1,20 @@
<!-- Client form content modal-->
<style>
input.parsley-error,
textarea.parsley-error,
select.parsley-error {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.12) !important;
}
input.parsley-success,
textarea.parsley-success,
select.parsley-success {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.08) !important;
}
</style>
<script src="<?= base_url('public/assets/js/pages/new_client_modal_validation.js') ?>"></script>
<div class="modal fade" id="upload_enrollment_model" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">

View File

@ -395,6 +395,8 @@
</div>
</div>
<div id="append_html_for_other_policy_terms_72_after_family"></div>
<div class="other_special_condition" style="margin-top: 10px;">
@ -521,6 +523,7 @@
$('body').on('click', '.btnPolicyMaster', function() {
$("#append_html_for_other_policy_terms").empty();
$("#append_html_for_other_policy_terms_72_after_family").empty();
var client_policy_id = $(this).data('id');
policy_type_id = $(this).data('typeid');
@ -575,6 +578,10 @@
if (res.data) {
if (policy_type_id == 72) {
appendPolicyTermsHTML(policy_type_id);
}
//special conditions fields
let otherTermsJsonObjectSpecialCondition = JSON.parse(res.data);
Object.keys(otherTermsJsonObjectSpecialCondition).forEach(function(key) {
@ -757,6 +764,10 @@
$('#other_member_max_age_others').val(jsonObject[key].elders.max);
}
if (key.includes("enrollment_display_key") && jsonObject[key]) {
processOtherEnrollmentDisplayKey(jsonObject[key]);
}
}
});
@ -929,7 +940,12 @@
function appendPolicyTermsHTML(policy_type) {
var termsHTML = ""
$('#append_html_for_other_policy_terms').empty();
var targetContainer = '#append_html_for_other_policy_terms';
if (policy_type == 72) {
targetContainer = '#append_html_for_other_policy_terms_72_after_family';
}
$(targetContainer).empty();
if (policy_type == 7) {
@ -1132,10 +1148,126 @@
</div>
`
} else if (policy_type == 72) {
termsHTML = `
<div class="form-group OPD_POLICY_TERMS">
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="mode_of_serviceability_display" id="mode_of_serviceability_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="mode_of_serviceability">Mode of Serviceability</label>
</div>
<div class="col-md-6">
<input type="text" name="mode_of_serviceability" id="mode_of_serviceability" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="eligibility_display" id="eligibility_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="eligibility">Eligibility</label>
</div>
<div class="col-md-6">
<input type="text" name="eligibility" id="eligibility" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="total_sum_insured_limit_display" id="total_sum_insured_limit_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="total_sum_insured_limit">Total Sum Insured limit</label>
</div>
<div class="col-md-6">
<input type="text" name="total_sum_insured_limit" id="total_sum_insured_limit" class="form-control" value="INR 15000">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="in_person_doctor_consultation_display" id="in_person_doctor_consultation_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="in_person_doctor_consultation">In Person Doctor Consultation</label>
</div>
<div class="col-md-6">
<input type="text" name="in_person_doctor_consultation" id="in_person_doctor_consultation" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="prescribed_lab_test_pathology_radiology_display" id="prescribed_lab_test_pathology_radiology_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="prescribed_lab_test_pathology_radiology">Prescribed Lab test (Pathology & Radiology)</label>
</div>
<div class="col-md-6">
<input type="text" name="prescribed_lab_test_pathology_radiology" id="prescribed_lab_test_pathology_radiology" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="prescribed_pharmacy_display" id="prescribed_pharmacy_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="prescribed_pharmacy">Prescribed Pharmacy</label>
</div>
<div class="col-md-6">
<input type="text" name="prescribed_pharmacy" id="prescribed_pharmacy" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="dental_display" id="dental_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="dental">Dental</label>
</div>
<div class="col-md-6">
<input type="text" name="dental" id="dental" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="vision_display" id="vision_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="vision">Vision</label>
</div>
<div class="col-md-6">
<input type="text" name="vision" id="vision" class="form-control">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-1">
<input type="checkbox" style="margin-top: 12px" name="vaccination_for_children_and_adults_display" id="vaccination_for_children_and_adults_display" class="unchecked" checked>
</div>
<div class="col-md-5">
<label for="vaccination_for_children_and_adults">Vaccination for children & adults</label>
</div>
<div class="col-md-6">
<input type="text" name="vaccination_for_children_and_adults" id="vaccination_for_children_and_adults" class="form-control">
</div>
</div>
</div>
`
}
$('#append_html_for_other_policy_terms').append(termsHTML);
$(targetContainer).append(termsHTML);
}
function appendOtherSIAddMore(data = null) {
@ -1177,6 +1309,41 @@
.join(' '); // Join with spaces instead of underscores
}
function processOtherEnrollmentDisplayKey(displayObject) {
if (!displayObject || typeof displayObject !== 'object' || Array.isArray(displayObject)) {
return;
}
const displayMap = {
'Mode Of Serviceability': 'mode_of_serviceability_display',
'Eligibility': 'eligibility_display',
'Total Sum Insured Limit': 'total_sum_insured_limit_display',
'In Person Doctor Consultation': 'in_person_doctor_consultation_display',
'Prescribed Lab Test Pathology Radiology': 'prescribed_lab_test_pathology_radiology_display',
'Prescribed Pharmacy': 'prescribed_pharmacy_display',
'Dental': 'dental_display',
'Vision': 'vision_display',
'Vaccination For Children And Adults': 'vaccination_for_children_and_adults_display'
};
$('#append_html_for_other_policy_terms_72_after_family .unchecked').prop('checked', false);
Object.keys(displayObject).forEach(function(label) {
let checkboxId = displayMap[label];
if (!checkboxId) {
checkboxId = label
.toLowerCase()
.replace(/[\s\-\/,&]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '') + '_display';
}
if (displayObject[label] !== '' && displayObject[label] !== null && displayObject[label] !== undefined) {
$('#' + checkboxId).prop('checked', true);
}
});
}
function autoSaveOtherTerms() {
console.log('autoSaveOtherTerms function called');

View File

@ -149,7 +149,23 @@
color: #000;
}
/* Live field highlight driven by Parsley state */
input.parsley-error,
textarea.parsley-error,
select.parsley-error {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.12) !important;
}
input.parsley-success,
textarea.parsley-success,
select.parsley-success {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.08) !important;
}
</style>
<script src="<?= base_url('public/assets/js/pages/policy_transaction_endorsement_form_validation.js') ?>"></script>
<div class="tab-pane fade active show" id="form">
<div class="row" id="endorsement_form">

View File

@ -292,11 +292,27 @@
color: #dc3545;
font-size: 12px;
}
/* Live field highlight driven by Parsley state */
input.parsley-error,
textarea.parsley-error,
select.parsley-error {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.12) !important;
}
input.parsley-success,
textarea.parsley-success,
select.parsley-success {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.08) !important;
}
</style>
<script>
var pageSubTitle = undefined;
var pageBackButton = undefined;
</script>
<script src="<?= base_url('public/assets/js/pages/policy_transaction_inception_form_validation.js') ?>"></script>
<div class="tab-pane fade active show" id="form">
<input type="hidden" id="entity_type_id">
<div class="row" id="inception_form">

View File

@ -1185,7 +1185,7 @@ function addHTMLInput(data = null, container_id = 'dynamic-form-container')
newRow.innerHTML = `
<div class="form-group col-md-5">
<label for="file_name">Document Name<span class="text-danger">${required_star}</span></label>
<input type="text" class="form-control" id="docs_name" name="doc_name[]" placeholder="Enter file name" ${required}>
<input type="text" class="form-control" id="docs_name" name="doc_name[]" placeholder="Enter file name" data-parsley-inceptioncharset="true" data-parsley-trigger="input change" ${required}>
</div>
<div class="form-group col-md-5">
<label for="file">Upload File<span class="text-danger">${required_star}</span></label>
@ -1371,7 +1371,7 @@ function addHTMLInputForVehicleFileUpload(data = null, container_id = 'dynamic-f
newRow.innerHTML = `
<div class="form-group col-md-5">
<label for="file_name">Document Name<span class="text-danger">${required_star}</span></label>
<input type="text" class="form-control" id="docs_name" name="other_docs_name[]" placeholder="Enter file name" ${required}>
<input type="text" class="form-control" id="docs_name" name="other_docs_name[]" placeholder="Enter file name" data-parsley-inceptioncharset="true" data-parsley-trigger="input change" ${required}>
</div>
<div class="form-group col-md-5">
<label for="file">Upload File<span class="text-danger">${required_star}</span></label>

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

@ -71,3 +71,115 @@ Old function copied as `uploadRequiredDoc_v1` (preserved for reference).
### API Docs
- `nonebapidocs.md`
- `dev_logs/non_eb_claim_api.md`
---
## LeadsController — Bug Fix: `createClientWithLeadData` for Non-EB Leads
### 6. Fix: `prepareClientPolicyData` crashes on Non-EB leads
**Problem:** `proposel_data` is always `null` for Non-EB leads. The function unconditionally did:
```php
$proposel_data = json_decode($data['proposel_data'], true);
list($insurer_branch_id, $insurer_id) = explode('-', $proposel_data['insurer'], 2);
```
This throws a PHP 8 TypeError (cannot access key on null) for any Non-EB lead, making `createClientWithLeadData` silently fail.
**Fix:** Split insurer resolution by `lead_form_type`:
- **EB (`lead_form_type == 1`):** parse insurer from `proposel_data['insurer']` as `"{insurer_branch_id}-{insurer_id}"` — unchanged
- **Non-EB:** read `insurer_id` and `insurer_branch_id` directly from lead row columns
### 7. Fix: `getPlacementJson` null-safety for Non-EB leads
**Problem:** Same null `proposel_data` issue — `json_decode($data['proposel_data'], true)` returned null, and downstream `$proposel_data['proposel_name']` access would crash if a Non-EB lead had QCR data.
**Fix:** `json_decode($data['proposel_data'] ?? '', true) ?? []``proposel_data` is now always an array, so all `??` key accesses are safe.
### Smoke Test Results
| Path | Step | Result |
|---|---|---|
| EB | `proposel_data` decode | ✅ decodes normally |
| EB | `explode('-', proposel_data['insurer'])` | ✅ `insurer_branch_id` + `insurer_id` parsed correctly |
| EB | `preparePolicyTermsFromRFQ` | ✅ unaffected |
| Non-EB | `proposel_data` null → `[]` | ✅ safe |
| Non-EB | insurer from lead row columns | ✅ `insurer_id` + `insurer_branch_id` read directly |
| Non-EB | `getPlacementJson` — no QCR data | ✅ returns null safely, policy terms skipped |
| Non-EB | `getPlacementJson` — QCR data exists | ⚠️ line 4356: `$proposel_data['proposel_name']` missing `??` — pending clarification on whether Non-EB leads can have QCR data |
### Files Changed
- `app/Controllers/LeadsController.php`
## Task Plan — Policy Transaction Inception Form JS Validation
### Goal
Implement client-side form validation for `app/Views/policy_transaction_inception_form.php` by following the existing validation approach used in `app/Views/ticket_form_gmc.php` (centralized submit + reusable validator), but moving inception validation into a dedicated external JavaScript file.
### Current-State Notes
- `policy_transaction_inception_form.php` currently contains inline submit and validation logic on `#inception_form_id` (Parsley validation + custom checks + toastr + AJAX submit flow).
- `ticket_form_gmc.php` follows a cleaner pattern where submit handler delegates validation and shows first invalid field feedback.
- No dedicated inception validation `.js` file currently exists.
### Proposed File Changes
1. **Create** `public/assets/js/policy_transaction_inception_validation.js`
- Add a single public validation entry function (example: `window.validateInceptionFormInputs(form)`).
- Keep all custom rules here (beyond HTML `required` and Parsley):
- `follow_insurer_id[]` must be selected for all rows.
- `pt_form_sumbit_handler` must be `1`.
- CD account selection rule for client/policy status condition.
- Any date/business-rule checks currently done during submit.
- Return a boolean result and handle user-facing messages consistently via toastr.
2. **Refactor** `app/Views/policy_transaction_inception_form.php`
- Keep submit flow in one handler, but delegate custom validations to the new JS file.
- Minimize inline validation logic in view.
- Ensure first invalid field is focused/scrolled for better UX.
- Include the new script after shared dependencies (jQuery/Parsley/toastr), before submit logic usage.
3. **(Optional Cleanup)** Move remaining inline helper validation code to dedicated JS if it is inception-form-specific and not used elsewhere.
### Implementation Steps (Execution Order)
- [x] Step 1: Create new file `public/assets/js/pages/policy_transaction_inception_validation.js`.
- [x] Step 2: Extract custom validation blocks from `#inception_form_id` submit handler into reusable functions.
- [x] Step 3: Expose one callable function for submit handler (`validateInceptionFormInputs`).
- [x] Step 4: Update `policy_transaction_inception_form.php` to include new JS file.
- [x] Step 5: Replace inline custom checks with function call and keep existing AJAX submit behavior unchanged.
- [x] Step 6: Ensure invalid-field focus and warning message are preserved.
- [ ] Step 7: Verify create/edit journeys and conditional sections (renewal, co-insurer, CD account, policy status).
### Validation Rules Checklist (to implement in JS)
- [ ] Parsley base validation must pass.
- [ ] Every `follow_insurer_id[]` select must have value.
- [ ] `pt_form_sumbit_handler != 0`.
- [ ] If `client_type == 1` and `policy_status == completed` and `policy_type_id > 7`, at least one `cd_ac_no_for_child[]` must be selected.
- [ ] Keep existing toastr wording (or align to one consistent warning style).
### Testing Checklist
- [ ] Submit with empty required fields -> blocked with field-level indication.
- [ ] Submit with any empty co-insurer selector -> blocked with warning.
- [ ] Submit with base premium/CD mismatch (`pt_form_sumbit_handler = 0`) -> blocked.
- [ ] Submit valid data -> AJAX create request fires successfully.
- [ ] Edit existing inception record -> validation still works and submit succeeds.
- [ ] No regression in date conversion before submit (`policy_issue_date`, `policy_start_date`, `policy_end_date`, `renewal_date`, `rollover_date`, `month`).
### Risks / Attention Points
- Large inline script currently mixes validation and business logic; refactor should avoid changing API payload or field names.
- Multiple dynamic rows (`follow_insurer_id[]`, `cd_ac_no_for_child[]`) need delegated-safe selectors.
- Script include order is critical (new validation JS must load before submit handler executes).
### Completion Update (Implemented)
- Added `public/assets/js/pages/policy_transaction_inception_validation.js` with:
- `window.validateInceptionFormInputs(form)` as centralized entrypoint
- Parsley validation gate
- Co-insurer (`follow_insurer_id[]`) mandatory selection check
- `pt_form_sumbit_handler` (CD amount) guard check
- Child CD account selection rule for applicable completed flow
- First invalid field focus/scroll helper
- Updated `app/Views/policy_transaction_inception_form.php`:
- Included external script: `assets/js/pages/policy_transaction_inception_validation.js`
- Refactored `#inception_form_id` submit handler to delegate custom validation to new file
- Preserved existing AJAX submit and payload/date conversion behavior
- Technical validation done:
- JS syntax check passed (`node --check public/assets/js/pages/policy_transaction_inception_validation.js`)

View File

@ -0,0 +1,92 @@
# Policy Transaction Inception Form - Live Validation Plan
## Objective
- Add live field-level validation for `input`, `textarea`, and `select` in `app/Views/policy_transaction_inception_form.php`.
- Trigger validation errors on `oninput` and `onchange` behavior without disrupting existing Parsley-based submit validation.
- Restrict special characters to only `/`, `_`, `-`, `.`, and space (along with letters and numbers).
## Implementation Steps
- Create a dedicated JS module at `public/assets/js/pages/policy_transaction_inception_form_validation.js`.
- Register one custom Parsley validator (`inceptioncharset`) for allowed character set checks.
- Bind delegated events (`input` and `change`) on:
- `#inception_form_id`
- `#vehicle_form`
- `#CDMasterForm`
- Apply Parsley attributes non-destructively:
- Add `data-parsley-inceptioncharset="true"` to character-validated fields.
- Set `data-parsley-trigger` only when missing, so existing field-level Parsley settings remain intact.
- Support dynamic fields by reapplying constraints and refreshing Parsley instances before validating changed fields.
- Include the new JS file in the view after existing inline scripts.
## Character Rule
- Allowed characters: `A-Z`, `a-z`, `0-9`, `/`, `_`, `-`, `.`, and space.
- Validation message:
- `Only letters, numbers, spaces, and the characters / _ - . are allowed.`
## Non-Disruption Controls
- Do not replace existing submit handlers.
- Do not remove existing inline `onchange`/`oninput` handlers.
- Do not override existing Parsley triggers if already defined on a field.
- Ignore hidden/disabled fields for live validation.
## Validation Scope Notes
- `select` elements are revalidated on `change` (for required/Parsley feedback).
- Character set validation is applied to textual inputs and textareas only.
- File, checkbox, radio, hidden, button, submit, and reset fields are excluded from charset validation.
## Manual QA Checklist
- Type an invalid special character (example: `@`) in a text field and confirm immediate Parsley error.
- Type valid characters (`abc 123 / _ - .`) and confirm error clears.
- Change required select fields and confirm Parsley error appears/disappears on change.
- Add dynamic rows/fields (if applicable) and confirm live validation still works.
- Submit each form (`inception`, `vehicle`, `CD master`) and confirm existing submit flow is unchanged.
## Delivered Changes
- Added: `public/assets/js/pages/policy_transaction_inception_form_validation.js`
- Updated: `app/Views/policy_transaction_inception_form.php` (script include)
- Updated: `app/Views/policy_transaction_inception_form.php` (Parsley-driven red/green live field highlight styles)
- Updated: `app/Views/policy_transaction_inception_list.php` (dynamic `Document Name` rows now include Parsley charset attributes)
- Updated: `app/Views/client_kyc.php` (`Document Name` fields and template rows now include Parsley charset attributes)
- Updated: `app/Views/client_kyc.php` (loads validation script so KYC doc fields validate live in client onboarding screens)
- Updated: `public/assets/js/pages/policy_transaction_inception_form_validation.js` (extended to cover `#file_upload_form`, `#kyc_form`, and document-name field detection by name/class)
- Added: `public/assets/js/pages/policy_transaction_endorsement_form_validation.js` (same live Parsley validation flow for endorsement forms and policy-doc upload form)
- Updated: `app/Views/policy_transaction_endorsement_form.php` (script include for endorsement validation)
- Updated: `app/Views/policy_transaction_endorsement_form.php` (Parsley-driven red/green live field highlight styles)
## Leads Form Analysis (New Scope)
- Target file: `app/Views/leads_form.php`
- Main form identified: `#leads_form_id` (Parsley form)
- Existing constraints found:
- PAN and GST already use dedicated Parsley regex rules.
- Several dynamic sections append policy/custom fields into `#dynamic-form-container` and `#appendArea_*`.
- Contact fields include `contact_person_mobile` and `contact_person_email` as text inputs.
- Risk points for charset-only validation:
- Email fields must allow `@` and domain characters.
- Mobile should stay digits-only and length constrained.
- Existing PAN/GST pattern validation must remain untouched.
## Leads Form Plan
- Create separate JS file:
- `public/assets/js/pages/leads_form_validation.js`
- Add custom Parsley validators for leads page:
- Generic charset validator (allow: letters, numbers, `/`, `_`, `-`, `.`, space)
- Mobile validator (10 digits)
- Email validator (valid email format)
- Bind delegated `input`/`change` live validation on `#leads_form_id` so dynamic fields are automatically covered.
- Apply constraints by field type/ID:
- `contact_person_mobile` -> mobile validator
- `contact_person_email` -> email validator
- PAN/GST fields keep their existing `data-parsley-pattern` rules
- Other textual fields -> charset validator
- Add non-intrusive Parsley visual styles (`parsley-error` / `parsley-success`) in `leads_form.php`.
- Include new JS file in `leads_form.php` after existing page-level script setup.
## Leads Delivered Changes
- Added: `public/assets/js/pages/leads_form_validation.js`
- Updated: `app/Views/leads_form.php` (script include for leads live validation)
- Updated: `app/Views/leads_form.php` (Parsley-driven red/green live field highlight styles)
## New Client Modal Delivered Changes
- Added: `public/assets/js/pages/new_client_modal_validation.js`
- Updated: `app/Views/newClientModal.php` (script include for modal live validation)
- Updated: `app/Views/newClientModal.php` (Parsley-driven red/green live field highlight styles)

View File

@ -0,0 +1,247 @@
(function (getJq) {
'use strict';
function $(selector, context) {
var jq = getJq();
if (!jq) {
return { length: 0 };
}
return arguments.length > 1 ? jq(selector, context) : jq(selector);
}
var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var FORM_SELECTOR = '#leads_form_id';
var NS = '.leadsFormValidate';
var MESSAGES = {
text: 'Only letters, numbers, spaces, and the characters / _ - . are allowed.',
mobile: 'Mobile number must be exactly 10 digits (numbers only).',
email: 'Please enter a valid email address.'
};
function isParsleyReady() {
return !!window.Parsley;
}
function isSkippableField(el) {
return !el || el.disabled || el.type === 'hidden';
}
function isPanOrGstField(el) {
if (!el) {
return false;
}
var id = (el.id || '').toLowerCase();
return id === 'pan' || id === 'gst';
}
function isEmailField(el) {
if (!el) {
return false;
}
var id = (el.id || '').toLowerCase();
var name = (el.name || '').toLowerCase();
var type = (el.type || '').toLowerCase();
return id === 'contact_person_email' || name === 'contact_person_email' || type === 'email';
}
function isMobileField(el) {
if (!el) {
return false;
}
var id = (el.id || '').toLowerCase();
var name = (el.name || '').toLowerCase();
return id === 'contact_person_mobile' || name === 'contact_person_mobile';
}
function isCharsetCandidate(el) {
if (!el || el.tagName === 'SELECT') {
return false;
}
var type = (el.type || '').toLowerCase();
if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'submit' || type === 'button' || type === 'reset') {
return false;
}
if (isPanOrGstField(el) || isEmailField(el) || isMobileField(el)) {
return false;
}
return true;
}
function registerValidators() {
if (!isParsleyReady() || window.Parsley.__leadsFormValidatorsRegistered) {
return;
}
window.Parsley.addValidator('leadscharset', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return TEXT_ALLOWED.test(String(value));
},
messages: { en: MESSAGES.text }
});
window.Parsley.addValidator('leadsmobile10', {
validateString: function (value, req, instance) {
var v = String(value || '').replace(/\D/g, '');
if (!instance.$element.prop('required') && v.length === 0) {
return true;
}
if (instance.$element.prop('required') && v.length === 0) {
return true;
}
return v.length === 10;
},
messages: { en: MESSAGES.mobile }
});
window.Parsley.addValidator('leadsemail', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return EMAIL_RE.test(String(value).trim());
},
messages: { en: MESSAGES.email }
});
window.Parsley.__leadsFormValidatorsRegistered = true;
}
function clearAttrs($el) {
['data-parsley-leadscharset', 'data-parsley-leadsmobile10', 'data-parsley-leadsemail'].forEach(function (a) {
$el.removeAttr(a);
});
}
function applyConstraints($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input, textarea, select').each(function () {
var el = this;
var $el = $(el);
if (isSkippableField(el) || isPanOrGstField(el)) {
return;
}
clearAttrs($el);
if (isMobileField(el)) {
$el.attr('data-parsley-leadsmobile10', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (isEmailField(el)) {
$el.attr('data-parsley-leadsemail', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (isCharsetCandidate(el)) {
$el.attr('data-parsley-leadscharset', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'change');
}
});
}
function sanitizeOnInput(el) {
if (!el || isSkippableField(el)) {
return;
}
if (isMobileField(el)) {
var clean = (el.value || '').replace(/\D/g, '').substring(0, 10);
if (el.value !== clean) {
el.value = clean;
}
return;
}
if (isCharsetCandidate(el)) {
var raw = el.value || '';
if (!TEXT_ALLOWED.test(raw)) {
el.value = raw.replace(/[^A-Za-z0-9/_.\- ]/g, '');
}
}
}
function refreshParsley($form) {
if (!$form || !$form.length || !isParsleyReady()) {
return;
}
try {
var instance = $form.parsley();
if (instance && typeof instance.refresh === 'function') {
instance.refresh();
}
} catch (e) {
// no-op
}
}
function validateField(field) {
if (!field || isSkippableField(field) || !isParsleyReady()) {
return;
}
var $field = $(field);
var $form = $field.closest('form');
if (!$form.length || typeof $field.parsley !== 'function') {
return;
}
applyConstraints($form);
refreshParsley($form);
try {
$field.parsley().validate();
} catch (e) {
// no-op
}
}
function init() {
var $form = $(FORM_SELECTOR);
if (!$form.length || !isParsleyReady()) {
return;
}
registerValidators();
applyConstraints($form);
refreshParsley($form);
$form.off(NS);
$form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
sanitizeOnInput(this);
validateField(this);
});
$form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
validateField(this);
});
}
$(function () {
init();
});
window.refreshLeadsFormValidation = function () {
init();
};
})(function () {
return window.jQuery;
});

View File

@ -0,0 +1,220 @@
(function (getJq) {
'use strict';
function $(selector, context) {
var jq = getJq();
if (!jq) {
return { length: 0 };
}
return arguments.length > 1 ? jq(selector, context) : jq(selector);
}
var FORM_SELECTOR = '#client_form';
var NS = '.newClientModalValidate';
var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function isParsleyReady() {
return !!window.Parsley;
}
function isSkippable(el) {
return !el || el.disabled || el.type === 'hidden';
}
function isPanOrGst(el) {
var id = (el.id || '').toLowerCase();
return id === 'pan' || id === 'gst';
}
function isMobile(el) {
var id = (el.id || '').toLowerCase();
var name = (el.name || '').toLowerCase();
return id === 'mobile' || id === 'phone' || name === 'mobile' || name === 'phone';
}
function isEmail(el) {
var type = (el.type || '').toLowerCase();
var id = (el.id || '').toLowerCase();
var name = (el.name || '').toLowerCase();
return type === 'email' || id === 'email' || id === 'email2' || name === 'email' || name === 'email2';
}
function isCharsetCandidate(el) {
if (!el || el.tagName === 'SELECT') {
return false;
}
var type = (el.type || '').toLowerCase();
if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'submit' || type === 'button' || type === 'reset') {
return false;
}
if (isPanOrGst(el) || isMobile(el) || isEmail(el)) {
return false;
}
return true;
}
function registerValidators() {
if (!isParsleyReady() || window.Parsley.__newClientModalValidatorsRegistered) {
return;
}
window.Parsley.addValidator('newclientcharset', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return TEXT_ALLOWED.test(String(value));
},
messages: { en: 'Only letters, numbers, spaces, and the characters / _ - . are allowed.' }
});
window.Parsley.addValidator('newclientmobile10', {
validateString: function (value, req, instance) {
var v = String(value || '').replace(/\D/g, '');
if (!instance.$element.prop('required') && v.length === 0) {
return true;
}
if (instance.$element.prop('required') && v.length === 0) {
return true;
}
return v.length === 10;
},
messages: { en: 'Mobile number must be exactly 10 digits (numbers only).' }
});
window.Parsley.addValidator('newclientemail', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return EMAIL_RE.test(String(value).trim());
},
messages: { en: 'Please enter a valid email address.' }
});
window.Parsley.__newClientModalValidatorsRegistered = true;
}
function clearAttrs($el) {
['data-parsley-newclientcharset', 'data-parsley-newclientmobile10', 'data-parsley-newclientemail'].forEach(function (a) {
$el.removeAttr(a);
});
}
function applyConstraints($form) {
$form.find('input, textarea, select').each(function () {
var el = this;
var $el = $(el);
if (isSkippable(el) || isPanOrGst(el)) {
return;
}
clearAttrs($el);
if (isMobile(el)) {
$el.attr('data-parsley-newclientmobile10', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (isEmail(el)) {
$el.attr('data-parsley-newclientemail', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (isCharsetCandidate(el)) {
$el.attr('data-parsley-newclientcharset', 'true');
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'change');
}
});
}
function sanitizeOnInput(el) {
if (!el || isSkippable(el)) {
return;
}
if (isMobile(el)) {
var d = (el.value || '').replace(/\D/g, '').substring(0, 10);
if (el.value !== d) {
el.value = d;
}
return;
}
if (isCharsetCandidate(el)) {
var raw = el.value || '';
if (!TEXT_ALLOWED.test(raw)) {
el.value = raw.replace(/[^A-Za-z0-9/_.\- ]/g, '');
}
}
}
function refreshParsley($form) {
try {
var p = $form.parsley();
if (p && typeof p.refresh === 'function') {
p.refresh();
}
} catch (e) {
// no-op
}
}
function validateField(el) {
if (!el || isSkippable(el) || !isParsleyReady()) {
return;
}
var $f = $(el).closest('form');
if (!$f.length) {
return;
}
applyConstraints($f);
refreshParsley($f);
try {
$(el).parsley().validate();
} catch (e) {
// no-op
}
}
function init() {
var $form = $(FORM_SELECTOR);
if (!$form.length || !isParsleyReady()) {
return;
}
registerValidators();
applyConstraints($form);
refreshParsley($form);
$form.off(NS);
$form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
sanitizeOnInput(this);
validateField(this);
});
$form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
validateField(this);
});
}
$(function () {
init();
});
window.refreshNewClientModalValidation = function () {
init();
};
})(function () {
return window.jQuery;
});

View File

@ -0,0 +1,166 @@
(function (getJq) {
'use strict';
function $(selector, context) {
var jq = getJq();
if (!jq) {
return { length: 0 };
}
return arguments.length > 1 ? jq(selector, context) : jq(selector);
}
var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
var MESSAGE = 'Only letters, numbers, spaces, and the characters / _ - . are allowed.';
var FORM_SELECTORS = ['#endorsement_form_id', '#drive_file_upload_form', '#file_upload_form'];
var NS = '.endorsementFormValidate';
function isParsleyReady() {
return !!window.Parsley;
}
function isSkippableField(el) {
return !el || el.disabled || el.type === 'hidden';
}
function isDocumentNameField(el) {
if (!el) {
return false;
}
var name = (el.name || '').toLowerCase();
var id = (el.id || '').toLowerCase();
return name === 'doc_name[]' ||
name === 'other_docs_name[]' ||
name === 'docs_name[]' ||
id === 'docs_name' ||
$(el).hasClass('other-doc-name-field');
}
function isCharsetCandidate(el) {
if (!el || el.tagName === 'SELECT') {
return false;
}
var type = (el.type || '').toLowerCase();
if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'email' || type === 'url' || type === 'submit' || type === 'button' || type === 'reset') {
return false;
}
return true;
}
function registerValidator() {
if (!isParsleyReady() || window.Parsley.__endorsementFormValidatorRegistered) {
return;
}
window.Parsley.addValidator('endorsementcharset', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return TEXT_ALLOWED.test(String(value));
},
messages: { en: MESSAGE }
});
window.Parsley.__endorsementFormValidatorRegistered = true;
}
function applyConstraints($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input, textarea, select').each(function () {
var el = this;
var $el = $(el);
if (isSkippableField(el)) {
return;
}
if (isCharsetCandidate(el) || isDocumentNameField(el)) {
if (!$el.attr('data-parsley-endorsementcharset')) {
$el.attr('data-parsley-endorsementcharset', 'true');
}
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'change');
}
});
}
function refreshParsley($form) {
if (!$form || !$form.length || !isParsleyReady()) {
return;
}
try {
var instance = $form.parsley();
if (instance && typeof instance.refresh === 'function') {
instance.refresh();
}
} catch (e) {
// no-op
}
}
function validateField(field) {
if (!field || isSkippableField(field) || !isParsleyReady()) {
return;
}
var $field = $(field);
var $form = $field.closest('form');
if (!$form.length || typeof $field.parsley !== 'function') {
return;
}
applyConstraints($form);
refreshParsley($form);
try {
$field.parsley().validate();
} catch (e) {
// no-op
}
}
function bindForm(selector) {
var $form = $(selector);
if (!$form.length) {
return;
}
registerValidator();
applyConstraints($form);
refreshParsley($form);
$form.off(NS);
$form.on('input' + NS, 'input:not([type=hidden]), textarea', function () {
validateField(this);
});
$form.on('change' + NS, 'select, input:not([type=hidden]), textarea', function () {
validateField(this);
});
}
function init() {
if (!isParsleyReady()) {
return;
}
FORM_SELECTORS.forEach(function (selector) {
bindForm(selector);
});
}
$(function () {
init();
});
window.refreshEndorsementFormValidation = function (formSelector) {
if (!formSelector || !isParsleyReady()) {
return;
}
bindForm(formSelector);
};
})(function () {
return window.jQuery;
});

View File

@ -0,0 +1,185 @@
(function (getJq) {
'use strict';
function $(selector, context) {
var jq = getJq();
if (!jq) {
return { length: 0 };
}
return arguments.length > 1 ? jq(selector, context) : jq(selector);
}
var TEXT_ALLOWED = /^[A-Za-z0-9/_.\- ]*$/;
var MESSAGE = 'Only letters, numbers, spaces, and the characters / _ - . are allowed.';
var FORM_SELECTORS = ['#inception_form_id', '#vehicle_form', '#CDMasterForm', '#file_upload_form', '#kyc_form'];
var INCEPTION_NS = '.inceptionFormValidate';
function isParsleyReady() {
return !!window.Parsley;
}
function isSkippableField(el) {
if (!el) {
return true;
}
if (el.disabled) {
return true;
}
if (el.type === 'hidden') {
return true;
}
return false;
}
function isCharsetCandidate(el) {
if (!el || el.tagName === 'SELECT') {
return false;
}
var type = (el.type || '').toLowerCase();
if (type === 'file' || type === 'checkbox' || type === 'radio' || type === 'password' || type === 'email' || type === 'url' || type === 'submit' || type === 'button' || type === 'reset') {
return false;
}
return true;
}
function isDocumentNameField(el) {
if (!el) {
return false;
}
var name = (el.name || '').toLowerCase();
var id = (el.id || '').toLowerCase();
return name === 'doc_name[]' ||
name === 'other_docs_name[]' ||
name === 'docs_name[]' ||
id === 'docs_name' ||
$(el).hasClass('other-doc-name-field');
}
function registerValidator() {
if (!isParsleyReady() || window.Parsley.__inceptionFormValidatorRegistered) {
return;
}
window.Parsley.addValidator('inceptioncharset', {
validateString: function (value) {
if (!value || String(value).trim() === '') {
return true;
}
return TEXT_ALLOWED.test(String(value));
},
messages: { en: MESSAGE }
});
window.Parsley.__inceptionFormValidatorRegistered = true;
}
function applyConstraints($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input, textarea, select').each(function () {
var el = this;
var $el = $(el);
if (isSkippableField(el)) {
return;
}
if (isCharsetCandidate(el) || isDocumentNameField(el)) {
if (!$el.attr('data-parsley-inceptioncharset')) {
$el.attr('data-parsley-inceptioncharset', 'true');
}
if (!$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'input change');
}
return;
}
if (el.tagName === 'SELECT' && !$el.attr('data-parsley-trigger')) {
$el.attr('data-parsley-trigger', 'change');
}
});
}
function refreshParsleyInstance($form) {
if (!$form || !$form.length || !isParsleyReady()) {
return;
}
try {
var parsleyInstance = $form.parsley();
if (parsleyInstance && typeof parsleyInstance.refresh === 'function') {
parsleyInstance.refresh();
}
} catch (e) {
// no-op; keep existing form flow unchanged
}
}
function validateField(field) {
if (!field || isSkippableField(field) || !isParsleyReady()) {
return;
}
var $field = $(field);
var $form = $field.closest('form');
if (!$form.length || typeof $field.parsley !== 'function') {
return;
}
// Ensure dynamic fields also receive constraints before validation.
applyConstraints($form);
refreshParsleyInstance($form);
try {
$field.parsley().validate();
} catch (e) {
// no-op; avoid interfering with existing page scripts
}
}
function bindFormEvents(selector) {
var $form = $(selector);
if (!$form.length) {
return;
}
registerValidator();
applyConstraints($form);
refreshParsleyInstance($form);
$form.off(INCEPTION_NS);
$form.on('input' + INCEPTION_NS, 'input:not([type=hidden]), textarea', function () {
validateField(this);
});
$form.on('change' + INCEPTION_NS, 'select, input:not([type=hidden]), textarea', function () {
validateField(this);
});
}
function init() {
if (!isParsleyReady()) {
return;
}
FORM_SELECTORS.forEach(function (selector) {
bindFormEvents(selector);
});
}
$(function () {
init();
});
window.refreshInceptionFormValidation = function (formSelector) {
if (!formSelector || !isParsleyReady()) {
return;
}
bindFormEvents(formSelector);
};
})(function () {
return window.jQuery;
});

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.

View File

@ -0,0 +1,157 @@
# 2026-04-06 — OPD Policy Terms Plan (`policy_type_id = 72`)
## Context
- Policy type: `72`
- Policy terms label: `OPD Policy Terms`
- Requested fields/content:
- `Mode of Serviceability`
- `Eligibility`
- `Total Sum Insured limit - INR 15000`
- `In Person Doctor Consultation`
- `Prescribed Lab test (Pathology & Radiology)`
- `Prescribed Pharmacy`
- `Dental`
- `Vision`
- `Vaccination for children & adults`
- Target view: `app/Views/other_policy_terms.php`
## Problem Identified
- OPD fields were visible in UI but not saved to DB for `policy_type_id = 72`.
- Root cause: `app/Controllers/ClientController.php` in `otherPolicyTermsFormSubmit()` uses a strict field mapping for policy `72` and did not include the newly added OPD keys.
- Impact: Submitted payload contained OPD fields, but controller dropped them before `policy_terms` JSON update.
## Fix Plan
1. Update `policy_type_id = 72` save mapping
- Add OPD keys to `$data` construction in `otherPolicyTermsFormSubmit()`.
- Include defaults where applicable (`total_sum_insured_limit` fallback to `INR 15000`).
2. Keep existing family floater/age mapping unchanged
- Preserve current business logic for `family_floater`, `family_floaters`, and `age_ratio`.
- Add OPD terms in additive mode only.
3. Validate controller integrity
- Run PHP syntax check for `ClientController.php`.
- Confirm no changes to unrelated policy type save flow.
## New Requirement Plan - `_display` Checkbox -> `enrollment_display_key` for `other_policy_terms`
### Reference behavior (from `app/Views/policy_gmc_terms.php`)
- Each term row includes a checkbox with `name` ending in `_display`.
- Only checked display keys are reflected in `enrollment_display_key`.
- Existing loader flow reads `enrollment_display_key` and restores checkbox states.
### Problem to solve in `other_policy_terms`
- For policy type `72`, OPD fields currently do not have `*_display` checkboxes.
- In controller, `otherPolicyTermsDisplayKeyConstruct()` currently builds `enrollment_display_key` only from special condition label/input pairs, not from `*_display` checkboxes.
- Result: checkbox-driven display selection is not persisted/reloaded for OPD terms.
### Implementation plan
1. Add `*_display` checkboxes to OPD rows in `app/Views/other_policy_terms.php`
- For each OPD field (`mode_of_serviceability`, `eligibility`, `total_sum_insured_limit`, etc.), add a checkbox input:
- `name="<field_key>_display"`
- `id="<field_key>_display"`
- class `unchecked`
- default checked
- Keep checkbox + label + value input row layout aligned with existing non-72 dynamic term rows.
2. Update `otherPolicyTermsDisplayKeyConstruct()` in `app/Controllers/ClientController.php`
- Extend logic to parse all incoming keys ending with `_display`.
- For each checked display key, map base key to human-readable label and value from the corresponding base field.
- Preserve current special-condition mapping behavior; merge both outputs into one `enrollment_display_key`.
3. Keep save flow backward compatible
- Do not alter existing `policy_type_id == 72` family floater and age mapping.
- Ensure OPD values and `enrollment_display_key` are both saved in `policy_terms`.
- Keep non-72 flow unchanged.
4. Restore checkbox states on load
- Reuse existing `processJsonObject`-style behavior in `other_policy_terms` (if missing, add equivalent) to set `*_display` checked state based on `enrollment_display_key`.
- Verify for both newly created and previously saved records.
5. Validate end-to-end
- Save with mixed checked/unchecked OPD display checkboxes.
- Confirm DB JSON includes expected `enrollment_display_key` entries.
- Reload and verify display checkbox states are restored.
## Implementation Plan
1. Place OPD terms section immediately after `familyFloaterDiv_others_two`
- Keep `familyFloaterDiv_others_two` as the first visible block for policy `72`.
- Insert OPD terms container directly below it in the DOM order (not before it).
- Ensure the OPD section appears before special conditions.
2. Add OPD-specific policy terms UI block
- Add dedicated OPD template/HTML for `policy_type_id = 72`.
- Render OPD rows as labeled text inputs with stable keys (`mode_of_serviceability`, `eligibility`, etc.).
- Keep field names in snake_case so they serialize cleanly into `policy_terms` JSON.
3. Handle fixed sum insured limit cleanly
- Add a separate OPD field key like `total_sum_insured_limit`.
- Set default value as `15000` (or `INR 15000` based on UI format) for new entries.
- Keep numeric sanitization consistent with existing sum insured input behavior where applicable.
4. Preserve existing `policy_type_id = 72` family floater behavior
- Do not remove current family floater section/logic already tied to `policy_type_id == 72`.
- Ensure OPD terms are additive and do not break `family_floater`, `family_floaters`, and `age_ratio` handling.
5. Populate saved data on edit
- Reuse existing JSON hydration flow (`Object.keys(jsonObject)` loop) so OPD fields auto-populate by `name`.
- Confirm keys in UI match keys stored in policy terms JSON exactly.
6. Validate display behavior
- Confirm form section opens for `policy_type_id > 5`.
- Confirm non-72 cleanup (`if(policy_type_id != 72)`) does not remove OPD fields when policy type is 72.
- Confirm OPD fields are not rendered for unrelated policy types.
- Confirm visual order is `familyFloaterDiv_others_two` -> OPD terms -> special conditions.
7. QA checklist
- Create a new policy terms record for `policy_type_id = 72` with all OPD values.
- Reload and verify values repopulate correctly.
- Verify submit and autosave payload include OPD keys in `policy_terms`.
- Verify no regressions for policy types `6` and `7`.
## Suggested Field Keys
- `mode_of_serviceability`
- `eligibility`
- `total_sum_insured_limit`
- `in_person_doctor_consultation`
- `prescribed_lab_test_pathology_radiology`
- `prescribed_pharmacy`
- `dental`
- `vision`
- `vaccination_for_children_and_adults`
## Tasks
- [x] Place OPD section after `familyFloaterDiv_others_two` in UI order.
- [x] Add `policy_type == 72` OPD HTML block in `appendPolicyTermsHTML()`.
- [x] Ensure defaults are applied for total sum insured limit.
- [x] Verify bind/populate works from existing JSON loader.
- [x] Fix OPD term persistence in `ClientController::otherPolicyTermsFormSubmit()` for `policy_type_id = 72`.
- [x] Add temporary debug logging for OPD72 save payload in controller.
- [x] Add `*_display` checkboxes for OPD term rows in `other_policy_terms.php`.
- [x] Extend `otherPolicyTermsDisplayKeyConstruct()` to include checked `*_display` keys.
- [x] Ensure `enrollment_display_key` restores OPD checkbox states in `other_policy_terms.php`.
- [ ] Verify DB `policy_terms.enrollment_display_key` for mixed checked/unchecked OPD fields.
- [ ] Run manual UI verification for create/edit/submit/autosave.
- [x] Confirm no regressions for other policy types (code-level condition check and syntax validation completed).
- [ ] Remove temporary debug logging after verification.
## Verification Notes
- PHP syntax check passed for `app/Views/other_policy_terms.php` (`php -l`).
- PHP syntax check passed for `app/Controllers/ClientController.php` (`php -l`).
- Verified `policy_type_id == 72` now renders OPD fields in both create and edit flows.
- Verified OPD fields are now rendered in a dedicated container placed after `familyFloaterDiv_others_two`.
- Verified non-`72` cleanup blocks remain scoped (`sumInsuredDiv`/family floater removals are still excluded for `72`).
- Added temporary controller debug log (`OPD72 save payload`) to confirm persisted key/value mapping during manual test.
- Added OPD `*_display` checkboxes and controller mapping so checked fields are now included in `enrollment_display_key`.
- Added `processOtherEnrollmentDisplayKey()` in `other_policy_terms.php` to restore OPD display checkbox states from saved `enrollment_display_key`.
- Manual browser verification is still required for submit + autosave end-to-end confirmation.