nhance/app/Controllers/ClaimReportDashboardController.php

756 lines
29 KiB
PHP

<?php
namespace App\Controllers;
use App\Models\ClaimReportDashboardModel;
use App\Models\ClaimsCollectionV2DashboardModel;
use App\Models\ClaimDumpFileModel;
use CodeIgniter\API\ResponseTrait;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
/**
* Claims Collection dashboard API using claim_report.
* ticket_master fallback is env-gated (CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER)
* and only applies when the policy TPA has no dump table.
*/
class ClaimReportDashboardController 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 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);
}
/**
* Download Excel of TPA dump rows linked from claim_report for a policy.
*
* Query: client_policy_id (or client_policy) — required.
* Uses claim_report.source_table + source_row_id to load dump rows.
* Excel columns = only fields AFTER master_reject_reason (that column and earlier are excluded).
*
* GET /util/claims-collection-report/download-excel?client_policy_id=123
*/
public function downloadExcel()
{
@set_time_limit(0);
@ini_set('max_execution_time', '0');
$policyId = $this->resolvePolicyId();
if ($policyId <= 0) {
return $this->respond([
'status' => false,
'message' => 'client_policy_id is required.',
], 422);
}
$db = db_connect();
if (!$db->tableExists('claim_report')) {
return $this->respond([
'status' => false,
'message' => 'Table claim_report does not exist.',
], 500);
}
$allowedDumpTables = [
'claims_dump_vidal',
'claims_dump_abhi',
'claims_dump_medi_assist',
'claims_dump_fhpl',
'claims_dump_reliance',
'claims_dump_icici',
];
$reportRows = $db->table('claim_report')
->select('source_table, source_row_id')
->where('client_policy_id', $policyId)
->where('is_active', 1)
->where('source_table IS NOT NULL', null, false)
->where('source_row_id IS NOT NULL', null, false)
->get()
->getResultArray();
if ($reportRows === []) {
return $this->respond([
'status' => false,
'message' => 'No claim_report records with dump linkage found for this policy.',
], 404);
}
// Group dump IDs by source_table
$idsByTable = [];
foreach ($reportRows as $row) {
$table = trim((string) ($row['source_table'] ?? ''));
$dumpId = (int) ($row['source_row_id'] ?? 0);
if ($table === '' || $dumpId <= 0 || ! in_array($table, $allowedDumpTables, true)) {
continue;
}
if (!$db->tableExists($table)) {
continue;
}
$idsByTable[$table][$dumpId] = $dumpId;
}
if ($idsByTable === []) {
return $this->respond([
'status' => false,
'message' => 'No valid TPA dump table references found for this policy.',
], 404);
}
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
$sheetIndex = 0;
$totalRows = 0;
foreach ($idsByTable as $dumpTable => $dumpIds) {
$dumpIds = array_values($dumpIds);
$exportColumns = $this->getDumpColumnsAfterMasterReject($db, $dumpTable);
if ($exportColumns === []) {
continue;
}
$dumpRows = $db->table($dumpTable)
->whereIn('id', $dumpIds)
->orderBy('id', 'ASC')
->get()
->getResultArray();
if ($dumpRows === []) {
continue;
}
$sheetTitle = substr($dumpTable, 0, 31);
$sheet = $spreadsheet->createSheet($sheetIndex);
$sheet->setTitle($sheetTitle);
$sheetIndex++;
// Headers
foreach ($exportColumns as $colIdx => $colName) {
$sheet->setCellValue(
Coordinate::stringFromColumnIndex($colIdx + 1) . '1',
$colName
);
}
// Data
$excelRow = 2;
foreach ($dumpRows as $dumpRow) {
foreach ($exportColumns as $colIdx => $colName) {
$sheet->setCellValue(
Coordinate::stringFromColumnIndex($colIdx + 1) . $excelRow,
$dumpRow[$colName] ?? null
);
}
$excelRow++;
$totalRows++;
}
}
if ($sheetIndex === 0 || $totalRows === 0) {
return $this->respond([
'status' => false,
'message' => 'No dump table rows found for the linked claim_report records.',
], 404);
}
$spreadsheet->setActiveSheetIndex(0);
$policy = $db->table('client_policy')
->select('policy_no')
->where('id', $policyId)
->get()
->getRowArray();
$policyNo = trim((string) ($policy['policy_no'] ?? ''));
if ($policyNo === '') {
$policyNo = (string) $policyId;
}
// Safe filename segment
$policyNoSafe = preg_replace('/[^A-Za-z0-9_\-]+/', '_', $policyNo) ?: (string) $policyId;
$filename = 'claim_report_' . $policyNoSafe . '_' . date('Ymd_His') . '.xlsx';
ob_start();
(new Xlsx($spreadsheet))->save('php://output');
$excelOutput = ob_get_clean();
return $this->response
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setHeader('Cache-Control', 'max-age=0')
->setBody($excelOutput);
}
/**
* Return dump-table column names that appear AFTER master_reject_reason
* (master_reject_reason itself and all earlier columns are excluded).
*
* @return list<string>
*/
protected function getDumpColumnsAfterMasterReject($db, string $table): array
{
$fields = $db->getFieldNames($table);
if ($fields === [] || $fields === false) {
return [];
}
$marker = null;
foreach ($fields as $idx => $name) {
$lower = strtolower((string) $name);
if (
$lower === 'master_reject_reason'
|| $lower === 'master_rejected_reason'
|| $lower === 'master_rejection_reason'
) {
$marker = $idx;
break;
}
}
// Marker not found — export nothing rather than leaking internal columns
if ($marker === null) {
return [];
}
return array_values(array_slice($fields, $marker + 1));
}
}