MERGE_TEST_DOCS&LIVEISSEUS

This commit is contained in:
Ubuntu 2026-05-22 15:03:40 +05:30
commit a86bbc4613
38 changed files with 5427 additions and 560 deletions

View File

@ -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)) {

View File

@ -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,

View File

@ -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'],
],

View File

@ -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 . '<br>';
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 . ' - ' . '<br>';
// 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,
];
}

View File

@ -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 = ?",

View File

@ -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,

View File

@ -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<string, mixed> $claimDetails
*
* @return array<string, mixed>
*/
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<array<string, mixed>>
*/
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<array<string, mixed>> $rows
*
* @return array<string, list<string>>
*/
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<array<string, mixed>> $rows
*
* @return array<string, list<string>>
*/
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<int, int> 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<int, int> $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'])) {

View File

@ -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');

View File

@ -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 ']);

View File

@ -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) {

View File

@ -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,

View File

@ -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([

View File

@ -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;
}
}

View File

@ -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)
{

View File

@ -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();

View File

@ -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: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
@ -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');

View File

@ -0,0 +1,292 @@
<?php
/**
* BDS commission content only
* app/Views/docs/bds-commission.php
*
* Based on:
* - app/Views/commission_file_upload.php
* - app/Views/commission_rules_list.php
* - app/Controllers/RuleImportController.php
* - app/Controllers/InsuranceCommissionController.php
*/
?>
<p>
<strong>BDS commission</strong> is a two-part flow: admins upload commission <strong>rules</strong>
(Excel) per insurer, month, and department; partner/BDS systems then call an API to
<strong>calculate payout</strong> for a policy using those rules.
</p>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li><strong>Setup:</strong> <code>RuleImportController</code> + <code>commission_file_upload.php</code> import and manage rules.</li>
<li><strong>Runtime:</strong> <code>POST getCommission</code> <code>InsuranceCommissionController::initiateCommissionCalc</code> read JSON, pick first matching rule, return payout.</li>
<li>Rules file path: <code>writable/uploads/commission/rules/{MONYYYY}/{insurer_id}_{department}.json</code> (e.g. <code>SEP2025/5_motor.json</code>).</li>
</ul>
<h2 id="key-files">Key files</h2>
<table>
<thead>
<tr><th>Part</th><th>File</th></tr>
</thead>
<tbody>
<tr><td>Upload list UI</td><td><code>app/Views/commission_file_upload.php</code></td></tr>
<tr><td>Rules editor UI</td><td><code>app/Views/commission_rules_list.php</code></td></tr>
<tr><td>Upload and editor API</td><td><code>app/Controllers/RuleImportController.php</code></td></tr>
<tr><td>Excel parsing</td><td><code>ruleImportService</code> (via <code>Config\Services::ruleImportService()</code>)</td></tr>
<tr><td>Payout calculation API</td><td><code>app/Controllers/InsuranceCommissionController.php</code></td></tr>
<tr><td>Upload metadata DB</td><td><code>commission_files</code> (<code>CommissionFilesModel</code>)</td></tr>
</tbody>
</table>
<h2 id="routes">Routes</h2>
<p><strong>Admin (commission group):</strong></p>
<table>
<thead>
<tr><th>Route</th><th>Handler</th></tr>
</thead>
<tbody>
<tr><td><code>GET/POST commission/list</code></td><td><code>commissionFileUploadList</code></td></tr>
<tr><td><code>POST commission/upload</code></td><td><code>upload</code></td></tr>
<tr><td><code>GET commission/sample_file</code></td><td>Sample CSV download</td></tr>
<tr><td><code>GET commission/downloadErrorFile</code></td><td>Annotated error Excel</td></tr>
<tr><td><code>GET commission/checkSameEntry</code></td><td>Duplicate insurer + month + department check</td></tr>
<tr><td><code>GET commission/rules/list/(:id)</code></td><td><code>ruleList</code> rules editor page</td></tr>
<tr><td><code>POST commission/rules/save/</code></td><td><code>saveRule</code></td></tr>
<tr><td><code>POST commission/rules/remove/</code></td><td><code>removeRule</code></td></tr>
<tr><td><code>GET commission/checkRuleUsage</code></td><td>Whether rule is used on partner policies</td></tr>
<tr><td><code>GET commission/deleteCommissionData/(:id)</code></td><td>Soft-delete file + mark rules deleted</td></tr>
</tbody>
</table>
<p><strong>Runtime API:</strong></p>
<pre><code class="language-text">POST getCommission
InsuranceCommissionController::initiateCommissionCalc
filter: CommissionApiFilter</code></pre>
<h2 id="commission-file-format">How to build the commission file</h2>
<p>
Use the template from the upload screen (<strong>Download sample file</strong>) or copy from
<code>public/sample_excel/sample_commission.csv</code>. Dev copies also live under
<code>writable/uploads/commission/files/</code> (e.g. <code>sample_commission.csv</code>,
<code>Sample_commission_file-New.xlsx</code>). After a successful import, see the generated JSON under
<code>writable/uploads/commission/rules/{MONYYYY}/</code> (example: <code>NOV2025/1_motor.json</code>).
</p>
<div class="callout warning">
<span>!</span>
<div>
<strong>Use the current column layout</strong>
Some older CSVs in <code>writable/uploads/commission/files/</code> use legacy headers
(<code>Rule Name</code>, <code>Commission Params(TP:OD:PA)</code>). The importer expects the
<strong>S.No</strong> layout below (<code>RuleImportService</code>). Wrong headers fail with
“Missing required columns”.
</div>
</div>
<h3 id="required-columns">Required columns (row 1 headers)</h3>
<p>Header text must match <strong>exactly</strong> (one row per rule, starting row 2). Empty cells are allowed and simply skip that condition.</p>
<table>
<thead>
<tr><th>Column</th><th>Purpose</th><th>Example</th></tr>
</thead>
<tbody>
<tr><td>S.No</td><td>Serial (not used in logic)</td><td>1</td></tr>
<tr><td>Premium Type</td><td>Maps to <code>policy_type</code></td><td>OD, TP, COM</td></tr>
<tr><td>Vehicle Type</td><td><code>vehicle_type</code>; comma = multiple (IN)</td><td>Two Wheeler, Four Wheeler</td></tr>
<tr><td>Vehicle Sub Type</td><td><code>vehicle_sub_type</code></td><td>Car, PCV</td></tr>
<tr><td>Make / Model</td><td>Vehicle make and model</td><td>Honda, i10</td></tr>
<tr><td>CC Min / CC Max</td><td>Cubic capacity range</td><td>1000, 3000</td></tr>
<tr><td>Fuel Type</td><td>Comma-separated fuels</td><td>Petrol, Diesel</td></tr>
<tr><td>Vehicle Age Min / Max</td><td>Vehicle age range (years)</td><td>1, 5</td></tr>
<tr><td>Vehicle Weight Min / Max</td><td>Weight range (kg)</td><td>2000, 3000</td></tr>
<tr><td>RTO State / RTO Code</td><td><code>geo_rto_state</code>, <code>geo_rto_city</code></td><td>RJ, 41</td></tr>
<tr><td>Renewal Type</td><td><code>renewal_type</code></td><td>Online, Cash, Card</td></tr>
<tr><td>Commission Type</td><td><code>percentage</code>, <code>composite</code>, <code>flat</code>, <code>tiered</code></td><td>percentage</td></tr>
<tr><td>Commission Value</td><td>% or flat amount (required for percentage / flat)</td><td>15 or 500</td></tr>
<tr><td>Commission Params (TP)</td><td>Composite: % on TP premium</td><td>18</td></tr>
<tr><td>Commission Params (OD)</td><td>Composite: % on OD premium</td><td>10</td></tr>
<tr><td>Commission Params (PA)</td><td>Composite: % on PA premium</td><td>0 or empty</td></tr>
</tbody>
</table>
<h3 id="commission-types">Commission types (what to fill)</h3>
<table>
<thead>
<tr><th>Type</th><th>Fill in sheet</th><th>Becomes in JSON</th></tr>
</thead>
<tbody>
<tr>
<td><code>percentage</code></td>
<td>Commission Value = e.g. <code>10</code></td>
<td>10% of <code>premium</code></td>
</tr>
<tr>
<td><code>composite</code></td>
<td>Leave Value empty; set TP / OD / PA param columns (percent each)</td>
<td>Split % on <code>tp_premium</code>, <code>od_premium</code>, <code>pa_premium</code></td>
</tr>
<tr>
<td><code>flat</code></td>
<td>Commission Value = fixed rupee amount e.g. <code>500</code></td>
<td>Fixed payout on <code>premium</code></td>
</tr>
</tbody>
</table>
<h3 id="commission-file-samples">Sample rows (from project files)</h3>
<p><strong>Template header + rows</strong> (<code>public/sample_excel/sample_commission.csv</code>):</p>
<pre><code class="language-text">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,</code></pre>
<p><strong>Example A composite two-wheeler</strong> (from <code>writable/.../rules/NOV2025/1_motor.json</code>):</p>
<ul>
<li>Vehicle Type = <code>Two Wheeler</code>, CC Min/Max = <code>100</code></li>
<li>Commission Type = <code>composite</code>, TP = <code>10</code>, OD = <code>25</code></li>
<li>Result: 10% on TP premium + 25% on OD premium when policy matches</li>
</ul>
<p><strong>Example B percentage four-wheeler</strong>:</p>
<ul>
<li>Vehicle Type = <code>Four Wheeler</code>, CC = <code>1000</code>, Vehicle Age Min/Max = <code>5</code></li>
<li>Commission Type = <code>percentage</code>, Commission Value = <code>10</code></li>
<li>Result: 10% of total premium</li>
</ul>
<p><strong>Example C composite TP-only</strong>:</p>
<ul>
<li>Premium Type = <code>TP</code>, Vehicle Type = <code>Four Wheeler</code></li>
<li>Commission Type = <code>composite</code>, Commission Params (TP) = <code>10</code></li>
<li>Result: 10% on TP premium only</li>
</ul>
<p><strong>Example D flat amount</strong>:</p>
<ul>
<li>Vehicle Type = <code>Two Wheeler,Four Wheeler</code> (comma matches either)</li>
<li>Commission Type = <code>flat</code>, Commission Value = <code>500</code></li>
<li>Result: fixed ₹500 when matched</li>
</ul>
<p><strong>Tips:</strong></p>
<ul>
<li>Min must not be greater than Max (CC, age, weight) or the row fails validation.</li>
<li>On failure, download the <strong>annotated</strong> file errors are written into the sheet.</li>
<li>Supported formats: <code>.csv</code>, <code>.xlsx</code>, <code>.xls</code>, <code>.ods</code>.</li>
<li>Upload UI departments: <code>motor</code>, <code>health</code>; import logic is built for <strong>motor</strong> columns today.</li>
</ul>
<h2 id="upload-flow">Rule upload</h2>
<p>From <code>commission_file_upload.php</code> the user picks insurer, commission month, department (motor / health), and an Excel/CSV file.</p>
<ol>
<li><code>checkSameEntry</code> if a successful upload already exists for the same trio, SweetAlert offers <strong>Overwrite</strong> or <strong>Append</strong> (<code>overwrite=1</code> or <code>0</code> on POST).</li>
<li><code>upload</code> stores file under <code>writable/uploads/commission/files/</code>, inserts <code>commission_files</code> row (<code>pending</code>).</li>
<li><code>ruleImportService->processUpload()</code> validates Excel rows; on success returns <code>rules</code> array; on failure returns <code>annotated_file</code> for download.</li>
<li>On success writes JSON to <code>rules/{MONYYYY}/{insurer_id}_{department}.json</code>; sets <code>file_status=success</code> and <code>rules_count</code>.</li>
<li>On failure <code>file_status=failed</code>; user downloads <code>annotated_{filename}</code> via <code>downloadErrorFile</code>.</li>
</ol>
<h2 id="rules-editor">Rules editor</h2>
<p>
For successful uploads, action <strong>View Rules</strong> opens
<code>commission/rules/list/{file_id}</code> (<code>commission_rules_list.php</code>).
</p>
<ul>
<li>Lists rules from the JSON file for that uploads insurer, month, and department.</li>
<li><code>saveRule</code> create or update a rule (conditions + calculation + name) in the JSON via <code>updateCommissionRules()</code>.</li>
<li><code>removeRule</code> soft-delete one rule (<code>is_deleted=true</code>).</li>
<li><code>checkRuleUsage</code> warns if <code>partner_policy.commission_applied_rule</code> references the rule.</li>
<li>Deleting the whole upload marks all rules with that <code>file_id</code> as deleted in JSON, then sets <code>commission_files.is_active=0</code>.</li>
</ul>
<h2 id="rule-json">Rule JSON shape</h2>
<p>Each rule is roughly:</p>
<pre><code class="language-json">{
"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"
}
}</code></pre>
<p>Calculation types in <code>InsuranceCommissionController</code>: <code>percentage</code>, <code>composite</code>, <code>fixed</code>. Conditions support <code>==</code>, <code>!=</code>, <code>&gt;</code>, <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</code>, <code>between</code>, <code>in</code>.</p>
<h2 id="calculation-api">Commission calculation API</h2>
<p><code>initiateCommissionCalc()</code> expects POST/JSON including at least:</p>
<ul>
<li><code>policy_issue_date</code> used to pick folder <code>{MON}{YEAR}</code> (e.g. SEP2025)</li>
<li><code>insurer_id</code></li>
<li><code>department</code> motor, health, etc.</li>
<li>Fields referenced in rule conditions and calculation bases (e.g. <code>premium</code>, <code>od_premium</code>)</li>
</ul>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p>Rules with <code>is_deleted: false</code> are loaded; the <strong>first</strong> matching rule wins (no priority field yet).</p>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Open <code>/commission/list</code> (logged-in admin).</li>
<li>Download sample file, fill rules for insurer + month + department, upload.</li>
<li>If validation fails, download the annotated error file and fix the sheet.</li>
<li>Use <strong>View Rules</strong> to tweak conditions or calculation without re-uploading the whole file.</li>
<li>Test payout: <code>POST getCommission</code> with the same insurer, department, and a <code>policy_issue_date</code> in that commission month.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Month folder must match policy date</strong> upload uses commission month; API uses <code>policy_issue_date</code> to resolve the same <code>MONYYYY</code> folder.</li>
<li><strong>Append vs overwrite</strong> append merges JSON arrays; overwrite backs up the old file then replaces.</li>
<li><strong>Departments</strong> upload UI currently offers motor and health; API department string must match the JSON filename slug.</li>
<li><strong>HTTP 200 on upload errors</strong> check <code>status</code> and <code>code</code> in the JSON body, not only HTTP status.</li>
</ul>

View File

@ -0,0 +1,309 @@
<?php
/**
* BDS Insurer statement content only
* app/Views/docs/bds-insurer-statement.php
*
* Based on:
* - app/Views/insurer_statement_list.php
* - app/Controllers/PolicyTransactionController.php
* (statementList, uploadInsurerStatement, validateInsurerStatement, updateInsurerStatement)
*/
?>
<p>
<strong>BDS Insurer statement</strong> lets finance users upload an insurer-provided Excel statement,
validate each row against NHance policy transactions (<code>pt_co_share_details</code> +
<code>policy_transaction</code>), and persist matched brokerage amounts into
<code>co_share_stmt_details</code>. The admin UI is
<code>app/Views/insurer_statement_list.php</code>; all server logic lives in
<code>PolicyTransactionController</code> under the <code>policy_tranction/statement</code> route group.
</p>
<div class="callout info">
<span>i</span>
<div>
<strong>Auth</strong>
Statement routes use the <code>authMVC</code> filter. Upload and list are browser AJAX/form calls from an authenticated session, not public API endpoints.
</div>
</div>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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]
</div>
</div>
<h2 id="row-validation-flowchart">Row validation logic</h2>
<p>
<code>validateInsurerStatement()</code> checks that every Excel line maps to a real NHance transaction
for the selected insurer branch. Each row is matched on <strong>policy number</strong> (column B) and
<strong>endorsement number</strong> (column C).
</p>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li>Empty rows are ignored.</li>
<li>A row is <strong>valid</strong> only if that policy + endorsement exists in NHance and appears once in the upload.</li>
<li>If anything fails, the whole file is marked <code>failed</code> and errors are shown per row in the UI.</li>
</ul>
<h2 id="key-files-routes">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>Location</th></tr>
</thead>
<tbody>
<tr>
<td>UI</td>
<td><code>app/Views/insurer_statement_list.php</code> DataTable list, upload modal, validation error modal, invoice modal</td>
</tr>
<tr>
<td>Controller</td>
<td><code>app/Controllers/PolicyTransactionController.php</code></td>
</tr>
<tr>
<td>Statement header model</td>
<td><code>app/Models/InsurerStatements.php</code> <code>insurer_statements</code></td>
</tr>
<tr>
<td>Line-item model</td>
<td><code>app/Models/COShareStmtDetailsModel.php</code> <code>co_share_stmt_details</code></td>
</tr>
<tr>
<td>NHance source rows</td>
<td><code>app/Models/PTCOShareDetailsModel.php</code> <code>getNonReconcileredPolicyTransactionByPolicyAndEndorsement()</code></td>
</tr>
<tr>
<td>Sample Excel</td>
<td><code>public/sample_excel/insurer_stament_sample.xlsx</code></td>
</tr>
</tbody>
</table>
<p>Routes (prefix <code>policy_tranction/statement</code>, filter <code>authMVC</code>):</p>
<table>
<thead>
<tr><th>Method</th><th>Route</th><th>Handler</th></tr>
</thead>
<tbody>
<tr><td>GET</td><td><code>list</code></td><td><code>statementList</code></td></tr>
<tr><td>POST</td><td><code>upload</code></td><td><code>uploadInsurerStatement</code></td></tr>
<tr><td>GET</td><td><code>downloadSampleInsurerStatement</code></td><td>Sample file download</td></tr>
<tr><td>GET</td><td><code>downloadInsurerStatement/(:num)</code></td><td>Uploaded file download</td></tr>
<tr><td>GET</td><td><code>getFileErr/(:any)</code></td><td>Validation failure JSON for modal</td></tr>
<tr><td>GET</td><td><code>getInsurerStatementMonth</code></td><td>Used to disable already-used statement numbers</td></tr>
<tr><td>GET</td><td><code>deleteStatement/(:any)</code></td><td>Soft-delete statement + related rows</td></tr>
<tr><td>GET</td><td><code>getPaymentDetails/(:any)</code></td><td>Invoice modal data</td></tr>
<tr><td>POST</td><td><code>saveInvoicePaymentDetails</code></td><td>Invoice / payment save</td></tr>
</tbody>
</table>
<pre><code class="language-php">$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');
// ...
});
});</code></pre>
<h2 id="statement-list-ui">Statement list UI</h2>
<p>
<code>statementList()</code> loads insurers/branches via
<code>insurerBranchModel::getInsurerBranchesWithInsurerNames()</code>, invoice status labels, and
statements from the last <strong>180 days</strong> (<code>is_active = 1</code>). The view shows:
</p>
<ul>
<li>Insurer branch, statement month, statement serial no, filename (download link), line items count</li>
<li><code>file_status</code> <code>success</code> or <code>failed</code> (failed rows show an alert icon error modal)</li>
<li><code>invoice_status</code> pending / generated / sent / payment received</li>
<li>Actions (non-failed only): invoice status update, delete</li>
</ul>
<p>
Upload form fields (<code>#insurer_statement_upload_form</code>): insurer
(<code>insurer_id-branch_id</code>), statement month (flatpickr), statement no (17), Excel file.
Submit is AJAX POST to relative <code>upload</code>. On success the page reloads; on validation failure
the API still returns HTTP 200 with <code>dataStatus: false</code> and <code>error_data</code>.
</p>
<h2 id="upload-flow">uploadInsurerStatement()</h2>
<ol>
<li>Validates uploaded file: Excel MIME types, max 16 MB (<code>max_size[statement,16384]</code> KB).</li>
<li>Moves file to <code>WRITEPATH . 'uploads/statements/'</code> (see <code>createStatementFolder()</code> for folder creation).</li>
<li>Parses POST: <code>insurer</code> as <code>{insurer_id}-{branch_id}</code>, <code>statement_month</code> (converted to first-of-month <code>Y-m-d</code>), <code>statement_no</code> <code>stmt_sno</code>.</li>
<li>Inserts <code>insurer_statements</code> row via <code>InsurerStatements</code> model.</li>
<li>Calls <code>validateInsurerStatement(['file_id' => $file_id])</code>.</li>
<li>If validation passes, calls <code>updateInsurerStatement(['file_id' => $file_id])</code>.</li>
<li>On validation failure: responds with <code>dataStatus: false</code>, <code>error_data</code>, <code>error_code</code> (HTTP 200).</li>
<li>On success: sets <code>invoice_status = 'pending'</code> and returns <code>dataStatus: true</code>.</li>
</ol>
<h2 id="validate-flow">validateInsurerStatement($params)</h2>
<p>Runs immediately after upload (and can be re-run manually in dev with a hard-coded <code>file_id</code> in <code>statementList()</code> comment).</p>
<h3 id="validate-steps">Steps</h3>
<ol>
<li>Load <code>insurer_statements</code> by <code>file_id</code>; fail if missing or physical file absent under <code>writable/uploads/statements/</code>.</li>
<li>Load active sheet via PhpSpreadsheet; drop header row; sanitize with <code>ExcelSanitizeHelper::sanitizeArrayData()</code>.</li>
<li>Collect unique policy numbers from column <strong>B</strong> (index <code>1</code>), skipping empty rows via <code>check_row_is_empty_or_null()</code>.</li>
<li>Fetch NHance candidates:
<code>PTCOShareDetailsModel::getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id, branch_id, policy_no[])</code>
matches <code>pt.policy_no</code> for completed transactions on that insurer branch.</li>
<li>Build lookup keys <code>policy_no|endorsement_no</code> (sanitized) for source and Excel rows.</li>
<li>For each non-empty Excel row, match policy + endorsement; track duplicates in <code>$matched_entry</code>.</li>
<li>On mismatch, append row-wise HTML messages under <code>error_data[row_index]</code>.</li>
<li>Update <code>insurer_statements.line_items</code>, <code>file_status</code> (<code>success</code> / <code>failed</code>), <code>reason</code> (JSON).</li>
</ol>
<h3 id="sanitize">sanitizeStatementLookupValue()</h3>
<p>Private helper trims Unicode spaces and strips zero-width / BOM characters from policy and endorsement values before comparison avoids “looks equal” mismatches in Excel.</p>
<h3 id="validation-errors">Validation error codes</h3>
<table>
<thead>
<tr><th>error_code</th><th>Meaning</th><th>UI</th></tr>
</thead>
<tbody>
<tr><td><code>0</code></td><td>DB row missing or physical file not found</td><td>Plain message in modal</td></tr>
<tr><td><code>1</code></td><td>Legacy: list of row numbers (old format)</td><td>Comma-separated row list</td></tr>
<tr><td><code>2</code></td><td>Row-wise validation (current)</td><td>Modal lists each row with policy / endorsement / duplicate messages</td></tr>
</tbody>
</table>
<p>Row keys in <code>error_data</code> are the <strong>array index</strong> from the Excel loop (first data row is typically <code>1</code> after header removal), not necessarily the Excel row number on sheet.</p>
<h2 id="update-flow">updateInsurerStatement($params)</h2>
<p>Runs only when validation returned <code>status: true</code>.</p>
<ol>
<li>Same file load / sanitize path as validation.</li>
<li>Rebuild policy list and source lookup (policy + endorsement array of <code>pt_co_share_details</code> rows).</li>
<li>For each Excel row with a matching source entry, compute brokerage totals and variance:
<ul>
<li>BP: amount col <code>3</code>, brokerage col <code>5</code></li>
<li>TP: amount col <code>4</code>, brokerage col <code>6</code></li>
<li>TEP: forced to <code>0</code> in current code</li>
<li><code>reward</code> from col <code>7</code></li>
<li><code>variance = exp_amt - total_amt</code> (from source <code>exp_amt</code>)</li>
</ul>
</li>
<li><code>COShareStmtDetailsModel::insertBatch($data_to_update)</code> one row per matched Excel line.</li>
<li>Sets <code>file_status = success</code>, <code>invoice_status = pending</code>, clears/sets <code>reason</code>.</li>
</ol>
<h2 id="excel-columns">Excel column mapping (0-based index)</h2>
<p>Header row is removed; data columns used by validation/update:</p>
<table>
<thead>
<tr><th>Index</th><th>Column</th><th>Use</th></tr>
</thead>
<tbody>
<tr><td><code>1</code></td><td>B</td><td>Policy number (required for matching)</td></tr>
<tr><td><code>2</code></td><td>C</td><td>Endorsement number</td></tr>
<tr><td><code>3</code></td><td>D</td><td>Actual BP amount</td></tr>
<tr><td><code>4</code></td><td>E</td><td>Actual TP amount</td></tr>
<tr><td><code>5</code></td><td>F</td><td>Actual BP brokerage</td></tr>
<tr><td><code>6</code></td><td>G</td><td>Actual TP brokerage</td></tr>
<tr><td><code>7</code></td><td>H</td><td>Reward</td></tr>
</tbody>
</table>
<p>Download the canonical layout from the list page link <code>downloadSampleInsurerStatement</code>.</p>
<h2 id="nhance-source-query">NHance source query</h2>
<p><code>getNonReconcileredPolicyTransactionByPolicyAndEndorsement()</code> joins:</p>
<ul>
<li><code>pt_co_share_details</code> (active) <code>policy_transaction</code> (active, <code>status = completed</code>)</li>
<li>Filtered by <code>insurer_id</code>, <code>insurer_branch_id</code>, and <code>pt.policy_no IN (...)</code></li>
</ul>
<p>
Matching is on sanitized <code>policy_no</code> + <code>endorsement_no</code>. The method name suggests
“non-reconciled” but the current query does <strong>not</strong> filter <code>statement_id IS NULL</code>;
be aware when re-uploading or debugging duplicate reconciliation.
</p>
<h2 id="invoice-and-delete">Invoice status and delete</h2>
<p>After a successful upload, users manage invoice lifecycle from the list (separate from upload/validate):</p>
<ul>
<li><code>invoice_status</code>: <code>pending</code>, <code>generated</code>, <code>sent</code>, <code>payment_received</code></li>
<li><code>saveInvoicePaymentDetails</code> JSON POST from invoice modal</li>
<li><code>deleteStatement($id)</code> soft-deletes <code>co_share_stmt_details</code>, <code>inv_payment_details</code>, and <code>insurer_statements</code> for that id</li>
</ul>
<p>
<code>getInsurerStatementMonth</code> returns successful statements for insurer+month so the UI can
disable statement numbers already used (<code>disableStatementNo()</code> in the view).
</p>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Ensure <code>writable/uploads/statements/</code> exists and is writable (or call <code>createStatementFolder()</code> once).</li>
<li>Open <code>policy_tranction/statement/list</code> in a logged-in session.</li>
<li>Use sample Excel; pick insurer branch and month; choose an unused statement number (17 per insurer/month).</li>
<li>Confirm policy/endorsement exist on a <strong>completed</strong> BDS transaction for that insurer branch.</li>
<li>On failure, open the alert icon modal calls <code>getFileErr/{id}</code> and renders <code>reason</code> JSON.</li>
<li>To debug validation only: temporarily uncomment the <code>validateInsurerStatement</code> / <code>updateInsurerStatement</code> one-liner in <code>statementList()</code> with a known <code>file_id</code>.</li>
</ol>
<h2 id="common-pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Hidden Excel characters</strong> policy/endorsement must pass <code>sanitizeStatementLookupValue()</code>; re-type values if NHance shows a match but upload fails.</li>
<li><strong>Duplicate policy + endorsement</strong> in the same file <code>error_code 2</code>, duplicate message on second row.</li>
<li><strong>Validation failed but file on disk</strong> <code>insurer_statements</code> row remains; user sees <code>failed</code> status; re-upload needs a new statement or delete the failed row.</li>
<li><strong>Statement number reuse</strong> only successful uploads for that insurer/month block numbers in the dropdown via <code>getInsurerStatementMonth</code>.</li>
<li><strong>Upload response HTTP 200 on error</strong> front-end checks <code>dataStatus</code>, not status code alone.</li>
<li><strong>Legacy handlers</strong> <code>validateInsurerStatementOld</code>, <code>updateInsurerStatementOLD</code> remain in the controller; production path is the non-<code>Old</code> methods documented here.</li>
</ul>
<h2 id="related-bds">Related BDS features</h2>
<ul>
<li>BDS reports: <code>policy_tranction/report/list</code>, variance, finance, outstanding lists</li>
<li>Daily BDS cron mail: <code>cronDailyBDSReport</code> (separate from statement upload)</li>
</ul>

