1617 lines
54 KiB
PHP
1617 lines
54 KiB
PHP
<?php
|
||
|
||
namespace App\Libraries;
|
||
|
||
use App\Models\ClaimDumpFileModel;
|
||
use App\Models\ClaimReportDashboardModel;
|
||
|
||
/**
|
||
* Builds Vidal Health Corporate Analysis MIS from claim_report → claims_dump_vidal.
|
||
*/
|
||
class VidalMisReportService
|
||
{
|
||
public const DUMP_TABLE = 'claims_dump_vidal';
|
||
|
||
private const VERSION = '1.0';
|
||
|
||
/** Exact ICR table row labels (HTML). */
|
||
private const STATUS_ROWS = [
|
||
'Reported',
|
||
'Settled',
|
||
'Rejected',
|
||
'Cancelled',
|
||
'Awaiting Utr',
|
||
'Shortfall',
|
||
'Approved',
|
||
'Underprocess',
|
||
'Bills Pending',
|
||
'Recommended For Repudiation',
|
||
'Recommended For Approval',
|
||
'Outstanding Claims',
|
||
'Incurred(Os+Settled)',
|
||
];
|
||
|
||
private const OS_STATUSES = [
|
||
'Awaiting Utr',
|
||
'Shortfall',
|
||
'Approved',
|
||
'Underprocess',
|
||
'Bills Pending',
|
||
'Recommended For Repudiation',
|
||
'Recommended For Approval',
|
||
];
|
||
|
||
private const DISPOSAL_STATUSES = [
|
||
'Settled',
|
||
'Rejected',
|
||
'Awaiting Utr',
|
||
'Cancelled',
|
||
];
|
||
|
||
/** Settled + Awaiting UTR for "approved claims" sections. */
|
||
private const APPROVED_STATUSES = [
|
||
'Settled',
|
||
'Awaiting Utr',
|
||
];
|
||
|
||
/** Settled / Approved / Awaiting UTR for hospitalisation type. */
|
||
private const HOSP_STATUSES = [
|
||
'Settled',
|
||
'Approved',
|
||
'Awaiting Utr',
|
||
];
|
||
|
||
private const TAT_STATUSES = [
|
||
'Settled',
|
||
'Awaiting Utr',
|
||
'Approved',
|
||
'Rejected',
|
||
];
|
||
|
||
private const LANES = ['Cashless', 'Member'];
|
||
|
||
private const HOSP_SUBTYPES = [
|
||
'Claim Benefits',
|
||
'Daycare',
|
||
'Domiciliary',
|
||
'Health_Check_Up',
|
||
'Hospitalization',
|
||
'Opd',
|
||
];
|
||
|
||
private const GENDER_RELATIONS = [
|
||
'Self', 'Spouse', 'Partner', 'Child', 'Parents', 'In Laws', 'Others',
|
||
];
|
||
|
||
private const AGE_RELATIONS = [
|
||
'Self', 'Spouse', 'Partner', 'Child', 'Parents', 'In Law', 'Other',
|
||
];
|
||
|
||
private const AGE_BANDS = [
|
||
'0-5', '6-10', '11-20', '21-30', '31-40', '41-50', '51-60', '61-70', '>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);
|
||
|
||
$generatedAt = $this->dumpFileModel->getGeneratedAtForPolicy($policyId);
|
||
$generatedAtDisplay = $generatedAt
|
||
? $this->formatDateTimeDisplay($generatedAt)
|
||
: date('d-M-Y H:i');
|
||
|
||
$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<array<string,mixed>>
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,summary:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $dumpRows
|
||
* @return list<array{relation:string,gender:string,age:int|null}>
|
||
*/
|
||
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<array{relation:string,gender:string,age:int|null}> $members
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,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<array{relation:string,gender:string,age:int|null}> $members
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,col_pct:array<string,float>}
|
||
*/
|
||
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<string,mixed> $memberGender
|
||
* @return list<array{relation:string,male:int,female:int}>
|
||
*/
|
||
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<string,mixed> $memberAge
|
||
* @return list<array{band:string,total:int,dominant:string}>
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,col_pct:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,col_pct:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @param list<string> $rowKeys
|
||
* @param callable(array):string $keyFn
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,col_pct:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>,col_pct:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return list<array{hospital_id:string,hospital_name:string,count:int,amount:float}>
|
||
*/
|
||
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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array{band:string,count:int,pct:float}>,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<array<string,mixed>> $rows
|
||
* @return array{rows:list<array<string,mixed>>,total:array<string,mixed>}
|
||
*/
|
||
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<array<string,mixed>> $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<string,mixed> $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<string,mixed> $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<string,mixed> $row */
|
||
private function isApprovedClaim(array $row): bool
|
||
{
|
||
return in_array($this->statusLabel($row), self::APPROVED_STATUSES, true);
|
||
}
|
||
|
||
/** @param array<string,mixed> $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<string,mixed> $row */
|
||
private function claimedAmount(array $row): float
|
||
{
|
||
return $this->toFloat($row['claim_amount'] ?? 0);
|
||
}
|
||
|
||
/** @param array<string,mixed> $row */
|
||
private function approvedAmount(array $row): float
|
||
{
|
||
return $this->toFloat($row['approved_amount'] ?? 0);
|
||
}
|
||
|
||
/** @param array<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<array<string,mixed>> $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;
|
||
}
|
||
}
|