Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
01d83a1f3e
@ -44,7 +44,7 @@ class Acl
|
||||
'#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
|
||||
'#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
|
||||
'#^/viewClaimFile#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID]],
|
||||
|
||||
'#^/employee/tpaReportsDashboard#' => ['roles' => [ACCOUNT_MANAGER_ROLE_ID]],
|
||||
|
||||
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ $routes->get('/fedeploy', 'DeployController::fedeploy_view', ['filter' => 'authM
|
||||
$routes->post('/fedeploy', 'DeployController::fedeploy', ['filter' => 'authMVC']);
|
||||
$routes->get('/visitOffBoardCheck', 'EmployeeController::visitOffBoardCheck');
|
||||
$routes->get('/metaDashboardDemo', 'TestingController::metaDashboardDemo');
|
||||
$routes->get('/apacheSuperSetDemo', 'TestingController::apacheSuperSetDemo');
|
||||
$routes->get('/metaTpaDashboardDemo', 'TestingController::metaTpaDashboardDemo');
|
||||
|
||||
// Reminder Mail Notification
|
||||
@ -49,6 +50,7 @@ $routes->get("updatajson", "EmpDataServiceController::updatajson");
|
||||
$routes->get("view", "EmployeeController::viewECard/$1");
|
||||
$routes->get("checkWellnessOnboardStatus/(:any)", "EmployeeController::checkWellnessOnboardStatus/$1");
|
||||
$routes->get("initiateWellnessOnboard/(:any)", "EmployeeController::initiateWellnessOnboard/$1");
|
||||
$routes->get("resetWellnessOnboard/(:any)", "EmployeeController::resetWellnessOnboard/$1");
|
||||
$routes->get("exportQCRandRFQ/(:any)", "LeadsController::exportQCRandRFQ/$1");
|
||||
$routes->get("testMailAttachments", "ClientController::testMailAttachments");
|
||||
$routes->get("updatePolicyTermsKey", "ClientController::updatePolicyTermsKey");
|
||||
@ -136,6 +138,7 @@ $routes->group("/client", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get('view_deposit/(:num)', 'ClientController::view_Deposit/$1');
|
||||
$routes->get('createtransaction', 'ClientController::createtransaction');
|
||||
$routes->post('save_deposit', 'ClientController::saveDeposit');
|
||||
$routes->post('update_deposit_amount', 'ClientController::updateDepositAmount');
|
||||
$routes->post("saveApiData", "ClientController::saveApiData");
|
||||
$routes->get("generateToken","ClientController::sendToken");
|
||||
// $routes->get('view_Deposit/(:num)/(:num)','ClientController/view_Deposit/$1/$2');
|
||||
@ -239,7 +242,7 @@ $routes->group("/employee", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get("bulkGenerateEcardAndStoreinS3", "EmployeeController::bulkGenerateEcardAndStoreinS3");
|
||||
$routes->get('clearCdSession', 'EmployeeController::clearCdSession');
|
||||
$routes->get('checkSessionStatus', 'EmployeeController::checkSessionStatus');
|
||||
|
||||
$routes->get("tpaReportsDashboard", "EmployeeController::tpaReportsDashboard");
|
||||
});
|
||||
|
||||
|
||||
@ -459,6 +462,15 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get('insertSampleTpaApiData/(:any)', 'TestingController::insertSampleTpaApiData/$1');
|
||||
$routes->get('listEmployeeCountByClientPolicy', 'TestingController::listEmployeeCountByClientPolicy');
|
||||
$routes->get('testMediAssistWellness','TestingController::testMediAssistWellness');
|
||||
|
||||
$routes->group('claims-collection-v2', static function ($routes) {
|
||||
$routes->get('preview', 'ClaimsCollectionV2DashboardController::preview');
|
||||
$routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1');
|
||||
$routes->get('kpi/(:segment)', 'ClaimsCollectionV2DashboardController::kpi/$1');
|
||||
$routes->get('all', 'ClaimsCollectionV2DashboardController::all');
|
||||
$routes->get('debug', 'ClaimsCollectionV2DashboardController::debug');
|
||||
$routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1');
|
||||
});
|
||||
});
|
||||
|
||||
$routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
|
||||
@ -766,6 +778,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel
|
||||
$routes->get("downloadPolicyFiles", "EmployeeRestController::downloadPolicyFiles");
|
||||
$routes->post("bulkEcardDownloadAsZip", "EmployeeRestController::bulkEcardDownloadAsZip");
|
||||
$routes->get("downloadSampleExcel/(:any)", "EmployeeController::downloadSampleExcelFile/$1");
|
||||
|
||||
$routes->group('claims-collection-v2', static function ($routes) {
|
||||
$routes->get('preview', 'ClaimsCollectionV2DashboardController::preview');
|
||||
$routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1');
|
||||
$routes->get('kpi/(:segment)', 'ClaimsCollectionV2DashboardController::kpi/$1');
|
||||
$routes->get('all', 'ClaimsCollectionV2DashboardController::all');
|
||||
$routes->get('debug', 'ClaimsCollectionV2DashboardController::debug');
|
||||
$routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1');
|
||||
});
|
||||
});
|
||||
|
||||
$routes->post("bulkEcardDownloadAsZip", "EmployeeRestController::bulkEcardDownloadAsZip");
|
||||
|
||||
166
app/Controllers/ClaimsCollectionV2DashboardController.php
Normal file
166
app/Controllers/ClaimsCollectionV2DashboardController.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ClaimsCollectionV2DashboardModel;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
/**
|
||||
* Claims Collection V2 KPI API (Metabase SQL as model methods).
|
||||
*/
|
||||
class ClaimsCollectionV2DashboardController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
/**
|
||||
* Resolve policy id from request; optional $fallback (e.g. route default for debug only).
|
||||
*/
|
||||
protected function resolvePolicyId(?int $fallback = null): int
|
||||
{
|
||||
$id = (int) (
|
||||
$this->request->getGet('client_policy')
|
||||
?? $this->request->getGet('client_policy_id')
|
||||
?? $this->request->getPost('client_policy')
|
||||
?? $this->request->getPost('client_policy_id')
|
||||
?? 0
|
||||
);
|
||||
|
||||
if ($id > 0) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ($fallback !== null && $fallback > 0) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve KPI by Metabase numeric id or method slug.
|
||||
*/
|
||||
protected function resolveKpiMethod(string $kpiKey): ?string
|
||||
{
|
||||
$kpiKey = trim($kpiKey);
|
||||
if ($kpiKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($kpiKey, ClaimsCollectionV2DashboardModel::KPI_MAP, true)) {
|
||||
return $kpiKey;
|
||||
}
|
||||
|
||||
if (ctype_digit($kpiKey)) {
|
||||
$id = (int) $kpiKey;
|
||||
return ClaimsCollectionV2DashboardModel::KPI_MAP[$id] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON: single KPI by slug (e.g. incurred_ratio) or Metabase id (e.g. 207).
|
||||
* Requires client_policy or client_policy_id.
|
||||
*/
|
||||
public function kpi(string $kpiMethod = '')
|
||||
{
|
||||
$policyId = $this->resolvePolicyId();
|
||||
|
||||
if ($policyId <= 0) {
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'client_policy or client_policy_id is required.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$kpiMethod = $this->resolveKpiMethod($kpiMethod);
|
||||
if ($kpiMethod === null) {
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'Unknown KPI. Pass Metabase id or method slug.',
|
||||
'allowed' => ClaimsCollectionV2DashboardModel::KPI_MAP,
|
||||
], 404);
|
||||
}
|
||||
|
||||
$model = new ClaimsCollectionV2DashboardModel();
|
||||
$metabaseId = array_search($kpiMethod, ClaimsCollectionV2DashboardModel::KPI_MAP, true);
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'policy_id' => $policyId,
|
||||
'kpi_id' => $metabaseId !== false ? (int) $metabaseId : null,
|
||||
'kpi' => $kpiMethod,
|
||||
'label' => ClaimsCollectionV2DashboardModel::KPI_LABELS[$kpiMethod] ?? $kpiMethod,
|
||||
'rows' => $model->getKpi($kpiMethod, $policyId),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON: all KPIs. Requires client_policy or client_policy_id.
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$policyId = $this->resolvePolicyId();
|
||||
|
||||
if ($policyId <= 0) {
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'client_policy or client_policy_id is required.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$model = new ClaimsCollectionV2DashboardModel();
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'policy_id' => $policyId,
|
||||
'data' => $model->getAllKpis($policyId),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin preview UI (authMVC only) — KPI grid for manual testing.
|
||||
* Default policy id only in method signature; override via query or /preview/{id}.
|
||||
*/
|
||||
public function preview(int $policyId = 4687)
|
||||
{
|
||||
$policyId = $this->resolvePolicyId($policyId);
|
||||
$path = $this->request->getUri()->getPath();
|
||||
$isJwt = stripos($path, 'employeeRest') !== false;
|
||||
$prefix = $isJwt ? 'employeeRest/claims-collection-v2' : 'util/claims-collection-v2';
|
||||
|
||||
return view('claims_collection_v2_dashboard', [
|
||||
'policy_id' => $policyId,
|
||||
'kpi_map' => ClaimsCollectionV2DashboardModel::KPI_MAP,
|
||||
'kpi_labels' => ClaimsCollectionV2DashboardModel::KPI_LABELS,
|
||||
'api_all_url' => base_url($prefix . '/all'),
|
||||
'api_kpi_url' => base_url($prefix . '/kpi'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin check only: raw JSON on screen (no dashboard UI).
|
||||
* Default policy id only here; override via ?client_policy= or /debug/{id}.
|
||||
*/
|
||||
public function debug(int $policyId = 4687)
|
||||
{
|
||||
$policyId = $this->resolvePolicyId($policyId);
|
||||
|
||||
if ($policyId <= 0) {
|
||||
return $this->response
|
||||
->setStatusCode(422)
|
||||
->setBody('client_policy or client_policy_id is required.');
|
||||
}
|
||||
|
||||
$model = new ClaimsCollectionV2DashboardModel();
|
||||
$body = json_encode([
|
||||
'status' => true,
|
||||
'policy_id' => $policyId,
|
||||
'data' => $model->getAllKpis($policyId),
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/json; charset=UTF-8')
|
||||
->setBody($body);
|
||||
}
|
||||
}
|
||||
@ -902,6 +902,109 @@ class ClientController extends AdminController
|
||||
return $this->response->setJSON(['success' => $response['success']]);
|
||||
}
|
||||
|
||||
public function updateDepositAmount()
|
||||
{
|
||||
$rules = [
|
||||
'deposit_id' => 'required|is_natural_no_zero',
|
||||
'amount' => 'required|numeric|greater_than_equal_to[0]',
|
||||
'transaction_type' => 'required|in_list[Credit,Debit]',
|
||||
'client_id' => 'required',
|
||||
'insurer_id' => 'required|is_natural_no_zero',
|
||||
'cd_ac_pk' => 'required|is_natural_no_zero',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return $this->response
|
||||
->setStatusCode(400)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Input validation failed',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
]);
|
||||
}
|
||||
|
||||
$postData = sanitizeInputArrayAdvanced($this->request->getPost());
|
||||
$depositId = (int) ($postData['deposit_id'] ?? 0);
|
||||
$clientId = $postData['client_id'] ?? null;
|
||||
$insurerId = (int) ($postData['insurer_id'] ?? 0);
|
||||
$cdAcPk = (int) ($postData['cd_ac_pk'] ?? 0);
|
||||
$amount = (float) ($postData['amount'] ?? 0);
|
||||
$transactionType = $postData['transaction_type'] ?? 'Credit';
|
||||
$loggedInUserID = get_session_userid();
|
||||
|
||||
$depositModel = new ClientDepositModel();
|
||||
|
||||
$targetRow = $depositModel
|
||||
->where('id', $depositId)
|
||||
->where('client_id', $clientId)
|
||||
->where('insurer_id', $insurerId)
|
||||
->where('cd_ac_pk', $cdAcPk)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (empty($targetRow)) {
|
||||
return $this->response
|
||||
->setStatusCode(404)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Transaction not found for this account.',
|
||||
]);
|
||||
}
|
||||
|
||||
$existingType = $targetRow['transaction_type'] ?? '';
|
||||
if ($existingType !== $transactionType) {
|
||||
return $this->response
|
||||
->setStatusCode(400)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Only ' . $existingType . ' value can be edited for this transaction.',
|
||||
]);
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transBegin();
|
||||
|
||||
$depositModel->update($depositId, [
|
||||
'amount' => $amount,
|
||||
'transaction_type' => $transactionType,
|
||||
'updated_by' => $loggedInUserID,
|
||||
]);
|
||||
|
||||
$rows = $depositModel
|
||||
->where('client_id', $clientId)
|
||||
->where('insurer_id', $insurerId)
|
||||
->where('cd_ac_pk', $cdAcPk)
|
||||
->where('is_active', 1)
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$runningBalance = 0.0;
|
||||
foreach ($rows as $row) {
|
||||
$rowAmount = (float) ($row['amount'] ?? 0);
|
||||
$runningBalance += (($row['transaction_type'] ?? '') === 'Credit') ? $rowAmount : -$rowAmount;
|
||||
$depositModel->update((int) $row['id'], [
|
||||
'balance' => $runningBalance,
|
||||
'updated_by' => $loggedInUserID,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($db->transStatus() === false) {
|
||||
$db->transRollback();
|
||||
return $this->response
|
||||
->setStatusCode(500)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to update transaction.',
|
||||
]);
|
||||
}
|
||||
|
||||
$db->transCommit();
|
||||
return $this->response->setJSON([
|
||||
'status' => 'success',
|
||||
'message' => 'Transaction updated successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function editClientOnboarding($id = null)
|
||||
{
|
||||
|
||||
@ -6652,7 +6755,7 @@ class ClientController extends AdminController
|
||||
// Check in Enrollment (client_policy)
|
||||
$client_policy_data = $this->clientPolicyModel
|
||||
->where('is_active', 1)
|
||||
->where('policy_status', 1)
|
||||
// ->where('policy_status', 1)
|
||||
->where('TRIM(policy_no)', $policy_no)
|
||||
->first();
|
||||
|
||||
@ -7312,6 +7415,8 @@ class ClientController extends AdminController
|
||||
// dd($res);
|
||||
|
||||
$employeeController = new EmployeeController();
|
||||
// $payload = json_decode('{"client_policy_id":"6272"}', true);
|
||||
// $response = $employeeController->initiateWellnessOnboardJob($payload); dd($response);
|
||||
// $response = $employeeController->getEmployeeEcardFromTmpFolderAndZipToS3(json_decode('{"batch_no":2,"last_emp_policy_id":"13218","folder_name":"bulk_ecards_IOCL-77448855996699885555_2026-02-05_09-32-22","processed_in_this_batch_data_count":7,"pdf_count":0,"hr_id":"1"}', true));
|
||||
// $response = $employeeController->bulkEcardDownloadAsZipFromS3(json_decode('{"client_policy_id":"6066","hr_id":"1"}', true));
|
||||
// dd($response);
|
||||
|
||||
@ -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'],
|
||||
],
|
||||
|
||||
|
||||
@ -55,6 +55,8 @@ use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
use Kint;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
class EmployeeController extends AdminController
|
||||
{
|
||||
|
||||
@ -896,7 +898,7 @@ class EmployeeController extends AdminController
|
||||
session()->setFlashdata('success', 'File Uploaded Successfully File is being validated.');
|
||||
return redirect()->to(base_url('employee/upload'));
|
||||
} else {
|
||||
session()->setFlashdata($return['status'], $return['message']);
|
||||
session()->setFlashdata($return['status'] ?? 'error', $return['message'] ?? 'Something went wrong! Try Later');
|
||||
return redirect()->to(base_url('employee/upload'));
|
||||
}
|
||||
} else if ($event_type == 'correction') {
|
||||
@ -3622,7 +3624,22 @@ class EmployeeController extends AdminController
|
||||
|
||||
|
||||
public function checkWellnessOnboardStatus($client_policy_id)
|
||||
{
|
||||
{
|
||||
|
||||
|
||||
$fetch_onboarded_employees = $this->request->getGet('fetch_onboarded_employees') ?? false;
|
||||
|
||||
if($fetch_onboarded_employees){
|
||||
$data = $this->employeePolicyModel->select('employee_polices.*')
|
||||
->where('employee_polices.is_active', 1)
|
||||
->where('employee_polices.status', 'active')
|
||||
->where('employee_polices.client_policy_id', $client_policy_id)
|
||||
->where('employee_polices.wellness_onboard !=', '0')
|
||||
->findAll();
|
||||
return $this->respond(['status' => true, 'code' => 200, 'data' => count($data)], 200);
|
||||
}
|
||||
|
||||
|
||||
// echo $client_policy_id;die();
|
||||
$data = $this->employeePolicyModel->select('employee_polices.*,emp.name,emp.relationship,emp.emp_code,emp.name,emp.email_corporate,emp.mobile,emp.dob,cp.policy_no,cp.wellness_plan_id,cp.wellness_vendor_id,cp.policy_start_date as cp_policy_start_date,cp.policy_end_date,cls.short_name')
|
||||
->join('client_policy cp', 'cp.id = employee_polices.client_policy_id')
|
||||
@ -3656,9 +3673,9 @@ class EmployeeController extends AdminController
|
||||
// return $this->respond(['status' => true, 'code' => 200, 'message' => 'Inception and Member data comparision skiped successfully!'], 200);
|
||||
}
|
||||
|
||||
public function initiateWellnessOnboard($client_policy_id)
|
||||
public function initiateWellnessOnboard($client_policy_id, $emp_code = null)
|
||||
{
|
||||
$r = Jobs::addJob(['job_name' => 'initiateWellnessOnboardJob','payload' => ['client_policy_id' => $client_policy_id]]);
|
||||
$r = Jobs::addJob(['job_name' => 'initiateWellnessOnboardJob','payload' => ['client_policy_id' => $client_policy_id, 'emp_code' => $emp_code ?? null ]]);
|
||||
// $this->initiateWellnessOnboardJob(['client_policy_id' => $client_policy_id]);
|
||||
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Process started'], 200);
|
||||
}
|
||||
@ -3774,6 +3791,7 @@ class EmployeeController extends AdminController
|
||||
public function initiateWellnessOnboardJob($arr)
|
||||
{
|
||||
$client_policy_id = $arr['client_policy_id'] ?? null;
|
||||
$emp_code = $arr['emp_code'] ?? null;
|
||||
|
||||
if (!$client_policy_id) {
|
||||
log_message('error', '[WellnessOnboard] Missing client_policy_id in payload');
|
||||
@ -3789,7 +3807,7 @@ class EmployeeController extends AdminController
|
||||
// ----------------------------------------------------------------
|
||||
// 1. FETCH DATA
|
||||
// ----------------------------------------------------------------
|
||||
$data = $this->employeePolicyModel
|
||||
$query = $this->employeePolicyModel
|
||||
->select('employee_polices.*,
|
||||
emp.name, emp.relationship, emp.emp_code, emp.email_corporate,
|
||||
emp.mobile, emp.dob, emp.gender,
|
||||
@ -3817,8 +3835,13 @@ class EmployeeController extends AdminController
|
||||
->orWhere('cp.wellness_vendor_id =', 0)
|
||||
->groupEnd()
|
||||
->where('cp.policy_status', 1)
|
||||
->where('cp.is_active', 1)
|
||||
->findAll($perPage, $offset);
|
||||
->where('cp.is_active', 1);
|
||||
|
||||
if(!empty($emp_code)){
|
||||
$query->where('emp.emp_code', trim($emp_code));
|
||||
}
|
||||
|
||||
$data = $query->findAll($perPage, $offset);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 2. NO DATA FOUND
|
||||
@ -3852,7 +3875,7 @@ class EmployeeController extends AdminController
|
||||
foreach ($families as $empCode => $members) {
|
||||
$familiesPayload[$empCode] = $this->buildFamilyPayload($empCode, $members);
|
||||
}
|
||||
log_message('error', "[WellnessOnboard] Page {$page} API calls done. Families processed: " . json_encode($familiesPayload));
|
||||
log_message('error', "[WellnessOnboard] Page {$page} API calls done. Families processed: " . json_encode($familiesPayload));
|
||||
// ----------------------------------------------------------------
|
||||
// 5. SEND TO WELLNESS API
|
||||
// ----------------------------------------------------------------
|
||||
@ -3885,6 +3908,33 @@ class EmployeeController extends AdminController
|
||||
return true;
|
||||
}
|
||||
|
||||
public function resetWellnessOnboard($client_policy_id, $emp_code = null)
|
||||
{
|
||||
if(!empty($emp_code)){
|
||||
|
||||
$employees = db_connect()->table('employees')
|
||||
->select('id')
|
||||
->where('emp_code', trim($emp_code))
|
||||
->get()
|
||||
->getResult();
|
||||
|
||||
if (!empty($employees)) {
|
||||
$employeeIds = array_column($employees, 'id');
|
||||
|
||||
$this->employeePolicyModel
|
||||
->where('client_policy_id', $client_policy_id)
|
||||
->whereIn('employee_id', $employeeIds)
|
||||
->set('wellness_onboard', '0')
|
||||
->update();
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->employeePolicyModel->where('client_policy_id', $client_policy_id)->set('wellness_onboard', '0')->update();
|
||||
}
|
||||
|
||||
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Wellness onboard resetted successfully'], 200);
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ------------------ FUNCTION TO BUILD FAMILY PAYLOAD ------------------
|
||||
@ -4174,6 +4224,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');
|
||||
@ -7290,4 +7341,75 @@ class EmployeeController extends AdminController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function tpaReportsDashboard()
|
||||
{
|
||||
$METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY');
|
||||
$policy_id = $this->request->getGet('client_policy') ?? null;
|
||||
|
||||
if ($this->request->getGet('api') != 1) {
|
||||
$data = [
|
||||
'tab_name' => 'TPA Reports',
|
||||
'page_name' => 'TPA Reports',
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
'dashboardApiUrl' => base_url('/employee/tpaReportsDashboard'),
|
||||
];
|
||||
|
||||
return $this->loadLayout('meta_dashboard_demo_one', $data);
|
||||
}
|
||||
|
||||
if (empty($policy_id)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Client policy is required.',
|
||||
'data' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
$client_policy_data = $this->clientPolicyModel
|
||||
->select('tpa.dashboard_id')
|
||||
->join('tpa', 'client_policy.tpa_id = tpa.id')
|
||||
->where('client_policy.id', $policy_id)
|
||||
->first();
|
||||
|
||||
$database_id = isset($client_policy_data['dashboard_id']) ? (int) $client_policy_data['dashboard_id'] : null;
|
||||
|
||||
if (empty($database_id)) {
|
||||
|
||||
if ($this->request->getGet('api') == 1) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'There is no dashboard for this TPA.',
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
return view('errors/404', [
|
||||
'message' => 'There is no dashboard for this TPA.'
|
||||
]);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'resource' => [
|
||||
'dashboard' => $database_id
|
||||
],
|
||||
'exp' => time() + (10 * 60), // 10 minutes
|
||||
'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase
|
||||
|
||||
];
|
||||
|
||||
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
|
||||
// dd($token);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Form data received successfully!',
|
||||
'data' => [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -2288,6 +2288,8 @@ class LeadsController extends BaseController
|
||||
// 'Insurer' => $rfq_data['insurer_name'] ?? " - ",
|
||||
// 'TPA' => $rfq_data['tpa_name'] ?? " - ",
|
||||
'Policy Type' => $this->leadType[$rfq_data['lead_type']] ?? "",
|
||||
'Mortality Experience for last 3 years' => $claim_history == "1" ? "Mentioned in Claims sheet" : "Nil",
|
||||
|
||||
];
|
||||
}else {
|
||||
|
||||
|
||||
@ -365,7 +365,7 @@ class TestingController extends BaseController
|
||||
$insurerBranchModel = new InsurerBranchModel();
|
||||
$insurer_branch = $insurerBranchModel->getInsurerBranchesWithInsurerNames();
|
||||
|
||||
$dataArray = $data['pt_co_share_details']; // Example
|
||||
$dataArray = $data['pt_co_share_details'] ?? []; // Example
|
||||
$cd_ac_pk = 'CD12345';
|
||||
$role_id = 1;
|
||||
$team_id = ['6'];
|
||||
@ -1099,6 +1099,64 @@ class TestingController extends BaseController
|
||||
public function metaDashboardDemo()
|
||||
{
|
||||
|
||||
// 🔐 Move this to .env in real projects
|
||||
$METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY');
|
||||
$database_id = (int) ($this->request->getGet('database_id') ?? 2);
|
||||
$policy_id = $this->request->getGet('client_policy') ?? null;
|
||||
$payload = [
|
||||
'resource' => [
|
||||
// 'dashboard' => 1
|
||||
'dashboard' => $database_id
|
||||
],
|
||||
'exp' => time() + (10 * 60), // 10 minutes
|
||||
];
|
||||
|
||||
if(!empty($policy_id)){
|
||||
$payload['params'] = (object)['client_policy' => $policy_id]; // MUST be object for Metabase
|
||||
}else{
|
||||
$payload['params'] = (object)[]; // MUST be object for Metabase
|
||||
}
|
||||
|
||||
// dd($payload);
|
||||
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
|
||||
|
||||
// // You can either return token only
|
||||
// return $this->response->setJSON([
|
||||
// 'token' => $token,
|
||||
// 'iframe_url' => "https://your-metabase-domain/embed/dashboard/{$token}#bordered=true&titled=true"
|
||||
// ]);
|
||||
if ($this->request->getGet('api') == 1) {
|
||||
if (empty($policy_id)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Client policy is required.',
|
||||
'data' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Dashboard token generated.',
|
||||
'data' => [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'tab_name' => 'Metabase Dashboard',
|
||||
'page_name' => 'Metabase Dashboard',
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
'dashboardApiUrl' => base_url('/metaDashboardDemo'),
|
||||
];
|
||||
|
||||
return $this->loadLayout('meta_dashboard_demo_one', $data);
|
||||
}
|
||||
public function apacheSuperSetDemo()
|
||||
{
|
||||
|
||||
// echo 'Dta';die;
|
||||
// 🔐 Move this to .env in real projects
|
||||
$METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY');
|
||||
$database_id = (int)$this->request->getGet('database_id') ?? 2;
|
||||
@ -1108,7 +1166,7 @@ class TestingController extends BaseController
|
||||
$payload = [
|
||||
'resource' => [
|
||||
// 'dashboard' => 1
|
||||
'dashboard' => $database_id
|
||||
'dashboard' => 2
|
||||
],
|
||||
'exp' => time() + (10 * 60), // 10 minutes
|
||||
];
|
||||
@ -1138,16 +1196,11 @@ class TestingController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
return view('meta_dashboard_demo_one', [
|
||||
return view('apache_dashboard_demo_one', [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testingquerys1(){
|
||||
|
||||
}
|
||||
|
||||
public function testingquerys()
|
||||
{
|
||||
$calendar = new \App\Libraries\GoogleCalendarService();
|
||||
@ -1175,7 +1228,26 @@ class TestingController extends BaseController
|
||||
public function metaTpaDashboardDemo()
|
||||
{
|
||||
$METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY');
|
||||
$policy_id = $this->request->getGet('client_policy') ?? 4687;
|
||||
$policy_id = $this->request->getGet('client_policy') ?? null;
|
||||
|
||||
if ($this->request->getGet('api') != 1) {
|
||||
$data = [
|
||||
'tab_name' => 'TPA Reports',
|
||||
'page_name' => 'TPA Reports',
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
'dashboardApiUrl' => base_url('/metaTpaDashboardDemo'),
|
||||
];
|
||||
|
||||
return $this->loadLayout('meta_dashboard_demo_one', $data);
|
||||
}
|
||||
|
||||
if (empty($policy_id)) {
|
||||
return $this->respond([
|
||||
'status' => 'failed',
|
||||
'message' => 'Client policy is required.',
|
||||
'data' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
$client_policy_data = $this->ClientPolicyModel
|
||||
->select('tpa.dashboard_id')
|
||||
@ -1212,20 +1284,13 @@ class TestingController extends BaseController
|
||||
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
|
||||
// dd($token);
|
||||
|
||||
if ($this->request->getGet('api') == 1) {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Form data received successfully!',
|
||||
'data' => [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return view('meta_dashboard_demo_one', [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Form data received successfully!',
|
||||
'data' => [
|
||||
'metabaseToken' => $token,
|
||||
'metabaseUrl' => 'https://nsights.nhanceindia.in',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -968,6 +968,58 @@ class TicketController extends BaseController
|
||||
return $extra_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay conditional `required` rules onto a base validation rule-set based on the
|
||||
* submitted `claim_status_id`. For example, when claim_status_id is a "Settled" status
|
||||
* (11/24/34/44), fields like `utr_details` and `settled_date` are promoted from
|
||||
* `permit_empty` to `required` so the request is rejected when they are blank.
|
||||
*
|
||||
* Letter URL fields (`approved_letter`, `settle_letter`) accept an alternative PDF
|
||||
* upload, so they are intentionally not promoted here; that pairing is enforced by
|
||||
* the upload validation flow. `approved_description` stays optional to match the
|
||||
* existing UI contract.
|
||||
*/
|
||||
private function applyClaimStatusConditionalRules(array $rules): array
|
||||
{
|
||||
$claimStatusId = (int) $this->request->getPost('claim_status_id');
|
||||
if ($claimStatusId <= 0) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if (!isset($this->extraFields[$claimStatusId]) || !is_array($this->extraFields[$claimStatusId])) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
$requiredFields = $this->extraFields[$claimStatusId];
|
||||
$skipFields = ['approved_letter', 'settle_letter', 'approved_description'];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (in_array($field, $skipFields, true) || !isset($rules[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingRules = $rules[$field]['rules'] ?? '';
|
||||
$parts = $existingRules === ''
|
||||
? []
|
||||
: array_values(array_filter(array_map('trim', explode('|', $existingRules)), function ($part) {
|
||||
return $part !== '' && $part !== 'permit_empty';
|
||||
}));
|
||||
|
||||
if (!in_array('required', $parts, true)) {
|
||||
array_unshift($parts, 'required');
|
||||
}
|
||||
$rules[$field]['rules'] = implode('|', array_unique($parts));
|
||||
|
||||
if (!isset($rules[$field]['errors']) || !is_array($rules[$field]['errors'])) {
|
||||
$rules[$field]['errors'] = [];
|
||||
}
|
||||
$label = $rules[$field]['label'] ?? ucwords(str_replace('_', ' ', $field));
|
||||
$rules[$field]['errors']['required'] = $label . ' is required for the selected claim status.';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function view_ticket($ticket_id)
|
||||
{
|
||||
// $ticket_data = $this->ticketMasterModel->where('id', $ticket_id)->where('is_active', 1)->first();
|
||||
@ -1456,6 +1508,8 @@ class TicketController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
$rules = $this->applyClaimStatusConditionalRules($rules);
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
@ -1944,6 +1998,9 @@ class TicketController extends BaseController
|
||||
'return_remark' => ['label' => 'Return Remark','rules' => 'permit_empty','errors' => []],
|
||||
];
|
||||
}
|
||||
|
||||
$rules = $this->applyClaimStatusConditionalRules($rules);
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
@ -3090,7 +3147,7 @@ class TicketController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function getLastMatchedStatus($incoming_form_values, $old_ticket_data)
|
||||
public function getLastMatchedStatusOld($incoming_form_values, $old_ticket_data)
|
||||
{
|
||||
if (empty($incoming_form_values) || !isset($incoming_form_values['claim_status_id']) || !isset($incoming_form_values['extra_fields_array_for_validate'])) {
|
||||
return null;
|
||||
@ -3159,6 +3216,71 @@ class TicketController extends BaseController
|
||||
return $lastMatchedStatus;
|
||||
}
|
||||
|
||||
public function getLastMatchedStatus($incoming_form_values, $old_ticket_data)
|
||||
{
|
||||
// Preserve the incoming claim_status_id (if any) as the safe fallback so callers
|
||||
// never accidentally nullify the status when validation inputs are missing.
|
||||
$incomingStatus = $incoming_form_values['claim_status_id'] ?? null;
|
||||
|
||||
if (empty($incoming_form_values) || !isset($incoming_form_values['extra_fields_array_for_validate'])) {
|
||||
return $incomingStatus;
|
||||
}
|
||||
|
||||
$lastMatchedStatus = $incomingStatus;
|
||||
|
||||
$statusMapping = json_decode($incoming_form_values['extra_fields_array_for_validate'], true);
|
||||
if (!is_array($statusMapping) || empty($statusMapping)) {
|
||||
return $incomingStatus;
|
||||
}
|
||||
|
||||
// Drop transitional / aggregate statuses that should never be auto-selected.
|
||||
$removeKeys = is_array($this->removeArrayKey) ? $this->removeArrayKey : [];
|
||||
$statusMapping = array_diff_key($statusMapping, $removeKeys);
|
||||
|
||||
$oldStatusId = is_array($old_ticket_data) ? ($old_ticket_data['claim_status_id'] ?? null) : null;
|
||||
|
||||
$skipFields = ['approved_description'];
|
||||
|
||||
foreach ($statusMapping as $status => $requiredFields) {
|
||||
if (empty($requiredFields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the current/old status — we only advance forward to a newer status.
|
||||
if ($oldStatusId !== null && (int) $status === (int) $oldStatusId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($requiredFields)) {
|
||||
$requiredFields = [$requiredFields];
|
||||
}
|
||||
|
||||
$allFieldsMatched = true;
|
||||
foreach ($requiredFields as $field) {
|
||||
if (in_array($field, $skipFields, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($incoming_form_values[$field]) || $incoming_form_values[$field] === '' || $incoming_form_values[$field] === null) {
|
||||
$allFieldsMatched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($allFieldsMatched) {
|
||||
$lastMatchedStatus = $status;
|
||||
}
|
||||
}
|
||||
|
||||
// When a TPA number is present, promote the base "registered" statuses
|
||||
// (1 -> 2 for standard, 67 -> 68 for the alternate flow).
|
||||
if (!empty($incoming_form_values['tpa_no']) && in_array((int) $lastMatchedStatus, [1, 67], true)) {
|
||||
$lastMatchedStatus = ((int) $lastMatchedStatus === 1) ? 2 : 68;
|
||||
}
|
||||
|
||||
return $lastMatchedStatus;
|
||||
}
|
||||
|
||||
public function getPolicyStartDate()
|
||||
{
|
||||
|
||||
@ -3597,7 +3719,7 @@ class TicketController extends BaseController
|
||||
{
|
||||
$fileId = $this->claimFilesModel->insert([
|
||||
'ticket_id' => $ticketId,
|
||||
'ticket_type' => $ticketTypeId,
|
||||
// 'ticket_type' => $ticketTypeId,
|
||||
'file_type' => 3,
|
||||
'doc_name' => $docName,
|
||||
'file_name' => $uploadedFile['file_name'],
|
||||
|
||||
@ -183,12 +183,24 @@ use App\Models\JobModel;
|
||||
|
||||
class MailHelper
|
||||
{
|
||||
private const SYSTEM_EMAIL_FOOTER = '<div style="
|
||||
text-align:center;
|
||||
font-size:9px;
|
||||
color:#999999;
|
||||
border-top:1px solid #eeeeee;
|
||||
margin-top:2px;
|
||||
padding-top:1px;
|
||||
line-height:1;
|
||||
">
|
||||
This is a system-generated email. Please do not reply.
|
||||
</div>';
|
||||
public static function send_email_smtp($params)
|
||||
{
|
||||
$myLogger = \Config\Services::mylogger();
|
||||
$emaill = $params['mail'];
|
||||
$subject = $params['subject'];
|
||||
$message = $params['message'];
|
||||
$message .= self::SYSTEM_EMAIL_FOOTER;
|
||||
if (isset($params['bcc'])) {
|
||||
$bcc = $params['bcc'];
|
||||
} else {
|
||||
@ -206,7 +218,8 @@ class MailHelper
|
||||
$curl = curl_init();
|
||||
$postData = [
|
||||
'from' => [
|
||||
'address' => $from_address
|
||||
'address' => $from_address,
|
||||
'name' => 'Nhance India Insurance'
|
||||
],
|
||||
'to' => [
|
||||
[
|
||||
@ -315,6 +328,9 @@ class MailHelper
|
||||
|
||||
$subject = $params['subject'];
|
||||
$message = $params['message'];
|
||||
$message .= self::SYSTEM_EMAIL_FOOTER;
|
||||
|
||||
// echo $message;die;
|
||||
$attachments = isset($params['attachments']) ? $params['attachments'] : [];
|
||||
$common = isset($params['common']) ? $params['common'] : '';
|
||||
$bcc = isset($params['bcc']) ? $params['bcc'] : '';
|
||||
@ -328,7 +344,8 @@ class MailHelper
|
||||
|
||||
$postData = [
|
||||
'from' => [
|
||||
'address' => $from_address
|
||||
'address' => $from_address,
|
||||
'name' => "Nhance India Insurance"
|
||||
],
|
||||
'to' => $emails,
|
||||
'subject' => $subject,
|
||||
|
||||
2090
app/Models/ClaimsCollectionV2DashboardModel.php
Normal file
2090
app/Models/ClaimsCollectionV2DashboardModel.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -311,6 +311,24 @@ class ClientPolicyModel extends Model
|
||||
// Fetch the deposit data based on client and insurer IDs
|
||||
$query = $this->db->table('cash_deposit')
|
||||
->select('cash_deposit.*')
|
||||
->select("
|
||||
(select old_value from auditing_history where pk = cash_deposit.id and field_name = 'amount' and table_name = 'cash_deposit' order by id desc limit 1) as old_amt,
|
||||
(select new_value from auditing_history where pk = cash_deposit.id and field_name = 'amount' and table_name = 'cash_deposit' order by id desc limit 1) as new_amt,
|
||||
(select created_by from auditing_history where pk = cash_deposit.id and field_name = 'amount' and table_name = 'cash_deposit' order by id desc limit 1) as amt_updated_by,
|
||||
(select created_at from auditing_history where pk = cash_deposit.id and field_name = 'amount' and table_name = 'cash_deposit' order by id desc limit 1) as amt_updated_at,
|
||||
(
|
||||
select up.first_name
|
||||
from user_profiles up
|
||||
where up.id = (
|
||||
select created_by
|
||||
from auditing_history
|
||||
where pk = cash_deposit.id and field_name = 'amount' and table_name = 'cash_deposit'
|
||||
order by id desc
|
||||
limit 1
|
||||
)
|
||||
limit 1
|
||||
) as amt_updated_by_name
|
||||
")
|
||||
->select('insurers.name as insurer_name, insurers.short_name as insurer_short')
|
||||
->select('clients.client_name as clientname, clients.short_name as clientshort, client_policy.policy_no')
|
||||
->select('cd_master.cd_ac_no as cd_master_account_no')
|
||||
|
||||
@ -133,6 +133,7 @@ class UserModel extends Model
|
||||
->select('user_profiles.*')
|
||||
->select('roles.role as user_role')
|
||||
->join('roles', 'roles.id = user_profiles.role')
|
||||
->where('user_profiles.is_active', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
@ -143,13 +144,26 @@ class UserModel extends Model
|
||||
->select('roles.role as user_role')
|
||||
->join('roles', 'roles.id = user_profiles.role')
|
||||
->join('user_teams', 'user_teams.user_id = user_profiles.id')
|
||||
->where('user_profiles.is_active', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// The user_teams join produces one row per (user, team) pair, so users
|
||||
// belonging to multiple teams appear multiple times. Collapse to one
|
||||
// row per user while keeping a team_id from [6, 7] when available, so
|
||||
// the existing in_array($user['team_id'], [6, 7]) checks in the views
|
||||
// continue to work correctly.
|
||||
$deduped = [];
|
||||
foreach ($data as $row) {
|
||||
$userId = $row['id'];
|
||||
if (!isset($deduped[$userId])) {
|
||||
$deduped[$userId] = $row;
|
||||
} elseif (in_array($row['team_id'], [6, 7])) {
|
||||
$deduped[$userId]['team_id'] = $row['team_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// dd($data);
|
||||
|
||||
return $data;
|
||||
return array_values($deduped);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
113
app/Views/apache_dashboard_demo_one.php
Normal file
113
app/Views/apache_dashboard_demo_one.php
Normal file
@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claims Overview</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: sans-serif; background: #f5f5f5; }
|
||||
#dashboard-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
#dashboard-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
#error-msg {
|
||||
display: none;
|
||||
padding: 2rem;
|
||||
color: #c0392b;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="dashboard-container"></div>
|
||||
<iframe src="https://venbait.in/chartBoard/public/share/8e130e4faa821740183feaa805f3ba0c7935841a64c5461ba6ed255f5b419f93?embed=1" width="1200" height="800" style="border:0;" loading="lazy" title="Chart-Board"></iframe>
|
||||
<div id="error-msg"></div>
|
||||
|
||||
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
|
||||
|
||||
<script>
|
||||
const SUPERSET_DOMAIN = "https://demo.venbait.in";
|
||||
const DASHBOARD_ID = "aaff42c8-1d7a-4e28-8beb-f6a7b9e08661";
|
||||
const USERNAME = "admin";
|
||||
const PASSWORD = "$tr0n9pa55w0rd ";
|
||||
|
||||
async function getGuestToken() {
|
||||
|
||||
// Step 1: Login to get access token
|
||||
const loginRes = await fetch(SUPERSET_DOMAIN + "/api/v1/security/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: USERNAME,
|
||||
password: PASSWORD,
|
||||
provider: "db",
|
||||
refresh: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginRes.ok) throw new Error("Login failed: " + loginRes.status);
|
||||
const { access_token } = await loginRes.json();
|
||||
|
||||
// Step 2: Get CSRF token
|
||||
const csrfRes = await fetch(SUPERSET_DOMAIN + "/api/v1/security/csrf_token/", {
|
||||
method: "GET",
|
||||
headers: { "Authorization": "Bearer " + access_token }
|
||||
});
|
||||
|
||||
if (!csrfRes.ok) throw new Error("CSRF fetch failed: " + csrfRes.status);
|
||||
const { result: csrf_token } = await csrfRes.json();
|
||||
|
||||
// Step 3: Get guest token
|
||||
const guestRes = await fetch(SUPERSET_DOMAIN + "/api/v1/security/guest_token/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + access_token,
|
||||
"X-CSRFToken": csrf_token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resources: [{ type: "dashboard", id: DASHBOARD_ID }],
|
||||
rls: [],
|
||||
user: { username: "guest", first_name: "Guest", last_name: "User" }
|
||||
})
|
||||
});
|
||||
|
||||
if (!guestRes.ok) throw new Error("Guest token failed: " + guestRes.status);
|
||||
const { token } = await guestRes.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
alert();
|
||||
return true;
|
||||
try {
|
||||
await supersetEmbeddedSdk.embedDashboard({
|
||||
id: DASHBOARD_ID,
|
||||
supersetDomain: SUPERSET_DOMAIN,
|
||||
mountPoint: document.getElementById("dashboard-container"),
|
||||
fetchGuestToken: () => getGuestToken(),
|
||||
dashboardUiConfig: {
|
||||
hideTitle: true,
|
||||
filters: { expanded: true }
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const el = document.getElementById("error-msg");
|
||||
el.style.display = "block";
|
||||
el.textContent = "Error loading dashboard: " + err.message;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -34,7 +34,7 @@ if (!empty($batch_list) && is_array($batch_list)) {
|
||||
$countStr = ($file['count'] ?? null) === null ? '-' : (string) $file['count'];
|
||||
$batch_col_max_len[9] = max($batch_col_max_len[9], mb_strlen($countStr));
|
||||
$batch_col_max_len[10] = max($batch_col_max_len[10], mb_strlen((string) format_indian_number($file['amount'])));
|
||||
$userTime = change_date_format($file['created_at'] ?? '', 'Y-m-d H:i:s', 'd M Y h:i a')
|
||||
$userTime = change_date_format($file['created_at'] ?? '', 'Y-m-d H:i:s', 'd/m/Y h:i a')
|
||||
. ' by '
|
||||
. get_username($file['created_by'] ?? '');
|
||||
$batch_col_max_len[11] = max($batch_col_max_len[11], mb_strlen($userTime));
|
||||
@ -350,7 +350,7 @@ for ($i = 0; $i < $batch_col_count; $i++) {
|
||||
|
||||
<td><?php echo format_indian_number($file['amount'])?></td>
|
||||
<td class="reload">
|
||||
<?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd M Y h:i a') ?> by <?php echo get_username($file['created_by']) ?>
|
||||
<?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd/m/Y h:i a') ?> by <?php echo get_username($file['created_by']) ?>
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
<td style="overflow: hidden;" class="reload truncate" data-toggle="tooltip" data-placement="top" title="<?php echo $file['file_name'] ?>">
|
||||
<?php echo $file['file_name'] ?>
|
||||
</td>
|
||||
<td><?php echo change_date_format($file['created_at'], 'Y-m-d H:i:s', 'd M Y h:i A') . ' by <strong>' . $file['user_name'] . '</strong>' ?> </td>
|
||||
<td><?php echo change_date_format($file['created_at'], 'Y-m-d H:i:s', 'd/m/Y h:i A') . ' by <strong>' . $file['user_name'] . '</strong>' ?> </td>
|
||||
<td>
|
||||
<?php if ($file['status'] == "failed") { ?>
|
||||
<span> <?= $file['status'] ?> </span>
|
||||
|
||||
@ -107,13 +107,50 @@
|
||||
var isCdMasterPage = <?php echo isset($CD_Master_Data) ? 'true' : 'false'; ?>;
|
||||
console.log("isCdMasterPage", isCdMasterPage);
|
||||
|
||||
function formatCdOpeningDateForInput(inputDate) {
|
||||
if (!inputDate || String(inputDate).trim() === '') {
|
||||
return '';
|
||||
}
|
||||
var str = String(inputDate).trim();
|
||||
var iso = str.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (iso) {
|
||||
return iso[3] + '/' + iso[2] + '/' + iso[1];
|
||||
}
|
||||
var dmy = str.match(/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})$/);
|
||||
if (dmy) {
|
||||
var day = ('0' + dmy[1]).slice(-2);
|
||||
var month = ('0' + dmy[2]).slice(-2);
|
||||
return day + '/' + month + '/' + dmy[3];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function cdOpeningDateForSubmit(inputDate) {
|
||||
var formatted = formatCdOpeningDateForInput(inputDate);
|
||||
if (!formatted) {
|
||||
return inputDate || '';
|
||||
}
|
||||
var parts = formatted.split('/');
|
||||
return parts[0] + '-' + parts[1] + '-' + parts[2];
|
||||
}
|
||||
|
||||
var openingDatePicker;
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
var openingDatePicker = flatpickr("#opening_date", {
|
||||
dateFormat: "d-m-Y",
|
||||
openingDatePicker = flatpickr("#opening_date", {
|
||||
dateFormat: "d/m/Y",
|
||||
allowInput: false
|
||||
});
|
||||
|
||||
$('#con-close-modal').on('shown.bs.modal', function() {
|
||||
var val = $('#opening_date').val();
|
||||
if (val) {
|
||||
var formatted = formatCdOpeningDateForInput(val);
|
||||
openingDatePicker.setDate(formatted, false);
|
||||
}
|
||||
});
|
||||
|
||||
if(isCdMasterPage == true){
|
||||
$('#cd_client_id').select2();
|
||||
$('#insurer_id_for_cd').select2();
|
||||
@ -172,6 +209,10 @@
|
||||
}
|
||||
|
||||
let formData = new FormData($('#CDMasterForm')[0]);
|
||||
var openingDate = formData.get('opening_date');
|
||||
if (openingDate) {
|
||||
formData.set('opening_date', cdOpeningDateForSubmit(openingDate));
|
||||
}
|
||||
console.log("formData", formData);
|
||||
|
||||
let url = '<?= base_url('master/cash_deposite/create') ?>';
|
||||
|
||||
@ -15,12 +15,12 @@ foreach ($CD_Master_Data as $index => $row) {
|
||||
$cdm_col_max_len[1] = max($cdm_col_max_len[1], mb_strlen($clientCell));
|
||||
$cdm_col_max_len[2] = max($cdm_col_max_len[2], mb_strlen((string) ($row['insurer_name'] ?? '')));
|
||||
$cdm_col_max_len[3] = max($cdm_col_max_len[3], mb_strlen((string) ($row['insurer_branch_name'] ?? '')));
|
||||
$od = ! empty($row['opening_date']) ? date('d-m-Y', strtotime((string) $row['opening_date'])) : '';
|
||||
$od = ! empty($row['opening_date']) ? date('d/m/Y', strtotime((string) $row['opening_date'])) : '';
|
||||
$cdm_col_max_len[4] = max($cdm_col_max_len[4], mb_strlen($od));
|
||||
$cdm_col_max_len[5] = max($cdm_col_max_len[5], mb_strlen((string) ($row['cd_ac_no'] ?? '')));
|
||||
$cdm_col_max_len[6] = max($cdm_col_max_len[6], mb_strlen((string) ($row['opening_bal'] ?? '')));
|
||||
$du = ! empty($row['created_at'])
|
||||
? (date('d-M-Y h:i A', strtotime((string) $row['created_at'])) . ' by ' . ($row['user_name'] ?? ''))
|
||||
? (date('d/m/Y h:i A', strtotime((string) $row['created_at'])) . ' by ' . ($row['user_name'] ?? ''))
|
||||
: '';
|
||||
$cdm_col_max_len[7] = max($cdm_col_max_len[7], mb_strlen($du));
|
||||
$cdm_col_max_len[8] = max($cdm_col_max_len[8], 4);
|
||||
@ -118,10 +118,10 @@ $cdm_col_width_px = nhance_dt_column_widths_px($cdm_header_labels, $cdm_col_max_
|
||||
<td><?php echo $row['client_name']; ?>( <?= $row['short_name'] ?> )</td>
|
||||
<td><?php echo $row['insurer_name']; ?></td>
|
||||
<td><?php echo $row['insurer_branch_name']; ?></td>
|
||||
<td><?php echo date('d-m-Y', strtotime($row['opening_date'])); ?></td>
|
||||
<td><?php echo date('d/m/Y', strtotime($row['opening_date'])); ?></td>
|
||||
<td><?php echo $row['cd_ac_no']; ?></td>
|
||||
<td><?php echo $row['opening_bal']; ?></td>
|
||||
<td><?php echo date('d-M-Y h:i A', strtotime($row['created_at'])) ?> <br>by <?php echo $row['user_name']; ?></td>
|
||||
<td><?php echo date('d/m/Y h:i A', strtotime($row['created_at'])) ?> <br>by <?php echo $row['user_name']; ?></td>
|
||||
<td class="text-center table-action-cell">
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript: void(0);"class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown" aria-expanded="false"><i class="mdi mdi-dots-horizontal"></i></a>
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
<td style="overflow: hidden;" class="reload truncate" data-toggle="tooltip" data-placement="top" title="<?php echo $file['file_name'] ?>">
|
||||
<?php echo $file['file_name'] ?>
|
||||
</td>
|
||||
<td><?php echo change_date_format($file['created_at'], 'Y-m-d H:i:s', 'd M Y h:i A') . ' by <strong>' . $file['user_name'] . '</strong>' ?> </td>
|
||||
<td><?php echo change_date_format($file['created_at'], 'Y-m-d H:i:s', 'd/m/Y h:i A') . ' by <strong>' . $file['user_name'] . '</strong>' ?> </td>
|
||||
<td>
|
||||
<?php if ($file['status'] == "failed") { ?>
|
||||
<span style="color : #BD0707 ;"> <?= $file['status'] ?> </span>
|
||||
@ -262,7 +262,7 @@
|
||||
<label for="tpa">TPA<span id="tpa_danger" class="text-danger"></span></label>
|
||||
<select class="form-control readonly-select" id="tpa_id" name="tpa_id">
|
||||
<option value="">Select TPA</option>
|
||||
<?php foreach ($tpa_list as $value) { ?>
|
||||
<?php foreach ($tpa_list ?? [] as $value) { ?>
|
||||
<option value="<?= $value['id'] ?>"><?= $value['short_name'] ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
|
||||
103
app/Views/claims_collection_v2_dashboard.php
Normal file
103
app/Views/claims_collection_v2_dashboard.php
Normal file
@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claims Collection V2 Dashboard</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 1.5rem; background: #f0f2f5; color: #1a1a1a; }
|
||||
h1 { font-size: 1.35rem; margin: 0 0 1rem; }
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-bottom: 1.25rem; }
|
||||
.toolbar input { padding: 0.45rem 0.6rem; border: 1px solid #ccc; border-radius: 6px; width: 140px; }
|
||||
.toolbar button { padding: 0.5rem 1rem; border: none; border-radius: 6px; background: #2563eb; color: #fff; cursor: pointer; }
|
||||
.toolbar button.secondary { background: #64748b; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
||||
.card { background: #fff; border-radius: 8px; padding: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
.card h2 { font-size: 0.85rem; margin: 0 0 0.5rem; color: #475569; font-weight: 600; }
|
||||
.card pre { font-size: 0.72rem; margin: 0; max-height: 200px; overflow: auto; background: #f8fafc; padding: 0.5rem; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }
|
||||
.card .status { font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.35rem; }
|
||||
.card.loading pre { color: #94a3b8; }
|
||||
.card.error pre { color: #b91c1c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Claims Collection V2</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<label>Policy ID <input type="number" id="policy-id" value="<?= (int) $policy_id ?>" min="1"></label>
|
||||
<button type="button" id="btn-load-all">Load all KPIs</button>
|
||||
<button type="button" class="secondary" id="btn-clear">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="grid" id="kpi-grid">
|
||||
<?php foreach ($kpi_map as $metabaseId => $method): ?>
|
||||
<div class="card" id="card-<?= esc($method) ?>" data-kpi="<?= esc($method) ?>">
|
||||
<div class="status">#<?= (int) $metabaseId ?> · <?= esc($kpi_labels[$method] ?? $method) ?></div>
|
||||
<h2><?= esc($method) ?></h2>
|
||||
<pre>Click “Load all KPIs” or open single KPI API.</pre>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const baseUrl = <?= json_encode(rtrim(base_url(), '/')) ?>;
|
||||
const policyInput = document.getElementById('policy-id');
|
||||
const grid = document.getElementById('kpi-grid');
|
||||
|
||||
function setCardState(method, state, text) {
|
||||
const card = document.getElementById('card-' + method);
|
||||
if (!card) return;
|
||||
card.classList.remove('loading', 'error');
|
||||
if (state) card.classList.add(state);
|
||||
card.querySelector('pre').textContent = text;
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
const policyId = policyInput.value;
|
||||
if (!policyId) {
|
||||
alert('Enter a policy ID');
|
||||
return;
|
||||
}
|
||||
grid.querySelectorAll('.card').forEach(function (c) {
|
||||
c.classList.add('loading');
|
||||
c.querySelector('pre').textContent = 'Loading…';
|
||||
});
|
||||
|
||||
try {
|
||||
const apiAllUrl = <?= json_encode($api_all_url ?? base_url('util/claims-collection-v2/all')) ?>;
|
||||
const res = await fetch(apiAllUrl + '?client_policy=' + encodeURIComponent(policyId));
|
||||
const json = await res.json();
|
||||
if (!json.status) {
|
||||
throw new Error(json.message || 'Request failed');
|
||||
}
|
||||
Object.keys(json.data || {}).forEach(function (method) {
|
||||
const block = json.data[method];
|
||||
setCardState(method, '', JSON.stringify(block.rows, null, 2));
|
||||
});
|
||||
} catch (e) {
|
||||
grid.querySelectorAll('.card').forEach(function (c) {
|
||||
c.classList.remove('loading');
|
||||
c.classList.add('error');
|
||||
c.querySelector('pre').textContent = e.message;
|
||||
});
|
||||
} finally {
|
||||
grid.querySelectorAll('.card.loading').forEach(function (c) {
|
||||
c.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btn-load-all').addEventListener('click', loadAll);
|
||||
document.getElementById('btn-clear').addEventListener('click', function () {
|
||||
grid.querySelectorAll('.card').forEach(function (c) {
|
||||
c.classList.remove('error');
|
||||
c.querySelector('pre').textContent = '—';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -236,7 +236,7 @@ input:checked + .slider:before {
|
||||
class="text-danger">*</span></label>
|
||||
<select class="form-control" id="entity_type" name="entity_type_id" required>
|
||||
<option value="">Select Entity</option>
|
||||
<?php foreach($entity as $value) { ?>
|
||||
<?php foreach($entity ?? [] as $value) { ?>
|
||||
<?php if(isset($client['entity_type_id'])){ ?>
|
||||
<?php if($value['id'] == $client['entity_type_id']) { ?>
|
||||
<option value="<?= $value['id'] ?>" selected><?= $value['name'] ?></option>
|
||||
@ -285,7 +285,7 @@ input:checked + .slider:before {
|
||||
<label for="parent_client_id">Parent Group<span class="text-danger"></span></label>
|
||||
<select class="form-control" id="parent_client_id" name="parent_client_id">
|
||||
<option value="" >Select Parent</option>
|
||||
<?php foreach($clients as $value) { ?>
|
||||
<?php foreach($clients ?? [] as $value) { ?>
|
||||
<option value="<?= $value['id'] ?>" <?= isset($client['parent_client_id']) && $client['parent_client_id'] == $value['id'] ? "selected" : '' ?>><?= $value['client_name'] ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
@ -293,7 +293,7 @@ input:checked + .slider:before {
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
|
||||
<label>HR File Processed By <span class="text-danger">*</span></label>
|
||||
<label>Enrollment file processed by <span class="text-danger">*</span></label>
|
||||
<div class="hr-processed-options">
|
||||
<div class="hr-processed-option">
|
||||
<input class="hr-radio-input"
|
||||
|
||||
@ -138,7 +138,7 @@ $cd_col_width_px = nhance_dt_column_widths_px($cd_header_labels, $cd_col_max_len
|
||||
<td>
|
||||
<a class="view-deposit-link"
|
||||
href="<?= base_url("client/view_deposit/{$value->insurer_id}?client_id={$value->client_id}&cd_ac_pk={$value->cd_ac_pk}"); ?>">
|
||||
<i class="mdi mdi-eye text-muted font-18 vertical-middle"></i><span>View Deposit</span>
|
||||
<i class="mdi mdi-eye text-muted font-18 vertical-middle"></i><span>View Details</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -209,7 +209,7 @@ input:checked + .slider_blue::before {
|
||||
data-parsley-pattern="^[1-9][0-9]*$"
|
||||
data-parsley-pattern-message="Please select a valid Policy Type.">
|
||||
<option value="0">Select Policy Type</option>
|
||||
<?php foreach ($policy_types as $value) { ?>
|
||||
<?php foreach ($policy_types ?? [] as $value) { ?>
|
||||
<option value="<?= $value['id']?>"><?= $value['policy_type'] ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
@ -230,7 +230,7 @@ input:checked + .slider_blue::before {
|
||||
<label for="insurer">Insurer<span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="insurer" name="insurer" onchange="setInsuerAndBranchValue(this)" required>
|
||||
<option value="">Select Insurer</option>
|
||||
<?php foreach ($insurer as $value) { ?>
|
||||
<?php foreach ($insurer ?? [] as $value) { ?>
|
||||
<option data-id="<?= $value['insurer_id']?>" value="<?= $value['id'] . '-' . $value['insurer_id'] ?>">
|
||||
<?= $value['insurer_name'] . '-' . $value['branch_code'] ?></option>
|
||||
<?php } ?>
|
||||
@ -241,7 +241,7 @@ input:checked + .slider_blue::before {
|
||||
<label for="tpa">TPA<span id="tpa_danger" class="text-danger">*</span></label>
|
||||
<select class="form-control" id="tpa" name="tpa" required>
|
||||
<option value="">Select TPA</option>
|
||||
<?php foreach ($tpa as $value) { ?>
|
||||
<?php foreach ($tpa ?? [] as $value) { ?>
|
||||
<option value="<?= $value['id'] . '-' . $value['tpa_id'] ?>">
|
||||
<?= $value['tpa_short_name'] . '-' . $value['branch_code'] ?></option>
|
||||
<?php } ?>
|
||||
@ -454,7 +454,7 @@ input:checked + .slider_blue::before {
|
||||
const showExpired = $('#chk-show-expired').is(':checked');
|
||||
|
||||
var startDatePicker = flatpickr("#start_date", {
|
||||
dateFormat: "d-m-Y",
|
||||
dateFormat: "d/m/Y",
|
||||
defaultDate: today,
|
||||
allowInput: false,
|
||||
onChange: function(selectedDates, dateStr, instance) {
|
||||
@ -467,7 +467,7 @@ input:checked + .slider_blue::before {
|
||||
|
||||
// Initialize Flatpickr for the start date with today's date
|
||||
var openDatePicker = flatpickr("#open_date", {
|
||||
dateFormat: "d-m-Y",
|
||||
dateFormat: "d/m/Y",
|
||||
defaultDate: today,
|
||||
allowInput: false,
|
||||
});
|
||||
@ -478,14 +478,14 @@ input:checked + .slider_blue::before {
|
||||
closeDate.setDate(closeDate.getDate() - 1); // Set end date to last day of next year
|
||||
|
||||
var endDatePicker = flatpickr("#end_date", {
|
||||
dateFormat: "d-m-Y",
|
||||
dateFormat: "d/m/Y",
|
||||
defaultDate: closeDate,
|
||||
allowInput: false
|
||||
});
|
||||
|
||||
// Initialize Flatpickr for the end date with the calculated end date
|
||||
var closeDatePicker = flatpickr("#close_date", {
|
||||
dateFormat: "d-m-Y",
|
||||
dateFormat: "d/m/Y",
|
||||
defaultDate: today,
|
||||
allowInput: false
|
||||
});
|
||||
@ -566,7 +566,7 @@ input:checked + .slider_blue::before {
|
||||
<td>${policy_name_data}</td>
|
||||
<td>${item.branch_name ? item.branch_name : ' - '}</td>
|
||||
<td>${tpaValue}</td>
|
||||
<td>${(item.policy_start_date)} / ${(item.policy_end_date)}</td>
|
||||
<td>${formatPolicyDisplayDate(item.policy_start_date)} - ${formatPolicyDisplayDate(item.policy_end_date)}</td>
|
||||
|
||||
<td>${checkDateStatus(item.policy_end_date, 1)}</td>
|
||||
<td>
|
||||
@ -751,6 +751,12 @@ input:checked + .slider_blue::before {
|
||||
$('.loader-mask').fadeIn();
|
||||
|
||||
var formData = new FormData($('#policy_form')[0]);
|
||||
['policy_start_date', 'policy_end_date', 'open_date', 'close_date'].forEach(function(fieldName) {
|
||||
var val = formData.get(fieldName);
|
||||
if (val) {
|
||||
formData.set(fieldName, policyFormDateForSubmit(val));
|
||||
}
|
||||
});
|
||||
var policy_form_action = $('#policy_form_action').val();
|
||||
console.log(formData+'form data');
|
||||
$.ajax({
|
||||
@ -853,7 +859,7 @@ input:checked + .slider_blue::before {
|
||||
<td>${item.branch_name ? item.branch_name : ' - '}</td>
|
||||
<td>${tpaValue}</td>
|
||||
|
||||
<td>${(item.policy_start_date)} / ${(item.policy_end_date)}</td>
|
||||
<td>${formatPolicyDisplayDate(item.policy_start_date)} - ${formatPolicyDisplayDate(item.policy_end_date)}</td>
|
||||
<td>${checkDateStatus(item.policy_end_date, 1)}</td>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
@ -1512,31 +1518,74 @@ input:checked + .slider_blue::before {
|
||||
|
||||
});
|
||||
|
||||
//for date convert to indian formate like this 'yyyy-mm-dd' to this 'dd-mm-yyyy'
|
||||
function rearrangeDateFormat(inputDate) {
|
||||
|
||||
console.log('inputDate', inputDate)
|
||||
// Check if inputDate is a string and not empty
|
||||
if (typeof inputDate === 'string' && inputDate.trim() !== '') {
|
||||
var dateComponents = inputDate.split("-");
|
||||
|
||||
// Check if dateComponents has the expected number of parts
|
||||
if (dateComponents.length === 3) {
|
||||
var rearrangedDate = dateComponents[2] + "-" + dateComponents[1] + "-" + dateComponents[0];
|
||||
return rearrangedDate;
|
||||
} else {
|
||||
// Handle unexpected date format
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// Handle the case where inputDate is not a valid string
|
||||
return '';
|
||||
function parsePolicyDate(inputDate) {
|
||||
if (!inputDate || (typeof inputDate === 'string' && inputDate.trim() === '')) {
|
||||
return null;
|
||||
}
|
||||
var str = String(inputDate).trim();
|
||||
var dmyMon = str.match(/^(\d{1,2})\/([A-Za-z]{3})\/(\d{4})$/i);
|
||||
if (dmyMon) {
|
||||
return new Date(str);
|
||||
}
|
||||
var dmy = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||
if (dmy) {
|
||||
return new Date(parseInt(dmy[3], 10), parseInt(dmy[2], 10) - 1, parseInt(dmy[1], 10));
|
||||
}
|
||||
var iso = str.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (iso) {
|
||||
return new Date(parseInt(iso[1], 10), parseInt(iso[2], 10) - 1, parseInt(iso[3], 10));
|
||||
}
|
||||
var dmyDash = str.match(/^(\d{1,2})-([A-Za-z]{3}|\d{1,2})-(\d{4})$/i);
|
||||
if (dmyDash) {
|
||||
if (/^[A-Za-z]{3}$/i.test(dmyDash[2])) {
|
||||
return new Date(str);
|
||||
}
|
||||
return new Date(parseInt(dmyDash[3], 10), parseInt(dmyDash[2], 10) - 1, parseInt(dmyDash[1], 10));
|
||||
}
|
||||
var parsed = new Date(str);
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
var policyMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
function formatPolicyDisplayDate(inputDate) {
|
||||
var d = parsePolicyDate(inputDate);
|
||||
if (!d) {
|
||||
return (inputDate && String(inputDate).trim() !== '') ? String(inputDate) : '';
|
||||
}
|
||||
return d.getDate() + '/' + policyMonthNames[d.getMonth()] + '/' + d.getFullYear();
|
||||
}
|
||||
|
||||
function formatPolicyFormDate(inputDate) {
|
||||
var d = parsePolicyDate(inputDate);
|
||||
if (!d) {
|
||||
return (inputDate && String(inputDate).trim() !== '') ? String(inputDate) : '';
|
||||
}
|
||||
var day = ('0' + d.getDate()).slice(-2);
|
||||
var month = ('0' + (d.getMonth() + 1)).slice(-2);
|
||||
return day + '/' + month + '/' + d.getFullYear();
|
||||
}
|
||||
|
||||
function policyFormDateForSubmit(inputDate) {
|
||||
var formatted = formatPolicyFormDate(inputDate);
|
||||
if (!formatted) {
|
||||
return inputDate || '';
|
||||
}
|
||||
var parts = formatted.split('/');
|
||||
return parts[0] + '-' + parts[1] + '-' + parts[2];
|
||||
}
|
||||
|
||||
// Convert API/storage dates to d/m/Y for form inputs
|
||||
function rearrangeDateFormat(inputDate) {
|
||||
return formatPolicyFormDate(inputDate);
|
||||
}
|
||||
|
||||
function checkDateStatus(inputDate, bg = false) {
|
||||
|
||||
var givenDate = new Date(inputDate);
|
||||
var givenDate = parsePolicyDate(inputDate);
|
||||
if (!givenDate) {
|
||||
return bg ? '' : '';
|
||||
}
|
||||
var currentDate = new Date();
|
||||
|
||||
if (bg != false) {
|
||||
@ -2763,7 +2812,7 @@ $(document).ready(function () {
|
||||
<td>${policy_name_data}</td>
|
||||
<td>${item.branch_name ? item.branch_name : ' - '}</td>
|
||||
<td>${tpaValue}</td>
|
||||
<td>${(item.policy_start_date)} / ${(item.policy_end_date)}</td>
|
||||
<td>${formatPolicyDisplayDate(item.policy_start_date)} - ${formatPolicyDisplayDate(item.policy_end_date)}</td>
|
||||
|
||||
<td>${checkDateStatus(item.policy_end_date, 1)}</td>
|
||||
<td>
|
||||
@ -2809,7 +2858,7 @@ $(document).ready(function () {
|
||||
<td>${policy_name_data}</td>
|
||||
<td>${item.branch_name ? item.branch_name : ' - '}</td>
|
||||
<td>${tpaValue}</td>
|
||||
<td>${(item.policy_start_date)} / ${(item.policy_end_date)}</td>
|
||||
<td>${formatPolicyDisplayDate(item.policy_start_date)} - ${formatPolicyDisplayDate(item.policy_end_date)}</td>
|
||||
|
||||
<td>${checkDateStatus(item.policy_end_date, 1)}</td>
|
||||
<td>
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
<td><?php echo ucfirst($row['department']) ?> </td>
|
||||
<td><?php echo $row['rules_count'] ?></td>
|
||||
<td><?php echo ucfirst($row['file_status']) ?> </td>
|
||||
<td><?php echo change_date_format($row['created_at'],'Y-m-d H:i:s', 'd M Y h:i A') . ' by <strong>' . $row['created_user_name'] . '</strong>' ?>
|
||||
<td><?php echo change_date_format($row['created_at'],'Y-m-d H:i:s', 'd/m/Y h:i A') . ' by <strong>' . $row['created_user_name'] . '</strong>' ?>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript: void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown" aria-expanded="false"><i class="mdi mdi-dots-horizontal"></i></a>
|
||||
|
||||
292
app/Views/docs/bds-commission.php
Normal file
292
app/Views/docs/bds-commission.php
Normal 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 upload’s 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>></code>, <code>>=</code>, <code><</code>, <code><=</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>
|
||||
309
app/Views/docs/bds-insurer-statement.php
Normal file
309
app/Views/docs/bds-insurer-statement.php
Normal 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 (1–7), 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 (1–7 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>
|
||||
265
app/Views/docs/correction.php
Normal file
265
app/Views/docs/correction.php
Normal 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 (A–H); 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>< 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 (A–H)</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
259
app/Views/docs/deletion.php
Normal 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 (A–G), 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 < 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>< 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 (A–G)</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>
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
|
||||
@ -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 row’s 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>
|
||||
|
||||
|
||||
389
app/Views/docs/inception.php
Normal file
389
app/Views/docs/inception.php
Normal 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 (> 1 MB) run steps 1–3 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 2–3</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>< 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 (A–S)</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 A–S 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 2–4 → 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 H–S 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>
|
||||
488
app/Views/docs/non-eb-claims.php
Normal file
488
app/Views/docs/non-eb-claims.php
Normal 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 = <that policy_type.id></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 ticket’s <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> (1–9)</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 template’s <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 3–1000 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>
|
||||
638
app/Views/docs/non-eb-opportunities.php
Normal file
638
app/Views/docs/non-eb-opportunities.php
Normal 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 lead’s 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 lead’s 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 lead’s 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 lead’s 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 product’s 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 lead’s <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 & 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 2–7 — 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 ~297–343) 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}&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}&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}&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}&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 4–6 — 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>
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
|
||||
259
app/Views/docs/si-enhancement.php
Normal file
259
app/Views/docs/si-enhancement.php
Normal 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 (A–E); <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>< 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 (A–E)</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 member’s 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>
|
||||
@ -361,6 +361,12 @@ $gst_total = 0;
|
||||
<a class="dropdown-item" onclick="showReGenerateConfirmation('<?= $employee['client_policy_id'];?>', '<?= $employee['emp_code'];?>')" ><i class="mdi mdi-refresh mr-2 text-muted font-18 vertical-middle"></i>Re-Generate E-Card</a>
|
||||
<?php } ?>
|
||||
|
||||
<?php if(strtolower($employee['relationship']) == 'self' && $employee['wellness_onboard'] == '0') { ?>
|
||||
<a class="dropdown-item" onclick="initiateWellnessOnboard('<?= $employee['client_policy_id'];?>', '<?= $employee['emp_code'];?>')" ><i class="mdi mdi-heart-pulse mr-2 text-muted font-18 vertical-middle"></i>Initiate Wellness Onboard</a>
|
||||
<?php }else if(strtolower($employee['relationship']) == 'self' && $employee['wellness_onboard'] != '0') { ?>
|
||||
<a class="dropdown-item" onclick="resetWellnessOnboard('<?= $employee['client_policy_id'];?>', '<?= $employee['emp_code'];?>')" ><i class="mdi mdi-restart mr-2 text-muted font-18 vertical-middle"></i>Reset Wellness Onboard</a>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label>Policy</label> <br />
|
||||
<select class="form-control" id="policies">
|
||||
<select class="form-control" id="policies" onchange="getOnboardedemployees(event)">
|
||||
<option value="0">Select</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -90,7 +90,7 @@
|
||||
<label>Status</label> <br />
|
||||
<select class="form-control" name="status2[]" id="status2" multiple>
|
||||
<option value="0">Select</option>
|
||||
<?php foreach ($status as $key => $value) { ?>
|
||||
<?php foreach ($status ?? [] as $key => $value) { ?>
|
||||
<option value="<?= $key ?>"
|
||||
<?php
|
||||
if ((!isset($getData) || in_array('active', $getData['status'])) && $key == 'active') {
|
||||
@ -116,6 +116,15 @@
|
||||
|
||||
</div>
|
||||
<div class="row justify-content-end">
|
||||
|
||||
<div id="resetWellnessOnboardBtn_div" class="col-auto" style="display: none;">
|
||||
<!-- <div id="categoryFilter_12"> -->
|
||||
<a href="#" id="resetWellnessOnboardBtn" class="btn btn-primary waves-effect waves-light"
|
||||
style="background-color: #F5FFFF; color:black; border:1px solid #00999E;"
|
||||
onclick="resetWellnessOnboard(this)">Wellness Onboard Reset</a>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<!-- <div id="categoryFilter_12"> -->
|
||||
<a href="#" class="btn btn-primary waves-effect waves-light"
|
||||
@ -148,6 +157,7 @@
|
||||
|
||||
<!-- end row -->
|
||||
<div id="loader" class="loader" style="display:none;">SPINNER</div>
|
||||
|
||||
<script>
|
||||
// document.addEventListener("DOMContentLoaded", function () {
|
||||
// const table = document.getElementById("tickets-table");
|
||||
@ -454,93 +464,6 @@
|
||||
$('#status2').trigger('change');
|
||||
});
|
||||
|
||||
|
||||
// $(document).ready(function() {
|
||||
// console.log("document loaded");
|
||||
// //fetchClientPolicies();
|
||||
// });
|
||||
|
||||
// $(window).on("load", function() {
|
||||
// console.log("window loaded");
|
||||
// fetchClientPolicies();
|
||||
// });
|
||||
|
||||
// function fetchClientPolicies() {
|
||||
// $('#loader').show();
|
||||
// var apiURL = '<?php echo base_url(); ?>' + 'util/clients-with-policies';
|
||||
// console.log('fetchClientPolicies');
|
||||
// // console.log(apiURL);
|
||||
// $.ajax({
|
||||
// url: apiURL,
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// "X-Requested-With": "XMLHttpRequest"
|
||||
// },
|
||||
// success: function(response) {
|
||||
// // console.log(response.code);
|
||||
// // console.log(response.dataStatus);
|
||||
// // console.log(response.data);
|
||||
// if (response.code === 200 && response.dataStatus === true && response.data !== "") {
|
||||
// try {
|
||||
// clientPolicies = (response.data);
|
||||
|
||||
// response.data.forEach(policy => {
|
||||
// // console.log('policies', policy.policies);
|
||||
// policy.policies.forEach(p => {
|
||||
// clientPoliciesWithBranch.policies.push(p);
|
||||
// });
|
||||
// });
|
||||
|
||||
// // console.log('1',clientPolicies);
|
||||
// appendClients(clientPolicies);
|
||||
|
||||
// //this function for reselect the policy
|
||||
// setTimeout(function() {
|
||||
// if (client_id != 0) {
|
||||
// var foundPolicies = clientPolicies.find(function(item) {
|
||||
// // console.log(typeof item.id)
|
||||
// return item.id == client_id;
|
||||
// });
|
||||
// appendBranch(foundPolicies.branchs);
|
||||
// }
|
||||
// }, 500);
|
||||
|
||||
// setTimeout(function() {
|
||||
// if (client_branch_id != 0) {
|
||||
|
||||
// var foundPolicies = clientPoliciesWithBranch.policies.filter(function(item) {
|
||||
// // console.log('item', item);
|
||||
// return item.branch_id === client_branch_id;
|
||||
// });
|
||||
|
||||
// if (foundPolicies) {
|
||||
// // console.log('foundPolicies', foundPolicies);
|
||||
// appendPolicies(foundPolicies);
|
||||
// } else {
|
||||
// console.log('No policies found for the selected client');
|
||||
// }
|
||||
// }
|
||||
// }, 500);
|
||||
// //end
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('Error parsing API response data:', error);
|
||||
// }
|
||||
// } else if (response.code === 404 && response.dataStatus === false) {
|
||||
// console.error('no data found', response);
|
||||
// } else {
|
||||
// console.error('Something went wrong!');
|
||||
// }
|
||||
// },
|
||||
// error: function(xhr, status, error) {
|
||||
// console.error('Error fetching data from API:', error);
|
||||
// }
|
||||
// });
|
||||
|
||||
// $('#loader').hide();
|
||||
// }
|
||||
|
||||
function appendClients(data) {
|
||||
var clientID = <?= isset($getData) ? $getData['client_id'] : '0' ?>;
|
||||
$.each(data, function(index, item) {
|
||||
@ -724,14 +647,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.getElementById('toggleIcon').addEventListener('click', function() {
|
||||
var icon = document.getElementById('icon');
|
||||
icon.classList.toggle('mdi-chevron-down');
|
||||
icon.classList.toggle('mdi-chevron-up');
|
||||
});
|
||||
|
||||
|
||||
function sendManualEcard(input) {
|
||||
console.log('sendManualEcard function called');
|
||||
|
||||
@ -793,4 +714,159 @@
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getOnboardedemployees(event)
|
||||
{
|
||||
console.log(event.target.id);
|
||||
var policy_id = (event.target.value);
|
||||
console.log('getOnboardedemployees called ' + policy_id);
|
||||
if(policy_id != 0 && policy_id != " " && policy_id != undefined)
|
||||
{
|
||||
var apiURL = '<?php echo base_url();?>' + 'checkWellnessOnboardStatus/' + policy_id + '?fetch_onboarded_employees=true';
|
||||
// console.log(apiURL);
|
||||
|
||||
$('.loader').fadeIn();
|
||||
$('.loader-mask').fadeIn();
|
||||
|
||||
$.ajax({
|
||||
url: apiURL,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
success: function(response) {
|
||||
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
|
||||
console.log('check wellness response', response);
|
||||
|
||||
if(response.data && response.data != 0) {
|
||||
var btn_txt = 'Wellness Onboard Reset ('+ response.data +')';
|
||||
$('#resetWellnessOnboardBtn').text(btn_txt);
|
||||
$('#resetWellnessOnboardBtn_div').show();
|
||||
|
||||
} else {
|
||||
console.log('wellness button remains disabled');
|
||||
$('#resetWellnessOnboardBtn_div').hide();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
console.error('Error fetching data from checkPolicyTermsAndRackRatesHasDefiend API:', error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resetWellnessOnboard($client_policy_id = null, $emp_code = null) {
|
||||
console.log('resetWellnessOnboard function called');
|
||||
|
||||
var client_id = $('#clients').val()
|
||||
var client_branch_id = $('#branch_id').val()
|
||||
var policy_id = $('#policies').val()
|
||||
|
||||
console.log('client_id', client_id);
|
||||
console.log('client_branch_id', client_branch_id);
|
||||
console.log('policy_id', policy_id);
|
||||
|
||||
if (client_id == 0 || client_branch_id == 0 || policy_id == 0) {
|
||||
toastr.warning('Please select the Client, Client Branch and Policy.', 'WARNING');
|
||||
return false
|
||||
}
|
||||
|
||||
if($client_policy_id && $emp_code) {
|
||||
var apiURL = '<?= base_url("resetWellnessOnboard/") ?>' + $client_policy_id + '/' + $emp_code;
|
||||
} else {
|
||||
var apiURL = '<?= base_url("resetWellnessOnboard/") ?>' + policy_id;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Do you want to reset Wellness Onboard?",
|
||||
showDenyButton: true,
|
||||
showCancelButton: false,
|
||||
confirmButtonText: "Yes,Reset",
|
||||
denyButtonText: "Don't Reset"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: apiURL,
|
||||
type: "GET",
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
console.log('resetWellnessOnboard', res)
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
toastr.success(res.message, 'Success');
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 800);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error(xhr.responseText);
|
||||
console.error(status, error);
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initiateWellnessOnboard($policy_id, $emp_code)
|
||||
{
|
||||
|
||||
if($policy_id == "" || $emp_code == "") {
|
||||
toastr.error('Please select policy to initiate wellness onboard', 'Error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if($policy_id != 0 && $policy_id != " " && $policy_id != undefined)
|
||||
{
|
||||
|
||||
var apiURL = '<?php echo base_url();?>' + 'initiateWellnessOnboard/' + $policy_id + '/' + $emp_code;
|
||||
// console.log(apiURL);
|
||||
|
||||
$('.loader').fadeIn();
|
||||
$('.loader-mask').fadeIn();
|
||||
|
||||
$.ajax({
|
||||
url: apiURL,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
success: function(response) {
|
||||
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
|
||||
console.log('initiate wellness response', response);
|
||||
toastr.success(response.message, 'Success');
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 800);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
console.error('Error initiate data from initiateWellnessOnboard API:', error);
|
||||
toastr.error(error, 'Error');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}else{
|
||||
|
||||
toastr.error('Please select policy to initiate wellness onboard', 'Error');
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@ -252,7 +252,7 @@ $ex_col_width_px = nhance_dt_column_widths_px($ex_header_labels, $ex_col_max_len
|
||||
<td><?= number_format((float) ($row['amount'] ?? 0), 2); ?></td>
|
||||
<td>
|
||||
<?php if (! empty($row['expense_date'])): ?>
|
||||
<?= date('d-m-Y', strtotime($row['expense_date'])); ?>
|
||||
<?= date('d/m/Y', strtotime($row['expense_date'])); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@ -23,7 +23,7 @@ if (!empty($fileList) && is_array($fileList)) {
|
||||
);
|
||||
$fl_col_max_len[4] = max($fl_col_max_len[4], mb_strlen($policy_display));
|
||||
$fl_col_max_len[5] = max($fl_col_max_len[5], mb_strlen((string) ($file['action'] ?? '')));
|
||||
$userTime = change_date_format($file['created_at'] ?? '', 'Y-m-d H:i:s', 'd M Y h:i a')
|
||||
$userTime = change_date_format($file['created_at'] ?? '', 'Y-m-d H:i:s', 'd/m/Y h:i a')
|
||||
. ' by '
|
||||
. ($file['first_name'] ?? '');
|
||||
$fl_col_max_len[6] = max($fl_col_max_len[6], mb_strlen($userTime));
|
||||
@ -203,7 +203,7 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
<!-- <td><?php echo isset($file['policy_name']) ? $file['policy_name'] : '' ?> - <?php echo isset($file['policy_no']) ? $file['policy_no'] : '' ?> - <?php echo isset($file['policy_type']) ? $file['policy_type'] : '' ?></td> -->
|
||||
<td><?php echo isset($file['policy_type']) ? $file['policy_type'] : '' ?> - <?php echo isset($file['policy_no']) ? $file['policy_no'] : '' ?></td>
|
||||
<td><?php echo $file['action'] ?></td>
|
||||
<td><?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd M Y h:i a') . ' by <strong>' . $file['first_name'] . '</strong>' ?>
|
||||
<td><?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd/m/Y h:i a') . ' by <strong>' . $file['first_name'] . '</strong>' ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
|
||||
@ -255,7 +255,7 @@ $hr_col_width_px = nhance_dt_column_widths_px($hr_header_labels, $hr_col_max_len
|
||||
<td><?php echo $file['branch_name'] ?></td>
|
||||
<td><?php echo $file['policy_no'] ?></td>
|
||||
<td><?php echo $file['file_action'] ?></td>
|
||||
<td><?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd M Y h:i A') . ' by <strong>' . $file['first_name'] . '</strong>' ?></td>
|
||||
<td><?php echo change_date_format($file['created_at'],'Y-m-d H:i:s', 'd/m/Y h:i A') . ' by <strong>' . $file['first_name'] . '</strong>' ?></td>
|
||||
<td><?php echo $file['status']; ?> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
|
||||
@ -2017,6 +2017,11 @@ body[data-sidebar-size="condensed"] .footer {
|
||||
<li>
|
||||
<a href="<?= base_url('/employee/retail-endorsement-list') ?>">Retail Endorsement</a>
|
||||
</li>
|
||||
<?php if(in_array(get_role_id(), [ACCOUNT_MANAGER_ROLE_ID])) { ?>
|
||||
<li>
|
||||
<a href="<?= base_url('/employee/tpaReportsDashboard') ?>">TPA Reports</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@ -2172,7 +2172,7 @@
|
||||
// <label for="first_year_${increment}">Year<span class="text-danger">*</span></label>
|
||||
// <select class="form-control first_year_" id="first_year_${increment}" name="first_year[]">
|
||||
// <option value="">Select Year</option>
|
||||
// <?php foreach ($lastFiveYears as $year) {echo "<option value='$year'>$year</option>";} ?>
|
||||
// <?php foreach ($lastFiveYears ?? [] as $year) {echo "<option value='$year'>$year</option>";} ?>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div class="form-group col-md-2">
|
||||
@ -2187,7 +2187,7 @@
|
||||
// <label for="first_cause_of_death_${increment}">Nature/Cause Of Death <span class="text-danger">*</span></label>
|
||||
// <select class="form-control" 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) {echo "<option value='$cause'>$death_value</option>";} ?>
|
||||
// <?php foreach ($causeOfDeath ?? [] as $cause => $death_value) {echo "<option value='$cause'>$death_value</option>";} ?>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div class="form-group col-md-2 lifeClaimFields" style="display:none;">
|
||||
@ -2198,7 +2198,7 @@
|
||||
// <label for="claim_type_${increment}">Claim Type<span class="text-danger">*</span></label>
|
||||
// <select class="form-control" id="claim_type_${increment}" name="claim_type[]">
|
||||
// <option value="">Select Claim Type</option>
|
||||
// <?php foreach ($gpaClaimType as $claimType => $claim_value) {echo "<option value='$claimType'>$claim_value</option>";} ?>
|
||||
// <?php foreach ($gpaClaimType ?? [] as $claimType => $claim_value) {echo "<option value='$claimType'>$claim_value</option>";} ?>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div class="form-group col-md-2">
|
||||
@ -2245,7 +2245,7 @@
|
||||
}
|
||||
|
||||
function getClaimHistoryLabel(leadType) {
|
||||
return String(leadType) === '1' ? 'Mortality Claims' : 'Claims History';
|
||||
return String(leadType) === '1' ? 'Mortality Experience' : 'Claims Experience';
|
||||
}
|
||||
|
||||
function shouldShowClaimHistorySwitch(leadType, policyTypeId) {
|
||||
@ -2304,7 +2304,7 @@
|
||||
<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) {
|
||||
<?php foreach ($lastFiveYears ?? [] as $year) {
|
||||
echo "<option value='$year'>$year</option>";
|
||||
} ?>
|
||||
</select>
|
||||
@ -2361,7 +2361,7 @@
|
||||
<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) {
|
||||
<?php foreach ($causeOfDeath ?? [] as $cause => $death_value) {
|
||||
echo "<option value='$cause'>$death_value</option>";
|
||||
} ?>
|
||||
</select>
|
||||
@ -2381,7 +2381,7 @@
|
||||
<label for="claim_type_${increment}">Claim Type<span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="claim_type_${increment}" name="claim_type[]">
|
||||
<option value="">Select Claim Type</option>
|
||||
<?php foreach ($gpaClaimType as $claimType => $claim_value) {
|
||||
<?php foreach ($gpaClaimType ?? [] as $claimType => $claim_value) {
|
||||
echo "<option value='$claimType'>$claim_value</option>";
|
||||
} ?>
|
||||
</select>
|
||||
|
||||
@ -400,7 +400,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
<?php
|
||||
$savedViewers = [];
|
||||
if (!empty($lead_edit_data['rfq_qcr_viewers'] ?? null)) {
|
||||
$decoded = json_decode($lead_edit_data['rfq_qcr_viewers'], true);
|
||||
$decoded = json_decode($lead_edit_data['rfq_qcr_viewers'] ?? "", true);
|
||||
if (is_array($decoded)) $savedViewers = $decoded;
|
||||
}
|
||||
?>
|
||||
@ -906,11 +906,11 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
})
|
||||
|
||||
function getClaimHistoryLabel(leadType) {
|
||||
return String(leadType) === '1' ? 'Mortality Claims' : 'Claims History';
|
||||
return String(leadType) === '1' ? 'Mortality Experience' : 'Claims Experience';
|
||||
}
|
||||
|
||||
function getClaimDetailsSectionTitle(leadType) {
|
||||
return String(leadType) === '1' ? 'Mortality Claims' : 'Claim Details';
|
||||
return String(leadType) === '1' ? 'Mortality Experience' : 'Claims Experience';
|
||||
}
|
||||
|
||||
function shouldShowNonEbClaimHistory(leadType) {
|
||||
@ -1402,7 +1402,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
<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) {
|
||||
<?php foreach ($lastFiveYears ?? [] as $year) {
|
||||
echo "<option value='$year'>$year</option>";
|
||||
} ?>
|
||||
</select>
|
||||
|
||||
@ -1,33 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Metabase Dashboard</title>
|
||||
<style>
|
||||
.meta-dashboard-demo .filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
align-items: flex-end;
|
||||
background: #eef5fa;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field {
|
||||
flex: 1 1 140px;
|
||||
min-width: 120px;
|
||||
max-width: 220px;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: #111;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field .select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field .select2-selection--single {
|
||||
height: 32px !important;
|
||||
min-height: 32px;
|
||||
border: 1px solid #d8e2ea !important;
|
||||
border-radius: 8px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field .select2-selection__rendered {
|
||||
line-height: 30px !important;
|
||||
padding-left: 10px !important;
|
||||
color: #333;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field .select2-selection__arrow {
|
||||
height: 30px !important;
|
||||
}
|
||||
.meta-dashboard-demo .filter-field .select2-container--disabled .select2-selection--single {
|
||||
background-color: #f5f7f9 !important;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.meta-dashboard-demo .dashboard-status {
|
||||
font-size: 12px;
|
||||
color: #5c6b7a;
|
||||
padding: 4px 0 10px;
|
||||
min-height: 20px;
|
||||
}
|
||||
.meta-dashboard-demo .dashboard-status.is-error { color: #c0392b; }
|
||||
.meta-dashboard-demo .dashboard-status.is-loading { color: #0a8794; }
|
||||
.meta-dashboard-demo #dashboard-container {
|
||||
min-height: 480px;
|
||||
}
|
||||
.meta-dashboard-demo #dashboard-container metabase-dashboard {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 480px;
|
||||
}
|
||||
.meta-dashboard-demo .dashboard-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: #7a8a99;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
border: 1px dashed #d0dce6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
<!-- Metabase embed script -->
|
||||
<script defer src="<?= esc($metabaseUrl) ?>/app/embed.js"></script>
|
||||
.emotion-65tole {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function defineMetabaseConfig(config) {
|
||||
window.metabaseConfig = config;
|
||||
<div class="row meta-dashboard-demo">
|
||||
<div class="col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<!-- <h4 class="mb-3"><?= esc($page_name ?? 'Metabase Dashboard') ?></h4> -->
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-field">
|
||||
<label for="filter-client">Client</label>
|
||||
<select id="filter-client" class="form-control form-control-sm meta-filter-select2">
|
||||
<option value="">Select</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<label for="filter-branch">Branch</label>
|
||||
<select id="filter-branch" class="form-control form-control-sm meta-filter-select2" disabled>
|
||||
<option value="">Select</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<label for="filter-policy">Policy</label>
|
||||
<select id="filter-policy" class="form-control form-control-sm meta-filter-select2" disabled>
|
||||
<option value="">Select</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-status" class="dashboard-status"></div>
|
||||
|
||||
<div id="dashboard-container">
|
||||
<div class="dashboard-placeholder" id="dashboard-placeholder">
|
||||
Select client, branch, and policy to load the dashboard.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function defineMetabaseConfig(config) {
|
||||
window.metabaseConfig = config;
|
||||
}
|
||||
|
||||
defineMetabaseConfig({
|
||||
theme: { preset: "light" },
|
||||
isGuest: true,
|
||||
instanceUrl: "<?= esc($metabaseUrl ?? '') ?>"
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
const CLIENTS_API = <?= json_encode(base_url('/util/getClientAndBranchAndPolicy')) ?>;
|
||||
const DASHBOARD_API = <?= json_encode($dashboardApiUrl ?? base_url('/metaTpaDashboardDemo')) ?>;
|
||||
const METABASE_URL = <?= json_encode($metabaseUrl ?? '') ?>;
|
||||
|
||||
const select2Options = {
|
||||
placeholder: 'Select',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: $(document.body)
|
||||
};
|
||||
|
||||
let branchByClient = {};
|
||||
let policyByClient = {};
|
||||
let embedScriptPromise = null;
|
||||
|
||||
const $elClient = $('#filter-client');
|
||||
const $elBranch = $('#filter-branch');
|
||||
const $elPolicy = $('#filter-policy');
|
||||
const elStatus = document.getElementById('dashboard-status');
|
||||
const elContainer = document.getElementById('dashboard-container');
|
||||
const elPlaceholder = document.getElementById('dashboard-placeholder');
|
||||
|
||||
function setStatus(message, type) {
|
||||
elStatus.textContent = message || '';
|
||||
elStatus.className = 'dashboard-status' + (type ? ' is-' + type : '');
|
||||
}
|
||||
|
||||
defineMetabaseConfig({
|
||||
theme: {
|
||||
preset: "light"
|
||||
},
|
||||
isGuest: true,
|
||||
instanceUrl: "<?= esc($metabaseUrl) ?>"
|
||||
function initSelect2($select, disabled) {
|
||||
if ($select.hasClass('select2-hidden-accessible')) {
|
||||
$select.select2('destroy');
|
||||
}
|
||||
$select.prop('disabled', false);
|
||||
$select.select2(select2Options);
|
||||
if (disabled) {
|
||||
$select.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildSelect($select, placeholder, disabled, items, getValue, getLabel) {
|
||||
if ($select.hasClass('select2-hidden-accessible')) {
|
||||
$select.select2('destroy');
|
||||
}
|
||||
$select.empty().append($('<option>', { value: '', text: placeholder }));
|
||||
(items || []).forEach(function (item) {
|
||||
$select.append($('<option>', {
|
||||
value: String(getValue(item)),
|
||||
text: getLabel(item)
|
||||
}));
|
||||
});
|
||||
$select.val('');
|
||||
initSelect2($select, disabled);
|
||||
}
|
||||
|
||||
function filtersComplete() {
|
||||
return !!($elClient.val() && $elBranch.val() && $elPolicy.val());
|
||||
}
|
||||
|
||||
function loadMetabaseEmbedScript() {
|
||||
if (window.customElements && window.customElements.get('metabase-dashboard')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (embedScriptPromise) {
|
||||
return embedScriptPromise;
|
||||
}
|
||||
embedScriptPromise = new Promise(function (resolve, reject) {
|
||||
const existing = document.querySelector('script[data-metabase-embed]');
|
||||
if (existing) {
|
||||
existing.addEventListener('load', function () { resolve(); }, { once: true });
|
||||
existing.addEventListener('error', reject, { once: true });
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = METABASE_URL + '/app/embed.js';
|
||||
script.defer = true;
|
||||
script.dataset.metabaseEmbed = '1';
|
||||
script.onload = function () { resolve(); };
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return embedScriptPromise;
|
||||
}
|
||||
|
||||
function clearDashboard() {
|
||||
const existing = elContainer.querySelector('metabase-dashboard');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
if (elPlaceholder) {
|
||||
elPlaceholder.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function renderDashboard(token) {
|
||||
clearDashboard();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
await loadMetabaseEmbedScript();
|
||||
if (elPlaceholder) {
|
||||
elPlaceholder.style.display = 'none';
|
||||
}
|
||||
const dash = document.createElement('metabase-dashboard');
|
||||
dash.setAttribute('token', token);
|
||||
dash.setAttribute('with-title', 'true');
|
||||
dash.setAttribute('with-downloads', 'true');
|
||||
elContainer.appendChild(dash);
|
||||
}
|
||||
|
||||
async function loadFilterData() {
|
||||
setStatus('Loading clients…', 'loading');
|
||||
try {
|
||||
const res = await fetch(CLIENTS_API, { credentials: 'same-origin' });
|
||||
const json = await res.json();
|
||||
if (!json.status) {
|
||||
setStatus('Could not load client list.', 'error');
|
||||
return;
|
||||
}
|
||||
branchByClient = json.branch_data || {};
|
||||
policyByClient = json.policyListByClient || {};
|
||||
|
||||
rebuildSelect($elClient, 'Select', false, json.client_data || [], function (c) { return c.id; }, function (c) {
|
||||
return c.client_name || c.client_short_name || ('Client ' + c.id);
|
||||
});
|
||||
rebuildSelect($elBranch, 'Select', true);
|
||||
rebuildSelect($elPolicy, 'Select', true);
|
||||
setStatus('');
|
||||
} catch (e) {
|
||||
setStatus('Failed to load filter options.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDashboard(policyId) {
|
||||
if (!filtersComplete()) {
|
||||
clearDashboard();
|
||||
setStatus('Select client, branch, and policy to load the dashboard.');
|
||||
return;
|
||||
}
|
||||
if (!policyId) {
|
||||
clearDashboard();
|
||||
setStatus('Select client, branch, and policy to load the dashboard.');
|
||||
return;
|
||||
}
|
||||
setStatus('Loading dashboard…', 'loading');
|
||||
try {
|
||||
const url = DASHBOARD_API + '?api=1&client_policy=' + encodeURIComponent(policyId);
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
const json = await res.json();
|
||||
if (json.status !== 'success' || !json.data || !json.data.metabaseToken) {
|
||||
clearDashboard();
|
||||
setStatus(json.message || 'No dashboard available for this policy.', 'error');
|
||||
return;
|
||||
}
|
||||
renderDashboard(json.data.metabaseToken);
|
||||
setStatus('');
|
||||
} catch (e) {
|
||||
clearDashboard();
|
||||
setStatus('Failed to load dashboard.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
$elClient.on('change', function () {
|
||||
const clientId = $(this).val();
|
||||
rebuildSelect($elBranch, 'Select', !clientId);
|
||||
rebuildSelect($elPolicy, 'Select', true);
|
||||
clearDashboard();
|
||||
setStatus(clientId ? '' : 'Select client, branch, and policy to load the dashboard.');
|
||||
|
||||
if (!clientId) {
|
||||
return;
|
||||
}
|
||||
const branches = branchByClient[clientId] || [];
|
||||
rebuildSelect($elBranch, 'Select', branches.length === 0, branches, function (b) { return b.id; }, function (b) {
|
||||
return b.branch_name || ('Branch ' + b.id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<metabase-dashboard
|
||||
token="<?= esc($metabaseToken) ?>"
|
||||
with-title="true"
|
||||
with-downloads="true">
|
||||
</metabase-dashboard>
|
||||
$elBranch.on('change', function () {
|
||||
const branchId = $(this).val();
|
||||
const clientId = $elClient.val();
|
||||
rebuildSelect($elPolicy, 'Select', !branchId);
|
||||
clearDashboard();
|
||||
setStatus(branchId ? '' : 'Select branch and policy to load the dashboard.');
|
||||
|
||||
</body>
|
||||
</html>
|
||||
if (!branchId || !clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const policiesForClient = policyByClient[clientId] || [];
|
||||
const policies = policiesForClient.filter(function (p) {
|
||||
return String(p.client_branch_id) === String(branchId);
|
||||
});
|
||||
|
||||
rebuildSelect($elPolicy, 'Select', policies.length === 0, policies, function (p) { return p.id; }, function (p) {
|
||||
const type = p.policy_type || '';
|
||||
const no = p.policy_no || '';
|
||||
return (type && no) ? (type + ' - ' + no) : (no || type || ('Policy ' + p.id));
|
||||
});
|
||||
if (policies.length === 0) {
|
||||
setStatus('No policies found for this branch.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
$elPolicy.on('change', function () {
|
||||
fetchDashboard($(this).val());
|
||||
});
|
||||
|
||||
initSelect2($elClient, false);
|
||||
initSelect2($elBranch, true);
|
||||
initSelect2($elPolicy, true);
|
||||
loadFilterData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<td><?= $row['nhance_claim_ref_no'] ?: 'N/A'; ?></td>
|
||||
<td><?= $row['short_name'] ?: ($row['client_name'] ?? 'N/A'); ?></td>
|
||||
<td><?= $row['insurer_short_name'] ?: ($row['insurer_name'] ?: 'N/A'); ?></td>
|
||||
<td><?= !empty($row['loss_date']) ? date('d-m-Y', strtotime($row['loss_date'])) : 'N/A'; ?></td>
|
||||
<td><?= !empty($row['loss_date']) ? date('d/m/Y', strtotime($row['loss_date'])) : 'N/A'; ?></td>
|
||||
<td><?= $row['nature_of_loss'] ?: 'N/A'; ?></td>
|
||||
|
||||
<td style="display:none;"><?= $row['acm_name'] ?? ''; ?></td>
|
||||
|
||||
@ -181,7 +181,7 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<script src="<?= base_url('public/assets/js/pages/policy_transaction_endorsement_form_validation.js') ?>"></script>
|
||||
<script src="<?= base_url('public/assets/js/pages/policy_transaction_endorsement_form_validation.js') ?>?v=20260529"></script>
|
||||
|
||||
<div class="tab-pane fade active show" id="form">
|
||||
<div class="row" id="endorsement_form">
|
||||
@ -297,7 +297,7 @@
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="addon_policy">Policy No<span id="base_danger"class="text-danger"></span></label>
|
||||
<input type="text" class="form-control" id="policy_no" name="policy_no"placeholder="Enter Policy No" readonly>
|
||||
<input type="text" class="form-control" id="policy_no" name="policy_no" placeholder="Enter Policy No" readonly data-parsley-validate="false" data-parsley-exclude-charset="true">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
@ -326,7 +326,7 @@
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="addon_policy">Endorsement No<span id="base_danger" class="text-danger"></span></label>
|
||||
<input type="text" class="form-control" id="endorsement_no" name="endorsement_no" placeholder="Enter Endorsement No" onchange="validateInput(this, 'policy_transaction', 'endorsement_no')">
|
||||
<input type="text" class="form-control" id="endorsement_no" name="endorsement_no" placeholder="Enter Endorsement No" data-parsley-validate="false" data-parsley-exclude-charset="true">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
@ -757,6 +757,9 @@
|
||||
// console.log('tpa',tpa);
|
||||
|
||||
$('#policy_no').val(policy_no);
|
||||
if (typeof window.clearEndorsementExcludedFieldsValidation === 'function') {
|
||||
window.clearEndorsementExcludedFieldsValidation('#endorsement_form_id');
|
||||
}
|
||||
$('#insurer_id').val(insurer);
|
||||
$('#tpa').val(tpa).change();
|
||||
// $('#cd_ac_no').val(cd_ac_no);
|
||||
@ -1206,6 +1209,9 @@
|
||||
$('#policy_no').val(res.data.policy_no);
|
||||
$('#action_type').val(res.data.action_type);
|
||||
$('#endorsement_no').val(res.data.endorsement_no);
|
||||
if (typeof window.clearEndorsementExcludedFieldsValidation === 'function') {
|
||||
window.clearEndorsementExcludedFieldsValidation('#endorsement_form_id');
|
||||
}
|
||||
$('#data_received_date').val(res.data.data_received_date);
|
||||
$('#policy_issue_date').val(res.data.policy_issue_date);
|
||||
$('#emp_count').val(res.data.emp_count);
|
||||
|
||||
@ -195,7 +195,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="<?= base_url('public/assets/js/pages/policy_transaction_endorsement_form_validation.js') ?>"></script>
|
||||
<script src="<?= base_url('public/assets/js/pages/policy_transaction_endorsement_form_validation.js') ?>?v=20260529"></script>
|
||||
|
||||
<div class="row" id="endorsement_form" style="display: none;">
|
||||
<div class="col-12">
|
||||
@ -310,7 +310,7 @@
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="addon_policy">Policy No<span id="base_danger"class="text-danger"></span></label>
|
||||
<input type="text" class="form-control" id="policy_no" name="policy_no"placeholder="Enter Policy No" readonly>
|
||||
<input type="text" class="form-control" id="policy_no" name="policy_no" placeholder="Enter Policy No" readonly data-parsley-validate="false" data-parsley-exclude-charset="true">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
@ -339,7 +339,7 @@
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="addon_policy">Endorsement No<span id="base_danger" class="text-danger"></span></label>
|
||||
<input type="text" class="form-control" id="endorsement_no" name="endorsement_no" placeholder="Enter Endorsement No" onchange="validateInput(this, 'policy_transaction', 'endorsement_no')">
|
||||
<input type="text" class="form-control" id="endorsement_no" name="endorsement_no" placeholder="Enter Endorsement No" data-parsley-validate="false" data-parsley-exclude-charset="true">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
@ -613,6 +613,9 @@
|
||||
// console.log('tpa',tpa);
|
||||
|
||||
$('#policy_no').val(policy_no);
|
||||
if (typeof window.clearEndorsementExcludedFieldsValidation === 'function') {
|
||||
window.clearEndorsementExcludedFieldsValidation('#endorsement_form_id');
|
||||
}
|
||||
$('#insurer_id').val(insurer);
|
||||
$('#tpa').val(tpa).change();
|
||||
// $('#cd_ac_no').val(cd_ac_no);
|
||||
@ -1037,6 +1040,9 @@
|
||||
$('#policy_no').val(res.data.policy_no);
|
||||
$('#action_type').val(res.data.action_type);
|
||||
$('#endorsement_no').val(res.data.endorsement_no);
|
||||
if (typeof window.clearEndorsementExcludedFieldsValidation === 'function') {
|
||||
window.clearEndorsementExcludedFieldsValidation('#endorsement_form_id');
|
||||
}
|
||||
$('#data_received_date').val(res.data.data_received_date);
|
||||
$('#policy_issue_date').val(res.data.policy_issue_date);
|
||||
$('#emp_count').val(res.data.emp_count);
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
<style>
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 8px;
|
||||
#scroll-horizontal-datatable thead th,
|
||||
#scroll-horizontal-datatable tbody td {
|
||||
padding: 4px 4px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table.dataTable tbody td {
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollHead table thead th,
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollBody table tbody td {
|
||||
padding: 4px 4px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
@ -76,7 +79,7 @@ table.dataTable tbody td {
|
||||
</div>
|
||||
|
||||
<div class="badge-container">
|
||||
Total Policy Count: <span class="text-primary"><?= $policy_count ?></span>
|
||||
Total Policy Count: <span class="text-primary"><?= $policy_count ?? 0 ?></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -111,7 +114,7 @@ table.dataTable tbody td {
|
||||
<th style="display: none;">Total Premium</th>
|
||||
<th>Agreed BP %</th>
|
||||
<th>Agreed TP %</th>
|
||||
<th style="display: true;">Rewards</th>
|
||||
<th>Rewards</th>
|
||||
<th>Agreed Amount</th>
|
||||
<th>Invoiced Amount</th>
|
||||
<th>Outstanding Amount</th>
|
||||
@ -199,7 +202,7 @@ table.dataTable tbody td {
|
||||
<td class="right-align-input" style="display: none;"><?php echo ($row['total_irda_amt'] != 0.00) ? ($row['total_premium'] ?: '0.00') : '0.00'; ?></td>
|
||||
<td class="right-align-input"><?php echo ($row['total_irda_amt'] != 0.00) ? ($row['agreed_bp_per'] ?: '0.00') : '0.00'; ?>%</td>
|
||||
<td class="right-align-input"><?php echo ($row['total_irda_amt'] != 0.00) ? ($row['agreed_tp_or_ter_per'] ?: '0.00') : '0.00'; ?>%</td>
|
||||
<td class="right-align-input" style="display: true;"><?php echo isset($row['reward']) ? $row['reward'] : '0.00'; ?></td>
|
||||
<td class="right-align-input"><?php echo isset($row['reward']) ? $row['reward'] : '0.00'; ?></td>
|
||||
|
||||
<?php
|
||||
// Below Line its old version i am removed. reason no value ['total_irda_amt'] means taken as ['exp_amt'] so.
|
||||
@ -335,8 +338,9 @@ $(document).ready(function() {
|
||||
var ticketsTable = $('#scroll-horizontal-datatable');
|
||||
|
||||
if (ticketsTable.length) {
|
||||
ticketsTable.DataTable({
|
||||
scrollX: true,
|
||||
nhanceListDataTableBeforeInit();
|
||||
var nhBdsReportTable = ticketsTable.DataTable(nhanceMergeListDataTableOptions({
|
||||
autoWidth: false,
|
||||
// dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" + // Filter left, buttons right
|
||||
// "<'row'<'col-sm-12'tr>>" +
|
||||
// "<'row'<'col-sm-5'i><'col-sm-7'p>>", // Pagination at the bottom, without page length selector
|
||||
@ -711,7 +715,9 @@ $(document).ready(function() {
|
||||
// var totalUnbilled = getUniqueUnbilled(27);
|
||||
// $('#total_unbilled').text(totalUnbilled.toFixed(2));
|
||||
}
|
||||
});
|
||||
}));
|
||||
nhanceListDataTableAfterInit();
|
||||
nhanceListDataTableBindAdjust(nhBdsReportTable);
|
||||
} else {
|
||||
console.error("Table atet found.");
|
||||
}
|
||||
|
||||
@ -257,7 +257,7 @@ $ret_col_width_px = nhance_dt_column_widths_px($ret_header_labels, $ret_col_max_
|
||||
<td><?php echo $employee['agent_name'] ?? 'N/A' ?></td>
|
||||
<td><?php echo $employee['manager_name'] ?? 'N/A' ?></td>
|
||||
<td><?php if (!empty($employee['created_at'])):
|
||||
$cd = date("j F Y", strtotime($employee['created_at']));
|
||||
$cd = date("j/F/Y", strtotime($employee['created_at']));
|
||||
$ct = date("h:i A", strtotime($employee['created_at']));
|
||||
echo $cd . "<br><span class='time'> " . $ct . "</span>";
|
||||
endif; ?></td>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
: [['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';
|
||||
$claimHistoryLabel = $leadType === 1 ? 'Mortality Experience' : 'Claims Experience';
|
||||
?>
|
||||
<?php if ($showClaimsSection) { ?>
|
||||
<div class="custom-control custom-switch">
|
||||
|
||||
@ -64,8 +64,8 @@
|
||||
&& 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';
|
||||
? 'Mortality Experience'
|
||||
: 'Claims Experience';
|
||||
?>
|
||||
<?php if ($showClaimHistorySwitch) { ?>
|
||||
<div class="custom-control custom-switch">
|
||||
|
||||
@ -901,7 +901,7 @@ async function viewDetail(id) {
|
||||
function renderCard(opps) {
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
let lead_type = o.lead_form_type == 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
|
||||
@ -269,7 +269,7 @@ $gst_total = 0;
|
||||
<td><?php echo $employee['emp_code']; ?></td>
|
||||
<td><?php echo $employee['relationship']; ?></td>
|
||||
<td><?php echo $employee['gender']; ?></td>
|
||||
<td><?php echo date('d/M/Y', strtotime($employee['dob'])); ?></td>
|
||||
<td><?php echo date('d/m/Y', strtotime($employee['dob'])); ?></td>
|
||||
<?php /*
|
||||
|
||||
<!-- <td><?php //echo isset($employee['policy_type']) ? $employee['policy_type'] : ''; ?> - <?php echo isset($employee['policy_no']) ? $employee['policy_no'] : ''; ?></td>
|
||||
|
||||
@ -188,7 +188,7 @@ beccause = dataTables_length and dataTables_paginate need in same line thats why
|
||||
<td class="text-left"><?php echo $row['assignee_name'] ? $row['assignee_name'] : 'N/A'; ?></td>
|
||||
<td class="text-left">
|
||||
<?php if (!empty($row['created_at'])):
|
||||
$cd = date("j M Y", strtotime($row['created_at']));
|
||||
$cd = date("j/m/Y", strtotime($row['created_at']));
|
||||
$ct = date("h:i A", strtotime($row['created_at']));
|
||||
echo $cd . "<br><span class='time'> " . $ct . "</span>";
|
||||
endif;
|
||||
@ -196,7 +196,7 @@ beccause = dataTables_length and dataTables_paginate need in same line thats why
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<?php if (!empty($row['updated_at'])):
|
||||
$ud = date("j M Y", strtotime($row['updated_at']));
|
||||
$ud = date("j/m/Y", strtotime($row['updated_at']));
|
||||
$ut = date("h:i A", strtotime($row['updated_at']));
|
||||
echo $ud . "<br><span class='time'> " . $ut . "</span>";
|
||||
endif;
|
||||
|
||||
@ -579,6 +579,42 @@
|
||||
var doa;
|
||||
var STATUS_SECTION_CLASSES = ['cda_ir', 'settled', 'approved', 'rejected', 'up_qdr', 'up_cnu', 'canceled', 'returned', 'payment', 'non_id'];
|
||||
var statusSectionInitialValues = {};
|
||||
// Cached full mapping of claim_status_id -> required fields.
|
||||
// The hidden input gets destructively reduced when the status dropdown changes,
|
||||
// so we capture the full map once at load time for use by frontend required-logic.
|
||||
var STATUS_REQUIRED_FIELDS_MAP = {};
|
||||
|
||||
function initStatusRequiredFieldsMap() {
|
||||
var raw = $('#extra_fields_array_for_validate').val();
|
||||
if (!raw) {
|
||||
STATUS_REQUIRED_FIELDS_MAP = {};
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
STATUS_REQUIRED_FIELDS_MAP = (parsed && typeof parsed === 'object') ? parsed : {};
|
||||
} catch (e) {
|
||||
STATUS_REQUIRED_FIELDS_MAP = {};
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredFieldsForSelectedStatus() {
|
||||
if (!STATUS_REQUIRED_FIELDS_MAP) {
|
||||
return [];
|
||||
}
|
||||
var selected = $('#claim_status_id').val();
|
||||
if (selected === undefined || selected === null || selected === '') {
|
||||
return [];
|
||||
}
|
||||
var fields = STATUS_REQUIRED_FIELDS_MAP[selected];
|
||||
if (Array.isArray(fields)) {
|
||||
return fields;
|
||||
}
|
||||
if (fields) {
|
||||
return [fields];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function captureSectionInitialValues(sectionClass) {
|
||||
if (!sectionClass) {
|
||||
@ -656,12 +692,40 @@
|
||||
return;
|
||||
}
|
||||
if (sectionClass === 'approved' || sectionClass === 'settled') {
|
||||
// The URL-or-File letter pair has its own validity rules (see enforceLetterPairRules).
|
||||
// We still need to enforce required on the other status-mandated fields
|
||||
// (e.g. utr_details, settled_date, approved_amount, approved_date), which the
|
||||
// previous early-return was skipping.
|
||||
enforceLetterPairRules();
|
||||
updateSectionRequiredLabels(sectionClass, hasSectionChanged(sectionClass));
|
||||
|
||||
var letterPairIds = sectionClass === 'approved'
|
||||
? ['approved_letter', 'approved_letter_file', 'approved_description']
|
||||
: ['settle_letter', 'settle_letter_file'];
|
||||
|
||||
var statusRequiredFields = getRequiredFieldsForSelectedStatus();
|
||||
var sectionChanged = hasSectionChanged(sectionClass);
|
||||
|
||||
$('.' + sectionClass).find('input, textarea, select').each(function () {
|
||||
var id = this.id;
|
||||
if (!id || letterPairIds.indexOf(id) !== -1) {
|
||||
return;
|
||||
}
|
||||
var statusRequires = statusRequiredFields.indexOf(id) !== -1;
|
||||
$(this).prop('required', statusRequires || sectionChanged);
|
||||
});
|
||||
|
||||
var statusMandatesSection = false;
|
||||
for (var i = 0; i < statusRequiredFields.length; i++) {
|
||||
if ($('.' + sectionClass + ' #' + statusRequiredFields[i]).length > 0) {
|
||||
statusMandatesSection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateSectionRequiredLabels(sectionClass, sectionChanged || statusMandatesSection);
|
||||
return;
|
||||
}
|
||||
var changed = hasSectionChanged(sectionClass);
|
||||
$('.' + sectionClass).find('input, textarea, select').each(function() {
|
||||
$('.' + sectionClass).find('input, textarea, select').each(function () {
|
||||
if (this.id === 'approved_description') {
|
||||
return;
|
||||
}
|
||||
@ -738,6 +802,7 @@
|
||||
allowInput: false
|
||||
});
|
||||
|
||||
initStatusRequiredFieldsMap();
|
||||
updateClaimStatusDisplay("<?= isset($ticket_data['claim_status_id']) ? $ticket_data['claim_status_id'] : 0 ?>")
|
||||
var extraFields = <?= json_encode(isset($extra_fields) ? $extra_fields : []); ?>;
|
||||
GlobelExtraFields = extraFields;
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
var end = moment(end, 'DD/MM/YYYY');
|
||||
|
||||
function cb(start, end) {
|
||||
$('#reportrange span').html(start.format('D-MM-YYYY') + ' - ' + end.format('D-MM-YYYY'));
|
||||
$('#reportrange span').html(start.format('D/MM/YYYY') + ' - ' + end.format('D/MM/YYYY'));
|
||||
$('#startDate').val(start.format('DD-MM-YYYY'));
|
||||
$('#endDate').val(end.format('DD-MM-YYYY'));
|
||||
}
|
||||
|
||||
@ -1,12 +1,79 @@
|
||||
|
||||
<?php
|
||||
helper('datatable_view');
|
||||
$vd_header_labels = [
|
||||
'Date', 'Record Date', 'Unit', 'Policy', 'Endorsement No', 'Sub Type',
|
||||
'Credit', 'Debit', 'Balance', 'Description', 'User',
|
||||
];
|
||||
$vd_col_count = count($vd_header_labels);
|
||||
$vd_col_max_len = array_fill(0, $vd_col_count, 0);
|
||||
for ($i = 0; $i < $vd_col_count; $i++) {
|
||||
$vd_col_max_len[$i] = mb_strlen($vd_header_labels[$i]);
|
||||
}
|
||||
$depositdata = $depositdata ?? [];
|
||||
$subTypeOptions = $subTypeOptions ?? [];
|
||||
foreach ($depositdata as $row) {
|
||||
$vd_col_max_len[0] = max($vd_col_max_len[0], mb_strlen(date('d-M-Y h:i A', strtotime((string) $row->created_at))));
|
||||
$rec = ! empty($row->record_date) ? date('d-M-Y', strtotime((string) $row->record_date)) : '-';
|
||||
$vd_col_max_len[1] = max($vd_col_max_len[1], mb_strlen($rec));
|
||||
$vd_col_max_len[2] = max($vd_col_max_len[2], mb_strlen(trim((string) ($row->unit ?? ' - '))));
|
||||
$policyType = $row->policy_type ?? '';
|
||||
$policyNo = $row->policy_no ?? '';
|
||||
$policyCell = ($policyType || $policyNo) ? trim($policyType . ' - ' . $policyNo, ' -') : '-';
|
||||
$vd_col_max_len[3] = max($vd_col_max_len[3], mb_strlen($policyCell));
|
||||
$vd_col_max_len[4] = max($vd_col_max_len[4], mb_strlen(strip_tags((string) ($row->endorsement_no ?? '-'))));
|
||||
$subLabel = isset($subTypeOptions[$row->sub_type]) ? (string) $subTypeOptions[$row->sub_type] : '';
|
||||
$vd_col_max_len[5] = max($vd_col_max_len[5], mb_strlen($subLabel));
|
||||
$credit = ($row->transaction_type ?? '') === 'Credit' ? (string) ($row->amount ?? '-') : '-';
|
||||
$debit = ($row->transaction_type ?? '') === 'Debit' ? (string) ($row->amount ?? '-') : '-';
|
||||
$vd_col_max_len[6] = max($vd_col_max_len[6], mb_strlen($credit));
|
||||
$vd_col_max_len[7] = max($vd_col_max_len[7], mb_strlen($debit));
|
||||
$vd_col_max_len[8] = max($vd_col_max_len[8], mb_strlen((string) ($row->balance ?? '')));
|
||||
$vd_col_max_len[9] = max($vd_col_max_len[9], mb_strlen((string) ($row->description ?? '')));
|
||||
$vd_col_max_len[10] = max($vd_col_max_len[10], mb_strlen((string) ($row->username ?? '')));
|
||||
}
|
||||
$vd_col_min_px = [120, 100, 72, 160, 110, 88, 150, 150, 170, 140, 100];
|
||||
$vd_col_max_px = [200, 120, 120, 360, 160, 140, 180, 180, 220, 400, 200];
|
||||
$vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len, $vd_col_min_px, $vd_col_max_px);
|
||||
?>
|
||||
<style>
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 8px;
|
||||
}
|
||||
table.dataTable tbody td {
|
||||
padding: 4px 4px !important;
|
||||
}
|
||||
<?php for ($i = 0; $i < $vd_col_count; $i++) : ?>
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollHead table thead th:nth-child(<?= $i + 1 ?>),
|
||||
#scroll-horizontal-datatable thead th:nth-child(<?= $i + 1 ?>) {
|
||||
min-width: <?= (int) $vd_col_width_px[$i] ?>px;
|
||||
width: <?= (int) $vd_col_width_px[$i] ?>px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
overflow: visible !important;
|
||||
}
|
||||
#scroll-horizontal-datatable tbody td:nth-child(<?= $i + 1 ?>) {
|
||||
width: <?= (int) $vd_col_width_px[$i] ?>px;
|
||||
max-width: <?= (int) $vd_col_width_px[$i] ?>px;
|
||||
min-width: <?= (int) $vd_col_width_px[$i] ?>px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
}
|
||||
<?php endfor; ?>
|
||||
#scroll-horizontal-datatable tbody td {
|
||||
padding: 5px 11px !important;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollHead table thead th,
|
||||
#scroll-horizontal-datatable thead th {
|
||||
padding: 5px 11px !important;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollHead table,
|
||||
#scroll-horizontal-datatable_wrapper .dataTables_scrollBody table {
|
||||
table-layout: fixed;
|
||||
width: 100% !important;
|
||||
}
|
||||
#List-page .card-body > .table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.deposit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -31,6 +98,165 @@
|
||||
}
|
||||
.dataTables_length label {height: 21px !important;}
|
||||
#scroll-horizontal-datatable tbody tr:hover { background-color: #e0e0e0; }
|
||||
.deposit-inline-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.deposit-inline-input {
|
||||
width: 150px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.deposit-inline-input-disabled {
|
||||
background-color: #f4f5f7 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.deposit-balance-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.deposit-history-icon {
|
||||
color: #0ea5b7;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
#moreInfoModal .modal-dialog {
|
||||
max-width: 520px;
|
||||
}
|
||||
#moreInfoModal .modal-content {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 14px;
|
||||
color: #111827;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
#moreInfoModal .modal-header,
|
||||
#moreInfoModal .modal-footer {
|
||||
border-color: #e5e7eb;
|
||||
padding: 14px 18px;
|
||||
background: #ffffff;
|
||||
}
|
||||
#moreInfoModal .modal-body {
|
||||
padding: 0 18px 16px;
|
||||
background: #ffffff;
|
||||
}
|
||||
#moreInfoModal .deposit-history-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#moreInfoModal .deposit-history-title-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
background: #0ea5b7;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
#moreInfoModal .modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
#moreInfoModal .deposit-history-close-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #6b7280;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
#moreInfoModal .deposit-history-close-btn:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
#moreInfoModal .deposit-history-amount-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
#moreInfoModal .deposit-history-amount-card {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
#moreInfoModal .deposit-history-amount-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
#moreInfoModal .deposit-history-amount-value {
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
word-break: break-word;
|
||||
}
|
||||
#moreInfoModal .deposit-history-meta-box {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f9fafb;
|
||||
}
|
||||
#moreInfoModal .deposit-history-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
#moreInfoModal .deposit-history-meta-row + .deposit-history-meta-row {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
#moreInfoModal .deposit-history-meta-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
#moreInfoModal .deposit-history-meta-value {
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
#moreInfoModal .deposit-history-footer-btn {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #374151;
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
#moreInfoModal .deposit-history-footer-btn:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
.deposit-inline-actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.deposit-inline-actions .btn {
|
||||
padding: 2px 6px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.deposit-title {
|
||||
font-size: 22px;
|
||||
@ -123,7 +349,7 @@
|
||||
</div> -->
|
||||
|
||||
<div class="table-responsive">
|
||||
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="scroll-horizontal-datatable">
|
||||
<table data-custom-table-css="table" class="table mb-0 nowrap w-100" cellspacing="0" id="scroll-horizontal-datatable">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="font-weight-medium">Date</th>
|
||||
@ -141,7 +367,26 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($depositdata as $row) { ?>
|
||||
<tr id="<?php echo $row->id;?>">
|
||||
<?php
|
||||
$isCreditRow = ($row->transaction_type == 'Credit');
|
||||
$isDebitRow = ($row->transaction_type == 'Debit');
|
||||
$creditAmount = $isCreditRow ? (float) $row->amount : 0;
|
||||
$debitAmount = $isDebitRow ? (float) $row->amount : 0;
|
||||
$hasAmtHistory = isset($row->new_amt) && trim((string) $row->new_amt) !== '';
|
||||
$historyOldAmt = trim((string) ($row->old_amt ?? '-')) !== '' ? (string) $row->old_amt : '-';
|
||||
$historyNewAmt = trim((string) ($row->new_amt ?? '-')) !== '' ? (string) $row->new_amt : '-';
|
||||
$historyUpdatedBy = trim((string) ($row->amt_updated_by_name ?? '')) !== ''
|
||||
? (string) $row->amt_updated_by_name
|
||||
: (!empty($row->amt_updated_by) ? ('User #' . $row->amt_updated_by) : '-');
|
||||
$historyUpdatedAt = !empty($row->amt_updated_at)
|
||||
? date('d M Y, h:i A', strtotime((string) $row->amt_updated_at))
|
||||
: '-';
|
||||
?>
|
||||
<tr id="<?php echo $row->id;?>"
|
||||
data-row-id="<?= (int) $row->id ?>"
|
||||
data-transaction-type="<?= esc((string) $row->transaction_type, 'attr') ?>"
|
||||
data-credit="<?= esc((string) $creditAmount, 'attr') ?>"
|
||||
data-debit="<?= esc((string) $debitAmount, 'attr') ?>">
|
||||
<td><?php echo date('d-M-Y h:i A', strtotime($row->created_at)); ?></td>
|
||||
<td><?php echo isset($row->record_date)? date('d-M-Y', strtotime($row->record_date)):'-' ?></td>
|
||||
<td><?php echo $row->unit ?? ' - '; ?></td>
|
||||
@ -154,17 +399,46 @@
|
||||
if ($policyType || $policyNo) {
|
||||
echo trim($policyType . ' - ' . $policyNo, ' -');
|
||||
} else {
|
||||
echo '<center> - </center>';
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo $row->endorsement_no ?? '<center> - </center>'; ?></td>
|
||||
<td><?php echo $row->endorsement_no ?? '-'; ?></td>
|
||||
<td><?php echo isset($subTypeOptions[$row->sub_type]) ? $subTypeOptions[$row->sub_type] : ''; ?>
|
||||
</td>
|
||||
<td><?php echo ($row->transaction_type == 'Credit') ? $row->amount : '-'; ?></td>
|
||||
<td><?php echo ($row->transaction_type == 'Debit') ? $row->amount : '-'; ?></td>
|
||||
<td><?php echo $row->balance; ?></td>
|
||||
<td><?php echo $row->description; ?></td>
|
||||
<td class="deposit-inline-edit-cell" data-type="credit">
|
||||
<div class="deposit-inline-cell">
|
||||
<input type="text" class="form-control form-control-sm deposit-inline-input <?= $isCreditRow ? '' : 'deposit-inline-input-disabled' ?>" placeholder="-" value="<?= $isCreditRow ? esc((string) $row->amount, 'attr') : '' ?>" <?= $isCreditRow ? '' : 'disabled' ?>>
|
||||
</div>
|
||||
</td>
|
||||
<td class="deposit-inline-edit-cell" data-type="debit">
|
||||
<div class="deposit-inline-cell">
|
||||
<input type="text" class="form-control form-control-sm deposit-inline-input <?= $isDebitRow ? '' : 'deposit-inline-input-disabled' ?>" placeholder="-" value="<?= $isDebitRow ? esc((string) $row->amount, 'attr') : '' ?>" <?= $isDebitRow ? '' : 'disabled' ?>>
|
||||
</div>
|
||||
</td>
|
||||
<td class="deposit-balance-cell">
|
||||
<span class="deposit-balance-wrap">
|
||||
<span><?php echo $row->balance; ?></span>
|
||||
<?php if ($hasAmtHistory) : ?>
|
||||
<i class="mdi mdi-information-outline deposit-history-icon js-deposit-history-trigger"
|
||||
data-old-amt="<?= esc($historyOldAmt, 'attr') ?>"
|
||||
data-new-amt="<?= esc($historyNewAmt, 'attr') ?>"
|
||||
data-updated-by="<?= esc($historyUpdatedBy, 'attr') ?>"
|
||||
data-updated-at="<?= esc($historyUpdatedAt, 'attr') ?>"
|
||||
onclick="openDepositHistoryModal(this); return false;"
|
||||
aria-label="Amount change history"
|
||||
tabindex="0"></i>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="deposit-description-cell"
|
||||
data-toggle="tooltip"
|
||||
data-bs-toggle="tooltip"
|
||||
data-placement="top"
|
||||
data-bs-placement="top"
|
||||
title="<?= esc((string) ($row->description ?? ''), 'attr') ?>">
|
||||
<?php echo $row->description; ?>
|
||||
</td>
|
||||
<td><?php echo $row->username; ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
@ -178,6 +452,95 @@
|
||||
<!-- end col -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="moreInfoModal" tabindex="-1" aria-labelledby="depositHistoryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="deposit-history-title-wrap">
|
||||
<span class="deposit-history-title-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-history"></i>
|
||||
</span>
|
||||
<h5 class="modal-title" id="depositHistoryModalLabel">Amount change history</h5>
|
||||
</div>
|
||||
<button type="button" class="deposit-history-close-btn" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="deposit-history-amount-grid">
|
||||
<div class="deposit-history-amount-card">
|
||||
<div class="deposit-history-amount-label">Old amount</div>
|
||||
<div class="deposit-history-amount-value" id="history_old_amt">-</div>
|
||||
</div>
|
||||
<div class="deposit-history-amount-card">
|
||||
<div class="deposit-history-amount-label">New amount</div>
|
||||
<div class="deposit-history-amount-value" id="history_new_amt">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deposit-history-meta-box">
|
||||
<div class="deposit-history-meta-row">
|
||||
<div class="deposit-history-meta-label">
|
||||
<i class="mdi mdi-account-outline"></i>
|
||||
<span>Changed by</span>
|
||||
</div>
|
||||
<div class="deposit-history-meta-value" id="history_updated_by">-</div>
|
||||
</div>
|
||||
<div class="deposit-history-meta-row">
|
||||
<div class="deposit-history-meta-label">
|
||||
<i class="mdi mdi-calendar-month-outline"></i>
|
||||
<span>Changed at</span>
|
||||
</div>
|
||||
<div class="deposit-history-meta-value" id="history_updated_at">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="deposit-history-footer-btn" data-dismiss="modal" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function formatDepositHistoryAmount(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '-';
|
||||
}
|
||||
var raw = String(value).trim();
|
||||
if (raw === '' || raw === '-') {
|
||||
return '-';
|
||||
}
|
||||
var num = parseFloat(raw.replace(/,/g, ''));
|
||||
if (isNaN(num)) {
|
||||
return raw;
|
||||
}
|
||||
return '₹' + num.toLocaleString('en-IN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function openDepositHistoryModal(iconEl) {
|
||||
var $icon = $(iconEl);
|
||||
$('#history_old_amt').text(formatDepositHistoryAmount($icon.data('old-amt')));
|
||||
$('#history_new_amt').text(formatDepositHistoryAmount($icon.data('new-amt')));
|
||||
$('#history_updated_by').text($icon.data('updated-by') || '-');
|
||||
$('#history_updated_at').text($icon.data('updated-at') || '-');
|
||||
|
||||
var modalElement = document.getElementById('moreInfoModal');
|
||||
if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal !== 'undefined' && modalElement) {
|
||||
var myModal = new bootstrap.Modal(document.getElementById('moreInfoModal'));
|
||||
myModal.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof $('#moreInfoModal').modal === 'function') {
|
||||
$('#moreInfoModal').modal('show');
|
||||
return;
|
||||
}
|
||||
|
||||
toastr.warning('History view is not available right now.');
|
||||
}
|
||||
</script>
|
||||
<!-- Add this modal markup at the end of your HTML body -->
|
||||
<!-- Button to trigger modal -->
|
||||
|
||||
@ -304,8 +667,42 @@
|
||||
allowInput: false,
|
||||
|
||||
});
|
||||
$('#scroll-horizontal-datatable').DataTable({
|
||||
scrollX: true,
|
||||
nhanceListDataTableBeforeInit();
|
||||
var depositPageStorageKey = 'view_deposit_datatable_page_' + window.location.pathname + window.location.search;
|
||||
function getStoredDepositPage() {
|
||||
var page = parseInt(sessionStorage.getItem(depositPageStorageKey), 10);
|
||||
return isNaN(page) || page < 0 ? 0 : page;
|
||||
}
|
||||
function storeDepositPage(pageNumber) {
|
||||
sessionStorage.setItem(depositPageStorageKey, String(pageNumber));
|
||||
}
|
||||
window.storeCurrentDepositTablePage = function () {
|
||||
try {
|
||||
var table = $('#scroll-horizontal-datatable').DataTable();
|
||||
storeDepositPage(table.page());
|
||||
} catch (e) {
|
||||
// No-op when table is not available.
|
||||
}
|
||||
};
|
||||
function initDepositDescriptionTooltips() {
|
||||
var selector = '#scroll-horizontal-datatable [data-bs-toggle="tooltip"]';
|
||||
if (typeof bootstrap !== 'undefined' && typeof bootstrap.Tooltip !== 'undefined') {
|
||||
document.querySelectorAll(selector).forEach(function (el) {
|
||||
var existing = bootstrap.Tooltip.getInstance(el);
|
||||
if (existing) {
|
||||
existing.dispose();
|
||||
}
|
||||
new bootstrap.Tooltip(el, { container: 'body' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof $.fn.tooltip === 'function') {
|
||||
var $targets = $('#scroll-horizontal-datatable [data-toggle="tooltip"]');
|
||||
$targets.tooltip('dispose');
|
||||
$targets.tooltip({ container: 'body' });
|
||||
}
|
||||
}
|
||||
var vdDepositTable = $('#scroll-horizontal-datatable').DataTable(nhanceMergeListDataTableOptions({
|
||||
// dom: "<'row'<'col-sm-2'f><'col-sm-10 text-right'B>>" +
|
||||
// "<'row'<'col-sm-12'tr>>" +
|
||||
// "<'row'<'col-sm-5'i><'col-sm-7'p>>",
|
||||
@ -363,6 +760,17 @@
|
||||
myModal.show();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '<i class="mdi mdi-content-save"></i><span class=" btn-custom"> Save </span>',
|
||||
className: 'btn app-btn-primary mr-2 deposit-top-save-btn d-none',
|
||||
action: function () {
|
||||
if (typeof window.saveDepositInlineChanges === 'function') {
|
||||
window.saveDepositInlineChanges();
|
||||
} else {
|
||||
toastr.warning('Save handler is not ready yet.');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
extend: 'collection',
|
||||
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
|
||||
@ -400,8 +808,35 @@
|
||||
emptyTable: '<div class="text-center text-muted">No Data found</div>'
|
||||
},
|
||||
ordering: false,
|
||||
paging: true
|
||||
});
|
||||
paging: true,
|
||||
autoWidth: false,
|
||||
columnDefs: [
|
||||
<?php for ($i = 0; $i < $vd_col_count; $i++) : ?>
|
||||
{ targets: <?= $i ?>, width: '<?= (int) $vd_col_width_px[$i] ?>px' },
|
||||
<?php endfor; ?>
|
||||
],
|
||||
}));
|
||||
vdDepositTable.on('page.dt', function () {
|
||||
storeDepositPage(vdDepositTable.page());
|
||||
});
|
||||
initDepositDescriptionTooltips();
|
||||
vdDepositTable.on('draw', function () {
|
||||
initDepositDescriptionTooltips();
|
||||
});
|
||||
var selectedPage = getStoredDepositPage();
|
||||
if (selectedPage > 0 && selectedPage < vdDepositTable.page.info().pages) {
|
||||
vdDepositTable.page(selectedPage).draw('page');
|
||||
}
|
||||
nhanceListDataTableAfterInit();
|
||||
nhanceListDataTableBindAdjust(vdDepositTable);
|
||||
|
||||
$(document).on('click keydown', '.js-deposit-history-trigger', function (e) {
|
||||
if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
openDepositHistoryModal(this);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -509,6 +944,9 @@
|
||||
// Handle success response
|
||||
toastr.success("Transaction saved successfully", "Success");
|
||||
// Reload the page or perform other actions as needed
|
||||
if (typeof window.storeCurrentDepositTablePage === 'function') {
|
||||
window.storeCurrentDepositTablePage();
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@ -540,6 +978,9 @@
|
||||
toastr.error('An unexpected error occurred. Please try again later.', 'Error');
|
||||
}
|
||||
|
||||
if (typeof window.storeCurrentDepositTablePage === 'function') {
|
||||
window.storeCurrentDepositTablePage();
|
||||
}
|
||||
location.reload(); // Uncomment if you need to reload the page on error
|
||||
}
|
||||
});
|
||||
@ -566,4 +1007,237 @@
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var numericRegex = /^[0-9]+(\.[0-9]{1,2})?$/;
|
||||
|
||||
function formatAmount(value) {
|
||||
var num = parseFloat(value);
|
||||
if (isNaN(num)) {
|
||||
return '0.00';
|
||||
}
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
function normalizeInput($input) {
|
||||
var value = ($input.val() || '').replace(/[^0-9.]/g, '');
|
||||
var parts = value.split('.');
|
||||
if (parts.length > 2) {
|
||||
value = parts[0] + '.' + parts.slice(1).join('');
|
||||
parts = value.split('.');
|
||||
}
|
||||
if (parts[1] && parts[1].length > 2) {
|
||||
value = parts[0] + '.' + parts[1].slice(0, 2);
|
||||
}
|
||||
$input.val(value);
|
||||
}
|
||||
|
||||
function setRowDisplay($row, creditAmount, debitAmount) {
|
||||
var $creditCell = $row.find('td[data-type="credit"]');
|
||||
var $debitCell = $row.find('td[data-type="debit"]');
|
||||
var creditNum = parseFloat(creditAmount) || 0;
|
||||
var debitNum = parseFloat(debitAmount) || 0;
|
||||
|
||||
$row.attr('data-credit', creditNum);
|
||||
$row.attr('data-debit', debitNum);
|
||||
$creditCell.find('.deposit-inline-input').val(creditNum > 0 ? formatAmount(creditNum) : '');
|
||||
$debitCell.find('.deposit-inline-input').val(debitNum > 0 ? formatAmount(debitNum) : '');
|
||||
}
|
||||
|
||||
function recalculateVisibleBalances() {
|
||||
var running = 0;
|
||||
var rows = $('#scroll-horizontal-datatable tbody tr').get().reverse();
|
||||
$(rows).each(function () {
|
||||
var $row = $(this);
|
||||
var credit = parseFloat($row.attr('data-credit')) || 0;
|
||||
var debit = parseFloat($row.attr('data-debit')) || 0;
|
||||
running += credit;
|
||||
running -= debit;
|
||||
$row.find('.deposit-balance-cell').text(formatAmount(running));
|
||||
});
|
||||
}
|
||||
|
||||
function getTopSaveButton() {
|
||||
return $('.deposit-top-save-btn');
|
||||
}
|
||||
|
||||
function hasPendingChanges() {
|
||||
var pending = false;
|
||||
$('#scroll-horizontal-datatable tbody tr').each(function () {
|
||||
var $row = $(this);
|
||||
var creditInputVal = ($row.find('td[data-type="credit"] .deposit-inline-input').val() || '').trim();
|
||||
var debitInputVal = ($row.find('td[data-type="debit"] .deposit-inline-input').val() || '').trim();
|
||||
var originalCredit = parseFloat($row.attr('data-credit')) || 0;
|
||||
var originalDebit = parseFloat($row.attr('data-debit')) || 0;
|
||||
var inputCredit = creditInputVal === '' ? 0 : parseFloat(creditInputVal);
|
||||
var inputDebit = debitInputVal === '' ? 0 : parseFloat(debitInputVal);
|
||||
|
||||
if (isNaN(inputCredit) || isNaN(inputDebit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(inputCredit - originalCredit) > 0.0001 || Math.abs(inputDebit - originalDebit) > 0.0001) {
|
||||
pending = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return pending;
|
||||
}
|
||||
|
||||
function updateTopSaveButtonVisibility() {
|
||||
var $saveBtn = getTopSaveButton();
|
||||
if (!$saveBtn.length) {
|
||||
return;
|
||||
}
|
||||
$saveBtn.toggleClass('d-none', !hasPendingChanges());
|
||||
}
|
||||
|
||||
$(document).on('input', '.deposit-inline-input', function () {
|
||||
var $input = $(this);
|
||||
normalizeInput($input);
|
||||
|
||||
updateTopSaveButtonVisibility();
|
||||
});
|
||||
|
||||
function collectChanges() {
|
||||
var changes = [];
|
||||
var hasValidationError = false;
|
||||
$('#scroll-horizontal-datatable tbody tr').each(function () {
|
||||
var $row = $(this);
|
||||
var creditInputVal = ($row.find('td[data-type="credit"] .deposit-inline-input').val() || '').trim();
|
||||
var debitInputVal = ($row.find('td[data-type="debit"] .deposit-inline-input').val() || '').trim();
|
||||
var originalCredit = parseFloat($row.attr('data-credit')) || 0;
|
||||
var originalDebit = parseFloat($row.attr('data-debit')) || 0;
|
||||
var hasCredit = creditInputVal !== '';
|
||||
var hasDebit = debitInputVal !== '';
|
||||
|
||||
if (!hasCredit && !hasDebit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasCredit && hasDebit) {
|
||||
toastr.warning('Please fill only one of Credit or Debit per row.');
|
||||
hasValidationError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
var type = hasCredit ? 'credit' : 'debit';
|
||||
var value = hasCredit ? creditInputVal : debitInputVal;
|
||||
var amount = parseFloat(value);
|
||||
var isChanged = (type === 'credit')
|
||||
? (Math.abs(amount - originalCredit) > 0.0001)
|
||||
: (Math.abs(amount - originalDebit) > 0.0001);
|
||||
|
||||
// Skip untouched rows (important for legacy 0/-ve values)
|
||||
if (!isChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!numericRegex.test(value)) {
|
||||
toastr.warning('Enter a valid amount (up to 2 decimals).');
|
||||
hasValidationError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
toastr.warning('Amount must be greater than 0.');
|
||||
hasValidationError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
rowId: $row.data('row-id'),
|
||||
type: type,
|
||||
amount: amount,
|
||||
$row: $row
|
||||
});
|
||||
});
|
||||
|
||||
if (hasValidationError) {
|
||||
return false;
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
window.saveDepositInlineChanges = function () {
|
||||
var changes = collectChanges();
|
||||
if (!Array.isArray(changes) || changes.length === 0) {
|
||||
if (changes !== false) {
|
||||
toastr.info('No changes to save.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var clientId = '<?= esc((string) $clientData->id, 'js') ?>';
|
||||
var insurerId = '<?= esc((string) $insurerName->id, 'js') ?>';
|
||||
var cdAcPk = $('#cd_ac_pk').val();
|
||||
|
||||
var completed = 0;
|
||||
function saveNext(index) {
|
||||
if (index >= changes.length) {
|
||||
toastr.success('Saved ' + completed + ' change(s) successfully.');
|
||||
setTimeout(function () {
|
||||
if (typeof window.storeCurrentDepositTablePage === 'function') {
|
||||
window.storeCurrentDepositTablePage();
|
||||
}
|
||||
location.reload();
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
|
||||
var change = changes[index];
|
||||
var transactionType = (change.type === 'credit') ? 'Credit' : 'Debit';
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '<?= base_url() . "client/update_deposit_amount" ?>',
|
||||
data: {
|
||||
deposit_id: change.rowId,
|
||||
amount: change.amount,
|
||||
transaction_type: transactionType,
|
||||
client_id: clientId,
|
||||
insurer_id: insurerId,
|
||||
cd_ac_pk: cdAcPk
|
||||
},
|
||||
success: function () {
|
||||
if (transactionType === 'Credit') {
|
||||
setRowDisplay(change.$row, change.amount, 0);
|
||||
} else {
|
||||
setRowDisplay(change.$row, 0, change.amount);
|
||||
}
|
||||
completed++;
|
||||
saveNext(index + 1);
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400 && xhr.responseText) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
toastr.error(response.message || 'Validation failed.');
|
||||
} catch (e) {
|
||||
toastr.error('Validation failed.');
|
||||
}
|
||||
} else if (xhr.status === 404) {
|
||||
toastr.error('Transaction not found for this account.');
|
||||
} else {
|
||||
toastr.error('Unable to update transaction right now.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveNext(0);
|
||||
};
|
||||
|
||||
$(document).on('keydown', '.deposit-inline-input', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (typeof window.saveDepositInlineChanges === 'function') {
|
||||
window.saveDepositInlineChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateTopSaveButtonVisibility();
|
||||
});
|
||||
</script>
|
||||
@ -1,3 +1,5 @@
|
||||
<?php $page_name = $page_name ?? ""; ?>
|
||||
|
||||
<style>
|
||||
|
||||
.table-container {
|
||||
@ -496,8 +498,8 @@
|
||||
</div>
|
||||
|
||||
<div class="col-2" style="position: relative;right: 255px;">
|
||||
<?php if($lead_data['demography_file_status'] == "failed") { ?>
|
||||
<a href="<?= base_url('util/getMemberDataExcelFileErrors?lead_id=').$lead_data['id'] ?>"
|
||||
<?php if($lead_data['demography_file_status'] ?? "" == "failed") { ?>
|
||||
<a href="<?= base_url('util/getMemberDataExcelFileErrors?lead_id=') . ($lead_data['id'] ?? '') ?>"
|
||||
class="mdi mdi-information-outline text-danger"
|
||||
style="cursor: pointer; font-size: 27px"
|
||||
data-toggle="tooltip"
|
||||
@ -801,7 +803,7 @@
|
||||
<div class="form-group col-md-4">
|
||||
<label for="cc">CC </label>
|
||||
<select class="form-control" id="cc" name="cc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
<?php if ($page_name == "RFQ" || in_array($user['role'],[1,5]) || in_array($user['team_id'],[6,7])) { ?>
|
||||
@ -981,7 +983,7 @@
|
||||
<div class="form-group col-md-6">
|
||||
<label for="placement_cc">CC </label>
|
||||
<select class="form-control" id="placement_cc" name="placement_cc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
@ -1356,7 +1358,7 @@ let intervalId;
|
||||
$(document).ready(function () {
|
||||
console.log("Document ready");
|
||||
|
||||
var demography_file_status_for_hide_and_show = '<?= $lead_data['demography_file_status'] ?>';
|
||||
var demography_file_status_for_hide_and_show = '<?= $lead_data['demography_file_status'] ?? "" ?>';
|
||||
toggleButtons(demography_file_status_for_hide_and_show);
|
||||
|
||||
let lead_status = '<?= isset($lead_data['status']) ? $lead_data['status'] : '' ?>';
|
||||
@ -1404,7 +1406,7 @@ $('.remove-icon').on('click', function() {
|
||||
|
||||
var currentThrdottedMenu = '';
|
||||
var proposal_colum_count = 1;
|
||||
var demography_file_status = '<?= $lead_data['demography_file_status'] ?>';
|
||||
var demography_file_status = '<?= $lead_data['demography_file_status'] ?? "" ?>';
|
||||
|
||||
var over_all_column_data = {
|
||||
"Proposal 1": {
|
||||
@ -1818,7 +1820,7 @@ function addSuggestionsToTable() {
|
||||
<span class="dropdown" onclick="showThreeDottedMenu(event)">
|
||||
<button class="dropbtn">⋮</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" class="addInsurer">Add Insurer</a>
|
||||
<a href="#" class="F">Add Insurer</a>
|
||||
<a href="#" class="qcrProposal" style="background-color: rgb(221, 221, 221);">
|
||||
QCR <i class="mdi mdi-checkbox-marked" style="color: green; margin-left: 8px;"></i>
|
||||
</a>
|
||||
@ -2549,7 +2551,7 @@ function detachDynamicEventListeners() {
|
||||
|
||||
function addInsurer(event) {
|
||||
|
||||
const insurers = <?= json_encode($insurer); ?>;
|
||||
const insurers = <?= json_encode($insurer ?? []); ?>;
|
||||
|
||||
selectInsurer(insurers).then(result => {
|
||||
|
||||
@ -2701,7 +2703,7 @@ function dupInsurer(event) {
|
||||
const thPosition = getThPosition(event);
|
||||
console.log(thPosition);
|
||||
|
||||
const insurers = <?= json_encode($insurer); ?>;
|
||||
const insurers = <?= json_encode($insurer ?? []); ?>;
|
||||
|
||||
selectInsurer(insurers).then(result => {
|
||||
|
||||
@ -3217,7 +3219,7 @@ function changeInsurer(event) {
|
||||
}
|
||||
|
||||
// let newInsurerName = prompt("Enter New insurer name:");
|
||||
const insurers = <?= json_encode($insurer); ?>;
|
||||
const insurers = <?= json_encode($insurer ?? []); ?>;
|
||||
|
||||
selectInsurer(insurers).then(result => {
|
||||
|
||||
@ -4124,7 +4126,7 @@ function appendInput(data) {
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_cc">CC </label>
|
||||
<select class="form-control" id="client_cc" name="client_cc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
<?php if ($page_name == "RFQ" || in_array($user['role'],[1,5]) || in_array($user['team_id'],[6,7])) { ?>
|
||||
@ -4139,7 +4141,7 @@ function appendInput(data) {
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_bcc">BCC </label>
|
||||
<select class="form-control" id="client_bcc" name="client_bcc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
<?php if ($page_name == "RFQ" || in_array($user['role'],[1,5]) || in_array($user['team_id'],[6,7])) { ?>
|
||||
@ -4190,7 +4192,7 @@ function appendInput(data) {
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_cc">CC </label>
|
||||
<select class="form-control" id="client_cc" name="client_cc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
<?php if ($page_name == "RFQ" || in_array($user['role'],[1,5]) || in_array($user['team_id'],[6,7])) { ?>
|
||||
@ -4205,10 +4207,11 @@ function appendInput(data) {
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_bcc">BCC </label>
|
||||
<select class="form-control" id="client_bcc" name="client_bcc" required multiple>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList; } ?>
|
||||
<?php if($page_name == "RFQ") { $exclusiveUserList = $userList ?? []; } ?>
|
||||
<?php if (isset($exclusiveUserList)) { ?>
|
||||
<?php foreach ($exclusiveUserList as $user) { ?>
|
||||
<?php if ($page_name == "RFQ" || in_array($user['role'],[1,5]) || in_array($user['team_id'],[6,7])) { ?>
|
||||
<option value="<?= $user['id'];?>">
|
||||
<?= $user['first_name'].' - '.$user['email'];?>
|
||||
</option>
|
||||
<?php } } ?>
|
||||
|
||||
262
hr-dashboard.md
Normal file
262
hr-dashboard.md
Normal file
@ -0,0 +1,262 @@
|
||||
# Claims Collection V2 — HR Dashboard API
|
||||
|
||||
**Created:** 2026-06-02
|
||||
**Controller:** `App\Controllers\ClaimsCollectionV2DashboardController`
|
||||
**Model:** `App\Models\ClaimsCollectionV2DashboardModel`
|
||||
**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2`
|
||||
**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php`
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Each Metabase question in the CSV is a separate PHP method on the model.
|
||||
The API exposes them in three ways:
|
||||
|
||||
| What | URL fragment | Use case |
|
||||
|------|-------------|----------|
|
||||
| **Single KPI** | `kpi/{slug or Metabase id}` | FE loads one card at a time |
|
||||
| **All KPIs** | `all` | FE loads entire dashboard in one call |
|
||||
| **Debug / preview** | `debug` / `preview` | Admin checks raw output in browser |
|
||||
|
||||
All endpoints require `client_policy` or `client_policy_id` as a query param
|
||||
(or route segment for `debug/{id}` / `preview/{id}`).
|
||||
|
||||
---
|
||||
|
||||
## Route stacks
|
||||
|
||||
### 1. Admin — `authMVC` (session login required)
|
||||
|
||||
Prefix: `util/claims-collection-v2`
|
||||
|
||||
| Method | Path | Handler | Purpose |
|
||||
|--------|------|---------|---------|
|
||||
| GET | `util/claims-collection-v2/preview` | `::preview` | UI debug grid in browser |
|
||||
| GET | `util/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
|
||||
| GET | `util/claims-collection-v2/debug` | `::debug` | Raw JSON dump (all KPIs) |
|
||||
| GET | `util/claims-collection-v2/debug/{policy_id}` | `::debug` | Same, policy in URL |
|
||||
| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 31 KPIs |
|
||||
| GET | `util/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
|
||||
|
||||
**Filters applied:** `authMVC`, `AclFilter`, `HttpRequestLog`, `Cors`, `SecurityInputFilter`
|
||||
|
||||
---
|
||||
|
||||
### 2. Frontend / HR App — `authJWT` (Bearer token required)
|
||||
|
||||
Prefix: `employeeRest/claims-collection-v2`
|
||||
|
||||
| Method | Path | Handler | Purpose |
|
||||
|--------|------|---------|---------|
|
||||
| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 31 KPIs |
|
||||
| GET | `employeeRest/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
|
||||
| GET | `employeeRest/claims-collection-v2/preview` | `::preview` | UI grid (FE debug) |
|
||||
| GET | `employeeRest/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
|
||||
| GET | `employeeRest/claims-collection-v2/debug` | `::debug` | Raw JSON (FE debug) |
|
||||
| GET | `employeeRest/claims-collection-v2/debug/{policy_id}` | `::debug` | Same, policy in URL |
|
||||
|
||||
**Filters applied:** `GlobalPostFileUploadGuard`, `ratelimit`, `appSignature`, `authJWT`
|
||||
|
||||
---
|
||||
|
||||
## Parameters
|
||||
|
||||
| Param | Location | Type | Required | Notes |
|
||||
|-------|----------|------|----------|-------|
|
||||
| `client_policy` | query string | int | **Yes** (API) | Policy ID |
|
||||
| `client_policy_id` | query string | int | **Yes** (API) | Alias for above |
|
||||
| `{policy_id}` | URL segment | int | No | Only on `preview/{id}` and `debug/{id}`; falls back to 4687 if omitted |
|
||||
| `{slug\|id}` | URL segment | string/int | Yes (kpi only) | KPI method slug OR Metabase question id |
|
||||
|
||||
> **Default policy ID (4687)** is only the method-signature default for `preview` and `debug`.
|
||||
> Change it by passing the query param or URL segment — never hardcoded elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## How to call — Admin (browser / Postman, session cookie)
|
||||
|
||||
Open in browser while logged in to admin:
|
||||
|
||||
```
|
||||
# UI preview page (loads grid, click "Load all KPIs")
|
||||
GET /index.php/util/claims-collection-v2/preview?client_policy=4687
|
||||
|
||||
# Preview with policy in URL
|
||||
GET /index.php/util/claims-collection-v2/preview/4687
|
||||
|
||||
# Raw JSON — all 31 KPIs
|
||||
GET /index.php/util/claims-collection-v2/debug?client_policy=4687
|
||||
|
||||
# Raw JSON — single KPI by slug
|
||||
GET /index.php/util/claims-collection-v2/kpi/incurred_ratio?client_policy=4687
|
||||
|
||||
# Raw JSON — single KPI by Metabase question id
|
||||
GET /index.php/util/claims-collection-v2/kpi/207?client_policy=4687
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to call — Frontend / HR App (JWT)
|
||||
|
||||
```http
|
||||
GET /index.php/employeeRest/claims-collection-v2/all?client_policy=4687
|
||||
Authorization: Bearer <jwt_token>
|
||||
X-App-Signature: <app_signature>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```http
|
||||
GET /index.php/employeeRest/claims-collection-v2/kpi/incurred_ratio?client_policy=4687
|
||||
Authorization: Bearer <jwt_token>
|
||||
X-App-Signature: <app_signature>
|
||||
```
|
||||
|
||||
```http
|
||||
GET /index.php/employeeRest/claims-collection-v2/kpi/207?client_policy=4687
|
||||
Authorization: Bearer <jwt_token>
|
||||
X-App-Signature: <app_signature>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sample responses
|
||||
|
||||
### `kpi/{slug}` or `kpi/{id}`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"policy_id": 4687,
|
||||
"kpi_id": 207,
|
||||
"kpi": "incurred_ratio",
|
||||
"label": "Incurred Ratio",
|
||||
"rows": [
|
||||
{ "incurred_ratio": "72.34%" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `all`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"policy_id": 4687,
|
||||
"data": {
|
||||
"policy_exposure_summary": {
|
||||
"id": 181,
|
||||
"label": "POLICY & EXPOSURE SUMMARY",
|
||||
"rows": [
|
||||
{
|
||||
"policy_start_date": "2024-01-01",
|
||||
"policy_end_date": "2024-12-31",
|
||||
"insurer_name": "HDFC Ergo",
|
||||
"tpa_name": "Medi Assist"
|
||||
}
|
||||
]
|
||||
},
|
||||
"incurred_ratio": {
|
||||
"id": 207,
|
||||
"label": "Incurred Ratio",
|
||||
"rows": [{ "incurred_ratio": "72.34%" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error — missing policy id
|
||||
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "client_policy or client_policy_id is required."
|
||||
}
|
||||
```
|
||||
**HTTP 422**
|
||||
|
||||
### Error — unknown KPI
|
||||
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Unknown KPI. Pass Metabase id or method slug.",
|
||||
"allowed": {
|
||||
"181": "policy_exposure_summary",
|
||||
"207": "incurred_ratio"
|
||||
}
|
||||
}
|
||||
```
|
||||
**HTTP 404**
|
||||
|
||||
---
|
||||
|
||||
## All 31 KPIs
|
||||
|
||||
| Metabase ID | Method slug | Label |
|
||||
|-------------|-------------|-------|
|
||||
| 181 | `policy_exposure_summary` | POLICY & EXPOSURE SUMMARY |
|
||||
| 185 | `premium_as_on_date` | PREMIUM AS ON DATE |
|
||||
| 186 | `claims_experience_summary` | CLAIMS EXPERIENCE SUMMARY |
|
||||
| 190 | `claim_amount_by_gender` | Claim Amount by Gender |
|
||||
| 191 | `age_band` | Age Band |
|
||||
| 194 | `top_5_hospitals_by_incurred_amount` | Top 5 Hospitals by Incurred amount |
|
||||
| 197 | `claims_incidence_rate` | Claims Incidence Rate |
|
||||
| 198 | `policy_start_date` | Policy Start Date |
|
||||
| 199 | `policy_end_date` | Policy End Date |
|
||||
| 200 | `insurer` | Insurer |
|
||||
| 201 | `tpa` | TPA |
|
||||
| 203 | `earned_premium` | Earned Premium |
|
||||
| 204 | `total_claims` | Total Claims |
|
||||
| 206 | `incurred_amount` | Incurred Amount |
|
||||
| 207 | `incurred_ratio` | Incurred Ratio |
|
||||
| 208 | `projected_claims` | Projected Claims |
|
||||
| 209 | `projected_ratio` | Projected Ratio |
|
||||
| 213 | `total_reimbursement_amount` | Total Reimbursement Amount |
|
||||
| 214 | `total_reimbursement_amt_pct` | Total Reimbursement Amt % |
|
||||
| 215 | `cashless_claim_amt` | Cashless Claim Amt |
|
||||
| 216 | `cashless_claim_amt_pct` | Cashless Claim Amt % |
|
||||
| 217 | `total_incurred_by_city` | Total Incurred by city |
|
||||
| 219 | `claim_amount_by_claim_status` | Claim Amount by Claim Status |
|
||||
| 220 | `hospitals_in_detail` | Hospitals in detail |
|
||||
| 221 | `hospital_city_wise_si_limit_pregnancy` | Hospital city wise SI Limit - Pregnancy |
|
||||
| 224 | `s_pregnancy_normal_delivery_exceeded_amt` | S-PREGNANCY - NORMAL DELIVERY Exceeded Amt |
|
||||
| 225 | `s_pregnancy_c_sec_avg_exceeded_amt` | S-Pregnancy C-Sec avg exceeded amt |
|
||||
| 228 | `cataract_exceeded_claim_amount` | Cataract exceeded claim amount |
|
||||
| 229 | `cataract_avg_exceeded_amount` | Cataract avg exceeded amount |
|
||||
| 230 | `hospital_city_wise_si_limit_cataract` | Hospital city wise SI Limit - Cataract |
|
||||
| 231 | `total_incurred_by_cliam_status` | Total Incurred by Cliam Status |
|
||||
|
||||
> **Note:** KPIs 214 and 216 were renamed from the auto-generated slug to avoid collision with 213 and 215.
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/Models/ClaimsCollectionV2DashboardModel.php` | 31 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` |
|
||||
| `app/Controllers/ClaimsCollectionV2DashboardController.php` | `kpi()`, `all()`, `preview()`, `debug()` |
|
||||
| `app/Views/claims_collection_v2_dashboard.php` | Admin/FE debug preview UI (KPI card grid) |
|
||||
| `app/Config/Routes.php` | Both route groups (search `claims-collection-v2`) |
|
||||
| `tests/smoke_claims_collection_v2.php` | CLI smoke test — run: `php tests/smoke_claims_collection_v2.php 4687` |
|
||||
| `metabase_raw_queries.csv` | Source of truth for all SQL queries |
|
||||
|
||||
---
|
||||
|
||||
## FE integration notes
|
||||
|
||||
- Call `all` once on dashboard mount; render each `data[method].rows` into its card.
|
||||
- Call `kpi/{slug}` for lazy/on-demand loading of individual cards.
|
||||
- `policy_id` should come from the HR session / selected policy context — never hardcoded.
|
||||
- All rows are raw arrays; formatting (currency, %, dates) is already applied inside the SQL (`FORMAT()`, `CONCAT()`).
|
||||
- `rows` may be empty `[]` if no claims exist for that policy — handle gracefully in UI.
|
||||
|
||||
---
|
||||
|
||||
## BE notes
|
||||
|
||||
- To add a new KPI: add an entry to `KPI_MAP` + `KPI_LABELS` in the model and write the corresponding method `public function my_kpi(int $policyId): array`.
|
||||
- All queries use named binding `:policy_id:` (CodeIgniter style, replaces Metabase `{{policy_id}}`).
|
||||
- Literal `\t` / `\n` in CSV SQL is normalized in `runKpiQuery()` — safe to re-generate from CSV.
|
||||
- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change.
|
||||
1048639
metabase_raw_queries.csv
Normal file
1048639
metabase_raw_queries.csv
Normal file
File diff suppressed because it is too large
Load Diff
@ -47,12 +47,61 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function isExcludedCharsetField(el) {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
if ($(el).attr('data-parsley-exclude-charset') === 'true') {
|
||||
return true;
|
||||
}
|
||||
var id = (el.id || '').toLowerCase();
|
||||
var name = (el.name || '').toLowerCase();
|
||||
return id === 'policy_no' || id === 'endorsement_no' ||
|
||||
name === 'policy_no' || name === 'endorsement_no';
|
||||
}
|
||||
|
||||
function clearExcludedFieldValidation($form) {
|
||||
if (!$form || !$form.length) {
|
||||
return;
|
||||
}
|
||||
$form.find('input, textarea, select').each(function () {
|
||||
var el = this;
|
||||
if (!isExcludedCharsetField(el)) {
|
||||
return;
|
||||
}
|
||||
var $el = $(el);
|
||||
$el.removeAttr('data-parsley-endorsementcharset');
|
||||
$el.attr('data-parsley-validate', 'false');
|
||||
$el.removeClass('parsley-error parsley-success');
|
||||
$el.siblings('ul.parsley-errors-list').remove();
|
||||
$el.closest('.form-group').find('ul.parsley-errors-list').remove();
|
||||
if (typeof $el.parsley === 'function') {
|
||||
try {
|
||||
var fieldInstance = $el.parsley();
|
||||
if (fieldInstance && typeof fieldInstance.reset === 'function') {
|
||||
fieldInstance.reset();
|
||||
}
|
||||
if (fieldInstance && typeof fieldInstance.destroy === 'function') {
|
||||
fieldInstance.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshParsley($form);
|
||||
}
|
||||
|
||||
function registerValidator() {
|
||||
if (!isParsleyReady() || window.Parsley.__endorsementFormValidatorRegistered) {
|
||||
return;
|
||||
}
|
||||
window.Parsley.addValidator('endorsementcharset', {
|
||||
validateString: function (value) {
|
||||
var el = this.$element && this.$element.length ? this.$element[0] : null;
|
||||
if (isExcludedCharsetField(el)) {
|
||||
return true;
|
||||
}
|
||||
if (!value || String(value).trim() === '') {
|
||||
return true;
|
||||
}
|
||||
@ -85,6 +134,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (isExcludedCharsetField(el)) {
|
||||
$el.removeAttr('data-parsley-endorsementcharset');
|
||||
$el.attr('data-parsley-validate', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCharsetCandidate(el) || isDocumentNameField(el)) {
|
||||
if (!$el.attr('data-parsley-endorsementcharset')) {
|
||||
$el.attr('data-parsley-endorsementcharset', 'true');
|
||||
@ -144,7 +199,7 @@
|
||||
}
|
||||
|
||||
function validateField(field) {
|
||||
if (!field || isSkippableField(field) || !isParsleyReady()) {
|
||||
if (!field || isSkippableField(field) || isExcludedCharsetField(field) || !isParsleyReady()) {
|
||||
return;
|
||||
}
|
||||
var $field = $(field);
|
||||
@ -181,6 +236,7 @@
|
||||
|
||||
registerValidator();
|
||||
applyConstraints($form);
|
||||
clearExcludedFieldValidation($form);
|
||||
refreshParsley($form);
|
||||
stripParsleyFormSubmitHandlers($form);
|
||||
|
||||
@ -237,18 +293,14 @@
|
||||
initWithRetry(40);
|
||||
});
|
||||
|
||||
$(window).on('load', function () {
|
||||
if (!isParsleyReady()) {
|
||||
window.clearEndorsementExcludedFieldsValidation = function (formSelector) {
|
||||
var sel = formSelector || '#endorsement_form_id';
|
||||
var $form = $(sel);
|
||||
if (!$form.length || !isParsleyReady()) {
|
||||
return;
|
||||
}
|
||||
FORM_SELECTORS.forEach(function (selector) {
|
||||
var $f = $(selector);
|
||||
if ($f.length) {
|
||||
refreshParsley($f);
|
||||
stripParsleyFormSubmitHandlers($f);
|
||||
}
|
||||
});
|
||||
});
|
||||
clearExcludedFieldValidation($form);
|
||||
};
|
||||
|
||||
window.refreshEndorsementFormValidation = function (formSelector) {
|
||||
if (!formSelector || !isParsleyReady()) {
|
||||
@ -271,12 +323,32 @@
|
||||
stripOrphanParsleyUi($form);
|
||||
$form.removeData(LIVE_VALIDATE_DATA);
|
||||
try {
|
||||
clearExcludedFieldValidation($form);
|
||||
refreshParsley($form);
|
||||
stripParsleyFormSubmitHandlers($form);
|
||||
} catch (e2) {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
// form-validation.init.js binds all .parsley-examples after this script loads
|
||||
$(window).on('load', function () {
|
||||
window.setTimeout(function () {
|
||||
if (!isParsleyReady()) {
|
||||
return;
|
||||
}
|
||||
FORM_SELECTORS.forEach(function (selector) {
|
||||
var $f = $(selector);
|
||||
if (!$f.length) {
|
||||
return;
|
||||
}
|
||||
applyConstraints($f);
|
||||
clearExcludedFieldValidation($f);
|
||||
refreshParsley($f);
|
||||
stripParsleyFormSubmitHandlers($f);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
})(function () {
|
||||
return window.jQuery;
|
||||
});
|
||||
|
||||
156
tests/smoke_claims_collection_v2.php
Normal file
156
tests/smoke_claims_collection_v2.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* One-off smoke test: Claims Collection V2 dashboard.
|
||||
* Run: php tests/smoke_claims_collection_v2.php [policy_id]
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
ob_start();
|
||||
|
||||
define('FCPATH', __DIR__ . '/../public/');
|
||||
chdir(FCPATH);
|
||||
|
||||
require FCPATH . '../app/Config/Paths.php';
|
||||
$paths = new Config\Paths();
|
||||
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
|
||||
require_once SYSTEMPATH . 'Config/DotEnv.php';
|
||||
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
|
||||
|
||||
defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development'));
|
||||
|
||||
$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
|
||||
if (is_file($boot)) {
|
||||
require_once $boot;
|
||||
}
|
||||
|
||||
// Load autoloaded services without full HTTP stack.
|
||||
helper('url');
|
||||
|
||||
use App\Controllers\ClaimsCollectionV2DashboardController;
|
||||
use App\Models\ClaimsCollectionV2DashboardModel;
|
||||
use Config\Services;
|
||||
|
||||
$policyId = isset($argv[1]) ? (int) $argv[1] : 4687;
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
$results = [];
|
||||
|
||||
function ok(string $label, bool $cond, string $detail = ''): void
|
||||
{
|
||||
global $pass, $fail, $results;
|
||||
if ($cond) {
|
||||
$pass++;
|
||||
$results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : '');
|
||||
} else {
|
||||
$fail++;
|
||||
$results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : '');
|
||||
}
|
||||
}
|
||||
|
||||
$model = new ClaimsCollectionV2DashboardModel();
|
||||
$kpiMap = ClaimsCollectionV2DashboardModel::KPI_MAP;
|
||||
|
||||
ok('KPI_MAP count', count($kpiMap) === 31, (string) count($kpiMap));
|
||||
|
||||
$uniqueMethods = array_unique(array_values($kpiMap));
|
||||
ok('unique KPI method names', count($uniqueMethods) === 31, count($uniqueMethods) . ' methods');
|
||||
|
||||
foreach (['policy_exposure_summary', 'incurred_ratio'] as $slug) {
|
||||
ok("slug map contains {$slug}", in_array($slug, $kpiMap, true));
|
||||
}
|
||||
ok('id 207 maps to incurred_ratio', ($kpiMap[207] ?? '') === 'incurred_ratio');
|
||||
ok('id 181 maps to policy_exposure_summary', ($kpiMap[181] ?? '') === 'policy_exposure_summary');
|
||||
|
||||
try {
|
||||
$rows = $model->policy_exposure_summary($policyId);
|
||||
ok('model policy_exposure_summary', is_array($rows), 'rows=' . count($rows));
|
||||
} catch (Throwable $e) {
|
||||
ok('model policy_exposure_summary', false, $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = $model->getKpi('incurred_ratio', $policyId);
|
||||
ok('model getKpi(incurred_ratio)', is_array($rows), 'rows=' . count($rows));
|
||||
} catch (Throwable $e) {
|
||||
ok('model getKpi(incurred_ratio)', false, $e->getMessage());
|
||||
}
|
||||
|
||||
$request = Services::request(null, false);
|
||||
$response = Services::response();
|
||||
$request->setGlobal('get', ['client_policy' => (string) $policyId]);
|
||||
|
||||
$controller = new ClaimsCollectionV2DashboardController();
|
||||
$controller->initController($request, $response, service('logger'));
|
||||
|
||||
$slugResp = json_decode($controller->kpi('incurred_ratio')->getJSON(), true);
|
||||
ok('controller kpi by slug', ($slugResp['status'] ?? false) === true && ($slugResp['kpi'] ?? '') === 'incurred_ratio');
|
||||
|
||||
$idResp = json_decode($controller->kpi('207')->getJSON(), true);
|
||||
ok('controller kpi by id 207', ($idResp['status'] ?? false) === true && ($idResp['kpi_id'] ?? 0) === 207);
|
||||
|
||||
$badResp = json_decode($controller->kpi('not_a_kpi')->getJSON(), true);
|
||||
ok('controller unknown kpi 404', ($badResp['status'] ?? true) === false);
|
||||
|
||||
$allResp = json_decode($controller->all()->getJSON(), true);
|
||||
ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 31);
|
||||
|
||||
$debugOut = $controller->debug($policyId);
|
||||
$debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody();
|
||||
$debugJson = json_decode($debugBody, true);
|
||||
ok('controller debug JSON', ($debugJson['status'] ?? false) === true && isset($debugJson['data']));
|
||||
|
||||
$previewOut = $controller->preview($policyId);
|
||||
$previewHtml = is_string($previewOut) ? $previewOut : $previewOut->getBody();
|
||||
ok('controller preview HTML', str_contains($previewHtml, 'Claims Collection V2') && str_contains($previewHtml, 'kpi-grid'));
|
||||
|
||||
$routeCollection = Services::routes(true);
|
||||
$mvcRoutes = [];
|
||||
$jwtRoutes = [];
|
||||
foreach ($routeCollection->getRoutes('get') as $pattern => $handler) {
|
||||
if (! str_contains($pattern, 'claims-collection-v2')) {
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($pattern, 'util/')) {
|
||||
$mvcRoutes[] = $pattern;
|
||||
}
|
||||
if (str_starts_with($pattern, 'employeeRest/')) {
|
||||
$jwtRoutes[] = $pattern;
|
||||
}
|
||||
}
|
||||
|
||||
ok('MVC claims-collection-v2 routes registered', count($mvcRoutes) >= 5, implode(', ', $mvcRoutes) ?: 'none');
|
||||
ok('JWT claims-collection-v2 routes registered', count($jwtRoutes) >= 5, implode(', ', $jwtRoutes) ?: 'none');
|
||||
|
||||
$baseUrl = rtrim((string) env('app.baseURL', ''), '/');
|
||||
if ($baseUrl !== '') {
|
||||
$results[] = '';
|
||||
$results[] = '=== HTTP auth gate checks (no session/token) ===';
|
||||
$urls = [
|
||||
'MVC preview' => $baseUrl . '/util/claims-collection-v2/preview?client_policy=' . $policyId,
|
||||
'MVC kpi slug' => $baseUrl . '/util/claims-collection-v2/kpi/incurred_ratio?client_policy=' . $policyId,
|
||||
'MVC kpi id' => $baseUrl . '/util/claims-collection-v2/kpi/207?client_policy=' . $policyId,
|
||||
'MVC debug' => $baseUrl . '/util/claims-collection-v2/debug?client_policy=' . $policyId,
|
||||
'JWT kpi slug' => $baseUrl . '/employeeRest/claims-collection-v2/kpi/incurred_ratio?client_policy=' . $policyId,
|
||||
'JWT debug' => $baseUrl . '/employeeRest/claims-collection-v2/debug?client_policy=' . $policyId,
|
||||
];
|
||||
foreach ($urls as $label => $url) {
|
||||
$ctx = stream_context_create(['http' => ['ignore_errors' => true, 'timeout' => 10]]);
|
||||
$body = @file_get_contents($url, false, $ctx);
|
||||
$code = 0;
|
||||
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
||||
$code = (int) $m[1];
|
||||
}
|
||||
$blocked = in_array($code, [401, 403, 302, 303], true);
|
||||
$reachable = $code >= 200 && $code < 500;
|
||||
ok("HTTP {$label} (" . ($code ?: 'no connection') . ')', $blocked || $reachable, $url);
|
||||
}
|
||||
} else {
|
||||
$results[] = '[SKIP] HTTP checks — app.baseURL not set in .env';
|
||||
}
|
||||
|
||||
ob_end_clean();
|
||||
echo '=== Claims Collection V2 smoke test (policy_id=' . $policyId . ') ===' . PHP_EOL . PHP_EOL;
|
||||
echo implode(PHP_EOL, $results) . PHP_EOL;
|
||||
echo PHP_EOL . "=== Summary: {$pass} passed, {$fail} failed ===" . PHP_EOL;
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
Loading…
Reference in New Issue
Block a user