View File

@ -0,0 +1,265 @@
<?php
/**
* Correction (employee Excel upload) content only
* app/Views/docs/correction.php
*
* Based on:
* - app/Views/employee_upload.php
* - app/Controllers/EmployeeServiceController.php
* (excelFileFormatValidation, excelFileDataValidation, employeesCorrectionProcess)
* - app/Controllers/EmployeeController.php (employeesUplodWithEvents)
* - app/Controllers/JobWorker.php
*/
?>
<p>
<strong>Correction</strong> updates existing member data on an active policy via Excel upload
(<code>files.action = correction</code>). The final step creates <strong>pending correction endorsements</strong>
on the <code>employees</code> table data is not updated until those endorsements are applied downstream.
</p>
<p>
Processing is in <code>EmployeeServiceController::employeesCorrectionProcess</code>, queued after the same
format and data validation steps used for inception and deletion.
</p>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li><strong>Step 1 Format:</strong> 8 columns (AH); field must be one of four allowed names; dates <code>d-M-Y</code>.</li>
<li><strong>Step 2 Data:</strong> Member must exist (emp code + name + active policy); code <strong>10</strong> if not found.</li>
<li><strong>Step 3 Correction:</strong> One pending endorsement per row per field (skips duplicate pending corrections).</li>
<li>Each Excel row = one field change for one member (not a full-family operation).</li>
</ul>
<h2 id="key-files-routes">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>Location</th></tr>
</thead>
<tbody>
<tr>
<td>UI</td>
<td><code>app/Views/employee_upload.php</code> action <strong>Correction</strong></td>
</tr>
<tr>
<td>Upload</td>
<td><code>EmployeeController::employeesUplodWithEvents</code></td>
</tr>
<tr>
<td>Validation</td>
<td><code>excelFileFormatValidation</code>, <code>excelFileDataValidation</code></td>
</tr>
<tr>
<td>Correction process</td>
<td><code>EmployeeServiceController::employeesCorrectionProcess</code></td>
</tr>
<tr>
<td>Column config API</td>
<td><code>getCorrectionExcelColumns()</code> used when building correction Excel programmatically</td>
</tr>
<tr>
<td>Job</td>
<td><code>employeesCorrectionProcess</code> in <code>JobWorker.php</code></td>
</tr>
<tr>
<td>Endorsements</td>
<td><code>EmpEndorsementModel</code> <code>actions = c</code>, <code>table_name = employees</code>, <code>status = pending</code></td>
</tr>
</tbody>
</table>
<p><strong>Routes</strong> (group <code>/employee</code>, <code>authMVC</code>):</p>
<ul>
<li><code>GET /employee/upload</code> upload screen</li>
<li><code>POST /employee/upload</code> <code>upload-action-type=correction</code></li>
<li><code>GET /employee/excel_error/{file_id}</code> validation errors</li>
</ul>
<div class="callout info">
<span>i</span>
<div>
<strong><code>files.policy_id</code></strong> is the client policy id. Lookup requires active
<code>employees</code> + <code>employee_polices</code> on that policy and branch.
</div>
</div>
<h2 id="sync-vs-jobs">Sync vs background jobs</h2>
<table>
<thead>
<tr><th>Step</th><th>&lt; 1 MB</th><th> 1 MB</th></tr>
</thead>
<tbody>
<tr><td>Format validation</td><td>Inline on upload</td><td>Job <code>excelFileFormatValidation</code></td></tr>
<tr><td>Data validation</td><td>Job <code>excelFileDataValidation</code></td><td>Same</td></tr>
<tr><td>Correction process</td><td>Job <code>employeesCorrectionProcess</code></td><td>Same</td></tr>
</tbody>
</table>
<h2 id="format-validation">Step 1: excelFileFormatValidation</h2>
<ul>
<li>Uses <code>$correction_excel_columns</code> <strong>8 columns (AH)</strong>.</li>
<li>Action code <code>C</code> for mandatory rules on correction-specific columns.</li>
<li><strong>Field</strong> (column D): only <code>name</code>, <code>dob</code>, <code>relationship</code>, <code>email_corporate</code>.</li>
<li><strong>Date of Correction</strong> (F): <code>d-M-Y</code>.</li>
<li><strong>Change event</strong> (G) and <strong>Value</strong> (E) are mandatory.</li>
</ul>
<h3 id="format-errors">Format error codes</h3>
<table>
<thead>
<tr><th>Code</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Mandatory missing</td></tr>
<tr><td>2</td><td>Wrong format</td></tr>
<tr><td>3</td><td>Not in allowed list (e.g. invalid Field value)</td></tr>
<tr><td>4</td><td>Custom validation failed</td></tr>
<tr><td>5</td><td>File / policy problem</td></tr>
<tr><td>6</td><td>Column headers wrong</td></tr>
</tbody>
</table>
<h2 id="data-validation">Step 2: excelFileDataValidation</h2>
<p>Rows grouped by EMP ID; for correction the critical check is <code>name_and_empid_check_in_db</code>:</p>
<ul>
<li>Each row must match an active employee on the uploaded policy (emp code + name).</li>
<li>Not found error code <strong>10</strong> (“Record Not found”).</li>
</ul>
<p>On success queues <code>employeesCorrectionProcess</code> (with optional <code>batch_file_id</code> for TPA multi-file flows).</p>
<h2 id="correction-process">Step 3: employeesCorrectionProcess</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>Per row:</strong></p>
<ul>
<li><code>field_name</code> column D (<code>name</code>, <code>dob</code>, <code>relationship</code>, <code>email_corporate</code>)</li>
<li><code>new_value</code> column E; if field is <code>dob</code>, converted from <code>d-M-Y</code> to <code>Y-m-d</code></li>
<li><code>date_of_correction</code> column F (converted to <code>Y-m-d</code>)</li>
<li><code>remarks</code> column H (optional)</li>
<li><code>old_value</code> current value from <code>employees.{field_name}</code></li>
</ul>
<p>
Skips insert when a pending correction endorsement already exists for the same
<code>emp_code</code>, <code>name</code>, and <code>field_name</code>
(<code>actions = c</code>, <code>endorsement_id IS NULL</code>, <code>status != truncated</code>).
</p>
<p>
Sets <code>files.status = success</code> 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).
</p>
<p>
<strong>TPA batch:</strong> If <code>batch_file_id</code> is set, also queues
<code>updateEmployeeDataFromTpa</code>, <code>reconTpaApiDataWithEmployeepolicies</code>, and
<code>initializeDeletionProcessForTpaApiData</code>.
</p>
<h2 id="excel-columns">Correction Excel columns</h2>
<table>
<thead>
<tr><th>Col</th><th>Header</th><th>Required</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>S.No</td><td>Yes</td><td></td></tr>
<tr><td>B</td><td>EMP ID</td><td>Yes</td><td></td></tr>
<tr><td>C</td><td>NAME OF EMP/DEP</td><td>Yes</td><td>Must match DB before correction</td></tr>
<tr><td>D</td><td>Field</td><td>Yes</td><td><code>name</code>, <code>dob</code>, <code>relationship</code>, <code>email_corporate</code></td></tr>
<tr><td>E</td><td>Value</td><td>Yes</td><td>New value; DOB as <code>d-M-Y</code></td></tr>
<tr><td>F</td><td>Date of Correction</td><td>Yes</td><td><code>d-M-Y</code></td></tr>
<tr><td>G</td><td>Change event</td><td>Yes</td><td>e.g. <code>correction</code></td></tr>
<tr><td>H</td><td>Remarks</td><td>No</td><td>Stored on endorsement</td></tr>
</tbody>
</table>
<h2 id="row-example">Row layout examples</h2>
<p><strong>Fix DOB</strong> one row, one field:</p>
<table>
<thead>
<tr><th>EMP ID</th><th>NAME</th><th>Field</th><th>Value</th><th>Date of Correction</th><th>Change event</th><th>Remarks</th></tr>
</thead>
<tbody>
<tr>
<td>EMP001</td>
<td>Raj Kumar</td>
<td>dob</td>
<td>15-Jan-1985</td>
<td>19-May-2026</td>
<td>correction</td>
<td>Typo in upload</td>
</tr>
</tbody>
</table>
<p><strong>Multiple fixes</strong> use separate rows (same or different members):</p>
<table>
<thead>
<tr><th>EMP ID</th><th>NAME</th><th>Field</th><th>Value</th></tr>
</thead>
<tbody>
<tr><td>EMP001</td><td>Raj Kumar</td><td>email_corporate</td><td>raj.kumar@company.com</td></tr>
<tr><td>EMP001</td><td>Priya Kumar</td><td>relationship</td><td>Spouse</td></tr>
</tbody>
</table>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Upload with <code>upload-action-type=correction</code>; note <code>file_id</code>.</li>
<li>On validation failure, check <code>/employee/excel_error/{file_id}</code> for code <strong>10</strong>.</li>
<li>After success, query <code>emp_endorsement</code> where <code>file_id</code> = upload id, <code>actions = 'c'</code>, <code>status = 'pending'</code>.</li>
<li>Compare <code>field_name</code>, <code>old_value</code>, <code>new_value</code> per row to the Excel.</li>
<li>To generate correction Excel in code, use <code>getCorrectionExcelColumns()</code> for header layout.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Name must match DB</strong> correction identifies the member by current <code>emp_code</code> + <code>name</code>; rename via a <code>name</code> field row uses the old name in column C.</li>
<li><strong>Only four fields</strong> mobile, SI, band, etc. are not supported in this upload path.</li>
<li><strong>Duplicate pending correction</strong> second upload for the same field is skipped until the first endorsement is processed or truncated.</li>
<li><strong>File success vs rows</strong> <code>files.status = success</code> does not mean every row created an endorsement.</li>
<li><strong>Not live update</strong> <code>employees</code> columns change only after endorsement approval/application.</li>
</ul>
<p>
Related:
<a href="<?= base_url('docs/inception') ?>">Inception</a>,
<a href="<?= base_url('docs/deletion') ?>">Deletion</a>.
</p>

259
app/Views/docs/deletion.php Normal file
View File

@ -0,0 +1,259 @@
<?php
/**
* Deletion (employee Excel upload) content only
* app/Views/docs/deletion.php
*
* Based on:
* - app/Views/employee_upload.php
* - app/Controllers/EmployeeServiceController.php
* (excelFileFormatValidation, excelFileDataValidation, employeeDisembark)
* - app/Controllers/EmployeeController.php
* (employeesUplodWithEvents, initializeDeletionProcessForTpaApiData)
* - app/Controllers/JobWorker.php
*/
?>
<p>
<strong>Deletion</strong> removes active members from a client policy via Excel upload
(<code>files.action = deletion</code>). Unlike inception, the final step does not delete rows immediately
it creates <strong>pending endorsement</strong> records on <code>employee_polices</code> for approval/processing later.
</p>
<p>
Core processing lives in <code>EmployeeServiceController::employeeDisembark</code>.
<code>EmployeeController::initializeDeletionProcessForTpaApiData</code> is a separate TPA-reconcile path that
builds a deletion Excel file and calls the same disembark function.
</p>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li><strong>Step 1 Format:</strong> 7 columns (AG), mandatory exit fields per action code <code>D</code>.</li>
<li><strong>Step 2 Data:</strong> Member must exist in DB (emp code + name + active policy); code 10 if not found.</li>
<li><strong>Step 3 Disembark:</strong> Writes pending deletion endorsements; Self row removes whole family, dependent row removes one member.</li>
<li>Files &lt; 1 MB run format validation inline; data validation and disembark are always queued.</li>
</ul>
<h2 id="key-files-routes">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>Location</th></tr>
</thead>
<tbody>
<tr>
<td>UI</td>
<td><code>app/Views/employee_upload.php</code> action <strong>Deletion</strong></td>
</tr>
<tr>
<td>Upload</td>
<td><code>EmployeeController::employeesUplodWithEvents</code></td>
</tr>
<tr>
<td>Validation</td>
<td><code>EmployeeServiceController::excelFileFormatValidation</code>, <code>excelFileDataValidation</code></td>
</tr>
<tr>
<td>Deletion process</td>
<td><code>EmployeeServiceController::employeeDisembark</code></td>
</tr>
<tr>
<td>TPA auto-deletion</td>
<td><code>EmployeeController::initializeDeletionProcessForTpaApiData</code></td>
</tr>
<tr>
<td>Job</td>
<td><code>employeeDisembark</code> in <code>JobWorker.php</code></td>
</tr>
<tr>
<td>Endorsements</td>
<td><code>EmpEndorsementModel</code> <code>emp_endorsement</code> (<code>actions = d</code>, <code>status = pending</code>)</td>
</tr>
</tbody>
</table>
<p><strong>Routes</strong> (group <code>/employee</code>, <code>authMVC</code>):</p>
<ul>
<li><code>GET /employee/upload</code> upload screen</li>
<li><code>POST /employee/upload</code> <code>upload-action-type=deletion</code></li>
<li><code>GET /employee/excel_error/{file_id}</code> read <code>files.reason</code> after validation failure</li>
</ul>
<div class="callout info">
<span>i</span>
<div>
<strong><code>files.policy_id</code></strong> is the <strong>client policy id</strong>. Member lookup joins
<code>employees</code> + <code>employee_polices</code> on that policy and branch.
</div>
</div>
<h2 id="sync-vs-jobs">Sync vs background jobs</h2>
<table>
<thead>
<tr><th>Step</th><th>&lt; 1 MB</th><th> 1 MB</th></tr>
</thead>
<tbody>
<tr><td>Format validation</td><td>Inline on upload</td><td>Job <code>excelFileFormatValidation</code></td></tr>
<tr><td>Data validation</td><td>Job <code>excelFileDataValidation</code></td><td>Same</td></tr>
<tr><td>Disembark</td><td>Job <code>employeeDisembark</code></td><td>Same</td></tr>
</tbody>
</table>
<h2 id="format-validation">Step 1: excelFileFormatValidation</h2>
<ul>
<li>Uses <code>$deletion_excel_columns</code> <strong>7 columns (AG)</strong>.</li>
<li>Action code <code>D</code> drives mandatory fields: Change event, Date of exit, Reason for exit, Claim status.</li>
<li>Date of exit format: <code>d-M-Y</code>.</li>
<li>Claim status allowed: <code>0</code> or <code>1</code>.</li>
</ul>
<h3 id="format-errors">Format error codes</h3>
<table>
<thead>
<tr><th>Code</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Mandatory missing</td></tr>
<tr><td>2</td><td>Wrong format</td></tr>
<tr><td>3</td><td>Not in allowed list</td></tr>
<tr><td>4</td><td>Custom validation failed</td></tr>
<tr><td>5</td><td>File / policy problem</td></tr>
<tr><td>6</td><td>Column headers wrong</td></tr>
</tbody>
</table>
<h2 id="data-validation">Step 2: excelFileDataValidation</h2>
<p>Rows are grouped by EMP ID. For deletion, the main check is <code>name_and_empid_check_in_db</code>:</p>
<ul>
<li>Each row must match an <strong>active</strong> employee + <strong>active</strong> <code>employee_polices</code> row on the uploaded policy/branch.</li>
<li>If not found error code <strong>10</strong> (“Record Not found”) on that Excel row.</li>
</ul>
<p>On success queues <code>employeeDisembark</code> (not <code>employeesOnboardPreprocess</code>).</p>
<h2 id="disembark">Step 3: employeeDisembark</h2>
<p>Loads the Excel again and processes each non-empty row.</p>
<div class="mermaid-wrapper">
<div class="mermaid">
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
</div>
</div>
<p><strong>Per matched member</strong>, four pending endorsement rows are inserted on <code>employee_polices</code>:</p>
<ul>
<li><code>date_of_exit</code> Excel column E (converted to <code>Y-m-d</code>)</li>
<li><code>reason_for_exit</code> column F</li>
<li><code>status</code> <code>inactive</code></li>
<li><code>claim_status</code> column G</li>
</ul>
<p>
Returns an array of processed <code>employee.id</code> values. Sets <code>files.status = success</code> when the loop finishes
(even if some rows were skipped check logs for “not found” or “existing endorsement pending”).
</p>
<h2 id="tpa-auto">TPA auto-deletion (EmployeeController)</h2>
<p><code>initializeDeletionProcessForTpaApiData($file_id)</code>:</p>
<ol>
<li>Reads TPA reconcile file context (<code>tpa_api_data</code>, <code>action_flag_status = D</code>).</li>
<li>Builds candidate members and writes <code>writable/uploads/excel/tpa_auto_deletion_{fileId}_{timestamp}.xls</code>.</li>
<li>Inserts a new <code>files</code> row with <code>action = deletion</code>.</li>
<li>Calls <code>employeeDisembark(['file_id' => $newFileId])</code> synchronously.</li>
<li>Exports endorsement data for TPA via <code>getDeletionEmployeeDataForExportExcel</code>.</li>
</ol>
<h2 id="excel-columns">Deletion Excel columns</h2>
<table>
<thead>
<tr><th>Col</th><th>Header</th><th>Required</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>S.No</td><td>Yes</td><td></td></tr>
<tr><td>B</td><td>EMP ID</td><td>Yes</td><td>Employee / family code</td></tr>
<tr><td>C</td><td>NAME OF EMP/DEP</td><td>Yes</td><td>Must match DB name exactly</td></tr>
<tr><td>D</td><td>Change event</td><td>Yes</td><td>e.g. <code>deletion</code></td></tr>
<tr><td>E</td><td>Date of exit</td><td>Yes</td><td><code>d-M-Y</code></td></tr>
<tr><td>F</td><td>Reason for exit</td><td>Yes</td><td></td></tr>
<tr><td>G</td><td>Claim status</td><td>Yes</td><td><code>0</code> or <code>1</code></td></tr>
</tbody>
</table>
<h2 id="row-example">Row layout examples</h2>
<p><strong>Delete one dependent</strong> only that name appears; Self row is not required in the file.</p>
<table>
<thead>
<tr><th>Row</th><th>EMP ID</th><th>NAME</th><th>Change event</th><th>Date of exit</th><th>Reason</th><th>Claim</th></tr>
</thead>
<tbody>
<tr><td>2</td><td>EMP001</td><td>Arjun Kumar</td><td>deletion</td><td>19-May-2026</td><td>Resigned</td><td>0</td></tr>
</tbody>
</table>
<p>Result: endorsements for <strong>Arjun only</strong> (relationship Self).</p>
<p><strong>Delete entire family</strong> list the <strong>Self</strong> row; disembark loads all active family members for that EMP ID.</p>
<table>
<thead>
<tr><th>Row</th><th>EMP ID</th><th>NAME</th><th>Change event</th><th>Date of exit</th><th>Reason</th><th>Claim</th></tr>
</thead>
<tbody>
<tr><td>2</td><td>EMP001</td><td>Raj Kumar</td><td>deletion</td><td>19-May-2026</td><td>Resigned</td><td>0</td></tr>
</tbody>
</table>
<p>Result: pending endorsements for <strong>Self + all active dependents</strong> on that policy (same exit date/reason/claim from the row).</p>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Upload via <code>/employee/upload</code> with action <code>deletion</code>; note <code>file_id</code>.</li>
<li>If validation fails, use <code>GET /employee/excel_error/{file_id}</code> look for code <strong>10</strong> (member not in DB).</li>
<li>After success, query <code>emp_endorsement</code> where <code>file_id</code> = upload id, <code>actions = 'd'</code>, <code>status = 'pending'</code>.</li>
<li>If rows were skipped, search logs for <code>not found</code> or <code>Existing endorsement pending</code>.</li>
<li>TPA path: trace <code>initializeDeletionProcessForTpaApiData</code> and the generated <code>tpa_auto_deletion_*.xls</code> file.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Name mismatch</strong> Excel name must match <code>employees.name</code> exactly (case/spacing).</li>
<li><strong>Not active</strong> only <code>emp_status = active</code> and <code>employee_polices.status = active</code> match.</li>
<li><strong>Duplicate pending deletion</strong> row skipped if a pending deletion endorsement already exists for that policy row.</li>
<li><strong>Self vs dependent</strong> wrong relationship in the row changes scope (one member vs whole family).</li>
<li><strong>File always success after disembark</strong> <code>files.status</code> does not reflect per-row skips; use endorsements table + logs.</li>
<li><strong>Not immediate delete</strong> members stay active until endorsements are approved/applied downstream.</li>
</ul>
<p>Related: <a href="<?= base_url('docs/inception') ?>">Inception</a> (onboard pipeline uses the same upload screen and first two validation steps).</p>

