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/DashboardController.php b/app/Controllers/DashboardController.php index 08a2b82a..3ff685d7 100755 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -1015,34 +1015,27 @@ class DashboardController extends AdminController $db2 = \Config\Database::connect('preDB'); - $builder = $db2->table('employees'); - $total_employee_draft_count = $builder->join('employee_polices', 'employees.id = employee_polices.employee_id') - ->select('employee_polices.status, count(*) as count') + $total_employee_draft_count = $db2->table('employee_polices') + ->join('employees', 'employees.id = employee_polices.employee_id') ->where('DATE(employee_polices.created_at)', $today) ->where('employee_polices.is_active', 1) ->where('employees.is_active', 1) - ->whereIn('employee_polices.status', ['draft']) - ->groupBy('employee_polices.status') + ->where('employee_polices.status', 'draft') ->countAllResults(); - - $builder1 = $db2->table('employees'); - $total_employee_enrolled_count = $builder1->join('employee_polices', 'employees.id = employee_polices.employee_id') - ->select('employee_polices.status, count(*) as count') + $total_employee_enrolled_count = $db2->table('employee_polices') + ->join('employees', 'employees.id = employee_polices.employee_id') ->where('DATE(employee_polices.created_at)', $today) ->where('employee_polices.is_active', 1) ->where('employees.is_active', 1) - ->whereIn('employee_polices.status', ['enrolled']) - ->groupBy('employee_polices.status') + ->where('employee_polices.status', 'enrolled') ->countAllResults(); - - $builder2 = $db2->table('files'); - $total_open_for_enrollemnt_policy_count = $builder2 - ->select('COUNT(*) as count') - ->where('enrollment_open_date <=', $today) - ->where('enrollment_close_date >=', $today) + + $total_open_for_enrollemnt_policy_count = $db2->table('client_policy') + ->where('open_date <=', $today) + ->where('close_date >=', $today) ->where('is_active', 1) - ->where('status', 'success') + ->where('policy_status', 1) ->countAllResults(); $data = [ diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 3066ada1..fcf7c9f6 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2266,6 +2266,7 @@ class EmployeeRestController extends AdminController client_policy.insurer_id as insurer_id, DATE_FORMAT(client_policy.policy_start_date, '%d-%m-%Y') AS policy_start_date, DATE_FORMAT(client_policy.policy_end_date, '%d-%m-%Y') AS policy_expiry_date, + clients.client_logo as client_logo, ( select COALESCE(ROUND(SUM(rata_premimum + gst)), 0) from employee_polices @@ -2275,6 +2276,7 @@ class EmployeeRestController extends AdminController ) as total_premium, CASE WHEN policy_type.allocg IN ('Non-EB', 'Marine') THEN 'Non-EB' ELSE 'EB' END as allocg ", false) + ->join('clients', 'clients.id = client_policy.client_id', 'left') ->join('policy_type', 'policy_type.id = client_policy.policy_type_id', 'left') ->where('md5(client_policy.client_id)', $this->request->getGet('client_id')) ->where('client_policy.client_branch_id', $this->request->getGet('client_branch_id')) @@ -2293,6 +2295,7 @@ class EmployeeRestController extends AdminController $value['policy_name'] = $policyTypeData->long_name; $value['insurer_name'] = $insurerData->name; $value['insurer_short_name'] = $insurerData->short_name; + $value['client_logo'] = ! empty($value['client_logo']) ? base_url() . 'public/uploads/logo/' . $value['client_logo'] : ''; $employeeDetails = $this->employeePolicyModel->getEmployeePolicy(client_id: $value['client_id'], policy_id: $value['client_policy_id'], status: 0, branch_id: $this->request->getGet('client_branch_id'), status_type: 'inactive'); // dd($employeeDetails); $activeCount = 0; 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/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 26da53b2..a8d1ab90 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -39,6 +39,7 @@ use App\Helpers\ExcelSanitizeHelper; use App\Models\NhanceBranchModel; use App\Models\BDSDumpModel; use App\Models\VehicleModel; +use CodeIgniter\CLI\CLI; use Exception; class PolicyTransactionController extends BaseController @@ -5324,21 +5325,18 @@ class PolicyTransactionController extends BaseController $bdsInstallmentData = []; try { - - $bdsInstallmentData = $this->BdsPlacementModel->getClientInstallmentDetails(); + $bdsInstallmentData = $this->BdsPlacementModel->getLeadInstallmentDetails(); } catch (Exception $e) { - $this->myLogger->logme('error', 'Exception: ' . $e->getMessage() . 'Page: ' . $e->getFile() . ' Line: ' . $e->getLine()); } $this->myLogger->logme("error", "Data for BDS Installment " . json_encode($bdsInstallmentData)); - // dd($bdsInstallmentData); + // print_r($bdsInstallmentData);die(); if (empty($bdsInstallmentData)) { - $this->myLogger->logme("error", "No Client Installment is Due in the 15th Day"); - - return false; + CLI::write("No Client Installment is Due in the 15th Day"); + return ['status' => false, 'message' => 'No data found', 'response' => []]; } foreach ($bdsInstallmentData as $installmentData) { @@ -5358,13 +5356,13 @@ class PolicyTransactionController extends BaseController $this->myLogger->logme('error', 'res: ' . json_encode($res)); if ($res->status == 'success') { - return true; + return ['status' => true, 'message' => 'Mail sent successfully', 'response' => $res]; } else { - return false; + return ['status' => false, 'message' => 'Mail sent failed', 'response' => $res]; } } - } catch (Exception $e) { + } catch (Exception $e) { $this->myLogger->logme('error', 'Exception: ' . $e->getMessage() . 'Page: ' . $e->getFile() . ' Line: ' . $e->getLine()); } } diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index edf64242..cd55d3a3 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -10,6 +10,7 @@ use App\Models\SalesLeadNoteModel; use App\Models\SalesTargetModel; use App\Models\UserModel; use App\Models\ClientModel; +use App\Helpers\MailHelper; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\API\ResponseTrait; @@ -330,7 +331,10 @@ class SalesController extends BaseController return $this->fail($this->leadModel->errors()); } - return $this->respondCreated(['status' => 'success', 'id' => $this->leadModel->getInsertID()]); + $leadId = $this->leadModel->getInsertID(); + $this->sendLeadCreateMail($leadId, $data); + + return $this->respondCreated(['status' => 'success', 'id' => $leadId]); } catch (\Exception $e) { return $this->failServerError($e->getMessage()); } @@ -2595,4 +2599,132 @@ public function salesManagerLevelDashboard($userId, $current_fin_year = null, $f ]; } } + + /** + * Send lead-create notification email when the logged-in user's branch is Chennai. + */ + private function sendLeadCreateMail(int $leadId, array $leadData): void + { + try { + if (!$this->isLoggedInUserChennaiBranch()) { + return; + } + + $toMail = trim((string) getenv('LEAD_CREATE_MAIL')); + if ($toMail === '') { + log_message('error', 'sendLeadCreateMail: LEAD_CREATE_MAIL is not configured.'); + return; + } + + $recipientEmails = array_filter(array_map('trim', explode(',', $toMail))); + if (empty($recipientEmails)) { + log_message('error', 'sendLeadCreateMail: No valid recipients in LEAD_CREATE_MAIL.'); + return; + } + + $creator = $this->getLeadCreateMailCreatorDetails(); + $assignedUser = $this->userModel->where('id', $leadData['assigned_to'] ?? 0)->first() ?? []; + + $companyName = $leadData['company_name'] ?? ''; + $subject = 'New Lead Created - ' . $companyName; + $message = $this->buildLeadCreateMailMessage( + $companyName, + $leadData['email'] ?? '', + $leadData['phone'] ?? '', + $leadData['status'] ?? '', + $creator['name'] ?? '', + $creator['email'] ?? '', + $creator['branch'] ?? '', + $assignedUser['first_name'] ?? '', + $assignedUser['email'] ?? '', + date('d/m/Y h:i A') + ); + + $res = MailHelper::send_email([ + 'mail' => $recipientEmails, + 'subject' => $subject, + 'message' => $message, + 'common' => ['module' => 'sales', 'pk' => $leadId, 'mail_type' => 'lead_create'], + ]); + + $resDecoded = is_string($res) ? json_decode($res, true) : $res; + if (!isset($resDecoded['status']) || $resDecoded['status'] !== 'success') { + log_message('error', 'sendLeadCreateMail: Email send failed - ' . json_encode($resDecoded)); + } + } catch (\Throwable $e) { + log_message('error', 'sendLeadCreateMail: ' . $e->getMessage()); + } + } + + private function isLoggedInUserChennaiBranch(): bool + { + $userId = get_session_userid(); + if (empty($userId)) { + return false; + } + + $row = $this->userModel + ->select('id') + ->where('id', $userId) + ->where('nhance_branch_id', 1) + ->first(); + + return !empty($row); + } + + private function getLeadCreateMailCreatorDetails(): array + { + $userId = get_session_userid(); + $db = \Config\Database::connect(); + + $row = $db->table('user_profiles up') + ->select('up.first_name, up.email, nb.branch_name') + ->join('nhance_branch nb', 'nb.id = up.nhance_branch_id', 'left') + ->where('up.id', $userId) + ->get() + ->getRowArray(); + + if (!$row) { + return ['name' => '', 'email' => '', 'branch' => '']; + } + + return [ + 'name' => $row['first_name'] ?? '', + 'email' => $row['email'] ?? '', + 'branch' => $row['branch_name'] ?? '', + ]; + } + + private function buildLeadCreateMailMessage( + string $companyName, + string $leadEmail, + string $leadMobile, + string $leadStatus, + string $createdByName, + string $createdByEmail, + string $createdByBranch, + string $assignedUserName, + string $assignedUserEmail, + string $createdDateTime + ): string { + $e = static fn (?string $value): string => htmlspecialchars((string) ($value ?? ''), ENT_QUOTES, 'UTF-8'); + + return '

Hello Team,

' + . '

A new lead has been created

' + . '

Lead Details

' + . '

Company Name : ' . $e($companyName) . '

' + . '

Email : ' . $e($leadEmail) . '

' + . '

Mobile : ' . $e($leadMobile) . '

' + . '

Status : ' . $e($leadStatus) . '

' + . '

Created By

' + . '

Name : ' . $e($createdByName) . '

' + . '

Email : ' . $e($createdByEmail) . '

' + . '

Branch : ' . $e($createdByBranch) . '

' + . '

Assigned To

' + . '

Name : ' . $e($assignedUserName) . '

' + . '

Email : ' . $e($assignedUserEmail) . '

' + . '

Created On

' + . '

Date & Time : ' . $e($createdDateTime) . '

' + . '

Regards,
NHANCE INDIA INSURANCE BROKING PRIVATE LIMITED

'; + } } \ No newline at end of file diff --git a/app/Models/BdsPlacementModel.php b/app/Models/BdsPlacementModel.php index 947f8a0f..e27ecd9c 100644 --- a/app/Models/BdsPlacementModel.php +++ b/app/Models/BdsPlacementModel.php @@ -114,6 +114,59 @@ class BdsPlacementModel extends Model return $data; } + + public function getLeadInstallmentDetails() + { + $heads = $this->db->table('user_profiles') + ->select('email')->where(['role' => 5, 'is_active' => 1]) + ->get()->getResultArray(); + + $admins = $this->db->table('user_profiles') + ->select('email')->where(['role' => 1, 'is_active' => 1]) + ->get()->getResultArray(); + + $businessTeam = $this->db->table("user_teams ut") + ->select("up.email") + ->join('user_profiles up', 'up.id = ut.user_id AND up.is_active = 1') + ->where(["ut.team_id" => 7, "ut.is_active" => 1]) + ->get()->getResultArray(); + + $builder = $this->db->table("lead_installment_payment_details lipd") + ->select("lipd.*, COALESCE(ct.client_name, leads.client_name) as client_name, COALESCE(ct.short_name, leads.client_short_name) as short_name, COALESCE(cb.branch_name, leads.branch_name) as branch_name, leads.salse_person_id, cp.policy_no") + ->join("leads", "leads.id = lipd.lead_id") + ->join("clients ct", "ct.id = leads.client_id", "left") + ->join("client_branch cb", "cb.id = leads.client_branch_id", "left") + ->join("client_policy cp", "cp.id = leads.source_policy_id", "left") + ->where("lipd.is_active", 1) + ->where("lipd.utr_no IS NULL") + ->where("lipd.payment_date", date('Y-m-d', strtotime('+15 days'))); + + $data = $builder->get()->getResultArray(); + + foreach ($data as &$row) { + $sales_person_ids = json_decode($row['salse_person_id'], true); + $sales_person_id = $sales_person_ids[0] ?? null; + + if ($sales_person_id) { + $user = $this->db->table('user_profiles') + ->select('email') + ->where('id', (int) $sales_person_id) + ->get() + ->getRowArray(); + + $row['sales_person'] = $user['email'] ?? 'N/A'; + } else { + $row['sales_person'] = 'Not Assigned'; + } + + $row['heads'] = $heads; + $row['admins'] = $admins; + $row['buisness_team'] = $businessTeam; + } + + return $data; + } + public function getClientInstallmentDetails_olds() { 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/daily_report_email_template.php b/app/Views/daily_report_email_template.php index 0b43ca82..47a19a4c 100644 --- a/app/Views/daily_report_email_template.php +++ b/app/Views/daily_report_email_template.php @@ -166,19 +166,19 @@
-
Date:
+
Date:

Inception & Endorsement (File Upload)

Total Inception
-
+
Employee file uploads marked as inception
Total Endorsement
-
+
All non‑inception successful uploads
@@ -203,12 +203,12 @@
TPA (Inception / Endorsement)
-
/
+
/
Successful TPA events
Insurer (Inception / Endorsement)
-
/
+
/
Successful insurer events
@@ -218,22 +218,22 @@
Opportunities & Won
-
/
+
/
Total opportunities created vs converted
RFQ (Created / Sent to Insurer)
-
/
+
/
Movement from opportunity to RFQ
QCR (Created / Sent to Client)
-
/
+
/
Quotes prepared and shared
Total Leads & Activities
-
/
+
/
Lead entries and logged touchpoints
@@ -243,12 +243,12 @@
Total BDS Transactions
-
+
All policy transactions processed
BDS (Policy / Endorsement)
-
/
+
/
Split of inceptions vs endorsements
@@ -289,7 +289,7 @@
Total Claims Registered for the day
-
+
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 @@ + + + + + + Enrollment Collection V1 Dashboard + + + + +

Enrollment Collection V1

+ +
+ + + +
+ +
+ $method): ?> +
+
# ·
+

