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: = $today ?>
+
Date: = $today ?? "" ?>
Inception & Endorsement (File Upload)
Total Inception
-
= $total_inception_count ?>
+
= $total_inception_count ?? 0 ?>
Employee file uploads marked as inception
Total Endorsement
-
= $total_endorsement_count ?>
+
= $total_endorsement_count ?? 0 ?>
All non‑inception successful uploads
@@ -203,12 +203,12 @@
TPA (Inception / Endorsement)
-
= $total_tpa_incetion_count ?> / = $total_tpa_endorsement_count ?>
+
= $total_tpa_incetion_count ?? 0 ?> / = $total_tpa_endorsement_count ?? 0 ?>
Successful TPA events
Insurer (Inception / Endorsement)
-
= $total_insurer_incetion_count ?> / = $total_insurer_endorsement_count ?>
+
= $total_insurer_incetion_count ?? 0 ?> / = $total_insurer_endorsement_count ?? 0 ?>
Successful insurer events
@@ -218,22 +218,22 @@
Opportunities & Won
-
= $total_opportunity_count ?> / = $total_placement_count ?>
+
= $total_opportunity_count ?? 0 ?> / = $total_placement_count ?? 0 ?>
Total opportunities created vs converted
RFQ (Created / Sent to Insurer)
-
= $total_rfq_created_count ?> / = $total_rfq_insurer_send_count ?>
+
= $total_rfq_created_count ?? 0 ?> / = $total_rfq_insurer_send_count ?? 0 ?>
Movement from opportunity to RFQ
QCR (Created / Sent to Client)
-
= $total_qcr_created_count ?> / = $total_qcr_client_send_count ?>
+
= $total_qcr_created_count ?? 0 ?> / = $total_qcr_client_send_count ?? 0 ?>
Quotes prepared and shared
Total Leads & Activities
-
= $total_lead_count ?> / = $total_activity_count ?>
+
= $total_lead_count ?? 0 ?> / = $total_activity_count ?? 0 ?>
Lead entries and logged touchpoints
@@ -243,12 +243,12 @@
Total BDS Transactions
-
= $total_bds_count ?>
+
= $total_bds_count ?? 0 ?>
All policy transactions processed
BDS (Policy / Endorsement)
-
= $total_bds_policy_wise_count ?> / = $total_bds_endorsement_wise_count ?>
+
= $total_bds_policy_wise_count ?? 0?> / = $total_bds_endorsement_wise_count ?? 0 ?>
Split of inceptions vs endorsements
@@ -289,7 +289,7 @@
Total Claims Registered for the day
-
= $total_claim_count ?>
+
= $total_claim_count ?? 0 ?>
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): ?>
+
+
#= (int) $metabaseId ?> · = esc($kpi_labels[$method] ?? $method) ?>
+
= esc($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);