View File

@ -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);

View File

@ -27,10 +27,11 @@ $toc = $toc ?? [];
<style>
/* ─── MAIN CONTENT ───────────────────────── */
.docs-main {
flex : 1;
max-width: 760px;
padding : 48px 56px 80px;
min-width: 0;
flex : 1 1 auto;
min-width : 0;
max-width : none;
width : 0;
padding : 48px 40px 80px 48px;
}
.docs-main__breadcrumb {
@ -62,13 +63,18 @@ $toc = $toc ?? [];
/* ─── RIGHT TOC ──────────────────────────── */
.docs-toc {
width : 200px;
min-width: 200px;
padding : 56px 20px 0;
position : sticky;
top : calc(var(--topbar-h) + 32px);
height : fit-content;
align-self: flex-start;
width : var(--toc-w);
min-width : var(--toc-w);
max-width : var(--toc-w);
padding : 56px 20px 48px 16px;
border-left: 1px solid var(--border);
background : var(--bg);
position : sticky;
top : calc(var(--topbar-h) + 32px);
height : fit-content;
max-height : calc(100vh - var(--topbar-h) - 48px);
overflow-y : auto;
align-self : flex-start;
flex-shrink: 0;
}
@ -92,6 +98,14 @@ $toc = $toc ?? [];
.docs-toc a:hover { color: var(--accent); }
.docs-toc a.is-sub { padding-left: 10px; font-size: 12px; }
@media (max-width: 1100px) {
.docs-toc { display: none; }
.docs-main {
width: auto;
padding-right: 48px;
}
}
</style>
<!-- ═══════════════════════════════════════════

View File

@ -285,34 +285,7 @@ flowchart TD
</div>
</div>
<h2 id="flow-group-slabs"><code>group_slab_rates_basedon_name</code></h2>
<p>
Flattens the list returned from the model into a map keyed by <code>rack_rate_name</code>. Each bucket keeps
<code>['slab_rates' => [...], 'grid_master' => ...]</code> where <code>grid_master</code> comes from the rows policy grid record
(<code>ui_type</code> becomes the numeric grid id used later in <code>premium_calculation_manager</code>).
</p>
<div class="mermaid-wrapper" id="flowchart-group-slab-rates">
<div class="mermaid">
flowchart LR
S["slab_rates rows"] --> 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"]
</div>
</div>
<div class="callout info">
<span>i</span>
<div>
When maintaining this helper, inspect the implementation for <strong>duplicate pushes</strong> on the first row of a new rack name;
downstream code tolerates duplicate slab rows but it can confuse debugging of premium matches.
</div>
</div>
<h2 id="flow-family-composition"><code>get_familiy_composition</code></h2>
@ -321,13 +294,122 @@ flowchart LR
(already grouped to one employee). Keys align with the JSON used in rack configuration (<code>additional_relationship</code>), except
<code>either-parents-pil</code> and <code>elders_count</code> which are stripped before comparison.
</p>
<p>
This function mainly:
<ul>
<li>Reads all family members</li>
<li>Normalizes relationship names</li>
<li>Builds a summarized family composition object</li>
<li>Maintains counts for:
<ul style="padding-left: 20px;">
<li>self</li>
<li>spouse</li>
<li>children</li>
<li>parents</li>
<li>parents-in-law</li>
</ul>
</li>
</ul>
</p>
<div class="mermaid-wrapper" id="flowchart-family-composition">
<div class="mermaid">
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"])
</div>
</div>
@ -348,12 +430,87 @@ flowchart TD
<div class="mermaid-wrapper" id="flowchart-compare-slab">
<div class="mermaid">
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"])
</div>
</div>
@ -378,13 +535,60 @@ flowchart TD
<div class="mermaid-wrapper" id="flowchart-applicable-members">
<div class="mermaid">
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"])
</div>
</div>

View File

@ -0,0 +1,389 @@
<?php
/**
* Inception (employee Excel upload) content only
* app/Views/docs/inception.php
*
* Based on:
* - app/Views/employee_upload.php
* - app/Controllers/EmployeeServiceController.php
* (excelFileFormatValidation, excelFileDataValidation, employeesOnboardPreprocess)
* - app/Controllers/EmployeeController.php (upload entry)
* - app/Controllers/JobWorker.php (background jobs)
*/
?>
<p>
<strong>Inception</strong> here means onboarding employees and dependents from an Excel upload
(<code>files.action = inception</code> or related actions like <code>missed_inception</code>,
<code>addition</code>, <code>dependent_addition</code>). The same three-step pipeline runs for those
actions; this page focuses on the <strong>inception</strong> path.
</p>
<p>
Manual policy inception (form UI under <code>policy_tranction/inception</code>) is a separate flow
in <code>PolicyTransactionController</code> not covered by these three functions.
</p>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li><strong>Step 1 Format:</strong> Headers, column count/order, per-cell type and mandatory rules.</li>
<li><strong>Step 2 Data:</strong> Family-level checks (Self row, duplicates, policy terms, DB conflicts).</li>
<li><strong>Step 3 Preprocess:</strong> Premium via rack rates, then insert employees and inception policy transaction.</li>
<li>Large files (&gt; 1 MB) run steps 13 as background jobs via <code>JobWorker</code>.</li>
</ul>
<h2 id="key-files-routes">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>Location</th></tr>
</thead>
<tbody>
<tr>
<td>UI</td>
<td><code>app/Views/employee_upload.php</code> client, branch, policy, action <strong>Inception</strong>, file upload</td>
</tr>
<tr>
<td>Upload handler</td>
<td><code>EmployeeController::employeesUplodWithEvents</code></td>
</tr>
<tr>
<td>Pipeline logic</td>
<td><code>EmployeeServiceController</code> the three functions on this page</td>
</tr>
<tr>
<td>Job dispatch</td>
<td><code>app/Controllers/JobWorker.php</code> maps job names to <code>EmployeeServiceController</code></td>
</tr>
<tr>
<td>Column / Excel helpers</td>
<td><code>app/Helpers/excel_util_helper.php</code> <code>check_columns_name</code>, custom validators</td>
</tr>
<tr>
<td>Upload record</td>
<td><code>files</code> table via <code>FileModel</code> <code>status</code>, <code>action</code>, <code>reason</code></td>
</tr>
<tr>
<td>Premium</td>
<td><a href="<?= base_url('docs/eb-rack-rate-calculation') ?>">EB rack rate calculation</a> <code>calculate_premium_new</code>, <code>employeesOnboardProcess</code></td>
</tr>
</tbody>
</table>
<p><strong>Routes</strong> (group <code>/employee</code>, filter <code>authMVC</code>):</p>
<ul>
<li><code>GET /employee/upload</code> upload screen</li>
<li><code>POST /employee/upload</code> upload + start validation (<code>upload-action-type=inception</code>)</li>
<li><code>GET /employee/excel_error/{file_id}</code> returns <code>files.reason</code> JSON for the error modal</li>
</ul>
<p>
REST upload (mobile/HR): <code>POST employeeRest/employeeUpload</code> same pipeline via
<code>EmployeeRestController</code>.
</p>
<div class="callout info">
<span>i</span>
<div>
<strong><code>files.policy_id</code></strong> stores <strong>client policy id</strong> (<code>client_policies.id</code>),
not the insurer policy master id. Slab/rack lookups use this id with <code>client_id</code>.
</div>
</div>
<h2 id="sync-vs-jobs">Sync vs background jobs</h2>
<table>
<thead>
<tr><th>File size</th><th>Step 1</th><th>Steps 23</th></tr>
</thead>
<tbody>
<tr>
<td>&lt; 1 MB</td>
<td><code>excelFileFormatValidation</code> runs inline in the upload request</td>
<td>Always queued: <code>excelFileDataValidation</code> <code>employeesOnboardPreprocess</code></td>
</tr>
<tr>
<td> 1 MB</td>
<td>Job <code>excelFileFormatValidation</code></td>
<td>Same job chain after format passes</td>
</tr>
</tbody>
</table>
<p>
After upload the UI usually shows <code>files.status = inprogress</code> until jobs finish.
Poll notifications or refresh the upload list; use <code>/employee/excel_error/{id}</code> when status is <code>failed</code>.
</p>
<h2 id="entry-points">Entry points</h2>
<p>Job chain for inception (and missed_inception / addition / dependent_addition):</p>
<p><code>excelFileFormatValidation</code> <code>excelFileDataValidation</code> <code>employeesOnboardPreprocess</code></p>
<h2 id="format-validation">Step 1: excelFileFormatValidation</h2>
<p>Runs on the uploaded sheet before any DB business rules.</p>
<ul>
<li>Loads file from <code>writable/uploads/excel/{file_name}</code>.</li>
<li>Uses <code>$inception_excel_columns</code> when <code>action</code> is inception (same column set for addition / dependent_addition / missed_inception).</li>
<li>Checks policy has terms and slab rates configured otherwise fails early.</li>
<li>Validates each data row: mandatory (by action code <code>I</code>), date/mobile formats, allowed lists, custom helpers (DOB, relationship, SI, mobile duplicate, etc.).</li>
<li>Stops at first empty row (treated as end of data).</li>
</ul>
<h3 id="format-errors">Common format error codes</h3>
<table>
<thead>
<tr><th>Code</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Mandatory value missing</td></tr>
<tr><td>2</td><td>Wrong format (e.g. date, mobile)</td></tr>
<tr><td>3</td><td>Value not in allowed list</td></tr>
<tr><td>4</td><td>Custom validation failed (DOB, relationship, SI, etc.)</td></tr>
<tr><td>5</td><td>File / policy / slab configuration problem</td></tr>
<tr><td>6</td><td>Column headers wrong or out of order</td></tr>
</tbody>
</table>
<p>On failure: <code>files.status = failed</code>, <code>reason</code> JSON with row/column errors; user notification via pull notification.</p>
<p><strong>Lead-policy branch:</strong> If inception file is for a policy created from leads (<code>policy_entry_from == 3</code> and <code>is_from_lead</code> set), format validation queues <code>compareMemberDataAndInceptionData</code> instead of going straight to data validation.</p>
<h3 id="reason-json">files.reason shape (debugging)</h3>
<p>Stored as JSON string on <code>files.reason</code>. Typical failure payload:</p>
<pre><code>{
"error_type": 1,
"error_summary": { "4": 2, "1": 1 },
"error_data": {
"3": {
"dob": { "error": ["Invalid date format"], "value": "01/01/1990" }
}
}
}</code></pre>
<ul>
<li><code>error_type</code> <code>1</code> = format step, <code>2</code> = data step</li>
<li><code>error_summary</code> counts per error code (after aggregation)</li>
<li><code>error_data</code> keyed by <strong>Excel row number</strong> (1-based, header is row 1)</li>
</ul>
<h2 id="data-validation">Step 2: excelFileDataValidation</h2>
<p>Runs after format passes. Groups rows by <strong>EMP ID</strong> (family) and applies business rules.</p>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p>Typical inception checks:</p>
<ul>
<li><strong>Self</strong> row required (unless GMC parents policy allows otherwise).</li>
<li>No duplicate names within the same family in the file.</li>
<li>Employee code must not already exist for inception / addition / enrollment.</li>
<li>Dependent rules vs <code>policy_terms</code> (family composition, LGBTQ flag, etc.).</li>
<li>Name + emp id consistency vs database (<code>name_and_empid_check_in_db</code>).</li>
</ul>
<p>On success for inception: queues job <code>employeesOnboardPreprocess</code>. Other actions queue different jobs (deletion, correction, etc.).</p>
<h3 id="data-errors">Common data-validation error codes</h3>
<table>
<thead>
<tr><th>Code</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td>7</td><td>Duplicate name within same family in the Excel file</td></tr>
<tr><td>9</td><td>Record already exists in DB (inception / addition)</td></tr>
<tr><td>10</td><td>Record not found (used on deletion flows)</td></tr>
<tr><td>14</td><td>Self row missing in family</td></tr>
<tr><td>26</td><td>Duplicate employee code in file or DB</td></tr>
</tbody>
</table>
<h2 id="preprocess">Step 3: employeesOnboardPreprocess</h2>
<p>Calculates premium and writes members to the database.</p>
<ol>
<li>Reload Excel; group by <code>emp_id</code>.</li>
<li>For each family: <code>calculate_premium_new()</code> using policy terms + slab rates + rack config.</li>
<li><code>employeesOnboardProcess()</code> insert/update <code>employees</code>, <code>employee_polices</code>, create <code>policy_transaction</code> (inception).</li>
<li>If at least one family inserted <code>files.status = success</code>; else failed with rack-rate message.</li>
</ol>
<p>
Optional second path: <code>client_policy_id</code> without <code>file_id</code> converts enrolled DB members
to inception (enrollment inception), not from Excel.
</p>
<p>
<strong>TPA batch:</strong> When <code>batch_file_id</code> is present on the job payload, success also queues
<code>updateEmployeeDataFromTpa</code>, <code>reconTpaApiDataWithEmployeepolicies</code>, and
<code>initializeDeletionProcessForTpaApiData</code> (multi-file TPA reconcile flow).
</p>
<h2 id="excel-columns">Inception Excel columns</h2>
<p>
Defined in <code>EmployeeServiceController::$inception_excel_columns</code>.
Header row must match exactly <strong>19 columns (AS)</strong>, row 1 only.
Dates use format <code>d-M-Y</code> (e.g. <code>4-Apr-1990</code>).
One <strong>Self</strong> row per <strong>EMP ID</strong>; other rows are dependents.
</p>
<table>
<thead>
<tr><th>Col</th><th>Header</th><th>Required (inception)</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>S.No</td><td>Yes</td><td></td></tr>
<tr><td>B</td><td>EMP ID</td><td>Yes</td><td>Family key</td></tr>
<tr><td>C</td><td>NAME OF EMP/DEP</td><td>Yes</td><td></td></tr>
<tr><td>D</td><td>DOB</td><td>Yes</td><td><code>d-M-Y</code>; age vs relationship checked</td></tr>
<tr><td>E</td><td>Gender</td><td>Yes</td><td>M / F (several casings allowed)</td></tr>
<tr><td>F</td><td>RELATIONSHIP</td><td>Yes</td><td>Self, Spouse, Son, Daughter, </td></tr>
<tr><td>G</td><td>BASIC COVER SI</td><td>Conditional</td><td>Validated against slab when applicable</td></tr>
<tr><td>H</td><td>Date of Coverage</td><td>No</td><td>Mandatory for addition / DA only</td></tr>
<tr><td>I</td><td>DOJ</td><td>No</td><td></td></tr>
<tr><td>J</td><td>Basic Pay</td><td>No</td><td>Used when policy terms need it</td></tr>
<tr><td>K</td><td>Band/Grade</td><td>No</td><td></td></tr>
<tr><td>L</td><td>Designation</td><td>No</td><td></td></tr>
<tr><td>M</td><td>Phone</td><td>No</td><td>Mobile format; duplicate check</td></tr>
<tr><td>N</td><td>Email</td><td>No</td><td>Duplicate check in file</td></tr>
<tr><td>O</td><td>PRE EXISTING AILMENTS</td><td>Yes</td><td><code>0</code> or <code>1</code></td></tr>
<tr><td>P</td><td>Change event</td><td>No</td><td>Not used for pure inception</td></tr>
<tr><td>Q</td><td>Date of exit</td><td>No</td><td>Deletion only</td></tr>
<tr><td>R</td><td>Reason for exit</td><td>No</td><td>Deletion only</td></tr>
<tr><td>S</td><td>Unit</td><td>No</td><td>Must match branch units when filled</td></tr>
</tbody>
</table>
<p>Sample file: use the download link on the employee upload screen (environment-specific).</p>
<h2 id="family-example">Family row layout example</h2>
<p>
All rows with the same <strong>EMP ID</strong> (column B) are treated as one family.
Step 2 requires exactly one <strong>Self</strong> row in that group; dependents share the same EMP ID.
Step 3 runs premium and DB insert once per family.
</p>
<p><strong>Example:</strong> one employee (<code>EMP001</code>) with spouse and son three data rows plus header.</p>
<table>
<thead>
<tr>
<th>Row</th>
<th>S.No</th>
<th>EMP ID</th>
<th>NAME</th>
<th>DOB</th>
<th>Gender</th>
<th>RELATIONSHIP</th>
<th>BASIC COVER SI</th>
<th>PED</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td colspan="8"><em>Header row (all 19 columns AS required in file)</em></td></tr>
<tr>
<td>2</td>
<td>1</td>
<td>EMP001</td>
<td>Raj Kumar</td>
<td>15-Jan-1985</td>
<td>M</td>
<td>Self</td>
<td>500000</td>
<td>0</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>EMP001</td>
<td>Priya Kumar</td>
<td>20-Mar-1988</td>
<td>F</td>
<td>Spouse</td>
<td>500000</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>EMP001</td>
<td>Arjun Kumar</td>
<td>10-Jun-2015</td>
<td>M</td>
<td>Son</td>
<td>500000</td>
<td>0</td>
</tr>
</tbody>
</table>
<p><strong>Rules illustrated:</strong></p>
<ul>
<li><strong>Same EMP ID</strong> on rows 24 one family processed in step 3.</li>
<li><strong>Self</strong> on row 2 only row 3 without Self would fail with code 14.</li>
<li><strong>DOB</strong> uses <code>d-M-Y</code>; ages are checked against relationship (e.g. Son vs Self).</li>
<li><strong>PED</strong> (PRE EXISTING AILMENTS) = <code>0</code> or <code>1</code> on every member for inception.</li>
<li><strong>BASIC COVER SI</strong> must match slab rules when the policy uses SI-based racks (often same amount across the family).</li>
<li>Columns HS can be blank for inception when not mandatory; phone/email must be unique in the file if filled.</li>
</ul>
<p>
A second employee in the same file uses a <strong>different EMP ID</strong> (e.g. <code>EMP002</code>) with its own Self row each EMP ID is a separate family loop in preprocess.
</p>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Confirm policy has <code>policy_terms</code> JSON and slab/rack rates for the client policy.</li>
<li>Upload via <code>/employee/upload</code> with action <code>inception</code>; note <code>file_id</code> in response or <code>files</code> table.</li>
<li>If <code>status = failed</code>, call <code>GET /employee/excel_error/{file_id}</code> or read <code>files.reason</code>.</li>
<li>Map <code>error_data</code> row keys back to Excel (row 1 = header).</li>
<li>If format passes but preprocess fails with rack message, debug <code>calculate_premium_new</code> (see rack-rate doc) and SI/slab config.</li>
<li>For stuck <code>inprogress</code>, check job queue / <code>JobWorker</code> logs for the three job names.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Policy not ready</strong> missing <code>policy_terms</code> or slab rates fails step 1 with code 5.</li>
<li><strong>Missing Self row</strong> one Self per EMP ID in the file (code 14 in step 2).</li>
<li><strong>Wrong header row</strong> column count or name mismatch (code 5 / 6).</li>
<li><strong>Rack rate / SI</strong> preprocess succeeds only if <code>calculate_premium_new</code> returns data for every family.</li>
<li><strong>File status</strong> watch <code>files.reason</code> JSON for row-level errors after failure.</li>
</ul>

View File

@ -0,0 +1,488 @@
<?php
/**
* Non-EB Claims content only
* app/Views/docs/non-eb-claims.php
*
* Scope: web MVC only (/non-eb-claim/*). REST API under employeeRest is out of scope.
* Sources:
* - app/Controllers/NonEbClaimController.php
* - app/Views/non_eb_claim_*.php
* - app/Config/Routes.php (authMVC group)
*/
?>
<p>
This page documents the <strong>Non-EB Claims</strong> web workflow: list and filter claims,
create and edit tickets, status-driven form sections, document uploads, manual email reply,
<strong>mail template CRUD</strong>, auto-mail on create/status change, and claim reports.
</p>
<p>
<strong>Scope:</strong> authenticated MVC routes under <code>/non-eb-claim/*</code> only.
Mobile/REST endpoints in <code>Api\NonEbClaimApiController</code> are not covered here.
</p>
<p>
Policy types are limited to <code>policy_type.allocg IN ('Non-EB', 'Marine')</code>.
Claim records live in <code>non_eb_ticket_master</code>; claim files use <code>ticket_type = 2</code>
in <code>claim_files</code>.
</p>
<h2 id="overview">End-to-end flow</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>Typical user path:</strong></p>
<ol>
<li>Open claim list (default: open claims excluding settled/closed/rejected/withdrawn).</li>
<li>Add claim form opens with canonical status policy type <code>50</code> see <a href="#policy-type-50-default">why 50 is hardcoded</a>.</li>
<li>On save, optional auto-mail fires if a matching template has <code>is_auto_mail = 1</code>.</li>
<li>Open claim from list edit view with history, messages, files, notes.</li>
<li>Change status allowed next statuses from <code>ticket_claim_status.allowed_status</code>; section visibility updates.</li>
<li>Configure templates at <code>/non-eb-claim/mail_template</code> (direct URL; not in main Claims sidebar today).</li>
</ol>
<h2 id="policy-type-50-default">Why policy type <code>50</code> is hardcoded (read this before changing Non-EB Claims)</h2>
<div style="border-left: 4px solid #c0392b; background: #fdf2f2; padding: 1rem 1.25rem; margin: 1.25rem 0;">
<p style="margin-top: 0;">
<strong>Non-EB has many real policy products</strong> (Fire, Marine, Liability, etc. each row in
<code>policy_type</code> with <code>allocg</code> Non-EB or Marine). Claim <strong>status workflows</strong> are
<em>not</em> stored separately per product today. The team configured <strong>one canonical policy type,
id <code>50</code></strong>, in the database as the single source of status definitions. The application
assumes <strong>every</strong> Non-EB/Marine claim uses that same status set unless you deliberately change
backend and frontend.
</p>
</div>
<h3 id="policy-type-50-assumption">Design assumption</h3>
<p>
Claim statuses live in <code>ticket_claim_status</code>, keyed by <code>ticket_type</code> (which equals
<code>policy_type.id</code>). Mail templates in <code>ticket_mail_template</code> also key off
<code>ticket_type</code> + <code>trigger_type</code> from that status row.
</p>
<p>Instead of maintaining duplicate status trees for every Non-EB product, developers assumed:</p>
<blockquote>
<p style="margin: 0;">
<strong>All Non-EB and Marine policy types share one identical claim status lifecycle.</strong>
That lifecycle is configured only under policy type <code>50</code> in
<code>ticket_claim_status</code> (and matching templates under <code>ticket_type = 50</code>).
</p>
</blockquote>
<p>
New claims opened via the list <strong>Add</strong> button or <strong>New Non EB Claim</strong> menu therefore
pass <code>50</code> into the form URL. Status dropdowns, section visibility, and initial status resolution in
<code>getClaimStatusForPolicyType($policy_type_id)</code> all use that id on create not the id of the policy
the user later picks from <code>client_policy</code>.
</p>
<h3 id="policy-type-50-vs-selected-policy">Policy type 50 vs policy selected on the form</h3>
<p>On create (<code>non_eb_claim_form.php</code>):</p>
<ul>
<li>Hidden <code>policy_type_id</code> is set from the URL segment (typically <code>50</code>) and is <strong>not</strong> updated when the user selects a branch policy.</li>
<li>The UI shows the real product name in <code>policy_type_display</code> from the selected policy row (<code>data-policy-type</code>).</li>
<li><code>POST /non-eb-claim/create</code> persists <code>policy_type_id</code> from that hidden field so new tickets from Add often store <code>policy_type_id = 50</code> even when the linked policy is Fire, Marine, etc.</li>
</ul>
<p>
On edit (<code>non_eb_claim_edit.php</code>), <code>policy_type_id</code> comes from
<code>non_eb_ticket_master</code> as saved. Status changes and templates use that stored id.
</p>
<h3 id="policy-type-50-where-hardcoded">Where <code>50</code> appears in code (change all if this design changes)</h3>
<div class="docs-table-wrap">
<table>
<thead>
<tr><th>Location</th><th>Usage</th></tr>
</thead>
<tbody>
<tr><td><code>app/Config/Routes.php</code></td><td><code>GET non-eb-claim/new</code> <code>claimForm/50</code></td></tr>
<tr><td><code>app/Views/non_eb_claim_list.php</code></td><td>DataTable Add button <code>/non-eb-claim/new/50</code></td></tr>
<tr><td><code>app/Views/non_eb_claim_form.php</code></td><td>Hidden <code>policy_type_id</code> from route; status/section AJAX uses this value</td></tr>
<tr><td><code>ticket_claim_status</code> (DB)</td><td>Master status rows maintained with <code>ticket_type = 50</code></td></tr>
<tr><td><code>ticket_mail_template</code> (DB)</td><td>Non-EB auto-mail templates should use <code>ticket_type = 50</code> if they follow the shared workflow</td></tr>
<tr><td><code>Api\NonEbClaimApiController::listClaimStatuses()</code></td><td>API hardcodes <code>where('ticket_type', 50)</code> (out of scope for this page but same assumption)</td></tr>
</tbody>
</table>
</div>
<h3 id="policy-type-50-future">When a Non-EB product needs a <em>different</em> status set</h3>
<p>
If a new (or existing) policy type must have its <strong>own</strong> statuses, triggers, or allowed transitions
(not the shared tree under <code>50</code>), you cannot only change the product row in
<code>policy_type</code>. You must update <strong>both backend and frontend</strong>:
</p>
<ol>
<li><strong>Database</strong> Add full <code>ticket_claim_status</code> rows with
<code>ticket_type = &lt;that policy_type.id&gt;</code> (claim_status, display_name, trigger_type,
allowed_status JSON). Add matching <code>ticket_mail_template</code> rows for that
<code>ticket_type</code> if auto-mail applies.</li>
<li><strong>Routes / entry URL</strong> Stop routing every new claim through <code>50</code>: e.g. change
<code>claimForm/50</code>, restore policy-type picker modal (commented in
<code>non_eb_claim_search.php</code>), or pass the correct <code>policy_type_id</code> per product.</li>
<li><strong>List Add button</strong> Replace hardcoded <code>new/50</code> in
<code>non_eb_claim_list.php</code> with the correct id or dynamic selection.</li>
<li><strong>Create form</strong> On policy selection, set hidden <code>#policy_type_id</code> to the real
<code>policy_type_id</code> from <code>getBranchAndPolicy</code> (field <code>p.policy_type_id</code> is
already returned) so create/update and <code>getVisibleSections</code> use the right status tree.</li>
<li><strong>Controller logic</strong> Ensure <code>getClaimStatusForPolicyType</code>,
<code>getTemplateDataByTicketID</code>, and filters that assume a single Non-EB status catalog are tested for
the new <code>ticket_type</code>.</li>
<li><strong>API</strong> Replace hardcoded <code>50</code> in <code>listClaimStatuses()</code> if mobile
clients need per-product statuses.</li>
</ol>
<p>
Until those steps are done, pointing Add at another id without cloning the full status + template set under
that id will produce <strong>empty status lists</strong>, <strong>wrong section visibility</strong>, or
<strong>missing auto-mail</strong>.
</p>
<h2 id="key-files">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>File / route</th></tr>
</thead>
<tbody>
<tr><td>Controller</td><td><code>app/Controllers/NonEbClaimController.php</code></td></tr>
<tr><td>Model</td><td><code>app/Models/NonEbTicketMasterModel.php</code></td></tr>
<tr><td>List + filters</td><td><code>app/Views/non_eb_claim_search.php</code>, <code>non_eb_claim_list.php</code></td></tr>
<tr><td>New claim</td><td><code>app/Views/non_eb_claim_form.php</code></td></tr>
<tr><td>View / edit</td><td><code>app/Views/non_eb_claim_edit.php</code></td></tr>
<tr><td>Mail templates</td><td><code>app/Views/non_eb_claim_mail_template.php</code></td></tr>
<tr><td>Reports</td><td><code>app/Views/non_eb_claim_reports.php</code></td></tr>
<tr><td>Routes</td><td><code>app/Config/Routes.php</code> group <code>/non-eb-claim</code>, filter <code>authMVC</code></td></tr>
<tr><td>ACL</td><td><code>app/Config/Acl.php</code> <code>#^/non-eb-claim#</code> (Claims team roles)</td></tr>
<tr><td>App menu</td><td><code>app/Views/layout/header.php</code> New / List only (EB mail template link is separate)</td></tr>
</tbody>
</table>
<h3 id="route-map">MVC route map</h3>
<div class="docs-table-wrap docs-table-wrap--fluid">
<table>
<thead>
<tr><th>Method</th><th>Route</th><th>Controller</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td>GET</td><td><code>/non-eb-claim/list</code></td><td><code>claimList</code></td><td>Search page + default open claims</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/list</code></td><td><code>claimList</code></td><td>Filter HTML partial for DataTable</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/new</code></td><td><code>claimForm/50</code></td><td>New claim (default policy type 50)</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/new/{policy_type_id}</code></td><td><code>claimForm</code></td><td>New claim for selected product</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/create</code></td><td><code>createClaim</code></td><td>Create ticket + assets + history + auto-mail</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/view/{id}</code></td><td><code>view_claim</code></td><td>Edit layout (<code>non_eb_claim_edit</code>)</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/update</code></td><td><code>updateClaim</code></td><td>Update; auto-mail on status change</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/remove?ticket_id=</code></td><td><code>removeClaim</code></td><td>Soft delete (<code>is_active = 0</code>)</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/mail_template</code></td><td><code>mailTemplate</code></td><td>Template list + modal CRUD UI</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/crud_mail_template/1</code></td><td><code>crudTemplate</code></td><td>Save template</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/crud_mail_template/2</code></td><td><code>crudTemplate</code></td><td>Fetch one template (edit)</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/crud_mail_template/3</code></td><td><code>crudTemplate</code></td><td>Soft delete template</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/note/1</code></td><td><code>crudNote</code></td><td>Get note</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/note/2</code></td><td><code>crudNote</code></td><td>Save note</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/reply</code></td><td><code>saveReply</code></td><td>Manual outbound mail + message row</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/reports</code></td><td><code>claimReports</code></td><td>Reports UI shell</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/reports</code></td><td><code>claimReports</code></td><td><em>Not implemented in controller</em> see <a href="#reports">Reports</a></td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/getBranchAndPolicy</code></td><td><code>getBranchAndPolicyByClientID</code></td><td>Branches, policies, contacts for client</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/getVisibleSections</code></td><td><code>getVisibleSectionsAjax</code></td><td>Section keys for status</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/getMoreInfo</code></td><td><code>getMoreInfo</code></td><td>Ticket row JSON</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/uploadFile</code></td><td><code>uploadFile</code></td><td>Claim docs (file or Drive URL)</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/getClaimFiles</code></td><td><code>getClaimFiles</code></td><td>List files for ticket</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/removeFile?id=</code></td><td><code>removeFile</code></td><td>Soft delete file</td></tr>
<tr><td>POST</td><td><code>/non-eb-claim/saveIRDocs</code></td><td><code>saveIRDocs</code></td><td>Persist required-docs JSON on ticket</td></tr>
<tr><td>GET</td><td><code>/non-eb-claim/testAutoMail/{id}</code></td><td><code>testAutoMailTrigger</code></td><td>Dev/test auto-mail (optional)</td></tr>
</tbody>
</table>
</div>
<h2 id="access">Access control</h2>
<p>
All routes in the <code>/non-eb-claim</code> group use the <code>authMVC</code> filter.
<code>Acl.php</code> allows roles <code>HEAD</code>, <code>ADMIN</code>, <code>MANAGER</code>,
<code>ACCOUNT_MANAGER</code> on the Claims team.
</p>
<p>
List row actions (view / delete) render only for roles <code>1, 2, 5</code> in
<code>non_eb_claim_list.php</code>.
</p>
<h2 id="list-flow">List and filter</h2>
<p><strong>GET <code>/non-eb-claim/list</code></strong> loads <code>non_eb_claim_search.php</code>, which includes the table partial. Initial data comes from <code>claimSearch(1)</code>: active tickets where status display name is <em>not</em> in Claim Settled, Claim Closed, Claim Rejected, Claim Withdrawn.</p>
<p><strong>Filter sidebar</strong> posts the same URL with criteria (at least one required):</p>
<ul>
<li><code>policy_type_id</code>, <code>insurer_id</code>, <code>claim_number</code>, <code>nhance_claim_ref_no</code></li>
<li><code>client_id</code>, <code>claim_status_id</code> (status options filtered by policy type in JS)</li>
<li><code>date_type</code> + <code>start_date</code> / <code>end_date</code> (<code>created_date</code> or <code>updated_date</code>)</li>
</ul>
<p>
Response is JSON <code>{ status: true, html: "" }</code>; JS replaces <code>#claim_list_div</code>
and re-initializes the DataTable. Row click navigates to <code>/non-eb-claim/view/{id}</code>.
</p>
<p>
<strong>Add</strong> button: <code>window.location.href = '/non-eb-claim/new/50'</code>.
Header menu uses <code>/non-eb-claim/new</code> (routes to <code>claimForm/50</code>).
See <a href="#policy-type-50-default">Policy type 50</a> for why this id is fixed and what to change if a product needs its own statuses.
</p>
<h2 id="create-edit">Create and edit claim</h2>
<h3 id="create">Create</h3>
<ol>
<li><code>GET /non-eb-claim/new/{policy_type_id}</code> <code>getFormData()</code>: ACMs (role 3), clients, insurers, initial claim status for product, visible sections.</li>
<li>User selects client <code>POST getBranchAndPolicy</code> fills branch, policy (Non-EB/Marine only), branch contact.</li>
<li>Status change <code>POST getVisibleSections</code> toggles accordion sections client-side.</li>
<li><code>POST /non-eb-claim/create</code> validation via <code>getValidationRules()</code>, <code>sanitizeInputArrayAdvanced</code>, date normalization, optional asset file upload.</li>
<li>Duplicate guard: same <code>client_id</code> + <code>loss_date</code> + <code>policy_no</code> (if policy set) HTTP 409-style JSON.</li>
<li>On success: insert <code>non_eb_ticket_master</code>, <code>saveAssets()</code>, history row, <code>sendAutoMailTrigger()</code>, redirect to list.</li>
</ol>
<h3 id="edit">View / edit</h3>
<p>
<code>view_claim($id)</code> loads ticket via <code>NonEbTicketMasterModel::getTicketDataByTicketID()</code>,
merges mail template preview (<code>getTemplateDataByTicketID</code> + placeholder replace),
messages, history, assets, and reuses the edit view <code>non_eb_claim_edit.php</code>.
</p>
<p>
<code>POST /non-eb-claim/update</code> mirrors create validation. If <code>claim_status_id</code> changes,
auto-mail runs again. <code>remark_mode=append</code> appends to <code>closure_remark</code> with a separator.
</p>
<h2 id="status-sections">Status-driven sections</h2>
<p>
<code>$statusSectionVisibility</code> in the controller maps each <code>ticket_claim_status.claim_status</code>
label to section keys: <code>policy_account</code>, <code>loss_incident</code>, <code>intimation</code>,
<code>insured_contact</code>, <code>asset</code>, <code>documents</code>, <code>surveyor</code>, <code>settlement</code>.
</p>
<p>
Allowed next statuses come from <code>getClaimStatusForPolicyType()</code>: current status plus IDs in
<code>allowed_status</code> JSON on the status row. Both create and edit forms call
<code>getVisibleSectionsAjax</code> when the user changes status.
</p>
<h2 id="auto-mail">Auto-mail and placeholders</h2>
<p>Template lookup (<code>NonEbTicketMasterModel::getTemplateDataByTicketID</code>):</p>
<ul>
<li>Join <code>ticket_claim_status</code> on tickets <code>claim_status_id</code> (or explicit <code>status_id</code> for tests).</li>
<li>Join <code>ticket_mail_template</code> where <code>ticket_type = policy_type_id</code> AND <code>trigger_type = tcs.trigger_type</code>.</li>
</ul>
<p>
<code>sendAutoMailTrigger($ticket_id)</code> sends only when the matched template has
<code>is_auto_mail = 1</code>. Mail goes to <code>insured_contact_email</code> from
<code>constructMailContent()</code>; from address <code>claims@nhanceindia.in</code> via <code>MailHelper::send_email()</code>.
Successful sends insert a <code>ticket_messages</code> row via <code>autoMessageInsertBasedOnMailResponse()</code>.
</p>
<p>Placeholders (subject/body):</p>
<div class="docs-table-wrap">
<table>
<thead><tr><th>Token</th><th>Ticket field</th></tr></thead>
<tbody>
<tr><td><code>((ACM))</code></td><td><code>acm</code></td></tr>
<tr><td><code>((ACM_CONTACT))</code></td><td><code>acm_mobile</code></td></tr>
<tr><td><code>((INSURED_NAME))</code></td><td><code>insured_contact_name</code></td></tr>
<tr><td><code>((CORPORATE_NAME))</code></td><td><code>client_name</code></td></tr>
<tr><td><code>((CLAIM_NO))</code></td><td><code>claim_number</code></td></tr>
<tr><td><code>((POLICY_TYPE))</code></td><td><code>policy_type_name</code></td></tr>
<tr><td><code>((NHANCE_REF_NO))</code></td><td><code>nhance_claim_ref_no</code></td></tr>
<tr><td><code>((LOSS_DATE))</code></td><td><code>loss_date</code></td></tr>
<tr><td><code>((LOSS_LOCATION))</code></td><td><code>loss_location</code></td></tr>
<tr><td><code>((NATURE_OF_LOSS))</code></td><td><code>nature_of_loss</code></td></tr>
</tbody>
</table>
</div>
<p>
Edit screen also supports <strong>manual reply</strong>: <code>POST /non-eb-claim/reply</code> validates To/Subject,
inserts <code>ticket_messages</code>, sends via <code>sendReplyMessage()</code> with placeholder replacement.
</p>
<h2 id="mail-template-crud">Mail template CRUD</h2>
<p>
Page: <code>GET /non-eb-claim/mail_template</code>. DataTable lists rows from
<code>ticket_mail_template</code> (<code>is_active = 1</code>). Tooltip on each row shows the
matching <code>ticket_claim_status.claim_status</code> for that policy type + trigger type.
</p>
<div class="mermaid-wrapper">
<div class="mermaid">
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
</div>
</div>
<h3 id="template-fields">Template fields</h3>
<ul>
<li><code>template_name</code>, <code>ticket_type</code> (policy type id), <code>trigger_type</code> (19)</li>
<li><code>subject</code>, <code>mail_content</code> (Jodit HTML)</li>
<li><code>is_auto_mail</code> checkbox “Auto Mail” (1 = send on create / status change when matched)</li>
<li>Optional <code>id</code> on save for update</li>
</ul>
<p>
<strong>Trigger status:</strong> Each <code>ticket_claim_status</code> row for a Non-EB/Marine
<code>ticket_type</code> has a <code>trigger_type</code>. The templates <code>trigger_type</code> must match
that column for auto-mail and for the “Claim Status” readonly hint (<code>tool_tip</code> from server on fetch).
</p>
<h3 id="template-actions">UI actions</h3>
<div class="docs-table-wrap docs-table-wrap--fluid">
<table>
<thead>
<tr><th>Action</th><th>Endpoint</th><th>Body</th><th>Result</th></tr>
</thead>
<tbody>
<tr><td>Add / Save</td><td><code>POST /crud_mail_template/1</code></td><td>Form fields + <code>mail_content</code> + <code>is_auto_mail</code></td><td><code>{ status: bool }</code> reload</td></tr>
<tr><td>Edit load</td><td><code>POST /crud_mail_template/2</code></td><td><code>id</code></td><td><code>{ status, data }</code> opens modal</td></tr>
<tr><td>Delete</td><td><code>POST /crud_mail_template/3</code></td><td><code>id</code></td><td>Soft delete (<code>is_active = 0</code>)</td></tr>
</tbody>
</table>
</div>
<p>
Placeholder dropdown in the modal inserts tokens into subject (focused input) or Jodit body.
Validation errors return HTTP 400 with <code>errors</code> map (shown via toastr).
</p>
<h2 id="notes">Notes</h2>
<p>On the edit screen:</p>
<ul>
<li><code>POST /non-eb-claim/note/1</code> <code>id</code> (ticket), optional <code>is_auto_query</code> fetch active note.</li>
<li><code>POST /non-eb-claim/note/2</code> save note (required 31000 chars) via <code>TicketNoteModel</code>.</li>
</ul>
<h2 id="documents">Documents and IR checklist</h2>
<ul>
<li><code>uploadFile</code> multi upload to <code>writable/uploads/claim_files/</code> or Google Drive URLs (<code>file_type</code> 1 vs 2).</li>
<li><code>getClaimFiles</code> lists rows; local files expose download URL <code>downloadClaimFile/{id}</code>.</li>
<li><code>removeFile</code> soft delete by file id.</li>
<li><code>saveIRDocs</code> stores JSON in <code>non_eb_ticket_master.required_docs</code>.</li>
<li>Asset spreadsheet on ticket: <code>asset_file</code> under <code>writable/uploads/non_eb_asset_files/</code>; loss description required when file uploaded.</li>
</ul>
<h2 id="reports">Reports</h2>
<p>
<code>GET /non-eb-claim/reports</code> renders filters: policy type, ACM name, date range (default last 60 days).
<code>generateReport()</code> in the view POSTs to the same URL and expects
<code>{ status: true, data: [ rows ] }</code> for DataTable columns (status, policy type, claim/ref, client, insurer, loss fields, surveyor, settlement, ACM, created date).
</p>
<p>
<strong>Gap:</strong> <code>NonEbClaimController::claimReports()</code> 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 <code>ticket_reports</code> or reuse <code>claimSearch</code> with report-specific selects).
</p>
<h2 id="data-model">Data model (summary)</h2>
<table>
<thead><tr><th>Table</th><th>Role</th></tr></thead>
<tbody>
<tr><td><code>non_eb_ticket_master</code></td><td>Main claim ticket</td></tr>
<tr><td><code>non_eb_claim_asset</code></td><td>Repeating asset lines per ticket</td></tr>
<tr><td><code>ticket_claim_status</code></td><td>Statuses per <code>ticket_type</code> (policy type id); <code>trigger_type</code>, <code>allowed_status</code></td></tr>
<tr><td><code>ticket_mail_template</code></td><td>Templates; <code>ticket_type</code> = policy type id</td></tr>
<tr><td><code>ticket_history</code></td><td>Field-level audit (status, ACM, priority, )</td></tr>
<tr><td><code>ticket_messages</code></td><td>Outbound mail log</td></tr>
<tr><td><code>ticket_notes</code></td><td>User notes per ticket</td></tr>
<tr><td><code>claim_files</code></td><td>Attachments; <code>ticket_type = 2</code> for Non-EB</td></tr>
</tbody>
</table>
<h2 id="controller-reference">Controller reference</h2>
<div class="docs-table-wrap docs-table-wrap--fluid">
<table>
<thead><tr><th>Method</th><th>Used for</th></tr></thead>
<tbody>
<tr><td><code>claimList</code> / <code>claimSearch</code></td><td>List UI and filtered HTML</td></tr>
<tr><td><code>claimForm</code> / <code>getFormData</code></td><td>New claim form bootstrap</td></tr>
<tr><td><code>view_claim</code></td><td>Edit view</td></tr>
<tr><td><code>createClaim</code> / <code>updateClaim</code></td><td>Persist ticket</td></tr>
<tr><td><code>getClaimStatusForPolicyType</code> / <code>getVisibleSections*</code></td><td>Status dropdown + sections</td></tr>
<tr><td><code>mailTemplate</code> / <code>crudTemplate</code></td><td>Template admin</td></tr>
<tr><td><code>sendAutoMailTrigger</code> / <code>constructMailContent</code></td><td>Automated email</td></tr>
<tr><td><code>saveReply</code> / <code>getTicketMessage</code></td><td>Manual email thread</td></tr>
<tr><td><code>crudNote</code></td><td>Notes</td></tr>
<tr><td><code>uploadFile</code> / <code>getClaimFiles</code> / <code>removeFile</code> / <code>saveIRDocs</code></td><td>Documents</td></tr>
<tr><td><code>saveAssets</code> / <code>getAssets</code></td><td>Asset grid</td></tr>
<tr><td><code>claimHistory</code> / <code>putHistoryAfterInsert</code></td><td>Audit trail</td></tr>
<tr><td><code>getBranchAndPolicyByClientID</code></td><td>Client cascade</td></tr>
<tr><td><code>claimReports</code></td><td>Reports page (GET only today)</td></tr>
<tr><td><code>testAutoMailTrigger</code></td><td>Dev preview/send test</td></tr>
</tbody>
</table>
</div>
<h2 id="developer-steps">Developer checklist</h2>
<ol>
<li>Ensure <code>ticket_claim_status</code> rows exist per Non-EB/Marine <code>policy_type.id</code> with correct <code>trigger_type</code> and <code>allowed_status</code>.</li>
<li>Create mail templates at <code>/non-eb-claim/mail_template</code> with matching <code>ticket_type</code> + <code>trigger_type</code>; enable Auto Mail only where intended.</li>
<li>Verify insured email is present before relying on auto-mail.</li>
<li>Before changing Add/new URLs: read <a href="#policy-type-50-default">Policy type 50</a> clone statuses + templates in DB and update every hardcoded <code>50</code> if a product needs its own workflow.</li>
<li>Implement POST branch in <code>claimReports()</code> if reports Generate must work.</li>
<li>Claim files: always set <code>ticket_type = 2</code> in new file-related code paths.</li>
<li>ACL: extend <code>#^/non-eb-claim#</code> if new roles need access.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Template mismatch:</strong> No row in <code>ticket_mail_template</code> for policy type + status <code>trigger_type</code> no auto-mail and empty reply preview.</li>
<li><strong>Reports POST missing:</strong> UI POSTs to <code>/non-eb-claim/reports</code> but controller only loads view on GET.</li>
<li><strong>Duplicate claims:</strong> Same client + loss date + policy number blocked on create.</li>
<li><strong>Policy type 50 assumption:</strong> Shared status catalog under DB <code>ticket_type = 50</code>; Add/new routes and hidden form field use <code>50</code> not the policy picked on the form. Different per-product statuses require full backend + frontend changes <a href="#policy-type-50-future">checklist</a>.</li>
<li><strong>Menu vs Non-EB templates:</strong> Sidebar “Mail Template” points to EB <code>/ticket/mail_template</code>, not Non-EB.</li>
<li><strong>Soft deletes:</strong> Remove claim/file/template sets <code>is_active = 0</code>; list queries filter active only.</li>
<li><strong>Asset file:</strong> Upload without loss description fails validation on create/update.</li>
<li><strong>REST API separate:</strong> Mobile create/list under <code>employeeRest</code> / <code>Api\NonEbClaimApiController</code> different validation and flows.</li>
</ul>

View File

@ -0,0 +1,638 @@
<?php
/**
* Non-EB Opportunities content only
* app/Views/docs/non-eb-opportunities.php
*
* Scope: list-view flow only create/edit form, then table actions on /leads/list.
* Sources:
* - app/Views/leads_list.php (Non-EB action buttons)
* - app/Views/leads_non_eb.php (add/edit form)
* - app/Views/leads_form_handler.php
* - app/Controllers/LeadsController.php (line ~4793+ and createLead / sendMail paths)
*/
?>
<p>
This page documents the <strong>Non-EB Opportunities</strong> workflow from the
<strong>Opportunities list</strong> (<code>/leads/list</code>). A user creates a Non-EB opportunity,
then progresses through RFQ QCR mail actions placement all from the list row action menu.
</p>
<p>
Non-EB rows are identified by <code>leads.lead_form_type = 2</code> (EB = <code>1</code>).
RFQ and QCR are managed in <strong>Google Sheets</strong>; sheet IDs are stored in
<code>leads.misc</code> JSON.
</p>
<h2 id="overview">End-to-end flow</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>Typical sequence from the list:</strong></p>
<ol>
<li><strong>Create</strong> opportunity (form submit) <code>queued</code></li>
<li><strong>RFQ</strong> create/open Google Sheet <code>rfq_created</code></li>
<li><strong>QCR</strong> copy RFQ sheet <code>qcr_created</code></li>
<li><strong>Send Internal Mail</strong> team mail with RFQ or QCR attachment (by current status)</li>
<li><strong>Send Insurer Mail</strong> RFQ sheet attached <code>rfq_sent</code></li>
<li><strong>Send Client Mail</strong> QCR sheet attached <code>qcr_sent</code></li>
<li><strong>Placement</strong> placement sheet + policy/payment fields <code>won</code></li>
</ol>
<p>
<strong>Edit</strong> is available at any stage from the same action menu and reuses the add form
with pre-filled data (<code>getLeadNonEB</code> + <code>POST /leads/create</code> with lead id).
</p>
<h2 id="key-files">Key files</h2>
<table>
<thead>
<tr><th>Area</th><th>File</th></tr>
</thead>
<tbody>
<tr><td>List + action dropdown</td><td><code>app/Views/leads_list.php</code></td></tr>
<tr><td>Non-EB add/edit form</td><td><code>app/Views/leads_non_eb.php</code></td></tr>
<tr><td>EB vs Non-EB form include</td><td><code>app/Views/leads_form_handler.php</code></td></tr>
<tr><td>All backend logic</td><td><code>app/Controllers/LeadsController.php</code></td></tr>
<tr><td>Google Sheets</td><td><code>GoogleSheetLib</code>, <code>Config\RfqConfig</code></td></tr>
</tbody>
</table>
<h2 id="google-sheet-config">Google Sheet config</h2>
<p>
Non-EB RFQ, QCR, and Placement sheets are <strong>Google Drive files</strong> created at runtime by
<code>GoogleSheetLib</code> using a service account. Only <strong>RFQ</strong> needs a pre-configured
template per product; QCR and Placement are copies of the leads RFQ/QCR sheets.
</p>
<h3 id="gsheet-app-drive-flow">App Google Drive flow</h3>
<div class="mermaid-wrapper">
<div class="mermaid">
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
</div>
</div>
<p><strong>Library:</strong> <code>app/Libraries/GoogleSheetLib.php</code> auth via service account JSON at
<code>{project-root}/nhance-ee8d1-e3c5269b1ec7.json</code>, scopes <code>DRIVE</code> + <code>SPREADSHEETS</code>.</p>
<h3 id="gsheet-prerequisites">What must be done before RFQ / QCR</h3>
<table>
<thead>
<tr><th>Step</th><th>Before</th><th>Why</th></tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Google service account JSON present; account email shared on template folder + each template sheet</td>
<td><code>GoogleSheetLib</code> cannot copy or edit without Drive access</td>
</tr>
<tr>
<td>2</td>
<td><code>RFQ_PARENT_FOLDER_ID</code> set in <code>.env</code></td>
<td>Target folder for every copied RFQ/QCR/Placement file</td>
</tr>
<tr>
<td>3</td>
<td>Master RFQ Google Sheet template created per product (Fire, Marine, GPA, etc.) with placeholder tokens</td>
<td>RFQ copy source one template per <code>policy_type</code> row</td>
</tr>
<tr>
<td>4</td>
<td><code>policy_type.misc</code> JSON updated with <code>rfq_template_sheet_id</code> for that product</td>
<td><code>createRfqSheet()</code> reads this; fails with “RFQ template not found” if missing</td>
</tr>
<tr>
<td>5</td>
<td>Non-EB opportunity created with correct <code>policy_type_id</code> and <code>rfq_qcr_viewers</code> emails</td>
<td>Lead must exist; viewers become sheet editors after copy</td>
</tr>
<tr>
<td>6</td>
<td><strong>Before QCR:</strong> RFQ action completed (<code>leads.misc.rfq_sheet_id</code> set)</td>
<td>QCR copies the leads RFQ sheet, not the policy template</td>
</tr>
<tr>
<td>7</td>
<td><strong>Before Placement mail:</strong> QCR action completed (<code>leads.misc.qcr_sheet_id</code> set)</td>
<td>Placement copies the QCR sheet</td>
</tr>
</tbody>
</table>
<h3 id="gsheet-product-config">Where to configure sheet ID per product</h3>
<p>
Template sheet IDs are stored <strong>per product</strong> on the <code>policy_type</code> table
one row per product (Fire, Marine, Burglary, etc.). The leads selected
<code>policy_type_id</code> determines which template is copied when RFQ is clicked.
</p>
<p><strong>Database column:</strong> <code>policy_type.misc</code> (JSON text)</p>
<pre><code>{
"rfq_template_sheet_id": "1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0"
}</code></pre>
<p>Example set or update for policy type id <code>12</code>:</p>
<pre><code>UPDATE policy_type
SET misc = JSON_SET(COALESCE(misc, '{}'), '$.rfq_template_sheet_id', 'YOUR_GOOGLE_DRIVE_FILE_ID')
WHERE id = 12;</code></pre>
<div class="callout info">
<span>i</span>
<div>
There is no admin UI field for <code>rfq_template_sheet_id</code> today configure via DB (or extend
<code>MasterController::editPolicyType</code> / <code>policy_type_onboarding</code> if you add a form field).
Model allow-list: <code>app/Models/PolicyTypeModel.php</code> includes <code>misc</code>.
</div>
</div>
<p><strong>How to find a template file ID:</strong></p>
<ul>
<li>From the Google Sheet URL: <code>https://docs.google.com/spreadsheets/d/<strong>{FILE_ID}</strong>/edit</code></li>
<li>CLI list all sheets in the RFQ parent folder:
<code>php public/index.php cli/list-sheet-folder-files {RFQ_PARENT_FOLDER_ID}</code>
writes <code>sheetid.json</code> with <code>name</code> + <code>sheetId</code> pairs
(<code>GoogleSheetController::listFolderSheetFilesCli</code>)</li>
</ul>
<p><strong>QCR and Placement:</strong> no separate template ID per product. They always copy from the leads existing sheets:</p>
<table>
<thead>
<tr><th>Stage</th><th>Copy source</th><th>Stored on lead</th></tr>
</thead>
<tbody>
<tr><td>RFQ</td><td><code>policy_type.misc.rfq_template_sheet_id</code></td><td><code>leads.misc.rfq_sheet_id</code></td></tr>
<tr><td>QCR</td><td><code>leads.misc.rfq_sheet_id</code></td><td><code>leads.misc.qcr_sheet_id</code></td></tr>
<tr><td>Placement</td><td><code>leads.misc.qcr_sheet_id</code> (filename: QCR Placement)</td><td><code>leads.misc.placement_sheet_id</code></td></tr>
</tbody>
</table>
<h3 id="gsheet-app-config">App-level config files</h3>
<table>
<thead>
<tr><th>Setting</th><th>Location</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>RFQ_PARENT_FOLDER_ID</code></td>
<td><code>.env</code> <code>Config\RfqConfig::$rfqParentFolderId</code></td>
<td>Google Drive folder where copied RFQ/QCR/Placement files are created</td>
</tr>
<tr>
<td><code>rfqPlaceholders</code></td>
<td><code>app/Config/RfqConfig.php</code></td>
<td>Maps template tokens like <code>{{INSURED_NAME}}</code> to lead field keys filled on RFQ create</td>
</tr>
<tr>
<td><code>rfqClaimsPlaceholder</code></td>
<td><code>app/Config/RfqConfig.php</code></td>
<td>Default <code>{{CLAIMS_DETAILS}}</code> multi-row claims table from <code>fin_years_claims</code></td>
</tr>
<tr>
<td><code>protections</code></td>
<td><code>app/Config/RfqConfig.php</code></td>
<td>Locked cell ranges on new RFQ sheets only (e.g. <code>RFQ Page!B12:C12</code>)</td>
</tr>
<tr>
<td>Service account key</td>
<td><code>nhance-ee8d1-e3c5269b1ec7.json</code> (project root)</td>
<td>Google API authentication for all sheet operations</td>
</tr>
</tbody>
</table>
<p>Placeholder tokens to embed in each products RFQ master template:</p>
<table>
<thead>
<tr><th>Token in template</th><th>Filled from</th></tr>
</thead>
<tbody>
<tr><td><code>{{INSURED_NAME}}</code></td><td>Client name / short name</td></tr>
<tr><td><code>{{COMMUNICATION_ADDRESS}}</code></td><td>Client or custom field address</td></tr>
<tr><td><code>{{GST}}</code> / <code>{{PAN}}</code></td><td>Lead GST / PAN</td></tr>
<tr><td><code>{{POLICY_PERIOD}}</code></td><td>Policy start end dates</td></tr>
<tr><td><code>{{OPPORTUNITY_TYPE}}</code></td><td>Fresh / Renewal label</td></tr>
<tr><td><code>{{RISK_LOCATION}}</code> / <code>{{OCCUPANCY}}</code></td><td>Custom policy-type fields</td></tr>
<tr><td><code>{{CLAIMS_DETAILS}}</code></td><td>Claim history table (renewal leads)</td></tr>
</tbody>
</table>
<p>
After RFQ copy, editors are granted from the leads <code>rfq_qcr_viewers</code> JSON email list (selected on the create/edit form).
The same list is applied to QCR and Placement copies.
</p>
<h2 id="create-edit">Step 1 Create opportunity</h2>
<h3 id="create-ui">From the list</h3>
<ol>
<li>Open <code>/leads/list</code> click <strong>Add</strong>.</li>
<li>Modal <strong>Select Opportunity Type</strong> choose <strong>Non-EB</strong> (<code>lead_form_type=2</code>).</li>
<li>Redirect: <code>GET /util/getLeadNonEB/2/0</code>.</li>
</ol>
<h3 id="create-form">Form (<code>leads_non_eb.php</code>)</h3>
<ul>
<li>Sections: Opportunity Details, Client Information, Branch Information, Policy Details, Sales &amp; Assignment.</li>
<li>Policy types limited to <code>allocg</code> = <strong>Non-EB</strong> or <strong>Marine</strong>.</li>
<li>Policy type change <code>GET /util/getPolicyTypeFields</code> dynamic fields in <code>#appendArea</code>.</li>
<li>Renewal types may show claim history rows stored as <code>fin_years_claims</code> JSON.</li>
<li><code>rfq_qcr_viewers</code> (emails) become Google Sheet editors later.</li>
<li>Submit AJAX <code>POST /leads/create</code> with <code>lead_form_type=2</code>.</li>
</ul>
<h3 id="create-backend">Controller: <code>createLead()</code></h3>
<ul>
<li><code>prepareLeadData()</code> <code>prepareSingleLeadData()</code> (one lead row; EB uses multi-row).</li>
<li>Non-EB validation: policy type, DOC/DOE, claim-history rows when <code>claim_history=1</code>.</li>
<li><code>insertNewLead()</code> no demography background job (EB-only).</li>
<li>Initial status: <code>queued</code> (In-Queued).</li>
</ul>
<h2 id="edit-flow">Edit flow</h2>
<p>From the list action menu <strong>Edit</strong> (available for both EB and Non-EB):</p>
<ol>
<li>JS: <code>getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)</code></li>
<li>Redirect: <code>GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}</code></li>
<li>Controller: <code>getLeadNonEB($type, $actual_lead_id, $id)</code> (~line 4818)
<ul>
<li>Loads master data (issuer, policy types, sales team, etc.)</li>
<li>When <code>$id</code> present: fetches <code>lead_edit_data</code>, files, custom fields, date formatting</li>
<li>Builds dynamic policy HTML via <code>generateViewPageHtml()</code></li>
<li>Renewal Non-EB: renders <code>rfq/claims_details_non_eb</code> into <code>claims_details_html</code></li>
<li>Returns layout with <code>leads_form_handler</code> includes <code>leads_non_eb.php</code> when <code>selected_lead_type != 1</code></li>
</ul>
</li>
<li>Form pre-fills client, branch, policy, files, viewers, status, lost reason, etc.</li>
<li>Submit same endpoint: <code>POST /leads/create</code> with hidden <code>id</code> <code>updateOldLead()</code></li>
</ol>
<h2 id="list-actions">Steps 27 List table actions (Non-EB)</h2>
<p>
When <code>lead_form_type === 2</code>, the row action menu in
<code>app/Views/leads_list.php</code> (lines ~297343) uses modal/AJAX flows instead of
navigating to <code>/rfq/list/{id}/1|2</code> (EB behaviour).
</p>
<div class="callout info">
<span>i</span>
<div>
<strong>Menu order on screen</strong> (after opportunity is created): Edit RFQ QCR
Send Internal Mail Send Insurer Mail Send Client Mail Placement Email History.
</div>
</div>
<h3 id="action-map">Action view handler controller endpoint</h3>
<div class="docs-table-wrap docs-table-wrap--fluid">
<table class="docs-action-map">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Visible when</th>
<th>View (JS) / handler class</th>
<th>Controller endpoint(s)</th>
<th>Status after</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td><strong>Edit</strong></td>
<td>Always (EB + Non-EB)</td>
<td><code>getLeadsDataForEdit()</code></td>
<td>
<span class="badge get">GET</span>
<code>/util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}</code><br>
<span class="badge post">POST</span>
<code>/leads/create</code> (update when <code>id</code> posted)
</td>
<td>User-selected on form</td>
</tr>
<tr>
<td>1</td>
<td><strong>RFQ</strong></td>
<td><code>lead_form_type === 2</code> only</td>
<td><code>.btnRfqSheetList</code> <code>createRfqSheetFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/leads/createRfqSheet?lead_id={id}</code><br>
<code>LeadsController::createRfqSheet()</code> opens Google Sheet URL in new tab
</td>
<td><code>rfq_created</code></td>
</tr>
<tr>
<td>2</td>
<td><strong>QCR</strong></td>
<td>
Non-EB; status not <code>queued</code> or <code>rfq_created</code>;
role <code>1, 5, 2, 3</code> or Business Support team
</td>
<td><code>.btnQcrSheetList</code> <code>createQcrSheetFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/leads/createQcrSheet?lead_id={id}</code><br>
<code>LeadsController::createQcrSheet()</code>
</td>
<td><code>qcr_created</code></td>
</tr>
<tr>
<td>3</td>
<td><strong>Send Internal Mail</strong></td>
<td>Non-EB; <code>status != queued</code></td>
<td><code>.btnInternalMailList</code> <code>openInternalMailFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/leads/mailTemplate?lead_id={id}&amp;template_type=rfq</code><br>
<span class="badge post">POST</span>
<code>/leads/sendMail</code> <code>recipient_type=internal</code>
</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td><strong>Send Insurer Mail</strong></td>
<td>Same as internal mail</td>
<td><code>.btnInsurerMailList</code> <code>openInsurerMailFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/leads/mailTemplate?lead_id={id}&amp;template_type=rfq</code><br>
<span class="badge post">POST</span>
<code>/leads/sendMail</code> <code>recipient_type=insurer</code> (RFQ sheet attach)
</td>
<td><code>rfq_sent</code></td>
</tr>
<tr>
<td>5</td>
<td><strong>Send Client Mail</strong></td>
<td>Same as internal mail</td>
<td><code>.btnClientMailList</code> <code>openClientMailFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/leads/mailTemplate?lead_id={id}&amp;template_type=qcr</code><br>
<span class="badge post">POST</span>
<code>/leads/sendMail</code> <code>recipient_type=client</code> (QCR sheet attach)
</td>
<td><code>qcr_sent</code></td>
</tr>
<tr>
<td>6</td>
<td><strong>Placement</strong></td>
<td>Same as internal mail</td>
<td><code>.btnPlacementList</code> <code>openPlacementFromList()</code></td>
<td>
<span class="badge get">GET</span>
<code>/rfq/placementData/{id}</code><br>
<span class="badge get">GET</span>
<code>/leads/mailTemplate?lead_id={id}&amp;template_type=placement</code><br>
<span class="badge post">POST</span>
<code>/leads/sendMail</code> <code>recipient_type=placement</code>
</td>
<td><code>won</code></td>
</tr>
<tr>
<td></td>
<td><strong>Email History</strong></td>
<td>Always (EB + Non-EB)</td>
<td><code>.btnHistory</code> <code>getLeadsDataForMailHistory()</code></td>
<td>
<span class="badge get">GET</span>
<code>/util/getLeadEmailHistory/{id}</code> <code>getLeadEmailHistory()</code>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<p><strong>EB contrast</strong> (same menu, <code>lead_form_type === 1</code>): RFQ/QCR are links to
<code>/rfq/list/{id}/1</code> and <code>/rfq/list/{id}/2</code>; internal/insurer/client/placement mail items are not shown.</p>
<p><strong>Shared mail modal helpers</strong> (all in <code>leads_list.php</code>):</p>
<ul>
<li><code>POST /leads/uploadLeadAttachment</code> optional extra files (<code>lead_id</code>, <code>docs_name</code>, file)</li>
<li><code>constructURL_ForInternalMailSend()</code>, insurer/client via <code>constructURL_ForInsurerOrClientMailSend()</code>, placement via <code>constructURL_ForPlacementMailSend()</code></li>
<li>External CC disclaimer modal (<code>#external_cc_disclaimer_modal</code>) before client/placement send to non-user emails</li>
</ul>
<h2 id="step-rfq">Step 2 RFQ (<code>createRfqSheet()</code>)</h2>
<ol>
<li>Load lead + <code>policy_type.misc.rfq_template_sheet_id</code>.</li>
<li>If <code>misc.rfq_sheet_id</code> exists return existing Google Sheet URL.</li>
<li>Copy template via <code>GoogleSheetLib::copyTemplate()</code> into <code>RfqConfig::rfqParentFolderId</code>.</li>
<li>Fill placeholders (<code>buildRfqPlaceholderData()</code>): client, GST, PAN, policy period, claims table.</li>
<li>Grant editors from <code>leads.rfq_qcr_viewers</code> email JSON.</li>
<li>Save <code>misc.rfq_sheet_id</code>; set <code>status = rfq_created</code>.</li>
<li>UI opens sheet in a new browser tab.</li>
</ol>
<h2 id="step-qcr">Step 3 QCR (<code>createQcrSheet()</code>)</h2>
<ol>
<li>Requires <code>misc.rfq_sheet_id</code> returns 400 if RFQ not created yet.</li>
<li>If <code>misc.qcr_sheet_id</code> exists return existing URL.</li>
<li>Copy the RFQ sheet (not the policy template) with a QCR filename.</li>
<li>Same editor permissions from <code>rfq_qcr_viewers</code>.</li>
<li>Save <code>misc.qcr_sheet_id</code>; set <code>status = qcr_created</code>.</li>
</ol>
<h2 id="step-mails">Steps 46 Mail actions</h2>
<h3 id="mail-template">Load template <code>getLeadMailTemplate()</code></h3>
<p>
Called before each mail modal opens. Query params:
<code>lead_id</code> + <code>template_type</code> (<code>rfq</code> | <code>qcr</code> | <code>placement</code>).
Returns subject, HTML body, and attachment checkbox HTML from <code>lead_files</code>.
</p>
<h3 id="send-mail">Send <code>sendMailWithAttachement()</code></h3>
<p>For Non-EB (<code>lead_form_type == 2</code>), the Excel attachment comes from Google Sheets:</p>
<table>
<thead>
<tr><th>recipient_type</th><th>Sheet used</th><th>Status after send</th></tr>
</thead>
<tbody>
<tr>
<td><code>internal</code></td>
<td>QCR if status is <code>qcr_created</code>/<code>qcr_sent</code>, else RFQ</td>
<td>Unchanged</td>
</tr>
<tr>
<td><code>insurer</code></td>
<td><code>misc.rfq_sheet_id</code></td>
<td><code>rfq_sent</code> (unless already past that stage)</td>
</tr>
<tr>
<td><code>client</code></td>
<td><code>misc.qcr_sheet_id</code></td>
<td><code>qcr_sent</code></td>
</tr>
<tr>
<td><code>placement</code></td>
<td><code>createAndDownloadPlacementSheet()</code> copies QCR sheet</td>
<td><code>won</code> + saves placement/payment/installment fields</td>
</tr>
</tbody>
</table>
<p>Implementation detail: <code>downloadFileFromGoogleSheet()</code> exports the chosen sheet to a temp
<code>.xlsx</code> under <code>writable/tmp/</code> before <code>MailHelper::send_email()</code>.</p>
<h2 id="step-placement">Step 7 Placement</h2>
<p>Opened from list action <code>openPlacementFromList(leadId)</code>:</p>
<ol>
<li><code>GET /rfq/placementData/{id}</code> pre-fills policy dates, premium, CD, installments, contacts.</li>
<li><code>GET /leads/mailTemplate?template_type=placement</code> subject/body/attachments.</li>
<li>User fills placement modal: dates, premium/CD/total, installment rows, To/CC, optional external CC.</li>
<li><code>POST /leads/sendMail</code> with <code>recipient_type=placement</code>.</li>
<li>Backend copies QCR placement Google Sheet, attaches Excel, updates lead to <code>won</code>,
persists <code>placement_date</code>, <code>premium_amount</code>, <code>cd_amount</code>, installments, etc.</li>
</ol>
<h2 id="status-lifecycle">Status lifecycle</h2>
<table>
<thead>
<tr><th>Status</th><th>Label</th><th>Set by</th></tr>
</thead>
<tbody>
<tr><td><code>queued</code></td><td>In-Queued</td><td>Create form (default)</td></tr>
<tr><td><code>rfq_created</code></td><td>RFQ Created</td><td><code>createRfqSheet()</code></td></tr>
<tr><td><code>rfq_sent</code></td><td>RFQ Sent</td><td>Insurer mail</td></tr>
<tr><td><code>qcr_created</code></td><td>QCR Created</td><td><code>createQcrSheet()</code></td></tr>
<tr><td><code>qcr_sent</code></td><td>QCR Sent</td><td>Client mail</td></tr>
<tr><td><code>won</code></td><td>Won</td><td>Placement mail</td></tr>
<tr><td><code>lost</code></td><td>Lost</td><td>User sets on edit form + lost reason</td></tr>
</tbody>
</table>
<h2 id="misc-json">leads.misc JSON</h2>
<pre><code>{
"rfq_sheet_id": "",
"qcr_sheet_id": "",
"placement_sheet_id": ""
}</code></pre>
<h2 id="controller-reference">Controller reference (list flow)</h2>
<p>Methods in <code>LeadsController.php</code> used by the list Non-EB flow:</p>
<table>
<thead>
<tr><th>Method</th><th>Triggered from</th></tr>
</thead>
<tbody>
<tr><td><code>createLead()</code></td><td>Form submit (create + edit)</td></tr>
<tr><td><code>getLeadNonEB()</code></td><td>Add / Edit navigation</td></tr>
<tr><td><code>getPolicyTypeFields()</code></td><td>Policy type change on form</td></tr>
<tr><td><code>createRfqSheet()</code></td><td>RFQ action</td></tr>
<tr><td><code>createQcrSheet()</code></td><td>QCR action</td></tr>
<tr><td><code>getLeadMailTemplate()</code></td><td>All mail modals</td></tr>
<tr><td><code>getPlacementData()</code></td><td>Placement modal pre-fill</td></tr>
<tr><td><code>uploadLeadAttachment()</code></td><td>Attachment upload in mail modals</td></tr>
<tr><td><code>sendMailWithAttachement()</code></td><td>All mail sends + placement</td></tr>
<tr><td><code>downloadFileFromGoogleSheet()</code></td><td>Mail attachment (internal/insurer/client)</td></tr>
<tr><td><code>createAndDownloadPlacementSheet()</code></td><td>Placement mail attachment</td></tr>
<tr><td><code>getLeadEmailHistory()</code></td><td>Email History modal</td></tr>
<tr><td><code>buildRfqPlaceholderData()</code></td><td>RFQ sheet placeholder fill (helper)</td></tr>
</tbody>
</table>
<h2 id="developer-steps">Developer checklist</h2>
<ol>
<li>Complete <a href="#google-sheet-config">Google Sheet config</a> for each Non-EB/Marine <code>policy_type</code> before first RFQ.</li>
<li>Set <code>policy_type.misc.rfq_template_sheet_id</code> per product (see <a href="#gsheet-product-config">sheet ID per product</a>).</li>
<li>Configure <code>RFQ_PARENT_FOLDER_ID</code> in <code>.env</code> and share folder/templates with the service account.</li>
<li>Ensure placeholders in master templates match <code>Config\RfqConfig::$rfqPlaceholders</code>.</li>
<li>Gate new list actions with <code>$isNonEb</code> in <code>leads_list.php</code>.</li>
<li>Respect sheet order: RFQ QCR mails placement.</li>
<li>QCR button: roles <code>[1, 5, 2, 3]</code> or <code>BUSINESS_SUPPORT_TEAM_ID</code> in user team.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>QCR before RFQ:</strong> <code>createQcrSheet</code> fails if <code>rfq_sheet_id</code> is missing.</li>
<li><strong>Queued status:</strong> Mail and placement actions are hidden until status moves past <code>queued</code>.</li>
<li><strong>Insurer mail needs RFQ sheet;</strong> client mail needs QCR sheet create sheets before sending.</li>
<li><strong>Internal mail attachment</strong> picks RFQ or QCR based on current lead status.</li>
<li><strong>Edit vs create:</strong> same <code>POST /leads/create</code>; presence of hidden <code>id</code> triggers update.</li>
</ul>

View File

@ -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);

