Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
VENKATESHWARAN 2026-06-04 11:55:33 +05:30
commit 9f3bd4162f
3 changed files with 175 additions and 18 deletions

View File

@ -49,6 +49,8 @@ class ClaimsCollectionV2DashboardModel extends Model
232 => 'inception_emp_lives',
233 => 'current_emp_lives',
234 => 'claim_value_by_month',
235 => 'claim_amount_by_relationship',
236 => 'top_10_ailments_by_claim_count',
];
/**
@ -89,6 +91,8 @@ class ClaimsCollectionV2DashboardModel extends Model
'inception_emp_lives' => 'Inception Employees & Lives',
'current_emp_lives' => 'Current Employees & Lives',
'claim_value_by_month' => 'Claim Value by Month',
'claim_amount_by_relationship' => 'Claim Amount by Relationship',
'top_10_ailments_by_claim_count' => 'Top 10 Ailments by Claim Count',
];
protected function runKpiQuery(string $sql, int $policyId): array
@ -2175,6 +2179,113 @@ GROUP BY
totals.total_count,
totals.total_value
ORDER BY month_sort;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #235: Claim Amount by Relationship */
public function claim_amount_by_relationship(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COALESCE(tm.relationship, 'Unknown') AS relationship,
COUNT(tm.id) AS claim_count,
ROUND(COUNT(tm.id) / NULLIF(totals.total_count, 0) * 100, 2)
AS count_pct,
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0) AS claim_value,
ROUND(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
/ NULLIF(totals.total_value, 0) * 100,
2)
AS value_pct
FROM ticket_master tm
JOIN (
SELECT
COUNT(id) AS total_count,
COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
FROM ticket_master
WHERE client_policy_id = :policy_id: AND is_active = 1
) totals ON 1=1
WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
GROUP BY tm.relationship
ORDER BY claim_count DESC;
-- SELECT
-- COALESCE(
-- CONCAT(UPPER(SUBSTRING(tm.relationship, 1, 1)), LOWER(SUBSTRING(tm.relationship, 2))),
-- 'Unknown'
-- ) AS relationship,
-- COUNT(tm.id) AS claim_count,
-- ROUND(COUNT(tm.id) / NULLIF(totals.total_count, 0) * 100, 2) AS count_pct,
-- COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0) AS claim_value,
-- ROUND(
-- COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
-- / NULLIF(totals.total_value, 0) * 100,
-- 2) AS value_pct
-- FROM ticket_master tm
-- JOIN (
-- SELECT
-- COUNT(id) AS total_count,
-- COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
-- FROM ticket_master
-- WHERE client_policy_id = :policy_id: AND is_active = 1
-- ) totals ON 1=1
-- WHERE tm.client_policy_id = :policy_id:
-- AND tm.is_active = 1
-- GROUP BY tm.relationship
-- ORDER BY
-- CASE LOWER(tm.relationship)
-- WHEN 'self' THEN 1
-- WHEN 'spouse' THEN 2
-- WHEN 'son' THEN 3
-- WHEN 'daughter' THEN 4
-- WHEN 'father' THEN 5
-- WHEN 'mother' THEN 6
-- WHEN 'father-in-law' THEN 7
-- WHEN 'mother-in-law' THEN 8
-- WHEN 'unknown' THEN 9
-- ELSE 10
-- END;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #236: Top 10 Ailments by Claim Count */
public function top_10_ailments_by_claim_count(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COALESCE(tm.tpa_ailments, 'Not Specified') AS ailment,
COUNT(tm.id) AS claim_count,
CONCAT(ROUND(COUNT(tm.id) / totals.total_count * 100, 2), '%')
AS count_pct,
FORMAT(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
, 0) AS claim_value,
CONCAT(ROUND(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
/ NULLIF(totals.total_value, 0) * 100
, 2), '%') AS value_pct
FROM ticket_master tm
JOIN (
SELECT
COUNT(id) AS total_count,
COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
FROM ticket_master
WHERE client_policy_id = :policy_id: AND is_active = 1
) totals ON 1=1
WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
AND tm.tpa_ailments IS NOT NULL
GROUP BY tm.tpa_ailments, totals.total_count, totals.total_value
ORDER BY claim_count DESC
LIMIT 10;
SQL;
return $this->runKpiQuery($sql, $policyId);
}

View File