+
Click “Load all KPIs” or open single KPI API.
+
+ +
+ + + + diff --git a/app/Views/view_deposit.php b/app/Views/view_deposit.php index 5bd008a9..d4b9087d 100755 --- a/app/Views/view_deposit.php +++ b/app/Views/view_deposit.php @@ -379,7 +379,7 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len ? (string) $row->amt_updated_by_name : (!empty($row->amt_updated_by) ? ('User #' . $row->amt_updated_by) : '-'); $historyUpdatedAt = !empty($row->amt_updated_at) - ? date('d M Y, h:i A', strtotime((string) $row->amt_updated_at)) + ? date('d/m/Y h:i A', strtotime((string) $row->amt_updated_at)) : '-'; ?> - created_at)); ?> + created_at)); ?> record_date)? date('d-M-Y', strtotime($row->record_date)):'-' ?> unit ?? ' - '; ?> @@ -481,14 +481,14 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len
- Changed by + Modified by
-
- Changed at + Modified at
-
@@ -668,6 +668,38 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len }); nhanceListDataTableBeforeInit(); + function depositExportFormatBody(data, row, column, node) { + if (column === 6 || column === 7) { + var $input = $(node).find('.deposit-inline-input'); + if ($input.length) { + var val = ($input.val() || '').trim(); + return val !== '' ? val : '-'; + } + var $tr = $(node).closest('tr'); + var credit = parseFloat($tr.attr('data-credit')) || 0; + var debit = parseFloat($tr.attr('data-debit')) || 0; + if (column === 6) { + return credit > 0 ? String(credit) : '-'; + } + return debit > 0 ? String(debit) : '-'; + } + if (column === 8) { + var $balanceSpan = $(node).find('.deposit-balance-wrap > span').first(); + if ($balanceSpan.length) { + return ($balanceSpan.text() || '').trim(); + } + return ($(node).text() || '').trim(); + } + if (typeof data === 'string' && data.indexOf('<') !== -1) { + return $('
').html(data).text().trim(); + } + return data; + } + var depositExportOptions = { + format: { + body: depositExportFormatBody + } + }; var depositPageStorageKey = 'view_deposit_datatable_page_' + window.location.pathname + window.location.search; function getStoredDepositPage() { var page = parseInt(sessionStorage.getItem(depositPageStorageKey), 10); @@ -780,7 +812,7 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len extend: 'csv', text: ' CSV ', title: 'CD TransactionDetails', - exportOptions: { columns: '' }, + exportOptions: $.extend(true, { columns: '' }, depositExportOptions), className: 'app-btn-primary ', }, { @@ -788,9 +820,7 @@ $vd_col_width_px = nhance_dt_column_widths_px($vd_header_labels, $vd_col_max_len text: ' EXCEL ', title: 'CD TransactionDetails', sheetName: 'CD TransactionDetails', - exportOptions: { - orthogonal: 'sort' - } + exportOptions: $.extend(true, { orthogonal: 'sort' }, depositExportOptions) } ] } diff --git a/enrollment-dashboard.md b/enrollment-dashboard.md new file mode 100644 index 00000000..d3c3c4c9 --- /dev/null +++ b/enrollment-dashboard.md @@ -0,0 +1,154 @@ +# Enrollment Collection V1 — HR Dashboard API + +**Created:** 2026-06-03 +**Controller:** `App\Controllers\EnrollmentCollectionV1DashboardController` +**Model:** `App\Models\EnrollmentCollectionV1DashboardModel` +**Source queries:** `metabase_raw_queries.csv` → collection `Enrollment Collection V1` +**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php` +**Total KPIs:** 10 + +--- + +## Overview + +All Enrollment Collection V1 KPIs from **Enrollment Collection V1** are implemented as PHP model methods. +SQL uses Metabase param `{{client_policy_id}}` → CodeIgniter bind `:client_policy_id:`. + +| What | URL fragment | Use case | +|------|-------------|----------| +| **Single KPI** | `kpi/{slug or Metabase id}` | One card at a time | +| **All KPIs** | `all` | Full enrollment dashboard (10 KPIs) | +| **Debug / preview** | `debug` / `preview` | Admin / FE testing | + +Query param: `client_policy` or `client_policy_id` (required for `kpi` / `all`). + +--- + +## Route stacks + +### 1. Admin — `authMVC` + +Prefix: `util/enrollment-collection-v1` + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `util/enrollment-collection-v1/preview` | UI debug grid | +| GET | `util/enrollment-collection-v1/preview/{policy_id}` | Same, policy in URL | +| GET | `util/enrollment-collection-v1/debug` | Raw JSON (all KPIs) | +| GET | `util/enrollment-collection-v1/debug/{policy_id}` | Same, policy in URL | +| GET | `util/enrollment-collection-v1/all` | JSON — all 6 KPIs | +| GET | `util/enrollment-collection-v1/kpi/{slug\|id}` | JSON — single KPI | + +### 2. Frontend / HR App — `authJWT` + +Prefix: `employeeRest/enrollment-collection-v1` + +Same paths as above under `employeeRest/` (Bearer + `X-App-Signature`). + +--- + +## How to call + +> **Important:** Admin routes live under the **`util/`** prefix (same as `util/claims-collection-v2`). +> `enrollment-collection-v1/preview` without `util/` will return **404**. + +``` +GET /index.php/util/enrollment-collection-v1/preview?client_policy=4687 +GET /index.php/util/enrollment-collection-v1/all?client_policy=4687 +GET /index.php/util/enrollment-collection-v1/kpi/originally_enrolled?client_policy=4687 +GET /index.php/util/enrollment-collection-v1/kpi/117?client_policy=4687 +GET /index.php/util/enrollment-collection-v1/debug?client_policy=4687 +``` + +Full local example: + +``` +https://localhost/PHP828APPS/ruc/nhance/index.php/util/enrollment-collection-v1/preview?client_policy=4687 +``` + +--- + +## All 10 KPIs + +| Metabase ID | Method slug | Label | Rows | +|-------------|-------------|-------|------| +| 115 | `originally_enrolled` | Originally Enrolled | single | +| 116 | `added_subsequently` | Added Subsequently | single | +| 117 | `gender_split` | Gender Split | multi | +| 119 | `enrollment_relationship` | Relationship | multi | +| 122 | `average_age_by_enrollment_month` | Average age by Enrolment month | multi | +| 123 | `original_base_premium` | Original Base Premium | single | +| 124 | `overall_active` | Overall Active | single | +| 125 | `enrollment_age_group` | Age Group | multi | +| 126 | `additions_premium` | Additions Premium | single | +| 127 | `net_premium` | Net Premium | single | + +> Slug `enrollment_relationship` avoids clashing with Claims V2 `claim_amount_by_relationship` (ticket-level). + +--- + +## Output columns + +| Slug | Key row fields | +|------|----------------| +| `originally_enrolled` | `Originally Enrolled` | +| `added_subsequently` | `Subsequent Additions` | +| `gender_split` | `gender`, `member_count`, `percentage` | +| `enrollment_relationship` | `relationship`, `member_count`, `percentage` | +| `average_age_by_enrollment_month` | `enrollment_month`, `month_label`, `average_age`, `enrollments` | +| `original_base_premium` | `opening_premium` | +| `overall_active` | `Overall Active` | +| `enrollment_age_group` | `age_group`, `member_count`, `percentage` | +| `additions_premium` | `additions_premium` | +| `net_premium` | `net_premium` | + +--- + +## Sample `all` response (excerpt) + +```json +{ + "status": true, + "policy_id": 4687, + "data": { + "originally_enrolled": { + "id": 115, + "label": "Originally Enrolled", + "rows": [{ "Originally Enrolled": "320" }] + }, + "gender_split": { + "id": 117, + "label": "Gender Split", + "rows": [ + { "gender": "Male", "member_count": "180", "percentage": "56.25" } + ] + } + } +} +``` + +--- + +## Files + +| File | Purpose | +|------|---------| +| `app/Models/EnrollmentCollectionV1DashboardModel.php` | KPI SQL, `KPI_MAP`, `KPI_LABELS` | +| `app/Controllers/EnrollmentCollectionV1DashboardController.php` | `kpi()`, `all()`, `preview()`, `debug()` | +| `app/Views/enrollment_collection_v1_dashboard.php` | Preview UI | +| `app/Config/Routes.php` | `enrollment-collection-v1` groups (util + employeeRest) | +| `tests/smoke_enrollment_collection_v1.php` | `php tests/smoke_enrollment_collection_v1.php 4687` | +| `enrollment-dashboard.md` | This document | + +--- + +## Related + +- Claims dashboard: `hr-dashboard.md` → `claims-collection-v2` (36 KPIs) + +--- + +## BE notes + +- Run `php tests/smoke_enrollment_collection_v1.php {policy_id}` after model changes. +- Status filters in CSV use `'Active'` (capital A) for some KPIs; kept as in Metabase export. diff --git a/hr-dashboard.md b/hr-dashboard.md index f56a6a9f..6563d0a7 100644 --- a/hr-dashboard.md +++ b/hr-dashboard.md @@ -407,3 +407,9 @@ X-App-Signature: - 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 === 36). + +--- + +## Related dashboards + +- **Enrollment Collection V1** (10 KPIs): see `enrollment-dashboard.md` — prefix `enrollment-collection-v1` diff --git a/tests/smoke_enrollment_collection_v1.php b/tests/smoke_enrollment_collection_v1.php new file mode 100644 index 00000000..dbe2c4dc --- /dev/null +++ b/tests/smoke_enrollment_collection_v1.php @@ -0,0 +1,105 @@ +systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development')); + +$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; +if (is_file($boot)) { + require_once $boot; +} + +helper('url'); + +$policyId = isset($argv[1]) ? (int) $argv[1] : 4687; +$pass = 0; +$fail = 0; +$results = []; + +function ok_enrollment(string $label, bool $cond, string $detail = ''): void +{ + global $pass, $fail, $results; + if ($cond) { + $pass++; + $results[] = '[PASS] ' . $label . ($detail ? " — {$detail}" : ''); + } else { + $fail++; + $results[] = '[FAIL] ' . $label . ($detail ? " — {$detail}" : ''); + } +} + +$model = new EnrollmentCollectionV1DashboardModel(); +$kpiMap = EnrollmentCollectionV1DashboardModel::KPI_MAP; +$expectedCount = 10; + +ok_enrollment('KPI_MAP count', count($kpiMap) === $expectedCount, (string) count($kpiMap)); + +$uniqueMethods = array_unique(array_values($kpiMap)); +ok_enrollment('unique KPI method names', count($uniqueMethods) === $expectedCount, count($uniqueMethods) . ' methods'); + +ok_enrollment('id 115 maps to originally_enrolled', ($kpiMap[115] ?? '') === 'originally_enrolled'); +ok_enrollment('id 119 maps to enrollment_relationship', ($kpiMap[119] ?? '') === 'enrollment_relationship'); +ok_enrollment('id 124 maps to overall_active', ($kpiMap[124] ?? '') === 'overall_active'); +ok_enrollment('id 125 maps to enrollment_age_group', ($kpiMap[125] ?? '') === 'enrollment_age_group'); +ok_enrollment('id 126 maps to additions_premium', ($kpiMap[126] ?? '') === 'additions_premium'); +ok_enrollment('id 127 maps to net_premium', ($kpiMap[127] ?? '') === 'net_premium'); + +try { + $rows = $model->originally_enrolled($policyId); + ok_enrollment('model originally_enrolled', is_array($rows), 'rows=' . count($rows)); +} catch (Throwable $e) { + ok_enrollment('model originally_enrolled', false, $e->getMessage()); +} + +$request = Services::request(null, false); +$response = Services::response(); +$request->setGlobal('get', ['client_policy' => (string) $policyId]); + +$controller = new EnrollmentCollectionV1DashboardController(); +$controller->initController($request, $response, service('logger')); + +$slugResp = json_decode($controller->kpi('gender_split')->getJSON(), true); +ok_enrollment('controller kpi by slug', ($slugResp['status'] ?? false) === true && ($slugResp['kpi'] ?? '') === 'gender_split'); + +$idResp = json_decode($controller->kpi('115')->getJSON(), true); +ok_enrollment('controller kpi by id 115', ($idResp['status'] ?? false) === true && ($idResp['kpi_id'] ?? 0) === 115); + +$allResp = json_decode($controller->all()->getJSON(), true); +ok_enrollment('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === $expectedCount); + +$previewOut = $controller->preview($policyId); +$previewHtml = is_string($previewOut) ? $previewOut : $previewOut->getBody(); +ok_enrollment('controller preview HTML', str_contains($previewHtml, 'Enrollment Collection V1') && str_contains($previewHtml, 'kpi-grid')); + +$sparkRoutes = (string) shell_exec('php ' . escapeshellarg(ROOTPATH . 'spark') . ' routes 2>&1'); +ok_enrollment('spark lists util preview route', str_contains($sparkRoutes, 'util/enrollment-collection-v1/preview')); +ok_enrollment('spark lists employeeRest preview route', str_contains($sparkRoutes, 'employeeRest/enrollment-collection-v1/preview')); + +$previewUrl = rtrim((string) base_url(), '/') . '/util/enrollment-collection-v1/preview?client_policy=' . $policyId; + +ob_end_clean(); +echo '=== Enrollment Collection V1 smoke test (policy_id=' . $policyId . ') ===' . PHP_EOL . PHP_EOL; +echo implode(PHP_EOL, $results) . PHP_EOL; +echo PHP_EOL . '=== Admin preview URL (note util/ prefix) ===' . PHP_EOL; +echo $previewUrl . PHP_EOL; +echo PHP_EOL . "=== Summary: {$pass} passed, {$fail} failed ===" . PHP_EOL; +exit($fail > 0 ? 1 : 0);