View File

@ -27,10 +27,11 @@ $toc = $toc ?? [];
<style>
/* ─── MAIN CONTENT ───────────────────────── */
.docs-main {
flex : 1;
max-width: 760px;
padding : 48px 56px 80px;
min-width: 0;
flex : 1 1 auto;
min-width : 0;
max-width : none;
width : 0; /* grow to fill space between sidebar and right TOC */
padding : 48px 40px 80px 48px;
}
.docs-main__breadcrumb {
@ -62,13 +63,18 @@ $toc = $toc ?? [];
/* ─── RIGHT TOC ──────────────────────────── */
.docs-toc {
width : 200px;
min-width: 200px;
padding : 56px 20px 0;
position : sticky;
top : calc(var(--topbar-h) + 32px);
height : fit-content;
align-self: flex-start;
width : var(--toc-w);
min-width : var(--toc-w);
max-width : var(--toc-w);
padding : 56px 20px 48px 16px;
border-left: 1px solid var(--border);
background : var(--bg);
position : sticky;
top : calc(var(--topbar-h) + 32px);
height : fit-content;
max-height : calc(100vh - var(--topbar-h) - 48px);
overflow-y : auto;
align-self : flex-start;
flex-shrink: 0;
}
@ -109,6 +115,14 @@ $toc = $toc ?? [];
border-radius : 999px;
padding : 3px 6px;
}
@media (max-width: 1100px) {
.docs-toc { display: none; }
.docs-main {
width: auto;
padding-right: 48px;
}
}
</style>
<!-- ═══════════════════════════════════════════

