diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 78a80888..656b22a9 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -471,6 +471,15 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('debug', 'ClaimsCollectionV2DashboardController::debug'); $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + + $routes->group('enrollment-collection-v1', static function ($routes) { + $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); + $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'EnrollmentCollectionV1DashboardController::kpi/$1'); + $routes->get('all', 'EnrollmentCollectionV1DashboardController::all'); + $routes->get('debug', 'EnrollmentCollectionV1DashboardController::debug'); + $routes->get('debug/(:num)', 'EnrollmentCollectionV1DashboardController::debug/$1'); + }); }); $routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); @@ -787,6 +796,15 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get('debug', 'ClaimsCollectionV2DashboardController::debug'); $routes->get('debug/(:num)', 'ClaimsCollectionV2DashboardController::debug/$1'); }); + + $routes->group('enrollment-collection-v1', static function ($routes) { + $routes->get('preview', 'EnrollmentCollectionV1DashboardController::preview'); + $routes->get('preview/(:num)', 'EnrollmentCollectionV1DashboardController::preview/$1'); + $routes->get('kpi/(:segment)', 'EnrollmentCollectionV1DashboardController::kpi/$1'); + $routes->get('all', 'EnrollmentCollectionV1DashboardController::all'); + $routes->get('debug', 'EnrollmentCollectionV1DashboardController::debug'); + $routes->get('debug/(:num)', 'EnrollmentCollectionV1DashboardController::debug/$1'); + }); }); $routes->post("bulkEcardDownloadAsZip", "EmployeeRestController::bulkEcardDownloadAsZip"); diff --git a/app/Controllers/EnrollmentCollectionV1DashboardController.php b/app/Controllers/EnrollmentCollectionV1DashboardController.php new file mode 100644 index 00000000..dd7fd498 --- /dev/null +++ b/app/Controllers/EnrollmentCollectionV1DashboardController.php @@ -0,0 +1,146 @@ +request->getGet('client_policy') + ?? $this->request->getGet('client_policy_id') + ?? $this->request->getPost('client_policy') + ?? $this->request->getPost('client_policy_id') + ?? 0 + ); + + if ($id > 0) { + return $id; + } + + if ($fallback !== null && $fallback > 0) { + return $fallback; + } + + return 0; + } + + protected function resolveKpiMethod(string $kpiKey): ?string + { + $kpiKey = trim($kpiKey); + if ($kpiKey === '') { + return null; + } + + if (in_array($kpiKey, EnrollmentCollectionV1DashboardModel::KPI_MAP, true)) { + return $kpiKey; + } + + if (ctype_digit($kpiKey)) { + $id = (int) $kpiKey; + + return EnrollmentCollectionV1DashboardModel::KPI_MAP[$id] ?? null; + } + + return null; + } + + public function kpi(string $kpiMethod = '') + { + $policyId = $this->resolvePolicyId(); + + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy or client_policy_id is required.', + ], 422); + } + + $kpiMethod = $this->resolveKpiMethod($kpiMethod); + if ($kpiMethod === null) { + return $this->respond([ + 'status' => false, + 'message' => 'Unknown KPI. Pass Metabase id or method slug.', + 'allowed' => EnrollmentCollectionV1DashboardModel::KPI_MAP, + ], 404); + } + + $model = new EnrollmentCollectionV1DashboardModel(); + $metabaseId = array_search($kpiMethod, EnrollmentCollectionV1DashboardModel::KPI_MAP, true); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'kpi_id' => $metabaseId !== false ? (int) $metabaseId : null, + 'kpi' => $kpiMethod, + 'label' => EnrollmentCollectionV1DashboardModel::KPI_LABELS[$kpiMethod] ?? $kpiMethod, + 'rows' => $model->getKpi($kpiMethod, $policyId), + ]); + } + + public function all() + { + $policyId = $this->resolvePolicyId(); + + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy or client_policy_id is required.', + ], 422); + } + + $model = new EnrollmentCollectionV1DashboardModel(); + + return $this->respond([ + 'status' => true, + 'policy_id' => $policyId, + 'data' => $model->getAllKpis($policyId), + ]); + } + + public function preview(int $policyId = 4687) + { + $policyId = $this->resolvePolicyId($policyId); + $path = $this->request->getUri()->getPath(); + $isJwt = stripos($path, 'employeeRest') !== false; + $prefix = $isJwt ? 'employeeRest/enrollment-collection-v1' : 'util/enrollment-collection-v1'; + + return view('enrollment_collection_v1_dashboard', [ + 'policy_id' => $policyId, + 'kpi_map' => EnrollmentCollectionV1DashboardModel::KPI_MAP, + 'kpi_labels' => EnrollmentCollectionV1DashboardModel::KPI_LABELS, + 'api_all_url' => base_url($prefix . '/all'), + 'api_kpi_url' => base_url($prefix . '/kpi'), + ]); + } + + public function debug(int $policyId = 4687) + { + $policyId = $this->resolvePolicyId($policyId); + + if ($policyId <= 0) { + return $this->response + ->setStatusCode(422) + ->setBody('client_policy or client_policy_id is required.'); + } + + $model = new EnrollmentCollectionV1DashboardModel(); + $body = json_encode([ + 'status' => true, + 'policy_id' => $policyId, + 'data' => $model->getAllKpis($policyId), + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + return $this->response + ->setHeader('Content-Type', 'application/json; charset=UTF-8') + ->setBody($body); + } +} diff --git a/app/Models/EnrollmentCollectionV1DashboardModel.php b/app/Models/EnrollmentCollectionV1DashboardModel.php new file mode 100644 index 00000000..f95f0fa1 --- /dev/null +++ b/app/Models/EnrollmentCollectionV1DashboardModel.php @@ -0,0 +1,290 @@ + PHP method name. + */ + public const KPI_MAP = [ + 115 => 'originally_enrolled', + 116 => 'added_subsequently', + 117 => 'gender_split', + 119 => 'enrollment_relationship', + 122 => 'average_age_by_enrollment_month', + 123 => 'original_base_premium', + 124 => 'overall_active', + 125 => 'enrollment_age_group', + 126 => 'additions_premium', + 127 => 'net_premium', + ]; + + /** + * Human-readable KPI labels (Metabase name). + */ + public const KPI_LABELS = [ + 'originally_enrolled' => 'Originally Enrolled', + 'added_subsequently' => 'Added Subsequently', + 'gender_split' => 'Gender Split', + 'enrollment_relationship' => 'Relationship', + 'average_age_by_enrollment_month' => 'Average age by Enrolment month', + 'original_base_premium' => 'Original Base Premium', + 'overall_active' => 'Overall Active', + 'enrollment_age_group' => 'Age Group', + 'additions_premium' => 'Additions Premium', + 'net_premium' => 'Net Premium', + ]; + + protected function runKpiQuery(string $sql, int $policyId): array + { + $sql = str_replace(['\\t', '\\n', '\\r'], ["\t", "\n", "\r"], $sql); + + $db = \Config\Database::connect($this->DBGroup); + $query = $db->query($sql, ['client_policy_id' => $policyId]); + + return $query->getResultArray(); + } + + public function getKpi(string $method, int $policyId): array + { + if (! method_exists($this, $method)) { + return []; + } + + return $this->{$method}($policyId); + } + + public function getAllKpis(int $policyId): array + { + $out = []; + foreach (self::KPI_MAP as $id => $method) { + $out[$method] = [ + 'id' => (int) $id, + 'label' => self::KPI_LABELS[$method] ?? $method, + 'rows' => $this->getKpi($method, $policyId), + ]; + } + + return $out; + } + + /** Metabase #115: Originally Enrolled */ + public function originally_enrolled(int $policyId): array + { + $sql = <<<'SQL' +SELECT + COUNT(DISTINCT ep.employee_id) AS `Originally Enrolled` +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id AND e.change_event = 'Inception' +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active' + AND e.emp_status = 'Active' + AND e.is_active = 1; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #116: Added Subsequently */ + public function added_subsequently(int $policyId): array + { + $sql = <<<'SQL' +SELECT + COUNT(DISTINCT ep.employee_id) AS `Subsequent Additions` +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id + AND LOWER(e.change_event) IN ('dependent addition', 'addition', 'missed inception') +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active' + AND e.emp_status = 'Active' + AND e.is_active = 1; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #117: Gender Split */ + public function gender_split(int $policyId): array + { + $sql = <<<'SQL' +SELECT + e.gender, + COUNT(DISTINCT ep.employee_id) AS member_count, + ROUND((COUNT(DISTINCT ep.employee_id) * 100.0) / + SUM(COUNT(DISTINCT ep.employee_id)) OVER(), 2) AS percentage +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status != 'Truncated' + AND e.is_active = 1 + AND e.emp_status != 'Truncated' + AND e.gender IS NOT NULL +GROUP BY e.gender +ORDER BY member_count DESC; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #119: Relationship */ + public function enrollment_relationship(int $policyId): array + { + $sql = <<<'SQL' +SELECT + e.relationship, + COUNT(DISTINCT ep.employee_id) AS member_count, + ROUND((COUNT(DISTINCT ep.employee_id) * 100.0) / + SUM(COUNT(DISTINCT ep.employee_id)) OVER(), 2) AS percentage +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status != 'Truncated' + AND e.is_active = 1 + AND e.emp_status != 'Truncated' + AND e.relationship IS NOT NULL +GROUP BY e.relationship +ORDER BY member_count DESC; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #122: Average age by Enrolment month */ + public function average_age_by_enrollment_month(int $policyId): array + { + $sql = <<<'SQL' +SELECT + DATE_FORMAT(ep.date_coverage, '%Y-%m') AS enrollment_month, + DATE_FORMAT(ep.date_coverage, '%b %Y') AS month_label, + ROUND(AVG(TIMESTAMPDIFF(YEAR, e.dob, ep.date_coverage)), 2) AS average_age, + COUNT(DISTINCT ep.employee_id) AS enrollments +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND e.is_active = 1 + AND e.dob IS NOT NULL + AND ep.date_coverage IS NOT NULL +GROUP BY DATE_FORMAT(ep.date_coverage, '%Y-%m') +ORDER BY enrollment_month; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #123: Original Base Premium */ + public function original_base_premium(int $policyId): array + { + $sql = <<<'SQL' +SELECT + ROUND(IFNULL(SUM(ep.rata_premimum + COALESCE(ep.gst, 0)), 0), 2) AS opening_premium +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id AND e.change_event = 'Inception' +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active' + AND e.emp_status = 'Active' + AND e.is_active = 1; +-- AND e.relationship = 'Self'; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #124: Overall Active */ + public function overall_active(int $policyId): array + { + $sql = <<<'SQL' +SELECT + COUNT(DISTINCT ep.employee_id) AS `Overall Active` +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active' + AND e.emp_status = 'Active' + AND e.is_active = 1; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #125: Age Group */ + public function enrollment_age_group(int $policyId): array + { + $sql = <<<'SQL' +SELECT + CASE + WHEN TIMESTAMPDIFF(YEAR, e.dob, CURDATE()) BETWEEN 0 AND 18 THEN '0-18' + WHEN TIMESTAMPDIFF(YEAR, e.dob, CURDATE()) BETWEEN 19 AND 30 THEN '19-30' + WHEN TIMESTAMPDIFF(YEAR, e.dob, CURDATE()) BETWEEN 31 AND 40 THEN '31-40' + WHEN TIMESTAMPDIFF(YEAR, e.dob, CURDATE()) BETWEEN 41 AND 50 THEN '41-50' + WHEN TIMESTAMPDIFF(YEAR, e.dob, CURDATE()) BETWEEN 51 AND 60 THEN '51-60' + ELSE '60+' + END AS age_group, + COUNT(DISTINCT ep.employee_id) AS member_count, + ROUND((COUNT(DISTINCT ep.employee_id) * 100.0) / + SUM(COUNT(DISTINCT ep.employee_id)) OVER(), 2) AS percentage +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status != 'Truncated' + AND e.is_active = 1 + AND e.emp_status != 'Truncated' + AND e.dob IS NOT NULL +GROUP BY age_group +ORDER BY age_group; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #126: Additions Premium */ + public function additions_premium(int $policyId): array + { + $sql = <<<'SQL' +SELECT + ROUND(IFNULL(SUM(ep.rata_premimum + COALESCE(ep.gst, 0)), 0), 2) AS additions_premium +FROM employee_polices ep +INNER JOIN employees e ON ep.employee_id = e.id + AND LOWER(e.change_event) IN ('dependent addition', 'addition', 'missed inception') +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active' + AND e.is_active = 1 + AND e.emp_status = 'Active'; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } + + /** Metabase #127: Net Premium */ + public function net_premium(int $policyId): array + { + $sql = <<<'SQL' +SELECT + ROUND(IFNULL(SUM(ep.rata_premimum + COALESCE(ep.gst, 0)), 0), 2) AS net_premium + -- ROUND(SUM(ep.rata_premimum), 2) as base_premium, + -- ROUND(SUM(COALESCE(ep.gst, 0)), 2) as total_gst +FROM employee_polices ep +WHERE ep.client_policy_id = :client_policy_id: + AND ep.is_active = 1 + AND ep.status = 'Active'; +SQL; + + return $this->runKpiQuery($sql, $policyId); + } +} diff --git a/app/Views/enrollment_collection_v1_dashboard.php b/app/Views/enrollment_collection_v1_dashboard.php new file mode 100644 index 00000000..c1e5e040 --- /dev/null +++ b/app/Views/enrollment_collection_v1_dashboard.php @@ -0,0 +1,102 @@ + + +
+ + +Click “Load all KPIs” or open single KPI API.+