diff --git a/app/Controllers/ApiServiceController.php b/app/Controllers/ApiServiceController.php index b69fe231..18eac66a 100644 --- a/app/Controllers/ApiServiceController.php +++ b/app/Controllers/ApiServiceController.php @@ -15,6 +15,7 @@ use App\Controllers\MediAssistApiController; use App\Controllers\FhplApiController; use App\Controllers\VoloApiController; use App\Models\BatchFileModel; +use App\Models\ClaimFilesModel; use App\Models\FileModel; use App\Helpers\TPADataCompareHelper; use App\Helpers\TPADataCompareHelper2; @@ -25,6 +26,7 @@ class ApiServiceController extends BaseController // protected $format = 'json'; protected $db; protected $employeePolicyModel; + protected $claimFilesModel; protected $medi_assist_primary_key; protected $vidal_primary_key; protected $icici_primary_key; @@ -36,6 +38,7 @@ class ApiServiceController extends BaseController { $this->db = \Config\Database::connect(); $this->employeePolicyModel = new EmployeePolicyModel(); + $this->claimFilesModel = new ClaimFilesModel(); $this->medi_assist_primary_key = getenv('MEDI_ASSIST_PRIMARY_KEY_CONSTANT'); $this->vidal_primary_key = getenv('VIDAL_PRIMARY_KEY_CONSTANT'); $this->icici_primary_key = getenv('ICICI_PRIMARY_KEY_CONSTANT'); @@ -177,26 +180,20 @@ class ApiServiceController extends BaseController $voloApiController = new VoloApiController(); $data['eCardDownload'] = $voloApiController->EcardRequest( $emp_code, $policy_no , $employee_policy[0]['tpa_id'] ); - }else{ - - if($type == "download"){ - // direct download - $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/1'; - }else{ - if(isset($all_member) && !empty($all_member)){ - // view and download all members - $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/1/1'; - }else{ - // view and download single member - $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/0/1'; - } - } } - $data['message'] = "E-card generated"; - if(empty($data['eCardDownload'])){ - $data['message'] = "E-card not generated"; - } + if (empty($data['eCardDownload'])) { + $data['eCardDownload'] = $this->buildDefaultEcardDownloadUrl( + $employee_policy[0]['rand_string'], + $type, + $all_member ?? null + ); + } + + $data['message'] = "E-card generated"; + if(empty($data['eCardDownload'])){ + $data['message'] = "E-card not generated"; + } } else { $data['eCardDownload'] = null; @@ -220,6 +217,19 @@ class ApiServiceController extends BaseController } } + private function buildDefaultEcardDownloadUrl(string $randString, string $type = 'download', $allMember = null): string + { + if ($type === 'download') { + return base_url('download-e-card/') . $randString . '/1'; + } + + if (!empty($allMember)) { + return base_url('download-e-card/') . $randString . '/1/1'; + } + + return base_url('download-e-card/') . $randString . '/0/1'; + } + // Get TPAID public function getTPAID() { @@ -583,6 +593,13 @@ class ApiServiceController extends BaseController ]); } + if (! $this->claimFilesModel->hasPdfFileForTicket($claimId)) { + return $this->response->setJSON([ + 'status' => false, + 'message' => 'No PDF file found for this claim' + ]); + } + $result = $this->pushClaims($claimId); if ($result !== null && is_array($result)) { diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 5ec861eb..d14c8de7 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -763,7 +763,7 @@ class ClientController extends AdminController public function saveDeposit() { - $rules = [ + $rules = [ 'amount' => [ 'rules' => 'required|numeric|greater_than_equal_to[0]', @@ -799,7 +799,7 @@ class ClientController extends AdminController ], 'cd_ac_no' => [ - 'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-_]+$/]', + 'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-_ ]+$/]', 'errors' => [ 'required' => 'CD Account number is required.', 'regex_match' => 'CD Account number can only contain letters, numbers, hyphens(-), underscores(_), and slashes(/).', @@ -874,12 +874,14 @@ class ClientController extends AdminController $record_date = null; } + $cd_master_data = $this->CDMasterModel->where('id', $cd_ac_pk)->where('is_active', 1)->first(); + $data = [ 'amount' => $sanitized_post_data['amount'] ?? null, 'sub_type_id' => $sanitized_post_data['sub_type_id'] ?? null, 'client_id' => $sanitized_post_data['client_id'] ?? null, 'client_policy_id' => null, - 'cd_ac_no' => $cd_ac_no ?? null, + 'cd_ac_no' => $cd_master_data['cd_ac_no'] ?? null, 'cd_ac_pk' => $cd_ac_pk ?? null, 'endorsement_no' => null, 'insurer_id' => $sanitized_post_data['insurer_id'] ?? null, diff --git a/app/Controllers/Docs/DocsController.php b/app/Controllers/Docs/DocsController.php index ee117868..d85d77b0 100644 --- a/app/Controllers/Docs/DocsController.php +++ b/app/Controllers/Docs/DocsController.php @@ -63,6 +63,14 @@ class DocsController extends BaseController ['id' => 'tpa-recon', 'label' => 'TPA Recon', 'url' => 'docs/tpa-recon'], ['id' => 'eb-rack-rate-config', 'label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'], ['id' => 'eb-rack-rate-calculation', 'label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'], + ['id' => 'bds-insurer-statement', 'label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'], + ['id' => 'bds-commission', 'label' => 'BDS commission', 'url' => 'docs/bds-commission'], + ['id' => 'inception', 'label' => 'Inception', 'url' => 'docs/inception'], + ['id' => 'deletion', 'label' => 'Deletion', 'url' => 'docs/deletion'], + ['id' => 'correction', 'label' => 'Correction', 'url' => 'docs/correction'], + ['id' => 'si-enhancement', 'label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'], + ['id' => 'non-eb-opportunities', 'label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'], + ['id' => 'non-eb-claims', 'label' => 'Non-EB Claims', 'url' => 'docs/non-eb-claims'], ], ], [ @@ -451,6 +459,226 @@ class DocsController extends BaseController ['label' => 'Related', 'href' => '#related'], ], 'prev' => ['label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'], + 'next' => ['label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'], + ], + + 'bds-insurer-statement' => [ + 'view' => 'docs/bds-insurer-statement', + 'title' => 'BDS Insurer statement', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '12 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Row validation logic', 'href' => '#row-validation-flowchart'], + ['label' => 'Key files and routes', 'href' => '#key-files-routes'], + ['label' => 'Statement list UI', 'href' => '#statement-list-ui'], + ['label' => 'uploadInsurerStatement','href' => '#upload-flow'], + ['label' => 'validateInsurerStatement', 'href' => '#validate-flow'], + ['label' => 'Validation steps', 'href' => '#validate-steps', 'level' => 'h3'], + ['label' => 'Validation error codes', 'href' => '#validation-errors', 'level' => 'h3'], + ['label' => 'updateInsurerStatement', 'href' => '#update-flow'], + ['label' => 'Excel column mapping', 'href' => '#excel-columns'], + ['label' => 'NHance source query', 'href' => '#nhance-source-query'], + ['label' => 'Invoice and delete', 'href' => '#invoice-and-delete'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#common-pitfalls'], + ['label' => 'Related BDS features', 'href' => '#related-bds'], + ], + 'prev' => ['label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'], + 'next' => ['label' => 'BDS commission', 'url' => 'docs/bds-commission'], + ], + + 'bds-commission' => [ + 'view' => 'docs/bds-commission', + 'title' => 'BDS commission', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '10 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Key files', 'href' => '#key-files'], + ['label' => 'Routes', 'href' => '#routes'], + ['label' => 'Commission file format', 'href' => '#commission-file-format'], + ['label' => 'Required columns', 'href' => '#required-columns', 'level' => 'h3'], + ['label' => 'Sample rows', 'href' => '#commission-file-samples', 'level' => 'h3'], + ['label' => 'Rule upload', 'href' => '#upload-flow'], + ['label' => 'Rules editor', 'href' => '#rules-editor'], + ['label' => 'Rule JSON shape', 'href' => '#rule-json'], + ['label' => 'Calculation API', 'href' => '#calculation-api'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'], + 'next' => ['label' => 'Inception', 'url' => 'docs/inception'], + ], + + 'inception' => [ + 'view' => 'docs/inception', + 'title' => 'Inception', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '8 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Key files and routes', 'href' => '#key-files-routes'], + ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'], + ['label' => 'Format validation', 'href' => '#format-validation'], + ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'], + ['label' => 'reason JSON', 'href' => '#reason-json', 'level' => 'h3'], + ['label' => 'Data validation', 'href' => '#data-validation'], + ['label' => 'Data error codes', 'href' => '#data-errors', 'level' => 'h3'], + ['label' => 'Onboard preprocess', 'href' => '#preprocess'], + ['label' => 'Excel columns', 'href' => '#excel-columns'], + ['label' => 'Family row example', 'href' => '#family-example'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'BDS commission', 'url' => 'docs/bds-commission'], + 'next' => ['label' => 'Deletion', 'url' => 'docs/deletion'], + ], + + 'deletion' => [ + 'view' => 'docs/deletion', + 'title' => 'Deletion', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '7 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Key files and routes', 'href' => '#key-files-routes'], + ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'], + ['label' => 'Format validation', 'href' => '#format-validation'], + ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'], + ['label' => 'Data validation', 'href' => '#data-validation'], + ['label' => 'employeeDisembark', 'href' => '#disembark'], + ['label' => 'TPA auto-deletion', 'href' => '#tpa-auto'], + ['label' => 'Excel columns', 'href' => '#excel-columns'], + ['label' => 'Row examples', 'href' => '#row-example'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'Inception', 'url' => 'docs/inception'], + 'next' => ['label' => 'Correction', 'url' => 'docs/correction'], + ], + + 'correction' => [ + 'view' => 'docs/correction', + 'title' => 'Correction', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '7 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Key files and routes', 'href' => '#key-files-routes'], + ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'], + ['label' => 'Format validation', 'href' => '#format-validation'], + ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'], + ['label' => 'Data validation', 'href' => '#data-validation'], + ['label' => 'employeesCorrectionProcess', 'href' => '#correction-process'], + ['label' => 'Excel columns', 'href' => '#excel-columns'], + ['label' => 'Row examples', 'href' => '#row-example'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'Deletion', 'url' => 'docs/deletion'], + 'next' => ['label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'], + ], + + 'si-enhancement' => [ + 'view' => 'docs/si-enhancement', + 'title' => 'SI Enhancement', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '7 min read', + 'toc' => [ + ['label' => 'Overview', 'href' => '#overview'], + ['label' => 'Key files and routes', 'href' => '#key-files-routes'], + ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'], + ['label' => 'Format validation', 'href' => '#format-validation'], + ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'], + ['label' => 'Data validation', 'href' => '#data-validation'], + ['label' => 'employeesSIEnhanceProcess', 'href' => '#si-process'], + ['label' => 'Excel columns', 'href' => '#excel-columns'], + ['label' => 'Row examples', 'href' => '#row-example'], + ['label' => 'Developer steps', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'Correction', 'url' => 'docs/correction'], + 'next' => ['label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'], + ], + + 'non-eb-opportunities' => [ + 'view' => 'docs/non-eb-opportunities', + 'title' => 'Non-EB Opportunities', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '18 min read', + 'toc' => [ + ['label' => 'End-to-end flow', 'href' => '#overview'], + ['label' => 'Key files', 'href' => '#key-files'], + ['label' => 'Google Sheet config', 'href' => '#google-sheet-config'], + ['label' => 'App ↔ Drive flow', 'href' => '#gsheet-app-drive-flow', 'level' => 'h3'], + ['label' => 'Before RFQ / QCR', 'href' => '#gsheet-prerequisites', 'level' => 'h3'], + ['label' => 'Sheet ID per product', 'href' => '#gsheet-product-config', 'level' => 'h3'], + ['label' => 'App-level config', 'href' => '#gsheet-app-config', 'level' => 'h3'], + ['label' => 'Create opportunity', 'href' => '#create-edit'], + ['label' => 'Edit flow', 'href' => '#edit-flow'], + ['label' => 'List table actions', 'href' => '#list-actions'], + ['label' => 'Action endpoint map', 'href' => '#action-map', 'level' => 'h3'], + ['label' => 'RFQ', 'href' => '#step-rfq'], + ['label' => 'QCR', 'href' => '#step-qcr'], + ['label' => 'Mail actions', 'href' => '#step-mails'], + ['label' => 'Placement', 'href' => '#step-placement'], + ['label' => 'Status lifecycle', 'href' => '#status-lifecycle'], + ['label' => 'Controller reference', 'href' => '#controller-reference'], + ['label' => 'Developer checklist', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'], + 'next' => ['label' => 'Non-EB Claims', 'url' => 'docs/non-eb-claims'], + ], + + 'non-eb-claims' => [ + 'view' => 'docs/non-eb-claims', + 'title' => 'Non-EB Claims', + 'breadcrumb' => 'Features', + 'last_updated' => 'May 2026', + 'author' => 'Core Team', + 'read_time' => '16 min read', + 'toc' => [ + ['label' => 'End-to-end flow', 'href' => '#overview'], + ['label' => 'Why policy type 50', 'href' => '#policy-type-50-default'], + ['label' => 'Design assumption', 'href' => '#policy-type-50-assumption', 'level' => 'h3'], + ['label' => '50 vs selected policy', 'href' => '#policy-type-50-vs-selected-policy', 'level' => 'h3'], + ['label' => 'Where 50 is hardcoded', 'href' => '#policy-type-50-where-hardcoded', 'level' => 'h3'], + ['label' => 'Different status set', 'href' => '#policy-type-50-future', 'level' => 'h3'], + ['label' => 'Key files and routes', 'href' => '#key-files'], + ['label' => 'Route map', 'href' => '#route-map', 'level' => 'h3'], + ['label' => 'Access control', 'href' => '#access'], + ['label' => 'List and filter', 'href' => '#list-flow'], + ['label' => 'Create and edit', 'href' => '#create-edit'], + ['label' => 'Status-driven sections', 'href' => '#status-sections'], + ['label' => 'Auto-mail', 'href' => '#auto-mail'], + ['label' => 'Mail template CRUD', 'href' => '#mail-template-crud'], + ['label' => 'Template fields', 'href' => '#template-fields', 'level' => 'h3'], + ['label' => 'Template actions', 'href' => '#template-actions', 'level' => 'h3'], + ['label' => 'Notes', 'href' => '#notes'], + ['label' => 'Documents', 'href' => '#documents'], + ['label' => 'Reports', 'href' => '#reports'], + ['label' => 'Data model', 'href' => '#data-model'], + ['label' => 'Controller reference', 'href' => '#controller-reference'], + ['label' => 'Developer checklist', 'href' => '#developer-steps'], + ['label' => 'Common pitfalls', 'href' => '#pitfalls'], + ], + 'prev' => ['label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'], 'next' => ['label' => 'Endpoints', 'url' => 'docs/endpoints'], ], diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index 05873250..3688f759 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -517,7 +517,7 @@ class EmployeeController extends AdminController ") ->join('client_policy', 'client_policy.id = batch_files.client_policy_id') ->join('insurers', 'client_policy.insurer_id = insurers.id') - ->join('tpa', 'client_policy.tpa_id = tpa.id') + ->join('tpa', 'client_policy.tpa_id = tpa.id', 'left') ->join('client_branch', 'client_branch.id = batch_files.client_branch_id') ->join('policy_type', 'policy_type.id = client_policy.policy_type_id') ->join('clients', 'clients.id = client_policy.client_id') @@ -574,7 +574,6 @@ class EmployeeController extends AdminController ->orderBy('batch_files.id', 'desc') ->find(); - // dd($data['fileList']);die(); if ($this->request->getMethod() == "get") { $this->loadLayout('import_export', $data); @@ -4175,6 +4174,7 @@ class EmployeeController extends AdminController } // print_r($allUpdates);die(); // Do a single batch update for all families/members + log_message('error','[WellnessOnboard] DB update data - ' . json_encode($allUpdates)); if (!empty($allUpdates)) { // 2nd param is the key to match on; here it's 'id' $this->employeePolicyModel->updateBatch($allUpdates, 'id'); @@ -4255,25 +4255,36 @@ class EmployeeController extends AdminController $emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id); // Reconcile DB records against TPA records and classify records for rec_type updates. + $consumedTpaIdsByEmpCode = []; foreach ($emp_data_wo_tpa_id as $db_key => $db_row) { - $tpa_temp_data = $tpaByEmpCode[$db_row['emp_code']] ?? []; - $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data); + $empCode = (string) ($db_row['emp_code'] ?? ''); + $empId = (string) ($db_row['id'] ?? ''); + $empName = (string) ($db_row['name'] ?? ''); + $tpa_temp_data = $tpaByEmpCode[$empCode] ?? []; + $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? []; + $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data, $excludeTpaIds); $emp_data_wo_tpa_id[$db_key]['match'] = $match; if (($match['status'] ?? '') === 'matched') { $matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0); + // echo 'matched' . $matchedTpaId . ' for emp_code ' . $empCode . PHP_EOL . '
'; if ($matchedTpaId > 0) { + $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId; // If compare-fields list has differences, the row must be reviewed. // Otherwise keep it as matched. $recTypeById[$matchedTpaId] = empty($match['not_matching']) ? 'matched' : 'need_to_review'; } } else { - // No relation-level match found for this DB member. - // Mark all candidate TPA rows for the same emp_code as review-required. + // echo 'not matched' . ' for emp_code ' . $empCode . PHP_EOL . ' - ' . $empId . ' - ' . $empName . ' - ' . '
'; + // No match for this DB member: flag same-relation TPA rows not already paired. + $dbRel = strtolower(trim((string) ($db_row['relationship'] ?? ''))); foreach ($tpa_temp_data as $candidate) { + if (strtolower(trim((string) ($candidate['relation'] ?? ''))) !== $dbRel) { + continue; + } $candidateId = (int) ($candidate['id'] ?? 0); - if ($candidateId > 0) { - $recTypeById[$candidateId] = 'need_to_review'; + if ($candidateId > 0 && !in_array($candidateId, $excludeTpaIds, true)) { + $recTypeById[$candidateId] = ''; } } } @@ -4306,76 +4317,75 @@ class EmployeeController extends AdminController $db->transComplete(); } - // this will match tpa api data with emp/emp policy table and update ref in tpa api data once - $this->reconTpaApiDataWithEmployeepolicies(['file_id' => $file_id]); + // this will match tpa api data with emp/emp policy table and update ref in tpa api data once + $this->reconTpaApiDataWithEmployeepolicies(['file_id' => $file_id]); } else { - // echo 'else';die; // Cached mode: - // Read previously classified rows from rec_type, keep response shape compatible - // with existing UI/export (`mismatch_data` still contains DB row + match payload). + // `not_in_nhance` stays sourced from persisted rec_type snapshot. + // `mismatch_data` must match compute-mode shape: every policy row with + // tpa_id NULL gets reconcileDbWithTpa against *all* active TPA rows for + // that emp_code (not only rows flagged need_to_review), otherwise second+ + // loads drop rows / lose not_matching vs the first compute pass. $not_in_nhance = $tpaApiDataModel->select('*') ->where('is_active', 1) ->where('file_id', $file_id) ->where('rec_type', 'not_in_nhance') ->findAll(); - $needToReviewRows = $tpaApiDataModel->select('*') - ->where('is_active', 1) + $allActiveTpaRowsCached = $tpaApiDataModel->select('*') ->where('file_id', $file_id) - ->where('rec_type', 'need_to_review') + ->where('is_active', 1) ->findAll(); - $needToReviewByEmpCode = []; - foreach ($needToReviewRows as $row) { - $needToReviewByEmpCode[$row['emp_code']][] = $row; + $tpaByEmpCodeCached = []; + foreach ($allActiveTpaRowsCached as $tpaRow) { + $tpaByEmpCodeCached[$tpaRow['emp_code']][] = $tpaRow; } - $baseRows = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id); - foreach ($baseRows as $db_row) { - $candidates = $needToReviewByEmpCode[$db_row['emp_code']] ?? []; - if (empty($candidates)) { - continue; - } - - $selectedTpa = null; - foreach ($candidates as $candidate) { - // Prefer same-relation row to mimic reconcileDbWithTpa relation matching. - if (strtolower((string) ($candidate['relation'] ?? '')) === strtolower((string) ($db_row['relationship'] ?? ''))) { - $selectedTpa = $candidate; - break; + $emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id); + $consumedTpaIdsByEmpCodeCached = []; + foreach ($emp_data_wo_tpa_id as $db_key => $db_row) { + $empCode = (string) ($db_row['emp_code'] ?? ''); + $tpa_temp_data = $tpaByEmpCodeCached[$empCode] ?? []; + $excludeTpaIds = $consumedTpaIdsByEmpCodeCached[$empCode] ?? []; + $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data, $excludeTpaIds); + $emp_data_wo_tpa_id[$db_key]['match'] = $match; + if (($match['status'] ?? '') === 'matched') { + $matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0); + if ($matchedTpaId > 0) { + $consumedTpaIdsByEmpCodeCached[$empCode][] = $matchedTpaId; } } - if ($selectedTpa === null) { - $selectedTpa = $candidates[0]; - } - - $db_row['match'] = [ - 'status' => 'matched', - 'tpa_record' => $selectedTpa, - 'not_matching' => [], - ]; - $emp_data_wo_tpa_id[] = $db_row; } } + // print_rr($emp_data_wo_tpa_id);die(); + + // Intentionally keep `not_in_tpa` live from current join/query logic // (as requested) and do not source it from rec_type snapshot. - $tpa_emp_codes = $tpaApiDataModel->select('emp_code') + $tpa_emp_codes = $tpaApiDataModel->select('ref') ->where('file_id', $file_id) ->where('is_active', 1) - ->groupBy('emp_code') + ->groupBy('ref') ->findAll(); - $tpa_emp_codes = array_column($tpa_emp_codes, 'emp_code'); - $not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, $tpa_emp_codes); + + // $tpa_emp_codes = array_column($tpa_emp_codes, 'ref'); + $tpa_ref = array_column($tpa_emp_codes, 'ref'); + $not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, $tpa_ref); + + // Format dates for UI/export (dd/mm/yyyy). Applied after reconciliation so + // reconcileDbWithTpa can still compare raw Y-m-d values from the database. + $this->formatVariationReportDataForDisplay($not_in_tpa, $not_in_nhance, $emp_data_wo_tpa_id); if (!empty($not_in_tpa) || !empty($not_in_nhance) || !empty($emp_data_wo_tpa_id)) { $not_in_nhance_button_enable_status = false; - if(count($not_in_tpa)) - { - foreach($not_in_nhance as $nih) - { - if($nih['ref'] === '' || empty($nih['ref'])) - { + if (count($not_in_nhance)) { + foreach ($not_in_nhance as $nih) { + $hasRefKey = array_key_exists('ref', $nih); + $ref = $hasRefKey ? $nih['ref'] : null; + $refIsEmpty = $ref === null || $ref === ''; + if ($refIsEmpty) { $not_in_nhance_button_enable_status = true; break; } @@ -4410,6 +4420,21 @@ class EmployeeController extends AdminController $not_in_nhance_deletion_count ); + $not_matched_count = 0; + $not_matched_data = []; + foreach ($emp_data_wo_tpa_id as $key => $row) { + $notMatching = $row['match']['not_matching'] ?? []; + if (isset($row['match']) && is_array($notMatching) && $notMatching !== []) { + $not_matched_count++; + $not_matched_data[] = $row; + } + } + + $need_to_review_proceed_button_text = sprintf( + 'Proceed - Need to Review ( %d )', + $not_matched_count + ); + $response = [ 'not_in_tpa' => $not_in_tpa, 'not_in_nhance' => $not_in_nhance, @@ -4418,6 +4443,9 @@ class EmployeeController extends AdminController 'not_in_nhance_inception_count' => $not_in_nhance_inception_count, 'not_in_nhance_deletion_count' => $not_in_nhance_deletion_count, 'not_in_nhance_proceed_button_text' => $not_in_nhance_proceed_button_text, + 'not_matched_count' => $not_matched_count, + 'not_matched_data' => $not_matched_data, + 'need_to_review_proceed_button_text' => $need_to_review_proceed_button_text, ]; if ($type === 'view') { @@ -4889,10 +4917,12 @@ class EmployeeController extends AdminController } $mismatchRows = []; + $consumedTpaIdsByEmpCode = []; foreach ($dbRows as $dbRow) { + $empCode = (string) ($dbRow['emp_code'] ?? ''); $tpaRows = $TpaApiDataModel->select('*') - ->where('emp_code', $dbRow['emp_code']) + ->where('emp_code', $empCode) ->where('file_id', $batchFileId) ->where('is_active', 1) ->findAll(); @@ -4901,7 +4931,14 @@ class EmployeeController extends AdminController continue; } - $match = $this->reconcileDbWithTpa($dbRow, $tpaRows); + $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? []; + $match = $this->reconcileDbWithTpa($dbRow, $tpaRows, $excludeTpaIds); + if (($match['status'] ?? '') === 'matched') { + $matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0); + if ($matchedTpaId > 0) { + $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId; + } + } if (($match['status'] ?? '') !== 'matched') { continue; @@ -5190,9 +5227,12 @@ class EmployeeController extends AdminController $rowsSkippedNoDiff = 0; $rowsSkippedNoEmployee = 0; + $consumedTpaIdsByEmpCode = []; + foreach ($dbRows as $dbRow) { + $empCode = (string) ($dbRow['emp_code'] ?? ''); $tpaRows = $TpaApiDataModel->select('*') - ->where('emp_code', $dbRow['emp_code'] ?? '') + ->where('emp_code', $empCode) ->where('file_id', $batchFileId) ->where('is_active', 1) ->findAll(); @@ -5201,7 +5241,8 @@ class EmployeeController extends AdminController continue; } - $match = $this->reconcileDbWithTpa($dbRow, $tpaRows); + $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? []; + $match = $this->reconcileDbWithTpa($dbRow, $tpaRows, $excludeTpaIds); if (($match['status'] ?? '') !== 'matched') { continue; @@ -5210,6 +5251,11 @@ class EmployeeController extends AdminController $tpaRecord = $match['tpa_record'] ?? []; $notMatching = $match['not_matching'] ?? []; + $matchedTpaId = (int) ($tpaRecord['id'] ?? 0); + if ($matchedTpaId > 0) { + $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId; + } + if (!is_array($notMatching) || $notMatching === []) { $rowsSkippedNoDiff++; continue; @@ -5524,57 +5570,169 @@ class EmployeeController extends AdminController ]; } - public function reconcileDbWithTpa(array $db, array $tpaRows): array + /** + * Convert a single variation-report date value to dd/mm/yyyy for display. + */ + private function formatVariationReportDateField($value): string { - // Name normalization - $normalizeName = function ($name) { + if ($value === null || $value === '') { + return ''; + } + + $formatted = change_date_format((string) $value, null, 'd/m/Y'); + + return ($formatted !== null && $formatted !== '') ? (string) $formatted : (string) $value; + } + + /** + * Format known date columns on one variation-report row (TPA or DB). + */ + private function formatVariationReportRowDates(array $row, array $dateFields = ['dob', 'doj']): array + { + foreach ($dateFields as $field) { + if (!array_key_exists($field, $row)) { + continue; + } + if ($row[$field] === null || $row[$field] === '') { + continue; + } + $row[$field] = $this->formatVariationReportDateField($row[$field]); + } + + return $row; + } + + /** + * Apply dd/mm/yyyy formatting to all variation-report payloads returned to the UI/export. + */ + private function formatVariationReportDataForDisplay( + array &$notInTpa, + array &$notInNhance, + array &$mismatchData + ): void { + foreach ($notInTpa as $idx => $row) { + $notInTpa[$idx] = $this->formatVariationReportRowDates($row); + } + + foreach ($notInNhance as $idx => $row) { + $notInNhance[$idx] = $this->formatVariationReportRowDates($row); + } + + foreach ($mismatchData as $idx => $row) { + $row = $this->formatVariationReportRowDates($row); + if (isset($row['match']['tpa_record']) && is_array($row['match']['tpa_record'])) { + $row['match']['tpa_record'] = $this->formatVariationReportRowDates($row['match']['tpa_record']); + } + $mismatchData[$idx] = $row; + } + } + + /** + * Pair one Nhance policy row with a TPA API row for the same emp_code. + * + * When multiple dependents share a relation (e.g. two Sons), candidates are scored + * on name / DOB / gender and the best unique match wins. Already-paired TPA ids + * (same emp_code) can be passed via $excludeTpaIds so each TPA row maps once. + */ + public function reconcileDbWithTpa(array $db, array $tpaRows, array $excludeTpaIds = []): array + { + $normalizeName = static function ($name) { return strtolower( - preg_replace('/[.\s_]+/', '', trim($name)) + preg_replace('/[.\s_]+/', '', trim((string) $name)) ); }; - foreach ($tpaRows as $tpa) { + $normalizeRelation = static function ($relation) { + return strtolower(trim((string) $relation)); + }; - // 1️⃣ emp_code + relation must match - if ( - // ($db['emp_code'] ?? '') !== ($tpa['emp_code'] ?? '') || - strtolower($db['relationship']) !== strtolower($tpa['relation']) - ) { + $dbRel = $normalizeRelation($db['relationship'] ?? ''); + + $candidates = []; + foreach ($tpaRows as $tpa) { + if (isset($tpa['match']['status']) && $tpa['match']['status'] === 'matched') { continue; } - // 2️⃣ Field comparison - $diff = []; - - if ( - ($db['name'] ?? '') !== - ($tpa['name'] ?? '') - ) { - $diff[] = 'name'; + $tpaId = (int) ($tpa['id'] ?? 0); + if ($tpaId > 0 && in_array($tpaId, $excludeTpaIds, true)) { + continue; } - if (($db['dob'] ?? '') !== ($tpa['dob'] ?? '')) { - $diff[] = 'dob'; + if ($normalizeRelation($tpa['relation'] ?? '') !== $dbRel) { + continue; } - if ( - strtoupper($db['gender'] ?? '') !== - strtoupper($tpa['gender'] ?? '') - ) { - $diff[] = 'gender'; - } - - // 3️⃣ Match found - return [ - 'status' => 'matched', - 'tpa_record' => $tpa, - 'not_matching' => $diff // empty = perfect match - ]; + $candidates[] = $tpa; + } + + if ($candidates === []) { + return ['status' => 'no_match']; + } + + $scoreCandidate = static function (array $tpa) use ($db, $normalizeName) { + $score = 0; + if ($normalizeName($db['name'] ?? '') === $normalizeName($tpa['name'] ?? '')) { + $score += 4; + } + if ((string) ($db['dob'] ?? '') === (string) ($tpa['dob'] ?? '')) { + $score += 2; + } + if (strtoupper(trim((string) ($db['gender'] ?? ''))) === strtoupper(trim((string) ($tpa['gender'] ?? '')))) { + $score += 1; + } + + return $score; + }; + + $bestTpa = null; + $bestScore = -1; + foreach ($candidates as $tpa) { + $score = $scoreCandidate($tpa); + if ($score > $bestScore) { + $bestScore = $score; + $bestTpa = $tpa; + } + } + + if ($bestTpa === null) { + return ['status' => 'no_match']; + } + + // Multiple Son/Daughter rows: require a unique tie-breaker (name or DOB). + if (count($candidates) > 1) { + $topCount = 0; + foreach ($candidates as $tpa) { + if ($scoreCandidate($tpa) === $bestScore) { + $topCount++; + } + } + if ($topCount > 1 || $bestScore < 2) { + return ['status' => 'no_match']; + } + } + + $diff = []; + + if ($normalizeName($db['name'] ?? '') !== $normalizeName($bestTpa['name'] ?? '')) { + $diff[] = 'name'; + } + + if ((string) ($db['dob'] ?? '') !== (string) ($bestTpa['dob'] ?? '')) { + $diff[] = 'dob'; + } + + if ( + strtoupper(trim((string) ($db['gender'] ?? ''))) !== + strtoupper(trim((string) ($bestTpa['gender'] ?? ''))) + ) { + $diff[] = 'gender'; } - // 4️⃣ No match found return [ - 'status' => 'no_match' + 'status' => 'matched', + 'tpa_record' => $bestTpa, + 'not_matching' => $diff, ]; } diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 9518d202..3066ada1 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -3785,14 +3785,6 @@ class EmployeeRestController extends AdminController $this->handleCliamFiles($file_data, $ticket_id, $ticket_message_id); - // Merge all uploaded PDFs for this ticket into a single combined PDF - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $ticket_id); - } catch (\Throwable $e) { - log_message('error', 'EmployeeRestController::initiateClaim | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); - } - $mail_sent_status = ($this->ticketController->sendAutoMailTrigger($ticket_id)); if (gettype($mail_sent_status) == 'array') { $message = 'Claim Iniated Successfully'; @@ -3931,6 +3923,15 @@ class EmployeeRestController extends AdminController } } + if (! empty($insert_ids)) { + try { + helper('merge_pdf'); + merge_ticket_pdfs((int) $ticket_id); + } catch (\Throwable $e) { + log_message('error', 'EmployeeRestController::handleCliamFiles | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); + } + } + $count = $this->ticketMaster->where('id', $ticket_id)->where('is_active', 1)->where('tpa_claim_push_reference_no IS NULL')->countAllResults(); log_message('error', 'Ticket validation | Ticket ID: ' . $ticket_id . ' | Matching active tickets with NULL TPA reference: ' . $count); if ($count > 0 && $pdf_exist_in_the_file && $tpa_claim_push == true) { @@ -5452,14 +5453,6 @@ class EmployeeRestController extends AdminController $result = $this->handleCliamFiles($file_data, $ticket_id, null, false, true); if (! empty($result)) { - // Merge all uploaded PDFs for this ticket into a single combined PDF - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $ticket_id); - } catch (\Throwable $e) { - log_message('error', 'EmployeeRestController::uploadIRDocs | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); - } - // $this->ticketMaster->where('id', $ticket_id)->set(['required_docs', $required_docs])->update(); db_connect()->query( "UPDATE ticket_master SET required_docs = ? WHERE id = ?", diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php index cbaf1115..6d684e1c 100644 --- a/app/Controllers/FhplApiController.php +++ b/app/Controllers/FhplApiController.php @@ -554,30 +554,32 @@ class FhplApiController extends BaseController foreach ($employeePolicyData as $policy) { $hasMatchForThisPolicy = false; foreach ($allMembers as $m) { + $apiRelation = map_relationship(trim($m['RELATION'] ?? '')); if ( - strtolower(trim($policy['name'])) === strtolower(trim($m['BENEFICIARY_NAME'] ?? '')) && - ($policy['emp_code'] ?? '') == ($m['EMPLOYEE_ID'] ?? '') && - strtolower($policy['relationship']) === strtolower($m['RELATION'] ?? '') + strtolower(trim((string) ($policy['name'] ?? ''))) === strtolower(trim((string) ($m['BENEFICIARY_NAME'] ?? ''))) + && trim((string) ($policy['emp_code'] ?? '')) === trim((string) ($m['EMPLOYEE_ID'] ?? '')) + && strtolower(trim((string) ($policy['relationship'] ?? ''))) === strtolower(trim((string) $apiRelation)) ) { - $hasMatchForThisPolicy = true; - - $sql = "UPDATE employee_polices SET tpa_id = ? WHERE id = ?"; - $this->db->query($sql, [$m['MEMBERSHIP_NO'], $policy['emp_policy_id']]); + $tpaId = $m['MEMBERSHIP_NO'] ?? $m['MEMBERSHIP_NO'] ?? null; + $sql = 'UPDATE employee_polices SET tpa_id = ? WHERE id = ?'; + $this->db->query($sql, [$tpaId, $policy['emp_policy_id']]); // for e-card send - if(strtolower(trim($policy['relationship'])) == 'self'){ + if (strtolower(trim((string) ($policy['relationship'] ?? ''))) === 'self') { $employee_policy_ids[] = $policy['emp_policy_id']; } - + if ($this->db->affectedRows() > 0) { - $updated++; - log_message('error', "FHPL - TPA ID Pull Updated tpa_id={$m['MEMBERSHIP_NO']} for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}"); + $updated++; + log_message('error', "FHPL - TPA ID Pull Updated tpa_id={$tpaId} for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}"); } else { - log_message('error', "FHPL - TPA ID Pull No update (already set or not matched) for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}"); + log_message('error', "FHPL - TPA ID Pull No update (already set or not matched) for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}"); } + + break; } } @@ -1070,7 +1072,7 @@ class FhplApiController extends BaseController 'gender' => format_gender_v2($row['GENDER'] ?? null), 'self' => strtolower($row['RELATION'] ?? '') === 'self' ? 1 : 0, - 'tpa_id' => trim($row['TPA_TPADETAIL_ID'] ?? null), + 'tpa_id' => trim($row['MEMBERSHIP_NO'] ?? null), 'age' => is_numeric($row['AGE'] ?? null) ? (int) $row['AGE'] : null, 'is_active' => 1, diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index 7f347d6f..0b1fc090 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -368,8 +368,10 @@ class LeadsController extends BaseController $id = $this->request->getPost('id'); $actual_lead_id = $this->request->getPost('actual_lead_id'); $actual_lead_id = ! empty($actual_lead_id) ? $actual_lead_id : null; - $postData = $this->request->getPost(); - $data = $this->prepareLeadData(); + + $this->normalizeClaimHistoryPostData(); + $postData = $this->request->getPost(); + $data = $this->prepareLeadData(); $rules = [ @@ -526,47 +528,65 @@ class LeadsController extends BaseController ]; if ((int) $this->request->getPost('claim_history') === 1) { - $rules['first_year.*'] = [ - 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', - '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 _-]+$/]', - 'errors' => ['required' => 'Policy Type is required in Claim History.', - 'regex_match' => 'Policy Type only letters, numbers, space, hyphens and underscores are allowed', - ], - ]; - $rules['first_date_of_loss_.*'] = [ - 'rules' => 'required|regex_match[/^[0-9]{2}-[0-9]{2}-[0-9]{4}$/]', - 'errors' => ['required' => 'Date of Loss is required.', - 'regex_match' => 'Date of Loss must be inValid format.'], - ]; - $rules['first_cause_of_loss.*'] = [ - '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', - ], - ]; - $rules['first_claim_amount.*'] = [ - 'rules' => 'required|numeric', - 'errors' => [ - 'required' => 'Claim Amount is required.', - 'numeric' => 'Claim Amount must be a number.', - ], - ]; - $rules['first_settled_amount.*'] = [ - 'rules' => 'required|numeric', - 'errors' => ['required' => 'Settled Amount is required.', - 'numeric' => 'Settled Amount must be a number.'], - ]; - $rules['first_claim_status.*'] = [ - 'rules' => 'required|alpha_space', - 'errors' => [ - 'required' => 'Claim Status is required.', - 'alpha_space' => 'Claim Status should only contain letters and spaces.', - ], - ]; + $nilClaimsRows = $this->resolveNilClaimRowFlags($postData); + $claimRowCount = $this->getClaimHistoryRowCount($postData, $nilClaimsRows); + + for ($claimIndex = 0; $claimIndex < $claimRowCount; $claimIndex++) { + $rules["first_year.$claimIndex"] = [ + 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', + 'errors' => [ + 'required' => 'Claim Year is required for all entries.', + 'regex_match' => 'Year must be in format YYYY-YYYY.', + ], + ]; + + if (! empty($nilClaimsRows[$claimIndex])) { + continue; + } + + $rules["first_policy_type_.$claimIndex"] = [ + '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', + ], + ]; + $rules["first_date_of_loss_.$claimIndex"] = [ + 'rules' => 'required|regex_match[/^[0-9]{2}-[0-9]{2}-[0-9]{4}$/]', + 'errors' => [ + 'required' => 'Date of Loss is required.', + 'regex_match' => 'Date of Loss must be inValid format.', + ], + ]; + $rules["first_cause_of_loss.$claimIndex"] = [ + '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', + ], + ]; + $rules["first_claim_amount.$claimIndex"] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Claim Amount is required.', + 'numeric' => 'Claim Amount must be a number.', + ], + ]; + $rules["first_settled_amount.$claimIndex"] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Settled Amount is required.', + 'numeric' => 'Settled Amount must be a number.', + ], + ]; + $rules["first_claim_status.$claimIndex"] = [ + 'rules' => 'required|alpha_space', + 'errors' => [ + 'required' => 'Claim Status is required.', + 'alpha_space' => 'Claim Status should only contain letters and spaces.', + ], + ]; + } } } @@ -576,65 +596,75 @@ class LeadsController extends BaseController $claimHistoryPolicyTypes = [1, 6, 7]; $hasClaimHistoryPolicy = count(array_intersect($postedPolicyTypeIds, $claimHistoryPolicyTypes)) > 0; $isEbClaimHistory = $leadFormType === 1 - && in_array((int) ($postData['lead_type'] ?? 0), [2, 3], true) + && in_array((int) ($postData['lead_type'] ?? 0), [1, 2, 3], true) && $hasClaimHistoryPolicy && (int) ($postData['claim_history'] ?? 0) === 1; if ($isEbClaimHistory) { - $rules['first_year.*'] = [ - 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', - 'errors' => [ - 'required' => 'Claim Year is required for all entries.', - 'regex_match' => 'Year must be in format YYYY-YYYY.', - ], - ]; - $rules['emp_id.*'] = [ - 'rules' => 'required', - 'errors' => ['required' => 'Employee ID is required in Claim History.'], - ]; - $rules['emp_name.*'] = [ - 'rules' => 'required', - 'errors' => ['required' => 'Employee Name is required in Claim History.'], - ]; - $rules['gender.*'] = [ - 'rules' => 'required|in_list[Female,Male]', - 'errors' => [ - 'required' => 'Gender is required in Claim History.', - 'in_list' => 'Gender must be Female or Male.', - ], - ]; - $rules['designation.*'] = [ - 'rules' => 'required', - 'errors' => ['required' => 'Designation is required in Claim History.'], - ]; - $rules['sum_insured.*'] = [ - 'rules' => 'required|numeric', - 'errors' => [ - 'required' => 'Sum Insured is required in Claim History.', - 'numeric' => 'Sum Insured must be a number.', - ], - ]; - $rules['first_death_date.*'] = [ - 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', - 'errors' => [ - 'required' => 'Date of Death is required.', - 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', - ], - ]; - $rules['first_cause_of_death.*'] = [ - 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', - 'errors' => [ - 'required' => 'Nature/Cause Of Death is required.', - 'in_list' => 'Nature/Cause Of Death is invalid.', - ], - ]; - $rules['first_claim_amount.*'] = [ - 'rules' => 'required|numeric', - 'errors' => [ - 'required' => 'Claim/Settled Amount is required.', - 'numeric' => 'Claim/Settled Amount must be a number.', - ], - ]; + $nilClaimsRows = $this->resolveNilClaimRowFlags($postData); + $claimRowCount = $this->getClaimHistoryRowCount($postData, $nilClaimsRows); + + for ($claimIndex = 0; $claimIndex < $claimRowCount; $claimIndex++) { + $rules["first_year.$claimIndex"] = [ + 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', + 'errors' => [ + 'required' => 'Claim Year is required for all entries.', + 'regex_match' => 'Year must be in format YYYY-YYYY.', + ], + ]; + + if (! empty($nilClaimsRows[$claimIndex])) { + continue; + } + + $rules["emp_id.$claimIndex"] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee ID is required in Claim History.'], + ]; + $rules["emp_name.$claimIndex"] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee Name is required in Claim History.'], + ]; + $rules["gender.$claimIndex"] = [ + 'rules' => 'required|in_list[Female,Male]', + 'errors' => [ + 'required' => 'Gender is required in Claim History.', + 'in_list' => 'Gender must be Female or Male.', + ], + ]; + $rules["designation.$claimIndex"] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Designation is required in Claim History.'], + ]; + $rules["sum_insured.$claimIndex"] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Sum Insured is required in Claim History.', + 'numeric' => 'Sum Insured must be a number.', + ], + ]; + $rules["first_death_date.$claimIndex"] = [ + 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', + 'errors' => [ + 'required' => 'Date of Death is required.', + 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', + ], + ]; + $rules["first_cause_of_death.$claimIndex"] = [ + 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', + 'errors' => [ + 'required' => 'Nature/Cause Of Death is required.', + 'in_list' => 'Nature/Cause Of Death is invalid.', + ], + ]; + $rules["first_claim_amount.$claimIndex"] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Claim/Settled Amount is required.', + 'numeric' => 'Claim/Settled Amount must be a number.', + ], + ]; + } } // 1. MANUALLY VALIDATE FILES BEFORE PROCESSING @@ -2866,7 +2896,9 @@ class LeadsController extends BaseController //claim history new sheet; if ($claim_history == 1 && !empty($rfq_data['fin_years_claims'])) { - $claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? []; + $claim_details = $this->sanitizeClaimDetailsForExport( + json_decode($rfq_data['fin_years_claims'], true) ?? [] + ); if (! empty($claim_details['finyear'])) { @@ -4654,6 +4686,209 @@ class LeadsController extends BaseController } } + private function normalizeClaimHistoryPostData(): void + { + $post = $this->request->getPost(); + + if ((int) ($post['claim_history'] ?? 0) !== 1) { + return; + } + + $rows = $this->getFinyearRowsFromPost($post); + + if ($rows === []) { + return; + } + + $leadFormType = (int) ($post['lead_form_type'] ?? 1); + $claimArrays = $leadFormType === 2 + ? $this->buildNonEbClaimHistoryPostArrays($rows) + : $this->buildEbClaimHistoryPostArrays($rows); + + $this->request->setGlobal('post', array_merge($post, $claimArrays)); + } + + /** + * Remove internal nil_claims flag from stored/exported claim rows. + * + * @param array $claimDetails + * + * @return array + */ + private function sanitizeClaimDetailsForExport(array $claimDetails): array + { + if (empty($claimDetails['finyear']) || ! is_array($claimDetails['finyear'])) { + return $claimDetails; + } + + $claimDetails['finyear'] = array_map(static function ($record) { + if (! is_array($record)) { + return $record; + } + + unset($record['nil_claims']); + + return $record; + }, $claimDetails['finyear']); + + return $claimDetails; + } + + /** + * @return list> + */ + private function getFinyearRowsFromPost(array $postData): array + { + $jsonPayload = $postData['finyear'] ?? $postData['fin_years_claims'] ?? null; + + if (! is_string($jsonPayload) || $jsonPayload === '') { + return []; + } + + $decoded = json_decode($jsonPayload, true); + + if (! is_array($decoded) || empty($decoded['finyear']) || ! is_array($decoded['finyear'])) { + return []; + } + + return $decoded['finyear']; + } + + /** + * @param list> $rows + * + * @return array> + */ + private function buildEbClaimHistoryPostArrays(array $rows): array + { + $arrays = [ + 'first_year' => [], + 'nil_claims' => [], + 'emp_id' => [], + 'emp_name' => [], + 'gender' => [], + 'designation' => [], + 'sum_insured' => [], + 'first_death_date' => [], + 'first_cause_of_death' => [], + 'first_claim_amount' => [], + ]; + + foreach ($rows as $row) { + $isNil = ! empty($row['nil_claims']); + + $arrays['first_year'][] = (string) ($row['year'] ?? ''); + $arrays['nil_claims'][] = $isNil ? '1' : '0'; + + if ($isNil) { + $arrays['emp_id'][] = 'Nil'; + $arrays['emp_name'][] = 'Nil'; + $arrays['gender'][] = 'Nil'; + $arrays['designation'][] = 'Nil'; + $arrays['sum_insured'][] = 'Nil'; + $arrays['first_death_date'][] = 'Nil'; + $arrays['first_cause_of_death'][] = 'Nil'; + $arrays['first_claim_amount'][] = 'Nil'; + } else { + $arrays['emp_id'][] = (string) ($row['emp_id'] ?? ''); + $arrays['emp_name'][] = (string) ($row['emp_name'] ?? ''); + $arrays['gender'][] = (string) ($row['gender'] ?? ''); + $arrays['designation'][] = (string) ($row['designation'] ?? ''); + $arrays['sum_insured'][] = (string) ($row['sum_insured'] ?? ''); + $arrays['first_death_date'][] = (string) ($row['death_date'] ?? ''); + $arrays['first_cause_of_death'][] = (string) ($row['cause_of_death'] ?? ''); + $arrays['first_claim_amount'][] = (string) ($row['settled'] ?? ($row['claim_amount'] ?? '')); + } + } + + return $arrays; + } + + /** + * @param list> $rows + * + * @return array> + */ + private function buildNonEbClaimHistoryPostArrays(array $rows): array + { + $arrays = [ + 'first_year' => [], + 'nil_claims' => [], + 'first_policy_type_' => [], + 'first_date_of_loss_' => [], + 'first_cause_of_loss' => [], + 'first_claim_amount' => [], + 'first_settled_amount' => [], + 'first_claim_status' => [], + ]; + + foreach ($rows as $row) { + $isNil = ! empty($row['nil_claims']); + + $arrays['first_year'][] = (string) ($row['year'] ?? ''); + $arrays['nil_claims'][] = $isNil ? '1' : '0'; + + if ($isNil) { + $arrays['first_policy_type_'][] = 'Nil'; + $arrays['first_date_of_loss_'][] = 'Nil'; + $arrays['first_cause_of_loss'][] = 'Nil'; + $arrays['first_claim_amount'][] = 'Nil'; + $arrays['first_settled_amount'][] = 'Nil'; + $arrays['first_claim_status'][] = 'Nil'; + } else { + $arrays['first_policy_type_'][] = (string) ($row['policy_type'] ?? ''); + $arrays['first_date_of_loss_'][] = (string) ($row['date_of_loss'] ?? ''); + $arrays['first_cause_of_loss'][] = (string) ($row['cause_of_loss'] ?? ''); + $arrays['first_claim_amount'][] = (string) ($row['claim_amount'] ?? ''); + $arrays['first_settled_amount'][] = (string) ($row['settled_amount'] ?? ''); + $arrays['first_claim_status'][] = (string) ($row['status'] ?? ''); + } + } + + return $arrays; + } + + /** + * @return array Row index => 1 when nil claims, else 0 + */ + private function resolveNilClaimRowFlags(array $postData): array + { + $flags = []; + + foreach ((array) ($postData['nil_claims'] ?? []) as $idx => $val) { + $flags[(int) $idx] = (int) $val; + } + + foreach ($this->getFinyearRowsFromPost($postData) as $idx => $row) { + if (! is_array($row)) { + continue; + } + + if (! empty($row['nil_claims'])) { + $flags[(int) $idx] = 1; + } elseif (! isset($flags[(int) $idx])) { + $flags[(int) $idx] = 0; + } + } + + return $flags; + } + + /** + * @param array $nilClaimFlags + */ + private function getClaimHistoryRowCount(array $postData, array $nilClaimFlags): int + { + $counts = [ + count((array) ($postData['first_year'] ?? [])), + count($nilClaimFlags), + ]; + + $counts[] = count($this->getFinyearRowsFromPost($postData)); + + return max(0, ...$counts); + } + public function getLastFiveFinancialYears(): array { $year = (int) date('Y'); @@ -4921,8 +5156,11 @@ class LeadsController extends BaseController $data['lead_edit_data']['multi_file_html'] = trim($html) !== '' ? $html : null; } - if (! in_array((int) $data['lead_edit_data']['lead_type'], [1, 3], true) && $data['lead_edit_data']['lead_form_type'] == 2) { - $data['lead_edit_data']['claims_details_html'] = view('rfq/claims_details_non_eb', $data['lead_edit_data']); + if (in_array((int) $data['lead_edit_data']['lead_type'], [1, 2, 3], true) && $data['lead_edit_data']['lead_form_type'] == 2) { + $data['lead_edit_data']['claims_details_html'] = view('rfq/claims_details_non_eb', [ + 'lead_edit_data' => $data['lead_edit_data'], + 'lastFiveYears' => $data['lastFiveYears'] ?? $this->getLastFiveFinancialYears(), + ]); } else { $data['lead_edit_data']['claims_details_html'] = ""; } @@ -5372,6 +5610,16 @@ class LeadsController extends BaseController { helper('excel_util_helper'); + $finyear = array_map(static function ($record) { + if (! is_array($record)) { + return $record; + } + + unset($record['nil_claims']); + + return $record; + }, $finyear); + $first = $finyear[0] ?? null; if (! is_array($first) || $first === []) { @@ -5844,8 +6092,11 @@ class LeadsController extends BaseController public function generateViewPageHtml($policy_type_id, $data = []) { - $data['insurer'] = $this->insurerBranchModel->getInsurerBranchesWithInsurerNames(); - $data['tpa'] = $this->tpaBranchModel->getTpaBranchesWithTpaNames(); + $data['insurer'] = $this->insurerBranchModel->getInsurerBranchesWithInsurerNames(); + $data['tpa'] = $this->tpaBranchModel->getTpaBranchesWithTpaNames(); + $data['lastFiveYears'] = $data['lastFiveYears'] ?? $this->getLastFiveFinancialYears(); + $data['gpaClaimType'] = $data['gpaClaimType'] ?? $this->claim_type_for_gpa; + $data['causeOfDeath'] = $data['causeOfDeath'] ?? $this->cause_of_death; $viewMap = [ 1 => 'rfq/gpa', @@ -6483,7 +6734,9 @@ class LeadsController extends BaseController if (! empty($rfq_data['fin_years_claims']) && $claim_history == 1) { - $claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? []; + $claim_details = $this->sanitizeClaimDetailsForExport( + json_decode($rfq_data['fin_years_claims'], true) ?? [] + ); if (! empty($claim_details['finyear'])) { diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php index fb073f21..209d797c 100755 --- a/app/Controllers/LoginController.php +++ b/app/Controllers/LoginController.php @@ -20,12 +20,9 @@ class LoginController extends BaseController public function index() { - // $session_data = ['isLoggedIn' => True ,'userid' => '25']; - // set_session_data($session_data); - // $isLoggedIn = check_session(); $isLoggedIn = check_session(); $hasCookie = check_cookie(); - if ($isLoggedIn && $hasCookie) { + if ($isLoggedIn || $hasCookie) { return redirect()->to(base_url('/dashboard/view')); } return view('login'); diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 6addab9a..4e219a4a 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -3677,13 +3677,6 @@ class TicketController extends BaseController $employeeRest = new EmployeeRestController(); $result = $employeeRest->handleCliamFiles($file_data, $ticket_id); if(!empty($result)){ - // Merge all uploaded PDFs for this ticket into a single combined PDF - try { - helper('merge_pdf'); - merge_ticket_pdfs((int) $ticket_id); - } catch (\Throwable $e) { - log_message('error', 'TicketController::upload_url | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage()); - } return $this->respond(['status' => true, 'message' => 'File uploaded successfully ']); }else{ return $this->respond(['status' => false, 'message' => 'Failed to upload file ']); diff --git a/app/Controllers/TicketServiceController.php b/app/Controllers/TicketServiceController.php index 24e5e463..ffa6f154 100644 --- a/app/Controllers/TicketServiceController.php +++ b/app/Controllers/TicketServiceController.php @@ -1297,6 +1297,7 @@ class TicketServiceController extends AdminController } else { //check the employee and insured + $params['is_from'] = 'claim_dump'; $employee_data = $ticketMasterModal->getEmployeeAndEmployeePolicyDetails($params) ?? []; // dd($employee_data); if (count($employee_data) == 0) { diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index c222289c..e1ccc426 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -189,7 +189,7 @@ if (! function_exists('merge_ticket_pdfs')) { $insertData = [ 'ticket_id' => $ticket_master_id, - 'ticket_type' => $opts['ticket_type'], + // 'ticket_type' => $opts['ticket_type'], 'file_type' => MERGED_CLAIM_FILE_TYPE, 'doc_name' => 'MERGED_CLAIM_DOCS_PDF', 'file_name' => $mergedName, diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index 566bb703..467fceba 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -19,6 +19,7 @@ abstract class BaseTpaClaimImportService protected $claimDumpFileModel; protected $clientPolicyModel; protected $policyNumberMapping; + protected $tpaTableMapping; public function __construct() { @@ -33,6 +34,14 @@ abstract class BaseTpaClaimImportService (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'Policy Number', (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'POLICY_NO', ]; + $this->tpaTableMapping = [ + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal', + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi', + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist', + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl', + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance', + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici', + ]; } /** @@ -58,8 +67,7 @@ abstract class BaseTpaClaimImportService } if (empty($rows)) { - $this->db->transRollback(); // ROLLBACK BEFORE RETURN - return ['status' => false, 'message' => 'Excel file contains no data or wrong file upload']; + return $this->failTpaClaimDumpInsert($fileId, 'Excel file contains no data or wrong file upload'); } if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){ @@ -70,22 +78,19 @@ abstract class BaseTpaClaimImportService if($client_policy_data['policy_no'] != ($rows[0][$policy_number_column] ?? '')){ - $this->db->transRollback(); - return ['status' => false, 'message' => 'Policy number mismatch in the file and in the system']; + return $this->failTpaClaimDumpInsert($fileId, 'Policy number mismatch in the file and in the system'); } $tpaInsertData = $this->mapTPAData($rows, $fileId); if (empty($tpaInsertData)) { - $this->db->transRollback(); // ROLLBACK BEFORE RETURN - return ['status' => false, 'message' => 'These records already exist in the system.']; + return $this->failTpaClaimDumpInsert($fileId, 'These records already exist in the system.'); } $return_res = $this->bulkInsertTPATable($tpaInsertData); if ($return_res !== true) { - $this->db->transRollback(); // ROLLBACK BEFORE RETURN - return ['status' => false, 'message' => 'TPA Import bulk insert failed']; + return $this->failTpaClaimDumpInsert($fileId, 'TPA Import bulk insert failed'); } // 2. Commit if everything is fine @@ -93,9 +98,7 @@ abstract class BaseTpaClaimImportService return ['status' => true, 'message' => 'File uploaded successfully', 'record_count' => count($tpaInsertData)]; } catch (\Throwable $e) { - // 3. Rollback on any crash/exception - $this->db->transRollback(); - return ['status' => false, 'message' => 'System error : ' . $e->getMessage()]; + return $this->failTpaClaimDumpInsert($fileId, 'System error : ' . $e->getMessage()); } } @@ -113,7 +116,7 @@ abstract class BaseTpaClaimImportService // Check if mapping failed if (!$ticketMasterData['status']) { - $this->db->transRollback(); // ALWAYS rollback before early return + $this->rollbackAndCleanupClaimDumpData($file_id); return $ticketMasterData; } @@ -125,13 +128,11 @@ abstract class BaseTpaClaimImportService if (!empty($ticketMasterData['mapped_array'])) { $insert_res = $this->importClaimMaster($ticketMasterData['mapped_array']); if (!$insert_res) { - $this->db->transRollback(); - return ['status' => false, 'message' => 'Ticket Master Claim bulk insert failed']; + return $this->failTicketMasterInsert($file_id, 'Ticket Master Claim bulk insert failed'); } // Map newly created ticket IDs back to the TPA staging table if (!$this->updateTicketIdInTPATable($file_id)) { - $this->db->transRollback(); - return ['status' => false, 'message' => 'Updating ticket_id in TPA table failed']; + return $this->failTicketMasterInsert($file_id, 'Updating ticket_id in TPA table failed'); } $message .= 'Ticket Master Claim bulk insert success. '; @@ -144,8 +145,7 @@ abstract class BaseTpaClaimImportService if (!empty($ticketMasterData['rejected_reason_array'])) { $update_res = $this->updateTicketMasterRejectedReasonInTPATable($ticketMasterData['rejected_reason_array']); if (!$update_res) { - $this->db->transRollback(); - return ['status' => false, 'message' => 'Updating rejected reasons failed']; + return $this->failTicketMasterInsert($file_id, 'Updating rejected reasons failed'); } $message .= empty($ticketMasterData['mapped_array']) @@ -156,24 +156,69 @@ abstract class BaseTpaClaimImportService // If nothing was processed but no error occurred if (!$hasExecutedTask) { - $this->db->transRollback(); - return ['status' => false, 'message' => 'No data found to process.']; + return $this->failTicketMasterInsert($file_id, 'No data found to process.'); } // 2. Commit the transaction $this->db->transCommit(); + + if (!$status) { + $this->cleanupClaimDumpData($file_id); + } + return ['status' => $status, 'message' => trim($message)]; } catch (\Throwable $th) { - // 3. Rollback on crash - $this->db->transRollback(); - return [ - 'status' => false, - 'message' => 'System error during Ticket Master Insert: ' . $th->getMessage() - ]; + $fileId = (int) ($params['file_id'] ?? 0); + return $this->failTicketMasterInsert( + $fileId, + 'System error during Ticket Master Insert: ' . $th->getMessage() + ); } } + /** + * Remove TPA staging rows and ticket_master rows created for a failed claim dump upload. + */ + protected function cleanupClaimDumpData(int $fileId): void + { + if ($fileId <= 0) { + return; + } + + $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + if (empty($fileData)) { + return; + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + $tpaTable = $this->tpaTableMapping[$tpaId] ?? null; + + if ($tpaTable !== null) { + $this->db->table($tpaTable)->where('file_id', $fileId)->delete(); + } + + $this->db->table('ticket_master')->where('file_id', $fileId)->delete(); + } + + protected function rollbackAndCleanupClaimDumpData(int $fileId): void + { + $this->db->transRollback(); + $this->cleanupClaimDumpData($fileId); + } + + protected function failTpaClaimDumpInsert(int $fileId, string $message): array + { + $this->rollbackAndCleanupClaimDumpData($fileId); + return ['status' => false, 'message' => $message]; + } + + protected function failTicketMasterInsert(int $fileId, string $message): array + { + $this->rollbackAndCleanupClaimDumpData($fileId); + return ['status' => false, 'message' => $message]; + } + /** * Read Excel and return associative rows (header based) */ @@ -324,7 +369,15 @@ abstract class BaseTpaClaimImportService * Dublicate check in the TPA specific table records */ public function getEmployeeDetails(int $client_id, int $client_policy_id, string $emp_code, string $relation): array - { + { + + $client_policy_data = $this->clientPolicyModel->where('id', $client_policy_id)->first(); + if(!empty($client_policy_data)){ + if(!empty($client_policy_data['base_policy'])){ + $client_policy_id = $client_policy_data['base_policy']; + } + } + $EmployeeModel = new EmployeeModel(); $employeeData = $EmployeeModel ->select([ diff --git a/app/Models/ClaimFilesModel.php b/app/Models/ClaimFilesModel.php index 055b9b3b..66bd25ed 100644 --- a/app/Models/ClaimFilesModel.php +++ b/app/Models/ClaimFilesModel.php @@ -59,4 +59,18 @@ class ClaimFilesModel extends Model return $data; } + + /** + * Whether the claim (ticket) has at least one active PDF in claim_files. + */ + public function hasPdfFileForTicket($ticketId): bool + { + return $this->where('ticket_id', $ticketId) + ->where('is_active', 1) + ->groupStart() + ->where('mime_type', 'pdf') + ->orWhere('mime_type', 'application/pdf') + ->groupEnd() + ->countAllResults(false) > 0; + } } diff --git a/app/Models/EmployeePolicyModel.php b/app/Models/EmployeePolicyModel.php index dac8f8d6..474221b2 100755 --- a/app/Models/EmployeePolicyModel.php +++ b/app/Models/EmployeePolicyModel.php @@ -756,14 +756,17 @@ class EmployeePolicyModel extends Model LEFT JOIN ( SELECT emp_code, + group_key, CAST(MAX(CASE WHEN field_name = 'basic_cover_si' THEN new_value END) AS UNSIGNED) AS new_basic_cover_si, CAST(MAX(CASE WHEN field_name = 'premium' THEN new_value END) AS DECIMAL(10,2)) AS new_si_premium, CAST(MAX(CASE WHEN field_name = 'premium' THEN old_value END) AS DECIMAL(10,2)) AS old_si_premium, MAX(CASE WHEN field_name = 'si_enhancement_date' THEN new_value END) AS date_of_coverage FROM emp_endorsement $subquery_endorsement_condition - GROUP BY emp_code - ) AS sidata ON a.emp_code = sidata.emp_code + AND actions = 'si' + AND status != 'truncated' + GROUP BY emp_code, group_key + ) AS sidata ON a.emp_code = sidata.emp_code AND a.group_key = sidata.group_key WHERE employee_polices.client_policy_id = '{$client_policy_id}' AND employees.client_branch_id = '{$client_branch_id}' $endorsement_condition @@ -2252,7 +2255,7 @@ class EmployeePolicyModel extends Model } else if(count($emp_codes) > 0 && $all == false) { - $result->whereNotIn('emp.emp_code',$emp_codes); + $result->whereNotIn('employee_polices.id',$emp_codes); } else if(count($emp_codes) == 0 && $all == true) { diff --git a/app/Models/TicketMasterModel.php b/app/Models/TicketMasterModel.php index 8a6128ca..f6ea661d 100644 --- a/app/Models/TicketMasterModel.php +++ b/app/Models/TicketMasterModel.php @@ -1118,12 +1118,23 @@ class TicketMasterModel extends Model $policy_no = $params['policy_no'] ?? null; $relationship = $params['relationship'] ?? null; $insured_name = $params['insured_name'] ?? null; + $is_from = $params['is_from'] ?? null; // $client_name = "6-Eleven"; // $emp_code = "EMP0020K12"; // $emp_name = "Lokesh"; // $policy_no = "GMC-1999/2000/2021/2022"; + $base_policy_id = null; + if($is_from == "claim_dump"){ + $client_policy_data = $this->db->table('client_policy')->where('policy_no', trim($policy_no))->where('is_active', 1)->get()->getRowArray(); + if(isset($client_policy_data) && !empty($client_policy_data)){ + if(!empty($client_policy_data['base_policy'])) { + $base_policy_id = $client_policy_data['base_policy']; + } + } + } + $builder = $this->db->table('employees e'); $builder->select(" e.id as emp_id, @@ -1161,7 +1172,13 @@ class TicketMasterModel extends Model $builder->where('e.emp_code', trim($emp_code)); $builder->where('e.name', trim($emp_name)); $builder->where('c.client_name', trim($client_name)); - $builder->where('cp.policy_no', trim($policy_no)); + + if(!empty($base_policy_id)) { + $builder->where('cp.id', trim($base_policy_id)); + } else { + $builder->where('cp.policy_no', trim($policy_no)); + } + $builder->where('LOWER(e.relationship)', strtolower('self')); $query = $builder->get(); diff --git a/app/Views/batch_list.php b/app/Views/batch_list.php index e63140ef..5ebdebc5 100755 --- a/app/Views/batch_list.php +++ b/app/Views/batch_list.php @@ -112,18 +112,44 @@ for ($i = 0; $i < $batch_col_count; $i++) { max-width: 95%; } +/* Make the modal content a flex column so only the body scrolls */ #tpa_variation_modal .modal-content { max-height: 90vh; + display: flex; + flex-direction: column; } +/* Internal vertical scroll for large tables, keep header/footer fixed */ #tpa_variation_modal .modal-body { - max-height: calc(85vh - 50px); + flex: 1 1 auto; overflow-y: auto; direction: ltr; } +/* Tab panes must shrink inside flex modal-body so scrollX width is correct */ +#tpa_variation_modal .tab-content, +#tpa_variation_modal .tab-pane { + min-width: 0; +} + +/* Outer .table-responsive must not scroll when DataTables scrollX is active (see custom.css) */ #tpa_variation_modal .table-responsive { - overflow-x: auto; + width: 100%; + max-width: 100%; +} + +#tpa_variation_modal .dataTables_wrapper { + width: 100%; + max-width: 100%; +} + +#tpa_variation_modal .dataTables_scrollHead { + overflow: hidden !important; +} + +#tpa_variation_modal .dataTables_scrollBody { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; } #tpa_variation_modal table { @@ -548,6 +574,10 @@ for ($i = 0; $i < $batch_col_count; $i++) { let tpaNotInNhanceProceedEnabled = true; /** From API `not_in_nhance_proceed_button_text` — label for Proceed on "Not in Nhance" tab. */ let tpaNotInNhanceProceedButtonText = 'Proceed - Not in Nhance'; + /** From API `not_matched_count` — when 0, Proceed is disabled on "Need to Review" tab. */ + let tpaNeedToReviewProceedEnabled = true; + /** From API `need_to_review_proceed_button_text` — label for Proceed on "Need to Review" tab. */ + let tpaNeedToReviewProceedButtonText = 'Proceed - Need to Review ( 0 )'; const tpaVariationColumns = { not_in_nhance: [ @@ -694,6 +724,16 @@ for ($i = 0; $i < $batch_col_count; $i++) { tpaNotInNhanceProceedButtonText = 'Proceed - Not in Nhance'; } + const notMatchedCount = parseInt(data.not_matched_count, 10); + tpaNeedToReviewProceedEnabled = !isNaN(notMatchedCount) && notMatchedCount > 0; + if (typeof data.need_to_review_proceed_button_text === 'string' && data.need_to_review_proceed_button_text.trim() !== '') { + tpaNeedToReviewProceedButtonText = data.need_to_review_proceed_button_text.trim(); + } else if (!isNaN(notMatchedCount)) { + tpaNeedToReviewProceedButtonText = 'Proceed - Need to Review ( ' + notMatchedCount + ' )'; + } else { + tpaNeedToReviewProceedButtonText = 'Proceed - Need to Review ( 0 )'; + } + showTPAVariationTabByLinkId('not-in-nhance-tab'); updateTPAProceedButton('not-in-nhance-tab'); adjustVariationTableByTabId('not-in-nhance-tab'); @@ -711,6 +751,20 @@ for ($i = 0; $i < $batch_col_count; $i++) { updateTPAProceedButton($activeTab.attr('id')); } + function bindVariationScrollHeadSync($wrap) { + const $body = $wrap.find('.dataTables_scrollBody'); + const $head = $wrap.find('.dataTables_scrollHead'); + if (!$body.length || !$head.length) { + return; + } + $body.off('scroll.nhTpaVarHScroll').on('scroll.nhTpaVarHScroll', function () { + $head.scrollLeft($(this).scrollLeft()); + }); + $head.off('scroll.nhTpaVarHScroll').on('scroll.nhTpaVarHScroll', function () { + $body.scrollLeft($(this).scrollLeft()); + }); + } + function getVariationDataTableConfig() { return { dom: "<'dt-top'lf>" + @@ -722,7 +776,15 @@ for ($i = 0; $i < $batch_col_count; $i++) { paging: true, ordering: false, info: false, + scrollX: true, + scrollY: false, + scrollCollapse: false, autoWidth: false, + initComplete: function () { + const $wrap = $(this.api().table().container()); + $wrap.closest('.table-responsive').addClass('nh-dt-no-outer-scroll'); + bindVariationScrollHeadSync($wrap); + }, language: { search: `
@@ -773,8 +835,17 @@ for ($i = 0; $i < $batch_col_count; $i++) { } const dt = $table.DataTable(); + const $wrap = $(dt.table().container()); + bindVariationScrollHeadSync($wrap); dt.columns.adjust().draw(false); bindVariationSearchIcons($table); + + /* Re-measure after tab/modal becomes visible — hidden tables mis-size columns */ + setTimeout(function () { + if ($table.is(':visible')) { + dt.columns.adjust().draw(false); + } + }, 0); } function adjustVariationTableByTabId(tabId) { @@ -814,7 +885,10 @@ for ($i = 0; $i < $batch_col_count; $i++) { $button.prop('disabled', true); } } else if (activeId === 'need-to-review-tab') { - $button.text('Proceed - Need to Review'); + $button.text(tpaNeedToReviewProceedButtonText); + if (!tpaNeedToReviewProceedEnabled) { + $button.prop('disabled', true); + } } else if (activeId === 'not-in-tpa-tab') { $button.addClass('d-none'); } else { @@ -829,12 +903,32 @@ for ($i = 0; $i < $batch_col_count; $i++) { const activeId = $(e.target).attr('id'); updateTPAProceedButton(activeId); adjustVariationTableByTabId(activeId); + setTimeout(function () { + const tabToTableMap = { + 'not-in-nhance-tab': '#not_in_nhance_table', + 'not-in-tpa-tab': '#not_in_tpa_table', + 'need-to-review-tab': '#need_to_review_table' + }; + const sel = tabToTableMap[activeId]; + const $t = sel ? $(sel) : $(); + if ($.fn.DataTable.isDataTable($t)) { + $t.DataTable().columns.adjust().draw(false); + } + }, 50); }); $('#tpa_variation_modal').on('shown.bs.modal', function () { showTPAVariationTabByLinkId('not-in-nhance-tab'); updateTPAProceedButton('not-in-nhance-tab'); adjustVariationTableByTabId('not-in-nhance-tab'); + setTimeout(function () { + ['#not_in_nhance_table', '#not_in_tpa_table', '#need_to_review_table'].forEach(function (sel) { + const $t = $(sel); + if ($.fn.DataTable.isDataTable($t) && $t.is(':visible')) { + $t.DataTable().columns.adjust().draw(false); + } + }); + }, 50); }); $('#tpa_variation_modal').on('hidden.bs.modal', function () { @@ -1155,6 +1249,10 @@ for ($i = 0; $i < $batch_col_count; $i++) { } else if (activeId === 'not-in-tpa-tab') { proceedNotInTPA(); } else if (activeId === 'need-to-review-tab') { + if (!tpaNeedToReviewProceedEnabled) { + toastr.warning('No mismatched records to proceed.', 'WARNING'); + return; + } proceedNeedToReview(); } else { toastr.warning('Unknown tab selected.', 'WARNING'); diff --git a/app/Views/docs/bds-commission.php b/app/Views/docs/bds-commission.php new file mode 100644 index 00000000..55f83aed --- /dev/null +++ b/app/Views/docs/bds-commission.php @@ -0,0 +1,292 @@ + + +

+ BDS commission is a two-part flow: admins upload commission rules + (Excel) per insurer, month, and department; partner/BDS systems then call an API to + calculate payout for a policy using those rules. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload rules Excel"] --> B["Save JSON on disk"] + B --> C["Edit rules in UI"] + D["Policy data POST"] --> E["Load matching JSON"] + E --> F["Match rule and return payout"] +
+
+ +

In short:

+
    +
  • Setup: RuleImportController + commission_file_upload.php — import and manage rules.
  • +
  • Runtime: POST getCommissionInsuranceCommissionController::initiateCommissionCalc — read JSON, pick first matching rule, return payout.
  • +
  • Rules file path: writable/uploads/commission/rules/{MONYYYY}/{insurer_id}_{department}.json (e.g. SEP2025/5_motor.json).
  • +
+ +

Key files

+ + + + + + + + + + + + + +
PartFile
Upload list UIapp/Views/commission_file_upload.php
Rules editor UIapp/Views/commission_rules_list.php
Upload and editor APIapp/Controllers/RuleImportController.php
Excel parsingruleImportService (via Config\Services::ruleImportService())
Payout calculation APIapp/Controllers/InsuranceCommissionController.php
Upload metadata DBcommission_files (CommissionFilesModel)
+ +

Routes

+ +

Admin (commission group):

+ + + + + + + + + + + + + + + + +
RouteHandler
GET/POST commission/listcommissionFileUploadList
POST commission/uploadupload
GET commission/sample_fileSample CSV download
GET commission/downloadErrorFileAnnotated error Excel
GET commission/checkSameEntryDuplicate insurer + month + department check
GET commission/rules/list/(:id)ruleList — rules editor page
POST commission/rules/save/saveRule
POST commission/rules/remove/removeRule
GET commission/checkRuleUsageWhether rule is used on partner policies
GET commission/deleteCommissionData/(:id)Soft-delete file + mark rules deleted
+ +

Runtime API:

+
POST getCommission
+  → InsuranceCommissionController::initiateCommissionCalc
+  → filter: CommissionApiFilter
+ +

How to build the commission file

+ +

+ Use the template from the upload screen (Download sample file) or copy from + public/sample_excel/sample_commission.csv. Dev copies also live under + writable/uploads/commission/files/ (e.g. sample_commission.csv, + Sample_commission_file-New.xlsx). After a successful import, see the generated JSON under + writable/uploads/commission/rules/{MONYYYY}/ (example: NOV2025/1_motor.json). +

+ +
+ ! +
+ Use the current column layout + Some older CSVs in writable/uploads/commission/files/ use legacy headers + (Rule Name, Commission Params(TP:OD:PA)). The importer expects the + S.No layout below (RuleImportService). Wrong headers fail with + “Missing required columns”. +
+
+ +

Required columns (row 1 headers)

+ +

Header text must match exactly (one row per rule, starting row 2). Empty cells are allowed and simply skip that condition.

+ + + + + + + + + + + + + + + + + + + + + + + +
ColumnPurposeExample
S.NoSerial (not used in logic)1
Premium TypeMaps to policy_typeOD, TP, COM
Vehicle Typevehicle_type; comma = multiple (IN)Two Wheeler, Four Wheeler
Vehicle Sub Typevehicle_sub_typeCar, PCV
Make / ModelVehicle make and modelHonda, i10
CC Min / CC MaxCubic capacity range1000, 3000
Fuel TypeComma-separated fuelsPetrol, Diesel
Vehicle Age Min / MaxVehicle age range (years)1, 5
Vehicle Weight Min / MaxWeight range (kg)2000, 3000
RTO State / RTO Codegeo_rto_state, geo_rto_cityRJ, 41
Renewal Typerenewal_typeOnline, Cash, Card
Commission Typepercentage, composite, flat, tieredpercentage
Commission Value% or flat amount (required for percentage / flat)15 or 500
Commission Params (TP)Composite: % on TP premium18
Commission Params (OD)Composite: % on OD premium10
Commission Params (PA)Composite: % on PA premium0 or empty
+ +

Commission types (what to fill)

+ + + + + + + + + + + + + + + + + + + + + + +
TypeFill in sheetBecomes in JSON
percentageCommission Value = e.g. 1010% of premium
compositeLeave Value empty; set TP / OD / PA param columns (percent each)Split % on tp_premium, od_premium, pa_premium
flatCommission Value = fixed rupee amount e.g. 500Fixed payout on premium
+ +

Sample rows (from project files)

+ +

Template header + rows (public/sample_excel/sample_commission.csv):

+
S.No,Premium Type,Vehicle Type,...,Commission Type,Commission Value,Commission Params (TP),Commission Params (OD),Commission Params (PA)
+1,OD,Car,...,percentage,15,,,
+2,TP,Two Wheeler,...,composite,,18,10,
+3,COM,PCV,...,percentage,25,,,
+4,COM,GCV,...,composite,,,10,
+ +

Example A — composite two-wheeler (from writable/.../rules/NOV2025/1_motor.json):

+
    +
  • Vehicle Type = Two Wheeler, CC Min/Max = 100
  • +
  • Commission Type = composite, TP = 10, OD = 25
  • +
  • Result: 10% on TP premium + 25% on OD premium when policy matches
  • +
+ +

Example B — percentage four-wheeler:

+
    +
  • Vehicle Type = Four Wheeler, CC = 1000, Vehicle Age Min/Max = 5
  • +
  • Commission Type = percentage, Commission Value = 10
  • +
  • Result: 10% of total premium
  • +
+ +

Example C — composite TP-only:

+
    +
  • Premium Type = TP, Vehicle Type = Four Wheeler
  • +
  • Commission Type = composite, Commission Params (TP) = 10
  • +
  • Result: 10% on TP premium only
  • +
+ +

Example D — flat amount:

+
    +
  • Vehicle Type = Two Wheeler,Four Wheeler (comma → matches either)
  • +
  • Commission Type = flat, Commission Value = 500
  • +
  • Result: fixed ₹500 when matched
  • +
+ +

Tips:

+
    +
  • Min must not be greater than Max (CC, age, weight) or the row fails validation.
  • +
  • On failure, download the annotated file — errors are written into the sheet.
  • +
  • Supported formats: .csv, .xlsx, .xls, .ods.
  • +
  • Upload UI departments: motor, health; import logic is built for motor columns today.
  • +
+ +

Rule upload

+ +

From commission_file_upload.php the user picks insurer, commission month, department (motor / health), and an Excel/CSV file.

+ +
    +
  1. checkSameEntry — if a successful upload already exists for the same trio, SweetAlert offers Overwrite or Append (overwrite=1 or 0 on POST).
  2. +
  3. upload — stores file under writable/uploads/commission/files/, inserts commission_files row (pending).
  4. +
  5. ruleImportService->processUpload() — validates Excel rows; on success returns rules array; on failure returns annotated_file for download.
  6. +
  7. On success — writes JSON to rules/{MONYYYY}/{insurer_id}_{department}.json; sets file_status=success and rules_count.
  8. +
  9. On failure — file_status=failed; user downloads annotated_{filename} via downloadErrorFile.
  10. +
+ +

Rules editor

+ +

+ For successful uploads, action View Rules opens + commission/rules/list/{file_id} (commission_rules_list.php). +

+
    +
  • Lists rules from the JSON file for that upload’s insurer, month, and department.
  • +
  • saveRule — create or update a rule (conditions + calculation + name) in the JSON via updateCommissionRules().
  • +
  • removeRule — soft-delete one rule (is_deleted=true).
  • +
  • checkRuleUsage — warns if partner_policy.commission_applied_rule references the rule.
  • +
  • Deleting the whole upload marks all rules with that file_id as deleted in JSON, then sets commission_files.is_active=0.
  • +
+ +

Rule JSON shape

+ +

Each rule is roughly:

+
{
+  "id": "rule_…",
+  "name": "Rule name",
+  "department": "motor",
+  "file_id": 12,
+  "is_deleted": false,
+  "conditions": [
+    { "field": "vehicle_type", "operator": "==", "value": "car" }
+  ],
+  "calculation": {
+    "type": "percentage",
+    "value": 10,
+    "on": "premium"
+  }
+}
+ +

Calculation types in InsuranceCommissionController: percentage, composite, fixed. Conditions support ==, !=, >, >=, <, <=, between, in.

+ +

Commission calculation API

+ +

initiateCommissionCalc() expects POST/JSON including at least:

+
    +
  • policy_issue_date — used to pick folder {MON}{YEAR} (e.g. SEP2025)
  • +
  • insurer_id
  • +
  • department — motor, health, etc.
  • +
  • Fields referenced in rule conditions and calculation bases (e.g. premium, od_premium)
  • +
+ +
+
+flowchart TD + A["POST getCommission"] --> B{"Required fields present?"} + B -->|No| C["Validation error"] + B -->|Yes| D["Load rules JSON for month, insurer, department"] + D --> E{"File exists?"} + E -->|No| F["Rules file not found"] + E -->|Yes| G["Find first rule where all conditions match"] + G --> H{"Rule found?"} + H -->|No| I["No matching rules"] + H -->|Yes| J["Apply calculation type"] + J --> K["Return payout and rule"] +
+
+ +

Rules with is_deleted: false are loaded; the first matching rule wins (no priority field yet).

+ +

Developer steps

+ +
    +
  1. Open /commission/list (logged-in admin).
  2. +
  3. Download sample file, fill rules for insurer + month + department, upload.
  4. +
  5. If validation fails, download the annotated error file and fix the sheet.
  6. +
  7. Use View Rules to tweak conditions or calculation without re-uploading the whole file.
  8. +
  9. Test payout: POST getCommission with the same insurer, department, and a policy_issue_date in that commission month.
  10. +
+ +

Common pitfalls

+ +
    +
  • Month folder must match policy date — upload uses commission month; API uses policy_issue_date to resolve the same MONYYYY folder.
  • +
  • Append vs overwrite — append merges JSON arrays; overwrite backs up the old file then replaces.
  • +
  • Departments — upload UI currently offers motor and health; API department string must match the JSON filename slug.
  • +
  • HTTP 200 on upload errors — check status and code in the JSON body, not only HTTP status.
  • +
diff --git a/app/Views/docs/bds-insurer-statement.php b/app/Views/docs/bds-insurer-statement.php new file mode 100644 index 00000000..a504be59 --- /dev/null +++ b/app/Views/docs/bds-insurer-statement.php @@ -0,0 +1,309 @@ + + +

+ BDS Insurer statement lets finance users upload an insurer-provided Excel statement, + validate each row against NHance policy transactions (pt_co_share_details + + policy_transaction), and persist matched brokerage amounts into + co_share_stmt_details. The admin UI is + app/Views/insurer_statement_list.php; all server logic lives in + PolicyTransactionController under the policy_tranction/statement route group. +

+ +
+ i +
+ Auth + Statement routes use the authMVC filter. Upload and list are browser AJAX/form calls from an authenticated session, not public API endpoints. +
+
+ +

Overview

+ +
+
+flowchart LR + A[Upload Excel] --> B[validateInsurerStatement] + B --> C{All rows OK?} + C -->|Yes| D[updateInsurerStatement] + C -->|No| E[file_status failed] + D --> F[co_share_stmt_details] +
+
+ +

Row validation logic

+ +

+ validateInsurerStatement() checks that every Excel line maps to a real NHance transaction + for the selected insurer branch. Each row is matched on policy number (column B) and + endorsement number (column C). +

+ +
+
+flowchart TD + A["Read Excel"] --> B["Fetch NHance rows for policies in the file"] + B --> C["Check each non-empty row"] + C --> D{"Policy and endorsement found in NHance?"} + D -->|"Yes, first time in file"| E["Row valid"] + D -->|"Same combo again"| F["Duplicate"] + D -->|"Policy not found"| G["Invalid policy"] + D -->|"Policy OK, wrong endorsement"| H["Invalid endorsement"] + E --> I{"Any bad rows?"} + F --> I + G --> I + H --> I + I -->|No| J["Validation passes"] + I -->|Yes| K["Validation fails"] +
+
+ +

In short:

+
    +
  • Empty rows are ignored.
  • +
  • A row is valid only if that policy + endorsement exists in NHance and appears once in the upload.
  • +
  • If anything fails, the whole file is marked failed and errors are shown per row in the UI.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/insurer_statement_list.php — DataTable list, upload modal, validation error modal, invoice modal
Controllerapp/Controllers/PolicyTransactionController.php
Statement header modelapp/Models/InsurerStatements.phpinsurer_statements
Line-item modelapp/Models/COShareStmtDetailsModel.phpco_share_stmt_details
NHance source rowsapp/Models/PTCOShareDetailsModel.phpgetNonReconcileredPolicyTransactionByPolicyAndEndorsement()
Sample Excelpublic/sample_excel/insurer_stament_sample.xlsx
+ +

Routes (prefix policy_tranction/statement, filter authMVC):

+ + + + + + + + + + + + + + + + +
MethodRouteHandler
GETliststatementList
POSTuploaduploadInsurerStatement
GETdownloadSampleInsurerStatementSample file download
GETdownloadInsurerStatement/(:num)Uploaded file download
GETgetFileErr/(:any)Validation failure JSON for modal
GETgetInsurerStatementMonthUsed to disable already-used statement numbers
GETdeleteStatement/(:any)Soft-delete statement + related rows
GETgetPaymentDetails/(:any)Invoice modal data
POSTsaveInvoicePaymentDetailsInvoice / payment save
+ +
$routes->group('policy_tranction', ['filter' => 'authMVC'], function ($routes) {
+    $routes->group('statement', ['filter' => 'authMVC'], function ($routes) {
+        $routes->get('list', 'PolicyTransactionController::statementList');
+        $routes->post('upload', 'PolicyTransactionController::uploadInsurerStatement');
+        // ...
+    });
+});
+ +

Statement list UI

+ +

+ statementList() loads insurers/branches via + insurerBranchModel::getInsurerBranchesWithInsurerNames(), invoice status labels, and + statements from the last 180 days (is_active = 1). The view shows: +

+ +
    +
  • Insurer branch, statement month, statement serial no, filename (download link), line items count
  • +
  • file_statussuccess or failed (failed rows show an alert icon → error modal)
  • +
  • invoice_status — pending / generated / sent / payment received
  • +
  • Actions (non-failed only): invoice status update, delete
  • +
+ +

+ Upload form fields (#insurer_statement_upload_form): insurer + (insurer_id-branch_id), statement month (flatpickr), statement no (1–7), Excel file. + Submit is AJAX POST to relative upload. On success the page reloads; on validation failure + the API still returns HTTP 200 with dataStatus: false and error_data. +

+ +

uploadInsurerStatement()

+ +
    +
  1. Validates uploaded file: Excel MIME types, max 16 MB (max_size[statement,16384] KB).
  2. +
  3. Moves file to WRITEPATH . 'uploads/statements/' (see createStatementFolder() for folder creation).
  4. +
  5. Parses POST: insurer as {insurer_id}-{branch_id}, statement_month (converted to first-of-month Y-m-d), statement_nostmt_sno.
  6. +
  7. Inserts insurer_statements row via InsurerStatements model.
  8. +
  9. Calls validateInsurerStatement(['file_id' => $file_id]).
  10. +
  11. If validation passes, calls updateInsurerStatement(['file_id' => $file_id]).
  12. +
  13. On validation failure: responds with dataStatus: false, error_data, error_code (HTTP 200).
  14. +
  15. On success: sets invoice_status = 'pending' and returns dataStatus: true.
  16. +
+ +

validateInsurerStatement($params)

+ +

Runs immediately after upload (and can be re-run manually in dev with a hard-coded file_id in statementList() comment).

+ +

Steps

+ +
    +
  1. Load insurer_statements by file_id; fail if missing or physical file absent under writable/uploads/statements/.
  2. +
  3. Load active sheet via PhpSpreadsheet; drop header row; sanitize with ExcelSanitizeHelper::sanitizeArrayData().
  4. +
  5. Collect unique policy numbers from column B (index 1), skipping empty rows via check_row_is_empty_or_null().
  6. +
  7. Fetch NHance candidates: + PTCOShareDetailsModel::getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id, branch_id, policy_no[]) + — matches pt.policy_no for completed transactions on that insurer branch.
  8. +
  9. Build lookup keys policy_no|endorsement_no (sanitized) for source and Excel rows.
  10. +
  11. For each non-empty Excel row, match policy + endorsement; track duplicates in $matched_entry.
  12. +
  13. On mismatch, append row-wise HTML messages under error_data[row_index].
  14. +
  15. Update insurer_statements.line_items, file_status (success / failed), reason (JSON).
  16. +
+ +

sanitizeStatementLookupValue()

+ +

Private helper trims Unicode spaces and strips zero-width / BOM characters from policy and endorsement values before comparison — avoids “looks equal” mismatches in Excel.

+ +

Validation error codes

+ + + + + + + + + + +
error_codeMeaningUI
0DB row missing or physical file not foundPlain message in modal
1Legacy: list of row numbers (old format)Comma-separated row list
2Row-wise validation (current)Modal lists each row with policy / endorsement / duplicate messages
+ +

Row keys in error_data are the array index from the Excel loop (first data row is typically 1 after header removal), not necessarily the Excel row number on sheet.

+ +

updateInsurerStatement($params)

+ +

Runs only when validation returned status: true.

+ +
    +
  1. Same file load / sanitize path as validation.
  2. +
  3. Rebuild policy list and source lookup (policy + endorsement → array of pt_co_share_details rows).
  4. +
  5. For each Excel row with a matching source entry, compute brokerage totals and variance: +
      +
    • BP: amount col 3, brokerage col 5
    • +
    • TP: amount col 4, brokerage col 6
    • +
    • TEP: forced to 0 in current code
    • +
    • reward from col 7
    • +
    • variance = exp_amt - total_amt (from source exp_amt)
    • +
    +
  6. +
  7. COShareStmtDetailsModel::insertBatch($data_to_update) — one row per matched Excel line.
  8. +
  9. Sets file_status = success, invoice_status = pending, clears/sets reason.
  10. +
+ +

Excel column mapping (0-based index)

+ +

Header row is removed; data columns used by validation/update:

+ + + + + + + + + + + + + + +
IndexColumnUse
1BPolicy number (required for matching)
2CEndorsement number
3DActual BP amount
4EActual TP amount
5FActual BP brokerage
6GActual TP brokerage
7HReward
+ +

Download the canonical layout from the list page link → downloadSampleInsurerStatement.

+ +

NHance source query

+ +

getNonReconcileredPolicyTransactionByPolicyAndEndorsement() joins:

+ +
    +
  • pt_co_share_details (active) → policy_transaction (active, status = completed)
  • +
  • Filtered by insurer_id, insurer_branch_id, and pt.policy_no IN (...)
  • +
+ +

+ Matching is on sanitized policy_no + endorsement_no. The method name suggests + “non-reconciled” but the current query does not filter statement_id IS NULL; + be aware when re-uploading or debugging duplicate reconciliation. +

+ +

Invoice status and delete

+ +

After a successful upload, users manage invoice lifecycle from the list (separate from upload/validate):

+ +
    +
  • invoice_status: pending, generated, sent, payment_received
  • +
  • saveInvoicePaymentDetails — JSON POST from invoice modal
  • +
  • deleteStatement($id) — soft-deletes co_share_stmt_details, inv_payment_details, and insurer_statements for that id
  • +
+ +

+ getInsurerStatementMonth returns successful statements for insurer+month so the UI can + disable statement numbers already used (disableStatementNo() in the view). +

+ +

Developer steps

+ +
    +
  1. Ensure writable/uploads/statements/ exists and is writable (or call createStatementFolder() once).
  2. +
  3. Open policy_tranction/statement/list in a logged-in session.
  4. +
  5. Use sample Excel; pick insurer branch and month; choose an unused statement number (1–7 per insurer/month).
  6. +
  7. Confirm policy/endorsement exist on a completed BDS transaction for that insurer branch.
  8. +
  9. On failure, open the alert icon → modal calls getFileErr/{id} and renders reason JSON.
  10. +
  11. To debug validation only: temporarily uncomment the validateInsurerStatement / updateInsurerStatement one-liner in statementList() with a known file_id.
  12. +
+ +

Common pitfalls

+ +
    +
  • Hidden Excel characters — policy/endorsement must pass sanitizeStatementLookupValue(); re-type values if NHance shows a match but upload fails.
  • +
  • Duplicate policy + endorsement in the same file → error_code 2, duplicate message on second row.
  • +
  • Validation failed but file on diskinsurer_statements row remains; user sees failed status; re-upload needs a new statement or delete the failed row.
  • +
  • Statement number reuse — only successful uploads for that insurer/month block numbers in the dropdown via getInsurerStatementMonth.
  • +
  • Upload response HTTP 200 on error — front-end checks dataStatus, not status code alone.
  • +
  • Legacy handlersvalidateInsurerStatementOld, updateInsurerStatementOLD remain in the controller; production path is the non-Old methods documented here.
  • +
+ + + +
    +
  • BDS reports: policy_tranction/report/list, variance, finance, outstanding lists
  • +
  • Daily BDS cron mail: cronDailyBDSReport (separate from statement upload)
  • +
diff --git a/app/Views/docs/correction.php b/app/Views/docs/correction.php new file mode 100644 index 00000000..d8d68029 --- /dev/null +++ b/app/Views/docs/correction.php @@ -0,0 +1,265 @@ + + +

+ Correction updates existing member data on an active policy via Excel upload + (files.action = correction). The final step creates pending correction endorsements + on the employees table — data is not updated until those endorsements are applied downstream. +

+ +

+ Processing is in EmployeeServiceController::employeesCorrectionProcess, queued after the same + format and data validation steps used for inception and deletion. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel action correction"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesCorrectionProcess job"] + D --> E["emp_endorsement pending on employees"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|loop done| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: 8 columns (A–H); field must be one of four allowed names; dates d-M-Y.
  • +
  • Step 2 — Data: Member must exist (emp code + name + active policy); code 10 if not found.
  • +
  • Step 3 — Correction: One pending endorsement per row per field (skips duplicate pending corrections).
  • +
  • Each Excel row = one field change for one member (not a full-family operation).
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action Correction
UploadEmployeeController::employeesUplodWithEvents
ValidationexcelFileFormatValidation, excelFileDataValidation
Correction processEmployeeServiceController::employeesCorrectionProcess
Column config APIgetCorrectionExcelColumns() — used when building correction Excel programmatically
JobemployeesCorrectionProcess in JobWorker.php
EndorsementsEmpEndorsementModelactions = c, table_name = employees, status = pending
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=correction
  • +
  • GET /employee/excel_error/{file_id} — validation errors
  • +
+ +
+ i +
+ files.policy_id is the client policy id. Lookup requires active + employees + employee_polices on that policy and branch. +
+
+ +

Sync vs background jobs

+ + + + + + + + + + +
Step< 1 MB≥ 1 MB
Format validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
Correction processJob employeesCorrectionProcessSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $correction_excel_columns8 columns (A–H).
  • +
  • Action code C for mandatory rules on correction-specific columns.
  • +
  • Field (column D): only name, dob, relationship, email_corporate.
  • +
  • Date of Correction (F): d-M-Y.
  • +
  • Change event (G) and Value (E) are mandatory.
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list (e.g. invalid Field value)
4Custom validation failed
5File / policy problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

Rows grouped by EMP ID; for correction the critical check is name_and_empid_check_in_db:

+
    +
  • Each row must match an active employee on the uploaded policy (emp code + name).
  • +
  • Not found → error code 10 (“Record Not found”).
  • +
+ +

On success → queues employeesCorrectionProcess (with optional batch_file_id for TPA multi-file flows).

+ +

Step 3: employeesCorrectionProcess

+ +
+
+flowchart TD + A["Read each Excel row"] --> B["Match emp_code plus name on active policy"] + B --> C{"Pending correction for same field?"} + C -->|Yes| D["Skip row"] + C -->|No| E["Insert emp_endorsement actions c"] + E --> F["old_value from DB new_value from Excel"] +
+
+ +

Per row:

+
    +
  • field_name ← column D (name, dob, relationship, email_corporate)
  • +
  • new_value ← column E; if field is dob, converted from d-M-Y to Y-m-d
  • +
  • date_of_correction ← column F (converted to Y-m-d)
  • +
  • remarks ← column H (optional)
  • +
  • old_value ← current value from employees.{field_name}
  • +
+ +

+ Skips insert when a pending correction endorsement already exists for the same + emp_code, name, and field_name + (actions = c, endorsement_id IS NULL, status != truncated). +

+ +

+ Sets files.status = success when the loop completes and sends a success pull notification. + Rows with no DB match are not endorsed (no row-level failure on the file record). +

+ +

+ TPA batch: If batch_file_id is set, also queues + updateEmployeeDataFromTpa, reconTpaApiDataWithEmployeepolicies, and + initializeDeletionProcessForTpaApiData. +

+ +

Correction Excel columns

+ + + + + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYes
CNAME OF EMP/DEPYesMust match DB before correction
DFieldYesname, dob, relationship, email_corporate
EValueYesNew value; DOB as d-M-Y
FDate of CorrectionYesd-M-Y
GChange eventYese.g. correction
HRemarksNoStored on endorsement
+ +

Row layout examples

+ +

Fix DOB — one row, one field:

+ + + + + + + + + + + + + + + + +
EMP IDNAMEFieldValueDate of CorrectionChange eventRemarks
EMP001Raj Kumardob15-Jan-198519-May-2026correctionTypo in upload
+ +

Multiple fixes — use separate rows (same or different members):

+ + + + + + + + + +
EMP IDNAMEFieldValue
EMP001Raj Kumaremail_corporateraj.kumar@company.com
EMP001Priya KumarrelationshipSpouse
+ +

Developer steps

+ +
    +
  1. Upload with upload-action-type=correction; note file_id.
  2. +
  3. On validation failure, check /employee/excel_error/{file_id} for code 10.
  4. +
  5. After success, query emp_endorsement where file_id = upload id, actions = 'c', status = 'pending'.
  6. +
  7. Compare field_name, old_value, new_value per row to the Excel.
  8. +
  9. To generate correction Excel in code, use getCorrectionExcelColumns() for header layout.
  10. +
+ +

Common pitfalls

+ +
    +
  • Name must match DB — correction identifies the member by current emp_code + name; rename via a name field row uses the old name in column C.
  • +
  • Only four fields — mobile, SI, band, etc. are not supported in this upload path.
  • +
  • Duplicate pending correction — second upload for the same field is skipped until the first endorsement is processed or truncated.
  • +
  • File success vs rowsfiles.status = success does not mean every row created an endorsement.
  • +
  • Not live updateemployees columns change only after endorsement approval/application.
  • +
+ +

+ Related: + Inception, + Deletion. +

diff --git a/app/Views/docs/deletion.php b/app/Views/docs/deletion.php new file mode 100644 index 00000000..aff0fdba --- /dev/null +++ b/app/Views/docs/deletion.php @@ -0,0 +1,259 @@ + + +

+ Deletion removes active members from a client policy via Excel upload + (files.action = deletion). Unlike inception, the final step does not delete rows immediately — + it creates pending endorsement records on employee_polices for approval/processing later. +

+ +

+ Core processing lives in EmployeeServiceController::employeeDisembark. + EmployeeController::initializeDeletionProcessForTpaApiData is a separate TPA-reconcile path that + builds a deletion Excel file and calls the same disembark function. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel action deletion"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeeDisembark job"] + D --> E["emp_endorsement pending rows"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|always| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: 7 columns (A–G), mandatory exit fields per action code D.
  • +
  • Step 2 — Data: Member must exist in DB (emp code + name + active policy); code 10 if not found.
  • +
  • Step 3 — Disembark: Writes pending deletion endorsements; Self row removes whole family, dependent row removes one member.
  • +
  • Files < 1 MB run format validation inline; data validation and disembark are always queued.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action Deletion
UploadEmployeeController::employeesUplodWithEvents
ValidationEmployeeServiceController::excelFileFormatValidation, excelFileDataValidation
Deletion processEmployeeServiceController::employeeDisembark
TPA auto-deletionEmployeeController::initializeDeletionProcessForTpaApiData
JobemployeeDisembark in JobWorker.php
EndorsementsEmpEndorsementModelemp_endorsement (actions = d, status = pending)
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=deletion
  • +
  • GET /employee/excel_error/{file_id} — read files.reason after validation failure
  • +
+ +
+ i +
+ files.policy_id is the client policy id. Member lookup joins + employees + employee_polices on that policy and branch. +
+
+ +

Sync vs background jobs

+ + + + + + + + + + +
Step< 1 MB≥ 1 MB
Format validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
DisembarkJob employeeDisembarkSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $deletion_excel_columns7 columns (A–G).
  • +
  • Action code D drives mandatory fields: Change event, Date of exit, Reason for exit, Claim status.
  • +
  • Date of exit format: d-M-Y.
  • +
  • Claim status allowed: 0 or 1.
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list
4Custom validation failed
5File / policy problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

Rows are grouped by EMP ID. For deletion, the main check is name_and_empid_check_in_db:

+
    +
  • Each row must match an active employee + active employee_polices row on the uploaded policy/branch.
  • +
  • If not found → error code 10 (“Record Not found”) on that Excel row.
  • +
+ +

On success → queues employeeDisembark (not employeesOnboardPreprocess).

+ +

Step 3: employeeDisembark

+ +

Loads the Excel again and processes each non-empty row.

+ +
+
+flowchart TD + A["Match emp_code plus name on active policy"] --> B{"Found?"} + B -->|No| C["Log error skip row"] + B -->|Yes| D{"Pending deletion endorsement?"} + D -->|Yes| E["Log skip row"] + D -->|No| F{"Relationship Self?"} + F -->|No| G["Endorse this member only"] + F -->|Yes| H["Endorse all active family members"] + G --> I["emp_endorsement pending"] + H --> I +
+
+ +

Per matched member, four pending endorsement rows are inserted on employee_polices:

+
    +
  • date_of_exit ← Excel column E (converted to Y-m-d)
  • +
  • reason_for_exit ← column F
  • +
  • statusinactive
  • +
  • claim_status ← column G
  • +
+ +

+ Returns an array of processed employee.id values. Sets files.status = success when the loop finishes + (even if some rows were skipped — check logs for “not found” or “existing endorsement pending”). +

+ +

TPA auto-deletion (EmployeeController)

+ +

initializeDeletionProcessForTpaApiData($file_id):

+
    +
  1. Reads TPA reconcile file context (tpa_api_data, action_flag_status = D).
  2. +
  3. Builds candidate members and writes writable/uploads/excel/tpa_auto_deletion_{fileId}_{timestamp}.xls.
  4. +
  5. Inserts a new files row with action = deletion.
  6. +
  7. Calls employeeDisembark(['file_id' => $newFileId]) synchronously.
  8. +
  9. Exports endorsement data for TPA via getDeletionEmployeeDataForExportExcel.
  10. +
+ +

Deletion Excel columns

+ + + + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYesEmployee / family code
CNAME OF EMP/DEPYesMust match DB name exactly
DChange eventYese.g. deletion
EDate of exitYesd-M-Y
FReason for exitYes
GClaim statusYes0 or 1
+ +

Row layout examples

+ +

Delete one dependent — only that name appears; Self row is not required in the file.

+ + + + + + + + +
RowEMP IDNAMEChange eventDate of exitReasonClaim
2EMP001Arjun Kumardeletion19-May-2026Resigned0
+

Result: endorsements for Arjun only (relationship ≠ Self).

+ +

Delete entire family — list the Self row; disembark loads all active family members for that EMP ID.

+ + + + + + + + +
RowEMP IDNAMEChange eventDate of exitReasonClaim
2EMP001Raj Kumardeletion19-May-2026Resigned0
+

Result: pending endorsements for Self + all active dependents on that policy (same exit date/reason/claim from the row).

+ +

Developer steps

+ +
    +
  1. Upload via /employee/upload with action deletion; note file_id.
  2. +
  3. If validation fails, use GET /employee/excel_error/{file_id} — look for code 10 (member not in DB).
  4. +
  5. After success, query emp_endorsement where file_id = upload id, actions = 'd', status = 'pending'.
  6. +
  7. If rows were skipped, search logs for not found or Existing endorsement pending.
  8. +
  9. TPA path: trace initializeDeletionProcessForTpaApiData and the generated tpa_auto_deletion_*.xls file.
  10. +
+ +

Common pitfalls

+ +
    +
  • Name mismatch — Excel name must match employees.name exactly (case/spacing).
  • +
  • Not active — only emp_status = active and employee_polices.status = active match.
  • +
  • Duplicate pending deletion — row skipped if a pending deletion endorsement already exists for that policy row.
  • +
  • Self vs dependent — wrong relationship in the row changes scope (one member vs whole family).
  • +
  • File always success after disembarkfiles.status does not reflect per-row skips; use endorsements table + logs.
  • +
  • Not immediate delete — members stay active until endorsements are approved/applied downstream.
  • +
+ +

Related: Inception (onboard pipeline uses the same upload screen and first two validation steps).

diff --git a/app/Views/docs/docs_header.php b/app/Views/docs/docs_header.php index 2b8a8e6e..949ee16e 100644 --- a/app/Views/docs/docs_header.php +++ b/app/Views/docs/docs_header.php @@ -39,6 +39,7 @@ $base = base_url(); /* ─── TOKENS ─────────────────────────────── */ :root { --sidebar-w : 260px; + --toc-w : 200px; --topbar-h : 56px; --bg : #ffffff; --bg2 : #f7f8fa; @@ -143,6 +144,8 @@ $base = base_url(); /* ─── LAYOUT WRAPPER ─────────────────────── */ .docs-layout { display : flex; + width : 100%; + align-items: flex-start; margin-top: var(--topbar-h); min-height: calc(100vh - var(--topbar-h)); } @@ -208,6 +211,15 @@ $base = base_url(); .callout.success { background: #f0fdf4; border-color: #4ade80; color: #15803d; } /* ─── TABLES ─────────────────────────────── */ + .docs-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 20px 0; + border: 1px solid var(--border); + border-radius: 8px; + } + .docs-table-wrap table { margin: 0; min-width: 640px; } + .docs-table-wrap--fluid table { min-width: 0; width: 100%; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13.5px; } th { background : var(--bg2); diff --git a/app/Views/docs/docs_main_open.php b/app/Views/docs/docs_main_open.php index e964f522..e9a75656 100644 --- a/app/Views/docs/docs_main_open.php +++ b/app/Views/docs/docs_main_open.php @@ -27,10 +27,11 @@ $toc = $toc ?? []; L["iterate in DB order"] - L --> K{"rack_rate_name changed"} - K -->|yes| B["open new bucket"] - K -->|no| U["same bucket"] - B --> P["append row to slab_rates array"] - U --> P - P --> G["store grid_master from row"] -
- - -
- i -
- When maintaining this helper, inspect the implementation for duplicate pushes on the first row of a new rack name; - downstream code tolerates duplicate slab rows but it can confuse debugging of premium matches. -
-

get_familiy_composition

@@ -321,13 +294,122 @@ flowchart LR (already grouped to one employee). Keys align with the JSON used in rack configuration (additional_relationship), except either-parents-pil and elders_count which are stripped before comparison.

+

+ This function mainly: +

    +
  • Reads all family members
  • +
  • Normalizes relationship names
  • +
  • Builds a summarized family composition object
  • +
  • Maintains counts for: +
      +
    • self
    • +
    • spouse
    • +
    • children
    • +
    • parents
    • +
    • parents-in-law
    • +
    +
  • +
+

flowchart TD - A["single pass over family_data rows"] --> B["slugify column 5 per row"] - B --> C["bump self spouse childrens parents parents-in-law counters"] - C --> D["return family_composition map"] + + START([Start get_familiy_composition]) + + START --> INIT["Initialize family_composition array and slug service"] + + INIT --> LOOP_MEMBERS{"Loop each family member"} + + LOOP_MEMBERS --> GET_REL["Read relationship from row[5]"] + + GET_REL --> SLUGIFY["Slugify relationship"] + + %% SELF + + SLUGIFY --> CHECK_SELF{"relationship == self"} + + CHECK_SELF -->|Yes| SET_SELF_ONE["Set self = 1"] + + CHECK_SELF -->|No| CHECK_SELF_EXISTS{"self key exists?"} + + CHECK_SELF_EXISTS -->|No| SET_SELF_ZERO["Set self = 0"] + + CHECK_SELF_EXISTS -->|Yes| CHECK_SPOUSE + + SET_SELF_ONE --> CHECK_SPOUSE + SET_SELF_ZERO --> CHECK_SPOUSE + + %% SPOUSE + + CHECK_SPOUSE{"relationship == spouse"} + + CHECK_SPOUSE -->|Yes| SET_SPOUSE_ONE["Set spouse = 1"] + + CHECK_SPOUSE -->|No| CHECK_SPOUSE_EXISTS{"spouse key exists?"} + + CHECK_SPOUSE_EXISTS -->|No| SET_SPOUSE_ZERO["Set spouse = 0"] + + CHECK_SPOUSE_EXISTS -->|Yes| CHECK_CHILDREN + + SET_SPOUSE_ONE --> CHECK_CHILDREN + SET_SPOUSE_ZERO --> CHECK_CHILDREN + + %% CHILDREN + + CHECK_CHILDREN{"relationship == son OR daughter"} + + CHECK_CHILDREN -->|Yes| INC_CHILDREN["Increment childrens count"] + + CHECK_CHILDREN -->|No| CHECK_CHILDREN_EXISTS{"childrens key exists?"} + + CHECK_CHILDREN_EXISTS -->|No| SET_CHILDREN_ZERO["Set childrens = 0"] + + CHECK_CHILDREN_EXISTS -->|Yes| CHECK_PARENTS + + INC_CHILDREN --> CHECK_PARENTS + SET_CHILDREN_ZERO --> CHECK_PARENTS + + %% PARENTS + + CHECK_PARENTS{"relationship == father OR mother"} + + CHECK_PARENTS -->|Yes| INC_PARENTS["Increment parents count"] + + CHECK_PARENTS -->|No| CHECK_PARENTS_EXISTS{"parents key exists?"} + + CHECK_PARENTS_EXISTS -->|No| SET_PARENTS_ZERO["Set parents = 0"] + + CHECK_PARENTS_EXISTS -->|Yes| CHECK_INLAWS + + INC_PARENTS --> CHECK_INLAWS + SET_PARENTS_ZERO --> CHECK_INLAWS + + %% PARENTS IN LAW + + CHECK_INLAWS{"relationship == father-in-law OR mother-in-law"} + + CHECK_INLAWS -->|Yes| INC_INLAWS["Increment parents-in-law count"] + + CHECK_INLAWS -->|No| CHECK_INLAW_EXISTS{"parents-in-law key exists?"} + + CHECK_INLAW_EXISTS -->|No| SET_INLAW_ZERO["Set parents-in-law = 0"] + + CHECK_INLAW_EXISTS -->|Yes| NEXT_MEMBER + + INC_INLAWS --> NEXT_MEMBER + SET_INLAW_ZERO --> NEXT_MEMBER + + %% LOOP + + NEXT_MEMBER --> MORE_MEMBERS{"More family members?"} + + MORE_MEMBERS -->|Yes| LOOP_MEMBERS + + MORE_MEMBERS -->|No| RETURN_RESULT + + RETURN_RESULT(["Return family_composition"])
@@ -348,12 +430,87 @@ flowchart TD
flowchart TD - J["decode additional_relationship JSON"] --> U["strip either-parents-pil and elders_count"] - U --> E{"non-NA rules exist"} - E -->|no| F["rack not applicable"] - E -->|yes| K["sequential AND each non-NA key"] - K -->|incoming matches value or any| Y["rack applicable plus relationship tokens"] - K -->|missing key or mismatch| X["rack not applicable"] + + START([Start compare_incoming_family_slab_with_configured_slab]) + + START --> INIT_MAP["Initialize relationship mapping"] + + INIT_MAP --> INIT_RESULT["Initialize result + is_applicable = false + applicable_members = []"] + + INIT_RESULT --> READ_CONFIG["Read configured family composition from slab"] + + READ_CONFIG --> REMOVE_KEYS["Remove ignored keys + either-parents-pil + elders_count"] + + REMOVE_KEYS --> CHECK_CONFIG{"Configured composition has values?"} + + %% EMPTY CONFIGURATION + + CHECK_CONFIG -->|No| RESET_RESULT["Set result as not applicable"] + + RESET_RESULT --> RETURN_RESULT + + %% LOOP START + + CHECK_CONFIG -->|Yes| LOOP_CONFIG{"Loop configured relationships"} + + %% CHECK NA + + LOOP_CONFIG --> CHECK_NA{"Value != NA ?"} + + CHECK_NA -->|No| NEXT_RELATION + + %% CHECK KEY EXISTS + + CHECK_NA -->|Yes| CHECK_KEY_EXISTS{"Relationship exists in incoming composition?"} + + CHECK_KEY_EXISTS -->|No| INVALID_RESULT_1["Set: + is_applicable = false + applicable_members = []"] + + INVALID_RESULT_1 --> BREAK_LOOP + + %% VALUE COMPARISON + + CHECK_KEY_EXISTS -->|Yes| CHECK_MATCH{ + Incoming count == configured count + OR + configured value == any + } + + %% MATCH FOUND + + CHECK_MATCH -->|Yes| SET_APPLICABLE["Set is_applicable = true"] + + SET_APPLICABLE --> MERGE_MEMBERS["Merge mapped relationships into applicable_members"] + + MERGE_MEMBERS --> NEXT_RELATION + + %% MATCH FAILED + + CHECK_MATCH -->|No| INVALID_RESULT_2["Set: + is_applicable = false + applicable_members = []"] + + INVALID_RESULT_2 --> BREAK_LOOP + + %% LOOP CONTROL + + NEXT_RELATION --> MORE_RELATIONS{"More relationships?"} + + MORE_RELATIONS -->|Yes| LOOP_CONFIG + + MORE_RELATIONS -->|No| RETURN_RESULT + + BREAK_LOOP --> RETURN_RESULT + + %% RETURN + + RETURN_RESULT(["Return result array"]) +
@@ -378,13 +535,60 @@ flowchart TD
flowchart TD - A["applicable_members from compare"] --> B["scan family_data by index"] - B --> C{"relationship slug in list"} - C -->|yes| D["record index and age from DOB"] - C -->|no| B - D --> B - B -->|done| E["max_count equals number of indexes"] - E --> F["walk indexes in order first gets acting_self true remainder false"] + + START([Start get_applicable_familiy_members]) + + START --> INIT["Initialize: + index = [] + max_age = [] + max_count = 0"] + + INIT --> INIT_SLUG["Initialize slug service"] + + INIT_SLUG --> LOOP_MEMBERS{"Loop each family member"} + + %% READ RELATIONSHIP + + LOOP_MEMBERS --> READ_REL["Read relationship from family_member[5]"] + + READ_REL --> SLUGIFY["Slugify relationship"] + + %% CHECK APPLICABLE + + SLUGIFY --> CHECK_APPLICABLE{ + Relationship exists in applicable_members? + } + + %% MATCH FOUND + + CHECK_APPLICABLE -->|Yes| STORE_INDEX["Add member index into result.index"] + + STORE_INDEX --> CALCULATE_AGE["Calculate member age from DOB"] + + CALCULATE_AGE --> STORE_AGE["Add age into result.max_age"] + + STORE_AGE --> NEXT_MEMBER + + %% NO MATCH + + CHECK_APPLICABLE -->|No| NEXT_MEMBER + + %% LOOP CONTROL + + NEXT_MEMBER --> MORE_MEMBERS{"More family members?"} + + MORE_MEMBERS -->|Yes| LOOP_MEMBERS + + %% FINAL COUNT + + MORE_MEMBERS -->|No| CALCULATE_COUNT["Set max_count = + count(result.index)"] + + CALCULATE_COUNT --> RETURN_RESULT + + %% RETURN + + RETURN_RESULT(["Return result array"])
diff --git a/app/Views/docs/inception.php b/app/Views/docs/inception.php new file mode 100644 index 00000000..ad406a78 --- /dev/null +++ b/app/Views/docs/inception.php @@ -0,0 +1,389 @@ + + +

+ Inception here means onboarding employees and dependents from an Excel upload + (files.action = inception or related actions like missed_inception, + addition, dependent_addition). The same three-step pipeline runs for those + actions; this page focuses on the inception path. +

+ +

+ Manual policy inception (form UI under policy_tranction/inception) is a separate flow + in PolicyTransactionController — not covered by these three functions. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel on employee upload"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesOnboardPreprocess"] + D --> E["employeesOnboardProcess plus policy_transaction"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|no families inserted| F + D -->|OK| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: Headers, column count/order, per-cell type and mandatory rules.
  • +
  • Step 2 — Data: Family-level checks (Self row, duplicates, policy terms, DB conflicts).
  • +
  • Step 3 — Preprocess: Premium via rack rates, then insert employees and inception policy transaction.
  • +
  • Large files (> 1 MB) run steps 1–3 as background jobs via JobWorker.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — client, branch, policy, action Inception, file upload
Upload handlerEmployeeController::employeesUplodWithEvents
Pipeline logicEmployeeServiceController — the three functions on this page
Job dispatchapp/Controllers/JobWorker.php — maps job names to EmployeeServiceController
Column / Excel helpersapp/Helpers/excel_util_helper.phpcheck_columns_name, custom validators
Upload recordfiles table via FileModelstatus, action, reason
PremiumEB rack rate calculationcalculate_premium_new, employeesOnboardProcess
+ +

Routes (group /employee, filter authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/upload — upload + start validation (upload-action-type=inception)
  • +
  • GET /employee/excel_error/{file_id} — returns files.reason JSON for the error modal
  • +
+ +

+ REST upload (mobile/HR): POST employeeRest/employeeUpload — same pipeline via + EmployeeRestController. +

+ +
+ i +
+ files.policy_id stores client policy id (client_policies.id), + not the insurer policy master id. Slab/rack lookups use this id with client_id. +
+
+ +

Sync vs background jobs

+ + + + + + + + + + + + + + + + + +
File sizeStep 1Steps 2–3
< 1 MBexcelFileFormatValidation runs inline in the upload requestAlways queued: excelFileDataValidationemployeesOnboardPreprocess
≥ 1 MBJob excelFileFormatValidationSame job chain after format passes
+ +

+ After upload the UI usually shows files.status = inprogress until jobs finish. + Poll notifications or refresh the upload list; use /employee/excel_error/{id} when status is failed. +

+ +

Entry points

+ +

Job chain for inception (and missed_inception / addition / dependent_addition):

+

excelFileFormatValidationexcelFileDataValidationemployeesOnboardPreprocess

+ +

Step 1: excelFileFormatValidation

+ +

Runs on the uploaded sheet before any DB business rules.

+ +
    +
  • Loads file from writable/uploads/excel/{file_name}.
  • +
  • Uses $inception_excel_columns when action is inception (same column set for addition / dependent_addition / missed_inception).
  • +
  • Checks policy has terms and slab rates configured — otherwise fails early.
  • +
  • Validates each data row: mandatory (by action code I), date/mobile formats, allowed lists, custom helpers (DOB, relationship, SI, mobile duplicate, etc.).
  • +
  • Stops at first empty row (treated as end of data).
  • +
+ +

Common format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory value missing
2Wrong format (e.g. date, mobile)
3Value not in allowed list
4Custom validation failed (DOB, relationship, SI, etc.)
5File / policy / slab configuration problem
6Column headers wrong or out of order
+ +

On failure: files.status = failed, reason JSON with row/column errors; user notification via pull notification.

+ +

Lead-policy branch: If inception file is for a policy created from leads (policy_entry_from == 3 and is_from_lead set), format validation queues compareMemberDataAndInceptionData instead of going straight to data validation.

+ +

files.reason shape (debugging)

+ +

Stored as JSON string on files.reason. Typical failure payload:

+
{
+  "error_type": 1,
+  "error_summary": { "4": 2, "1": 1 },
+  "error_data": {
+    "3": {
+      "dob": { "error": ["Invalid date format"], "value": "01/01/1990" }
+    }
+  }
+}
+
    +
  • error_type1 = format step, 2 = data step
  • +
  • error_summary — counts per error code (after aggregation)
  • +
  • error_data — keyed by Excel row number (1-based, header is row 1)
  • +
+ +

Step 2: excelFileDataValidation

+ +

Runs after format passes. Groups rows by EMP ID (family) and applies business rules.

+ +
+
+flowchart TD + A["Group rows by emp_id"] --> B{"Self in family?"} + B -->|No| C["Error: Self not found"] + B -->|Yes| D{"Duplicate name in family?"} + D -->|Yes| E["Error: Twofold name"] + D -->|No| F{"Emp code already in DB?"} + F -->|Yes| G["Error: duplicate emp code"] + F -->|No| H{"Dependents match policy terms?"} + H -->|No| I["Dependent conflict errors"] + H -->|Yes| J["Row OK for this family"] +
+
+ +

Typical inception checks:

+
    +
  • Self row required (unless GMC parents policy allows otherwise).
  • +
  • No duplicate names within the same family in the file.
  • +
  • Employee code must not already exist for inception / addition / enrollment.
  • +
  • Dependent rules vs policy_terms (family composition, LGBTQ flag, etc.).
  • +
  • Name + emp id consistency vs database (name_and_empid_check_in_db).
  • +
+ +

On success for inception: queues job employeesOnboardPreprocess. Other actions queue different jobs (deletion, correction, etc.).

+ +

Common data-validation error codes

+ + + + + + + + + + + + +
CodeMeaning
7Duplicate name within same family in the Excel file
9Record already exists in DB (inception / addition)
10Record not found (used on deletion flows)
14Self row missing in family
26Duplicate employee code in file or DB
+ +

Step 3: employeesOnboardPreprocess

+ +

Calculates premium and writes members to the database.

+ +
    +
  1. Reload Excel; group by emp_id.
  2. +
  3. For each family: calculate_premium_new() using policy terms + slab rates + rack config.
  4. +
  5. employeesOnboardProcess() — insert/update employees, employee_polices, create policy_transaction (inception).
  6. +
  7. If at least one family inserted → files.status = success; else failed with rack-rate message.
  8. +
+ +

+ Optional second path: client_policy_id without file_id — converts enrolled DB members + to inception (enrollment → inception), not from Excel. +

+ +

+ TPA batch: When batch_file_id is present on the job payload, success also queues + updateEmployeeDataFromTpa, reconTpaApiDataWithEmployeepolicies, and + initializeDeletionProcessForTpaApiData (multi-file TPA reconcile flow). +

+ +

Inception Excel columns

+ +

+ Defined in EmployeeServiceController::$inception_excel_columns. + Header row must match exactly — 19 columns (A–S), row 1 only. + Dates use format d-M-Y (e.g. 4-Apr-1990). + One Self row per EMP ID; other rows are dependents. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ColHeaderRequired (inception)Notes
AS.NoYes
BEMP IDYesFamily key
CNAME OF EMP/DEPYes
DDOBYesd-M-Y; age vs relationship checked
EGenderYesM / F (several casings allowed)
FRELATIONSHIPYesSelf, Spouse, Son, Daughter, …
GBASIC COVER SIConditionalValidated against slab when applicable
HDate of CoverageNoMandatory for addition / DA only
IDOJNo
JBasic PayNoUsed when policy terms need it
KBand/GradeNo
LDesignationNo
MPhoneNoMobile format; duplicate check
NEmailNoDuplicate check in file
OPRE EXISTING AILMENTSYes0 or 1
PChange eventNoNot used for pure inception
QDate of exitNoDeletion only
RReason for exitNoDeletion only
SUnitNoMust match branch units when filled
+ +

Sample file: use the download link on the employee upload screen (environment-specific).

+ +

Family row layout example

+ +

+ All rows with the same EMP ID (column B) are treated as one family. + Step 2 requires exactly one Self row in that group; dependents share the same EMP ID. + Step 3 runs premium and DB insert once per family. +

+ +

Example: one employee (EMP001) with spouse and son — three data rows plus header.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RowS.NoEMP IDNAMEDOBGenderRELATIONSHIPBASIC COVER SIPED
1Header row (all 19 columns A–S required in file)
21EMP001Raj Kumar15-Jan-1985MSelf5000000
32EMP001Priya Kumar20-Mar-1988FSpouse5000000
43EMP001Arjun Kumar10-Jun-2015MSon5000000
+ +

Rules illustrated:

+
    +
  • Same EMP ID on rows 2–4 → one family processed in step 3.
  • +
  • Self on row 2 only — row 3 without Self would fail with code 14.
  • +
  • DOB uses d-M-Y; ages are checked against relationship (e.g. Son vs Self).
  • +
  • PED (PRE EXISTING AILMENTS) = 0 or 1 on every member for inception.
  • +
  • BASIC COVER SI must match slab rules when the policy uses SI-based racks (often same amount across the family).
  • +
  • Columns H–S can be blank for inception when not mandatory; phone/email must be unique in the file if filled.
  • +
+ +

+ A second employee in the same file uses a different EMP ID (e.g. EMP002) with its own Self row — each EMP ID is a separate family loop in preprocess. +

+ +

Developer steps

+ +
    +
  1. Confirm policy has policy_terms JSON and slab/rack rates for the client policy.
  2. +
  3. Upload via /employee/upload with action inception; note file_id in response or files table.
  4. +
  5. If status = failed, call GET /employee/excel_error/{file_id} or read files.reason.
  6. +
  7. Map error_data row keys back to Excel (row 1 = header).
  8. +
  9. If format passes but preprocess fails with rack message, debug calculate_premium_new (see rack-rate doc) and SI/slab config.
  10. +
  11. For stuck inprogress, check job queue / JobWorker logs for the three job names.
  12. +
+ +

Common pitfalls

+ +
    +
  • Policy not ready — missing policy_terms or slab rates fails step 1 with code 5.
  • +
  • Missing Self row — one Self per EMP ID in the file (code 14 in step 2).
  • +
  • Wrong header row — column count or name mismatch (code 5 / 6).
  • +
  • Rack rate / SI — preprocess succeeds only if calculate_premium_new returns data for every family.
  • +
  • File status — watch files.reason JSON for row-level errors after failure.
  • +
diff --git a/app/Views/docs/non-eb-claims.php b/app/Views/docs/non-eb-claims.php new file mode 100644 index 00000000..71aec5c5 --- /dev/null +++ b/app/Views/docs/non-eb-claims.php @@ -0,0 +1,488 @@ + + +

+ This page documents the Non-EB Claims web workflow: list and filter claims, + create and edit tickets, status-driven form sections, document uploads, manual email reply, + mail template CRUD, auto-mail on create/status change, and claim reports. +

+ +

+ Scope: authenticated MVC routes under /non-eb-claim/* only. + Mobile/REST endpoints in Api\NonEbClaimApiController are not covered here. +

+ +

+ Policy types are limited to policy_type.allocg IN ('Non-EB', 'Marine'). + Claim records live in non_eb_ticket_master; claim files use ticket_type = 2 + in claim_files. +

+ +

End-to-end flow

+ +
+
+flowchart TD + L["GET /non-eb-claim/list"] --> F["Filter sidebar → POST /list"] + F --> T["Table non_eb_claim_list.php"] + T --> A{"Action"} + A -->|Add| N["GET /non-eb-claim/new/50 or new/{policy_type_id}"] + N --> C["POST /non-eb-claim/create"] + C --> AM["sendAutoMailTrigger if template is_auto_mail=1"] + A -->|Row click / View| V["GET /non-eb-claim/view/{id}"] + V --> U["POST /non-eb-claim/update"] + U --> SC{"claim_status_id changed?"} + SC -->|yes| AM2["sendAutoMailTrigger"] + MT["GET /non-eb-claim/mail_template"] --> CRUD["POST crud_mail_template/1|2|3"] + R["GET /non-eb-claim/reports"] --> RP["POST /reports — UI only; see pitfalls"] +
+
+ +

Typical user path:

+
    +
  1. Open claim list (default: open claims excluding settled/closed/rejected/withdrawn).
  2. +
  3. Add claim → form opens with canonical status policy type 50 — see why 50 is hardcoded.
  4. +
  5. On save, optional auto-mail fires if a matching template has is_auto_mail = 1.
  6. +
  7. Open claim from list → edit view with history, messages, files, notes.
  8. +
  9. Change status → allowed next statuses from ticket_claim_status.allowed_status; section visibility updates.
  10. +
  11. Configure templates at /non-eb-claim/mail_template (direct URL; not in main Claims sidebar today).
  12. +
+ +

Why policy type 50 is hardcoded (read this before changing Non-EB Claims)

+ +
+

+ Non-EB has many real policy products (Fire, Marine, Liability, etc. — each row in + policy_type with allocg Non-EB or Marine). Claim status workflows are + not stored separately per product today. The team configured one canonical policy type, + id 50, in the database as the single source of status definitions. The application + assumes every Non-EB/Marine claim uses that same status set unless you deliberately change + backend and frontend. +

+
+ +

Design assumption

+ +

+ Claim statuses live in ticket_claim_status, keyed by ticket_type (which equals + policy_type.id). Mail templates in ticket_mail_template also key off + ticket_type + trigger_type from that status row. +

+ +

Instead of maintaining duplicate status trees for every Non-EB product, developers assumed:

+ +
+

+ All Non-EB and Marine policy types share one identical claim status lifecycle. + That lifecycle is configured only under policy type 50 in + ticket_claim_status (and matching templates under ticket_type = 50). +

+
+ +

+ New claims opened via the list Add button or New Non EB Claim menu therefore + pass 50 into the form URL. Status dropdowns, section visibility, and initial status resolution in + getClaimStatusForPolicyType($policy_type_id) all use that id on create — not the id of the policy + the user later picks from client_policy. +

+ +

Policy type 50 vs policy selected on the form

+ +

On create (non_eb_claim_form.php):

+
    +
  • Hidden policy_type_id is set from the URL segment (typically 50) and is not updated when the user selects a branch policy.
  • +
  • The UI shows the real product name in policy_type_display from the selected policy row (data-policy-type).
  • +
  • POST /non-eb-claim/create persists policy_type_id from that hidden field — so new tickets from Add often store policy_type_id = 50 even when the linked policy is Fire, Marine, etc.
  • +
+ +

+ On edit (non_eb_claim_edit.php), policy_type_id comes from + non_eb_ticket_master as saved. Status changes and templates use that stored id. +

+ +

Where 50 appears in code (change all if this design changes)

+ +
+ + + + + + + + + + + + +
LocationUsage
app/Config/Routes.phpGET non-eb-claim/newclaimForm/50
app/Views/non_eb_claim_list.phpDataTable Add button → /non-eb-claim/new/50
app/Views/non_eb_claim_form.phpHidden policy_type_id from route; status/section AJAX uses this value
ticket_claim_status (DB)Master status rows maintained with ticket_type = 50
ticket_mail_template (DB)Non-EB auto-mail templates should use ticket_type = 50 if they follow the shared workflow
Api\NonEbClaimApiController::listClaimStatuses()API hardcodes where('ticket_type', 50) (out of scope for this page but same assumption)
+
+ +

When a Non-EB product needs a different status set

+ +

+ If a new (or existing) policy type must have its own statuses, triggers, or allowed transitions + (not the shared tree under 50), you cannot only change the product row in + policy_type. You must update both backend and frontend: +

+ +
    +
  1. Database — Add full ticket_claim_status rows with + ticket_type = <that policy_type.id> (claim_status, display_name, trigger_type, + allowed_status JSON). Add matching ticket_mail_template rows for that + ticket_type if auto-mail applies.
  2. +
  3. Routes / entry URL — Stop routing every new claim through 50: e.g. change + claimForm/50, restore policy-type picker modal (commented in + non_eb_claim_search.php), or pass the correct policy_type_id per product.
  4. +
  5. List Add button — Replace hardcoded new/50 in + non_eb_claim_list.php with the correct id or dynamic selection.
  6. +
  7. Create form — On policy selection, set hidden #policy_type_id to the real + policy_type_id from getBranchAndPolicy (field p.policy_type_id is + already returned) so create/update and getVisibleSections use the right status tree.
  8. +
  9. Controller logic — Ensure getClaimStatusForPolicyType, + getTemplateDataByTicketID, and filters that assume a single Non-EB status catalog are tested for + the new ticket_type.
  10. +
  11. API — Replace hardcoded 50 in listClaimStatuses() if mobile + clients need per-product statuses.
  12. +
+ +

+ Until those steps are done, pointing Add at another id without cloning the full status + template set under + that id will produce empty status lists, wrong section visibility, or + missing auto-mail. +

+ +

Key files and routes

+ + + + + + + + + + + + + + + + + +
AreaFile / route
Controllerapp/Controllers/NonEbClaimController.php
Modelapp/Models/NonEbTicketMasterModel.php
List + filtersapp/Views/non_eb_claim_search.php, non_eb_claim_list.php
New claimapp/Views/non_eb_claim_form.php
View / editapp/Views/non_eb_claim_edit.php
Mail templatesapp/Views/non_eb_claim_mail_template.php
Reportsapp/Views/non_eb_claim_reports.php
Routesapp/Config/Routes.php — group /non-eb-claim, filter authMVC
ACLapp/Config/Acl.php#^/non-eb-claim# (Claims team roles)
App menuapp/Views/layout/header.php — New / List only (EB mail template link is separate)
+ +

MVC route map

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRouteControllerPurpose
GET/non-eb-claim/listclaimListSearch page + default open claims
POST/non-eb-claim/listclaimListFilter → HTML partial for DataTable
GET/non-eb-claim/newclaimForm/50New claim (default policy type 50)
GET/non-eb-claim/new/{policy_type_id}claimFormNew claim for selected product
POST/non-eb-claim/createcreateClaimCreate ticket + assets + history + auto-mail
GET/non-eb-claim/view/{id}view_claimEdit layout (non_eb_claim_edit)
POST/non-eb-claim/updateupdateClaimUpdate; auto-mail on status change
GET/non-eb-claim/remove?ticket_id=removeClaimSoft delete (is_active = 0)
GET/non-eb-claim/mail_templatemailTemplateTemplate list + modal CRUD UI
POST/non-eb-claim/crud_mail_template/1crudTemplateSave template
POST/non-eb-claim/crud_mail_template/2crudTemplateFetch one template (edit)
POST/non-eb-claim/crud_mail_template/3crudTemplateSoft delete template
POST/non-eb-claim/note/1crudNoteGet note
POST/non-eb-claim/note/2crudNoteSave note
POST/non-eb-claim/replysaveReplyManual outbound mail + message row
GET/non-eb-claim/reportsclaimReportsReports UI shell
POST/non-eb-claim/reportsclaimReportsNot implemented in controller — see Reports
POST/non-eb-claim/getBranchAndPolicygetBranchAndPolicyByClientIDBranches, policies, contacts for client
POST/non-eb-claim/getVisibleSectionsgetVisibleSectionsAjaxSection keys for status
POST/non-eb-claim/getMoreInfogetMoreInfoTicket row JSON
POST/non-eb-claim/uploadFileuploadFileClaim docs (file or Drive URL)
POST/non-eb-claim/getClaimFilesgetClaimFilesList files for ticket
GET/non-eb-claim/removeFile?id=removeFileSoft delete file
POST/non-eb-claim/saveIRDocssaveIRDocsPersist required-docs JSON on ticket
GET/non-eb-claim/testAutoMail/{id}testAutoMailTriggerDev/test auto-mail (optional)
+
+ +

Access control

+ +

+ All routes in the /non-eb-claim group use the authMVC filter. + Acl.php allows roles HEAD, ADMIN, MANAGER, + ACCOUNT_MANAGER on the Claims team. +

+ +

+ List row actions (view / delete) render only for roles 1, 2, 5 in + non_eb_claim_list.php. +

+ +

List and filter

+ +

GET /non-eb-claim/list loads non_eb_claim_search.php, which includes the table partial. Initial data comes from claimSearch(1): active tickets where status display name is not in Claim Settled, Claim Closed, Claim Rejected, Claim Withdrawn.

+ +

Filter sidebar posts the same URL with criteria (at least one required):

+
    +
  • policy_type_id, insurer_id, claim_number, nhance_claim_ref_no
  • +
  • client_id, claim_status_id (status options filtered by policy type in JS)
  • +
  • date_type + start_date / end_date (created_date or updated_date)
  • +
+ +

+ Response is JSON { status: true, html: "…" }; JS replaces #claim_list_div + and re-initializes the DataTable. Row click navigates to /non-eb-claim/view/{id}. +

+ +

+ Add button: window.location.href = '/non-eb-claim/new/50'. + Header menu uses /non-eb-claim/new (routes to claimForm/50). + See Policy type 50 for why this id is fixed and what to change if a product needs its own statuses. +

+ +

Create and edit claim

+ +

Create

+ +
    +
  1. GET /non-eb-claim/new/{policy_type_id}getFormData(): ACMs (role 3), clients, insurers, initial claim status for product, visible sections.
  2. +
  3. User selects client → POST getBranchAndPolicy fills branch, policy (Non-EB/Marine only), branch contact.
  4. +
  5. Status change → POST getVisibleSections toggles accordion sections client-side.
  6. +
  7. POST /non-eb-claim/create — validation via getValidationRules(), sanitizeInputArrayAdvanced, date normalization, optional asset file upload.
  8. +
  9. Duplicate guard: same client_id + loss_date + policy_no (if policy set) → HTTP 409-style JSON.
  10. +
  11. On success: insert non_eb_ticket_master, saveAssets(), history row, sendAutoMailTrigger(), redirect to list.
  12. +
+ +

View / edit

+ +

+ view_claim($id) loads ticket via NonEbTicketMasterModel::getTicketDataByTicketID(), + merges mail template preview (getTemplateDataByTicketID + placeholder replace), + messages, history, assets, and reuses the edit view non_eb_claim_edit.php. +

+ +

+ POST /non-eb-claim/update mirrors create validation. If claim_status_id changes, + auto-mail runs again. remark_mode=append appends to closure_remark with a separator. +

+ +

Status-driven sections

+ +

+ $statusSectionVisibility in the controller maps each ticket_claim_status.claim_status + label to section keys: policy_account, loss_incident, intimation, + insured_contact, asset, documents, surveyor, settlement. +

+ +

+ Allowed next statuses come from getClaimStatusForPolicyType(): current status plus IDs in + allowed_status JSON on the status row. Both create and edit forms call + getVisibleSectionsAjax when the user changes status. +

+ +

Auto-mail and placeholders

+ +

Template lookup (NonEbTicketMasterModel::getTemplateDataByTicketID):

+
    +
  • Join ticket_claim_status on ticket’s claim_status_id (or explicit status_id for tests).
  • +
  • Join ticket_mail_template where ticket_type = policy_type_id AND trigger_type = tcs.trigger_type.
  • +
+ +

+ sendAutoMailTrigger($ticket_id) sends only when the matched template has + is_auto_mail = 1. Mail goes to insured_contact_email from + constructMailContent(); from address claims@nhanceindia.in via MailHelper::send_email(). + Successful sends insert a ticket_messages row via autoMessageInsertBasedOnMailResponse(). +

+ +

Placeholders (subject/body):

+ +
+ + + + + + + + + + + + + + +
TokenTicket field
((ACM))acm
((ACM_CONTACT))acm_mobile
((INSURED_NAME))insured_contact_name
((CORPORATE_NAME))client_name
((CLAIM_NO))claim_number
((POLICY_TYPE))policy_type_name
((NHANCE_REF_NO))nhance_claim_ref_no
((LOSS_DATE))loss_date
((LOSS_LOCATION))loss_location
((NATURE_OF_LOSS))nature_of_loss
+
+ +

+ Edit screen also supports manual reply: POST /non-eb-claim/reply validates To/Subject, + inserts ticket_messages, sends via sendReplyMessage() with placeholder replacement. +

+ +

Mail template CRUD

+ +

+ Page: GET /non-eb-claim/mail_template. DataTable lists rows from + ticket_mail_template (is_active = 1). Tooltip on each row shows the + matching ticket_claim_status.claim_status for that policy type + trigger type. +

+ +
+
+flowchart LR + UI["mail_template UI"] --> S1["POST crud_mail_template/1 Save"] + UI --> S2["POST crud_mail_template/2 Fetch"] + UI --> S3["POST crud_mail_template/3 Delete"] + S1 --> DB["ticket_mail_template"] + S2 --> DB + S3 --> DB +
+
+ +

Template fields

+ +
    +
  • template_name, ticket_type (policy type id), trigger_type (1–9)
  • +
  • subject, mail_content (Jodit HTML)
  • +
  • is_auto_mail — checkbox “Auto Mail” (1 = send on create / status change when matched)
  • +
  • Optional id on save for update
  • +
+ +

+ Trigger ↔ status: Each ticket_claim_status row for a Non-EB/Marine + ticket_type has a trigger_type. The template’s trigger_type must match + that column for auto-mail and for the “Claim Status” readonly hint (tool_tip from server on fetch). +

+ +

UI actions

+ +
+ + + + + + + + + +
ActionEndpointBodyResult
Add / SavePOST …/crud_mail_template/1Form fields + mail_content + is_auto_mail{ status: bool } → reload
Edit loadPOST …/crud_mail_template/2id{ status, data } opens modal
DeletePOST …/crud_mail_template/3idSoft delete (is_active = 0)
+
+ +

+ Placeholder dropdown in the modal inserts tokens into subject (focused input) or Jodit body. + Validation errors return HTTP 400 with errors map (shown via toastr). +

+ +

Notes

+ +

On the edit screen:

+
    +
  • POST /non-eb-claim/note/1id (ticket), optional is_auto_query → fetch active note.
  • +
  • POST /non-eb-claim/note/2 — save note (required 3–1000 chars) via TicketNoteModel.
  • +
+ +

Documents and IR checklist

+ +
    +
  • uploadFile — multi upload to writable/uploads/claim_files/ or Google Drive URLs (file_type 1 vs 2).
  • +
  • getClaimFiles — lists rows; local files expose download URL downloadClaimFile/{id}.
  • +
  • removeFile — soft delete by file id.
  • +
  • saveIRDocs — stores JSON in non_eb_ticket_master.required_docs.
  • +
  • Asset spreadsheet on ticket: asset_file under writable/uploads/non_eb_asset_files/; loss description required when file uploaded.
  • +
+ +

Reports

+ +

+ GET /non-eb-claim/reports renders filters: policy type, ACM name, date range (default last 60 days). + generateReport() in the view POSTs to the same URL and expects + { status: true, data: [ rows ] } for DataTable columns (status, policy type, claim/ref, client, insurer, loss fields, surveyor, settlement, ACM, created date). +

+ +

+ Gap: NonEbClaimController::claimReports() only handles GET (layout load). + There is no POST handler to return report data — Generate Report will fail until POST logic is added + (mirror EB ticket_reports or reuse claimSearch with report-specific selects). +

+ +

Data model (summary)

+ + + + + + + + + + + + + +
TableRole
non_eb_ticket_masterMain claim ticket
non_eb_claim_assetRepeating asset lines per ticket
ticket_claim_statusStatuses per ticket_type (policy type id); trigger_type, allowed_status
ticket_mail_templateTemplates; ticket_type = policy type id
ticket_historyField-level audit (status, ACM, priority, …)
ticket_messagesOutbound mail log
ticket_notesUser notes per ticket
claim_filesAttachments; ticket_type = 2 for Non-EB
+ +

Controller reference

+ +
+ + + + + + + + + + + + + + + + + + + +
MethodUsed for
claimList / claimSearchList UI and filtered HTML
claimForm / getFormDataNew claim form bootstrap
view_claimEdit view
createClaim / updateClaimPersist ticket
getClaimStatusForPolicyType / getVisibleSections*Status dropdown + sections
mailTemplate / crudTemplateTemplate admin
sendAutoMailTrigger / constructMailContentAutomated email
saveReply / getTicketMessageManual email thread
crudNoteNotes
uploadFile / getClaimFiles / removeFile / saveIRDocsDocuments
saveAssets / getAssetsAsset grid
claimHistory / putHistoryAfterInsertAudit trail
getBranchAndPolicyByClientIDClient cascade
claimReportsReports page (GET only today)
testAutoMailTriggerDev preview/send test
+
+ +

Developer checklist

+ +
    +
  1. Ensure ticket_claim_status rows exist per Non-EB/Marine policy_type.id with correct trigger_type and allowed_status.
  2. +
  3. Create mail templates at /non-eb-claim/mail_template with matching ticket_type + trigger_type; enable Auto Mail only where intended.
  4. +
  5. Verify insured email is present before relying on auto-mail.
  6. +
  7. Before changing Add/new URLs: read Policy type 50 — clone statuses + templates in DB and update every hardcoded 50 if a product needs its own workflow.
  8. +
  9. Implement POST branch in claimReports() if reports Generate must work.
  10. +
  11. Claim files: always set ticket_type = 2 in new file-related code paths.
  12. +
  13. ACL: extend #^/non-eb-claim# if new roles need access.
  14. +
+ +

Common pitfalls

+ +
    +
  • Template mismatch: No row in ticket_mail_template for policy type + status trigger_type → no auto-mail and empty reply preview.
  • +
  • Reports POST missing: UI POSTs to /non-eb-claim/reports but controller only loads view on GET.
  • +
  • Duplicate claims: Same client + loss date + policy number blocked on create.
  • +
  • Policy type 50 assumption: Shared status catalog under DB ticket_type = 50; Add/new routes and hidden form field use 50 — not the policy picked on the form. Different per-product statuses require full backend + frontend changes — checklist.
  • +
  • Menu vs Non-EB templates: Sidebar “Mail Template” points to EB /ticket/mail_template, not Non-EB.
  • +
  • Soft deletes: Remove claim/file/template sets is_active = 0; list queries filter active only.
  • +
  • Asset file: Upload without loss description fails validation on create/update.
  • +
  • REST API separate: Mobile create/list under employeeRest / Api\NonEbClaimApiController — different validation and flows.
  • +
diff --git a/app/Views/docs/non-eb-opportunities.php b/app/Views/docs/non-eb-opportunities.php new file mode 100644 index 00000000..eb4e7576 --- /dev/null +++ b/app/Views/docs/non-eb-opportunities.php @@ -0,0 +1,638 @@ + + +

+ This page documents the Non-EB Opportunities workflow from the + Opportunities list (/leads/list). A user creates a Non-EB opportunity, + then progresses through RFQ → QCR → mail actions → placement — all from the list row action menu. +

+ +

+ Non-EB rows are identified by leads.lead_form_type = 2 (EB = 1). + RFQ and QCR are managed in Google Sheets; sheet IDs are stored in + leads.misc JSON. +

+ +

End-to-end flow

+ +
+
+flowchart TD + A["/leads/list — Add → Non-EB"] --> B["Add form leads_non_eb.php"] + B --> C["POST /leads/create"] + C --> D["status: queued"] + D --> E["Action: RFQ"] + E --> F["GET /leads/createRfqSheet"] + F --> G["status: rfq_created"] + G --> H["Action: QCR"] + H --> I["GET /leads/createQcrSheet"] + I --> J["status: qcr_created"] + J --> K["Action: Send Internal Mail"] + K --> L["POST /leads/sendMail internal"] + L --> M["Action: Send Insurer Mail"] + M --> N["POST /leads/sendMail insurer → rfq_sent"] + N --> O["Action: Send Client Mail"] + O --> P["POST /leads/sendMail client → qcr_sent"] + P --> Q["Action: Placement"] + Q --> R["POST /leads/sendMail placement → won"] +
+
+ +

Typical sequence from the list:

+
    +
  1. Create opportunity (form submit) → queued
  2. +
  3. RFQ — create/open Google Sheet → rfq_created
  4. +
  5. QCR — copy RFQ sheet → qcr_created
  6. +
  7. Send Internal Mail — team mail with RFQ or QCR attachment (by current status)
  8. +
  9. Send Insurer Mail — RFQ sheet attached → rfq_sent
  10. +
  11. Send Client Mail — QCR sheet attached → qcr_sent
  12. +
  13. Placement — placement sheet + policy/payment fields → won
  14. +
+ +

+ Edit is available at any stage from the same action menu and reuses the add form + with pre-filled data (getLeadNonEB + POST /leads/create with lead id). +

+ +

Key files

+ + + + + + + + + + + + +
AreaFile
List + action dropdownapp/Views/leads_list.php
Non-EB add/edit formapp/Views/leads_non_eb.php
EB vs Non-EB form includeapp/Views/leads_form_handler.php
All backend logicapp/Controllers/LeadsController.php
Google SheetsGoogleSheetLib, Config\RfqConfig
+ +

Google Sheet config

+ +

+ Non-EB RFQ, QCR, and Placement sheets are Google Drive files created at runtime by + GoogleSheetLib using a service account. Only RFQ needs a pre-configured + template per product; QCR and Placement are copies of the lead’s RFQ/QCR sheets. +

+ +

App ↔ Google Drive flow

+ +
+
+flowchart TB + subgraph setup ["One-time / per product setup"] + T["Drive: master RFQ template per product"] + PT["policy_type.misc.rfq_template_sheet_id"] + ENV[".env RFQ_PARENT_FOLDER_ID"] + SA["Service account JSON + share folder/templates"] + end + + subgraph rfq ["RFQ — createRfqSheet"] + R1["Read template ID from policy_type.misc"] + R1 --> C1["Drive copyTemplate → parent folder"] + C1 --> F1["Sheets find/replace placeholders"] + F1 --> P1["Drive applyPermissions viewers"] + P1 --> PR1["Sheets applyProtections"] + PR1 --> S1["Save leads.misc.rfq_sheet_id"] + end + + subgraph qcr ["QCR — createQcrSheet"] + S1 --> C2["Drive copyTemplate from rfq_sheet_id"] + C2 --> P2["applyPermissions viewers"] + P2 --> S2["Save leads.misc.qcr_sheet_id"] + end + + subgraph placement ["Placement — createAndDownloadPlacementSheet"] + S2 --> C3["Drive copyTemplate from qcr_sheet_id"] + C3 --> P3["applyPermissions viewers"] + P3 --> S3["Save leads.misc.placement_sheet_id"] + S3 --> X1["Drive export .xlsx → mail attachment"] + end + + subgraph mail ["Mail — downloadFileFromGoogleSheet"] + S1 --> X2["Export RFQ or QCR as Excel"] + S2 --> X2 + end + + PT --> R1 + ENV --> C1 + T --> PT + SA --> C1 +
+
+ +

Library: app/Libraries/GoogleSheetLib.php — auth via service account JSON at +{project-root}/nhance-ee8d1-e3c5269b1ec7.json, scopes DRIVE + SPREADSHEETS.

+ +

What must be done before RFQ / QCR

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StepBeforeWhy
1Google service account JSON present; account email shared on template folder + each template sheetGoogleSheetLib cannot copy or edit without Drive access
2RFQ_PARENT_FOLDER_ID set in .envTarget folder for every copied RFQ/QCR/Placement file
3Master RFQ Google Sheet template created per product (Fire, Marine, GPA, etc.) with placeholder tokensRFQ copy source — one template per policy_type row
4policy_type.misc JSON updated with rfq_template_sheet_id for that productcreateRfqSheet() reads this; fails with “RFQ template not found” if missing
5Non-EB opportunity created with correct policy_type_id and rfq_qcr_viewers emailsLead must exist; viewers become sheet editors after copy
6Before QCR: RFQ action completed (leads.misc.rfq_sheet_id set)QCR copies the lead’s RFQ sheet, not the policy template
7Before Placement mail: QCR action completed (leads.misc.qcr_sheet_id set)Placement copies the QCR sheet
+ +

Where to configure sheet ID per product

+ +

+ Template sheet IDs are stored per product on the policy_type table — + one row per product (Fire, Marine, Burglary, etc.). The lead’s selected + policy_type_id determines which template is copied when RFQ is clicked. +

+ +

Database column: policy_type.misc (JSON text)

+ +
{
+  "rfq_template_sheet_id": "1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0"
+}
+ +

Example — set or update for policy type id 12:

+ +
UPDATE policy_type
+SET misc = JSON_SET(COALESCE(misc, '{}'), '$.rfq_template_sheet_id', 'YOUR_GOOGLE_DRIVE_FILE_ID')
+WHERE id = 12;
+ +
+ i +
+ There is no admin UI field for rfq_template_sheet_id today — configure via DB (or extend + MasterController::editPolicyType / policy_type_onboarding if you add a form field). + Model allow-list: app/Models/PolicyTypeModel.php includes misc. +
+
+ +

How to find a template file ID:

+
    +
  • From the Google Sheet URL: https://docs.google.com/spreadsheets/d/{FILE_ID}/edit
  • +
  • CLI — list all sheets in the RFQ parent folder: + php public/index.php cli/list-sheet-folder-files {RFQ_PARENT_FOLDER_ID} + → writes sheetid.json with name + sheetId pairs + (GoogleSheetController::listFolderSheetFilesCli)
  • +
+ +

QCR and Placement: no separate template ID per product. They always copy from the lead’s existing sheets:

+ + + + + + + + + + +
StageCopy sourceStored on lead
RFQpolicy_type.misc.rfq_template_sheet_idleads.misc.rfq_sheet_id
QCRleads.misc.rfq_sheet_idleads.misc.qcr_sheet_id
Placementleads.misc.qcr_sheet_id (filename: QCR → Placement)leads.misc.placement_sheet_id
+ +

App-level config files

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SettingLocationPurpose
RFQ_PARENT_FOLDER_ID.envConfig\RfqConfig::$rfqParentFolderIdGoogle Drive folder where copied RFQ/QCR/Placement files are created
rfqPlaceholdersapp/Config/RfqConfig.phpMaps template tokens like {{INSURED_NAME}} to lead field keys filled on RFQ create
rfqClaimsPlaceholderapp/Config/RfqConfig.phpDefault {{CLAIMS_DETAILS}} — multi-row claims table from fin_years_claims
protectionsapp/Config/RfqConfig.phpLocked cell ranges on new RFQ sheets only (e.g. RFQ Page!B12:C12)
Service account keynhance-ee8d1-e3c5269b1ec7.json (project root)Google API authentication for all sheet operations
+ +

Placeholder tokens to embed in each product’s RFQ master template:

+ + + + + + + + + + + + + + +
Token in templateFilled from
{{INSURED_NAME}}Client name / short name
{{COMMUNICATION_ADDRESS}}Client or custom field address
{{GST}} / {{PAN}}Lead GST / PAN
{{POLICY_PERIOD}}Policy start – end dates
{{OPPORTUNITY_TYPE}}Fresh / Renewal label
{{RISK_LOCATION}} / {{OCCUPANCY}}Custom policy-type fields
{{CLAIMS_DETAILS}}Claim history table (renewal leads)
+ +

+ After RFQ copy, editors are granted from the lead’s rfq_qcr_viewers JSON email list (selected on the create/edit form). + The same list is applied to QCR and Placement copies. +

+ +

Step 1 — Create opportunity

+ +

From the list

+
    +
  1. Open /leads/list → click Add.
  2. +
  3. Modal Select Opportunity Type → choose Non-EB (lead_form_type=2).
  4. +
  5. Redirect: GET /util/getLeadNonEB/2/0.
  6. +
+ +

Form (leads_non_eb.php)

+
    +
  • Sections: Opportunity Details, Client Information, Branch Information, Policy Details, Sales & Assignment.
  • +
  • Policy types limited to allocg = Non-EB or Marine.
  • +
  • Policy type change → GET /util/getPolicyTypeFields → dynamic fields in #appendArea.
  • +
  • Renewal types may show claim history rows → stored as fin_years_claims JSON.
  • +
  • rfq_qcr_viewers (emails) become Google Sheet editors later.
  • +
  • Submit → AJAX POST /leads/create with lead_form_type=2.
  • +
+ +

Controller: createLead()

+
    +
  • prepareLeadData()prepareSingleLeadData() (one lead row; EB uses multi-row).
  • +
  • Non-EB validation: policy type, DOC/DOE, claim-history rows when claim_history=1.
  • +
  • insertNewLead() — no demography background job (EB-only).
  • +
  • Initial status: queued (In-Queued).
  • +
+ +

Edit flow

+ +

From the list action menu → Edit (available for both EB and Non-EB):

+ +
    +
  1. JS: getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)
  2. +
  3. Redirect: GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}
  4. +
  5. Controller: getLeadNonEB($type, $actual_lead_id, $id) (~line 4818) +
      +
    • Loads master data (issuer, policy types, sales team, etc.)
    • +
    • When $id present: fetches lead_edit_data, files, custom fields, date formatting
    • +
    • Builds dynamic policy HTML via generateViewPageHtml()
    • +
    • Renewal Non-EB: renders rfq/claims_details_non_eb into claims_details_html
    • +
    • Returns layout with leads_form_handler → includes leads_non_eb.php when selected_lead_type != 1
    • +
    +
  6. +
  7. Form pre-fills client, branch, policy, files, viewers, status, lost reason, etc.
  8. +
  9. Submit same endpoint: POST /leads/create with hidden idupdateOldLead()
  10. +
+ +

Steps 2–7 — List table actions (Non-EB)

+ +

+ When lead_form_type === 2, the row action menu in + app/Views/leads_list.php (lines ~297–343) uses modal/AJAX flows instead of + navigating to /rfq/list/{id}/1|2 (EB behaviour). +

+ +
+ i +
+ Menu order on screen (after opportunity is created): Edit → RFQ → QCR → + Send Internal Mail → Send Insurer Mail → Send Client Mail → Placement → Email History. +
+
+ +

Action → view handler → controller endpoint

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ActionVisible whenView (JS) / handler classController endpoint(s)Status after
EditAlways (EB + Non-EB)getLeadsDataForEdit() + GET + /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}
+ POST + /leads/create (update when id posted) +
User-selected on form
1RFQlead_form_type === 2 only.btnRfqSheetListcreateRfqSheetFromList() + GET + /leads/createRfqSheet?lead_id={id}
+ LeadsController::createRfqSheet() — opens Google Sheet URL in new tab +
rfq_created
2QCR + Non-EB; status not queued or rfq_created; + role 1, 5, 2, 3 or Business Support team + .btnQcrSheetListcreateQcrSheetFromList() + GET + /leads/createQcrSheet?lead_id={id}
+ LeadsController::createQcrSheet() +
qcr_created
3Send Internal MailNon-EB; status != queued.btnInternalMailListopenInternalMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=rfq
+ POST + /leads/sendMailrecipient_type=internal +
4Send Insurer MailSame as internal mail.btnInsurerMailListopenInsurerMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=rfq
+ POST + /leads/sendMailrecipient_type=insurer (RFQ sheet attach) +
rfq_sent
5Send Client MailSame as internal mail.btnClientMailListopenClientMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=qcr
+ POST + /leads/sendMailrecipient_type=client (QCR sheet attach) +
qcr_sent
6PlacementSame as internal mail.btnPlacementListopenPlacementFromList() + GET + /rfq/placementData/{id}
+ GET + /leads/mailTemplate?lead_id={id}&template_type=placement
+ POST + /leads/sendMailrecipient_type=placement +
won
Email HistoryAlways (EB + Non-EB).btnHistorygetLeadsDataForMailHistory() + GET + /util/getLeadEmailHistory/{id}getLeadEmailHistory() +
+
+ +

EB contrast (same menu, lead_form_type === 1): RFQ/QCR are links to +/rfq/list/{id}/1 and /rfq/list/{id}/2; internal/insurer/client/placement mail items are not shown.

+ +

Shared mail modal helpers (all in leads_list.php):

+
    +
  • POST /leads/uploadLeadAttachment — optional extra files (lead_id, docs_name, file)
  • +
  • constructURL_ForInternalMailSend(), insurer/client via constructURL_ForInsurerOrClientMailSend(), placement via constructURL_ForPlacementMailSend()
  • +
  • External CC disclaimer modal (#external_cc_disclaimer_modal) before client/placement send to non-user emails
  • +
+ +

Step 2 — RFQ (createRfqSheet())

+ +
    +
  1. Load lead + policy_type.misc.rfq_template_sheet_id.
  2. +
  3. If misc.rfq_sheet_id exists → return existing Google Sheet URL.
  4. +
  5. Copy template via GoogleSheetLib::copyTemplate() into RfqConfig::rfqParentFolderId.
  6. +
  7. Fill placeholders (buildRfqPlaceholderData()): client, GST, PAN, policy period, claims table.
  8. +
  9. Grant editors from leads.rfq_qcr_viewers email JSON.
  10. +
  11. Save misc.rfq_sheet_id; set status = rfq_created.
  12. +
  13. UI opens sheet in a new browser tab.
  14. +
+ +

Step 3 — QCR (createQcrSheet())

+ +
    +
  1. Requires misc.rfq_sheet_id — returns 400 if RFQ not created yet.
  2. +
  3. If misc.qcr_sheet_id exists → return existing URL.
  4. +
  5. Copy the RFQ sheet (not the policy template) with a QCR filename.
  6. +
  7. Same editor permissions from rfq_qcr_viewers.
  8. +
  9. Save misc.qcr_sheet_id; set status = qcr_created.
  10. +
+ +

Steps 4–6 — Mail actions

+ +

Load template — getLeadMailTemplate()

+

+ Called before each mail modal opens. Query params: + lead_id + template_type (rfq | qcr | placement). + Returns subject, HTML body, and attachment checkbox HTML from lead_files. +

+ +

Send — sendMailWithAttachement()

+ +

For Non-EB (lead_form_type == 2), the Excel attachment comes from Google Sheets:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
recipient_typeSheet usedStatus after send
internalQCR if status is qcr_created/qcr_sent, else RFQUnchanged
insurermisc.rfq_sheet_idrfq_sent (unless already past that stage)
clientmisc.qcr_sheet_idqcr_sent
placementcreateAndDownloadPlacementSheet() — copies QCR sheetwon + saves placement/payment/installment fields
+ +

Implementation detail: downloadFileFromGoogleSheet() exports the chosen sheet to a temp +.xlsx under writable/tmp/ before MailHelper::send_email().

+ +

Step 7 — Placement

+ +

Opened from list action → openPlacementFromList(leadId):

+
    +
  1. GET /rfq/placementData/{id} — pre-fills policy dates, premium, CD, installments, contacts.
  2. +
  3. GET /leads/mailTemplate?template_type=placement — subject/body/attachments.
  4. +
  5. User fills placement modal: dates, premium/CD/total, installment rows, To/CC, optional external CC.
  6. +
  7. POST /leads/sendMail with recipient_type=placement.
  8. +
  9. Backend copies QCR → placement Google Sheet, attaches Excel, updates lead to won, + persists placement_date, premium_amount, cd_amount, installments, etc.
  10. +
+ +

Status lifecycle

+ + + + + + + + + + + + + + +
StatusLabelSet by
queuedIn-QueuedCreate form (default)
rfq_createdRFQ CreatedcreateRfqSheet()
rfq_sentRFQ SentInsurer mail
qcr_createdQCR CreatedcreateQcrSheet()
qcr_sentQCR SentClient mail
wonWonPlacement mail
lostLostUser sets on edit form + lost reason
+ +

leads.misc JSON

+ +
{
+  "rfq_sheet_id": "…",
+  "qcr_sheet_id": "…",
+  "placement_sheet_id": "…"
+}
+ +

Controller reference (list flow)

+ +

Methods in LeadsController.php used by the list Non-EB flow:

+ + + + + + + + + + + + + + + + + + + + +
MethodTriggered from
createLead()Form submit (create + edit)
getLeadNonEB()Add / Edit navigation
getPolicyTypeFields()Policy type change on form
createRfqSheet()RFQ action
createQcrSheet()QCR action
getLeadMailTemplate()All mail modals
getPlacementData()Placement modal pre-fill
uploadLeadAttachment()Attachment upload in mail modals
sendMailWithAttachement()All mail sends + placement
downloadFileFromGoogleSheet()Mail attachment (internal/insurer/client)
createAndDownloadPlacementSheet()Placement mail attachment
getLeadEmailHistory()Email History modal
buildRfqPlaceholderData()RFQ sheet placeholder fill (helper)
+ +

Developer checklist

+ +
    +
  1. Complete Google Sheet config for each Non-EB/Marine policy_type before first RFQ.
  2. +
  3. Set policy_type.misc.rfq_template_sheet_id per product (see sheet ID per product).
  4. +
  5. Configure RFQ_PARENT_FOLDER_ID in .env and share folder/templates with the service account.
  6. +
  7. Ensure placeholders in master templates match Config\RfqConfig::$rfqPlaceholders.
  8. +
  9. Gate new list actions with $isNonEb in leads_list.php.
  10. +
  11. Respect sheet order: RFQ → QCR → mails → placement.
  12. +
  13. QCR button: roles [1, 5, 2, 3] or BUSINESS_SUPPORT_TEAM_ID in user team.
  14. +
+ +

Common pitfalls

+ +
    +
  • QCR before RFQ: createQcrSheet fails if rfq_sheet_id is missing.
  • +
  • Queued status: Mail and placement actions are hidden until status moves past queued.
  • +
  • Insurer mail needs RFQ sheet; client mail needs QCR sheet — create sheets before sending.
  • +
  • Internal mail attachment picks RFQ or QCR based on current lead status.
  • +
  • Edit vs create: same POST /leads/create; presence of hidden id triggers update.
  • +
diff --git a/app/Views/docs/partials/docs_header.php b/app/Views/docs/partials/docs_header.php index 2b8a8e6e..949ee16e 100644 --- a/app/Views/docs/partials/docs_header.php +++ b/app/Views/docs/partials/docs_header.php @@ -39,6 +39,7 @@ $base = base_url(); /* ─── TOKENS ─────────────────────────────── */ :root { --sidebar-w : 260px; + --toc-w : 200px; --topbar-h : 56px; --bg : #ffffff; --bg2 : #f7f8fa; @@ -143,6 +144,8 @@ $base = base_url(); /* ─── LAYOUT WRAPPER ─────────────────────── */ .docs-layout { display : flex; + width : 100%; + align-items: flex-start; margin-top: var(--topbar-h); min-height: calc(100vh - var(--topbar-h)); } @@ -208,6 +211,15 @@ $base = base_url(); .callout.success { background: #f0fdf4; border-color: #4ade80; color: #15803d; } /* ─── TABLES ─────────────────────────────── */ + .docs-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 20px 0; + border: 1px solid var(--border); + border-radius: 8px; + } + .docs-table-wrap table { margin: 0; min-width: 640px; } + .docs-table-wrap--fluid table { min-width: 0; width: 100%; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13.5px; } th { background : var(--bg2); diff --git a/app/Views/docs/partials/docs_main_open.php b/app/Views/docs/partials/docs_main_open.php index 8e53fffe..b81630d2 100644 --- a/app/Views/docs/partials/docs_main_open.php +++ b/app/Views/docs/partials/docs_main_open.php @@ -27,10 +27,11 @@ $toc = $toc ?? []; B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesSIEnhanceProcess job"] + D --> E["Recalc premium plus pending SI endorsements"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|loop done| G["files.status success"] + + + +

In short:

+
    +
  • Step 1 — Format: 5 columns (A–E); Augmented SI validated against slabs (check_si).
  • +
  • Step 2 — Data: Member must exist on active policy; code 10 if not found.
  • +
  • Step 3 — SI enhance: Picks rack rate by relationship, recalculates premium, inserts 3 pending endorsements per member.
  • +
  • One Excel row = one member SI change (not whole-family in one row).
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action SI Enhancement
UploadEmployeeController::employeesUplodWithEvents
ValidationexcelFileFormatValidation, excelFileDataValidation
SI processemployeesSIEnhanceProcess
Onboard SI pathemployeesSIEnhanceProcessWhileOnbboard — SI during dependent addition onboard (not Excel)
JobemployeesSIEnhanceProcess in JobWorker.php
Helpersexcel_util_helper.phptransform_si_excel_row_to_calculatable_format, premium_calculation_manager
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=si_enhancement
  • +
  • GET /employee/excel_error/{file_id} — validation errors
  • +
+ +
+ i +
+ files.policy_id is the client policy id. Slab rates load via + getPolicySlabRatesForEmpOnboard(policy_id, client_id). +
+
+ +

Sync vs background jobs

+ + + + + + + + + + +
Step< 1 MB≥ 1 MB
Format validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
SI enhanceJob employeesSIEnhanceProcessSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $si_enhance_excel_columns5 columns (A–E).
  • +
  • Action code SI for mandatory rules.
  • +
  • Augmented SI (D): custom check_si against policy slabs (same helper as inception SI column).
  • +
  • Date of SI Enhancement (E): d-M-Y.
  • +
  • Policy must have slab/rack configuration or format step fails early (code 5).
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list
4Custom validation failed (e.g. invalid Augmented SI for slab)
5File / policy / slab problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

name_and_empid_check_in_db for si_enhancement:

+
    +
  • Row must match active employee + active employee_polices on the policy.
  • +
  • Not found → error code 10 (“Record Not found”).
  • +
+ +

On success → queues employeesSIEnhanceProcess.

+ +

Step 3: employeesSIEnhanceProcess

+ +
+
+flowchart TD + A["Read row Augmented SI and date"] --> B["Match active employee and policy"] + B --> C{"Pending SI on basic_cover_si?"} + C -->|Yes| D["Skip row log error"] + C -->|No| E["Resolve rack rate by relationship"] + E --> F["premium_calculation_manager"] + F --> G["Insert 3 pending endorsements actions si"] +
+
+ +

Per row:

+
    +
  1. Strip number formatting from Augmented SI (removeNumberFormatting).
  2. +
  3. Load employee (emp code + name + branch) and active employee_polices row.
  4. +
  5. Find applicable slab/rack rate from member relationship (Son/Daughter → childrens, parents, etc.).
  6. +
  7. Build calculatable member payload via transform_si_excel_row_to_calculatable_format (uses family max age/count).
  8. +
  9. premium_calculation_manager → new premium for the augmented SI.
  10. +
  11. Insert pending endorsements on employee_polices (actions = si):
  12. +
+ + + + + + + + + + +
field_namenew_value source
basic_cover_siExcel Augmented SI (column D)
premiumRecalculated premium from rack logic
si_enhancement_dateExcel Date of SI Enhancement (column E)
+ +

+ Sets files.status = success when the loop completes. Skipped rows (duplicate pending SI, missing member) + are logged only — the file still succeeds. +

+ +

SI Enhancement Excel columns

+ + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYes
CNAME OF EMP/DEPYesMust match DB
DAugmented SIYesNew SI; validated via check_si
EDate of SI EnhancementYesd-M-Y
+ +

Row layout examples

+ +

Enhance Self SI — one row:

+ + + + + + + + +
EMP IDNAMEAugmented SIDate of SI Enhancement
EMP001Raj Kumar100000019-May-2026
+ +

Enhance spouse only — separate row (premium uses that member’s relationship for rack selection):

+ + + + + + + + +
EMP IDNAMEAugmented SIDate of SI Enhancement
EMP001Priya Kumar50000019-May-2026
+ +

Developer steps

+ +
    +
  1. Confirm slab/rack rates exist for the client policy (same as inception).
  2. +
  3. Upload with upload-action-type=si_enhancement; note file_id.
  4. +
  5. On validation failure, check /employee/excel_error/{file_id} — codes 4 (SI/slab) or 10 (member missing).
  6. +
  7. After success, query emp_endorsement where file_id = upload id, actions = 'si', status = 'pending' — expect up to 3 rows per member (same group_key).
  8. +
  9. Compare new_value on basic_cover_si and premium to Excel and rack expectations.
  10. +
+ +

Common pitfalls

+ +
    +
  • Slab / rack not configured — format step or premium calc fails; relationship must map to a non-zero rack key.
  • +
  • Augmented SI vs slabcheck_si in step 1 must pass before the job runs.
  • +
  • Pending SI already exists — duplicate upload for same member skipped until prior endorsement cleared.
  • +
  • Per-member rows — enhancing whole family requires one row per member (unlike deletion Self = whole family).
  • +
  • File success vs rowsfiles.status = success does not guarantee every row created endorsements.
  • +
  • Not live SI updateemployee_polices.basic_cover_si changes after endorsement processing.
  • +
+ +

+ Related: + Inception, + Correction, + EB rack rate calculation. +

diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 36d930c8..d6942deb 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -163,7 +163,8 @@ -->
+ enctype="multipart/form-data" + data-parsley-excluded="input[type=button], input[type=submit], input[type=reset], input[type=hidden], [disabled], :hidden, .select2-search__field"> @@ -487,7 +488,7 @@ function restoreActualLeadGstNumber() { if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) { - $('#gst').val(actualLeadGstNumber); + $('#gst').val(String(actualLeadGstNumber).trim().toUpperCase()); } } @@ -770,18 +771,7 @@ leadTypeBsedHideAndShow(lead_type, false, policy_type_id) - if (lead_type == 1) { - $('.claim-row').hide(); - } else { - $('.claim-row').show(); - if (policy_type_id == 1) { - $('.gpaClaimFileds').show(); - $('.lifeClaimFields').hide(); - } else if (policy_type_id == 6 || policy_type_id == 7) { - $('.gpaClaimFileds').hide(); - $('.lifeClaimFields').show(); - } - } + updateClaimRowVisibility(policy_type_id); if (lead_type != 1 && policy_type_id != 1 && policy_type_id != 6 && policy_type_id != 7) { // updateRenewalFields(dataIncrement); @@ -1160,11 +1150,9 @@ leadTypeBsedHideAndShow(lead_type) - if (lead_type == 1) { - $('.claim-row').hide(); - } else if (!shouldShowClaimHistorySwitch(lead_type, policy_type_id)) { - $('.claim-row').show(); + updateClaimRowVisibility(policy_type_id); + if (!shouldShowClaimHistorySwitch(lead_type, policy_type_id)) { if (policy_type_id == 1) { $('.gpaClaimFileds').show(); $('.lifeClaimFields').hide(); @@ -1774,15 +1762,56 @@ event.preventDefault(); + $('.claim-row').each(function() { + const isNil = $(this).find('.nil-claims-checkbox').prop('checked'); + $(this).find('.nil-claims-value').val(isNil ? '1' : '0'); + }); + + if (typeof window.prepareLeadsFormForSubmit === 'function') { + window.prepareLeadsFormForSubmit(); + } + var isValid = $('#leads_form_id').parsley().validate(); if (!isValid) { - $('#leads_form_id').find('input, select, textarea').each(function() { - if ($(this).parsley().isValid() === false && !$(this).val()) { - console.log('Empty field ID:', this.id); + var invalidFields = typeof window.getLeadsFormInvalidFields === 'function' + ? window.getLeadsFormInvalidFields() + : []; + + if (invalidFields.length) { + console.table(invalidFields); + invalidFields.forEach(function(f) { + console.log('Invalid field:', f.label || f.name || f.id, f); + }); + + var firstInvalid = invalidFields[0]; + var fieldLabel = firstInvalid.label || firstInvalid.name || firstInvalid.id || 'A required field'; + var fieldMessage = (firstInvalid.messages && firstInvalid.messages.length) + ? firstInvalid.messages[0] + : (firstInvalid.isEmpty ? 'This field is required.' : 'Please check this field.'); + + if (typeof toastr !== 'undefined') { + toastr.warning(fieldLabel + ': ' + fieldMessage, 'Validation'); } - }); - console.log('Form is Empty', 'Warning'); + + if (firstInvalid.id) { + var $invalidField = $('#' + firstInvalid.id); + if ($invalidField.length) { + var scrollTarget = $invalidField.closest('.form-group, .card, .dynamic-form-row'); + if (scrollTarget.length) { + $('html, body').animate({ + scrollTop: scrollTarget.offset().top - 100 + }, 300); + } + $invalidField.focus(); + } + } + } else { + console.warn('Form validation failed'); + if (typeof toastr !== 'undefined') { + toastr.warning('Please correct the highlighted fields.', 'Validation'); + } + } return; } @@ -1803,12 +1832,11 @@ const jsonString = JSON.stringify(salse_person_id); console.log('jsonString', jsonString); - // Append the JSON string to the FormData object formData.append('salse_person_id', jsonString); - // Convert the claim experience array to JSON const finyearJsonString = gatherClaimExperienceData(policy_type_ids); console.log('finyearJsonString', finyearJsonString); formData.append('finyear', finyearJsonString); + syncClaimHistoryFieldsToFormData(formData); let claimHistoryStatus = $("#claim_history").length > 0 && $("#claim_history").prop("checked") ? 1 : 0; formData.set('claim_history', claimHistoryStatus); @@ -2065,6 +2093,25 @@ $(".claim-row").each(function() { + let isNilClaims = $(this).find(".nil-claims-checkbox").prop("checked"); + + if (isNilClaims) { + let year = $(this).find("[name='first_year[]']").val(); + claimData.push({ + "year": year, + "emp_id": "Nil", + "emp_name": "Nil", + "gender": "Nil", + "designation": "Nil", + "sum_insured": "Nil", + "death_date": "Nil", + "cause_of_death": "Nil", + "settled": "Nil", + "nil_claims": 1, + }); + return; + } + let year = $(this).find("[name='first_year[]']").val(); let claimAmount = $(this).find("[name='first_claim_amount[]']").val(); let claimStatus = $(this).find("[name='first_claim_status[]']").val(); @@ -2088,6 +2135,7 @@ "death_date": deathDate, "cause_of_death": causeOfDeath, "settled": claimAmount, + "nil_claims": 0, }); }); @@ -2196,8 +2244,37 @@ return ['1', '6', '7'].includes(String(policyTypeId)); } + function getClaimHistoryLabel(leadType) { + return String(leadType) === '1' ? 'Mortality Claims' : 'Claims History'; + } + function shouldShowClaimHistorySwitch(leadType, policyTypeId) { - return (leadType == 2 || leadType == 3) && isClaimHistoryPolicyType(policyTypeId); + return ['1', '2', '3'].includes(String(leadType)) && isClaimHistoryPolicyType(policyTypeId); + } + + function updateClaimRowVisibility(fallbackPolicyTypeId = null) { + const leadType = $('#lead_type').val(); + let policyTypeId = fallbackPolicyTypeId; + + if (policyTypeId == null || policyTypeId === '') { + const $policySelect = $('[id^="policy_type_id_"]').first(); + policyTypeId = $policySelect.length ? $policySelect.val() : ''; + } + + if (!shouldShowClaimHistorySwitch(leadType, policyTypeId)) { + $('.claim-row').hide(); + return; + } + + if ($('#claim_history').length) { + if ($('#claim_history').prop('checked')) { + $('.claim-row').show(); + } else { + $('.claim-row').hide(); + } + } else { + $('.claim-row').show(); + } } function appendThreeYearsClaims(count) { @@ -2210,20 +2287,21 @@ let lead_type = $('#lead_type').val(); let showClaimHistorySwitch = shouldShowClaimHistorySwitch(lead_type, policy_type_id); + let claimHistoryLabel = getClaimHistoryLabel(lead_type); console.log('claimIndex from parent', claimIndex); let increment = claimIndex; let claimsFields = `${showClaimHistorySwitch && !document.querySelector('#claim_history') ? `
- +

` : ``}`; claimsFields += `
-
- +
+ +
+ + +
+
+ +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
@@ -2345,6 +2432,8 @@ if (showClaimHistorySwitch) { claimHistoryToggle(); + } else { + updateClaimRowVisibility(policy_type_id); } toggleRequiredFields(); @@ -2363,15 +2452,198 @@ } } + function isClaimRowNilClaims($row) { + return $row.find('.nil-claims-checkbox').prop('checked'); + } + + function setClaimSelectValue($select, value) { + if (!$select.length) { + return; + } + + if ($select.find('option[value="' + value + '"]').length === 0) { + $select.append($('
` - let referenceDiv = document.getElementById('appendAreaForClaim'); - referenceDiv.insertAdjacentHTML('beforeend', html); - - appendThreeYearsClaims(); - } + setupNonEbClaimDetailsSection(); // Hide loader $('.loader').fadeOut(); @@ -675,6 +668,11 @@ var pageBackButton = '" class="topbar-icon const finyearJsonString = gatherClaimExperienceData(); console.log('finyearJsonString', finyearJsonString); formData.append('fin_years_claims', finyearJsonString); + syncClaimHistoryFieldsToFormData(formData); cliam_history_status = $("#claim_history").prop("checked") ? 1 : 0; @@ -799,7 +798,7 @@ var pageBackButton = '" class="topbar-icon } - leadTypeBsedHideAndShow(value, true) + leadTypeBsedHideAndShow(value, true); $('#contact_person_summary').empty(); $('#contact_person_summary').append($('

${sectionTitle}

`; + + referenceDiv.insertAdjacentHTML('beforeend', html); + appendThreeYearsClaims(); + updateNonEbClaimRowVisibility(leadType); + } + + function leadTypeBsedHideAndShow(value, resetValues = false, skipClaimSetup = false) { var actual_lead_id = $('#actual_lead_id').val(); if (value == 1 || value == 3) { $('.btnDiv').show(); - $('.claim-row').hide(); + + if (!skipClaimSetup && shouldShowNonEbClaimHistory(value)) { + setupNonEbClaimDetailsSection(value); + } else if (!shouldShowNonEbClaimHistory(value)) { + $('#appendAreaForClaim').empty(); + $('.claim-row').hide(); + } $('.emp_title_text').text('No of Employees') @@ -950,7 +1008,10 @@ var pageBackButton = '" class="topbar-icon var increment = 1; + function isClaimRowNilClaims($row) { + return $row.find('.nil-claims-checkbox').prop('checked'); + } + + function setClaimSelectValue($select, value) { + if (!$select.length) { + return; + } + + if ($select.find('option[value="' + value + '"]').length === 0) { + $select.append($(' -
- +
+
+
- + + +
+ + +
+
+ +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
@@ -1241,7 +1459,10 @@ var pageBackButton = '" class="topbar-icon let claimData = []; + if ($("#claim_history").length > 0 && !$("#claim_history").prop("checked")) { + return JSON.stringify({ + "finyear": claimData + }); + } + $(".claim-row").each(function() { + let isNilClaims = $(this).find(".nil-claims-checkbox").prop("checked"); + + if (isNilClaims) { + let year = $(this).find("[name='first_year[]']").val(); + claimData.push({ + "year": year, + "policy_type": "Nil", + "date_of_loss": "Nil", + "claim_amount": "Nil", + "settled_amount": "Nil", + "cause_of_loss": "Nil", + "status": "Nil", + "nil_claims": 1, + }); + return; + } + let year = $(this).find("[name='first_year[]']").val(); let policyType = $(this).find("[name='first_policy_type_[]']").val(); let dateOfLoss = $(this).find("[name='first_date_of_loss_[]']").val(); @@ -1292,6 +1536,7 @@ var pageBackButton = '" class="topbar-icon function claimHistoryToggle() { cliam_history_status = $("#claim_history").prop("checked") ? 1 : 0; - // alert(cliam_history_status); + if (cliam_history_status == 1) { if ($(".claim-row").length > 0) { - // Rows already exist, just show them - $(".claim-row").show(); + $(".claim-row").show(); } else { - // No rows yet, so create them appendThreeYearsClaims(); } - $(".claim-input").attr("required", true); - } else { + $(".claim-row").each(function() { + const $row = $(this); + $row.find('[name="first_year[]"]').prop("required", true); + + if (!isClaimRowNilClaims($row)) { + $row.find(".claim-input").not('[name="first_year[]"]').prop("required", true); + } + }); + } else { $(".claim-row").hide(); - $(".claim-input").removeAttr("required"); // Required Attributes Remove - $(".claim-input").val(""); // Value Reset + $(".claim-row").each(function() { + const $row = $(this); + $row.find('.nil-claims-checkbox').prop('checked', false); + $row.find('.nil-claims-value').val('0'); + $row.removeClass('claim-row-nil'); + $row.find('.claim-required-star').show(); + }); + $(".claim-input").removeAttr("required").prop('disabled', false).val(""); $(".claim-input").each(function() { - $(this).parsley().reset(); // Validation Reset + if ($(this).hasClass('select2-hidden-accessible')) { + $(this).val(null).trigger('change'); + } + if ($(this).parsley) { + $(this).parsley().reset(); + } }); } - } $('#exixting_client').change(function () { @@ -1476,5 +1736,12 @@ var pageBackButton = ' \ No newline at end of file diff --git a/app/Views/rfq/claims_details_non_eb.php b/app/Views/rfq/claims_details_non_eb.php index 8f565ee0..3141be9b 100644 --- a/app/Views/rfq/claims_details_non_eb.php +++ b/app/Views/rfq/claims_details_non_eb.php @@ -1,55 +1,85 @@ - '', 'claim_amount' => '', 'status' => '', 'claim_type' => '', 'cause_of_death' => '', 'death_date' => ''] ]; += 4) ? $year : $year - 1; + $lastFiveYears = []; + + for ($i = 0; $i < 6; $i++) { + $startYear = $currentFYStart - $i; + $lastFiveYears[] = "{$startYear}-" . ($startYear + 1); + } + } + + $claims = ! empty($lead_edit_data['fin_years_claims_array']) + ? $lead_edit_data['fin_years_claims_array'] + : [['year' => '', 'claim_amount' => '', 'status' => '', 'policy_type' => '', 'date_of_loss' => '', 'cause_of_loss' => '']]; + $leadType = (int) ($lead_edit_data['lead_type'] ?? 0); + $showClaimsSection = in_array($leadType, [1, 2, 3], true); + $claimHistoryLabel = $leadType === 1 ? 'Mortality Claims' : 'Claims History'; ?> +
- /> - + /> +

$value) { ?> + foreach ($claims as $key => $value) { + $isNilClaimRow = ! empty($value['nil_claims']); + ?> -
-
- - $year"; - } ?> + $selected = ($year == $value['year']) ? 'selected' : ''; + echo ""; + } ?>
+
- - + + +
+ > + +
-
- - + +
+ + >
-
- - +
+ + >
-
- - +
+ + >
-
- - +
+ + >
-
- - +
+ + > +
+
+ + >
@@ -59,5 +89,24 @@
- - \ No newline at end of file + + + + diff --git a/app/Views/rfq/gpa.php b/app/Views/rfq/gpa.php index 7fe37fb4..21029eaa 100644 --- a/app/Views/rfq/gpa.php +++ b/app/Views/rfq/gpa.php @@ -42,16 +42,35 @@ = 4) ? $year : $year - 1; + $lastFiveYears = []; + + for ($i = 0; $i < 6; $i++) { + $startYear = $currentFYStart - $i; + $lastFiveYears[] = "{$startYear}-" . ($startYear + 1); + } + } + + if (! isset($causeOfDeath) || ! is_array($causeOfDeath)) { + $causeOfDeath = []; + } + $savedClaims = !empty($lead_edit_data['fin_years_claims_array']) ? $lead_edit_data['fin_years_claims_array'] : []; $isClaimHistoryChecked = (isset($lead_edit_data['claim_history']) && (int) $lead_edit_data['claim_history'] === 1) || !empty($savedClaims); $showClaimHistorySwitch = isset($lead_edit_data['lead_type'], $lead_edit_data['policy_type_id']) - && in_array((int) $lead_edit_data['lead_type'], [2, 3], true) + && in_array((int) $lead_edit_data['lead_type'], [1, 2, 3], true) && in_array((int) $lead_edit_data['policy_type_id'], [1, 6, 7], true); + $claimHistoryLabel = (isset($lead_edit_data['lead_type']) && (int) $lead_edit_data['lead_type'] === 1) + ? 'Mortality Claims' + : 'Claims History'; ?>
> - +

@@ -59,13 +78,15 @@ $claims = !empty($savedClaims) ? $savedClaims : [ ['year' => '', 'claim_amount' => '', 'status' => '', 'claim_type' => '', 'cause_of_death' => '', 'death_date' => ''] ]; - foreach ($claims as $key => $value) { ?> + foreach ($claims as $key => $value) { + $isNilClaimRow = !empty($value['nil_claims']); + ?> -
+
-
- -
- - + + +
+ > + +
+
+ +
+ + >
-
- - +
+ + >
-
- - > - + + + +
-
- - +
+ + >
-
- - +
+ + >
-
- - +
+ + >
-
- - > $death_value) { $selected = ($cause == $value['cause_of_death']) ? 'selected' : ''; echo ""; } ?> + + +
-
- - +
+ + >
+