View File

@ -0,0 +1,259 @@
<?php
/**
* SI Enhancement (employee Excel upload) content only
* app/Views/docs/si-enhancement.php
*
* Based on:
* - app/Views/employee_upload.php
* - app/Controllers/EmployeeServiceController.php
* (excelFileFormatValidation, excelFileDataValidation, employeesSIEnhanceProcess)
* - app/Controllers/EmployeeController.php (employeesUplodWithEvents)
* - app/Controllers/JobWorker.php
*/
?>
<p>
<strong>SI Enhancement</strong> increases sum insured for active members on a policy via Excel upload
(<code>files.action = si_enhancement</code>). The process recalculates premium from slab/rack rates and
creates <strong>pending SI endorsements</strong> on <code>employee_polices</code> SI and premium are not
updated in place until endorsements are applied downstream.
</p>
<p>
Core logic: <code>EmployeeServiceController::employeesSIEnhanceProcess</code>.
Premium math uses <code>transform_si_excel_row_to_calculatable_format</code> and
<code>premium_calculation_manager</code> (see
<a href="<?= base_url('docs/eb-rack-rate-calculation') ?>">EB rack rate calculation</a>).
</p>
<h2 id="overview">Overview</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
flowchart LR
A["Upload Excel action si_enhancement"] --> 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"]
</div>
</div>
<p><strong>In short:</strong></p>
<ul>
<li><strong>Step 1 Format:</strong> 5 columns (AE); <strong>Augmented SI</strong> validated against slabs (<code>check_si</code>).</li>
<li><strong>Step 2 Data:</strong> Member must exist on active policy; code <strong>10</strong> if not found.</li>
<li><strong>Step 3 SI enhance:</strong> Picks rack rate by relationship, recalculates premium, inserts 3 pending endorsements per member.</li>
<li>One Excel row = one member SI change (not whole-family in one row).</li>
</ul>
<h2 id="key-files-routes">Key files and routes</h2>
<table>
<thead>
<tr><th>Area</th><th>Location</th></tr>
</thead>
<tbody>
<tr>
<td>UI</td>
<td><code>app/Views/employee_upload.php</code> action <strong>SI Enhancement</strong></td>
</tr>
<tr>
<td>Upload</td>
<td><code>EmployeeController::employeesUplodWithEvents</code></td>
</tr>
<tr>
<td>Validation</td>
<td><code>excelFileFormatValidation</code>, <code>excelFileDataValidation</code></td>
</tr>
<tr>
<td>SI process</td>
<td><code>employeesSIEnhanceProcess</code></td>
</tr>
<tr>
<td>Onboard SI path</td>
<td><code>employeesSIEnhanceProcessWhileOnbboard</code> SI during dependent addition onboard (not Excel)</td>
</tr>
<tr>
<td>Job</td>
<td><code>employeesSIEnhanceProcess</code> in <code>JobWorker.php</code></td>
</tr>
<tr>
<td>Helpers</td>
<td><code>excel_util_helper.php</code> <code>transform_si_excel_row_to_calculatable_format</code>, <code>premium_calculation_manager</code></td>
</tr>
</tbody>
</table>
<p><strong>Routes</strong> (group <code>/employee</code>, <code>authMVC</code>):</p>
<ul>
<li><code>GET /employee/upload</code> upload screen</li>
<li><code>POST /employee/upload</code> <code>upload-action-type=si_enhancement</code></li>
<li><code>GET /employee/excel_error/{file_id}</code> validation errors</li>
</ul>
<div class="callout info">
<span>i</span>
<div>
<strong><code>files.policy_id</code></strong> is the client policy id. Slab rates load via
<code>getPolicySlabRatesForEmpOnboard(policy_id, client_id)</code>.
</div>
</div>
<h2 id="sync-vs-jobs">Sync vs background jobs</h2>
<table>
<thead>
<tr><th>Step</th><th>&lt; 1 MB</th><th> 1 MB</th></tr>
</thead>
<tbody>
<tr><td>Format validation</td><td>Inline on upload</td><td>Job <code>excelFileFormatValidation</code></td></tr>
<tr><td>Data validation</td><td>Job <code>excelFileDataValidation</code></td><td>Same</td></tr>
<tr><td>SI enhance</td><td>Job <code>employeesSIEnhanceProcess</code></td><td>Same</td></tr>
</tbody>
</table>
<h2 id="format-validation">Step 1: excelFileFormatValidation</h2>
<ul>
<li>Uses <code>$si_enhance_excel_columns</code> <strong>5 columns (AE)</strong>.</li>
<li>Action code <code>SI</code> for mandatory rules.</li>
<li><strong>Augmented SI</strong> (D): custom <code>check_si</code> against policy slabs (same helper as inception SI column).</li>
<li><strong>Date of SI Enhancement</strong> (E): <code>d-M-Y</code>.</li>
<li>Policy must have slab/rack configuration or format step fails early (code 5).</li>
</ul>
<h3 id="format-errors">Format error codes</h3>
<table>
<thead>
<tr><th>Code</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Mandatory missing</td></tr>
<tr><td>2</td><td>Wrong format</td></tr>
<tr><td>3</td><td>Not in allowed list</td></tr>
<tr><td>4</td><td>Custom validation failed (e.g. invalid Augmented SI for slab)</td></tr>
<tr><td>5</td><td>File / policy / slab problem</td></tr>
<tr><td>6</td><td>Column headers wrong</td></tr>
</tbody>
</table>
<h2 id="data-validation">Step 2: excelFileDataValidation</h2>
<p><code>name_and_empid_check_in_db</code> for <code>si_enhancement</code>:</p>
<ul>
<li>Row must match active employee + active <code>employee_polices</code> on the policy.</li>
<li>Not found error code <strong>10</strong> (“Record Not found”).</li>
</ul>
<p>On success queues <code>employeesSIEnhanceProcess</code>.</p>
<h2 id="si-process">Step 3: employeesSIEnhanceProcess</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
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"]
</div>
</div>
<p><strong>Per row:</strong></p>
<ol>
<li>Strip number formatting from Augmented SI (<code>removeNumberFormatting</code>).</li>
<li>Load employee (emp code + name + branch) and active <code>employee_polices</code> row.</li>
<li>Find applicable slab/rack rate from member <strong>relationship</strong> (Son/Daughter childrens, parents, etc.).</li>
<li>Build calculatable member payload via <code>transform_si_excel_row_to_calculatable_format</code> (uses family max age/count).</li>
<li><code>premium_calculation_manager</code> new premium for the augmented SI.</li>
<li>Insert pending endorsements on <code>employee_polices</code> (<code>actions = si</code>):</li>
</ol>
<table>
<thead>
<tr><th>field_name</th><th>new_value source</th></tr>
</thead>
<tbody>
<tr><td><code>basic_cover_si</code></td><td>Excel Augmented SI (column D)</td></tr>
<tr><td><code>premium</code></td><td>Recalculated premium from rack logic</td></tr>
<tr><td><code>si_enhancement_date</code></td><td>Excel Date of SI Enhancement (column E)</td></tr>
</tbody>
</table>
<p>
Sets <code>files.status = success</code> when the loop completes. Skipped rows (duplicate pending SI, missing member)
are logged only the file still succeeds.
</p>
<h2 id="excel-columns">SI Enhancement Excel columns</h2>
<table>
<thead>
<tr><th>Col</th><th>Header</th><th>Required</th><th>Notes</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>S.No</td><td>Yes</td><td></td></tr>
<tr><td>B</td><td>EMP ID</td><td>Yes</td><td></td></tr>
<tr><td>C</td><td>NAME OF EMP/DEP</td><td>Yes</td><td>Must match DB</td></tr>
<tr><td>D</td><td>Augmented SI</td><td>Yes</td><td>New SI; validated via <code>check_si</code></td></tr>
<tr><td>E</td><td>Date of SI Enhancement</td><td>Yes</td><td><code>d-M-Y</code></td></tr>
</tbody>
</table>
<h2 id="row-example">Row layout examples</h2>
<p><strong>Enhance Self SI</strong> one row:</p>
<table>
<thead>
<tr><th>EMP ID</th><th>NAME</th><th>Augmented SI</th><th>Date of SI Enhancement</th></tr>
</thead>
<tbody>
<tr><td>EMP001</td><td>Raj Kumar</td><td>1000000</td><td>19-May-2026</td></tr>
</tbody>
</table>
<p><strong>Enhance spouse only</strong> separate row (premium uses that members relationship for rack selection):</p>
<table>
<thead>
<tr><th>EMP ID</th><th>NAME</th><th>Augmented SI</th><th>Date of SI Enhancement</th></tr>
</thead>
<tbody>
<tr><td>EMP001</td><td>Priya Kumar</td><td>500000</td><td>19-May-2026</td></tr>
</tbody>
</table>
<h2 id="developer-steps">Developer steps</h2>
<ol>
<li>Confirm slab/rack rates exist for the client policy (same as inception).</li>
<li>Upload with <code>upload-action-type=si_enhancement</code>; note <code>file_id</code>.</li>
<li>On validation failure, check <code>/employee/excel_error/{file_id}</code> codes <strong>4</strong> (SI/slab) or <strong>10</strong> (member missing).</li>
<li>After success, query <code>emp_endorsement</code> where <code>file_id</code> = upload id, <code>actions = 'si'</code>, <code>status = 'pending'</code> expect up to 3 rows per member (same <code>group_key</code>).</li>
<li>Compare <code>new_value</code> on <code>basic_cover_si</code> and <code>premium</code> to Excel and rack expectations.</li>
</ol>
<h2 id="pitfalls">Common pitfalls</h2>
<ul>
<li><strong>Slab / rack not configured</strong> format step or premium calc fails; relationship must map to a non-zero rack key.</li>
<li><strong>Augmented SI vs slab</strong> <code>check_si</code> in step 1 must pass before the job runs.</li>
<li><strong>Pending SI already exists</strong> duplicate upload for same member skipped until prior endorsement cleared.</li>
<li><strong>Per-member rows</strong> enhancing whole family requires one row per member (unlike deletion Self = whole family).</li>
<li><strong>File success vs rows</strong> <code>files.status = success</code> does not guarantee every row created endorsements.</li>
<li><strong>Not live SI update</strong> <code>employee_polices.basic_cover_si</code> changes after endorsement processing.</li>
</ul>
<p>
Related:
<a href="<?= base_url('docs/inception') ?>">Inception</a>,
<a href="<?= base_url('docs/correction') ?>">Correction</a>,
<a href="<?= base_url('docs/eb-rack-rate-calculation') ?>">EB rack rate calculation</a>.
</p>

View File

@ -163,7 +163,8 @@
</h4> -->
</div>
<form role="form" class="parsley-examples" method="post" id="leads_form_id"
enctype="multipart/form-data">
enctype="multipart/form-data"
data-parsley-excluded="input[type=button], input[type=submit], input[type=reset], input[type=hidden], [disabled], :hidden, .select2-search__field">
<input type="hidden" name="id" id="leads_primarykey">
<input type="hidden" name="lead_form_type" id="lead_form_type_id" value="<?= isset($selected_lead_type) ? $selected_lead_type : 1 ?>">
@ -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') ? `<div class="custom-control custom-switch">
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" checked>
<label class="custom-control-label" for="claim_history">Claims History</label>
<label class="custom-control-label" for="claim_history">${claimHistoryLabel}</label>
</div><br>` : ``}`;
claimsFields += `
<div class="row claim-row">
<div class="form-group col-md-2">
<label for="first_year_${increment}">Year<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-year-group">
<label for="first_year_${increment}">Year<span class="text-danger claim-required-star">*</span></label>
<select class="form-control first_year_ claim-input" id="first_year_${increment}" name="first_year[]">
<option value="">Select Year</option>
<?php foreach ($lastFiveYears as $year) {
@ -2233,18 +2311,27 @@
</div>
<div class="form-group col-md-2">
<label for="emp_id_${increment}">Emp ID<span class="text-danger">*</span></label>
<label>Nil Claims</label>
<input type="hidden" class="nil-claims-value" name="nil_claims[]" value="0">
<div class="custom-control custom-checkbox" style="padding-top: 8px;">
<input type="checkbox" class="custom-control-input nil-claims-checkbox" id="nil_claims_${increment}" onchange="nilClaimsToggle(this)">
<label class="custom-control-label" for="nil_claims_${increment}">Nil Claims</label>
</div>
</div>
<div class="form-group col-md-2 claim-field-group">
<label for="emp_id_${increment}">Emp ID<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="emp_id_${increment}" name="emp_id[]">
</div>
<div class="form-group col-md-2">
<label for="emp_name_${increment}">Employee Name<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="emp_name_${increment}">Employee Name<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="emp_name_${increment}" name="emp_name[]">
</div>
<div class="form-group col-md-2">
<label for="gender_${increment}">Gender<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="gender_${increment}">Gender<span class="text-danger claim-required-star">*</span></label>
<select class="form-control claim-input" id="gender_${increment}" name="gender[]">
<option value="">Select Gender</option>
<option value="Female">Female</option>
@ -2252,26 +2339,26 @@
</select>
</div>
<div class="form-group col-md-2">
<label for="designation_${increment}">Designation <span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="designation_${increment}">Designation <span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="designation_${increment}" name="designation[]">
</div>
<div class="form-group col-md-2">
<label for="sum_insured_${increment}">Sum Insured <span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="sum_insured_${increment}">Sum Insured <span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="sum_insured_${increment}" name="sum_insured[]">
</div>
<div class="form-group col-md-2">
<label for="first_death_date_${increment}">Date of Death<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_death_date_${increment}">Date of Death<span class="text-danger claim-required-star">*</span></label>
<div class="input-icon">
<input type="text" class="form-control death_date flatpickr-date claim-input" id="first_death_date_${increment}" name="first_death_date[]" autocomplete="off">
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
</div>
</div>
<div class="form-group col-md-2">
<label for="first_cause_of_death_${increment}">Nature/Cause Of Death <span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_cause_of_death_${increment}">Nature/Cause Of Death <span class="text-danger claim-required-star">*</span></label>
<select class="form-control claim-input" id="first_cause_of_death_${increment}" name="first_cause_of_death[]">
<option value="">Select Cause of Death</option>
<?php foreach ($causeOfDeath as $cause => $death_value) {
@ -2280,8 +2367,8 @@
</select>
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount_${increment}">Claim/Settled Amount<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_amount_${increment}">Claim/Settled Amount<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_amount_${increment}" name="first_claim_amount[]">
</div>
@ -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($('<option>', { value: value, text: value }));
}
$select.val(value);
if ($select.hasClass('select2-hidden-accessible')) {
$select.trigger('change');
}
}
function resetClaimYearField($yearField) {
if (!$yearField.length) {
return;
}
$yearField.find('option[value="Nil"]').remove();
if ($yearField.val() === 'Nil') {
$yearField.val('').trigger('change');
}
}
function nilClaimsToggle(checkbox) {
const $row = $(checkbox).closest('.claim-row');
const isNil = $(checkbox).prop('checked');
const $yearField = $row.find('[name="first_year[]"]');
resetClaimYearField($yearField);
$row.find('.nil-claims-value').val(isNil ? '1' : '0');
$row.toggleClass('claim-row-nil', isNil);
$row.find('.claim-field-group .claim-required-star').toggle(!isNil);
$row.find('.claim-input').not('[name="first_year[]"]').each(function() {
const $field = $(this);
if (isNil) {
if ($field.is('select')) {
setClaimSelectValue($field, 'Nil');
} else {
$field.val('Nil');
}
$field.prop('disabled', true).removeAttr('required');
if ($field.hasClass('select2-hidden-accessible')) {
$field.next('.select2-container').addClass('select2-container--disabled');
}
if ($field.parsley) {
$field.parsley().reset();
}
} else {
$field.prop('disabled', false);
if ($field.hasClass('select2-hidden-accessible')) {
$field.next('.select2-container').removeClass('select2-container--disabled');
}
if ($field.is('select')) {
$field.find('option[value="Nil"]').remove();
$field.val('').trigger('change');
} else {
$field.val('');
}
}
});
const claimHistoryOn = !$('#claim_history').length || $('#claim_history').prop('checked');
if (claimHistoryOn) {
$yearField.prop('required', true).prop('disabled', false);
if ($yearField.hasClass('select2-hidden-accessible')) {
$yearField.next('.select2-container').removeClass('select2-container--disabled');
}
if (!isNil) {
$row.find('.claim-input').not('[name="first_year[]"]').prop('required', true);
}
}
toggleRequiredFields();
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
}
function initNilClaimsRows() {
$('.claim-row .nil-claims-checkbox:checked').each(function() {
nilClaimsToggle(this);
});
}
function syncClaimHistoryFieldsToFormData(formData) {
const $rows = $('.claim-row');
if (!$rows.length) {
return;
}
const isEbRow = $rows.first().find('[name="emp_id[]"]').length > 0;
const fieldNames = isEbRow
? ['first_year[]', 'nil_claims[]', 'emp_id[]', 'emp_name[]', 'gender[]', 'designation[]', 'sum_insured[]', 'first_death_date[]', 'first_cause_of_death[]', 'first_claim_amount[]']
: ['first_year[]', 'nil_claims[]', 'first_policy_type_[]', 'first_date_of_loss_[]', 'first_cause_of_loss[]', 'first_claim_amount[]', 'first_settled_amount[]', 'first_claim_status[]'];
fieldNames.forEach(function(name) {
if (typeof formData.delete === 'function') {
formData.delete(name);
}
});
$rows.each(function() {
const $row = $(this);
const isNil = $row.find('.nil-claims-checkbox').prop('checked');
$row.find('.nil-claims-value').val(isNil ? '1' : '0');
formData.append('first_year[]', $row.find('[name="first_year[]"]').val() || '');
formData.append('nil_claims[]', isNil ? '1' : '0');
if (isEbRow) {
if (isNil) {
formData.append('emp_id[]', 'Nil');
formData.append('emp_name[]', 'Nil');
formData.append('gender[]', 'Nil');
formData.append('designation[]', 'Nil');
formData.append('sum_insured[]', 'Nil');
formData.append('first_death_date[]', 'Nil');
formData.append('first_cause_of_death[]', 'Nil');
formData.append('first_claim_amount[]', 'Nil');
} else {
formData.append('emp_id[]', $row.find('[name="emp_id[]"]').val() || '');
formData.append('emp_name[]', $row.find('[name="emp_name[]"]').val() || '');
formData.append('gender[]', $row.find('[name="gender[]"]').val() || '');
formData.append('designation[]', $row.find('[name="designation[]"]').val() || '');
formData.append('sum_insured[]', $row.find('[name="sum_insured[]"]').val() || '');
formData.append('first_death_date[]', $row.find('[name="first_death_date[]"]').val() || '');
formData.append('first_cause_of_death[]', $row.find('[name="first_cause_of_death[]"]').val() || '');
formData.append('first_claim_amount[]', $row.find('[name="first_claim_amount[]"]').val() || '');
}
} else if (isNil) {
formData.append('first_policy_type_[]', 'Nil');
formData.append('first_date_of_loss_[]', 'Nil');
formData.append('first_cause_of_loss[]', 'Nil');
formData.append('first_claim_amount[]', 'Nil');
formData.append('first_settled_amount[]', 'Nil');
formData.append('first_claim_status[]', 'Nil');
} else {
formData.append('first_policy_type_[]', $row.find('[name="first_policy_type_[]"]').val() || '');
formData.append('first_date_of_loss_[]', $row.find('[name="first_date_of_loss_[]"]').val() || '');
formData.append('first_cause_of_loss[]', $row.find('[name="first_cause_of_loss[]"]').val() || '');
formData.append('first_claim_amount[]', $row.find('[name="first_claim_amount[]"]').val() || '');
formData.append('first_settled_amount[]', $row.find('[name="first_settled_amount[]"]').val() || '');
formData.append('first_claim_status[]', $row.find('[name="first_claim_status[]"]').val() || '');
}
});
}
function claimHistoryToggle() {
let claimHistoryStatus = $("#claim_history").prop("checked") ? 1 : 0;
if (claimHistoryStatus == 1) {
$(".claim-row").show();
$(".claim-input").attr("required", true);
$(".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").val("");
$(".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() {
if ($(this).hasClass('select2-hidden-accessible')) {
$(this).val(null).trigger('change');
@ -2408,7 +2680,7 @@
if (value == 1) {
$('.btnDiv').show();
$('.claim-row').hide();
updateClaimRowVisibility(fallbackPolicyTypeId);
$('.emp_title_text').text('No of Employees')
$('.depnd_title_text').text('No of Dependents')
@ -2497,7 +2769,7 @@
} else {
$('.btnDiv').hide();
$('.claim-row').show();
updateClaimRowVisibility(fallbackPolicyTypeId);
$('.emp_title_text').text('No of Employees at Inception')
$('.depnd_title_text').text(' No of Dependents at Inception')
@ -2564,27 +2836,30 @@
toggleRequiredFields();
restoreActualLeadGstNumber();
restoreActualLeadContactDetails();
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
}
function toggleRequiredFields() {
try {
const claimHistoryOn = !$('#claim_history').length || $('#claim_history').prop('checked');
$('.claim-row').each(function() {
var isRowHidden = $(this).css('display') === 'none';
var isNilClaims = isClaimRowNilClaims($(this));
$(this).find('.form-group').each(function() {
var input = $(this).find('input, select');
$(this).find('.claim-year-group .claim-input').prop('required', claimHistoryOn && !isRowHidden);
if (input.attr('name') === "gender[]") {
return;
}
$(this).find('.claim-field-group').each(function() {
var input = $(this).find('input.claim-input, select.claim-input');
if (input.length === 0) {
console.warn('No input/select fields found in:', this);
return;
}
// Remove required if row is hidden, otherwise check field visibility
input.prop('required', !isRowHidden && $(this).css('display') !== 'none');
input.prop('required', claimHistoryOn && !isRowHidden && !isNilClaims && $(this).css('display') !== 'none');
});
});
} catch (error) {
@ -2694,7 +2969,7 @@
actualLeadClientName = actual_lead_client_details.company_name || '';
$('#client_name').val(actualLeadClientName);
actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || '';
actualLeadGstNumber = (actual_lead_client_details.gst_number || actual_lead_client_details.gst || '').toString().trim().toUpperCase();
$('#gst').val(actualLeadGstNumber);
if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') {
$('#client_type').val(String(actual_lead_client_details.client_type));

View File

@ -846,12 +846,22 @@ if (isset($selected_lead_type)) {
claimHistoryToggle();
}
if (typeof initNilClaimsRows === 'function') {
initNilClaimsRows();
}
if (typeof updateClaimRowVisibility === 'function') {
updateClaimRowVisibility(policy_type_id);
}
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
if (lead_type == 1 || lead_type == 3) {
if (lead_type == 1 || !shouldShowClaimHistorySwitch(lead_type, policy_type_id)) {
if (typeof updateClaimRowVisibility === 'function') {
updateClaimRowVisibility(policy_type_id);
} else if (!shouldShowClaimHistorySwitch(lead_type, policy_type_id)) {
$('.claim-row').hide();
}
@ -948,6 +958,14 @@ if (isset($selected_lead_type)) {
claimHistoryToggle();
}
if (typeof initNilClaimsRows === 'function') {
initNilClaimsRows();
}
if (typeof updateClaimRowVisibility === 'function') {
updateClaimRowVisibility(policy_type_id);
}
if(lead_type == 3){
let incurred_claim_date_id = 'incurred_claim_date';
@ -1121,7 +1139,7 @@ if (isset($selected_lead_type)) {
let policy_type_id = data.policy_type_id || null;
let lead_type = data.lead_type || null;
leadTypeBsedHideAndShow(lead_type);
$('#lead_type').val(lead_type || '');
const isExistingClient = data.client_id !== undefined
&& data.client_id !== null
@ -1192,7 +1210,6 @@ if (isset($selected_lead_type)) {
$('#leads_primarykey').val(data.id || '');
$('#actual_lead_id').val(data.actual_lead_id || 0);
$('#policy_start_date').val(data.policy_end_date || '');
$('#lead_type').val(data.lead_type || '');
$('#issuer').val(data.issuer || '');
$('#client_type').val(data.client_type || '');
$('#client_name').val(data.client_name || '');
@ -1253,9 +1270,23 @@ if (isset($selected_lead_type)) {
selecSalsePerson(data.salse_person_id);
}
leadTypeBsedHideAndShow(lead_type, false, Boolean(data.claims_details_html));
let referenceDiv = document.getElementById('appendAreaForClaim');
referenceDiv.innerHTML = '';
referenceDiv.insertAdjacentHTML('beforeend', data.claims_details_html);
referenceDiv.insertAdjacentHTML('beforeend', data.claims_details_html || '');
if (typeof initNilClaimsRows === 'function') {
initNilClaimsRows();
}
if (typeof updateNonEbClaimRowVisibility === 'function') {
updateNonEbClaimRowVisibility(lead_type);
} else if (typeof updateClaimRowVisibility === 'function') {
updateClaimRowVisibility(data.policy_type_id);
} else if (!data.claims_details_html && typeof setupNonEbClaimDetailsSection === 'function' && shouldShowNonEbClaimHistory(lead_type)) {
setupNonEbClaimDetailsSection(lead_type);
}
console.log("wdesfgefwregfefwregffeegf",data.lead_type);
var lead_type_value = data.lead_type;

View File

@ -647,14 +647,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
console.log(response.message, 'WARNING');
}
if (lead_type != 1 && lead_type != 3) {
let html = `<hr><div class="form-row"> <div class="form-group col-md-3"><h4> Claim Details </h4></div></div>`
let referenceDiv = document.getElementById('appendAreaForClaim');
referenceDiv.insertAdjacentHTML('beforeend', html);
appendThreeYearsClaims();
}
setupNonEbClaimDetailsSection();
// Hide loader
$('.loader').fadeOut();
@ -675,6 +668,11 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
event.preventDefault();
$('.claim-row').each(function() {
const isNil = $(this).find('.nil-claims-checkbox').prop('checked');
$(this).find('.nil-claims-value').val(isNil ? '1' : '0');
});
var isValid = $('#leads_non_eb_form_id').parsley().validate();
console.log('isValid', isValid)
@ -728,6 +726,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" 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 = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
});
$('#lead_type').change(function() {
let value = $(this).val()
let value = $(this).val();
// alert(value);
if (value == 1 || value == 3) {
// alert("Function Called");
@ -840,7 +839,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
}
leadTypeBsedHideAndShow(value, true)
leadTypeBsedHideAndShow(value, true);
$('#contact_person_summary').empty();
$('#contact_person_summary').append($('<option>', {
@ -906,13 +905,72 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
})
function leadTypeBsedHideAndShow(value, resetValues = false) {
function getClaimHistoryLabel(leadType) {
return String(leadType) === '1' ? 'Mortality Claims' : 'Claims History';
}
function getClaimDetailsSectionTitle(leadType) {
return String(leadType) === '1' ? 'Mortality Claims' : 'Claim Details';
}
function shouldShowNonEbClaimHistory(leadType) {
return ['1', '2', '3'].includes(String(leadType));
}
function updateNonEbClaimRowVisibility(leadTypeOverride) {
const leadType = leadTypeOverride ?? $('#lead_type').val();
if (!shouldShowNonEbClaimHistory(leadType)) {
$('.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 setupNonEbClaimDetailsSection(leadTypeOverride) {
const leadType = leadTypeOverride ?? $('#lead_type').val();
const referenceDiv = document.getElementById('appendAreaForClaim');
if (!referenceDiv) {
return;
}
referenceDiv.innerHTML = '';
if (!shouldShowNonEbClaimHistory(leadType)) {
return;
}
const sectionTitle = getClaimDetailsSectionTitle(leadType);
const html = `<hr><div class="form-row claim-details-section-header"><div class="form-group col-md-3"><h4>${sectionTitle}</h4></div></div>`;
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 = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
} else {
$('.btnDiv').hide();
$('.claim-row').show();
if (!skipClaimSetup && shouldShowNonEbClaimHistory(value)) {
setupNonEbClaimDetailsSection(value);
}
$('.emp_title_text').text('No of Employees at Inception')
$('.depnd_title_text').text(' No of Dependents at Inception')
@ -1178,11 +1239,158 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" 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($('<option>', { value: value, text: value }));
}
$select.val(value);
if ($select.hasClass('select2-hidden-accessible')) {
$select.trigger('change');
}
}
function resetClaimYearField($yearField) {
if (!$yearField.length) {
return;
}
$yearField.find('option[value="Nil"]').remove();
if ($yearField.val() === 'Nil') {
$yearField.val('').trigger('change');
}
}
function nilClaimsToggle(checkbox) {
const $row = $(checkbox).closest('.claim-row');
const isNil = $(checkbox).prop('checked');
const $yearField = $row.find('[name="first_year[]"]');
resetClaimYearField($yearField);
$row.find('.nil-claims-value').val(isNil ? '1' : '0');
$row.toggleClass('claim-row-nil', isNil);
$row.find('.claim-field-group .claim-required-star').toggle(!isNil);
$row.find('.claim-input').not('[name="first_year[]"]').each(function() {
const $field = $(this);
if (isNil) {
if ($field.is('select')) {
setClaimSelectValue($field, 'Nil');
} else {
$field.val('Nil');
}
$field.prop('disabled', true).removeAttr('required');
if ($field.hasClass('select2-hidden-accessible')) {
$field.next('.select2-container').addClass('select2-container--disabled');
}
if ($field.parsley) {
$field.parsley().reset();
}
} else {
$field.prop('disabled', false);
if ($field.hasClass('select2-hidden-accessible')) {
$field.next('.select2-container').removeClass('select2-container--disabled');
}
if ($field.is('select')) {
$field.find('option[value="Nil"]').remove();
$field.val('').trigger('change');
} else {
$field.val('');
}
}
});
const claimHistoryOn = !$('#claim_history').length || $('#claim_history').prop('checked');
if (claimHistoryOn) {
$yearField.prop('required', true).prop('disabled', false);
if ($yearField.hasClass('select2-hidden-accessible')) {
$yearField.next('.select2-container').removeClass('select2-container--disabled');
}
if (!isNil) {
$row.find('.claim-input').not('[name="first_year[]"]').prop('required', true);
}
}
if (typeof window.refreshLeadsFormValidation === 'function') {
window.refreshLeadsFormValidation();
}
}
function initNilClaimsRows() {
$('.claim-row .nil-claims-checkbox:checked').each(function() {
nilClaimsToggle(this);
});
}
function syncClaimHistoryFieldsToFormData(formData) {
const $rows = $('.claim-row');
if (!$rows.length) {
return;
}
const fieldNames = ['first_year[]', 'nil_claims[]', 'first_policy_type_[]', 'first_date_of_loss_[]', 'first_cause_of_loss[]', 'first_claim_amount[]', 'first_settled_amount[]', 'first_claim_status[]'];
fieldNames.forEach(function(name) {
if (typeof formData.delete === 'function') {
formData.delete(name);
}
});
$rows.each(function() {
const $row = $(this);
const isNil = $row.find('.nil-claims-checkbox').prop('checked');
$row.find('.nil-claims-value').val(isNil ? '1' : '0');
formData.append('first_year[]', $row.find('[name="first_year[]"]').val() || '');
formData.append('nil_claims[]', isNil ? '1' : '0');
if (isNil) {
formData.append('first_policy_type_[]', 'Nil');
formData.append('first_date_of_loss_[]', 'Nil');
formData.append('first_cause_of_loss[]', 'Nil');
formData.append('first_claim_amount[]', 'Nil');
formData.append('first_settled_amount[]', 'Nil');
formData.append('first_claim_status[]', 'Nil');
} else {
formData.append('first_policy_type_[]', $row.find('[name="first_policy_type_[]"]').val() || '');
formData.append('first_date_of_loss_[]', $row.find('[name="first_date_of_loss_[]"]').val() || '');
formData.append('first_cause_of_loss[]', $row.find('[name="first_cause_of_loss[]"]').val() || '');
formData.append('first_claim_amount[]', $row.find('[name="first_claim_amount[]"]').val() || '');
formData.append('first_settled_amount[]', $row.find('[name="first_settled_amount[]"]').val() || '');
formData.append('first_claim_status[]', $row.find('[name="first_claim_status[]"]').val() || '');
}
});
}
function appendThreeYearsClaims() {
const leadType = $('#lead_type').val();
const claimHistoryLabel = getClaimHistoryLabel(leadType);
let claimsFields = `${!document.querySelector('#claim_history') ? `<div class="custom-control custom-switch">
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" checked />
<label class="custom-control-label" for="claim_history">Claims History</label>
<label class="custom-control-label" for="claim_history">${claimHistoryLabel}</label>
</div><br>`: ``}`
claimsFields += `
@ -1190,8 +1398,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
<div id = "claimHistoryRow" class="row claim-row">
<div class="form-group col-md-2">
<label for="first_year_${increment}">Year<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-year-group">
<label for="first_year_${increment}">Year<span class="text-danger claim-required-star">*</span></label>
<select class="form-control claim-input first_year_" id="first_year_${increment}" name="first_year[]">
<option value="">Select Year</option>
<?php foreach ($lastFiveYears as $year) {
@ -1199,28 +1407,38 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
} ?>
</select>
</div>
<div class="form-group col-md-2">
<label for="first_policy_type_${increment}">Policy Type<span class="text-danger">*</span></label>
<label>Nil Claims</label>
<input type="hidden" class="nil-claims-value" name="nil_claims[]" value="0">
<div class="custom-control custom-checkbox" style="padding-top: 8px;">
<input type="checkbox" class="custom-control-input nil-claims-checkbox" id="nil_claims_${increment}" onchange="nilClaimsToggle(this)">
<label class="custom-control-label" for="nil_claims_${increment}">Nil Claims</label>
</div>
</div>
<div class="form-group col-md-2 claim-field-group">
<label for="first_policy_type_${increment}">Policy Type<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_policy_type_${increment}" name="first_policy_type_[]">
</div>
<div class="form-group col-md-2">
<label for="first_date_of_loss_${increment}">Date of Loss<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_date_of_loss_${increment}">Date of Loss<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control loss_date claim-input" id="first_date_of_loss_${increment}" name="first_date_of_loss_[]">
</div>
<div class="form-group col-md-2">
<label for="first_cause_of_death_${increment}">Cause Of Loss <span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_cause_of_loss_${increment}">Cause Of Loss <span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_cause_of_loss_${increment}" name="first_cause_of_loss[]">
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount_${increment}">Claim Amount<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_amount_${increment}">Claim Amount<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_amount_${increment}" name="first_claim_amount[]">
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount_${increment}">Settled Amount<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_settled_amount_${increment}">Settled Amount<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_settled_amount_${increment}" name="first_settled_amount[]">
</div>
<div class="form-group col-md-2">
<label for="first_claim_status_${increment}">Claim Status<span class="text-danger">*</span></label>
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_status_${increment}">Claim Status<span class="text-danger claim-required-star">*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_status_${increment}" name="first_claim_status[]">
</div>
<div class="form-group col-md-2">
@ -1241,7 +1459,10 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
let lastRow = $(".claim-row").last();
if (isChecked) {
lastRow.show();
lastRow.find(".claim-input").attr("required", true);
lastRow.find('[name="first_year[]"]').prop("required", true);
if (!isClaimRowNilClaims(lastRow)) {
lastRow.find(".claim-input").not('[name="first_year[]"]').prop("required", true);
}
} else {
lastRow.hide();
lastRow.find(".claim-input").removeAttr("required");
@ -1274,8 +1495,31 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" 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 = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
"settled_amount": settledAmount,
"cause_of_loss": causeOfLoss,
"status": claimStatus,
"nil_claims": 0,
});
});
@ -1309,26 +1554,41 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" 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 = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
$('#lost_reason').val("");
$('#lost_reason').removeAttr('required');
}
const initialLeadType = $('#lead_type').val();
const isEditLead = String($('#actual_lead_id').val() || '0') !== '0';
if (initialLeadType && !isEditLead) {
leadTypeBsedHideAndShow(initialLeadType, false);
}
});
</script>

View File

@ -1,55 +1,85 @@
<?php if(isset($lead_edit_data)) {
$claims = !empty($lead_edit_data['fin_years_claims_array']) ? $lead_edit_data['fin_years_claims_array'] : [ ['year' => '', 'claim_amount' => '', 'status' => '', 'claim_type' => '', 'cause_of_death' => '', 'death_date' => ''] ];
<?php if (isset($lead_edit_data)) {
if (! isset($lastFiveYears) || ! is_array($lastFiveYears)) {
$year = (int) date('Y');
$month = (int) date('m');
$currentFYStart = ($month >= 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';
?>
<?php if ($showClaimsSection) { ?>
<div class="custom-control custom-switch">
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" <?= $lead_edit_data['claim_history'] == 1 ? "checked" : "" ?> />
<label class="custom-control-label" for="claim_history">Claims History</label>
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" <?= $lead_edit_data['claim_history'] == 1 ? 'checked' : '' ?> />
<label class="custom-control-label" for="claim_history"><?= $claimHistoryLabel ?></label>
</div><br>
<?php
if ($lead_edit_data['claim_history'] == 1) {
foreach ($claims as $key => $value) { ?>
foreach ($claims as $key => $value) {
$isNilClaimRow = ! empty($value['nil_claims']);
?>
<div class="row claim-row">
<div class="form-group col-md-2">
<label for="first_year">Year<span class="text-danger">*</span></label>
<select class="form-control first_year_" id="first_year" name="first_year[]">
<div class="row claim-row<?= $isNilClaimRow ? ' claim-row-nil' : '' ?>">
<div class="form-group col-md-2 claim-year-group">
<label for="first_year_<?= $key ?>">Year<span class="text-danger claim-required-star">*</span></label>
<select class="form-control first_year_ claim-input" id="first_year_<?= $key ?>" name="first_year[]">
<option value="">Select Year</option>
<?php foreach ($lastFiveYears as $year) {
$selected = ($year == $value['year']) ? 'selected' : '';
echo "<option value='$year' $selected>$year</option>";
} ?>
$selected = ($year == $value['year']) ? 'selected' : '';
echo "<option value='$year' $selected>$year</option>";
} ?>
</select>
</div>
<div class="form-group col-md-2">
<label for="first_policy_type_${increment}">Policy Type<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="first_policy_type_${increment}" name="first_policy_type_[]"
value="<?= htmlspecialchars($value['policy_type']) ?>">
<label>Nil Claims</label>
<input type="hidden" class="nil-claims-value" name="nil_claims[]" value="<?= $isNilClaimRow ? '1' : '0' ?>">
<div class="custom-control custom-checkbox" style="padding-top: 8px;">
<input type="checkbox" class="custom-control-input nil-claims-checkbox" id="nil_claims_<?= $key ?>" onchange="nilClaimsToggle(this)"<?= $isNilClaimRow ? ' checked' : '' ?>>
<label class="custom-control-label" for="nil_claims_<?= $key ?>">Nil Claims</label>
</div>
</div>
<div class="form-group col-md-2">
<label for="first_date_of_loss_${increment}">Date of Loss<span class="text-danger">*</span></label>
<input type="text" class="form-control loss_date" id="first_date_of_loss_${increment}" name="first_date_of_loss_[]"
value="<?= htmlspecialchars($value['date_of_loss']) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_policy_type_<?= $key ?>">Policy Type<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_policy_type_<?= $key ?>" name="first_policy_type_[]"
value="<?= htmlspecialchars($value['policy_type'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_cause_of_death_${increment}">Cause Of Loss <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="first_cause_of_loss_${increment}" name="first_cause_of_loss[]"
value="<?= htmlspecialchars($value['cause_of_loss']) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_date_of_loss_<?= $key ?>">Date of Loss<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control loss_date claim-input" id="first_date_of_loss_<?= $key ?>" name="first_date_of_loss_[]"
value="<?= htmlspecialchars($value['date_of_loss'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount_${increment}">Claim Amount<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="first_claim_amount_${increment}" name="first_claim_amount[]"
value="<?= htmlspecialchars($value['claim_amount']) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_cause_of_loss_<?= $key ?>">Cause Of Loss <span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_cause_of_loss_<?= $key ?>" name="first_cause_of_loss[]"
value="<?= htmlspecialchars($value['cause_of_loss'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount_${increment}">Settled Amount<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="first_settled_amount_${increment}" name="first_settled_amount[]"
value="<?= htmlspecialchars($value['settled_amount']) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_amount_<?= $key ?>">Claim Amount<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_amount_<?= $key ?>" name="first_claim_amount[]"
value="<?= htmlspecialchars($value['claim_amount'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_claim_status_${increment}">Claim Status<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="first_claim_status_${increment}" name="first_claim_status[]"
value="<?= htmlspecialchars($value['status']) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_settled_amount_<?= $key ?>">Settled Amount<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_settled_amount_<?= $key ?>" name="first_settled_amount[]"
value="<?= htmlspecialchars($value['settled_amount'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_status_<?= $key ?>">Claim Status<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_status_<?= $key ?>" name="first_claim_status[]"
value="<?= htmlspecialchars($value['status'] ?? '') ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<div class="" style="position: relative; top: 28px; float: right; text-align: end;">
@ -59,5 +89,24 @@
</div>
</div>
<?php } } ?>
<?php } ?>
<?php
}
}
?>
<script>
$(".loss_date").each(function () {
flatpickr(this, {
dateFormat: "d-m-Y",
});
});
if (typeof initNilClaimsRows === 'function') {
initNilClaimsRows();
}
if (typeof updateNonEbClaimRowVisibility === 'function') {
updateNonEbClaimRowVisibility();
}
</script>
<?php } ?>
<?php } ?>

