70', ]; private const AMOUNT_BANDS = [ '00K-10K', '10K-20K', '20K-30K', '30K-40K', '40K-50K', '50K-60K', '60K-70K', '70K-80K', '80K-90K', '90K-100K', '>100K', ]; private const TAT_BANDS = [ '0-7', '8-15', '16-30', '31-45', '46-60', '61-90', '>90', ]; private const TOC = [ 'Incurred Claims Ratio (ICR)', 'Hospitalisation Type Details', 'Member Details - Relationship & Gender wise', 'Member Details - Age Band & Relationship wise', 'Claims Approved - Age Band & Relationship wise', 'Claims Approved - Amount Band & Relationship wise', 'Claims Approved - Ailment wise', 'Top 15 Hospital wise utilization', 'Claims Approved - Cashless & Member Summary', 'Turn Around Time (TAT)', 'Month On Month', 'Payout Ratio', 'Policy Details', ]; private ClaimReportDashboardModel $kpiModel; private ClaimDumpFileModel $dumpFileModel; public function __construct( ?ClaimReportDashboardModel $kpiModel = null, ?ClaimDumpFileModel $dumpFileModel = null ) { $this->kpiModel = $kpiModel ?? new ClaimReportDashboardModel(); $this->dumpFileModel = $dumpFileModel ?? new ClaimDumpFileModel(); } /** * @return array{status:bool,message?:string,data?:array} */ public function build(int $policyId): array { if ($policyId <= 0) { return ['status' => false, 'message' => 'client_policy_id is required.']; } $db = db_connect(); if (!$db->tableExists('claim_report') || !$db->tableExists(self::DUMP_TABLE)) { return [ 'status' => false, 'message' => 'Required tables claim_report or claims_dump_vidal do not exist.', ]; } $dumpRows = $this->getLinkedDumpRows($policyId); if ($dumpRows === []) { return [ 'status' => false, 'message' => 'No Vidal dump rows linked via claim_report. Run sync: /util/claims-collection-report/sync', ]; } $policy = $db->table('client_policy cp') ->select('cp.*, c.client_name, i.name AS insurer_name, t.name AS tpa_name') ->join('clients c', 'c.id = cp.client_id', 'left') ->join('insurers i', 'i.id = cp.insurer_id', 'left') ->join('tpa t', 't.id = cp.tpa_id', 'left') ->where('cp.id', $policyId) ->get() ->getRowArray(); if (!$policy) { return ['status' => false, 'message' => 'Policy not found.']; } $exposure = $this->firstRow($this->kpiModel->policy_exposure_summary($policyId)); $premiumRow = $this->firstRow($this->kpiModel->premium_as_on_date($policyId)); $livesRow = $this->firstRow($this->kpiModel->current_emp_lives($policyId)); $experience = $this->firstRow($this->kpiModel->claims_experience_summary($policyId)); $premium = $this->parseFormattedNumber($premiumRow['premium_as_on_date'] ?? null); $earned = $this->parseFormattedNumber($experience['earned_premium'] ?? null); $lives = (int) ($livesRow['current_lives'] ?? 0); $firstDump = $dumpRows[0]; $startRaw = $policy['policy_start_date'] ?? $exposure['policy_start_date'] ?? ($firstDump['policy_start_date'] ?? null); $endRaw = $policy['policy_end_date'] ?? $exposure['policy_end_date'] ?? ($firstDump['policy_end_date'] ?? null); $generatedAtTs = $this->dumpFileModel->getLatestClaimDumpTimestamp($policyId) ?: time(); $generatedAtDisplay = date('d-M-Y', $generatedAtTs); $corporateName = trim((string) ( $firstDump['corporate_name'] ?? $policy['client_name'] ?? '' )); $policyNumber = trim((string) ( $firstDump['insurer_policy_number'] ?? $policy['policy_no'] ?? '' )); $icr = $this->buildIcr($dumpRows, $earned, $lives); $hospitalization = $this->buildHospitalization($dumpRows); $enrollment = $this->loadEnrollmentMembers($policyId, $dumpRows, $lives); $memberGender = $this->buildMemberGender($enrollment); $memberAge = $this->buildMemberAge($enrollment); $claimsAge = $this->buildClaimsByAgeRelation($dumpRows); $claimsAmount = $this->buildClaimsByAmountRelation($dumpRows); $ailments = $this->buildTopAilments($dumpRows, 15); $hospitals = $this->buildTopHospitals($dumpRows, 15); $cmSummary = $this->buildCashlessMemberSummary($dumpRows); $tat = $this->buildTat($dumpRows); $mom = $this->buildMonthOnMonth($dumpRows); $payout = $this->buildPayout($dumpRows); $data = [ 'meta' => [ 'policy_id' => $policyId, 'version' => self::VERSION, 'tpa_name' => trim((string) ($policy['tpa_name'] ?? 'Vidal Health')), 'generated_at' => $generatedAtDisplay, 'client_name' => trim((string) ($policy['client_name'] ?? '')), ], 'header' => [ 'corporate_name' => $corporateName, 'policy_number' => $policyNumber, 'start' => $this->formatDate($startRaw), 'end' => $this->formatDate($endRaw), 'premium' => $premium, 'earned' => $earned, 'lives' => $lives > 0 ? $lives : (int) ($memberGender['total']['total'] ?? 0), 'generated_by' => '', 'generated_at' => $generatedAtDisplay, 'insurer_name' => trim((string) ( $firstDump['insurance_company_name'] ?? $policy['insurer_name'] ?? $exposure['insurer_name'] ?? '' )), ], 'toc' => self::TOC, 'icr' => $icr, 'hospitalization' => $hospitalization, 'member_gender' => $memberGender, 'member_age' => $memberAge, 'claims_age' => $claimsAge, 'claims_amount' => $claimsAmount, 'ailments' => $ailments, 'hospitals' => $hospitals, 'cashless_member_summary' => $cmSummary, 'tat' => $tat, 'month_on_month' => $mom, 'payout' => $payout, 'chart_gender' => $this->buildChartGender($memberGender), 'chart_age' => $this->buildChartAge($memberAge), 'dump_row_count' => count($dumpRows), ]; $data['view_model'] = $data; return ['status' => true, 'data' => $data]; } /** * @return list> */ public function getLinkedDumpRows(int $policyId): array { $db = db_connect(); $reportRows = $db->table('claim_report') ->select('source_row_id') ->where('client_policy_id', $policyId) ->where('is_active', 1) ->where('source_table', self::DUMP_TABLE) ->where('source_row_id IS NOT NULL', null, false) ->get() ->getResultArray(); if ($reportRows === []) { return []; } $dumpIds = array_values(array_unique(array_filter(array_map( static fn ($r) => (int) ($r['source_row_id'] ?? 0), $reportRows )))); if ($dumpIds === []) { return []; } return $db->table(self::DUMP_TABLE) ->whereIn('id', $dumpIds) ->where('is_active', 1) ->orderBy('id', 'ASC') ->get() ->getResultArray(); } // ------------------------------------------------------------------------- // ICR // ------------------------------------------------------------------------- /** * @param list> $rows * @return array{rows:list>,summary:array} */ private function buildIcr(array $rows, float $earned, int $lives): array { $empty = ['count' => 0, 'amount' => 0.0]; $grid = []; foreach (self::STATUS_ROWS as $label) { $grid[$label] = [ 'Cashless' => $empty, 'Member' => $empty, ]; } foreach ($rows as $row) { $status = $this->statusLabel($row); $lane = $this->claimLane($row); if ($status === '' || !isset($grid[$status])) { // Still count toward Reported even if status unknown $statusKey = null; } else { $statusKey = $status; } $claimed = $this->claimedAmount($row); $approved = $this->approvedAmount($row); $osAmt = $this->osAmount($row); // Reported = all $grid['Reported'][$lane]['count']++; $grid['Reported'][$lane]['amount'] += $claimed; if ($statusKey === null) { continue; } // Settled / Awaiting Utr / Approved → approved_amount; Rejected/Cancelled → claimed; other OS → claim/incurred $amt = match ($statusKey) { 'Settled', 'Awaiting Utr', 'Approved' => $approved > 0 ? $approved : $claimed, 'Rejected', 'Cancelled' => $claimed, default => $osAmt, }; $grid[$statusKey][$lane]['count']++; $grid[$statusKey][$lane]['amount'] += $amt; } // Outstanding = sum of OS statuses foreach (self::LANES as $lane) { $c = 0; $a = 0.0; foreach (self::OS_STATUSES as $os) { $c += $grid[$os][$lane]['count']; $a += $grid[$os][$lane]['amount']; } $grid['Outstanding Claims'][$lane] = ['count' => $c, 'amount' => $a]; } // Incurred = OS + Settled foreach (self::LANES as $lane) { $grid['Incurred(Os+Settled)'][$lane] = [ 'count' => $grid['Outstanding Claims'][$lane]['count'] + $grid['Settled'][$lane]['count'], 'amount' => $grid['Outstanding Claims'][$lane]['amount'] + $grid['Settled'][$lane]['amount'], ]; } $outRows = []; foreach (self::STATUS_ROWS as $label) { $c = $grid[$label]['Cashless']; $m = $grid[$label]['Member']; $outRows[] = [ 'label' => $label, 'cashless' => $c, 'member' => $m, 'total' => [ 'count' => $c['count'] + $m['count'], 'amount' => $c['amount'] + $m['amount'], ], ]; } $reportedC = $grid['Reported']['Cashless']['count']; $reportedM = $grid['Reported']['Member']['count']; $reportedT = $reportedC + $reportedM; $disposalNum = static function (array $g, string $lane): int { $n = 0; foreach (self::DISPOSAL_STATUSES as $s) { $n += $g[$s][$lane]['count']; } return $n; }; $disposalC = $reportedC > 0 ? round(($disposalNum($grid, 'Cashless') / $reportedC) * 100) : 0; $disposalM = $reportedM > 0 ? round(($disposalNum($grid, 'Member') / $reportedM) * 100) : 0; $disposalT = $reportedT > 0 ? round((($disposalNum($grid, 'Cashless') + $disposalNum($grid, 'Member')) / $reportedT) * 100) : 0; $incurredTotal = $grid['Incurred(Os+Settled)']['Cashless']['amount'] + $grid['Incurred(Os+Settled)']['Member']['amount']; $icrOnEp = $earned > 0 ? round(($incurredTotal / $earned) * 100, 1) : 0.0; $incidence = $lives > 0 ? round(($reportedT / $lives) * 100, 1) : 0.0; // CPC = approved amt / events for settled + awaiting utr $cpc = $this->computeCpc($rows); return [ 'rows' => $outRows, 'summary' => [ 'icr_on_ep' => $icrOnEp, 'incidence_rate' => $incidence, 'disposal_rate' => [ 'cashless' => $disposalC, 'member' => $disposalM, 'total' => $disposalT, ], 'cpc' => $cpc, ], ]; } /** * @param list> $rows * @return array{cashless:float,member:float,total:float} */ private function computeCpc(array $rows): array { $amt = ['Cashless' => 0.0, 'Member' => 0.0]; $cnt = ['Cashless' => 0, 'Member' => 0]; foreach ($rows as $row) { $status = $this->statusLabel($row); if (!in_array($status, self::APPROVED_STATUSES, true)) { continue; } if (!$this->isMainClaim($row)) { continue; } $lane = $this->claimLane($row); $amt[$lane] += $this->approvedAmount($row); $cnt[$lane]++; } $cpcC = $cnt['Cashless'] > 0 ? round($amt['Cashless'] / $cnt['Cashless']) : 0.0; $cpcM = $cnt['Member'] > 0 ? round($amt['Member'] / $cnt['Member']) : 0.0; $totN = $cnt['Cashless'] + $cnt['Member']; $totA = $amt['Cashless'] + $amt['Member']; $cpcT = $totN > 0 ? round($totA / $totN) : 0.0; return [ 'cashless' => $cpcC, 'member' => $cpcM, 'total' => $cpcT, ]; } // ------------------------------------------------------------------------- // Hospitalisation // ------------------------------------------------------------------------- /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildHospitalization(array $rows): array { $empty = ['count' => 0, 'amount' => 0.0]; $grid = []; foreach (self::HOSP_SUBTYPES as $sub) { $grid[$sub] = ['Cashless' => $empty, 'Member' => $empty]; } foreach ($rows as $row) { $status = $this->statusLabel($row); if (!in_array($status, self::HOSP_STATUSES, true)) { continue; } $sub = $this->hospSubtype($row); $lane = $this->claimLane($row); if (!isset($grid[$sub])) { continue; } $grid[$sub][$lane]['count']++; $grid[$sub][$lane]['amount'] += $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); } $outRows = []; $totals = ['Cashless' => $empty, 'Member' => $empty]; foreach (self::HOSP_SUBTYPES as $sub) { $c = $grid[$sub]['Cashless']; $m = $grid[$sub]['Member']; $outRows[] = [ 'label' => $sub, 'cashless' => $c, 'member' => $m, ]; $totals['Cashless']['count'] += $c['count']; $totals['Cashless']['amount'] += $c['amount']; $totals['Member']['count'] += $m['count']; $totals['Member']['amount'] += $m['amount']; } return [ 'rows' => $outRows, 'total' => [ 'cashless' => $totals['Cashless'], 'member' => $totals['Member'], ], ]; } // ------------------------------------------------------------------------- // Enrollment / member matrices // ------------------------------------------------------------------------- /** * Prefer employees × employee_polices; else unique dump members. * * @param list> $dumpRows * @return list */ private function loadEnrollmentMembers(int $policyId, array $dumpRows, int $lives): array { $db = db_connect(); $members = []; if ($db->tableExists('employees') && $db->tableExists('employee_polices')) { try { $rows = $db->table('employees e') ->select('e.gender, e.dob, e.relationship, e.relationship_code') ->join('employee_polices ep', 'ep.employee_id = e.id') ->where('ep.client_policy_id', $policyId) ->where('ep.is_active', 1) ->whereIn('ep.status', ['active', 'expired']) ->get() ->getResultArray(); foreach ($rows as $r) { $age = null; $dob = trim((string) ($r['dob'] ?? '')); if ($dob !== '' && $dob !== '0000-00-00') { $ts = strtotime($dob); if ($ts) { $age = (int) floor((time() - $ts) / 31557600); } } $members[] = [ 'relation' => $this->normalizeRelation( (string) ($r['relationship'] ?? $r['relationship_code'] ?? ''), true ), 'gender' => $this->normalizeGender($r['gender'] ?? ''), 'age' => $age, ]; } } catch (\Throwable $e) { $members = []; } } if ($members !== []) { return $members; } // Fallback: unique members from dump $seen = []; foreach ($dumpRows as $row) { $uhid = trim((string) ($row['patient_health_card_id'] ?? '')); $key = $uhid !== '' ? $uhid : trim((string) ($row['patient_name'] ?? '')) . '|' . ($row['age'] ?? '') . '|' . ($row['relation'] ?? ''); if ($key === '|' || isset($seen[$key])) { continue; } $seen[$key] = true; $ageRaw = $row['age'] ?? null; $age = is_numeric($ageRaw) ? (int) $ageRaw : null; $members[] = [ 'relation' => $this->normalizeRelation((string) ($row['relation'] ?? ''), true), 'gender' => $this->normalizeGender($row['gender'] ?? ''), 'age' => $age, ]; } // If still empty but lives known, return empty (counts only from dump elsewhere) unset($lives); return $members; } /** * @param list $members * @return array{rows:list>,total:array,pct_gender:array{male:float,female:float}} */ private function buildMemberGender(array $members): array { $grid = []; foreach (self::GENDER_RELATIONS as $rel) { $grid[$rel] = ['male' => 0, 'female' => 0, 'total' => 0]; } foreach ($members as $m) { $rel = $this->mapToGenderRelation($m['relation']); $g = $m['gender']; if (!isset($grid[$rel])) { $rel = 'Others'; } if ($g === 'Male') { $grid[$rel]['male']++; } elseif ($g === 'Female') { $grid[$rel]['female']++; } else { // Unknown gender — skip gender cells but still count? Put in Others male as 0 continue; } $grid[$rel]['total'] = $grid[$rel]['male'] + $grid[$rel]['female']; } $grand = ['male' => 0, 'female' => 0, 'total' => 0]; $rows = []; foreach (self::GENDER_RELATIONS as $rel) { $r = $grid[$rel]; $grand['male'] += $r['male']; $grand['female'] += $r['female']; $grand['total'] += $r['total']; } foreach (self::GENDER_RELATIONS as $rel) { $r = $grid[$rel]; $rows[] = [ 'relation' => $rel, 'male' => $r['male'], 'female' => $r['female'], 'total' => $r['total'], 'pct' => $grand['total'] > 0 ? round(($r['total'] / $grand['total']) * 100, 2) : 0.0, ]; } return [ 'rows' => $rows, 'total' => $grand, 'pct_gender' => [ 'male' => $grand['total'] > 0 ? round(($grand['male'] / $grand['total']) * 100) : 0, 'female' => $grand['total'] > 0 ? round(($grand['female'] / $grand['total']) * 100) : 0, ], ]; } /** * @param list $members * @return array{rows:list>,total:array,col_pct:array} */ private function buildMemberAge(array $members): array { $matrix = []; foreach (self::AGE_BANDS as $band) { foreach (self::AGE_RELATIONS as $rel) { $matrix[$band][$rel] = 0; } $matrix[$band]['_total'] = 0; } foreach ($members as $m) { if ($m['age'] === null) { continue; } $band = $this->ageBandFromInt((int) $m['age']); $rel = $this->mapToAgeRelation($m['relation']); if (!isset($matrix[$band])) { continue; } $matrix[$band][$rel]++; $matrix[$band]['_total']++; } $colTotals = array_fill_keys(self::AGE_RELATIONS, 0); $grand = 0; foreach (self::AGE_BANDS as $band) { foreach (self::AGE_RELATIONS as $rel) { $colTotals[$rel] += $matrix[$band][$rel]; } $grand += $matrix[$band]['_total']; } $rows = []; foreach (self::AGE_BANDS as $band) { $row = ['band' => $band]; foreach (self::AGE_RELATIONS as $rel) { $row[$rel] = $matrix[$band][$rel]; } $row['total'] = $matrix[$band]['_total']; $row['pct'] = $grand > 0 ? round(($matrix[$band]['_total'] / $grand) * 100, 2) : 0.0; $rows[] = $row; } $colPct = []; foreach (self::AGE_RELATIONS as $rel) { $colPct[$rel] = $grand > 0 ? round(($colTotals[$rel] / $grand) * 100) : 0; } return [ 'rows' => $rows, 'total' => array_merge($colTotals, ['_total' => $grand]), 'col_pct' => $colPct, ]; } /** * @param array $memberGender * @return list */ private function buildChartGender(array $memberGender): array { $out = []; foreach ($memberGender['rows'] ?? [] as $r) { $out[] = [ 'relation' => (string) ($r['relation'] ?? ''), 'male' => (int) ($r['male'] ?? 0), 'female' => (int) ($r['female'] ?? 0), ]; } return $out; } /** * @param array $memberAge * @return list */ private function buildChartAge(array $memberAge): array { $out = []; foreach ($memberAge['rows'] ?? [] as $r) { $band = (string) ($r['band'] ?? ''); $total = (int) ($r['total'] ?? 0); $parts = []; foreach (self::AGE_RELATIONS as $rel) { if ((int) ($r[$rel] ?? 0) > 0) { $parts[] = $rel; } } $dominant = match (true) { in_array('Child', $parts, true) && count($parts) === 1 => 'Child', in_array('Self', $parts, true) && in_array('Child', $parts, true) => 'Self+Child', in_array('Self', $parts, true) && in_array('Spouse', $parts, true) => 'Self+Spouse', in_array('Parents', $parts, true) && count(array_diff($parts, ['Parents', 'In Law', 'Other'])) === 0 => 'Parents', default => implode('+', array_slice($parts, 0, 2)), }; $out[] = [ 'band' => $band, 'total' => $total, 'dominant' => $dominant, 'counts' => array_combine( self::AGE_RELATIONS, array_map(static fn ($rel) => (int) ($r[$rel] ?? 0), self::AGE_RELATIONS) ), ]; } return $out; } // ------------------------------------------------------------------------- // Claims approved matrices // ------------------------------------------------------------------------- /** * @param list> $rows * @return array{rows:list>,total:array,col_pct:array} */ private function buildClaimsByAgeRelation(array $rows): array { return $this->buildApprovedRelationGrid( $rows, self::AGE_BANDS, function ($row) { $age = $this->rowAge($row); return $age < 0 ? '' : $this->ageBandFromInt($age); }, 'band' ); } /** * @param list> $rows * @return array{rows:list>,total:array,col_pct:array} */ private function buildClaimsByAmountRelation(array $rows): array { return $this->buildApprovedRelationGrid( $rows, self::AMOUNT_BANDS, fn ($row) => $this->amountBand($this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row)), 'band' ); } /** * @param list> $rows * @param list $rowKeys * @param callable(array):string $keyFn * @return array{rows:list>,total:array,col_pct:array} */ private function buildApprovedRelationGrid(array $rows, array $rowKeys, callable $keyFn, string $keyName): array { $emptyCell = ['count' => 0, 'amount' => 0.0]; $matrix = []; foreach ($rowKeys as $k) { foreach (self::AGE_RELATIONS as $rel) { $matrix[$k][$rel] = $emptyCell; } $matrix[$k]['_total'] = $emptyCell; } foreach ($rows as $row) { if (!$this->isApprovedClaim($row)) { continue; } $key = $keyFn($row); if ($key === '' || !isset($matrix[$key])) { continue; } $rel = $this->mapToAgeRelation($this->normalizeRelation((string) ($row['relation'] ?? ''), false)); $amt = $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); $matrix[$key][$rel]['count']++; $matrix[$key][$rel]['amount'] += $amt; $matrix[$key]['_total']['count']++; $matrix[$key]['_total']['amount'] += $amt; } $colTotals = []; foreach (self::AGE_RELATIONS as $rel) { $colTotals[$rel] = $emptyCell; } $grand = $emptyCell; foreach ($rowKeys as $k) { foreach (self::AGE_RELATIONS as $rel) { $colTotals[$rel]['count'] += $matrix[$k][$rel]['count']; $colTotals[$rel]['amount'] += $matrix[$k][$rel]['amount']; } $grand['count'] += $matrix[$k]['_total']['count']; $grand['amount'] += $matrix[$k]['_total']['amount']; } $outRows = []; foreach ($rowKeys as $k) { $row = [$keyName => $k]; foreach (self::AGE_RELATIONS as $rel) { $row[$rel] = $matrix[$k][$rel]; } $row['total'] = $matrix[$k]['_total']; $row['pct'] = [ 'count' => $grand['count'] > 0 ? round(($matrix[$k]['_total']['count'] / $grand['count']) * 100, 2) : 0.0, 'amount' => $grand['amount'] > 0 ? round(($matrix[$k]['_total']['amount'] / $grand['amount']) * 100, 2) : 0.0, ]; $outRows[] = $row; } $colPct = []; foreach (self::AGE_RELATIONS as $rel) { $colPct[$rel] = [ 'count' => $grand['count'] > 0 ? round(($colTotals[$rel]['count'] / $grand['count']) * 100) : 0, 'amount' => $grand['amount'] > 0 ? round(($colTotals[$rel]['amount'] / $grand['amount']) * 100) : 0, ]; } return [ 'rows' => $outRows, 'total' => array_merge($colTotals, ['_total' => $grand]), 'col_pct' => $colPct, ]; } /** * @param list> $rows * @return array{rows:list>,total:array,col_pct:array} */ private function buildTopAilments(array $rows, int $limit = 15): array { $emptyCell = ['count' => 0, 'amount' => 0.0]; $byAilment = []; foreach ($rows as $row) { if (!$this->isApprovedClaim($row)) { continue; } $name = trim((string) ($row['ailment_grouping'] ?? '')); if ($name === '') { $name = trim((string) ($row['diagnosis'] ?? '')); } if ($name === '') { $name = 'OTHERS'; } $name = strtoupper($name); $rel = $this->mapToAgeRelation($this->normalizeRelation((string) ($row['relation'] ?? ''), false)); $amt = $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); if (!isset($byAilment[$name])) { $byAilment[$name] = ['_total' => $emptyCell]; foreach (self::AGE_RELATIONS as $r) { $byAilment[$name][$r] = $emptyCell; } } $byAilment[$name][$rel]['count']++; $byAilment[$name][$rel]['amount'] += $amt; $byAilment[$name]['_total']['count']++; $byAilment[$name]['_total']['amount'] += $amt; } uasort($byAilment, static function ($a, $b) { $cmp = ($b['_total']['amount'] <=> $a['_total']['amount']); if ($cmp !== 0) { return $cmp; } return $b['_total']['count'] <=> $a['_total']['count']; }); $top = array_slice($byAilment, 0, $limit, true); $grand = $emptyCell; foreach ($byAilment as $block) { $grand['count'] += $block['_total']['count']; $grand['amount'] += $block['_total']['amount']; } // Recalculate col totals from top only for display total row of top-N $colTotals = []; foreach (self::AGE_RELATIONS as $rel) { $colTotals[$rel] = $emptyCell; } $topGrand = $emptyCell; $outRows = []; foreach ($top as $name => $block) { $row = ['ailment' => $name]; foreach (self::AGE_RELATIONS as $rel) { $row[$rel] = $block[$rel]; $colTotals[$rel]['count'] += $block[$rel]['count']; $colTotals[$rel]['amount'] += $block[$rel]['amount']; } $row['total'] = $block['_total']; $topGrand['count'] += $block['_total']['count']; $topGrand['amount'] += $block['_total']['amount']; // % against overall approved (not just top-N), matching HTML sample $row['pct'] = [ 'count' => $grand['count'] > 0 ? round(($block['_total']['count'] / $grand['count']) * 100, 2) : 0.0, 'amount' => $grand['amount'] > 0 ? round(($block['_total']['amount'] / $grand['amount']) * 100, 2) : 0.0, ]; $outRows[] = $row; } // Total row = all approved (HTML shows full total), not just top-N sum $allColTotals = []; foreach (self::AGE_RELATIONS as $rel) { $allColTotals[$rel] = $emptyCell; } foreach ($byAilment as $block) { foreach (self::AGE_RELATIONS as $rel) { $allColTotals[$rel]['count'] += $block[$rel]['count']; $allColTotals[$rel]['amount'] += $block[$rel]['amount']; } } $colPct = []; foreach (self::AGE_RELATIONS as $rel) { $colPct[$rel] = [ 'count' => $grand['count'] > 0 ? round(($allColTotals[$rel]['count'] / $grand['count']) * 100) : 0, 'amount' => $grand['amount'] > 0 ? round(($allColTotals[$rel]['amount'] / $grand['amount']) * 100) : 0, ]; } return [ 'rows' => $outRows, 'total' => array_merge($allColTotals, ['_total' => $grand]), 'col_pct' => $colPct, ]; } /** * Top 15 cashless hospitals by approved_amount. * * @param list> $rows * @return list */ private function buildTopHospitals(array $rows, int $limit = 15): array { $map = []; foreach ($rows as $row) { if ($this->claimLane($row) !== 'Cashless') { continue; } if (!$this->isApprovedClaim($row)) { continue; } $id = trim((string) ($row['hospital_id'] ?? '')); $name = trim((string) ($row['hospital_name'] ?? '')); $key = $id !== '' ? $id : ('name:' . strtoupper($name)); if ($name === '' && $id === '') { continue; } if (!isset($map[$key])) { $map[$key] = [ 'hospital_id' => $id, 'hospital_name' => $name !== '' ? $name : $id, 'count' => 0, 'amount' => 0.0, ]; } $map[$key]['count']++; $map[$key]['amount'] += $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); } usort($map, static function ($a, $b) { $cmp = $b['amount'] <=> $a['amount']; if ($cmp !== 0) { return $cmp; } return $b['count'] <=> $a['count']; }); return array_values(array_slice($map, 0, $limit)); } /** * @param list> $rows * @return array{rows:list>,total:array{count:int,amount:float}} */ private function buildCashlessMemberSummary(array $rows): array { $data = [ 'CASHLESS' => ['count' => 0, 'amount' => 0.0], 'MEMBER' => ['count' => 0, 'amount' => 0.0], ]; foreach ($rows as $row) { if (!$this->isApprovedClaim($row)) { continue; } $lane = strtoupper($this->claimLane($row)); if (!isset($data[$lane])) { continue; } $data[$lane]['count']++; $data[$lane]['amount'] += $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); } $totalCount = $data['CASHLESS']['count'] + $data['MEMBER']['count']; $totalAmount = $data['CASHLESS']['amount'] + $data['MEMBER']['amount']; $outRows = []; foreach (['CASHLESS', 'MEMBER'] as $label) { $outRows[] = [ 'label' => $label, 'count' => $data[$label]['count'], 'count_pct' => $totalCount > 0 ? round(($data[$label]['count'] / $totalCount) * 100, 2) : 0.0, 'amount' => $data[$label]['amount'], 'amount_pct' => $totalAmount > 0 ? round(($data[$label]['amount'] / $totalAmount) * 100, 2) : 0.0, ]; } return [ 'rows' => $outRows, 'total' => ['count' => $totalCount, 'amount' => $totalAmount], ]; } // ------------------------------------------------------------------------- // TAT / MoM / Payout // ------------------------------------------------------------------------- /** * @param list> $rows * @return array{rows:list,total:int} */ private function buildTat(array $rows): array { $counts = array_fill_keys(self::TAT_BANDS, 0); $total = 0; foreach ($rows as $row) { if ($this->claimLane($row) !== 'Member') { continue; } $status = $this->statusLabel($row); if (!in_array($status, self::TAT_STATUSES, true)) { continue; } $days = $this->tatDays($row); if ($days === null) { continue; } $band = $this->tatBand($days); $counts[$band]++; $total++; } $outRows = []; foreach (self::TAT_BANDS as $band) { $outRows[] = [ 'band' => $band, 'count' => $counts[$band], 'pct' => $total > 0 ? round(($counts[$band] / $total) * 100, 2) : 0.0, ]; } return ['rows' => $outRows, 'total' => $total]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildMonthOnMonth(array $rows): array { $empty = ['count' => 0, 'amount' => 0.0]; $byMonth = []; foreach ($rows as $row) { if (!$this->isApprovedClaim($row)) { continue; } $adm = $this->parseDate($row['date_of_admission'] ?? null); if ($adm === null) { continue; } $key = date('Y-m', $adm); $label = date('M Y', $adm); if (!isset($byMonth[$key])) { $byMonth[$key] = [ 'label' => $label, 'hosp' => $empty, 'other' => $empty, ]; } $amt = $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); $sub = strtolower(trim((string) ($row['type_of_hospitalization'] ?? ''))); $isHosp = str_contains($sub, 'hospital') || str_contains($sub, 'daycare') || $sub === 'day care'; $bucket = $isHosp ? 'hosp' : 'other'; $byMonth[$key][$bucket]['count']++; $byMonth[$key][$bucket]['amount'] += $amt; } ksort($byMonth); $totals = ['hosp' => $empty, 'other' => $empty, 'total' => $empty]; $outRows = []; foreach ($byMonth as $block) { $total = [ 'count' => $block['hosp']['count'] + $block['other']['count'], 'amount' => $block['hosp']['amount'] + $block['other']['amount'], ]; $outRows[] = [ 'month' => $block['label'], 'hosp' => $block['hosp'], 'other' => $block['other'], 'total' => $total, ]; $totals['hosp']['count'] += $block['hosp']['count']; $totals['hosp']['amount'] += $block['hosp']['amount']; $totals['other']['count'] += $block['other']['count']; $totals['other']['amount'] += $block['other']['amount']; $totals['total']['count'] += $total['count']; $totals['total']['amount'] += $total['amount']; } return ['rows' => $outRows, 'total' => $totals]; } /** * @param list> $rows * @return array{claimed:float,settled:float,payout_pct:float} */ private function buildPayout(array $rows): array { $claimed = 0.0; $settled = 0.0; foreach ($rows as $row) { $status = $this->statusLabel($row); if ($status === 'Settled') { $claimed += $this->claimedAmount($row); $settled += $this->approvedAmount($row) > 0 ? $this->approvedAmount($row) : $this->claimedAmount($row); } } // HTML sample claimed > settled sum — claimed across settled claims only return [ 'claimed' => $claimed, 'settled' => $settled, 'payout_pct' => $claimed > 0 ? round(($settled / $claimed) * 100) : 0.0, ]; } // ------------------------------------------------------------------------- // Field helpers // ------------------------------------------------------------------------- /** @param array $row */ private function statusLabel(array $row): string { $raw = strtoupper(trim((string) ($row['claim_status'] ?? ''))); $raw = preg_replace('/\s+/', ' ', $raw) ?? $raw; return match (true) { $raw === 'SETTLED' => 'Settled', $raw === 'REJECTED' => 'Rejected', $raw === 'CANCELLED', $raw === 'CANCELED' => 'Cancelled', $raw === 'AWAITING UTR', str_contains($raw, 'AWAITING UTR') => 'Awaiting Utr', $raw === 'SHORTFALL' => 'Shortfall', $raw === 'APPROVED' => 'Approved', $raw === 'UNDERPROCESS', $raw === 'UNDER PROCESS', $raw === 'IN PROCESS' => 'Underprocess', $raw === 'BILLS PENDING', str_contains($raw, 'BILLS PENDING') => 'Bills Pending', str_contains($raw, 'RECOMMENDED FOR REPUDIATION') => 'Recommended For Repudiation', str_contains($raw, 'RECOMMENDED FOR APPROVAL') => 'Recommended For Approval', default => '', }; } /** @param array $row */ private function claimLane(array $row): string { $t = strtoupper(trim((string) ($row['type_of_claim'] ?? ''))); if (str_contains($t, 'CASHLESS')) { return 'Cashless'; } // Member / Reimbursement / RI return 'Member'; } /** @param array $row */ private function isApprovedClaim(array $row): bool { return in_array($this->statusLabel($row), self::APPROVED_STATUSES, true); } /** @param array $row */ private function isMainClaim(array $row): bool { $t = strtolower(trim((string) ($row['mainclaim_prepost_type'] ?? ''))); if ($t === '') { return true; } if (str_contains($t, 'pre') || str_contains($t, 'post') || str_contains($t, 'addendum')) { return false; } return true; // Normal / Main / blank } /** @param array $row */ private function claimedAmount(array $row): float { return $this->toFloat($row['claim_amount'] ?? 0); } /** @param array $row */ private function approvedAmount(array $row): float { return $this->toFloat($row['approved_amount'] ?? 0); } /** @param array $row */ private function osAmount(array $row): float { $incurred = $this->toFloat($row['total_incurred_amount'] ?? 0); if ($incurred > 0) { return $incurred; } return $this->claimedAmount($row); } /** @param array $row */ private function hospSubtype(array $row): string { $raw = trim((string) ($row['type_of_hospitalization'] ?? '')); $norm = strtolower(str_replace([' ', '-'], '_', $raw)); return match (true) { str_contains($norm, 'claim_benefit') || $norm === 'claim_benefits' => 'Claim Benefits', str_contains($norm, 'daycare') || $norm === 'day_care' => 'Daycare', str_contains($norm, 'domiciliary') => 'Domiciliary', str_contains($norm, 'health_check') || str_contains($norm, 'healthcheck') => 'Health_Check_Up', str_contains($norm, 'hospital') => 'Hospitalization', $norm === 'opd' || str_contains($norm, 'opd') => 'Opd', default => 'Hospitalization', }; } /** @param array $row */ private function rowAge(array $row): int { if (is_numeric($row['age'] ?? null)) { return (int) $row['age']; } $dob = trim((string) ($row['date_of_birth'] ?? '')); if ($dob !== '' && $dob !== '0000-00-00') { $ts = strtotime($dob); if ($ts) { return (int) floor((time() - $ts) / 31557600); } } return -1; } private function ageBandFromInt(int $age): string { if ($age < 0) { return '>70'; } if ($age <= 5) { return '0-5'; } if ($age <= 10) { return '6-10'; } if ($age <= 20) { return '11-20'; } if ($age <= 30) { return '21-30'; } if ($age <= 40) { return '31-40'; } if ($age <= 50) { return '41-50'; } if ($age <= 60) { return '51-60'; } if ($age <= 70) { return '61-70'; } return '>70'; } private function amountBand(float $amount): string { if ($amount <= 10000) { return '00K-10K'; } if ($amount <= 20000) { return '10K-20K'; } if ($amount <= 30000) { return '20K-30K'; } if ($amount <= 40000) { return '30K-40K'; } if ($amount <= 50000) { return '40K-50K'; } if ($amount <= 60000) { return '50K-60K'; } if ($amount <= 70000) { return '60K-70K'; } if ($amount <= 80000) { return '70K-80K'; } if ($amount <= 90000) { return '80K-90K'; } if ($amount <= 100000) { return '90K-100K'; } return '>100K'; } /** @param array $row */ private function tatDays(array $row): ?int { $start = $this->parseDate($row['last_document_received_date'] ?? null) ?? $this->parseDate($row['claim_received_date'] ?? null); $end = $this->parseDate($row['claim_decision_date'] ?? null); if ($start === null || $end === null) { return null; } $days = (int) floor(($end - $start) / 86400); if ($days < 0) { return null; } return $days; } private function tatBand(int $days): string { if ($days <= 7) { return '0-7'; } if ($days <= 15) { return '8-15'; } if ($days <= 30) { return '16-30'; } if ($days <= 45) { return '31-45'; } if ($days <= 60) { return '46-60'; } if ($days <= 90) { return '61-90'; } return '>90'; } private function normalizeGender(mixed $value): string { $g = strtoupper(trim((string) $value)); if ($g === '' || $g === 'U' || $g === 'UNKNOWN') { return ''; } if (str_starts_with($g, 'M')) { return 'Male'; } if (str_starts_with($g, 'F')) { return 'Female'; } return ''; } private function normalizeRelation(string $raw, bool $forGender): string { $r = strtolower(trim($raw)); $r = str_replace(['_', '-'], ' ', $r); $r = preg_replace('/\s+/', ' ', $r) ?? $r; if ($r === '' || $r === 'self' || $r === 'employee' || $r === 'primary' || $r === 'e') { return 'Self'; } if (str_contains($r, 'spouse') || $r === 'wife' || $r === 'husband') { return 'Spouse'; } if (str_contains($r, 'partner')) { return 'Partner'; } if (str_contains($r, 'child') || str_contains($r, 'son') || str_contains($r, 'daughter')) { return 'Child'; } if (str_contains($r, 'in law') || str_contains($r, 'inlaw') || str_contains($r, 'father in') || str_contains($r, 'mother in')) { return $forGender ? 'In Laws' : 'In Law'; } if (str_contains($r, 'parent') || $r === 'father' || $r === 'mother' || $r === 'f' || $r === 'm') { return 'Parents'; } return $forGender ? 'Others' : 'Other'; } private function mapToGenderRelation(string $rel): string { return match ($rel) { 'Self', 'Spouse', 'Partner', 'Child', 'Parents', 'In Laws', 'Others' => $rel, 'In Law' => 'In Laws', 'Other' => 'Others', default => 'Others', }; } private function mapToAgeRelation(string $rel): string { return match ($rel) { 'Self', 'Spouse', 'Partner', 'Child', 'Parents', 'In Law', 'Other' => $rel, 'In Laws' => 'In Law', 'Others' => 'Other', default => 'Other', }; } // ------------------------------------------------------------------------- // Format helpers // ------------------------------------------------------------------------- public static function fmtNum(mixed $value, int $decimals = 0): string { if ($value === null || $value === '') { return '0'; } $n = is_numeric($value) ? (float) $value : 0.0; return number_format($n, $decimals, '.', ','); } public static function fmtPct(mixed $value, int $decimals = 0, bool $withSymbol = true): string { if ($value === null || $value === '') { return $withSymbol ? '0%' : '0'; } $n = is_numeric($value) ? (float) $value : 0.0; $s = number_format($n, $decimals, '.', ''); return $withSymbol ? $s . '%' : $s; } private function toFloat(mixed $value): float { if ($value === null || $value === '') { return 0.0; } if (is_int($value) || is_float($value)) { return (float) $value; } $s = trim((string) $value); $s = str_replace([',', '₹', 'Rs.', 'Rs', ' '], '', $s); return $s === '' || $s === '-' || $s === '.' ? 0.0 : (float) $s; } private function parseFormattedNumber(mixed $value): float { return $this->toFloat($value); } /** @param list> $rows */ private function firstRow(array $rows): array { return $rows[0] ?? []; } private function formatDate(mixed $value): string { if ($value === null || $value === '' || $value === '0000-00-00') { return ''; } $ts = strtotime((string) $value); return $ts ? date('d-M-Y', $ts) : (string) $value; } private function formatDateTimeDisplay(string $raw): string { // ClaimDumpFileModel returns d-m-Y $ts = strtotime(str_replace('/', '-', $raw)); if ($ts) { if (preg_match('/\d{1,2}:\d{2}/', $raw)) { return date('d-M-Y H:i', $ts); } return date('d-M-Y', $ts); } return $raw; } private function parseDate(mixed $value): ?int { if ($value === null || $value === '' || $value === '0000-00-00' || $value === '0000-00-00 00:00:00') { return null; } $ts = strtotime((string) $value); return $ts !== false ? $ts : null; } }