From 0d6d8f6555659e187d2818d2f0ffd7a1e4cce7a5 Mon Sep 17 00:00:00 2001 From: velz Date: Wed, 3 Jun 2026 16:44:44 +0530 Subject: [PATCH 1/4] FEAT_HR_DASBOARD_MISSED_KPI_ADDED --- .../ClaimsCollectionV2DashboardModel.php | 92 ++++++++ hr-dashboard.md | 205 +++++++++++++----- tests/smoke_claims_collection_v2.php | 6 +- 3 files changed, 248 insertions(+), 55 deletions(-) diff --git a/app/Models/ClaimsCollectionV2DashboardModel.php b/app/Models/ClaimsCollectionV2DashboardModel.php index d459d79a..447dcee0 100644 --- a/app/Models/ClaimsCollectionV2DashboardModel.php +++ b/app/Models/ClaimsCollectionV2DashboardModel.php @@ -46,6 +46,9 @@ class ClaimsCollectionV2DashboardModel extends Model 229 => 'cataract_avg_exceeded_amount', 230 => 'hospital_city_wise_si_limit_cataract', 231 => 'total_incurred_by_cliam_status', + 232 => 'inception_emp_lives', + 233 => 'current_emp_lives', + 234 => 'claim_value_by_month', ]; /** @@ -83,6 +86,9 @@ class ClaimsCollectionV2DashboardModel extends Model 'cataract_avg_exceeded_amount' => 'Cataract avg exceeded amount', 'hospital_city_wise_si_limit_cataract' => 'Hospital city wise SI Limit - Cataract', 'total_incurred_by_cliam_status' => 'Total Incurred by Cliam Status ', + 'inception_emp_lives' => 'Inception Employees & Lives', + 'current_emp_lives' => 'Current Employees & Lives', + 'claim_value_by_month' => 'Claim Value by Month', ]; protected function runKpiQuery(string $sql, int $policyId): array @@ -2083,6 +2089,92 @@ WHERE tm.client_policy_id = :policy_id: AND tm.is_active = 1 GROUP BY tcs.display_name, totals.total_count, totals.total_value -- ORDER BY claim_count DESC; +SQL; + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #232: Inception Employees & Lives */ + public function inception_emp_lives(int $policyId): array + { + $sql = <<<'SQL' +SELECT + COUNT(DISTINCT CASE + WHEN e.relationship = 'Self' THEN e.id + END) AS inception_emp, + + COUNT(DISTINCT e.id) AS inception_lives + +FROM employee_polices ep +JOIN employees e ON e.id = ep.employee_id +WHERE ep.client_policy_id = :policy_id: + AND LOWER(e.change_event) LIKE '%inception%' + AND ep.is_active = 1 + AND e.is_active = 1; +SQL; + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #233: Current Employees & Lives */ + public function current_emp_lives(int $policyId): array + { + $sql = <<<'SQL' +SELECT + COUNT(DISTINCT CASE + WHEN e.relationship = 'Self' THEN e.id + END) AS current_emp, + + COUNT(DISTINCT e.id) AS current_lives, + + ROUND( + COUNT(DISTINCT e.id) + / NULLIF(COUNT(DISTINCT CASE + WHEN e.relationship = 'Self' THEN e.id + END), 0) + , 2) AS avg_family_size + +FROM employee_polices ep +JOIN employees e ON e.id = ep.employee_id +WHERE ep.client_policy_id = :policy_id: + AND ep.status = 'active' + AND ep.is_active = 1 + AND e.emp_status = 'active' + AND e.is_active = 1; +SQL; + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #234: Claim Value by Month */ + public function claim_value_by_month(int $policyId): array + { + $sql = <<<'SQL' +SELECT + DATE_FORMAT(tm.created_at, '%b %Y') AS claim_month, + DATE_FORMAT(tm.created_at, '%Y-%m') AS month_sort, + -- COUNT(tm.id) AS claim_count, + -- CONCAT(ROUND(COUNT(tm.id) / totals.total_count * 100, 2), '%') AS count_pct, + + COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 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 +GROUP BY + DATE_FORMAT(tm.created_at, '%b %Y'), + DATE_FORMAT(tm.created_at, '%Y-%m'), + totals.total_count, + totals.total_value +ORDER BY month_sort; SQL; return $this->runKpiQuery($sql, $policyId); } diff --git a/hr-dashboard.md b/hr-dashboard.md index de69b3ac..f1ca5a87 100644 --- a/hr-dashboard.md +++ b/hr-dashboard.md @@ -1,22 +1,25 @@ # Claims Collection V2 — HR Dashboard API **Created:** 2026-06-02 +**Updated:** 2026-06-03 **Controller:** `App\Controllers\ClaimsCollectionV2DashboardController` **Model:** `App\Models\ClaimsCollectionV2DashboardModel` -**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2` -**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php` +**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2` (31 original) + 3 HR exposure/trend KPIs added in-model +**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php` +**Total KPIs:** 34 --- ## Overview -Each Metabase question in the CSV is a separate PHP method on the model. +Each KPI is a separate PHP method on the model, registered in `KPI_MAP` (Metabase question id → slug) and `KPI_LABELS` (slug → display label). + 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 | +| **All KPIs** | `all` | FE loads entire dashboard in one call (34 KPIs) | | **Debug / preview** | `debug` / `preview` | Admin checks raw output in browser | All endpoints require `client_policy` or `client_policy_id` as a query param @@ -36,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 31 KPIs | +| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 34 KPIs | | GET | `util/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI | **Filters applied:** `authMVC`, `AclFilter`, `HttpRequestLog`, `Cors`, `SecurityInputFilter` @@ -49,7 +52,7 @@ Prefix: `employeeRest/claims-collection-v2` | Method | Path | Handler | Purpose | |--------|------|---------|---------| -| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 31 KPIs | +| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 34 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 | @@ -85,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 31 KPIs +# Raw JSON — all 34 KPIs GET /index.php/util/claims-collection-v2/debug?client_policy=4687 # Raw JSON — single KPI by slug @@ -93,6 +96,11 @@ GET /index.php/util/claims-collection-v2/kpi/incurred_ratio?client_policy=4687 # Raw JSON — single KPI by Metabase question id GET /index.php/util/claims-collection-v2/kpi/207?client_policy=4687 + +# Exposure KPIs (added 2026-06-03) +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 ``` --- @@ -113,7 +121,13 @@ X-App-Signature: ``` ```http -GET /index.php/employeeRest/claims-collection-v2/kpi/207?client_policy=4687 +GET /index.php/employeeRest/claims-collection-v2/kpi/inception_emp_lives?client_policy=4687 +Authorization: Bearer +X-App-Signature: +``` + +```http +GET /index.php/employeeRest/claims-collection-v2/kpi/234?client_policy=4687 Authorization: Bearer X-App-Signature: ``` @@ -122,7 +136,7 @@ X-App-Signature: ## Sample responses -### `kpi/{slug}` or `kpi/{id}` +### `kpi/{slug}` — single-row KPI ```json { @@ -137,6 +151,54 @@ X-App-Signature: } ``` +### `kpi/inception_emp_lives` — exposure (single row) + +```json +{ + "status": true, + "policy_id": 4687, + "kpi_id": 232, + "kpi": "inception_emp_lives", + "label": "Inception Employees & Lives", + "rows": [ + { "inception_emp": "150", "inception_lives": "420" } + ] +} +``` + +### `kpi/current_emp_lives` — exposure (single row) + +```json +{ + "status": true, + "policy_id": 4687, + "kpi_id": 233, + "kpi": "current_emp_lives", + "label": "Current Employees & Lives", + "rows": [ + { "current_emp": "145", "current_lives": "410", "avg_family_size": "2.83" } + ] +} +``` + +### `kpi/claim_value_by_month` — time series (multi-row) + +```json +{ + "status": true, + "policy_id": 4687, + "kpi_id": 234, + "kpi": "claim_value_by_month", + "label": "Claim Value by Month", + "rows": [ + { "claim_month": "Jan 2024", "month_sort": "2024-01", "claim_value": "125000.00" }, + { "claim_month": "Feb 2024", "month_sort": "2024-02", "claim_value": "98000.50" } + ] +} +``` + +> Sort charts by `month_sort` (ISO `YYYY-MM`), not `claim_month` (display label). + ### `all` ```json @@ -160,6 +222,23 @@ X-App-Signature: "id": 207, "label": "Incurred Ratio", "rows": [{ "incurred_ratio": "72.34%" }] + }, + "inception_emp_lives": { + "id": 232, + "label": "Inception Employees & Lives", + "rows": [{ "inception_emp": "150", "inception_lives": "420" }] + }, + "current_emp_lives": { + "id": 233, + "label": "Current Employees & Lives", + "rows": [{ "current_emp": "145", "current_lives": "410", "avg_family_size": "2.83" }] + }, + "claim_value_by_month": { + "id": 234, + "label": "Claim Value by Month", + "rows": [ + { "claim_month": "Jan 2024", "month_sort": "2024-01", "claim_value": "125000.00" } + ] } } } @@ -183,80 +262,102 @@ X-App-Signature: "message": "Unknown KPI. Pass Metabase id or method slug.", "allowed": { "181": "policy_exposure_summary", - "207": "incurred_ratio" + "207": "incurred_ratio", + "232": "inception_emp_lives", + "233": "current_emp_lives", + "234": "claim_value_by_month" } } ``` -**HTTP 404** +**HTTP 404** — `allowed` lists the full `KPI_MAP` (34 entries). --- -## All 31 KPIs +## All 34 KPIs -| Metabase ID | Method slug | Label | -|-------------|-------------|-------| -| 181 | `policy_exposure_summary` | POLICY & EXPOSURE SUMMARY | -| 185 | `premium_as_on_date` | PREMIUM AS ON DATE | -| 186 | `claims_experience_summary` | CLAIMS EXPERIENCE SUMMARY | -| 190 | `claim_amount_by_gender` | Claim Amount by Gender | -| 191 | `age_band` | Age Band | -| 194 | `top_5_hospitals_by_incurred_amount` | Top 5 Hospitals by Incurred amount | -| 197 | `claims_incidence_rate` | Claims Incidence Rate | -| 198 | `policy_start_date` | Policy Start Date | -| 199 | `policy_end_date` | Policy End Date | -| 200 | `insurer` | Insurer | -| 201 | `tpa` | TPA | -| 203 | `earned_premium` | Earned Premium | -| 204 | `total_claims` | Total Claims | -| 206 | `incurred_amount` | Incurred Amount | -| 207 | `incurred_ratio` | Incurred Ratio | -| 208 | `projected_claims` | Projected Claims | -| 209 | `projected_ratio` | Projected Ratio | -| 213 | `total_reimbursement_amount` | Total Reimbursement Amount | -| 214 | `total_reimbursement_amt_pct` | Total Reimbursement Amt % | -| 215 | `cashless_claim_amt` | Cashless Claim Amt | -| 216 | `cashless_claim_amt_pct` | Cashless Claim Amt % | -| 217 | `total_incurred_by_city` | Total Incurred by city | -| 219 | `claim_amount_by_claim_status` | Claim Amount by Claim Status | -| 220 | `hospitals_in_detail` | Hospitals in detail | -| 221 | `hospital_city_wise_si_limit_pregnancy` | Hospital city wise SI Limit - Pregnancy | -| 224 | `s_pregnancy_normal_delivery_exceeded_amt` | S-PREGNANCY - NORMAL DELIVERY Exceeded Amt | -| 225 | `s_pregnancy_c_sec_avg_exceeded_amt` | S-Pregnancy C-Sec avg exceeded amt | -| 228 | `cataract_exceeded_claim_amount` | Cataract exceeded claim amount | -| 229 | `cataract_avg_exceeded_amount` | Cataract avg exceeded amount | -| 230 | `hospital_city_wise_si_limit_cataract` | Hospital city wise SI Limit - Cataract | -| 231 | `total_incurred_by_cliam_status` | Total Incurred by Cliam Status | +| Metabase ID | Method slug | Label | Rows | +|-------------|-------------|-------|------| +| 181 | `policy_exposure_summary` | POLICY & EXPOSURE SUMMARY | single | +| 185 | `premium_as_on_date` | PREMIUM AS ON DATE | single | +| 186 | `claims_experience_summary` | CLAIMS EXPERIENCE SUMMARY | single | +| 190 | `claim_amount_by_gender` | Claim Amount by Gender | multi | +| 191 | `age_band` | Age Band | multi | +| 194 | `top_5_hospitals_by_incurred_amount` | Top 5 Hospitals by Incurred amount | multi | +| 197 | `claims_incidence_rate` | Claims Incidence Rate | single | +| 198 | `policy_start_date` | Policy Start Date | single | +| 199 | `policy_end_date` | Policy End Date | single | +| 200 | `insurer` | Insurer | single | +| 201 | `tpa` | TPA | single | +| 203 | `earned_premium` | Earned Premium | single | +| 204 | `total_claims` | Total Claims | single | +| 206 | `incurred_amount` | Incurred Amount | single | +| 207 | `incurred_ratio` | Incurred Ratio | single | +| 208 | `projected_claims` | Projected Claims | single | +| 209 | `projected_ratio` | Projected Ratio | single | +| 213 | `total_reimbursement_amount` | Total Reimbursement Amount | single | +| 214 | `total_reimbursement_amt_pct` | Total Reimbursement Amt % | single | +| 215 | `cashless_claim_amt` | Cashless Claim Amt | single | +| 216 | `cashless_claim_amt_pct` | Cashless Claim Amt % | single | +| 217 | `total_incurred_by_city` | Total Incurred by city | multi | +| 219 | `claim_amount_by_claim_status` | Claim Amount by Claim Status | multi | +| 220 | `hospitals_in_detail` | Hospitals in detail | multi | +| 221 | `hospital_city_wise_si_limit_pregnancy` | Hospital city wise SI Limit - Pregnancy | multi | +| 224 | `s_pregnancy_normal_delivery_exceeded_amt` | S-PREGNANCY - NORMAL DELIVERY Exceeded Amt | multi | +| 225 | `s_pregnancy_c_sec_avg_exceeded_amt` | S-Pregnancy C-Sec avg exceeded amt | multi | +| 228 | `cataract_exceeded_claim_amount` | Cataract exceeded claim amount | multi | +| 229 | `cataract_avg_exceeded_amount` | Cataract avg exceeded amount | multi | +| 230 | `hospital_city_wise_si_limit_cataract` | Hospital city wise SI Limit - Cataract | multi | +| 231 | `total_incurred_by_cliam_status` | Total Incurred by Cliam Status | multi | +| 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 | > **Note:** KPIs 214 and 216 were renamed from the auto-generated slug to avoid collision with 213 and 215. +> **Note:** IDs **232–234** 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 232–234 + +| 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` | + --- ## Files | File | Purpose | |------|---------| -| `app/Models/ClaimsCollectionV2DashboardModel.php` | 31 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` | +| `app/Models/ClaimsCollectionV2DashboardModel.php` | 34 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`) | | `tests/smoke_claims_collection_v2.php` | CLI smoke test — run: `php tests/smoke_claims_collection_v2.php 4687` | -| `metabase_raw_queries.csv` | Source of truth for all SQL queries | +| `metabase_raw_queries.csv` | Source of truth for original Metabase SQL queries | +| `hr-dashboard.md` | This document | --- ## FE integration notes -- Call `all` once on dashboard mount; render each `data[method].rows` into its card. +- Call `all` once on dashboard mount; render each `data[method].rows` into its card (34 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 (`FORMAT()`, `CONCAT()`). -- `rows` may be empty `[]` if no claims exist for that policy — handle gracefully in UI. +- All rows are raw arrays; formatting (currency, %, dates) is already applied inside the SQL where applicable (`FORMAT()`, `CONCAT()`, `DATE_FORMAT()`). +- `rows` may be empty `[]` if no data exists for that policy — handle gracefully in UI. +- **Single-row KPIs** (e.g. `incurred_ratio`, `inception_emp_lives`): use `rows[0]`. +- **Multi-row KPIs** (e.g. `claim_value_by_month`, `age_band`): iterate `rows`; for time series use `month_sort` for sort order. +- **Exposure block:** `inception_emp_lives` + `current_emp_lives` pair for inception vs current headcount. --- ## BE notes -- To add a new KPI: add an entry to `KPI_MAP` + `KPI_LABELS` in the model and write the corresponding method `public function my_kpi(int $policyId): array`. +- 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. +- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change (expects `KPI_MAP` count === 34). diff --git a/tests/smoke_claims_collection_v2.php b/tests/smoke_claims_collection_v2.php index 324bb8ab..606fe2c4 100644 --- a/tests/smoke_claims_collection_v2.php +++ b/tests/smoke_claims_collection_v2.php @@ -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) === 31, (string) count($kpiMap)); +ok('KPI_MAP count', count($kpiMap) === 34, (string) count($kpiMap)); $uniqueMethods = array_unique(array_values($kpiMap)); -ok('unique KPI method names', count($uniqueMethods) === 31, count($uniqueMethods) . ' methods'); +ok('unique KPI method names', count($uniqueMethods) === 34, 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'] ?? []) === 31); +ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 34); $debugOut = $controller->debug($policyId); $debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody(); From 67d89e1fd5579b216033ae63849c07d8341cff3c Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 3 Jun 2026 17:40:58 +0530 Subject: [PATCH 2/4] Sales tracker Changes --- app/Controllers/SalesController.php | 30 ++++ .../sales/branch_level_dashboard_view.php | 152 ++++++++++++++---- 2 files changed, 151 insertions(+), 31 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 659299cd..edf64242 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -2028,6 +2028,35 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s ORDER BY l.created_at DESC ", [$uid, $fyStart, $fyEnd])->getResultArray(); + // ── Lost Leads for this user in FY (Opportunities tab — toggle off) ── + $lossLeads = $db->query(" + SELECT + l.id AS opportunities_id, + l.actual_lead_id, + COALESCE(l.lead_form_type, 1) AS lead_form_type_id, + l.lead_type AS lead_type_id, + sal.company_name AS company, + l.client_name AS client_name, + CASE WHEN COALESCE(l.lead_form_type, 1) = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_form_type, + CASE + WHEN l.lead_type = 1 THEN 'Fresh' + WHEN l.lead_type = 2 THEN 'Renewal' + WHEN l.lead_type = 3 THEN 'Roll Over' + ELSE '' + END AS lead_type, + l.created_at AS created_at, + l.status, + l.lost_reason AS lost_reason + FROM leads l + INNER JOIN sales_actual_leads sal + ON sal.lead_id = l.actual_lead_id + WHERE l.status = 'lost' + AND sal.assigned_to = ? + AND l.created_at >= ? + AND l.created_at <= ? + ORDER BY l.created_at DESC + ", [$uid, $fyStart, $fyEnd])->getResultArray(); + // -- NOTE: AND TRIM(sal.company_name) = TRIM(l.client_name) $result[$uid] = [ 'total_policies' => $totalPolicies, @@ -2035,6 +2064,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s 'target_amt' => $targetAmt, 'policies' => $policies, 'won_leads' => $wonLeads, // for Table 2 in modal Tab 2 + 'loss_leads' => $lossLeads, ]; } diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php index eba15043..09be0731 100644 --- a/app/Views/sales/branch_level_dashboard_view.php +++ b/app/Views/sales/branch_level_dashboard_view.php @@ -230,6 +230,17 @@ .opp-card { background: #f8fafd; border-radius: 12px; padding: 16px; border: 1px solid #e4eaf5; text-align: center; } .opp-card-val { font-size: 22px; font-weight: 800; color: #0f172a; } .opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; } + .opp-section-head { display: flex; justify-content: space-between; align-items: center; margin: 24px 0 12px; gap: 12px; flex-wrap: wrap; } + .opp-section-head .split-section-title { margin: 0; } + .opp-toggle-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } + .opp-toggle-lbl { font-size: 12px; font-weight: 700; color: #94a3b8; transition: color .2s; } + .opp-toggle-lbl.active { color: #0f172a; } + .opp-toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; } + .opp-toggle-switch input { opacity: 0; width: 0; height: 0; } + .opp-toggle-slider { position: absolute; cursor: pointer; inset: 0; background: #cbd5e1; border-radius: 99px; transition: background .2s; } + .opp-toggle-slider::before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform .2s; box-shadow: 0 1px 3px rgba(15,23,42,0.15); } + .opp-toggle-switch input:checked + .opp-toggle-slider { background: #4f46e5; } + .opp-toggle-switch input:checked + .opp-toggle-slider::before { transform: translateX(20px); } @media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } } @media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } } @@ -502,7 +513,9 @@ const TEAM = ; const OPP_DATA = ; /* OPP_DATA shape per userId: { total_policies, total_exp_amt, target_amt, - policies: [{ policy_no, issue_date, amount, created_at }] } */ + policies: [{ policy_no, issue_date, amount, created_at }], + won_leads: [...], loss_leads: [...] } */ +let currentModalOppUserId = null; /* ── JS Helpers ── */ const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'}; @@ -545,6 +558,89 @@ function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) { + encodeURIComponent(opportunityId); } +function escOppCell(text) { + return String(text ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function updateOppTableHeader(showWon) { + const headRow = document.getElementById('oppTableHead'); + if (!headRow) return; + + if (showWon) { + headRow.innerHTML = + 'Company' + + 'Opportunity Type' + + 'Date'; + } else { + headRow.innerHTML = + 'Company' + + 'Opportunity Type' + + 'Lost Reason' + + 'Date'; + } +} + +function buildOpportunityTableRows(leads, showWon) { + return (leads || []).map(function(l) { + const opportunityType = [ + l.lead_form_type || '', + ].filter(Boolean).join('').toUpperCase(); + const typeBadgeClass = l.lead_form_type === 'EB' + ? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"' + : 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'; + const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id; + const rowAttrs = canRedirect + ? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity(' + + JSON.stringify(String(l.lead_form_type_id)) + ',' + + JSON.stringify(String(l.actual_lead_id)) + ',' + + JSON.stringify(String(l.opportunities_id)) + ')\'' + : ''; + + const lostReasonCell = showWon ? '' : + '' + + escOppCell((l.lost_reason || '').trim() || '—') + + ''; + + return '' + + '' + escOppCell(l.company || '—') + '' + + '' + (opportunityType || '—') + '' + + lostReasonCell + + '' + fmtCreatedAt(l.created_at) + '' + + ''; + }).join(''); +} + +function updateOppToggleLabels(showWon) { + const lostLbl = document.getElementById('oppToggleLblLost'); + const wonLbl = document.getElementById('oppToggleLblWon'); + if (lostLbl) lostLbl.classList.toggle('active', !showWon); + if (wonLbl) wonLbl.classList.toggle('active', showWon); +} + +function renderOpportunitiesTable(userId, showWon) { + const opp = OPP_DATA[userId]; + const tbody = document.getElementById('oppTableBody'); + if (!tbody || !opp) return; + + const leads = showWon ? (opp.won_leads || []) : (opp.loss_leads || []); + const colSpan = showWon ? 3 : 4; + updateOppTableHeader(showWon); + const rows = buildOpportunityTableRows(leads, showWon); + const emptyMsg = showWon ? 'No won opportunities found' : 'No lost opportunities found'; + tbody.innerHTML = rows || '' + emptyMsg + ''; + updateOppToggleLabels(showWon); +} + +function onOppStatusToggle(checked) { + if (currentModalOppUserId) { + renderOpportunitiesTable(currentModalOppUserId, checked); + } +} + function goSalesPage(page, status, memberId) { const url = new URL(page === 'activities' ? SALES_ACTIVITIES_URL : SALES_LEADS_URL, window.location.origin); url.searchParams.set('fy', getSelectedDashboardFY()); @@ -939,48 +1035,33 @@ function openModal(id) { // + '' // + ''; - /* ── Won Leads Table (Table 2) ── */ - const wonLeads = opp.won_leads || []; - const wonRows = wonLeads.map(function(l) { - // const opportunityType = [ - // l.lead_form_type || '', - // l.lead_type || '' - // ].filter(Boolean).join('/').toUpperCase(); - const opportunityType = [ - l.lead_form_type || '', - ].filter(Boolean).join('').toUpperCase(); - const typeBadgeClass = l.lead_form_type === 'EB' - ? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"' - : 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'; - const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id; - const rowAttrs = canRedirect - ? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity(' - + JSON.stringify(String(l.lead_form_type_id)) + ',' - + JSON.stringify(String(l.actual_lead_id)) + ',' - + JSON.stringify(String(l.opportunities_id)) + ')\'' - : ''; - - return '' - + '' + (l.company || '—') + '' - + '' + (opportunityType || '—') + '' - + '' + fmtCreatedAt(l.created_at) + '' - + ''; - }).join(''); + currentModalOppUserId = id; tab2Html += - '
Opportunities
' + '
' + + '
Opportunities
' + + '
' + + 'Lost' + + '' + + 'Won' + + '
' + + '
' + '
' + '' - + '' + + '' + '' + '' + '' + '' - + '' + (wonRows || '') + '' + + '' + '
CompanyOpportunity TypeDate
No leads found
' + '
'; } else { + currentModalOppUserId = null; tab2Html = '
📋
No opportunities data available
'; } @@ -996,6 +1077,14 @@ function openModal(id) { document.getElementById('modalOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; + + if (currentModalOppUserId && OPP_DATA[currentModalOppUserId]) { + const toggle = document.getElementById('oppStatusToggle'); + if (toggle) toggle.checked = true; + renderOpportunitiesTable(currentModalOppUserId, true); + } else { + currentModalOppUserId = null; + } } function switchTab(idx, btn) { @@ -1007,6 +1096,7 @@ function switchTab(idx, btn) { function closeModal() { document.getElementById('modalOverlay').classList.remove('open'); document.body.style.overflow = ''; + currentModalOppUserId = null; } document.getElementById('modalOverlay').addEventListener('click', function(e) { if (e.target === e.currentTarget) closeModal(); }); From 2435112b796b67beb9b3560f4bce227b270903e4 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Thu, 4 Jun 2026 09:16:09 +0530 Subject: [PATCH 3/4] FIX_CD_INFFFUC_HR_MAIL_SEND --- app/Controllers/EmpDataServiceController.php | 14 ++- app/Views/insurer_export_templete.php | 90 ++++++++++++++++---- app/Views/insurer_or_tpa_data.php | 11 ++- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/app/Controllers/EmpDataServiceController.php b/app/Controllers/EmpDataServiceController.php index 45e7097c..07a7ce3b 100755 --- a/app/Controllers/EmpDataServiceController.php +++ b/app/Controllers/EmpDataServiceController.php @@ -1032,8 +1032,9 @@ class EmpDataServiceController extends BaseController return false; } + //for cd tranction and cd master data is empty check - if(!empty($inceptionData) && $inceptionData != null){ + if(!empty($inceptionData) && $inceptionData != null || !empty($additionData) && $additionData != null || !empty($dependentAdditionData) && $dependentAdditionData != null){ $policy_details = $this->clientPolicyModel->where('id', $export_data['client_policy_id'])->first(); @@ -1065,6 +1066,14 @@ class EmpDataServiceController extends BaseController $totals = $totals + $item->total; } + foreach ($additionData as $item) { + $totals = $totals + $item->total; + } + + foreach ($dependentAdditionData as $item) { + $totals = $totals + $item->total; + } + $totals = round($totals, 2); @@ -1127,8 +1136,7 @@ class EmpDataServiceController extends BaseController AND `insurer_excel_export_template`.`type_name` = :type_name: LIMIT 1"; - $binds = ['client_policy_id' => (int)$export_data['client_policy_id'], - 'type_name' => $export_data['actions']]; + $binds = ['client_policy_id' => (int)$export_data['client_policy_id'],'type_name' => $export_data['actions']]; $query = db_connect()->query($sql,$binds); $template_json = $query->getRowArray(); diff --git a/app/Views/insurer_export_templete.php b/app/Views/insurer_export_templete.php index 5edcc832..fa78b8e1 100755 --- a/app/Views/insurer_export_templete.php +++ b/app/Views/insurer_export_templete.php @@ -14,7 +14,8 @@ text-overflow: ellipsis; } - .select2-hidden-accessible + .select2-container .select2-dropdown { + /* Hide dropdown list only for the insurer copy select (search-only UX) */ + #insurer.select2-hidden-accessible + .select2-container .select2-dropdown { display: none !important; } @@ -53,6 +54,14 @@ z-index: 1060; } + #template_modal .select2-container { + z-index: 1061; + } + + #template_modal .select2-dropdown { + z-index: 1062; + } + #template_modal.show { display: flex !important; align-items: center; @@ -276,6 +285,44 @@