View File

@ -42,16 +42,35 @@
<?php if(isset($lead_edit_data)) { ?>
<?php
if (! isset($lastFiveYears) || ! is_array($lastFiveYears)) {
$year = (int) date('Y');
$month = (int) date('m');
$currentFYStart = ($month >= 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';
?>
<?php if ($showClaimHistorySwitch) { ?>
<div class="custom-control custom-switch">
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" <?= $isClaimHistoryChecked ? 'checked' : '' ?>>
<label class="custom-control-label" for="claim_history">Claims History</label>
<label class="custom-control-label" for="claim_history"><?= $claimHistoryLabel ?></label>
</div><br>
<?php } ?>
<div class="form-row" id="appendAreaForClaim_1">
@ -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']);
?>
<div class="row claim-row">
<div class="row claim-row<?= $isNilClaimRow ? ' claim-row-nil' : '' ?>">
<div class="form-group col-md-2">
<label for="first_year">Year<span class="text-danger">*</span></label>
<select class="form-control first_year_ claim-input" id="first_year" name="first_year[]">
<div class="form-group col-md-2 claim-year-group">
<label for="first_year_<?= $key ?>">Year<span class="text-danger claim-required-star">*</span></label>
<select class="form-control first_year_ claim-input" id="first_year_<?= $key ?>" name="first_year[]">
<option value="">Select Year</option>
<?php foreach ($lastFiveYears as $year) {
$selected = ($year == $value['year']) ? 'selected' : '';
@ -75,54 +96,69 @@
</div>
<div class="form-group col-md-2">
<label for="emp_id">Emp ID<span class="text-danger">*</span></label>
<input type="text" class="form-control claim-input" id="emp_id" name="emp_id[]" value="<?= htmlspecialchars(isset($value['emp_id']) ? $value['emp_id'] : '-' ) ?>">
<label>Nil Claims</label>
<input type="hidden" class="nil-claims-value" name="nil_claims[]" value="<?= $isNilClaimRow ? '1' : '0' ?>">
<div class="custom-control custom-checkbox" style="padding-top: 8px;">
<input type="checkbox" class="custom-control-input nil-claims-checkbox" id="nil_claims_<?= $key ?>" onchange="nilClaimsToggle(this)"<?= $isNilClaimRow ? ' checked' : '' ?>>
<label class="custom-control-label" for="nil_claims_<?= $key ?>">Nil Claims</label>
</div>
</div>
<div class="form-group col-md-2 claim-field-group">
<label for="emp_id_<?= $key ?>">Emp ID<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="emp_id_<?= $key ?>" name="emp_id[]" value="<?= htmlspecialchars(isset($value['emp_id']) ? $value['emp_id'] : '-' ) ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="emp_name">Employee Name<span class="text-danger">*</span></label>
<input type="text" class="form-control claim-input" id="emp_name" name="emp_name[]" value="<?= htmlspecialchars(isset($value['emp_name']) ? $value['emp_name'] : '-' ) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="emp_name_<?= $key ?>">Employee Name<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="emp_name_<?= $key ?>" name="emp_name[]" value="<?= htmlspecialchars(isset($value['emp_name']) ? $value['emp_name'] : '-' ) ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="gender">Gender<span class="text-danger">*</span></label>
<select class="form-control claim-input" id="gender" name="gender[]">
<div class="form-group col-md-2 claim-field-group">
<label for="gender_<?= $key ?>">Gender<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<select class="form-control claim-input" id="gender_<?= $key ?>" name="gender[]"<?= $isNilClaimRow ? ' disabled' : '' ?>>
<option value="">Select Gender</option>
<option value="Female" <?= (isset($value['gender']) && $value['gender'] == 'Female') ? 'selected' : '' ?>>Female</option>
<option value="Male" <?= (isset($value['gender']) && $value['gender'] == 'Male') ? 'selected' : '' ?>>Male</option>
<option value="Male" <?= (isset($value['gender']) && $value['gender'] == 'Male') ? 'selected' : '' ?>>Male</option>
<?php if ($isNilClaimRow) { ?>
<option value="Nil" selected>Nil</option>
<?php } ?>
</select>
</div>
<div class="form-group col-md-2">
<label for="designation">Designation <span class="text-danger">*</span></label>
<input type="text" class="form-control claim-input" id="designation" name="designation[]" value="<?= htmlspecialchars(isset($value['designation']) ? $value['designation'] : '-' ) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="designation_<?= $key ?>">Designation <span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="designation_<?= $key ?>" name="designation[]" value="<?= htmlspecialchars(isset($value['designation']) ? $value['designation'] : '-' ) ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="sum_insured">Sum Insured <span class="text-danger">*</span></label>
<input type="text" class="form-control claim-input" id="sum_insured" name="sum_insured[]" value="<?= htmlspecialchars(isset($value['sum_insured']) ? $value['sum_insured'] : '-' ) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="sum_insured_<?= $key ?>">Sum Insured <span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="sum_insured_<?= $key ?>" name="sum_insured[]" value="<?= htmlspecialchars(isset($value['sum_insured']) ? $value['sum_insured'] : '-' ) ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_death_date">Date of Death<span class="text-danger">*</span></label>
<input type="text" class="form-control flatpickr-date claim-input" id="first_death_date" name="first_death_date[]" value="<?= htmlspecialchars($value['death_date']) ?>" autocomplete="off">
<div class="form-group col-md-2 claim-field-group">
<label for="first_death_date_<?= $key ?>">Date of Death<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control flatpickr-date claim-input" id="first_death_date_<?= $key ?>" name="first_death_date[]" value="<?= htmlspecialchars($value['death_date'] ?? '') ?>" autocomplete="off"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<div class="form-group col-md-2">
<label for="first_cause_of_death">Nature/Cause Of Death <span class="text-danger">*</span></label>
<select class="form-control claim-input" id="first_cause_of_death" name="first_cause_of_death[]">
<div class="form-group col-md-2 claim-field-group">
<label for="first_cause_of_death_<?= $key ?>">Nature/Cause Of Death <span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<select class="form-control claim-input" id="first_cause_of_death_<?= $key ?>" name="first_cause_of_death[]"<?= $isNilClaimRow ? ' disabled' : '' ?>>
<option value="">Select Cause of Death</option>
<?php foreach ($causeOfDeath as $cause => $death_value) {
$selected = ($cause == $value['cause_of_death']) ? 'selected' : '';
echo "<option value='$cause' $selected>$death_value</option>";
} ?>
<?php if ($isNilClaimRow) { ?>
<option value="Nil" selected>Nil</option>
<?php } ?>
</select>
</div>
<div class="form-group col-md-2">
<label for="first_claim_amount">Claim/Settled Amount<span class="text-danger">*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_amount" name="first_claim_amount[]" value="<?= htmlspecialchars(isset($value['claim_amount']) ? $value['claim_amount'] : ( isset($value['settled']) ? $value['settled'] : '-' )) ?>">
<div class="form-group col-md-2 claim-field-group">
<label for="first_claim_amount_<?= $key ?>">Claim/Settled Amount<span class="text-danger claim-required-star"<?= $isNilClaimRow ? ' style="display:none;"' : '' ?>>*</span></label>
<input type="text" class="form-control claim-input" id="first_claim_amount_<?= $key ?>" name="first_claim_amount[]" value="<?= htmlspecialchars(isset($value['claim_amount']) ? $value['claim_amount'] : ( isset($value['settled']) ? $value['settled'] : '-' )) ?>"<?= $isNilClaimRow ? ' disabled' : '' ?>>
</div>
<!-- <div class="form-group col-md-2">
@ -165,5 +201,9 @@ flatpickr('.flatpickr-date', {
maxDate: 'today', // Optional: disable future dates
});
if (typeof initNilClaimsRows === 'function') {
initNilClaimsRows();
}
</script>

View File

@ -39,12 +39,25 @@
<div class="card" style="margin-right: 23px;">
<div class="card-body">
<?php
$tpaActionButtons = '';
if (
!empty($ticket_data['is_tpa_api_service_enabled']) &&
empty($ticket_data['tpa_claim_push_reference_no']) &&
($ticket_data['claim_created_by'] ?? '') !== 'TPA'
) {
$tpaActionButtons .= ' <a href="#" class="btn btn-success btn-sm mr-2" onclick="manualTpaClaimPush(); return false;">Manual TPA Claim Push</a>';
}
if (!empty($ticket_data['tpa_claim_push_reference_no'])) {
$tpaActionButtons .= ' <a href="#" class="btn btn-success btn-sm mr-2" onclick="fetchTpaClaimStatus(); return false;">Fetch Claim Status</a>';
}
?>
<script>
var pageHideMainNavTitle = true;
var pageTitle = 'Claims<?php if(isset($ticket_data["tpa_claim_push_reference_no"]) && !empty($ticket_data["tpa_claim_push_reference_no"])) { echo " <a href=\"#\" class=\"btn btn-success mr-2\" onclick=\"fetchTpaClaimStatus()\">Fetch Claim Status</a>"; } ?>';
var pageTitle = 'Claims <?= $tpaActionButtons ?>';
var pageBackButton = '<a href="<?= base_url("ticket/list"); ?>" class="topbar-icon-btn" data-toggle="tooltip" data-placement="top" title="Back" aria-label="Back to list"><i class="ri-arrow-left-s-line"></i></a>';
</script>
<!-- Header Section -->
<!-- Header Section (TPA actions rendered in top navbar via pageTitle) -->
<!-- <div class="row mb-3 align-items-center justify-content-between">
<div class="col-auto">
<h4 class="mb-0">Claims</h4>
@ -56,7 +69,7 @@
<?php if (
$ticket_data['is_tpa_api_service_enabled'] == true &&
empty($ticket_data['tpa_claim_push_reference_no']) &&
empty($ticket_data['claim_number'])
$ticket_data['claim_created_by'] != 'TPA'
) : ?>
<a href="#" class="btn btn-success mr-2" onclick="manualTpaClaimPush(); return false;">
Manual TPA Claim Push

View File

@ -577,6 +577,104 @@
<script>
let GlobelExtraFields = [];
var doa;
var STATUS_SECTION_CLASSES = ['cda_ir', 'settled', 'approved', 'rejected', 'up_qdr', 'up_cnu', 'canceled', 'returned', 'payment', 'non_id'];
var statusSectionInitialValues = {};
function captureSectionInitialValues(sectionClass) {
if (!sectionClass) {
return;
}
var values = {};
$('.' + sectionClass).find('input, textarea, select').each(function() {
var el = this;
var key = el.id || el.name;
if (!key) {
return;
}
values[key] = el.type === 'file' ? '' : String($(el).val() || '');
});
statusSectionInitialValues[sectionClass] = values;
}
function captureAllStatusSectionInitialValues() {
STATUS_SECTION_CLASSES.forEach(captureSectionInitialValues);
}
function hasSectionChanged(sectionClass) {
if (!sectionClass) {
return false;
}
var initial = statusSectionInitialValues[sectionClass];
if (!initial) {
return false;
}
var changed = false;
$('.' + sectionClass).find('input, textarea, select').each(function() {
var el = this;
var key = el.id || el.name;
if (!key) {
return;
}
if (el.type === 'file') {
if (el.files && el.files.length > 0) {
changed = true;
}
return;
}
var current = String($(el).val() || '');
var original = String(initial[key] !== undefined ? initial[key] : '');
if (current !== original) {
changed = true;
}
});
return changed;
}
function updateSectionRequiredLabels(sectionClass, isRequired) {
if (!sectionClass) {
return;
}
$('.' + sectionClass).find('input, textarea, select').each(function() {
var fieldId = $(this).attr('id');
if (!fieldId || fieldId === 'approved_description') {
return;
}
var $label = $("label[for='" + fieldId + "']");
if (!$label.length) {
return;
}
if (isRequired && !$label.find('.text-danger').length) {
$label.append(' <span class="text-danger">*</span>');
} else if (!isRequired) {
$label.find('.text-danger').remove();
}
});
}
function applyConditionalRequiredForSection(sectionClass) {
if (!sectionClass) {
return;
}
if (sectionClass === 'approved' || sectionClass === 'settled') {
enforceLetterPairRules();
updateSectionRequiredLabels(sectionClass, hasSectionChanged(sectionClass));
return;
}
var changed = hasSectionChanged(sectionClass);
$('.' + sectionClass).find('input, textarea, select').each(function() {
if (this.id === 'approved_description') {
return;
}
$(this).prop('required', changed);
});
updateSectionRequiredLabels(sectionClass, changed);
}
function refreshAllStatusSectionRequired() {
STATUS_SECTION_CLASSES.forEach(applyConditionalRequiredForSection);
$('#approved_description').prop('required', false);
$("label[for='approved_description'] .text-danger").remove();
}
$(document).ready(function() {
@ -645,6 +743,8 @@
GlobelExtraFields = extraFields;
console.log("extraFields", extraFields);
claimStatusFieldChanges(extraFields);
captureAllStatusSectionInitialValues();
refreshAllStatusSectionRequired();
handleTPARequired(tpa_id_for_hid_filed);
// syncApprovedLetterInputs();
// syncSettleLetterInputs();
@ -736,24 +836,16 @@
showClass = '.payment';
}
// Show the relevant section and enable required attributes
if (showClass) {
// $(showClass).show().find('input, textarea').attr('required', true);
$(showClass).show().find('input, textarea').attr('required', true).each(function() {
var $label = $("label[for='" + $(this).attr('id') + "']");
if ($label.length && !$label.find('.text-danger').length) {
$label.append(' <span class="text-danger">*</span>');
}
});
$(showClass).show();
captureSectionInitialValues(showClass.replace('.', ''));
}
$field = $('#approved_description')
$field.prop('required', false);
var $parentDiv = $field.closest("div");
$parentDiv.find("label[for='approved_description'] .text-danger").remove();
syncApprovedLetterInputs();
syncSettleLetterInputs();
refreshAllStatusSectionRequired();
}
$('#claim_status_id').on('change', function() {
@ -837,13 +929,13 @@
const hasUrl = $.trim($urlInput.val()) !== '';
const hasFile = $fileInput[0].files && $fileInput[0].files.length > 0;
const isApprovedSectionVisible = $urlInput.closest('.approved').is(':visible');
const approvedSectionChanged = hasSectionChanged('approved');
$fileInput.prop('disabled', hasUrl);
$urlInput.prop('disabled', hasFile);
// Keep the file optional; URL remains conditionally required for approved state unless file is present.
$fileInput.prop('required', false);
$urlInput.prop('required', isApprovedSectionVisible && !hasFile);
$urlInput.prop('required', isApprovedSectionVisible && approvedSectionChanged && !hasFile);
}
function syncSettleLetterInputs() {
@ -857,12 +949,13 @@
const hasUrl = $.trim($urlInput.val()) !== '';
const hasFile = $fileInput[0].files && $fileInput[0].files.length > 0;
const isSettledSectionVisible = $urlInput.closest('.settled').is(':visible');
const settledSectionChanged = hasSectionChanged('settled');
$fileInput.prop('disabled', hasUrl);
$urlInput.prop('disabled', hasFile);
$fileInput.prop('required', false);
$urlInput.prop('required', isSettledSectionVisible && !hasFile);
$urlInput.prop('required', isSettledSectionVisible && settledSectionChanged && !hasFile);
}
function enforceLetterPairRules() {
@ -956,10 +1049,14 @@
}
var selectedClaimStatus = $('#claim_status_id').val();
if (thirdClass === 'approved' || thirdClass === 'settled') {
applyConditionalRequiredForSection(thirdClass);
return;
}
var sectionChanged = hasSectionChanged(thirdClass);
var $thirdClass = $(`.${thirdClass}`);
var hasValue = $thirdClass.find('input, textarea, select').filter(function() {
return $(this).val().trim() !== ''; // Check if at least one field is not empty
}).length > 0;
$thirdClass.each(function() {
var id = $(this).find('[id]').first().attr('id');
@ -967,27 +1064,19 @@
var $thisDiv = $(this);
var $inputs = $thisDiv.find('input, textarea, select');
// ✅ Condition 1: Check if this ID is in extraFieldsArray (required by status)
var shouldAddRequired = extrafieldsArray[selectedClaimStatus] && extrafieldsArray[selectedClaimStatus].includes(id);
// ✅ Condition 2: If any field in the class has a value, make all required
if (shouldAddRequired || hasValue) {
if (shouldAddRequired || sectionChanged) {
if ($label.length && !$label.find('.text-danger').length) {
$label.append(' <span class="text-danger">*</span>');
}
console.log("input from if ", $inputs);
// $inputs.attr('required', true);
$('.' + thirdClass).find('input, textarea,select').attr('required', true);
} else {
$label.find('.text-danger').remove();
console.log("input ", $inputs);
// $inputs.prop('required', false);
$('.' + thirdClass).find('input, textarea,select').attr('required', false);
}
});
// Re-apply URL/file pair rules after bulk required toggles.
enforceLetterPairRules();
}
@ -1034,12 +1123,12 @@
$field.prop('required', false);
var $parentDiv = $field.closest("div");
$parentDiv.find("label[for='approved_description'] .text-danger").remove();
enforceLetterPairRules();
refreshAllStatusSectionRequired();
});
$('#ticket_form_data').on('submit', function() {
enforceLetterPairRules();
refreshAllStatusSectionRequired();
});
// function addRequiredFieldSymbol(field_name) {

View File

@ -269,12 +269,12 @@ table.dataTable tbody td {
<td style="display: none;"><?php echo $row['insurer_name']; ?></td>
<td style="display: none;"><?php echo $row['tpa_name']; ?></td>
<td style="display: none;"><?php echo $row['policy_no']; ?></td>
<td style="display: none;"><?php echo isset($priorityType[$row['priority'] ?? 0]) ? $priorityType[$row['priority'] ?? 0] : ''; ?></td>
<td style="display: none;"><?php echo ($priorityType ?? [])[$row['priority'] ?? 0] ?? ''; ?></td>
<td style="display: none;"><?php echo $row['relationship']; ?></td>
<td style="display: none;"><?php echo $row['emp_mobile']; ?></td>
<td style="display: none;"><?php echo $row['emp_mail']; ?></td>
<td style="display: none;"><?php echo $row['emp_personal_mail']; ?></td>
<td style="display: none;"><?php echo isset($modeOFIntimate[$row['mode_of_intimation'] ?? '']) ? $modeOFIntimate[$row['mode_of_intimation']] : ''; ?></td>
<td style="display: none;"><?php echo ($modeOFIntimate ?? [])[$row['mode_of_intimation'] ?? ''] ?? ''; ?></td>
<td style="display: none;">
<?php
$typeId = $row['ticket_type_id'] ?? 0;
@ -393,7 +393,8 @@ function getClaimSourceBadgeHtml(claimCreatedBy) {
if (!cls) {
return '';
}
return '<span class="claim-source-badge ' + cls + '">' + escapeHtml(key) + '</span>';
var label = key === 'CRM' ? 'STAFF' : key;
return '<span class="claim-source-badge ' + cls + '">' + escapeHtml(label) + '</span>';
}
function getPolicyTypeIconHtml(tpaClaimType) {

View File

@ -29,7 +29,44 @@
}
function isSkippableField(el) {
return !el || el.disabled || el.type === 'hidden';
if (!el || el.disabled || el.type === 'hidden') {
return true;
}
if (el.classList && el.classList.contains('select2-search__field')) {
return true;
}
return false;
}
function isVisibleForValidation(el) {
if (isSkippableField(el)) {
return false;
}
var jq = getJq();
if (!jq) {
return true;
}
var $el = jq(el);
if (!$el.length || !$el.is(':visible')) {
return false;
}
return $el.closest('.form-group, .dynamic-form-row, .claim-row, .col-md-4, .col-md-12').filter(function () {
var display = jq(this).css('display');
return display === 'none';
}).length === 0;
}
function isEmptyValue(val) {
if (val == null || val === '') {
return true;
}
if (Array.isArray(val)) {
return val.length === 0;
}
return String(val).trim() === '';
}
function isPanOrGstField(el) {
@ -95,7 +132,7 @@
return true;
}
if (instance.$element.prop('required') && v.length === 0) {
return true;
return false;
}
return v.length === 10;
},
@ -181,7 +218,7 @@
}
function validateField(field) {
if (!field || isSkippableField(field) || !isParsleyReady()) {
if (!field || !isVisibleForValidation(field) || !isParsleyReady()) {
return;
}
var $field = $(field);
@ -200,6 +237,70 @@
}
}
function prepareHiddenFieldsForSubmit($form) {
if (!$form || !$form.length) {
return;
}
$form.find('input, textarea, select').each(function () {
var el = this;
var $el = $(el);
if (isVisibleForValidation(el)) {
return;
}
$el.removeAttr('required').prop('required', false);
clearAttrs($el);
try {
if (typeof $el.parsley === 'function') {
var fieldInstance = $el.parsley();
if (fieldInstance && typeof fieldInstance.reset === 'function') {
fieldInstance.reset();
}
}
} catch (e) {
// no-op
}
});
}
function getInvalidFields($form) {
var invalid = [];
if (!$form || !$form.length || !isParsleyReady()) {
return invalid;
}
var instance = $form.parsley();
if (!instance) {
return invalid;
}
instance.fields.forEach(function (field) {
if (field.isValid()) {
return;
}
var el = field.$element[0];
if (!isVisibleForValidation(el)) {
return;
}
var val = field.$element.val();
invalid.push({
id: field.$element.attr('id') || '',
name: field.$element.attr('name') || '',
label: (field.$element.closest('.form-group, .mb-3, .col-md-4, .col-lg-4').find('label').first().text() || '').trim(),
messages: field.getErrorsMessages ? field.getErrorsMessages() : [],
isEmpty: isEmptyValue(val)
});
});
return invalid;
}
function init() {
var $form = $(FORM_SELECTOR);
if (!$form.length || !isParsleyReady()) {
@ -207,6 +308,7 @@
}
registerValidators();
prepareHiddenFieldsForSubmit($form);
applyConstraints($form);
refreshParsley($form);
@ -226,6 +328,22 @@
window.refreshLeadsFormValidation = function () {
init();
};
window.prepareLeadsFormForSubmit = function () {
var $form = $(FORM_SELECTOR);
if (!$form.length || !isParsleyReady()) {
return;
}
registerValidators();
prepareHiddenFieldsForSubmit($form);
applyConstraints($form);
refreshParsley($form);
};
window.getLeadsFormInvalidFields = function () {
return getInvalidFields($(FORM_SELECTOR));
};
})(function () {
return window.jQuery;
});