@ -4,9 +4,9 @@
**Updated:** 2026-06-03
**Controller:** `App\Controllers\ClaimsCollectionV2DashboardController`
**Model:** `App\Models\ClaimsCollectionV2DashboardModel`
**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2` (31 original) + 3 HR exposure/trend KPIs added in-model
**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2` (31 original) + 5 KPIs added in-model (232236)
**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php`
**Total KPIs:** 34
**Total KPIs:** 36
---
@ -19,7 +19,7 @@ The API exposes them in three ways:
| What | URL fragment | Use case |
|------|-------------|----------|
| **Single KPI** | `kpi/{slug or Metabase id}` | FE loads one card at a time |
| **All KPIs** | `all` | FE loads entire dashboard in one call (34 KPIs) |
| **All KPIs** | `all` | FE loads entire dashboard in one call (36 KPIs) |
| **Debug / preview** | `debug` / `preview` | Admin checks raw output in browser |
All endpoints require `client_policy` or `client_policy_id` as a query param
@ -39,7 +39,7 @@ Prefix: `util/claims-collection-v2`
| GET | `util/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
| GET | `util/claims-collection-v2/debug` | `::debug` | Raw JSON dump (all KPIs) |
| GET | `util/claims-collection-v2/debug/{policy_id}` | `::debug` | Same, policy in URL |
| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 34 KPIs |
| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 36 KPIs |
| GET | `util/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
**Filters applied:** `authMVC`, `AclFilter`, `HttpRequestLog`, `Cors`, `SecurityInputFilter`
@ -52,7 +52,7 @@ Prefix: `employeeRest/claims-collection-v2`
| Method | Path | Handler | Purpose |
|--------|------|---------|---------|
| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 34 KPIs |
| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 36 KPIs |
| GET | `employeeRest/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
| GET | `employeeRest/claims-collection-v2/preview` | `::preview` | UI grid (FE debug) |
| GET | `employeeRest/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
@ -88,7 +88,7 @@ GET /index.php/util/claims-collection-v2/preview?client_policy=4687
# Preview with policy in URL
GET /index.php/util/claims-collection-v2/preview/4687
# Raw JSON — all 34 KPIs
# Raw JSON — all 36 KPIs
GET /index.php/util/claims-collection-v2/debug?client_policy=4687
# Raw JSON — single KPI by slug
@ -101,6 +101,8 @@ GET /index.php/util/claims-collection-v2/kpi/207?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/inception_emp_lives?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/current_emp_lives?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/claim_value_by_month?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/claim_amount_by_relationship?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/top_10_ailments_by_claim_count?client_policy=4687
```
---
@ -127,7 +129,13 @@ X-App-Signature: <app_signature>
```
```http
GET /index.php/employeeRest/claims-collection-v2/kpi/234?client_policy=4687
GET /index.php/employeeRest/claims-collection-v2/kpi/claim_amount_by_relationship?client_policy=4687
Authorization: Bearer <jwt_token>
X-App-Signature: <app_signature>
```
```http
GET /index.php/employeeRest/claims-collection-v2/kpi/235?client_policy=4687
Authorization: Bearer <jwt_token>
X-App-Signature: <app_signature>
```
@ -265,15 +273,17 @@ X-App-Signature: <app_signature>
"207": "incurred_ratio",
"232": "inception_emp_lives",
"233": "current_emp_lives",
"234": "claim_value_by_month"
"234": "claim_value_by_month",
"235": "claim_amount_by_relationship",
"236": "top_10_ailments_by_claim_count"
}
}
```
**HTTP 404** — `allowed` lists the full `KPI_MAP` (34 entries).
**HTTP 404** — `allowed` lists the full `KPI_MAP` (36 entries).
---
## All 34 KPIs
## All 36 KPIs
| Metabase ID | Method slug | Label | Rows |
|-------------|-------------|-------|------|
@ -311,20 +321,56 @@ X-App-Signature: <app_signature>
| 232 | `inception_emp_lives` | Inception Employees & Lives | single |
| 233 | `current_emp_lives` | Current Employees & Lives | single |
| 234 | `claim_value_by_month` | Claim Value by Month | multi |
| 235 | `claim_amount_by_relationship` | Claim Amount by Relationship | multi |
| 236 | `top_10_ailments_by_claim_count` | Top 10 Ailments by Claim Count | multi |
> **Note:** KPIs 214 and 216 were renamed from the auto-generated slug to avoid collision with 213 and 215.
> **Note:** IDs **232234** were assigned in-app for KPIs added outside the original Metabase CSV export. Confirm real Metabase question IDs and update `KPI_MAP` if they differ.
> **Note:** IDs **232236** were assigned in-app for KPIs added outside the original Metabase CSV export. Confirm real Metabase question IDs and update `KPI_MAP` if they differ.
---
## Output columns — KPIs 232234
## Output columns — KPIs 232236
| Slug | Row fields | Description |
|------|------------|-------------|
| `inception_emp_lives` | `inception_emp`, `inception_lives` | Distinct Self employees and all lives at inception (`change_event` contains inception) |
| `current_emp_lives` | `current_emp`, `current_lives`, `avg_family_size` | Active employees/lives; `avg_family_size` = lives ÷ employees (2 dp) |
| `claim_value_by_month` | `claim_month`, `month_sort`, `claim_value` | Monthly sum of `claim_amount`; ordered by `month_sort` |
| `claim_amount_by_relationship` | `relationship`, `claim_count`, `count_pct`, `claim_value`, `value_pct` | Claims by `ticket_master.relationship`; `count_pct` / `value_pct` are numeric (not `%` strings) |
| `top_10_ailments_by_claim_count` | `ailment`, `claim_count`, `count_pct`, `claim_value`, `value_pct` | Top 10 `tpa_ailments` (non-null); `claim_value` formatted via `FORMAT()`; pct fields as `%` strings |
---
### `kpi/claim_amount_by_relationship` — breakdown (multi-row)
```json
{
"status": true,
"policy_id": 4687,
"kpi_id": 235,
"kpi": "claim_amount_by_relationship",
"label": "Claim Amount by Relationship",
"rows": [
{
"relationship": "Self",
"claim_count": "42",
"count_pct": "35.00",
"claim_value": "850000.00",
"value_pct": "40.50"
},
{
"relationship": "Spouse",
"claim_count": "28",
"count_pct": "23.33",
"claim_value": "520000.00",
"value_pct": "24.80"
}
]
}
```
> Not the same as `claim_amount_by_gender` (#190), which groups by employee `gender` via join — not `ticket_master.relationship`.
---
@ -332,7 +378,7 @@ X-App-Signature: <app_signature>
| File | Purpose |
|------|---------|
| `app/Models/ClaimsCollectionV2DashboardModel.php` | 34 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` |
| `app/Models/ClaimsCollectionV2DashboardModel.php` | 36 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` |
| `app/Controllers/ClaimsCollectionV2DashboardController.php` | `kpi()`, `all()`, `preview()`, `debug()` |
| `app/Views/claims_collection_v2_dashboard.php` | Admin/FE debug preview UI (KPI card grid) |
| `app/Config/Routes.php` | Both route groups (search `claims-collection-v2`) |
@ -344,7 +390,7 @@ X-App-Signature: <app_signature>
## FE integration notes
- Call `all` once on dashboard mount; render each `data[method].rows` into its card (34 keys under `data`).
- Call `all` once on dashboard mount; render each `data[method].rows` into its card (36 keys under `data`).
- Call `kpi/{slug}` for lazy/on-demand loading of individual cards.
- `policy_id` should come from the HR session / selected policy context — never hardcoded.
- All rows are raw arrays; formatting (currency, %, dates) is already applied inside the SQL where applicable (`FORMAT()`, `CONCAT()`, `DATE_FORMAT()`).
@ -360,4 +406,4 @@ X-App-Signature: <app_signature>
- To add a new KPI: add an entry to `KPI_MAP` + `KPI_LABELS` in the model, write `public function my_kpi(int $policyId): array`, update this doc and bump the smoke test KPI count.
- All queries use named binding `:policy_id:` (CodeIgniter style, replaces Metabase `{{policy_id}}`).
- Literal `\t` / `\n` in CSV SQL is normalized in `runKpiQuery()` — safe to re-generate from CSV.
- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change (expects `KPI_MAP` count === 34).
- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change (expects `KPI_MAP` count === 36).

View File

@ -51,10 +51,10 @@ function ok(string $label, bool $cond, string $detail = ''): void
$model = new ClaimsCollectionV2DashboardModel();
$kpiMap = ClaimsCollectionV2DashboardModel::KPI_MAP;
ok('KPI_MAP count', count($kpiMap) === 34, (string) count($kpiMap));
ok('KPI_MAP count', count($kpiMap) === 36, (string) count($kpiMap));
$uniqueMethods = array_unique(array_values($kpiMap));
ok('unique KPI method names', count($uniqueMethods) === 34, count($uniqueMethods) . ' methods');
ok('unique KPI method names', count($uniqueMethods) === 36, count($uniqueMethods) . ' methods');
foreach (['policy_exposure_summary', 'incurred_ratio'] as $slug) {
ok("slug map contains {$slug}", in_array($slug, $kpiMap, true));
@ -93,7 +93,7 @@ $badResp = json_decode($controller->kpi('not_a_kpi')->getJSON(), true);
ok('controller unknown kpi 404', ($badResp['status'] ?? true) === false);
$allResp = json_decode($controller->all()->getJSON(), true);
ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 34);
ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 36);
$debugOut = $controller->debug($policyId);
$debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody();