diff --git a/.env.sample b/.env.sample index e8a13f85..3d57c40a 100755 --- a/.env.sample +++ b/.env.sample @@ -116,6 +116,21 @@ VIDAL_PRIMARY_KEY_CONSTANT = MEDI_ASSIST_PRIMARY_KEY_CONSTANT = +#-------------------------------------------------------------------- +# TPA PRIMARY KEY CONSTANTS LIVE +#-------------------------------------------------------------------- + +MEDI_ASSIST_PRIMARY_KEY_CONSTANT_LIVE = +ICICI_PRIMARY_KEY_CONSTANT_LIVE = +ABHI_PRIMARY_KEY_CONSTANT_LIVE = +R_CARE_PRIMARY_KEY_CONSTANT_LIVE = +FHPL_PRIMARY_KEY_CONSTANT_LIVE = +VIDAL_PRIMARY_KEY_CONSTANT_LIVE = +VOLO_PRIMARY_KEY_CONSTANT_LIVE = + +# When true, claims-collection-report falls back to ticket_master only if the +# policy TPA has no mapped dump table (or dump table missing). Dump TPAs always use claim_report. +CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER = false FHPL_TOKEN_URL = FHPL_BASE_URL = diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3612ebe0..a059de1c 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -151,6 +151,7 @@ $routes->group("/client", ["filter" => "authMVC"], function ($routes) { $routes->get('createtransaction', 'ClientController::createtransaction'); $routes->post('save_deposit', 'ClientController::saveDeposit'); $routes->post('update_deposit_amount', 'ClientController::updateDepositAmount'); + $routes->post('update_deposit_description', 'ClientController::updateDepositDescription'); $routes->post("saveApiData", "ClientController::saveApiData"); $routes->get("generateToken","ClientController::sendToken"); // $routes->get('view_Deposit/(:num)/(:num)','ClientController/view_Deposit/$1/$2'); @@ -484,6 +485,15 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->match(['get', 'post'], 'decryptVisitSso', 'ApiServiceController::decryptVisitSso'); // disabled for now do not remove this $routes->get("getInsurerClaimFormDownloadUrl", "EmployeeRestController::getInsurerClaimFormDownloadUrl"); + // $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->group('claims-collection-v2', static function ($routes) { $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); @@ -493,6 +503,16 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + $routes->group('claims-collection-report', static function ($routes) { + $routes->get('preview', 'ClaimReportDashboardController::preview'); + $routes->get('preview/(:num)', 'ClaimReportDashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'ClaimReportDashboardController::kpi/$1'); + $routes->get('all', 'ClaimReportDashboardController::all'); + $routes->get('debug', 'ClaimReportDashboardController::debug'); + $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + $routes->get('sync', 'ClaimReportDashboardController::sync'); + }); + $routes->group('enrollment-collection-v1', static function ($routes) { $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); @@ -726,7 +746,7 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu //samulss oauth login api's $routes->get("getTokenforSamulssOAuthLogin", "RestAuthenticationController::getTokenforSamulssOAuthLogin"); - + $routes->get("downloadEmployeeListExcel", "EmployeeRestController::downloadEmployeeListExcel"); }); $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) { @@ -822,6 +842,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $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->group('claims-collection-v2', static function ($routes) { $routes->get('preview', 'ClaimsCollectionV2DashboardController::preview'); $routes->get('preview/(:num)', 'ClaimsCollectionV2DashboardController::preview/$1'); @@ -831,6 +860,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + $routes->group('claims-collection-report', static function ($routes) { + $routes->get('preview', 'ClaimReportDashboardController::preview'); + $routes->get('preview/(:num)', 'ClaimReportDashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'ClaimReportDashboardController::kpi/$1'); + $routes->get('all', 'ClaimReportDashboardController::all'); + $routes->get('debug', 'ClaimReportDashboardController::debug'); + $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + }); + $routes->group('enrollment-collection-v1', static function ($routes) { $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); diff --git a/app/Controllers/ApiServiceController.php b/app/Controllers/ApiServiceController.php index de934916..0a716086 100644 --- a/app/Controllers/ApiServiceController.php +++ b/app/Controllers/ApiServiceController.php @@ -898,6 +898,7 @@ class ApiServiceController extends BaseController ->where('client_policy.tpa_id', $tpa_id) ->where('batch_files.is_active', 1) ->where('batch_files.event_type', 'api') + ->where('batch_files.client_policy_id', $policy_id) ->where('batch_files.icici_status_flag !=', 'COMPLETED') ->countAllResults(); diff --git a/app/Controllers/ClaimReportDashboardController.php b/app/Controllers/ClaimReportDashboardController.php new file mode 100644 index 00000000..acbe8e8d --- /dev/null +++ b/app/Controllers/ClaimReportDashboardController.php @@ -0,0 +1,557 @@ +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 or Metabase 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 ClaimReportDashboardModel(); + $metabaseId = array_search($kpiMethod, ClaimsCollectionV2DashboardModel::KPI_MAP, true); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $model->resolveClaimsTable($policyId), + 'kpi_id' => $metabaseId !== false ? (int) $metabaseId : null, + 'kpi' => $kpiMethod, + 'label' => ClaimsCollectionV2DashboardModel::KPI_LABELS[$kpiMethod] ?? $kpiMethod, + 'rows' => $model->getKpi($kpiMethod, $policyId), + ]); + } + + /** + * JSON: all KPIs. + */ + public function all() + { + $policyId = $this->resolvePolicyId(); + + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy or client_policy_id is required.', + ], 422); + } + + $generatedAt = (new ClaimDumpFileModel())->getGeneratedAtForPolicy($policyId); + // if ($generatedAt === null) { + // return $this->respond([ + // 'status' => false, + // 'message' => 'claim_dump_date is mandatory. No claim dump date found for this policy.', + // ], 422); + // } + + $model = new ClaimReportDashboardModel(); + $data = $model->getAllKpis($policyId); + $source = $data['_meta']['claims_source'] ?? $model->resolveClaimsTable($policyId); + unset($data['_meta']); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $source, + 'generated_at' => $generatedAt, + 'data' => $data, + ]); + } + + /** + * Admin preview UI — KPI grid for manual testing. + */ + 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-report' : 'util/claims-collection-report'; + + 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'), + ]); + } + + /** + * Sync TPA dump tables → claim_report. + * Only rows where ticket_id IS NOT NULL and is_active = 1 are copied. + * Idempotent: existing (client_policy_id, claim_number) rows are skipped. + * No request parameters required. + * + * GET /util/claims-collection-report/sync + */ + public function sync() + { + @set_time_limit(0); + @ini_set('max_execution_time', '0'); + + $db = db_connect(); + + if (!$db->tableExists('claim_report')) { + return $this->respond([ + 'status' => false, + 'message' => 'Table claim_report does not exist. Run migrations first.', + ], 500); + } + + // env_key => [dump_table, claim_number_col, dump_col => claim_report_col mapping] + $tpaTableMap = [ + 'VIDAL_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_vidal', + 'claim_col' => 'insurer_claim_number', + 'mapping' => [ + 'insurer_claim_number' => 'claim_number', + 'employee_number' => 'emp_code', + 'date_of_admission' => 'doa', + 'date_of_discharge' => 'dod', + 'claim_amount' => 'claim_amount', + 'approved_amount' => 'approved_amount', + 'sum_insured' => 'si_amt', + 'claim_status' => 'tpa_claim_status', + 'hospital_name' => 'hospital_name', + 'hospital_address' => 'hospital_address', + 'hospital_city' => 'hospital_city', + 'hospital_state' => 'hospital_state', + 'hospital_pincode' => 'hospital_pin_code', + 'type_of_claim' => 'tpa_claim_type', + 'diagnosis' => 'tpa_ailments', + 'tpa_claim_number' => 'tpa_no', + ], + ], + 'ABHI_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_abhi', + 'claim_col' => 'abhi_claim_no', + 'mapping' => [ + 'abhi_claim_no' => 'claim_number', + 'member_code' => 'emp_code', + 'doa' => 'doa', + 'dod' => 'dod', + 'intimation_date' => 'date_of_intimat', + 'claim_status' => 'tpa_claim_status', + 'claimed_amount' => 'claim_amount', + 'hospital_name' => 'hospital_name', + 'hospital_city' => 'hospital_city', + 'hospital_state' => 'hospital_state', + 'settled_date' => 'settled_date', + 'healthcard_id' => 'tpa_no', + 'claim_type' => 'tpa_claim_type', + 'diagnosis' => 'tpa_ailments', + 'abhi_amount_less_coins_current_month' => 'approved_amount', + 'patient_age' => 'age', + 'gender' => 'gender', + 'relation' => 'relation', + ], + ], + 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_medi_assist', + 'claim_col' => 'claim_id', + 'mapping' => [ + 'claim_id' => 'claim_number', + 'pribenef_employee_code' => 'emp_code', + 'date_of_admission' => 'doa', + 'date_of_discharge' => 'dod', + 'intimation_date' => 'date_of_intimat', + 'settled_date' => 'settled_date', + 'processed_date' => 'approved_date', + 'claim_status' => 'tpa_claim_status', + 'claim_amount' => 'claim_amount', + 'claim_approved_amount' => 'approved_amount', + 'hospital_name' => 'hospital_name', + 'hospital_address' => 'hospital_address', + 'hospital_city' => 'hospital_city', + 'hospital_state' => 'hospital_state', + 'hospital_pincode' => 'hospital_pin_code', + 'claim_type' => 'tpa_claim_type', + 'primary_ailment_name' => 'tpa_ailments', + 'benef_sum_insured' => 'si_amt', + 'benef_gender' => 'gender', + 'benef_age' => 'age', + 'benef_relation' => 'relation', + 'incurred_amount' => 'incurred_amount', + ], + ], + 'FHPL_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_fhpl', + 'claim_col' => 'claim_id', + 'mapping' => [ + 'claim_id' => 'claim_number', + 'employee_id' => 'emp_code', + 'admission_date' => 'doa', + 'discharge_date' => 'dod', + 'claim_received_date' => 'date_of_intimat', + 'claim_passed_date' => 'approved_date', + 'settled_date' => 'settled_date', + 'current_claim_status' => 'tpa_claim_status', + 'claim_amount' => 'claim_amount', + 'settled_amount' => 'approved_amount', + 'incurred_amount' => 'incurred_amount', + 'coverage_amount' => 'si_amt', + 'provider_name' => 'hospital_name', + 'provider_address' => 'hospital_address', + 'provider_state' => 'hospital_state', + 'provider_place' => 'hospital_city', + 'provider_pincode' => 'hospital_pin_code', + 'claim_type' => 'tpa_claim_type', + 'diagnosis' => 'tpa_ailments', + 'uhid_no' => 'tpa_no', + 'gender' => 'gender', + 'years' => 'age', + 'relationship' => 'relation', + ], + ], + 'R_CARE_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_reliance', + 'claim_col' => 'cl_inward_no', + 'mapping' => [ + 'cl_inward_no' => 'claim_number', + 'employee_member_id' => 'emp_code', + 'doa_opd_treatment_from' => 'doa', + 'dod_opd_treatment_to' => 'dod', + 'approved_date' => 'approved_date', + 'cheque_neft_date' => 'settled_date', + 'claimed_amount' => 'claim_amount', + 'net_sanction_amount' => 'approved_amount', + 'final_status' => 'tpa_claim_status', + 'hospital_name' => 'hospital_name', + 'hospital_state' => 'hospital_state', + 'hospital_district' => 'hospital_city', + 'uhid' => 'tpa_no', + 'diagnosis' => 'tpa_ailments', + 'member_reimbursement_cl_type' => 'tpa_claim_type', + 'gender' => 'gender', + 'age' => 'age', + 'relation' => 'relation', + 'sum_insured' => 'si_amt', + ], + ], + 'ICICI_PRIMARY_KEY_CONSTANT' => [ + 'table' => 'claims_dump_icici', + 'claim_col' => 'claim_number', + 'mapping' => [ + 'claim_number' => 'claim_number', + 'employee_member_id' => 'emp_code', + 'doa' => 'doa', + 'dod' => 'dod', + 'payment_date' => 'settled_date', + 'claimed_amount' => 'claim_amount', + 'net_sanct_amt' => 'approved_amount', + 'updated_status' => 'tpa_claim_status', + 'hospital_name' => 'hospital_name', + 'hospital_city' => 'hospital_city', + 'hospital_state' => 'hospital_state', + 'type_of_claim' => 'tpa_claim_type', + 'diagnosis' => 'tpa_ailments', + 'uhid' => 'tpa_no', + 'sum_insured' => 'si_amt', + 'gender' => 'gender', + 'age' => 'age', + 'relation' => 'relation', + ], + ], + ]; + + // Build tpa_id → config map from env + $tpaMap = []; + foreach ($tpaTableMap as $envKey => $cfg) { + $tpaId = (int) env($envKey); + if ($tpaId > 0) { + $tpaMap[$tpaId] = $cfg; + } + } + + $totalFound = 0; + $totalInserted = 0; + $totalSkipped = 0; + $totalFailed = 0; + $tpaResults = []; + $now = date('Y-m-d H:i:s'); + + foreach ($tpaMap as $tpaId => $cfg) { + $dumpTable = $cfg['table']; + $claimCol = $cfg['claim_col']; + $mapping = $cfg['mapping']; + + if (!$db->tableExists($dumpTable)) { + $tpaResults[] = [ + 'tpa_id' => $tpaId, + 'table' => $dumpTable, + 'status' => 'skipped', + 'detail' => 'Dump table does not exist.', + 'found' => 0, + 'inserted' => 0, + 'skipped' => 0, + 'failed' => 0, + ]; + continue; + } + + // Fetch all active dump rows with a ticket_id (all columns) + $dumpRows = $db->table($dumpTable) + ->where('is_active', 1) + ->where('ticket_id IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + // Pre-load all related ticket_master rows in one query + $ticketIds = array_filter(array_column($dumpRows, 'ticket_id')); + $ticketsById = []; + if (!empty($ticketIds)) { + $ticketRows = $db->table('ticket_master') + ->whereIn('id', array_values(array_unique($ticketIds))) + ->get() + ->getResultArray(); + foreach ($ticketRows as $tm) { + $ticketsById[(int) $tm['id']] = $tm; + } + } + + $found = count($dumpRows); + $inserted = 0; + $skipped = 0; + $failed = 0; + $totalFound += $found; + + foreach ($dumpRows as $row) { + $claimNumber = trim((string) ($row[$claimCol] ?? '')); + $clientPolicyId = (int) ($row['client_policy_id'] ?? 0); + + if ($claimNumber === '' || $clientPolicyId <= 0) { + $skipped++; + $totalSkipped++; + continue; + } + + // Fetch linked ticket_master row for emp_id / insured_emp_id + $ticket = $ticketsById[(int) ($row['ticket_id'] ?? 0)] ?? null; + + // Check if already exists in claim_report + $existing = $db->table('claim_report') + ->where('client_policy_id', $clientPolicyId) + ->where('claim_number', $claimNumber) + ->get() + ->getRowArray(); + + // Base row metadata from dump; employee/system IDs always from ticket_master + $reportRow = [ + 'tpa_id' => $tpaId, + 'client_id' => $row['client_id'] ?? ($ticket['client_id'] ?? null), + 'file_id' => $row['file_id'] ?? ($ticket['file_id'] ?? null), + 'ticket_id' => $row['ticket_id'], + 'source_table' => $dumpTable, + 'source_row_id' => $row['id'], + 'is_active' => 1, + 'updated_at' => $now, + 'emp_id' => $ticket['emp_id'] ?? null, + 'insured_emp_id' => $ticket['insured_emp_id'] ?? null, + 'claim_status_id' => $ticket['claim_status_id'] ?? null, + ]; + + // 1) Fill ALL claim_report data fields from ticket_master first + $ticketCols = [ + 'emp_code', 'tpa_no', + 'gender', 'age', 'relation', + 'claim_amount', 'approved_amount', 'si_amt', 'incurred_amount', + 'tpa_claim_status', 'tpa_claim_type', 'tpa_ailments', + 'doa', 'dod', 'date_of_intimat', 'settled_date', 'approved_date', + 'hospital_name', 'hospital_city', 'hospital_state', + 'hospital_pin_code', 'hospital_address', + 'claim_dump_date', + ]; + if ($ticket) { + foreach ($ticketCols as $col) { + $ticketVal = $ticket[$col] ?? null; + // ticket_master uses "relationship"; claim_report uses "relation" + if ($col === 'relation' && ($ticketVal === null || $ticketVal === '')) { + $ticketVal = $ticket['relationship'] ?? null; + } + if ($ticketVal !== null && $ticketVal !== '') { + $reportRow[$col] = $ticketVal; + } + } + } + + // 2) Dump table fills only fields still null/empty after ticket_master + foreach ($mapping as $dumpCol => $reportCol) { + $current = $reportRow[$reportCol] ?? null; + if ($current !== null && $current !== '') { + continue; + } + if (array_key_exists($dumpCol, $row) && $row[$dumpCol] !== null && $row[$dumpCol] !== '') { + $reportRow[$reportCol] = $row[$dumpCol]; + } + } + + // incurred_amount final fallback + if (empty($reportRow['incurred_amount'])) { + $reportRow['incurred_amount'] = $reportRow['approved_amount'] ?? $reportRow['claim_amount'] ?? null; + } + + if ($existing) { + // Update existing row with full data + $ok = $db->table('claim_report') + ->where('client_policy_id', $clientPolicyId) + ->where('claim_number', $claimNumber) + ->update($reportRow); + if ($ok) { + $skipped++; + $totalSkipped++; + } else { + $failed++; + $totalFailed++; + } + continue; + } + + // New insert + $reportRow['client_policy_id'] = $clientPolicyId; + $reportRow['claim_number'] = $claimNumber; + $reportRow['created_at'] = $now; + + $ok = $db->table('claim_report')->insert($reportRow); + + if ($ok) { + $inserted++; + $totalInserted++; + } else { + $failed++; + $totalFailed++; + } + } + + $tpaResults[] = [ + 'tpa_id' => $tpaId, + 'table' => $dumpTable, + 'status' => 'done', + 'found' => $found, + 'inserted' => $inserted, + 'skipped' => $skipped, + 'failed' => $failed, + ]; + } + + return $this->respond([ + 'status' => $totalFailed === 0, + 'message' => $totalFailed === 0 ? 'Sync completed successfully.' : 'Sync completed with some failures.', + 'summary' => [ + 'total_found' => $totalFound, + 'total_inserted' => $totalInserted, + 'total_skipped' => $totalSkipped, + 'total_failed' => $totalFailed, + ], + 'tpa_results' => $tpaResults, + ]); + } + + /** + * Admin check only: raw JSON on screen (no dashboard UI). + */ + 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 ClaimReportDashboardModel(); + $data = $model->getAllKpis($policyId); + $source = $data['_meta']['claims_source'] ?? $model->resolveClaimsTable($policyId); + unset($data['_meta']); + + $body = json_encode([ + 'status' => true, + 'policy_id' => $policyId, + 'claims_source' => $source, + 'data' => $data, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + return $this->response + ->setHeader('Content-Type', 'application/json; charset=UTF-8') + ->setBody($body); + } + +} diff --git a/app/Controllers/ClaimsCollectionV2DashboardController.php b/app/Controllers/ClaimsCollectionV2DashboardController.php index e49739d1..6f299fd8 100644 --- a/app/Controllers/ClaimsCollectionV2DashboardController.php +++ b/app/Controllers/ClaimsCollectionV2DashboardController.php @@ -3,6 +3,7 @@ namespace App\Controllers; use App\Models\ClaimsCollectionV2DashboardModel; +use App\Models\ClaimDumpFileModel; use CodeIgniter\API\ResponseTrait; /** @@ -109,12 +110,21 @@ class ClaimsCollectionV2DashboardController extends BaseController ], 422); } + $generatedAt = (new ClaimDumpFileModel())->getGeneratedAtForPolicy($policyId); + // if ($generatedAt === null) { + // return $this->respond([ + // 'status' => false, + // 'message' => 'claim_dump_date is mandatory. No claim dump date found for this policy.', + // ], 422); + // } + $model = new ClaimsCollectionV2DashboardModel(); return $this->respond([ - 'status' => true, - 'policy_id' => $policyId, - 'data' => $model->getAllKpis($policyId), + 'status' => true, + 'policy_id' => $policyId, + 'generated_at' => $generatedAt, + 'data' => $model->getAllKpis($policyId), ]); } diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 182f9f22..f47dbc68 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -1005,6 +1005,73 @@ class ClientController extends AdminController ]); } + public function updateDepositDescription() + { + $rules = [ + 'deposit_id' => 'required|is_natural_no_zero', + 'description' => 'required|string|min_length[1]|max_length[1000]', + '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); + $description = trim((string) ($postData['description'] ?? '')); + $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.', + ]); + } + + $updated = $depositModel->update($depositId, [ + 'description' => $description, + 'updated_by' => $loggedInUserID, + ]); + + if ($updated === false) { + return $this->response + ->setStatusCode(500) + ->setJSON([ + 'status' => 'error', + 'message' => 'Failed to update description.', + ]); + } + + return $this->response->setJSON([ + 'status' => 'success', + 'message' => 'Description updated successfully.', + ]); + } + public function editClientOnboarding($id = null) { diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index b1fd0e42..7d30450c 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -605,8 +605,16 @@ class EmployeeRestController extends AdminController public function getEmployeeAndDependenceByClientId() { try { + $search = trim((string) $this->request->getGet('search')); - $empData = $this->employeePolicyModel->getEmployeePolicy(client_id: $this->request->getGet('client_id'), policy_id: $this->request->getGet('client_policy_id'), status: 0, branch_id: $this->request->getGet('client_branch_id'), status_type: 'inactive'); + $empData = $this->employeePolicyModel->getEmployeePolicy( + client_id: $this->request->getGet('client_id'), + policy_id: $this->request->getGet('client_policy_id'), + status: 0, + branch_id: $this->request->getGet('client_branch_id'), + status_type: 'inactive', + search: $search + ); if ($empData) { return $this->respond(['status' => 'success', 'code' => 200, 'data' => $empData], 200); @@ -687,6 +695,105 @@ class EmployeeRestController extends AdminController } } + /** + * Download all employees for client/branch/policy as Excel + * (Emp Code, Name, TPA ID, Relationship, Date Of Birth, Gender, Mobile, Email, Status). + * + * Query params: client_id, policy_id (or client_policy_id), branch_id (or client_branch_id) + */ + public function downloadEmployeeListExcel() + { + try { + $clientId = $this->request->getGet('client_id'); + $policyId = $this->request->getGet('policy_id') ?: $this->request->getGet('client_policy_id'); + $branchId = $this->request->getGet('branch_id') ?: $this->request->getGet('client_branch_id'); + + if (empty($clientId) || empty($policyId) || empty($branchId)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'client_id, policy_id and branch_id are required', + 'data' => [], + ], 200); + } + + $empData = $this->employeePolicyModel->getEmployeePolicy( + client_id: $clientId, + policy_id: $policyId, + status: 0, + branch_id: $branchId, + status_type: 'inactive' + ); + + if (!count($empData)) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => []], 200); + } + + $headers = [ + 'Emp Code', + 'Name', + 'TPA ID', + 'Relationship', + 'Date Of Birth', + 'Gender', + 'Mobile', + 'Email', + 'Status', + ]; + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $column = 'A'; + foreach ($headers as $header) { + $sheet->setCellValue($column . '1', $header); + $column++; + } + + $row = 2; + foreach ($empData as $employee) { + $gender = strtoupper(trim((string) ($employee['gender'] ?? ''))); + if ($gender === 'MALE' || $gender === 'M') { + $gender = 'M'; + } elseif ($gender === 'FEMALE' || $gender === 'F') { + $gender = 'F'; + } elseif ($gender !== '') { + $gender = strtoupper(substr($gender, 0, 1)); + } + + $dob = $employee['formatted_dob'] ?? ''; + if ($dob === '' && !empty($employee['dob'])) { + $ts = strtotime((string) $employee['dob']); + $dob = $ts ? date('d/m/Y', $ts) : (string) $employee['dob']; + } + + $sheet->setCellValue('A' . $row, $employee['emp_code'] ?? ''); + $sheet->setCellValue('B' . $row, $employee['name'] ?? ''); + $sheet->setCellValue('C' . $row, $employee['tpa_id'] ?? ''); + $sheet->setCellValue('D' . $row, $employee['relationship'] ?? ''); + $sheet->setCellValue('E' . $row, $dob); + $sheet->setCellValue('F' . $row, $gender); + $sheet->setCellValueExplicit('G' . $row, (string) ($employee['mobile'] ?? ''), \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING); + $sheet->setCellValue('H' . $row, $employee['email_corporate'] ?? ''); + $sheet->setCellValue('I' . $row, $employee['emp_status'] ?? ($employee['status'] ?? '')); + $row++; + } + + $policyName = preg_replace('/[^A-Za-z0-9_\- ]/', '', (string) ($empData[0]['policy_name'] ?? 'Employees')); + $filename = trim($policyName) !== '' ? $policyName . '-Employees.xlsx' : 'Employees.xlsx'; + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="' . $filename . '"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + exit; + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + //not in use public function getClientPolicy() { @@ -4290,6 +4397,7 @@ class EmployeeRestController extends AdminController $TicketMasterModel = new TicketMasterModel(); $ticket_data = $TicketMasterModel->get_ticket_data($emp_id, $returnType, $ticket_type, $ticket_id); + // print_r($ticket_data);die; if (! empty($ticket_data)) { diff --git a/app/Controllers/ICICILombardController.php b/app/Controllers/ICICILombardController.php index 92b6c594..203952ee 100644 --- a/app/Controllers/ICICILombardController.php +++ b/app/Controllers/ICICILombardController.php @@ -238,14 +238,15 @@ class ICICILombardController extends AdminController ]; $response = call_third_party_api($url, $method, $headers, $body); - + log_message('error', 'ICICI - generateAuthToken API URL: ' . json_encode(["url" => $url, "method" => $method, "headers" => $headers, "body" => $body], JSON_PRETTY_PRINT)); + log_message('error', 'ICICI - generateAuthToken API response: ' . json_encode($response, JSON_PRETTY_PRINT)); if($response['status'] != true){ - return $this->response->setJSON([ + return [ 'status' => false, 'message' => 'Token generation failed.', 'data' => $response - ]); + ]; } // Debug removed: return token response to caller. diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 851a63ee..351d674f 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -4072,8 +4072,8 @@ class PolicyTransactionController extends BaseController $user_id = $this->request->getGet('user_id') ?? $this->request->getPost('user_id'); if ($date_type == 'statement_month' && $start_date && $end_date) { - $start_date = (string) date('Y-m-01', strtotime($start_date)); - $end_date = (string) date('Y-m-31', strtotime($end_date)); + $start_date = (string) date('Y-m-01', strtotime($start_date)); + $end_date = (string) date('Y-m-t', strtotime($end_date)); } $normalize = static function ($value) { @@ -4090,7 +4090,7 @@ class PolicyTransactionController extends BaseController $ids = array_filter(explode(',', $sanitized_post_data['ids'] ?? '')); if (!empty($ids)) { - $idsStr = implode(',', array_map('intval', $ids)); + $idsStr = implode(',', array_map('intval', $ids)); $where = "policy_transaction.id IN ($idsStr)"; } else { $where = []; @@ -4367,7 +4367,7 @@ class PolicyTransactionController extends BaseController $insurer_branch_id = $this->request->getGet('insurer_branch_id'); $start_date = (!isset($start_date) || $start_date === '' || $start_date === null) ? 0 : change_date_format($start_date, 'd-m-Y', 'Y-m-01'); - $end_date = (!isset($end_date) || $end_date === '' || $end_date === null) ? 0 : change_date_format($end_date, 'd-m-Y', 'Y-m-31'); + $end_date = (!isset($end_date) || $end_date === '' || $end_date === null) ? 0 : change_date_format($end_date, 'd-m-Y', 'Y-m-t'); // dd([$start_date,$end_date]); @@ -6135,7 +6135,7 @@ class PolicyTransactionController extends BaseController // Handle statement month range if ($date_type == 'statement_month') { $start_date = (string) date('Y-m-01', strtotime($start_date)); - $end_date = (string) date('Y-m-31', strtotime($end_date)); + $end_date = (string) date('Y-m-t', strtotime($end_date)); } // Ensure default values diff --git a/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php b/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php new file mode 100644 index 00000000..eec7c461 --- /dev/null +++ b/app/Database/Migrations/2026-07-28-090700_CreateClaimReportTable.php @@ -0,0 +1,207 @@ +forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'tpa_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'client_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'client_policy_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => false, + ], + 'file_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'ticket_id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'null' => true, + ], + 'source_table' => [ + 'type' => 'VARCHAR', + 'constraint' => 64, + 'null' => true, + ], + 'source_row_id' => [ + 'type' => 'BIGINT', + 'unsigned' => true, + 'null' => true, + ], + 'claim_number' => [ + 'type' => 'VARCHAR', + 'constraint' => 191, + 'null' => false, + ], + 'emp_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'tpa_no' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'emp_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'insured_emp_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'claim_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'approved_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'incurred_amount' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'si_amt' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'tpa_claim_status' => [ + 'type' => 'VARCHAR', + 'constraint' => 191, + 'null' => true, + ], + 'claim_status_id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + ], + 'tpa_claim_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + ], + 'tpa_ailments' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'doa' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'dod' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'date_of_intimat' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'settled_date' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'approved_date' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'claim_dump_date' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'hospital_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + ], + 'hospital_city' => [ + 'type' => 'VARCHAR', + 'constraint' => 150, + 'null' => true, + ], + 'hospital_state' => [ + 'type' => 'VARCHAR', + 'constraint' => 150, + 'null' => true, + ], + 'hospital_pin_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'hospital_address' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'gender' => [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ], + 'age' => [ + 'type' => 'VARCHAR', + 'constraint' => 20, + 'null' => true, + ], + 'relation' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'is_active' => [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['client_policy_id', 'claim_number'], 'uq_claim_report_policy_claim'); + $this->forge->addKey(['client_policy_id', 'is_active'], false, false, 'idx_claim_report_policy_active'); + $this->forge->addKey('ticket_id', false, false, 'idx_claim_report_ticket'); + $this->forge->addKey(['source_table', 'source_row_id'], false, false, 'idx_claim_report_source'); + + $this->forge->createTable('claim_report', true); + } + + public function down() + { + $this->forge->dropTable('claim_report', true); + } +} diff --git a/app/Database/claim_report.sql b/app/Database/claim_report.sql new file mode 100644 index 00000000..105aa531 --- /dev/null +++ b/app/Database/claim_report.sql @@ -0,0 +1,49 @@ +-- claim_report: normalized claims analytics table (UNIQUE client_policy_id + claim_number) +-- Prefer: php spark migrate +-- Manual fallback if migrations are not used in this environment. + +CREATE TABLE IF NOT EXISTS `claim_report` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tpa_id` INT UNSIGNED NULL, + `client_id` INT UNSIGNED NULL, + `client_policy_id` INT UNSIGNED NOT NULL, + `file_id` INT UNSIGNED NULL, + `ticket_id` BIGINT UNSIGNED NULL, + `source_table` VARCHAR(64) NULL, + `source_row_id` BIGINT UNSIGNED NULL, + `claim_number` VARCHAR(191) NOT NULL, + `emp_code` VARCHAR(100) NULL, + `tpa_no` VARCHAR(100) NULL, + `emp_id` INT UNSIGNED NULL, + `insured_emp_id` INT UNSIGNED NULL, + `claim_amount` VARCHAR(50) NULL, + `approved_amount` VARCHAR(50) NULL, + `incurred_amount` VARCHAR(50) NULL, + `si_amt` VARCHAR(50) NULL, + `tpa_claim_status` VARCHAR(191) NULL, + `claim_status_id` INT UNSIGNED NULL, + `tpa_claim_type` VARCHAR(100) NULL, + `tpa_ailments` TEXT NULL, + `doa` DATE NULL, + `dod` DATE NULL, + `date_of_intimat` DATE NULL, + `settled_date` DATE NULL, + `approved_date` DATE NULL, + `claim_dump_date` DATETIME NULL, + `hospital_name` VARCHAR(255) NULL, + `hospital_city` VARCHAR(150) NULL, + `hospital_state` VARCHAR(150) NULL, + `hospital_pin_code` VARCHAR(20) NULL, + `hospital_address` TEXT NULL, + `gender` VARCHAR(30) NULL, + `age` VARCHAR(20) NULL, + `relation` VARCHAR(50) NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `created_at` DATETIME NULL, + `updated_at` DATETIME NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_claim_report_policy_claim` (`client_policy_id`, `claim_number`), + KEY `idx_claim_report_policy_active` (`client_policy_id`, `is_active`), + KEY `idx_claim_report_ticket` (`ticket_id`), + KEY `idx_claim_report_source` (`source_table`, `source_row_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php index 94ef8876..7e8851f3 100644 --- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php @@ -113,6 +113,8 @@ class AbhiClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index f256bc6e..533394b1 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -1164,7 +1164,7 @@ abstract class BaseTpaClaimImportService ->where('is_active', 1) ->countAllResults() > 0; } - + /** * Map Excel rows to TPA table structure */ diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php index 577b7045..496c61cf 100644 --- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php @@ -175,6 +175,8 @@ class FhplClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php index 2e74334c..98a6d5d0 100644 --- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php @@ -104,6 +104,8 @@ class IciciClaimImportService extends BaseTpaClaimImportService 'diagnosis' => 'tpa_ailments', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'PAID' => 11, 'SETTLED' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php index 978c92c5..beb08156 100644 --- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php @@ -155,6 +155,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService 'primary_ailment_name' => 'tpa_ailments', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php index 7a07bd75..0eb3d9d3 100644 --- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php @@ -92,6 +92,8 @@ class RcareClaimImportService extends BaseTpaClaimImportService 'uhid' => 'tpa_no', ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'CL Paid with Settlement Letter' => 11, 'Settled' => 11, diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php index 5c5762c2..44d2a6a5 100644 --- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php @@ -348,6 +348,8 @@ class VidalClaimImportService extends BaseTpaClaimImportService ]; + /** + * Extra dump → claim_report fields beyond ticketMasterMapping. protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, diff --git a/app/Models/ClaimDumpFileModel.php b/app/Models/ClaimDumpFileModel.php index c6466479..c13d2f9f 100644 --- a/app/Models/ClaimDumpFileModel.php +++ b/app/Models/ClaimDumpFileModel.php @@ -57,4 +57,35 @@ class ClaimDumpFileModel extends Model return $data; } + /** + * Latest claim_dump_date for a policy, formatted as dd-mm-yyyy. + * Returns null when no dump file has a claim_dump_date. + */ + public function getGeneratedAtForPolicy(int $policyId): ?string + { + if ($policyId <= 0) { + return null; + } + + $row = $this->select('claim_dump_date') + ->where('client_policy_id', $policyId) + ->where('is_active', 1) + ->where('claim_dump_date IS NOT NULL', null, false) + ->where("TRIM(claim_dump_date) != ''", null, false) + ->orderBy('claim_dump_date', 'DESC') + ->orderBy('id', 'DESC') + ->first(); + + $raw = trim((string) ($row['claim_dump_date'] ?? '')); + if ($raw === '') { + return null; + } + + $ts = strtotime($raw); + if ($ts === false) { + return null; + } + + return date('d-m-Y', $ts); + } } diff --git a/app/Models/ClaimReportDashboardModel.php b/app/Models/ClaimReportDashboardModel.php new file mode 100644 index 00000000..61f00511 --- /dev/null +++ b/app/Models/ClaimReportDashboardModel.php @@ -0,0 +1,109 @@ + */ + protected array $claimsTableCache = []; + + /** + * Resolve claim fact table for a policy. + * + * - TPA dump table exists → always claim_report (even if empty; never ticket_master). + * - Dump table missing / TPA unmapped → ticket_master only when + * CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER=true; otherwise claim_report. + */ + public function resolveClaimsTable(int $policyId): string + { + if (isset($this->claimsTableCache[$policyId])) { + return $this->claimsTableCache[$policyId]; + } + + $db = \Config\Database::connect($this->DBGroup); + $fallbackEnabled = $this->isTicketMasterFallbackEnabled(); + + if ($policyId <= 0) { + return $this->claimsTableCache[$policyId] = $fallbackEnabled + ? 'ticket_master' + : 'claim_report'; + } + + // Table missing — cannot query claim_report; ticket_master is the only option. + if (!$db->tableExists('claim_report')) { + return $this->claimsTableCache[$policyId] = 'ticket_master'; + } + + $policy = $db->table('client_policy') + ->select('tpa_id') + ->where('id', $policyId) + ->get() + ->getRowArray(); + + $tpaId = (int) ($policy['tpa_id'] ?? 0); + $dumpTable = $this->getTpaDumpTableMap()[$tpaId] ?? null; + + // Mapped dump TPA with existing dump table → claim_report only. + if ($dumpTable !== null && $db->tableExists($dumpTable)) { + return $this->claimsTableCache[$policyId] = 'claim_report'; + } + + // Non-dump / missing dump table → env-gated ticket_master fallback. + return $this->claimsTableCache[$policyId] = $fallbackEnabled + ? 'ticket_master' + : 'claim_report'; + } + + protected function isTicketMasterFallbackEnabled(): bool + { + return filter_var( + env('CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER', false), + FILTER_VALIDATE_BOOLEAN + ); + } + + /** + * @return array + */ + protected function getTpaDumpTableMap(): array + { + return [ + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal', + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi', + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist', + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl', + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance', + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici', + ]; + } + + protected function runKpiQuery(string $sql, int $policyId): array + { + $sql = str_replace(['\\t', '\\n', '\\r'], ["\t", "\n", "\r"], $sql); + + $claimsTable = $this->resolveClaimsTable($policyId); + if ($claimsTable !== 'ticket_master') { + $sql = preg_replace('/\bticket_master\b/', $claimsTable, $sql) ?? $sql; + } + + $db = \Config\Database::connect($this->DBGroup); + $query = $db->query($sql, ['policy_id' => $policyId]); + + return $query->getResultArray(); + } + + public function getAllKpis(int $policyId): array + { + $out = parent::getAllKpis($policyId); + $out['_meta'] = [ + 'claims_source' => $this->resolveClaimsTable($policyId), + ]; + + return $out; + } +} diff --git a/app/Models/ClaimReportModel.php b/app/Models/ClaimReportModel.php new file mode 100644 index 00000000..df809eb1 --- /dev/null +++ b/app/Models/ClaimReportModel.php @@ -0,0 +1,58 @@ +where('employee_polices.status !=', 'inactive'); } + + if (!empty($search)) { + $this->applyEmployeeGlobalSearch($result, $search); + } $res = $result->findAll(); @@ -371,6 +375,31 @@ class EmployeePolicyModel extends Model return $res; } + /** + * Global search across employee + policy fields (OR + LIKE). + * Searches: emp_code, name, mobile, email, tpa_id, dob, relationship, status. + */ + protected function applyEmployeeGlobalSearch($builder, string $search) + { + $search = trim($search); + if ($search === '') { + return $builder; + } + + return $builder->groupStart() + ->like('emp.emp_code', $search) + ->orLike('emp.name', $search) + ->orLike('emp.mobile', $search) + ->orLike('emp.email_corporate', $search) + ->orLike('employee_polices.tpa_id', $search) + ->orLike('emp.dob', $search) + ->orLike("DATE_FORMAT(emp.dob, '%d/%m/%Y')", $search) + ->orLike('emp.relationship', $search) + ->orLike('employee_polices.status', $search) + ->orLike('emp.emp_status', $search) + ->groupEnd(); + } + public function getEmployeePolicyForEcard($policy_id = 0) { $result = $this->select([ diff --git a/app/Models/TicketMasterModel.php b/app/Models/TicketMasterModel.php index 07bdd813..9a9d3514 100644 --- a/app/Models/TicketMasterModel.php +++ b/app/Models/TicketMasterModel.php @@ -857,7 +857,7 @@ class TicketMasterModel extends Model ") ->join('ticket_messages tms', 'ticket_master.id = tms.ticket_id', 'left') ->where('ticket_master.is_active', 1); - $query->whereIn('sender', ['staff', 'user']); + // $query->whereIn('sender', ['staff', 'user']); $query->where('ticket_master.emp_id', $emp_id); if (! empty($ticket_id)) { @@ -871,7 +871,7 @@ class TicketMasterModel extends Model $query->groupBy('ticket_master.id'); $data = $query->findAll(); // dd($data, db_connect()->getLastQuery()); - // print_r($this->db->getLastQuery());die; + // print_r($this->db->getLastQuery()->getQuery());die; return $data; } diff --git a/app/Views/view_deposit.php b/app/Views/view_deposit.php index d4b9087d..782b874d 100755 --- a/app/Views/view_deposit.php +++ b/app/Views/view_deposit.php @@ -60,6 +60,11 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len overflow: hidden; text-overflow: ellipsis; } +#scroll-horizontal-datatable tbody td.deposit-description-cell { + overflow: visible; + text-overflow: clip; + white-space: normal; +} #scroll-horizontal-datatable_wrapper .dataTables_scrollHead table thead th, #scroll-horizontal-datatable thead th { padding: 5px 11px !important; @@ -111,6 +116,17 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len background-color: #f4f5f7 !important; cursor: not-allowed; } + .deposit-description-cell .deposit-inline-cell { + width: 100%; + } + .deposit-description-input { + width: 100%; + max-width: 100%; + min-height: 30px; + resize: vertical; + font-size: 13px; + line-height: 1.25; + } .deposit-balance-wrap { display: inline-flex; align-items: center; @@ -386,7 +402,8 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len data-row-id="id ?>" data-transaction-type="transaction_type, 'attr') ?>" data-credit="" - data-debit=""> + data-debit="" + data-description="description ?? ''), 'attr') ?>"> created_at)); ?> record_date)? date('d-M-Y', strtotime($row->record_date)):'-' ?> unit ?? ' - '; ?> @@ -431,13 +448,13 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len - - description; ?> + +
+ +
username; ?> @@ -690,6 +707,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len } return ($(node).text() || '').trim(); } + if (column === 9) { + var $descInput = $(node).find('.deposit-description-input'); + if ($descInput.length) { + return ($descInput.val() || '').trim(); + } + } if (typeof data === 'string' && data.indexOf('<') !== -1) { return $('
').html(data).text().trim(); } @@ -1102,6 +1125,13 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len var originalDebit = parseFloat($row.attr('data-debit')) || 0; var inputCredit = creditInputVal === '' ? 0 : parseFloat(creditInputVal); var inputDebit = debitInputVal === '' ? 0 : parseFloat(debitInputVal); + var descriptionVal = ($row.find('.deposit-description-input').val() || '').trim(); + var originalDescription = ($row.attr('data-description') || '').trim(); + + if (descriptionVal !== originalDescription) { + pending = true; + return false; + } if (isNaN(inputCredit) || isNaN(inputDebit)) { return; @@ -1130,6 +1160,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len updateTopSaveButtonVisibility(); }); + $(document).on('input change', '.deposit-description-input', function () { + var $input = $(this); + $input.attr('title', ($input.val() || '').trim()); + updateTopSaveButtonVisibility(); + }); + function collectChanges() { var changes = []; var hasValidationError = false; @@ -1190,12 +1226,52 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len return changes; } + function collectDescriptionChanges() { + var changes = []; + var hasValidationError = false; + $('#scroll-horizontal-datatable tbody tr').each(function () { + var $row = $(this); + var $input = $row.find('.deposit-description-input'); + if (!$input.length) { + return; + } + var current = ($input.val() || '').trim(); + var original = ($row.attr('data-description') || '').trim(); + if (current === original) { + return; + } + if (current === '') { + toastr.warning('Description cannot be empty.'); + hasValidationError = true; + return false; + } + if (current.length > 1000) { + toastr.warning('Description must not exceed 1000 characters.'); + hasValidationError = true; + return false; + } + changes.push({ + rowId: $row.data('row-id'), + description: current, + $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.'); - } + var descriptionChanges = collectDescriptionChanges(); + if (changes === false || descriptionChanges === false) { + return; + } + if ((!Array.isArray(changes) || changes.length === 0) && + (!Array.isArray(descriptionChanges) || descriptionChanges.length === 0)) { + toastr.info('No changes to save.'); return; } @@ -1204,15 +1280,61 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len 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(); + var totalChanges = changes.length + descriptionChanges.length; + + function finishSave() { + toastr.success('Saved ' + completed + ' change(s) successfully.'); + setTimeout(function () { + if (typeof window.storeCurrentDepositTablePage === 'function') { + window.storeCurrentDepositTablePage(); + } + location.reload(); + }, 250); + } + + function saveDescriptionNext(index) { + if (index >= descriptionChanges.length) { + finishSave(); + return; + } + + var change = descriptionChanges[index]; + $.ajax({ + type: 'POST', + url: '', + data: { + deposit_id: change.rowId, + description: change.description, + client_id: clientId, + insurer_id: insurerId, + cd_ac_pk: cdAcPk + }, + success: function () { + change.$row.attr('data-description', change.description); + change.$row.find('.deposit-description-input').attr('title', change.description); + completed++; + saveDescriptionNext(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 description right now.'); } - location.reload(); - }, 250); + } + }); + } + + function saveAmountNext(index) { + if (index >= changes.length) { + saveDescriptionNext(0); return; } @@ -1237,7 +1359,7 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len setRowDisplay(change.$row, 0, change.amount); } completed++; - saveNext(index + 1); + saveAmountNext(index + 1); }, error: function (xhr) { if (xhr.status === 400 && xhr.responseText) { @@ -1256,7 +1378,12 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len }); } - saveNext(0); + if (totalChanges === 0) { + toastr.info('No changes to save.'); + return; + } + + saveAmountNext(0); }; $(document).on('keydown', '.deposit-inline-input', function (e) { @@ -1268,6 +1395,15 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len } }); + $(document).on('keydown', '.deposit-description-input', function (e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (typeof window.saveDepositInlineChanges === 'function') { + window.saveDepositInlineChanges(); + } + } + }); + updateTopSaveButtonVisibility(); }); \ No newline at end of file diff --git a/tests/smoke_get_employee_and_dependence_by_client_id.php b/tests/smoke_get_employee_and_dependence_by_client_id.php new file mode 100644 index 00000000..2a87b870 --- /dev/null +++ b/tests/smoke_get_employee_and_dependence_by_client_id.php @@ -0,0 +1,378 @@ +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; +} + +use App\Controllers\EmployeeRestController; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\URI; +use CodeIgniter\HTTP\UserAgent; +use Config\Services; + +$pass = 0; +$fail = 0; +$skip = 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}" : ''); + } +} + +function skip(string $label, string $detail = ''): void +{ + global $skip, $results; + $skip++; + $results[] = '[SKIP] ' . $label . ($detail !== '' ? " — {$detail}" : ''); +} + +/** + * @return array{http:int,body:array,raw:string,error:?string} + */ +function invokeGetEmployeeAndDependenceByClientId(array $query): array +{ + try { + $uri = new URI('http://localhost/nhance_v2/employeeRest/getEmployeeAndDependenceByClientId'); + if ($query !== []) { + $uri->setQuery(http_build_query($query)); + } + + $request = new IncomingRequest( + config('App'), + $uri, + null, + new UserAgent() + ); + $request->setMethod('get'); + $request->setGlobal('get', $query); + + $controller = new EmployeeRestController(); + $response = Services::response(); + $logger = Services::logger(); + $controller->initController($request, $response, $logger); + + $resp = $controller->getEmployeeAndDependenceByClientId(); + $raw = $resp->getBody(); + $body = json_decode($raw, true); + + return [ + 'http' => $resp->getStatusCode(), + 'body' => is_array($body) ? $body : [], + 'raw' => (string) $raw, + 'error' => null, + ]; + } catch (Throwable $e) { + return [ + 'http' => 500, + 'body' => [], + 'raw' => '', + 'error' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(), + ]; + } +} + +function rowMatchesSearch(array $row, string $search): bool +{ + $needle = mb_strtolower(trim($search)); + if ($needle === '') { + return true; + } + + $haystacks = [ + (string) ($row['emp_code'] ?? ''), + (string) ($row['name'] ?? ''), + (string) ($row['mobile'] ?? ''), + (string) ($row['email_corporate'] ?? ''), + (string) ($row['tpa_id'] ?? ''), + (string) ($row['dob'] ?? ''), + (string) ($row['formatted_dob'] ?? ''), + (string) ($row['relationship'] ?? ''), + (string) ($row['status'] ?? ''), + (string) ($row['emp_status'] ?? ''), + ]; + + foreach ($haystacks as $hay) { + if ($hay !== '' && mb_stripos($hay, $needle) !== false) { + return true; + } + } + + return false; +} + +// ─── Fixtures ──────────────────────────────────────────────────────────────── + +$db = db_connect('default'); + +$cliClientId = $argv[1] ?? null; +$cliClientPolicyId = isset($argv[2]) && ctype_digit((string) $argv[2]) ? (int) $argv[2] : null; +$cliClientBranchId = isset($argv[3]) && ctype_digit((string) $argv[3]) ? (int) $argv[3] : null; + +$fixtureSql = " + SELECT + emp.client_id, + MD5(emp.client_id) AS client_id_md5, + emp.client_branch_id, + ep.client_policy_id, + emp.emp_code, + emp.name, + emp.mobile, + emp.email_corporate, + ep.tpa_id, + emp.dob, + DATE_FORMAT(emp.dob, '%d/%m/%Y') AS formatted_dob, + emp.relationship, + ep.status, + emp.emp_status + FROM employee_polices ep + INNER JOIN employees emp ON emp.id = ep.employee_id + WHERE ep.is_active = 1 + AND emp.is_active = 1 + AND ep.status IN ('active', 'inactive') +"; + +$bindings = []; +if ($cliClientId !== null && $cliClientId !== '') { + if (is_string($cliClientId) && preg_match('/^[a-f0-9]{32}$/i', $cliClientId)) { + $fixtureSql .= ' AND MD5(emp.client_id) = ?'; + $bindings[] = $cliClientId; + } else { + $fixtureSql .= ' AND emp.client_id = ?'; + $bindings[] = (int) $cliClientId; + } +} +if ($cliClientPolicyId !== null) { + $fixtureSql .= ' AND ep.client_policy_id = ?'; + $bindings[] = $cliClientPolicyId; +} +if ($cliClientBranchId !== null) { + $fixtureSql .= ' AND emp.client_branch_id = ?'; + $bindings[] = $cliClientBranchId; +} + +// Prefer a row that has searchable fields populated (mobile/email/tpa_id). +$fixtureSql .= ' + ORDER BY + (emp.mobile IS NOT NULL AND emp.mobile <> "") DESC, + (emp.email_corporate IS NOT NULL AND emp.email_corporate <> "") DESC, + (ep.tpa_id IS NOT NULL AND ep.tpa_id <> "") DESC, + ep.id DESC + LIMIT 1 +'; + +$fixture = $db->query($fixtureSql, $bindings)->getRowArray(); + +if (!$fixture) { + echo "No fixture rows found for employee_polices + employees.\n"; + echo "Pass args: php tests/smoke_get_employee_and_dependence_by_client_id.php [client_id] [client_policy_id] [client_branch_id]\n"; + exit(1); +} + +$baseQuery = [ + 'client_id' => (string) $fixture['client_id_md5'], + 'client_policy_id' => (string) $fixture['client_policy_id'], + 'client_branch_id' => (string) $fixture['client_branch_id'], +]; + +echo "Fixture:\n"; +echo json_encode([ + 'client_id' => $fixture['client_id'], + 'client_id_md5' => $fixture['client_id_md5'], + 'client_policy_id' => $fixture['client_policy_id'], + 'client_branch_id' => $fixture['client_branch_id'], + 'emp_code' => $fixture['emp_code'], + 'name' => $fixture['name'], + 'mobile' => $fixture['mobile'], + 'email_corporate' => $fixture['email_corporate'], + 'tpa_id' => $fixture['tpa_id'], + 'dob' => $fixture['dob'], + 'formatted_dob' => $fixture['formatted_dob'], + 'relationship' => $fixture['relationship'], + 'status' => $fixture['status'], + 'emp_status' => $fixture['emp_status'], +], JSON_PRETTY_PRINT) . "\n\n"; + +// ─── Baseline (no search) ──────────────────────────────────────────────────── + +$baseline = invokeGetEmployeeAndDependenceByClientId($baseQuery); +ok( + 'Baseline call has no PHP error', + $baseline['error'] === null, + (string) ($baseline['error'] ?? '') +); +ok( + 'Baseline returns success/200 with non-empty data', + ($baseline['body']['status'] ?? '') === 'success' + && (int) ($baseline['body']['code'] ?? 0) === 200 + && is_array($baseline['body']['data'] ?? null) + && count($baseline['body']['data']) > 0, + 'http=' . $baseline['http'] . ' count=' . count($baseline['body']['data'] ?? []) . ' body_status=' . ($baseline['body']['status'] ?? '') +); + +$baselineRows = is_array($baseline['body']['data'] ?? null) ? $baseline['body']['data'] : []; +$baselineCount = count($baselineRows); + +// Empty search should behave like no search +$emptySearch = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => '']); +ok( + 'Empty search returns same count as baseline', + ($emptySearch['error'] ?? null) === null + && count($emptySearch['body']['data'] ?? []) === $baselineCount, + 'baseline=' . $baselineCount . ' empty_search=' . count($emptySearch['body']['data'] ?? []) +); + +// Nonsense search → failed/404 empty +$nonsense = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => '___no_such_value_zzz_999___']); +ok( + 'Nonsense search returns failed/404 empty data', + ($nonsense['body']['status'] ?? '') === 'failed' + && (int) ($nonsense['body']['code'] ?? 0) === 404 + && ($nonsense['body']['data'] ?? null) === [], + json_encode($nonsense['body']) +); + +// ─── Per-field search cases ────────────────────────────────────────────────── + +$searchCases = [ + 'emp_code' => (string) ($fixture['emp_code'] ?? ''), + 'name' => (string) ($fixture['name'] ?? ''), + 'mobile' => (string) ($fixture['mobile'] ?? ''), + 'email' => (string) ($fixture['email_corporate'] ?? ''), + 'tpa_id' => (string) ($fixture['tpa_id'] ?? ''), + 'dob' => (string) ($fixture['dob'] ?? ''), + 'formatted_dob'=> (string) ($fixture['formatted_dob'] ?? ''), + 'relationship' => (string) ($fixture['relationship'] ?? ''), + 'status' => (string) ($fixture['status'] ?? ''), +]; + +foreach ($searchCases as $field => $value) { + $value = trim($value); + if ($value === '') { + skip("Search by {$field}", 'fixture value empty'); + continue; + } + + // Use a short distinctive fragment when the value is long (e.g. name/email) + $term = $value; + if (in_array($field, ['name', 'email'], true) && mb_strlen($value) > 4) { + $term = mb_substr($value, 0, max(3, (int) floor(mb_strlen($value) / 2))); + } + + $resp = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $term]); + $rows = is_array($resp['body']['data'] ?? null) ? $resp['body']['data'] : []; + + ok( + "Search by {$field} returns success with rows", + ($resp['error'] ?? null) === null + && ($resp['body']['status'] ?? '') === 'success' + && (int) ($resp['body']['code'] ?? 0) === 200 + && count($rows) > 0, + 'term=' . $term . ' count=' . count($rows) . ' err=' . ($resp['error'] ?? '') + ); + + $allMatch = true; + foreach ($rows as $row) { + if (!rowMatchesSearch($row, $term)) { + $allMatch = false; + break; + } + } + ok( + "Search by {$field}: every returned row matches term", + $allMatch && count($rows) > 0, + 'term=' . $term . ' rows=' . count($rows) + ); + + ok( + "Search by {$field}: result count <= baseline", + count($rows) <= $baselineCount, + 'filtered=' . count($rows) . ' baseline=' . $baselineCount + ); +} + +// Partial emp_code (if long enough) +$empCode = trim((string) ($fixture['emp_code'] ?? '')); +if (mb_strlen($empCode) >= 3) { + $partial = mb_substr($empCode, 0, 3); + $resp = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $partial]); + $rows = is_array($resp['body']['data'] ?? null) ? $resp['body']['data'] : []; + ok( + 'Partial emp_code search returns matching rows', + ($resp['body']['status'] ?? '') === 'success' && count($rows) > 0, + 'term=' . $partial . ' count=' . count($rows) + ); +} else { + skip('Partial emp_code search', 'emp_code too short'); +} + +// MD5 client_id already used above; also verify numeric client_id works without search +$numericQuery = [ + 'client_id' => (string) $fixture['client_id'], + 'client_policy_id' => (string) $fixture['client_policy_id'], + 'client_branch_id' => (string) $fixture['client_branch_id'], +]; +$numeric = invokeGetEmployeeAndDependenceByClientId($numericQuery); +ok( + 'Numeric client_id baseline returns success', + ($numeric['body']['status'] ?? '') === 'success' + && count($numeric['body']['data'] ?? []) > 0, + 'count=' . count($numeric['body']['data'] ?? []) +); + +// Optional: if a search term was passed as 4th argv, dump raw result +if (isset($argv[4]) && trim((string) $argv[4]) !== '') { + $manualTerm = trim((string) $argv[4]); + $manual = invokeGetEmployeeAndDependenceByClientId($baseQuery + ['search' => $manualTerm]); + echo "\nManual search term={$manualTerm}\n"; + echo json_encode([ + 'http' => $manual['http'], + 'error' => $manual['error'], + 'status'=> $manual['body']['status'] ?? null, + 'code' => $manual['body']['code'] ?? null, + 'count' => count($manual['body']['data'] ?? []), + 'first' => ($manual['body']['data'][0] ?? null), + ], JSON_PRETTY_PRINT) . "\n"; +} + +echo "\nSmoke test summary: {$pass} passed, {$fail} failed, {$skip} skipped\n"; +foreach ($results as $line) { + echo $line . "\n"; +} + +exit($fail > 0 ? 1 : 0);