From 3cdd73a7eed22557f5f6fb8bd84a8c6383f9bd0d Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Feb 2026 14:40:34 +0530 Subject: [PATCH 01/12] FIX_salesTracker8 --- app/Config/Routes.php | 9 +- app/Controllers/SalesController.php | 136 +++++++ app/Models/SalesTargetModel.php | 38 ++ app/Views/sales/target_view.php | 581 ++++++++++++++++++++++++++++ 4 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 app/Models/SalesTargetModel.php create mode 100644 app/Views/sales/target_view.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6e7b375a..37e7f840 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -914,7 +914,7 @@ $routes->group('sales', function($routes) { $routes->get('/', 'SalesController::index'); $routes->get('loadactivities', 'SalesController::loadactivities'); - + $routes->get('loadtargets', 'SalesController::loadtargets'); $routes->get('page/(:segment)', 'SalesController::noPage/$1'); @@ -997,6 +997,13 @@ $routes->group('sales', function($routes) { // Delete note $routes->delete('notes/(:num)', 'SalesController::deleteNote/$1'); + // ==================== TARGETS ROUTES ==================== + $routes->get('targets', 'SalesController::getTargets'); + $routes->get('targets/user/(:num)', 'SalesController::getTargetByUser/$1'); + $routes->get('targets/fy/(:segment)','SalesController::getTargetByFY/$1'); + $routes->post('targets', 'SalesController::createTarget'); + $routes->put('targets/(:num)', 'SalesController::updateTarget/$1'); + $routes->delete('targets/(:num)', 'SalesController::deleteTarget/$1'); //Dashboard $routes->get('dashboard', 'SalesController::dashboard'); diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index dccd9d91..44ea7aae 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -7,6 +7,7 @@ use App\Models\SalesActualLeadModel; use App\Models\SalesContactPersonModel; use App\Models\SalesActivityModel; use App\Models\SalesLeadNoteModel; +use App\Models\SalesTargetModel; use App\Models\UserModel; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\API\ResponseTrait; @@ -20,6 +21,8 @@ class SalesController extends BaseController protected $activityModel; protected $noteModel; protected $userModel; + protected $targetModel; + public function __construct() { @@ -28,6 +31,7 @@ class SalesController extends BaseController $this->activityModel = new SalesActivityModel(); $this->noteModel = new SalesLeadNoteModel(); $this->userModel = new UserModel(); + $this->targetModel = new SalesTargetModel(); } @@ -760,6 +764,138 @@ class SalesController extends BaseController } } + // ==================== SALES TARGET APIs ==================== + /** + * Get all sales targets + * GET /api/sales/targets + */ + public function getTargets() + { + try { + $targets = $this->targetModel->findAll(); + return $this->respond([ + 'status' => 'success', + 'data' => $targets + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Get sales target by user + * GET /api/sales/targets/user/{userId} + */ + public function getTargetByUser($userId) + { + try { + $targets = $this->targetModel->where('user_id', $userId)->findAll(); + return $this->respond([ + 'status' => 'success', + 'data' => $targets + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Get sales target by FY year + * GET /api/sales/targets/fy/{fyYear} + */ + public function getTargetByFY($fyYear) + { + try { + $targets = $this->targetModel->where('fy_year', $fyYear)->findAll(); + return $this->respond([ + 'status' => 'success', + 'data' => $targets + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Create sales target + * POST /api/sales/targets + */ + public function createTarget() + { + try { + $data = $this->request->getJSON(true); + $data['created_by'] = $this->getUserId(); + $data['updated_by'] = $this->getUserId(); + + if (!$this->targetModel->insert($data)) { + return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST); + } + + $targetId = $this->targetModel->getInsertID(); + $target = $this->targetModel->find((int)$targetId); + + return $this->respondCreated([ + 'status' => 'success', + 'message' => 'Sales target created successfully', + 'data' => $target + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Update sales target + * PUT /api/sales/targets/{id} + */ + public function updateTarget($id) + { + try { + $target = $this->targetModel->find((int)$id); + if (!$target) { + return $this->failNotFound('Sales target not found'); + } + + $data = $this->request->getJSON(true); + $data['updated_by'] = $this->getUserId(); + + if (!$this->targetModel->update($id, $data)) { + return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST); + } + + $updatedTarget = $this->targetModel->find((int)$id); + return $this->respond([ + 'status' => 'success', + 'message' => 'Sales target updated successfully', + 'data' => $updatedTarget + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Delete sales target + * DELETE /api/sales/targets/{id} + */ + public function deleteTarget($id) + { + try { + $target = $this->targetModel->find((int)$id); + if (!$target) { + return $this->failNotFound('Sales target not found'); + } + + $this->targetModel->delete((int)$id); + return $this->respondDeleted([ + 'status' => 'success', + 'message' => 'Sales target deleted successfully' + ]); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + // ==================== HELPER METHODS ==================== /** diff --git a/app/Models/SalesTargetModel.php b/app/Models/SalesTargetModel.php new file mode 100644 index 00000000..f23193f6 --- /dev/null +++ b/app/Models/SalesTargetModel.php @@ -0,0 +1,38 @@ + 'required|integer', + 'fy_year' => 'required|max_length[9]', + 'target_amount' => 'required|decimal', + ]; + + protected $validationMessages = [ + 'user_id' => ['required' => 'User is required'], + 'fy_year' => ['required' => 'Financial year is required'], + 'target_amount' => ['required' => 'Target amount is required', 'decimal' => 'Target amount must be a valid number'], + ]; +} \ No newline at end of file diff --git a/app/Views/sales/target_view.php b/app/Views/sales/target_view.php new file mode 100644 index 00000000..a500a2ba --- /dev/null +++ b/app/Views/sales/target_view.php @@ -0,0 +1,581 @@ + +
+
+
+

+ +
+ +
+
+
+ + + + + + + + +
PersonAction
+
+
+
+ +
+ + + + + \ No newline at end of file From 9b154ee3a1df09e99a584feac01a9eded8708d59 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Feb 2026 15:38:58 +0530 Subject: [PATCH 02/12] FIX_salesTracker8 --- app/Controllers/SalesController.php | 4 +-- app/Views/DashBoard.php | 2 +- app/Views/layout/header.php | 2 +- app/Views/leads_dash.php | 2 +- .../sales/branch_level_dashboard_view.php | 6 ++--- app/Views/sales/target_view.php | 27 ++++++++++++++++--- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 44ea7aae..0a4f2471 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -60,8 +60,8 @@ class SalesController extends BaseController public function loadtargets(){ $data = $this->getSalesStaffData(); - $data['tab_name'] = 'Team Target'; - $data['page_name'] = 'Team Target'; + $data['tab_name'] = 'Sales Team Targets'; + $data['page_name'] = 'Sales Team Targets'; return $this->loadLayout('sales/target_view', $data); } diff --git a/app/Views/DashBoard.php b/app/Views/DashBoard.php index e71d67ff..0eff67a2 100755 --- a/app/Views/DashBoard.php +++ b/app/Views/DashBoard.php @@ -245,7 +245,7 @@ /assets/images/active_leads_and_bds_renewal.png" alt="Logo" height="14" class="active_leads " style="display:none;">    - Leads and BDS Renewals + Opportunities and BDS Renewals      diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php index 224a7bd8..365caf67 100755 --- a/app/Views/layout/header.php +++ b/app/Views/layout/header.php @@ -2106,7 +2106,7 @@ -->
  • - Team Targets + Sales Team Targets
  • diff --git a/app/Views/leads_dash.php b/app/Views/leads_dash.php index 0833c740..7fb07fb6 100644 --- a/app/Views/leads_dash.php +++ b/app/Views/leads_dash.php @@ -347,7 +347,7 @@ $isActive = get_role_id() == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID, user_ if (type == 1 && leadType != null && leadType == 1) { $('.leadStatusTitle_1').show(); - $('#main_tile').text(" Leads"); + $('#main_tile').text("Opportunities"); } if (type == 1 && leadType != null && leadType == 2) { diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php index fe1f6dba..e6f41756 100644 --- a/app/Views/sales/branch_level_dashboard_view.php +++ b/app/Views/sales/branch_level_dashboard_view.php @@ -248,14 +248,14 @@
    -

    All Leads Overview (".count($leads_overview).")" : ""; ?>

    - +

    All Leads Overview

    +
    diff --git a/app/Views/sales/target_view.php b/app/Views/sales/target_view.php index a500a2ba..3c6c79b3 100644 --- a/app/Views/sales/target_view.php +++ b/app/Views/sales/target_view.php @@ -225,6 +225,14 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- grid-template-columns: 1fr; /* stack vertically on mobile */ } } +@keyframes fadeHighlight { + 0% { background-color: #fffbcc; } + 100% { background-color: transparent; } +} + +.row-highlight { + animation: fadeHighlight 2s ease forwards; +}
    @@ -240,7 +248,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
    - + @@ -448,7 +456,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- tbody.innerHTML = ''; return; } - records.sort((a, b) => b.id - a.id); + // records.sort((a, b) => b.id - a.id); + records.sort((a, b) => { + let startYearA = parseInt(a.fy_year.split('-')[0], 10); + let startYearB = parseInt(b.fy_year.split('-')[0], 10); + return startYearB - startYearA; + }); + tbody.innerHTML = records.map(r => ` @@ -522,7 +536,14 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- document.getElementById('recordId').value = ''; document.getElementById('targetAmount').value = ''; populateFY(); - renderDetailTable(userId); + await renderDetailTable(userId); + const savedId = result.data?.id || recordId; + const targetRow = document.getElementById(`row_${savedId}`); + if (targetRow) { + targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); + targetRow.classList.add('row-highlight'); + setTimeout(() => targetRow.classList.remove('row-highlight'), 3000); + } } else { toastr.error(result.message || 'Failed to save target.'); } From 7e9b188720ac6866206505cea1bdb52355219931 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Feb 2026 16:06:39 +0530 Subject: [PATCH 03/12] FIX_salesTracker10 --- app/Controllers/SalesController.php | 6 ++++-- app/Models/SalesActivityModel.php | 2 +- app/Views/sales/activity_view.php | 19 ++++++++++++++----- .../sales/branch_level_dashboard_view.php | 8 ++++---- .../sales/sales_manager_level_dashboard.php | 2 +- app/Views/sales/tracker_view.php | 18 +++++++++++++----- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 0a4f2471..45e33f05 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -78,7 +78,8 @@ class SalesController extends BaseController $data = [ 'users' => [], - 'sales_manager_ids' => [] + 'sales_manager_ids' => [], + 'sales_role' => '' ]; $row = $db->table('user_profiles')->select('*') @@ -89,6 +90,7 @@ class SalesController extends BaseController // Is the logged-in user a Sales Manager? (Role 4, Team 5) if ($role == 4 && in_array(5, $team_id)) { + $data['sales_role'] = "Sales Manager"; $data['sales_manager_ids'] = [$logged_user_id]; $data['users'] = [ @@ -102,7 +104,7 @@ class SalesController extends BaseController } // Otherwise fetch ALL sales managers in this branch elseif (in_array($role,[1,5])) { - + $data['sales_role'] = "Sales Head"; $data['users'] = $db->table('user_profiles up') ->select('up.id, up.first_name, up.last_name, up.nhance_branch_id') ->join('user_teams ut', 'ut.user_id = up.id') diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php index 8d82097f..c27f526f 100644 --- a/app/Models/SalesActivityModel.php +++ b/app/Models/SalesActivityModel.php @@ -38,7 +38,7 @@ class SalesActivityModel extends Model // Validation protected $validationRules = [ 'lead_id' => 'required|integer', - 'activity_type' => 'required|in_list[Call,Email,Meeting,Demo,Share,Todo,Visit]', + 'activity_type' => 'required|in_list[Call,Email,Meeting,Visit,Demo,Share Docs,To Do]', 'notes' => 'required', 'scheduled_date' => 'required', 'assigned_to' => 'required|integer', diff --git a/app/Views/sales/activity_view.php b/app/Views/sales/activity_view.php index 0f82bc8f..3edb0181 100644 --- a/app/Views/sales/activity_view.php +++ b/app/Views/sales/activity_view.php @@ -256,8 +256,8 @@ - - + +
    @@ -326,8 +326,8 @@ - - + +
    @@ -429,7 +429,15 @@ function resetFlatpicker(){ }); } -const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "✓" }; +const activityIcons = { + "Call": "📞", + "Email": "✉️", + "Meeting": "📅", + "Visit": "🚗", + "Demo": "🖥️", + "Share Docs": "📄", + "To Do": "✓" +}; const salesManagerIds = ; const API = ''; @@ -441,6 +449,7 @@ let selectedFollowUpActivityType = ''; let currentPage = 1; let limit = 10; let currentOffset = 0; +const department = ''; function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); } diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php index e6f41756..efc8d3cb 100644 --- a/app/Views/sales/branch_level_dashboard_view.php +++ b/app/Views/sales/branch_level_dashboard_view.php @@ -135,8 +135,8 @@ 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', - 'Share' => '📄', - 'Todo' => '✓' + 'Share Docs' => '📄', + 'To Do' => '✓' ]; $icon = $activityIcons[$a['activity_type']] ?? '📌'; $statusClass = strtolower(str_replace(' ', '-', $a['status'])); @@ -217,8 +217,8 @@ 'Meeting' => ['color' => '#4299e1', 'icon' => '📅'], 'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'], 'Demo' => ['color' => '#9f7aea', 'icon' => '🖥️'], - 'Share' => ['color' => '#ed8936', 'icon' => '📄'], - 'Todo' => ['color' => '#718096', 'icon' => '✓'], + 'Share Docs' => ['color' => '#ed8936', 'icon' => '📄'], + 'To Do' => ['color' => '#718096', 'icon' => '✓'], ]; ?> diff --git a/app/Views/sales/sales_manager_level_dashboard.php b/app/Views/sales/sales_manager_level_dashboard.php index cfca7b7b..56891fb3 100644 --- a/app/Views/sales/sales_manager_level_dashboard.php +++ b/app/Views/sales/sales_manager_level_dashboard.php @@ -134,7 +134,7 @@ --> '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share' => '📄', 'Todo' => '✓' ]; + $activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ]; $icon = $activityIcons[$u['activity_type']] ?? '📌'; $formattedscheduledDate = date('M d, Y, h:i A', strtotime($u['scheduled_date'])); ?> diff --git a/app/Views/sales/tracker_view.php b/app/Views/sales/tracker_view.php index 7e8c4a92..d25a725e 100644 --- a/app/Views/sales/tracker_view.php +++ b/app/Views/sales/tracker_view.php @@ -285,8 +285,8 @@ - - + +
    @@ -354,8 +354,8 @@ - - + +
    @@ -936,7 +936,15 @@ function renderCard(opps) { } function renderTimeline(acts) { const cont = document.getElementById('timelineContainer'); - const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "✓" }; + const activityIcons = { + "Call": "📞", + "Email": "✉️", + "Meeting": "📅", + "Visit": "🚗", + "Demo": "🖥️", + "Share Docs": "📄", + "To Do": "✓" +}; if (acts.length === 0) { cont.classList.add('no-line'); From bd3151add9c2b4d743af3e44291778b5832616cc Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 27 Feb 2026 16:08:01 +0530 Subject: [PATCH 04/12] calander activity push --- app/Controllers/SalesController.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 44ea7aae..4e139caa 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -1238,9 +1238,23 @@ class SalesController extends BaseController /* ---------------- SUMMARY ---------------- */ - $summary = ucfirst($input['activity_type']) . - ' with ' . - $lead_data['company_name']; + $activityType = ucfirst($input['activity_type']); + + $prepositionMap = [ + 'Email' => 'to', + 'Call' => 'with', + 'Meeting' => 'with', + 'Visit' => 'to', + 'Demo' => 'with', + 'Share Docs' => 'to', + 'To Do' => 'for' + ]; + + $preposition = $prepositionMap[$activityType] ?? 'with'; + + $summary = "Activity scheduled : {$activityType} {$preposition} {$lead_data['company_name']}"; + + /* ---------------- GOOGLE PAYLOAD ---------------- */ From 1d7ac9d7eb249d58dadaece06939dfebd1f093ba Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Fri, 27 Feb 2026 17:08:30 +0530 Subject: [PATCH 05/12] FIX_salesTracker11 --- app/Controllers/SalesController.php | 130 ++++++++++++---------------- app/Models/SalesActivityModel.php | 2 +- 2 files changed, 55 insertions(+), 77 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 362a695b..164444e9 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -71,52 +71,59 @@ class SalesController extends BaseController */ private function getSalesStaffData(): array { - $db = \Config\Database::connect(); + $db = \Config\Database::connect(); $logged_user_id = get_session_userid(); - $role = get_role_id(); - $team_id = user_team(); + $role = get_role_id(); + $team_id = user_team(); $data = [ - 'users' => [], - 'sales_manager_ids' => [], - 'sales_role' => '' + 'users' => [], + 'sales_manager_ids'=> [], + 'sales_role' => '', + 'nhance_branch_id' => null, + 'assigned_ids' => [], ]; - $row = $db->table('user_profiles')->select('*') - ->where('is_active', 1)->where('id', $logged_user_id) - ->get()->getRow(); - - $nhance_branch_id = $row ? $row->nhance_branch_id : null; - - // Is the logged-in user a Sales Manager? (Role 4, Team 5) - if ($role == 4 && in_array(5, $team_id)) { - $data['sales_role'] = "Sales Manager"; + $row = $db->table('user_profiles')->select('*') + ->where('is_active', 1)->where('id', $logged_user_id) + ->get()->getRow(); + + $nhance_branch_id = $row ? $row->nhance_branch_id : null; + $data['nhance_branch_id']= $nhance_branch_id; + + // ── Sales Manager (Role 4, Team 5) ────────────────────────── + if ($role == 4 && in_array(5, $team_id)) { + + $data['sales_role'] = 'Sales Manager'; $data['sales_manager_ids'] = [$logged_user_id]; - - $data['users'] = [ + $data['assigned_ids'] = [$logged_user_id]; + $data['users'] = [ [ 'id' => $row->id, - 'first_name' => $row->first_name, - 'nhance_branch_id' => $nhance_branch_id + 'first_name' => $row->first_name, + 'last_name' => $row->last_name ?? '', + 'nhance_branch_id' => $nhance_branch_id, ] ]; - } - // Otherwise fetch ALL sales managers in this branch - elseif (in_array($role,[1,5])) { - $data['sales_role'] = "Sales Head"; - $data['users'] = $db->table('user_profiles up') - ->select('up.id, up.first_name, up.last_name, up.nhance_branch_id') - ->join('user_teams ut', 'ut.user_id = up.id') - ->where('up.is_active', 1) - ->where('ut.is_active', 1) - ->where('up.role', 4) - ->where('ut.team_id', 5) - ->where('up.nhance_branch_id', $nhance_branch_id) - ->get() - ->getResultArray(); + // ── Sales Head (Role 1 or 5) ───────────────────────────────── + } elseif (in_array($role, [1, 5])) { - $data['sales_manager_ids'] = array_column($data['users'], 'id'); + $data['sales_role'] = 'Sales Head'; + $data['users'] = $db->table('user_profiles up') + ->select('up.id, up.first_name, up.last_name, up.nhance_branch_id') + ->join('user_teams ut', 'ut.user_id = up.id') + ->where('up.is_active', 1) + ->where('ut.is_active', 1) + ->where('up.role', 4) + ->where('ut.team_id', 5) + ->where('up.nhance_branch_id', $nhance_branch_id) + ->get() + ->getResultArray(); + + $ids = array_column($data['users'], 'id'); + $data['sales_manager_ids'] = $ids; + $data['assigned_ids'] = $ids; // same value, both available } return $data; @@ -920,52 +927,23 @@ class SalesController extends BaseController // ==================== Dashboard ==================== - public function dashboard(){ - - $logged_user_id = get_session_userid(); - $role = get_role_id(); - $team_id = user_team(); + public function dashboard() + { $payload = $this->request->getGet(); + $base = $this->getSalesStaffData(); + $salesRole = $base['sales_role']; + $salesManagerIds = $base['sales_manager_ids']; + $userId = get_session_userid(); - $db = \Config\Database::connect(); + // Get branch id from users array + $nhanceBranchId = $base['users'][0]['nhance_branch_id'] ?? null; - $row = $db->table('user_profiles') - ->select('*') - ->where('is_active', 1) - ->where('id', $logged_user_id) - ->get() - ->getRowArray(); - - $nhance_branch_id = $row ? $row['nhance_branch_id'] : null; - - // dd($logged_user_id, $nhance_branch_id, $role, $team_id ); - - if (in_array($role,[1,5])) { - - $sales_manager_ids = array_column( - $db->table('user_profiles up') - ->select('up.id') - ->join('user_teams ut', 'ut.user_id = up.id') - ->where([ - 'up.is_active' => 1, - 'ut.is_active' => 1, - 'up.role' => 4, - 'ut.team_id' => 5, - 'up.nhance_branch_id' => $nhance_branch_id - ]) - ->get() - ->getResultArray(), - 'id' - ); - $this->branchLevelDashboard($nhance_branch_id,$sales_manager_ids); - - } - elseif ($role == 4 && in_array(5, $team_id)) { - $sales_manager_ids = [$logged_user_id]; - $this->salesManagerLevelDashboard($logged_user_id,$sales_manager_ids, $payload); + if ($salesRole === 'Sales Head') { + $this->branchLevelDashboard($nhanceBranchId, $salesManagerIds); + } elseif ($salesRole === 'Sales Manager') { + $this->salesManagerLevelDashboard($userId, $salesManagerIds,$payload); } - - } + } public function branchLevelDashboard($branchId,$sales_manager_ids) { diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php index c27f526f..e9a3801d 100644 --- a/app/Models/SalesActivityModel.php +++ b/app/Models/SalesActivityModel.php @@ -102,7 +102,7 @@ class SalesActivityModel extends Model $assignedToIds = is_array($assigned_to) ? $assigned_to : explode(',', $assigned_to); // Now it is guaranteed to be an array, making whereIn perfectly safe - $this->whereIn('sales_actual_leads.assigned_to', $assignedToIds); + $this->whereIn('sales_activities.assigned_to', $assignedToIds); } From 3765a2d865b8cfc8dc386efb97ee280487649668 Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Mon, 2 Mar 2026 10:02:42 +0530 Subject: [PATCH 06/12] FEAT_EXPENSE_MODULE_AND_TPA_DUMP_UPLOAD_FIXES --- app/Config/Acl.php | 2 + app/Config/Routes.php | 13 +- app/Controllers/ClientController.php | 11 +- app/Controllers/ExpenseController.php | 412 ++++++++++++ app/Controllers/TicketServiceController.php | 302 +++++++-- .../AbhiClaimImportService.php | 41 +- .../BaseTpaClaimImportService.php | 22 +- .../FhplClaimImportService.php | 38 +- .../IciciClaimImportService.php | 38 +- .../MediAssistClaimImportService.php | 37 +- .../RcareClaimImportService.php | 43 +- .../VidalClaimImportService.php | 45 +- app/Models/ExpenseModel.php | 73 +++ app/Views/claim_dump_file_list.php | 176 +++++- app/Views/expense_list.php | 586 ++++++++++++++++++ app/Views/layout/header.php | 11 + public/assets/images/expense_icon.png | Bin 0 -> 17291 bytes tests/unit/ExpenseControllerTest.php | 399 ++++++++++++ 18 files changed, 2146 insertions(+), 103 deletions(-) create mode 100644 app/Controllers/ExpenseController.php create mode 100644 app/Models/ExpenseModel.php create mode 100644 app/Views/expense_list.php create mode 100644 public/assets/images/expense_icon.png create mode 100644 tests/unit/ExpenseControllerTest.php diff --git a/app/Config/Acl.php b/app/Config/Acl.php index 2960595f..b4d85eac 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -32,6 +32,8 @@ class Acl '#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], '#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], + '#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], + diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d9a05733..123cf819 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -444,7 +444,9 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('proceedExcelFileDataValidation', 'EmployeeController::proceedExcelFileDataValidation'); $routes->get('checkTpaApiEnable', 'EmployeeRestController::checkTpaApiEnable'); $routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable'); - $routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend'); + $routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend'); + $routes->get('getTpaClaimDumpErrorData/(:any)', 'TicketServiceController::getTpaClaimDumpErrorData/$1'); + }); $routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); @@ -1002,4 +1004,13 @@ $routes->group('sales', function($routes) { $routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard'); }); +// Expence Module Route Group +$routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controllers'], static function($routes) { + $routes->get('/', 'ExpenseController::index'); + $routes->post('save', 'ExpenseController::save'); + $routes->get('get/(:num)', 'ExpenseController::getExpense/$1'); + $routes->post('delete/(:num)', 'ExpenseController::delete/$1'); + $routes->get('client-policies', 'ExpenseController::clientPolicies'); +}); + diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 62989198..1914d45b 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -6955,16 +6955,21 @@ class ClientController extends AdminController // $response = $ticketServiceController->getClaimExcelErrorData(["file_id" => 41]); // $response = $ticketServiceController->claimDumpOnBoardProcess(["file_id" => 17]); // $response = $ticketServiceController->extractExcelData("claims_dump_form_client.xlsx"); + // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 51]); //abhi // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 50]); //fhpl // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 53]); //icici // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 54]); //mediassist // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 52]); //reliance // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal - // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal - // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist - // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 4]); //abhi + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 53]); //icici + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 52]); //reliance + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal + // $response = $ticketServiceController->getTpaClaimDumpErrorData(["file_id" => 48]); // dd($response); // ---------- TICKET CONTROLLER -------------------------------------------------------------------------------- diff --git a/app/Controllers/ExpenseController.php b/app/Controllers/ExpenseController.php new file mode 100644 index 00000000..cf9a1bbe --- /dev/null +++ b/app/Controllers/ExpenseController.php @@ -0,0 +1,412 @@ +myLogger = \Config\Services::mylogger(); + $this->expenseModel = new ExpenseModel(); + $this->clientModel = new ClientModel(); + $this->clientPolicyModel = new ClientPolicyModel(); + } + + /** + * Web list + form view + */ + public function index() + { + try { + $data['tab_name'] = 'Expense'; + $data['page_name'] = 'Expense'; + $descriptionPattern = '/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/u'; + + // Raw GET filters + $rawFilters = $this->request->getGet() ?? []; + $rawFilters = is_array($rawFilters) ? $rawFilters : []; + + // Sanitize input array using existing helper + $sanitized = sanitizeInputArrayAdvanced($rawFilters); + + $filters = [ + 'client_id' => trim($sanitized['client_id'] ?? ''), + 'client_policy_id' => trim($sanitized['client_policy_id'] ?? ''), + 'approved_by' => trim($sanitized['approved_by'] ?? ''), + 'description' => trim($sanitized['description'] ?? ''), + 'amount' => trim($sanitized['amount'] ?? ''), + 'expense_date' => trim($sanitized['expense_date'] ?? ''), + ]; + + $validationErrors = []; + + // Basic type / format validation for filters + if ($filters['client_id'] !== '' && ! ctype_digit($filters['client_id'])) { + $validationErrors[] = 'Invalid client selected for search.'; + $filters['client_id'] = ''; + } + + if ($filters['client_policy_id'] !== '' && ! ctype_digit($filters['client_policy_id'])) { + $validationErrors[] = 'Invalid policy selected for search.'; + $filters['client_policy_id'] = ''; + } + + if ($filters['approved_by'] !== '' && ! ctype_digit($filters['approved_by'])) { + $validationErrors[] = 'Invalid approver selected for search.'; + $filters['approved_by'] = ''; + } + + if ($filters['description'] !== '' && ! preg_match($descriptionPattern, $filters['description'])) { + $validationErrors[] = 'Description filter contains invalid characters.'; + $filters['description'] = ''; + } + + if ($filters['amount'] !== '') { + if (! is_numeric($filters['amount']) || (float) $filters['amount'] < 0) { + $validationErrors[] = 'Amount filter must be a non-negative number.'; + $filters['amount'] = ''; + } + } + + if ($filters['expense_date'] !== '') { + $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']); + $errors = $dt ? \DateTime::getLastErrors() : ['warning_count' => 1, 'error_count' => 1]; + if (! $dt || ! empty($errors['warning_count']) || ! empty($errors['error_count'])) { + $validationErrors[] = 'Expense Date filter must be in DD-MM-YYYY format.'; + $filters['expense_date'] = ''; + } + } + + $data['filters'] = $filters; + $data['validation_errors'] = $validationErrors; + + // Clients for dropdown + $data['clients'] = $this->clientModel + ->select('id, client_name, short_name') + ->where('is_active', 1) + ->orderBy('client_name', 'ASC') + ->findAll(); + + // Approved by (users) dropdown + $db = db_connect(); + $data['approved_users'] = $db->table('user_profiles') + ->select('id, first_name') + ->where('is_active', 1) + ->whereIn('id', [7, 8]) + ->orderBy('first_name', 'ASC') + ->get() + ->getResultArray(); + + // Policies for filter dropdown (when client filter is selected) + $data['policies_for_filter'] = []; + if ($filters['client_id'] !== '') { + $data['policies_for_filter'] = $this->clientPolicyModel + ->select('id, policy_no') + ->where('client_id', (int) $filters['client_id']) + ->where('is_active', 1) + ->orderBy('policy_no', 'ASC') + ->findAll(); + } + + // Existing expenses with optional filters + $builder = $this->expenseModel + ->select(' + expenses.*, + clients.client_name, + clients.short_name, + client_policy.policy_no, + user_profiles.first_name AS approved_by_name + ') + ->join('clients', 'clients.id = expenses.client_id', 'left') + ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left') + ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left') + ->where('expenses.is_active', 1); + + if ($filters['client_id'] !== '') { + $builder->where('expenses.client_id', (int) $filters['client_id']); + } + + if ($filters['client_policy_id'] !== '') { + $builder->where('expenses.client_policy_id', (int) $filters['client_policy_id']); + } + + if ($filters['approved_by'] !== '') { + $builder->where('expenses.approved_by', (int) $filters['approved_by']); + } + + if ($filters['description'] !== '') { + $builder->like('expenses.description', (string) $filters['description']); + } + + if ($filters['amount'] !== '') { + $builder->where('expenses.amount', (float) $filters['amount']); + } + + if ($filters['expense_date'] !== '') { + $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']); + if ($dt) { + $builder->where('expenses.expense_date', $dt->format('Y-m-d')); + } + } + + $data['expenses'] = $builder + ->orderBy('expenses.id', 'DESC') + ->findAll(); + + return $this->loadLayout('expense_list', $data); + } catch (\Throwable $e) { + return handle_exception($e, $this->myLogger, $this->response); + } + } + + /** + * Create / update expense (AJAX) + */ + public function save() + { + try { + if ($this->request->getMethod() !== 'post') { + return $this->response + ->setStatusCode(405) + ->setJSON([ + 'status' => false, + 'message' => 'Invalid request method', + ]); + } + + $rawData = $this->request->getPost(); + $data = sanitizeInputArrayAdvanced($rawData); + + $id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null; + + $rules = [ + 'client_id' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Client is required', + ], + ], + 'client_policy_id' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Policy is required', + ], + ], + 'description' => [ + 'rules' => 'required|string|min_length[1]|max_length[2000]|regex_match[/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/]', + 'errors' => [ + 'required' => 'Description is required', + 'min_length' => 'Description cannot be empty', + 'regex_match' => 'Description contains invalid characters.', + ], + ], + 'expense_date' => [ + 'rules' => 'required|valid_date[d-m-Y]', + 'errors' => [ + 'required' => 'Expense Date is required', + 'valid_date' => 'Expense Date must be in DD-MM-YYYY format', + ], + ], + 'approved_by' => [ + 'rules' => 'required|is_natural_no_zero', + 'errors' => [ + 'required' => 'Approved By is required', + ], + ], + 'amount' => [ + 'rules' => 'required|numeric|greater_than_equal_to[0]', + 'errors' => [ + 'required' => 'Amount is required', + 'numeric' => 'Amount must be numeric', + 'greater_than_equal_to' => 'Amount cannot be negative', + ], + ], + ]; + + if (! $this->validate($rules)) { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => false, + 'message' => 'Input validation failed', + 'errors' => $this->validator->getErrors(), + ]); + } + + $payload = [ + 'client_id' => (int) ($data['client_id'] ?? 0), + 'client_policy_id' => (int) ($data['client_policy_id'] ?? 0), + 'description' => $data['description'] ?? null, + 'approved_by' => (int) ($data['approved_by'] ?? 0), + 'amount' => $data['amount'] ?? null, + 'is_active' => 1, + ]; + + $expenseDate = $data['expense_date'] ?? null; + if (! empty($expenseDate)) { + $dt = \DateTime::createFromFormat('d-m-Y', $expenseDate); + $payload['expense_date'] = $dt ? $dt->format('Y-m-d') : null; + } else { + $payload['expense_date'] = null; + } + + if ($id === null) { + $insertId = $this->expenseModel->insert($payload, true); + $success = ! empty($insertId); + $id = $insertId; + $message = $success + ? 'Expense created successfully' + : 'Unable to create expense. Please try again.'; + } else { + $success = $this->expenseModel->update($id, $payload); + $message = $success + ? 'Expense updated successfully' + : 'Unable to update expense. Please try again.'; + } + + return $this->response + ->setStatusCode($success ? 200 : 400) + ->setJSON([ + 'status' => (bool) $success, + 'message' => $message, + 'id' => $id, + ]); + } catch (\Throwable $e) { + return handle_exception($e, $this->myLogger, $this->response); + } + } + + /** + * Get single expense (AJAX) + */ + public function getExpense($id = null) + { + try { + $id = (int) $id; + + if (empty($id)) { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => false, + 'message' => 'Invalid expense id', + ]); + } + + $expense = $this->expenseModel + ->select(' + expenses.*, + clients.client_name, + clients.short_name, + client_policy.policy_no, + user_profiles.first_name AS approved_by_name + ') + ->join('clients', 'clients.id = expenses.client_id', 'left') + ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left') + ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left') + ->where('expenses.id', $id) + ->where('expenses.is_active', 1) + ->first(); + + if (empty($expense)) { + return $this->response->setStatusCode(404)->setJSON([ + 'status' => false, + 'message' => 'Expense not found', + ]); + } + + return $this->response->setStatusCode(200)->setJSON([ + 'status' => true, + 'data' => $expense, + ]); + } catch (\Throwable $e) { + return handle_exception($e, $this->myLogger, $this->response); + } + } + + /** + * Soft delete expense (AJAX) + */ + public function delete($id = null) + { + try { + if ($this->request->getMethod() !== 'post') { + return $this->response + ->setStatusCode(405) + ->setJSON([ + 'status' => false, + 'message' => 'Invalid request method', + ]); + } + + $id = (int) $id; + + if (empty($id)) { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => false, + 'message' => 'Invalid expense id', + ]); + } + + $payload = [ + 'is_active' => 0, + ]; + + $success = $this->expenseModel->update($id, $payload); + + return $this->response + ->setStatusCode($success ? 200 : 400) + ->setJSON([ + 'status' => (bool) $success, + 'message' => $success + ? 'Expense deleted successfully' + : 'Unable to delete expense. Please try again.', + ]); + } catch (\Throwable $e) { + return handle_exception($e, $this->myLogger, $this->response); + } + } + + /** + * Get policies by client for dropdown (AJAX) + */ + public function clientPolicies() + { + try { + $clientId = (int) ($this->request->getGet('client_id') ?? 0); + + if (empty($clientId)) { + return $this->response->setStatusCode(400)->setJSON([ + 'status' => false, + 'message' => 'Client is required', + ]); + } + + $policies = $this->clientPolicyModel + ->select('client_policy.id, client_policy.policy_no, policy_type.policy_type') + ->join('policy_type', 'policy_type.id = client_policy.policy_type_id AND policy_type.is_active = 1') + ->where('client_policy.client_id', $clientId) + ->where('client_policy.is_active', 1) + ->orderBy('client_policy.id', 'ASC') + ->findAll(); + + return $this->response->setStatusCode(200)->setJSON([ + 'status' => true, + 'data' => $policies, + ]); + } catch (\Throwable $e) { + return handle_exception($e, $this->myLogger, $this->response); + } + } +} + diff --git a/app/Controllers/TicketServiceController.php b/app/Controllers/TicketServiceController.php index f95f547d..8e96a780 100644 --- a/app/Controllers/TicketServiceController.php +++ b/app/Controllers/TicketServiceController.php @@ -30,8 +30,9 @@ use App\Models\TicketMasterModel; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Fill; -class TicketServiceController extends BaseController +class TicketServiceController extends AdminController { use ResponseTrait; @@ -1840,39 +1841,86 @@ class TicketServiceController extends BaseController { } - - + + // -------------------------------------------------------------------------------------------------------------------------------- - - public function tpaClaimDumpImporter($params) + /** + * Resolve and validate Claim Dump file metadata and physical file for TPA imports. + * + * @param int|null $fileId + * @return array{status:bool,message?:string,fileData?:array,filePath?:string} + */ + private function resolveTpaClaimDumpFile(?int $fileId): array { - $file_id = $params['file_id'] ?? null; // Move outside try to ensure catch can see it + if (empty($fileId)) { + return [ + 'status' => false, + 'message' => 'File ID is missing', + ]; + } + + $fileData = $this->claimDumpFileModel->find((int) $fileId); + if (!$fileData) { + return [ + 'status' => false, + 'message' => 'Invalid file ID. No file data found', + ]; + } + + $filePath = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR . $fileData['file_name']; + + if (!is_file($filePath)) { + return [ + 'status' => false, + 'message' => 'Claim dump file not found', + ]; + } + + return [ + 'status' => true, + 'fileData' => $fileData, + 'filePath' => $filePath, + ]; + } + + /** + * First TPA-wise job – parse Excel and push data into TPA staging table. + * + * @param array $params + * @return array + */ + public function tpaClaimDumpImporter(array $params) + { + $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null; try { - $file_path = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR; + $resolved = $this->resolveTpaClaimDumpFile($file_id); - if (!$file_id) { - return ['status' => false, 'message' => 'File ID is missing']; + if ($resolved['status'] === false) { + return [ + 'status' => false, + 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file', + ]; } - $fileData = $this->claimDumpFileModel->find((int)$file_id); - if (!$fileData) { - return ['status' => false, 'message' => 'Invalid file ID. No file data found']; - } - - $file_full_path = $file_path . $fileData['file_name']; - if (!is_file($file_full_path)) { - return ['status' => false, 'message' => 'Claim dump file not found']; - } + $fileData = $resolved['fileData']; + $filePath = $resolved['filePath']; $handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0); - $result = $handler->runTpaClaimDumpInsert($file_full_path, $file_id); + $result = $handler->runTpaClaimDumpInsert($filePath, $file_id); if (!empty($result['status']) && $result['status'] === true) { - Jobs::addJob(['job_name' => 'tpaClaimDumpToTicketMasterImporters', 'payload' => ['file_id' => $file_id]]); + Jobs::addJob([ + 'job_name' => 'tpaClaimDumpToTicketMasterImporters', + 'payload' => ['file_id' => $file_id], + ]); } else { // FORCE FAIL LOGIC - $this->markAsFailed($file_id, $result['message'] ?? 'System error contact admin', $fileData['created_by'] ?? null); + $this->markAsFailed( + $file_id, + $result['message'] ?? 'System error contact admin', + $fileData['created_by'] ?? null + ); } return $result; @@ -1884,9 +1932,9 @@ class TicketServiceController extends BaseController } return [ - 'status' => false, - 'message' => 'TPA Claim dump import failed', - 'error_data' => $th->getMessage() + 'status' => false, + 'message' => 'TPA Claim dump import failed', + 'error_data' => $th->getMessage(), ]; } } @@ -1908,30 +1956,37 @@ class TicketServiceController extends BaseController return $this->claimDumpFileModel->update($file_id, $data); } - public function tpaClaimDumpToTicketMasterImporters($params) + /** + * Second TPA-wise job – move data from TPA staging into ticket_master. + * + * @param array $params + * @return array + */ + public function tpaClaimDumpToTicketMasterImporters(array $params) { - $file_id = $params['file_id'] ?? null; + $file_id = isset($params['file_id']) ? (int) $params['file_id'] : null; $fileData = null; try { - if (!$file_id) { - return ['status' => false, 'message' => 'File ID is missing']; + $resolved = $this->resolveTpaClaimDumpFile($file_id); + + if ($resolved['status'] === false) { + return [ + 'status' => false, + 'message' => $resolved['message'] ?? 'Unable to resolve claim dump file', + ]; } - $fileData = $this->claimDumpFileModel->where('id', $file_id)->first(); - - if (!$fileData) { - return ['status' => false, 'message' => 'Invalid file ID. No file data found to import']; - } + $fileData = $resolved['fileData']; $handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0); - $result = $handler->runTicketMasterInsert($params); + $result = $handler->runTicketMasterInsert($params); if (!empty($result['status']) && $result['status'] === true) { // Success: Update the status to success $this->claimDumpFileModel->update($file_id, [ 'status' => 'success', - 'reason' => null + 'reason' => null, ]); } else { // Logic failure: The runTicketMasterInsert returned status false @@ -1957,8 +2012,8 @@ class TicketServiceController extends BaseController 'message' => $th->getMessage(), 'error_data' => [ 'line' => $th->getLine(), - 'file' => $th->getFile() - ] + 'file' => $th->getFile(), + ], ]; } } @@ -2011,5 +2066,176 @@ class TicketServiceController extends BaseController return $data; } - + public function getTpaClaimDumpErrorData($file_id) + { + $file_id = (int) $file_id; + + // Ensure we always have a Response object, even if controller was instantiated manually + $response = $this->response ?? service('response'); + + if ($file_id <= 0) { + return $response + ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST) + ->setJSON(['status' => false, 'message' => 'Invalid file id']); + } + + $fileData = $this->claimDumpFileModel->find($file_id); + + if (!$fileData) { + return $response + ->setStatusCode(ResponseInterface::HTTP_NOT_FOUND) + ->setJSON(['status' => false, 'message' => 'File record not found']); + } + + $tpaId = (int) ($fileData['tpa_id'] ?? 0); + + if ($tpaId <= 0) { + return $response + ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST) + ->setJSON(['status' => false, 'message' => 'TPA not linked with this file']); + } + + // Resolve TPA staging table based on configured TPA IDs + $tableName = match ($tpaId) { + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal', + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi', + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist', + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl', + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance', + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici', + default => null, + }; + + if ($tableName === null) { + return $response + ->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST) + ->setJSON(['status' => false, 'message' => 'Unsupported TPA for error dump export']); + } + + $db = \Config\Database::connect(); + $builder = $db->table($tableName); + + // Fetch only records belonging to this file and having a rejection reason + $rows = $builder + ->where('file_id', $file_id) + ->where('is_active', 1) + ->where('master_reject_reason IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + if (empty($rows)) { + // No error records – return a small Excel file with just a message + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Errors'); + $sheet->setCellValue('A1', 'Message'); + $sheet->setCellValue('A2', 'No rejected records found for this file.'); + + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + ob_start(); + $writer->save('php://output'); + $excelOutput = ob_get_clean(); + + $filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx'; + + return $response + ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setHeader('Cache-Control', 'max-age=0') + ->setBody($excelOutput); + } + + // Columns that must NOT be included in the Excel export + $excludedColumns = [ + 'id', + 'client_id', + 'client_policy_id', + 'ticket_id', + 'file_id', + 'created_at', + 'updated_at', + 'created_by', + 'updated_by', + 'is_active', + ]; + + $firstRow = $rows[0]; + + // Build header mapping and track the "Rejected Reason" column index + $dbColumnOrder = []; + $displayHeaderLabels = []; + $rejectedReasonColIdx = null; // 1-based index for Excel column + + foreach ($firstRow as $columnName => $_) { + if (in_array($columnName, $excludedColumns, true)) { + continue; + } + + $dbColumnOrder[] = $columnName; + + if ($columnName === 'master_reject_reason' || $columnName === 'master_rejection_reason_key') { + $displayHeaderLabels[] = 'Rejected Reason'; + $rejectedReasonColIdx = count($displayHeaderLabels); // current column index (1-based) + } else { + $displayHeaderLabels[] = $columnName; + } + } + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Errors'); + + // Header row + $rowIndex = 1; + foreach ($displayHeaderLabels as $colIndex => $headerLabel) { + $columnLetter = Coordinate::stringFromColumnIndex($colIndex + 1); + $cellAddress = $columnLetter . $rowIndex; + $sheet->setCellValue($cellAddress, $headerLabel); + } + + // Data rows + $rowIndex = 2; + foreach ($rows as $row) { + foreach ($dbColumnOrder as $i => $columnName) { + $colIndex = $i + 1; + $columnLetter = Coordinate::stringFromColumnIndex($colIndex); + $cellAddress = $columnLetter . $rowIndex; + $value = $row[$columnName] ?? null; + + $sheet->setCellValue($cellAddress, $value); + + // Highlight the "Rejected Reason" values + if ($rejectedReasonColIdx !== null && $colIndex === $rejectedReasonColIdx) { + $sheet->getStyle($cellAddress) + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setARGB('FFFFF4B2'); // light yellow + } + } + + $rowIndex++; + } + + // Autosize columns for better readability + $highestColumnIndex = count($displayHeaderLabels); + for ($col = 1; $col <= $highestColumnIndex; $col++) { + $columnLetter = Coordinate::stringFromColumnIndex($col); + $sheet->getColumnDimension($columnLetter)->setAutoSize(true); + } + + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + ob_start(); + $writer->save('php://output'); + $excelOutput = ob_get_clean(); + + $filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx'; + + return $response + ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setHeader('Cache-Control', 'max-age=0') + ->setBody($excelOutput); + } + } diff --git a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php index ffb4b970..103989bf 100644 --- a/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/AbhiClaimImportService.php @@ -119,6 +119,19 @@ class AbhiClaimImportService extends BaseTpaClaimImportService 'Settled' => 11, 'Rejected' => 8, 'Cancelled' => 13, + 'Approved' => 9, + 'Closed' => 12, + 'Query' => 4, + 'Required Information' => 4, + 'In-Progress' => 5, + 'Paid' => 11, + 'Denied' => 8, + 'Cancelled' => 13, + 'Processed' => 61, + 'Information Awaited' => 4, + 'Denied Letter Sent' => 66, + 'Cashless Document Awaited' => 3, + 'RI Cancelled' => 13, ]; protected $dateColumns = [ @@ -164,14 +177,30 @@ class AbhiClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; + $rows = $this->db->table('claims_dump_abhi cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); + + if (empty($rows)) { + return true; } - $builder = $this->db->table('claims_dump_abhi'); - return $builder->updateBatch($data, 'id'); + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_abhi')->updateBatch($updateData, 'id') !== false; } @@ -329,7 +358,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService } // Meta fields - $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']); $item['file_id'] = $file_id; $item['claim_dump_ref_id'] = $row['id']; $item['created_by'] = $file_data['created_by'] ?? null; diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index 3a6a5777..137caf25 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -81,7 +81,7 @@ abstract class BaseTpaClaimImportService $this->db->transBegin(); try { - $file_id = $params['file_id']; + $file_id = $params['file_id']; $ticketMasterData = $this->mapClaimMasterData($file_id); // Check if mapping failed @@ -101,6 +101,12 @@ abstract class BaseTpaClaimImportService $this->db->transRollback(); return ['status' => false, 'message' => 'Ticket Master Claim bulk insert failed']; } + // Map newly created ticket IDs back to the TPA staging table + if (!$this->updateTicketIdInTPATable($file_id)) { + $this->db->transRollback(); + return ['status' => false, 'message' => 'Updating ticket_id in TPA table failed']; + } + $message .= 'Ticket Master Claim bulk insert success. '; $hasExecutedTask = true; }else{ @@ -328,6 +334,16 @@ abstract class BaseTpaClaimImportService return $employeeData ?? []; } + public function checkStatusMapping($statusArray, $statusString) + { + foreach ($statusArray as $key => $value) { + if (strtolower($statusString) == strtolower($key) || strtolower($statusString) == strtolower(trim($key))) { + return $value; + } + } + return null; + } + /** * Map Excel rows to TPA table structure */ @@ -349,9 +365,9 @@ abstract class BaseTpaClaimImportService abstract protected function importClaimMaster(array $data): bool; /** - * Update TPA table with ticket_master primary key + * Update TPA table with ticket_master primary key for a given file. */ - abstract protected function updateTicketIdInTPATable(): bool; + abstract protected function updateTicketIdInTPATable(int $fileId): bool; /** * Update TPA table with ticket_master insert rejected reason diff --git a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php index 324fd3a7..7123230c 100644 --- a/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/FhplClaimImportService.php @@ -181,6 +181,13 @@ class FhplClaimImportService extends BaseTpaClaimImportService protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, + 'Paid' => 11, + 'Approved' => 9, + 'Closed' => 12, + 'Under Process' => 5, + 'Query' => 4, + 'Required Information' => 4, + 'In-Progress' => 5, ]; protected $dateColumns = [ @@ -235,14 +242,31 @@ class FhplClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; + // Join ticket_master with FHPL staging on file_id + claim_dump_ref_id -> id + $rows = $this->db->table('claims_dump_fhpl cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); + + if (empty($rows)) { + return true; } - - $builder = $this->db->table('claims_dump_fhpl'); - return $builder->updateBatch($data, 'id'); + + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_fhpl')->updateBatch($updateData, 'id') !== false; } @@ -400,7 +424,7 @@ class FhplClaimImportService extends BaseTpaClaimImportService } // Meta fields - $item['claim_status_id'] = $statusMapping[$row['current_claim_status']] ?? 61; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['current_claim_status']) ?? 61; $item['file_id'] = $file_id; $item['claim_dump_ref_id'] = $row['id']; $item['created_by'] = $file_data['created_by'] ?? null; diff --git a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php index 132986a2..6e055139 100644 --- a/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/IciciClaimImportService.php @@ -106,7 +106,13 @@ class IciciClaimImportService extends BaseTpaClaimImportService protected $statusMapping = [ 'PAID' => 11, + 'SETTLED' => 11, 'REJECTED' => 8, + 'APPROVED' => 9, + 'CLOSED' => 12, + 'QUERY' => 4, + 'REQUIRED INFORMATION' => 4, + 'IN-PROGRESS' => 5, ]; protected $dateColumns = [ @@ -152,14 +158,30 @@ class IciciClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; + $rows = $this->db->table('claims_dump_icici cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); + + if (empty($rows)) { + return true; } - - $builder = $this->db->table('claims_dump_icici'); - return $builder->upsertBatch($data, 'id'); + + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_icici')->updateBatch($updateData, 'id') !== false; } @@ -287,7 +309,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService $item['tpa_id'] = $client_policy_data['tpa_id'] ?? null; $item['acm_id'] = $client_policy_data['acm_id'] ?? null; $item['policy_no'] = $client_policy_data['policy_no'] ?? null; - $item['relationship'] = $this->convertRelation($row['relation_group'] ?? ''); + $item['relationship'] = $this->convertRelation($row['relation'] ?? ''); $employee_data = $this->getEmployeeDetails($file_data['client_id'], $file_data['client_policy_id'], $row['employee_member_id'], $item['relationship']); @@ -317,7 +339,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService } // Meta fields - $item['claim_status_id'] = $statusMapping[$row['updated_status']] ?? 61; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['updated_status']) ?? 61; $item['claim_dump_ref_id'] = $row['id']; $item['file_id'] = $file_id; $item['created_by'] = $file_data['created_by'] ?? null; diff --git a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php index d052c9f5..4a2952fb 100644 --- a/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/MediAssistClaimImportService.php @@ -158,12 +158,15 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService protected $statusMapping = [ 'Settled' => 11, 'Rejected' => 8, + 'Paid' => 11, 'Denied' => 8, 'Cancelled' => 13, 'Processed' => 61, 'Information Awaited' => 4, 'Denied Letter Sent' => 66, 'Cashless Document Awaited' => 3, + 'Approved' => 9, + 'Closed' => 12, ]; /** @@ -192,16 +195,30 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; - } - - $builder = $this->db->table('claims_dump_medi_assist'); - $builder->insertBatch($data); + $rows = $this->db->table('claims_dump_medi_assist cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); - return true; + if (empty($rows)) { + return true; + } + + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_medi_assist')->updateBatch($updateData, 'id') !== false; } @@ -356,8 +373,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService // Meta fields $item['claim_dump_ref_id'] = $row['id']; - $item['file_id'] = $file_id; - $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61; + $item['file_id'] = $file_id; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61; $item['created_by'] = $file_data['created_by'] ?? null; $item['claim_type'] = 1; $item['priority'] = 1; diff --git a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php index 770caadd..afe95c4f 100644 --- a/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/RcareClaimImportService.php @@ -96,14 +96,15 @@ class RcareClaimImportService extends BaseTpaClaimImportService ]; protected $statusMapping = [ + 'CL Paid with Settlement Letter' => 11, 'Settled' => 11, + 'Paid' => 11, 'Rejected' => 8, - 'Denied' => 8, - 'Cancelled' => 13, - 'Processed' => 61, - 'Information Awaited' => 4, - 'Denied Letter Sent' => 66, - 'Cashless Document Awaited' => 3, + 'Approved' => 9, + 'Closed' => 12, + 'CL Rejected' => 8, + 'CL Approved' => 9, + 'AL Closed' => 12, ]; protected $dateColumns = [ @@ -143,14 +144,30 @@ class RcareClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; + $rows = $this->db->table('claims_dump_reliance cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); + + if (empty($rows)) { + return true; } - - $builder = $this->db->table('claims_dump_reliance'); - return $builder->updateBatch($data, 'id'); + + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_reliance')->updateBatch($updateData, 'id') !== false; } @@ -307,7 +324,7 @@ class RcareClaimImportService extends BaseTpaClaimImportService } // Meta fields - $item['claim_status_id'] = $statusMapping[$row['final_status']] ?? 61; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['final_status']) ?? 61; $item['file_id'] = $file_id; $item['created_by'] = $file_data['created_by'] ?? null; $item['claim_dump_ref_id'] = $row['id']; diff --git a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php index c90d580b..70a1340c 100644 --- a/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/VidalClaimImportService.php @@ -349,10 +349,17 @@ class VidalClaimImportService extends BaseTpaClaimImportService ]; protected $statusMapping = [ - 'CL Paid with Settlement Letter' => 11, - 'CL Rejected' => 8, - 'CL Approved' => 9, - 'AL Closed' => 12, + 'Settled' => 11, + 'Rejected' => 8, + 'Paid' => 11, + 'Denied' => 8, + 'Cancelled' => 13, + 'Processed' => 61, + 'Information Awaited' => 4, + 'Denied Letter Sent' => 66, + 'Cashless Document Awaited' => 3, + 'Approved' => 9, + 'Closed' => 12, ]; @@ -382,14 +389,30 @@ class VidalClaimImportService extends BaseTpaClaimImportService } - public function updateTicketIdInTPATable(): bool + public function updateTicketIdInTPATable(int $fileId): bool { - if (empty($data)) { - return false; + $rows = $this->db->table('claims_dump_vidal cd') + ->select('cd.id, tm.id AS ticket_id') + ->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner') + ->where('cd.is_active', 1) + ->where('cd.file_id', $fileId) + ->where('cd.ticket_id IS NULL') + ->get() + ->getResultArray(); + + if (empty($rows)) { + return true; } - - $builder = $this->db->table('claims_dump_vidal'); - return $builder->updateBatch($data, 'id'); + + $updateData = []; + foreach ($rows as $row) { + $updateData[] = [ + 'id' => $row['id'], + 'ticket_id' => $row['ticket_id'], + ]; + } + + return $this->db->table('claims_dump_vidal')->updateBatch($updateData, 'id') !== false; } @@ -542,7 +565,7 @@ class VidalClaimImportService extends BaseTpaClaimImportService } // Meta fields - $item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61; + $item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61; $item['file_id'] = $file_id; $item['claim_dump_ref_id'] = $row['id']; $item['created_by'] = $file_data['created_by'] ?? null; diff --git a/app/Models/ExpenseModel.php b/app/Models/ExpenseModel.php new file mode 100644 index 00000000..7d4a37c1 --- /dev/null +++ b/app/Models/ExpenseModel.php @@ -0,0 +1,73 @@ + @@ -151,7 +221,16 @@
    PersonSales Manager Action
    No targets added yet.
    ${r.fy_year}
    + + + + + + + + + + + + + + + $row) : ?> + + + + + + + + + + + + + +
    S.NoClientPolicy NoDescriptionApproved ByAmountExpense DateAction
    + + + + + + + + +
    +
    + + + + + + diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php index b7271a2c..6695b729 100755 --- a/app/Views/layout/header.php +++ b/app/Views/layout/header.php @@ -1876,6 +1876,17 @@ + + +
  • + + /assets/images/expense_icon.png" alt="Logo" height="24"> + Expense + +
  • + +
  • diff --git a/public/assets/images/expense_icon.png b/public/assets/images/expense_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d1cba8e8053fa2f2b6947dc82372ecac9b465f8b GIT binary patch literal 17291 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 R_#+LFxyfbY=8N!q{|8;_&7uGR literal 0 HcmV?d00001 diff --git a/tests/unit/ExpenseControllerTest.php b/tests/unit/ExpenseControllerTest.php new file mode 100644 index 00000000..84ea3b29 --- /dev/null +++ b/tests/unit/ExpenseControllerTest.php @@ -0,0 +1,399 @@ +value = $value; + } + + public static function fromValue($value): self + { + return new self($value); + } + + public function map($callback): self + { + if ($this->value !== null) { + $this->value = $callback($this->value); + } + + return $this; + } + + public function getOrCall($callback) + { + return $this->value !== null ? $this->value : $callback(); + } + + public function getOrThrow($exception) + { + if ($this->value === null) { + throw $exception; + } + + return $this->value; + } + } +} + +namespace App\Filters { + use CodeIgniter\Filters\FilterInterface; + use CodeIgniter\HTTP\RequestInterface; + use CodeIgniter\HTTP\ResponseInterface; + + // Test stubs for application filters to bypass logging, ACL, and security checks during tests. + + class AclFilter implements FilterInterface + { + public function before(RequestInterface $request, $arguments = null) { return null; } + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; } + } + + class HttpRequestLog implements FilterInterface + { + public function before(RequestInterface $request, $arguments = null) { return null; } + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; } + } + + class SecurityInputFilter implements FilterInterface + { + public function before(RequestInterface $request, $arguments = null) { return null; } + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; } + } + + class GlobalPostFileUploadGuard implements FilterInterface + { + public function before(RequestInterface $request, $arguments = null) { return null; } + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; } + } + + class Cors implements FilterInterface + { + public function before(RequestInterface $request, $arguments = null) { return null; } + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; } + } +} + +namespace Dotenv\Repository { + interface RepositoryInterface + { + public function get($name); + + public function set($name, $value); + } + + class RepositoryBuilder + { + public static function createWithDefaultAdapters(): self + { + return new self(); + } + + public function addAdapter($adapter): self + { + // No-op for testing. + return $this; + } + + public function immutable(): self + { + return $this; + } + + public function make(): RepositoryInterface + { + return new class() implements RepositoryInterface { + public function get($name) + { + $value = getenv($name); + + return $value === false ? null : $value; + } + + public function set($name, $value) + { + putenv("$name=$value"); + } + }; + } + } +} + +namespace Dotenv\Repository\Adapter { + class PutenvAdapter + { + // Stub class used only to satisfy Illuminate\Support\Env references. + } +} + +namespace Tests\unit { + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\FeatureTestTrait; + +class ExpenseControllerTest extends CIUnitTestCase +{ + use FeatureTestTrait; + + protected function setUp(): void + { + parent::setUp(); + + // Override routes for testing to avoid filters/middleware. + $this->withRoutes([ + ['get', 'expense', 'ExpenseController::index'], + ['post', 'expense/save', 'ExpenseController::save'], + ['get', 'expense/get/(:num)', 'ExpenseController::getExpense/$1'], + ['post', 'expense/delete/(:num)','ExpenseController::delete/$1'], + ['get', 'expense/client-policies', 'ExpenseController::clientPolicies'], + ]); + + // Clean up any test data from previous runs + $db = db_connect('default'); + $db->table('expenses')->like('description', 'CI4_TEST_', 'after')->delete(); + } + + /** + * Helper to insert an expense row directly into the database. + */ + private function createExpenseRecord(array $overrides = []): int + { + $db = db_connect('default'); + + $data = array_merge([ + 'client_id' => 1, + 'client_policy_id' => 1, + 'description' => 'CI4_TEST_' . uniqid('', true), + 'approved_by' => 1, + 'amount' => 100.00, + 'created_at' => date('Y-m-d H:i:s'), + 'is_active' => 1, + ], $overrides); + + $db->table('expenses')->insert($data); + + return (int) $db->insertID(); + } + + public function testCreateExpenseSuccess(): void + { + $payload = [ + 'client_id' => '1', + 'client_policy_id' => '1', + 'approved_by' => '1', + 'description' => 'CI4_TEST_Valid Description 123-ABC', + 'expense_date' => '12-01-2026', + 'amount' => '250.75', + ]; + + $result = $this->post('expense/save', $payload); + + $result->assertStatus(200); + + $json = json_decode($result->getJSON(), true); + + $this->assertIsArray($json); + $this->assertTrue($json['status'] ?? false); + $this->assertNotEmpty($json['id'] ?? null); + + $insertedId = (int) $json['id']; + + $db = db_connect('default'); + $row = $db->table('expenses') + ->where('description', $payload['description']) + ->orderBy('id', 'DESC') + ->get() + ->getRowArray(); + + $this->assertNotEmpty($row); + $this->assertSame(1, (int) $row['is_active']); + $this->assertSame($payload['description'], $row['description']); + $this->assertEquals(250.75, (float) $row['amount']); + } + + public function testCreateExpenseValidationFailureMissingFields(): void + { + $payload = [ + // all required fields missing / empty + ]; + + $result = $this->post('expense/save', $payload); + + $result->assertStatus(400); + + $json = json_decode($result->getJSON(), true); + + $this->assertFalse($json['status'] ?? true); + $this->assertSame('Input validation failed', $json['message'] ?? ''); + $this->assertArrayHasKey('client_id', $json['errors'] ?? []); + $this->assertArrayHasKey('client_policy_id', $json['errors'] ?? []); + $this->assertArrayHasKey('approved_by', $json['errors'] ?? []); + $this->assertArrayHasKey('description', $json['errors'] ?? []); + $this->assertArrayHasKey('amount', $json['errors'] ?? []); + $this->assertArrayHasKey('expense_date', $json['errors'] ?? []); + } + + public function testCreateExpenseValidationFailureInvalidDescription(): void + { + $payload = [ + 'client_id' => '1', + 'client_policy_id' => '1', + 'approved_by' => '1', + 'description' => 'Invalid @ Description <>', // invalid characters + 'expense_date' => '12-01-2026', + 'amount' => '100.00', + ]; + + $result = $this->post('expense/save', $payload); + + $result->assertStatus(400); + + $json = json_decode($result->getJSON(), true); + + $this->assertFalse($json['status'] ?? true); + $this->assertArrayHasKey('description', $json['errors'] ?? []); + $this->assertStringContainsString( + 'invalid characters', + strtolower($json['errors']['description'] ?? '') + ); + } + + public function testUpdateExpenseSuccess(): void + { + $id = $this->createExpenseRecord([ + 'description' => 'CI4_TEST_Original Description', + 'amount' => 50.00, + ]); + + $payload = [ + 'id' => (string) $id, + 'client_id' => '1', + 'client_policy_id'=> '1', + 'approved_by' => '1', + 'description' => 'Updated Description 456', + 'expense_date' => '13-01-2026', + 'amount' => '75.50', + ]; + + $result = $this->post('expense/save', $payload); + + $result->assertStatus(200); + + $json = json_decode($result->getJSON(), true); + + $this->assertTrue($json['status'] ?? false); + $this->assertSame($id, (int) ($json['id'] ?? 0)); + + $db = db_connect('default'); + $row = $db->table('expenses')->where('id', $id)->get()->getRowArray(); + + $this->assertNotEmpty($row); + $this->assertSame('Updated Description 456', $row['description']); + $this->assertEquals(75.50, (float) $row['amount']); + } + + public function testUpdateExpenseValidationFailureInvalidAmount(): void + { + $id = $this->createExpenseRecord(); + + $payload = [ + 'id' => (string) $id, + 'client_id' => '1', + 'client_policy_id'=> '1', + 'approved_by' => '1', + 'description' => 'Another Valid Description', + 'expense_date' => '14-01-2026', + 'amount' => '-10', // invalid negative + ]; + + $result = $this->post('expense/save', $payload); + + $result->assertStatus(400); + + $json = json_decode($result->getJSON(), true); + + $this->assertFalse($json['status'] ?? true); + $this->assertArrayHasKey('amount', $json['errors'] ?? []); + $this->assertStringContainsString( + 'cannot be negative', + strtolower($json['errors']['amount'] ?? '') + ); + } + + public function testDeleteExpenseSoftDeleteSuccess(): void + { + $id = $this->createExpenseRecord(); + + $result = $this->post('expense/delete/' . $id); + + $result->assertStatus(200); + + $json = json_decode($result->getJSON(), true); + + $this->assertTrue($json['status'] ?? false); + + $db = db_connect(); + $row = $db->table('expenses')->where('id', $id)->get()->getRowArray(); + + $this->assertNotEmpty($row); + $this->assertSame(0, (int) $row['is_active']); + } + + public function testDeleteExpenseInvalidId(): void + { + $result = $this->post('expense/delete/0'); + + $result->assertStatus(400); + + $json = json_decode($result->getJSON(), true); + + $this->assertFalse($json['status'] ?? true); + $this->assertSame('Invalid expense id', $json['message'] ?? ''); + } + + public function testSearchFilterByClientAndDescription(): void + { + // Create two distinct expenses + $this->createExpenseRecord([ + 'client_id' => 10, + 'description' => 'FilterMatch Description', + ]); + + $this->createExpenseRecord([ + 'client_id' => 20, + 'description' => 'Other Description', + ]); + + $result = $this->get('expense?client_id=10&description=FilterMatch'); + + $result->assertStatus(200); + + $body = $result->getBody(); + + $this->assertStringContainsString('FilterMatch Description', $body); + $this->assertStringNotContainsString('Other Description', $body); + } + + public function testSearchFilterValidationInvalidCharacters(): void + { + $result = $this->get('expense?description='); + + $result->assertStatus(200); + + $body = $result->getBody(); + + $this->assertStringContainsString('Description filter contains invalid characters.', $body); + } +} + +} + + From e2739bc4bd7493894d1ad92861b9ed63376f61ff Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Mon, 2 Mar 2026 10:35:29 +0530 Subject: [PATCH 07/12] FIX_ISSUE --- app/Controllers/ExpenseController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Controllers/ExpenseController.php b/app/Controllers/ExpenseController.php index cf9a1bbe..c734284d 100644 --- a/app/Controllers/ExpenseController.php +++ b/app/Controllers/ExpenseController.php @@ -118,7 +118,7 @@ class ExpenseController extends AdminController ->select('id, policy_no') ->where('client_id', (int) $filters['client_id']) ->where('is_active', 1) - ->orderBy('policy_no', 'ASC') + ->orderBy('id', 'ASC') ->findAll(); } @@ -131,7 +131,7 @@ class ExpenseController extends AdminController client_policy.policy_no, user_profiles.first_name AS approved_by_name ') - ->join('clients', 'clients.id = expenses.client_id', 'left') + ->join('clients', 'clients.id = expenses.client_id') ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left') ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left') ->where('expenses.is_active', 1); From 53c4c845d37e61bbe83e94460d32650a59596fea Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Mon, 2 Mar 2026 13:44:19 +0530 Subject: [PATCH 08/12] FEAT_DAILY_BDS_REPORT --- .env.sample | 5 +- app/Config/Routes.php | 3 + .../PolicyTransactionController.php | 182 +++++++++++++++++- app/Libraries/MyGoogleDrive.php | 2 +- 4 files changed, 189 insertions(+), 3 deletions(-) diff --git a/.env.sample b/.env.sample index 49343c62..50244384 100755 --- a/.env.sample +++ b/.env.sample @@ -129,4 +129,7 @@ HEALTH_INDIA_TOKEN_URL = HEALTH_INDIA_USERNAME = HEALTH_INDIA_PASSWORD = -HEALTH_INDIA_PRIMARY_KEY_CONSTANT = \ No newline at end of file +HEALTH_INDIA_PRIMARY_KEY_CONSTANT = + +# BDS Daily Report Emails Configuration +bds.dailyReportEmails = diff --git a/app/Config/Routes.php b/app/Config/Routes.php index cf88f2b9..e07b1e01 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -451,6 +451,8 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); $routes->cli("cli/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); +$routes->cli("cli/cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport"); + $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { @@ -498,6 +500,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { //$routes->post('failedStatement',"PolicyTransactionController::failedStatementList"); }); + $routes->get("cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport"); }); $routes->group("leads", ["filter" => "authMVC"], function ($routes) { diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 59c115fe..3dfd21b6 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -6098,8 +6098,188 @@ class PolicyTransactionController extends BaseController return array('status' => false, 'message' => 'file_id is required'); } - $this->policyTransactionModel->select()->findAll(); + $data['is_active'] = 0; + $this->policyTransactionModel->where('file_id', $file_id)->set($data)->update(); + $this->PTCOShareDetailsModel->where('file_id', $file_id)->set($data)->update(); + return $this->respond(['status' => true, 'code' => 200, 'message' => 'BDS bulk upload data truncated successfully'], 200); + } + + // ------------------------------------------------------------------------------------------------------------------ + + /** + * Cron job: Daily BDS Report + * Fetches today's BDS entries, generates Excel (matching report_bds.php column order), and emails to recipients from .env. + * Recipients: comma-separated emails in bds.reportEmails + * File stored temporarily in writable/tmp/ and deleted after sending. + */ + public function cronDailyBDSReport() + { + + helper('excel_import_export_helper'); + + $filePath = null; + try { + + $today = date('Y-m-d'); + $reportList = $this->policyTransactionModel->getBDSReportList($today, $today,0,0,0,'created_at',0,0,0,0,0,[]); + + if (empty($reportList)) { + $this->myLogger->logme('error', "cronDailyBDSReport: No BDS records for {$today}"); + return $this->respond(['status' => 'success', 'message' => 'No BDS records for today. No report sent.'], 200); + } + + // Column headers exactly matching report_bds.php order (including display:none columns) + $headers = [ + 'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type', + 'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch', + 'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks', + 'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %', + 'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount', + 'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date', + 'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager', + 'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)', + 'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST', + 'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount', + 'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount', + 'CD Account No' + ]; + + $excelData = []; + foreach ($reportList as $idx => $row) { + $totalIrda = (float)($row['total_irda_amt'] ?? 0); + $billedAmt = (float)($row['billed_amt'] ?? 0); + $unbilledAmt = $totalIrda - $billedAmt; + if ($totalIrda == 0) { + $unbilledAmt = abs($unbilledAmt); + } + $unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt; + + $hasIrda = ($totalIrda != 0); + + $excelData[] = [ + $idx + 1, + $row['user_name'] ?? 'N/A', + $row['policy_issue_month'] ?? 'N/A', + $row['revenue_type'] ?? 'N/A', + $row['client_type'] ?? 'N/A', + $row['client_name'] ?? 'N/A', + $row['action_type'] ?? 'N/A', + $row['policy_type'] ?? 'N/A', + $row['bap'] ?? 'N/A', + $row['vehicle_no'] ?? 'N/A', + $row['policy_no'] ?? 'N/A', + $row['endorsement_no'] ?? 'N/A', + $row['insurer_branch_name'] ?? 'N/A', + !empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['ref'] ?? 'N/A', + $row['remarks'] ?? 'N/A', + $hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00', + $hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00', + $hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00', + $hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00', + $hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%', + $hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%', + $row['reward'] ?? '0.00', + $row['total_irda_amt'] ?? '0.00', + !empty($row['billed_amt']) ? $row['billed_amt'] : '0.00', + number_format((float)$unbilledAmt, 2, '.', ''), + $row['salse_person_name'] ?? 'N/A', + $row['service_person_name'] ?? 'N/A', + $row['nhance_branch'] ?? 'N/A', + $row['installment'] ?? 'N/A', + !empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['co_share'] ?? 'No', + $row['bro_payable_by'] ?? 'No', + $row['salse_manager_name'] ?? 'N/A', + $row['service_manager_name'] ?? 'N/A', + $row['service_branch'] ?? 'N/A', + !empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['policy_holder_name'] ?? 'N/A', + $row['same_as_proposer'] ?? 'No', + $row['follower_policy_no'] ?? 'N/A', + number_format((float)($row['co_share_per'] ?? 0), 2), + number_format((float)($row['non_comm_per_amt'] ?? 0), 2), + number_format((float)($row['bp_cgst'] ?? 0), 2), + number_format((float)($row['bp_sgst'] ?? 0), 2), + number_format((float)($row['bp_igst'] ?? 0), 2), + number_format((float)($row['stamp_duty'] ?? 0), 2), + number_format((float)($row['standerd_bp_per'] ?? 0), 2), + number_format((float)($row['standerd_tp_per'] ?? 0), 2), + number_format((float)($row['actual_bp_amt'] ?? 0), 2), + number_format((float)($row['actual_tp_amt'] ?? 0), 2), + number_format((float)($row['actual_bp_per'] ?? 0), 2), + number_format((float)($row['actual_tp_per'] ?? 0), 2), + number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2), + number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2), + $row['cd_ac_no'] ?? 'N/A' + ]; + } + + $fileName = 'BDS_Daily_Report_' . $today . '.xlsx'; + $tmpDir = WRITEPATH . 'tmp' . DIRECTORY_SEPARATOR; + if (!is_dir($tmpDir)) { + mkdir($tmpDir, 0755, true); + } + $filePath = $tmpDir . $fileName; + + $generated = generate_excel($headers, $excelData, $filePath); + if (!$generated) { + $this->myLogger->logme('error', 'cronDailyBDSReport: Excel generation failed'); + return $this->respond(['status' => false, 'message' => 'Excel generation failed'], 500); + } + + $emailList = getenv('bds.dailyReportEmails') ?: ''; + $recipientEmails = array_filter(array_map('trim', explode(',', $emailList))); + if (empty($recipientEmails)) { + @unlink($filePath); + $this->myLogger->logme('error', 'cronDailyBDSReport: No recipients in bds.dailyReportEmails. Report generated but not sent.'); + return $this->respond([ + 'status' => 'success', + 'message' => 'Report generated. No recipients configured (set bds.dailyReportEmails in .env).', + 'file' => $fileName + ], 200); + } + + $subject = "Daily BDS Report - {$today}"; + $message = "

    Please find attached the daily BDS report for {$today}.

    "; + $message .= "

    Total records: " . count($reportList) . "

    "; + + $attachments = [ + ['filePath' => $filePath, 'fileName' => $fileName] + ]; + + $res = MailHelper::send_email([ + 'mail' => $recipientEmails, + 'subject' => $subject, + 'message' => $message, + 'attachments' => $attachments + ]); + + @unlink($filePath); + + $resDecoded = is_string($res) ? json_decode($res, true) : $res; + if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') { + $this->myLogger->logme('error', "cronDailyBDSReport: Report sent to " . count($recipientEmails) . " recipients"); + return $this->respond([ + 'status' => true, + 'message' => 'Report generated and emailed successfully.', + 'recipients' => count($recipientEmails) + ], 200); + } + + $this->myLogger->logme('error', 'cronDailyBDSReport: Email send failed - ' . json_encode($resDecoded)); + return $this->respond(['status' => false, 'message' => 'Report generated but email send failed'], 500); + } catch (Exception $e) { + if ($filePath && is_file($filePath)) { + @unlink($filePath); + } + $this->myLogger->logme('error', 'cronDailyBDSReport Exception: ' . $e->getMessage()); + return $this->respond(['status' => false, 'message' => $e->getMessage()], 500); + } } } diff --git a/app/Libraries/MyGoogleDrive.php b/app/Libraries/MyGoogleDrive.php index 566098eb..2fea0f9e 100644 --- a/app/Libraries/MyGoogleDrive.php +++ b/app/Libraries/MyGoogleDrive.php @@ -16,7 +16,7 @@ class MyGoogleDrive { $this->myLogger = \Config\Services::mylogger(); $this->client = new Google_Client(); - $this->client->setAuthConfig(ROOTPATH . 'nhance-app-google-drive.json'); // App credentials + $this->client->setAuthConfig(ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'); // App credentials // putenv('GOOGLE_APPLICATION_CREDENTIALS=' . ROOTPATH . 'nhance-app-google-drive.json'); $this->client->useApplicationDefaultCredentials(); From 3b357901e00ab95c33cba444c0534ca72af43709 Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Mon, 2 Mar 2026 15:25:39 +0530 Subject: [PATCH 09/12] CHANGE_HR_CLAIM_TYPE --- app/Controllers/EmployeeRestController.php | 5 + .../unit/PolicyTransactionControllerTest.php | 278 ++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 tests/unit/PolicyTransactionControllerTest.php diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 3a0bea5b..73a1f1bd 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2418,6 +2418,11 @@ class EmployeeRestController extends AdminController ["ticket_type" => "4", "type_name" => "GTLI"], ]; + $claim_type = $this->ticketController->claimType; + unset($claim_type[1][2]); + unset($claim_type[1][4]); + $data['claim_type'] = $claim_type; + return $this->respond(['status' => (count($data) ? 'success' : 'failed'), 'code' => (count($data) ? 200 : 404), 'data' => $data], 200); } diff --git a/tests/unit/PolicyTransactionControllerTest.php b/tests/unit/PolicyTransactionControllerTest.php new file mode 100644 index 00000000..50abd143 --- /dev/null +++ b/tests/unit/PolicyTransactionControllerTest.php @@ -0,0 +1,278 @@ +initController($request, $response, $logger); + + return $controller; + } + + /** + * Helper to inject a stubbed PolicyTransactionModel into the controller. + * + * @param PolicyTransactionController $controller + * @param array $stubData + */ + protected function injectBdsStubModel(PolicyTransactionController $controller, array $stubData): void + { + $stubModel = new class($stubData) + { + private array $data; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function getBDSReportList( + $start_date = 0, + $end_date = 0, + $client_id = 0, + $insurer_id = 0, + $policy_type_id = 0, + $date_type = 0, + $issuer = 0, + $client_branch_id = 0, + $insurer_branch_id = 0, + $client_policy_id = 0, + $user_id = 0, + $where = [], + ) { + return $this->data; + } + }; + + $refClass = new \ReflectionClass($controller); + $prop = $refClass->getProperty('policyTransactionModel'); + $prop->setAccessible(true); + $prop->setValue($controller, $stubModel); + } + + public function testCronDailyBDSReportReturnsUnauthorizedWithoutKey(): void + { + putenv('cron.secretKey=unit-test-secret'); + + $_GET = []; + + $controller = $this->makeController(); + + $response = $controller->cronDailyBDSReport(); + + $this->assertSame(401, $response->getStatusCode()); + + $body = json_decode($response->getBody(), true); + $this->assertIsArray($body); + $this->assertSame('error', $body['status'] ?? null); + } + + public function testCronDailyBDSReportNoRecordsReturnsSuccessMessage(): void + { + putenv('cron.secretKey=unit-test-secret'); + putenv('bds.reportEmails='); // no recipients for this test + + $_GET['key'] = 'unit-test-secret'; + + $controller = $this->makeController(); + + $this->injectBdsStubModel($controller, []); + + $response = $controller->cronDailyBDSReport(); + + $this->assertSame(200, $response->getStatusCode()); + + $body = json_decode($response->getBody(), true); + $this->assertIsArray($body); + $this->assertSame('success', $body['status'] ?? null); + $this->assertStringContainsString( + 'No BDS records for today', + $body['message'] ?? '' + ); + } + + /** + * Integration-style test: generate an Excel file for today's BDS data + * and store it in the user's Downloads folder as BDS_DAILY_REPORT_TEST_CASE.xlsx. + * + * This reuses the same transformation logic as cronDailyBDSReport. + */ + public function testGenerateDailyBdsReportExcelToDownloads(): void + { + helper(['excel_import_export_helper', 'utility_helper']); + + // Sample BDS row to generate a test Excel (no DB dependency) + $today = date('Y-m-d'); + $reportList = [ + [ + 'user_name' => 'Test User', + 'policy_issue_month' => 'Mar 2026', + 'revenue_type' => 'Fresh', + 'client_type' => 'Group', + 'client_name' => 'Test Client', + 'action_type' => 'Policy', + 'policy_type' => 'Health', + 'bap' => 'BAP-GRP', + 'vehicle_no' => 'TN01AB1234', + 'policy_no' => 'P-TEST-001', + 'endorsement_no' => 'E-TEST-001', + 'insurer_branch_name' => 'Chennai Branch', + 'endorse_eff_date' => $today, + 'policy_start_date' => $today, + 'policy_end_date' => $today, + 'ref' => 'REF-001', + 'remarks' => 'Test remark', + 'bp_amt' => 1000, + 'tp_or_ter' => 500, + 'premium_wo_gst' => 1500, + 'total_premium' => 1770, + 'agreed_bp_per' => 10, + 'agreed_tp_or_ter_per' => 5, + 'reward' => 100, + 'total_irda_amt' => 800, + 'billed_amt' => 300, + 'salse_person_name' => 'Sales Person', + 'service_person_name' => 'Service Person', + 'nhance_branch' => 'Nhance Chennai', + 'installment' => '1', + 'data_received_date' => $today, + 'renewal_date' => $today, + 'co_share' => 'No', + 'bro_payable_by' => 'No', + 'salse_manager_name' => 'Sales Manager', + 'service_manager_name' => 'Service Manager', + 'service_branch' => 'Service Branch', + 'rollover_date' => $today, + 'policy_holder_name' => 'Holder Name', + 'same_as_proposer' => 'Yes', + 'follower_policy_no' => 'F-TEST-001', + 'co_share_per' => 0, + 'non_comm_per_amt' => 0, + 'bp_cgst' => 0, + 'bp_sgst' => 0, + 'bp_igst' => 0, + 'stamp_duty' => 0, + 'standerd_bp_per' => 0, + 'standerd_tp_per' => 0, + 'actual_bp_amt' => 0, + 'actual_tp_amt' => 0, + 'actual_bp_per' => 0, + 'actual_tp_per' => 0, + 'actual_tep_brokerage_amt' => 0, + 'actual_tp_brokerage_amt' => 0, + 'cd_ac_no' => 'CD-001', + ], + ]; + + $headers = [ + 'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type', + 'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch', + 'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks', + 'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %', + 'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount', + 'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date', + 'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager', + 'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)', + 'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST', + 'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount', + 'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount', + 'CD Account No' + ]; + + $excelData = []; + foreach ($reportList as $idx => $row) { + $totalIrda = (float)($row['total_irda_amt'] ?? 0); + $billedAmt = (float)($row['billed_amt'] ?? 0); + $unbilledAmt = $totalIrda - $billedAmt; + if ($totalIrda == 0) { + $unbilledAmt = abs($unbilledAmt); + } + $unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt; + + $hasIrda = ($totalIrda != 0); + + $excelData[] = [ + $idx + 1, + $row['user_name'] ?? 'N/A', + $row['policy_issue_month'] ?? 'N/A', + $row['revenue_type'] ?? 'N/A', + $row['client_type'] ?? 'N/A', + $row['client_name'] ?? 'N/A', + $row['action_type'] ?? 'N/A', + $row['policy_type'] ?? 'N/A', + $row['bap'] ?? 'N/A', + $row['vehicle_no'] ?? 'N/A', + $row['policy_no'] ?? 'N/A', + $row['endorsement_no'] ?? 'N/A', + $row['insurer_branch_name'] ?? 'N/A', + !empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['ref'] ?? 'N/A', + $row['remarks'] ?? 'N/A', + $hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00', + $hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00', + $hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00', + $hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00', + $hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%', + $hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%', + $row['reward'] ?? '0.00', + $row['total_irda_amt'] ?? '0.00', + !empty($row['billed_amt']) ? $row['billed_amt'] : '0.00', + number_format((float)$unbilledAmt, 2, '.', ''), + $row['salse_person_name'] ?? 'N/A', + $row['service_person_name'] ?? 'N/A', + $row['nhance_branch'] ?? 'N/A', + $row['installment'] ?? 'N/A', + !empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + !empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['co_share'] ?? 'No', + $row['bro_payable_by'] ?? 'No', + $row['salse_manager_name'] ?? 'N/A', + $row['service_manager_name'] ?? 'N/A', + $row['service_branch'] ?? 'N/A', + !empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A', + $row['policy_holder_name'] ?? 'N/A', + $row['same_as_proposer'] ?? 'No', + $row['follower_policy_no'] ?? 'N/A', + number_format((float)($row['co_share_per'] ?? 0), 2), + number_format((float)($row['non_comm_per_amt'] ?? 0), 2), + number_format((float)($row['bp_cgst'] ?? 0), 2), + number_format((float)($row['bp_sgst'] ?? 0), 2), + number_format((float)($row['bp_igst'] ?? 0), 2), + number_format((float)($row['stamp_duty'] ?? 0), 2), + number_format((float)($row['standerd_bp_per'] ?? 0), 2), + number_format((float)($row['standerd_tp_per'] ?? 0), 2), + number_format((float)($row['actual_bp_amt'] ?? 0), 2), + number_format((float)($row['actual_tp_amt'] ?? 0), 2), + number_format((float)($row['actual_bp_per'] ?? 0), 2), + number_format((float)($row['actual_tp_per'] ?? 0), 2), + number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2), + number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2), + $row['cd_ac_no'] ?? 'N/A' + ]; + } + + $filePath = '/home/venkat/Downloads/BDS_DAILY_REPORT_TEST_CASE.xlsx'; + + $generated = generate_excel($headers, $excelData, $filePath); + + $this->assertTrue($generated, 'Failed to generate BDS_DAILY_REPORT_TEST_CASE.xlsx in Downloads'); + } +} + From e18831785f238e314934598364f531d097923c0b Mon Sep 17 00:00:00 2001 From: "venkatesh.r" Date: Wed, 4 Mar 2026 11:49:57 +0530 Subject: [PATCH 10/12] FEAT_DAILY_ACTIVITY_REPORT_AND_OTHER_ISSUES_FIXES --- app/Config/Routes.php | 4 +- app/Controllers/ClientController.php | 4 +- app/Controllers/DashboardController.php | 314 ++++++++++++++++ app/Controllers/EmployeeRestController.php | 105 ++++-- .../PolicyTransactionController.php | 9 +- app/Controllers/TestingController.php | 4 +- app/Controllers/TicketController.php | 20 +- .../BaseTpaClaimImportService.php | 27 ++ app/Models/PolicyTransactionModel.php | 2 + app/Views/daily_report_email_template.php | 347 ++++++++++++++++++ 10 files changed, 785 insertions(+), 51 deletions(-) create mode 100644 app/Views/daily_report_email_template.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e07b1e01..cf5fe44e 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -446,12 +446,14 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable'); $routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend'); $routes->get('getTpaClaimDumpErrorData/(:any)', 'TicketServiceController::getTpaClaimDumpErrorData/$1'); - + $routes->get('croneDailyActivityReport', 'DashboardController::croneDailyActivityReport'); }); $routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); $routes->cli("cli/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail"); $routes->cli("cli/cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport"); +$routes->cli('cli/croneDailyActivityReport', 'DashboardController::croneDailyActivityReport'); + $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 6d2064fa..ee3a8098 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -6975,7 +6975,9 @@ class ClientController extends AdminController // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 53]); //icici // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 54]); //mediassist // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 52]); //reliance - // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal + // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal` + // $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 57]); //mediassist + // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi // $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 1dab325b..b4d224fe 100755 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -7,6 +7,7 @@ use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; use App\Helpers\sendMailNotification; +use App\Helpers\MailHelper; use CodeIgniter\API\ResponseTrait; @@ -24,6 +25,11 @@ use App\Controllers\EmpDataServiceController; use App\Models\TicketMasterModel; use App\Models\LeadsModel; use App\Models\TicketClaimStatusModel; +use App\Models\FileModel; +use App\Models\BatchFileModel; +use App\Models\SalesActivityModel; +use App\Models\SalesActualLeadModel; + class DashboardController extends AdminController @@ -43,6 +49,13 @@ class DashboardController extends AdminController protected $policyStatus; protected $colorShades; protected $claimDashLimit; + protected $filesModel; + protected $batchFilesModel; + protected $leadsModel; + protected $ticketMasterModel; + protected $ticketClaimStatusModel; + protected $salesActivityModel; + protected $salesActualModel; protected $myLogger; @@ -59,6 +72,13 @@ class DashboardController extends AdminController $this->ticketModel = new TicketMasterModel(); $this->leadModel = new LeadsModel(); $this->ticketStatusModel = new TicketClaimStatusModel(); + $this->filesModel = new FileModel(); + $this->batchFilesModel = new BatchFileModel(); + $this->leadsModel = new LeadsModel(); + $this->ticketMasterModel = new TicketMasterModel(); + $this->ticketClaimStatusModel = new TicketClaimStatusModel(); + $this->salesActivityModel = new SalesActivityModel(); + $this->salesActualModel = new SalesActualLeadModel(); $this->myLogger = \Config\Services::mylogger(); @@ -787,4 +807,298 @@ class DashboardController extends AdminController $data['ticket_type_id'] = $ticketTypeId; return $this->respond(['status' => "success", "data" => $data], 200); } + + public function croneDailyActivityReport() + { + + // $today = date('Y-m-d'); + $today = date('Y-m-d', strtotime('-1 day')); + + // Inception & endorsement counts (file uploads) + $total_inception_count = $this->filesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('action', 'inception') + ->where('status', 'success') + ->countAllResults(); + + $total_endorsement_count = $this->filesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('action !=', 'inception') + ->where('status', 'success') + ->countAllResults(); + + // TPA & Insurer batch file counts + $total_tpa_incetion_count = $this->batchFilesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('event_type', 'inception') + ->where('insurer_or_tpa', 'tpa') + ->where('status', 'success') + ->countAllResults(); + + $total_tpa_endorsement_count = $this->batchFilesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('event_type !=', 'inception') + ->where('insurer_or_tpa', 'tpa') + ->where('status', 'success') + ->countAllResults(); + + $total_insurer_incetion_count = $this->batchFilesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('event_type', 'inception') + ->where('insurer_or_tpa', 'insurer') + ->where('status', 'success') + ->countAllResults(); + + $total_insurer_endorsement_count = $this->batchFilesModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('event_type !=', 'inception') + ->where('insurer_or_tpa', 'insurer') + ->where('status', 'success') + ->countAllResults(); + + // Claim counts + $total_claim_count = $this->ticketMasterModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->countAllResults(); + + $total_gmc_status_wise_claim_count = $this->ticketMasterModel + ->select('tcs.claim_status, count(*) as count') + ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left') + ->where('ticket_master.is_active', 1) + ->where('ticket_master.ticket_type_id', 1) + // ->where('DATE(ticket_master.created_at)', $today) + ->where('tcs.is_active', 1) + ->groupBy('tcs.claim_status') + ->findAll(); + + $total_gpa_status_wise_claim_count = $this->ticketMasterModel + ->select('tcs.claim_status, count(*) as count') + ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left') + ->where('ticket_master.is_active', 1) + ->where('ticket_master.ticket_type_id', 2) + // ->where('DATE(ticket_master.created_at)', $today) + ->where('tcs.is_active', 1) + ->groupBy('tcs.claim_status') + ->findAll(); + + $total_edli_status_wise_claim_count = $this->ticketMasterModel + ->select('tcs.claim_status, count(*) as count') + ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left') + ->where('ticket_master.is_active', 1) + ->where('ticket_master.ticket_type_id', 3) + // ->where('DATE(ticket_master.created_at)', $today) + ->where('tcs.is_active', 1) + ->groupBy('tcs.claim_status') + ->findAll(); + + $total_gtli_status_wise_claim_count = $this->ticketMasterModel + ->select('tcs.claim_status, count(*) as count') + ->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left') + ->where('ticket_master.is_active', 1) + ->where('ticket_master.ticket_type_id', 4) + // ->where('DATE(ticket_master.created_at)', $today) + ->where('tcs.is_active', 1) + ->groupBy('tcs.claim_status') + ->findAll(); + + // Sales / lead counts + $total_opportunity_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->countAllResults(); + + $total_rfq_created_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('status', 'rfq_created') + ->countAllResults(); + + $total_rfq_insurer_send_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('status', 'rfq_sent') + ->countAllResults(); + + $total_qcr_created_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('status', 'qcr_created') + ->countAllResults(); + + $total_qcr_client_send_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('status', 'qcr_sent') + ->countAllResults(); + + $total_placement_count = $this->leadsModel + ->where('is_active', 1) + ->where('DATE(created_at)', $today) + ->where('status', 'won') + ->countAllResults(); + + $total_activity_count = $this->salesActivityModel + ->where('DATE(created_at)', $today) + ->countAllResults(); + + $total_lead_count = $this->salesActualModel + ->where('DATE(created_at)', $today) + ->countAllResults(); + + $total_bds_count = $this->policyTransactionModel + ->select('pt_co_share_details.*') + ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id') + ->where('DATE(policy_transaction.created_at)', $today) + ->where('pt_co_share_details.is_active', 1) + ->where('policy_transaction.is_active', 1) + ->countAllResults(); + + $total_bds_policy_wise_count = $this->policyTransactionModel + ->select('pt_co_share_details.*') + ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id') + ->where('DATE(policy_transaction.created_at)', $today) + ->where('pt_co_share_details.is_active', 1) + ->where('policy_transaction.action_type', 'inception') + ->where('policy_transaction.is_active', 1) + ->countAllResults(); + + + $total_bds_endorsement_wise_count = $this->policyTransactionModel + ->select('pt_co_share_details.*') + ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id') + ->where('DATE(policy_transaction.created_at)', $today) + ->where('pt_co_share_details.is_active', 1) + ->where('policy_transaction.action_type !=', 'inception') + ->where('policy_transaction.is_active', 1) + ->countAllResults(); + + $total_bds_policy_type_wise_count = $this->policyTransactionModel + ->select('policy_type.policy_type, count(*) as count') + ->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id') + ->join('policy_type', 'policy_transaction.policy_type_id = policy_type.id') + ->where('DATE(policy_transaction.created_at)', $today) + ->where('pt_co_share_details.is_active', 1) + ->where('policy_transaction.is_active', 1) + ->groupBy('policy_transaction.policy_type_id') + ->findAll(); + + // Connect Pre DB for Employee Enrollment Count + + $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') + ->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') + ->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') + ->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') + ->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) + ->where('is_active', 1) + ->where('status', 'success') + ->countAllResults(); + + $data = [ + 'total_inception_count' => $total_inception_count, + 'total_endorsement_count' => $total_endorsement_count, + 'total_tpa_incetion_count' => $total_tpa_incetion_count, + 'total_tpa_endorsement_count' => $total_tpa_endorsement_count, + 'total_insurer_incetion_count' => $total_insurer_incetion_count, + 'total_insurer_endorsement_count' => $total_insurer_endorsement_count, + 'total_claim_count' => $total_claim_count, + 'total_gmc_status_wise_claim_count' => $total_gmc_status_wise_claim_count, + 'total_gpa_status_wise_claim_count' => $total_gpa_status_wise_claim_count, + 'total_edli_status_wise_claim_count' => $total_edli_status_wise_claim_count, + 'total_gtli_status_wise_claim_count' => $total_gtli_status_wise_claim_count, + 'total_opportunity_count' => $total_opportunity_count, + 'total_rfq_created_count' => $total_rfq_created_count, + 'total_rfq_insurer_send_count' => $total_rfq_insurer_send_count, + 'total_qcr_created_count' => $total_qcr_created_count, + 'total_qcr_client_send_count' => $total_qcr_client_send_count, + 'total_placement_count' => $total_placement_count, + 'total_activity_count' => $total_activity_count, + 'total_lead_count' => $total_lead_count, + 'total_bds_count' => $total_bds_count, + 'total_bds_policy_wise_count' => $total_bds_policy_wise_count, + 'total_bds_endorsement_wise_count' => $total_bds_endorsement_wise_count, + 'total_bds_policy_type_wise_count' => $total_bds_policy_type_wise_count, + 'total_employee_draft_count' => $total_employee_draft_count, + 'total_employee_enrolled_count' => $total_employee_enrolled_count, + 'total_open_for_enrollemnt_policy_count' => $total_open_for_enrollemnt_policy_count, + ]; + + // dd($data); + + $today = date('d-m-Y', strtotime($today)); + $data['today'] = $today; + + // Render the HTML email using the dedicated view + $message = view('daily_report_email_template', $data); + // return $message; + + // Recipients: prefer dedicated env, fallback to BDS report emails if not set + $emailList = getenv('activity.dailyReportEmails') ?: getenv('bds.dailyReportEmails') ?: ''; + $recipientEmails = array_filter(array_map('trim', explode(',', $emailList))); + + if (empty($recipientEmails)) { + $this->myLogger->logme('error', 'croneDailyActivityReport: No recipients configured (set activity.dailyReportEmails in .env).'); + return $this->respond([ + 'status' => 'success', + 'message' => 'Daily activity data prepared but no recipients configured.', + 'data' => $data, + ], 200); + } + + $subject = "Daily Activity Report - {$today}"; + + $res = MailHelper::send_email([ + 'mail' => $recipientEmails, + 'subject' => $subject, + 'message' => $message, + ]); + + $resDecoded = is_string($res) ? json_decode($res, true) : $res; + + if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') { + $this->myLogger->logme('error', 'croneDailyActivityReport: Report email sent to ' . count($recipientEmails) . ' recipients'); + return $this->respond([ + 'status' => true, + 'message' => 'Daily activity report emailed successfully.', + 'recipients' => count($recipientEmails), + ], 200); + } + + $this->myLogger->logme('error', 'croneDailyActivityReport: Email send failed - ' . json_encode($resDecoded)); + return $this->respond([ + 'status' => false, + 'message' => 'Daily activity data prepared but email send failed.', + 'data' => $data, + ], 500); + } + } diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 73a1f1bd..df5dcfab 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2586,61 +2586,90 @@ class EmployeeRestController extends AdminController $required_docs = $this->ticketMaster->select('required_docs')->where('id', $ticket_id)->first(); $data['required_docs'] = json_decode($required_docs['required_docs'] ?? '{}', true) ?? []; - // $ticketData = $data['ticket_data']; - // $ticketHistory = $data['ticket_history']; - // print_r($ticketHistory); die; + // print_rr($data['ticket_history']); die; + + $filteredArray = array_filter($data['ticket_history'], function($item) { + return $item['field_name'] == 'claim_status_id'; + }); + + $filteredArray = array_reverse(array_values($filteredArray)); $currentClaimStatus = $this->claimStatusModel->select("claim_status")->where('id', $data['claims_data']['claim_status_id'])->where('is_active', 1)->first(); $ticketClaimStatus = $this->claimStatusModel->select('claim_status, display_name')->where('display_name is not null')->where('ticket_type', $data['claims_data']['ticket_type_id'])->where('is_active', 1)->findAll(); $status_list = array_column($ticketClaimStatus, 'display_name', 'claim_status'); - // print_r($currentClaimStatus); die; + // dd($filteredArray, $status_list); die; - $data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []); + $filtered_history = []; + $counter = 0; + + foreach ($filteredArray as $key => $value) { + foreach ($status_list as $status => $display_name) { + if ($value['new_value'] == $status) { - // Loop through ticket_data - foreach ($data['ticket_data'] as $status => &$fields) { - // Search ticket_history for matching old_status_value - foreach ($data['ticket_history'] as $history) { + if($display_name == 'Under Process'){ + $unique_key = $display_name . str_repeat("\u{200B}", $counter++); + }else{ + $unique_key = $display_name; + } - if ($history['old_status_value'] === $status) { - // Attach modified_by and created_at - // $fields['modified_by'] = $history['modified_by']; - $fields['modified_by'] = ""; - $fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at'])); - // Break after first match (assuming latest entry is enough) - break; + $filtered_history[$unique_key] = [ + 'modified_by' => "", + 'modified_at' => date('d-m-Y h:i A', strtotime($value['created_at'])), + ]; } } } + // dd($filteredArray, $status_list, $filtered_history); die; - $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = ""; - $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at'])); - $new_ticket_data = []; - foreach ($data['ticket_data'] as $oldKey => $value) { + // $data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []); - // Only process if key exists in status_list - if (! isset($status_list[$oldKey])) { - continue; // skip and do NOT add to new array - } + // // Loop through ticket_data + // foreach ($data['ticket_data'] as $status => &$fields) { + // // Search ticket_history for matching old_status_value + // foreach ($data['ticket_history'] as $history) { - // Get new key based on mapping - $newKey = $status_list[$oldKey]; + // if ($history['old_status_value'] === $status) { + // // Attach modified_by and created_at + // // $fields['modified_by'] = $history['modified_by']; + // $fields['modified_by'] = ""; + // $fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at'])); + // // Break after first match (assuming latest entry is enough) + // break; + // } + // } + // } - // Avoid duplicates - if (! isset($new_ticket_data[$newKey])) { - $new_ticket_data[$newKey] = $value; - } - } + // $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = ""; + // $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at'])); - uasort($new_ticket_data, function ($a, $b) { - $timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']); - $timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']); - return $timeA <=> $timeB; // Ascending - }); + // $new_ticket_data = []; - $data['ticket_data'] = $new_ticket_data; + // foreach ($data['ticket_data'] as $oldKey => $value) { + + // // Only process if key exists in status_list + // if (! isset($status_list[$oldKey])) { + // continue; // skip and do NOT add to new array + // } + + // // Get new key based on mapping + // $newKey = $status_list[$oldKey]; + + // // Avoid duplicates + // if (! isset($new_ticket_data[$newKey])) { + // $new_ticket_data[$newKey] = $value; + // } + // } + + // uasort($new_ticket_data, function ($a, $b) { + // $timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']); + // $timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']); + // return $timeA <=> $timeB; // Ascending + // }); + + // $data['ticket_data'] = $new_ticket_data; + $data['ticket_data'] = $filtered_history; $ticketMesssageModel = new TicketMessageModel(); $ticket_message = $ticketMesssageModel @@ -5456,7 +5485,7 @@ class EmployeeRestController extends AdminController 'dashboard' => $database_id, ], 'exp' => time() + (10 * 60), - 'params' => (object) [], + 'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase ]; $token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256'); diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 3dfd21b6..44ed315b 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -6121,9 +6121,9 @@ class PolicyTransactionController extends BaseController $filePath = null; try { - $today = date('Y-m-d'); + $today = date('Y-m-d', strtotime('-1 day')); $reportList = $this->policyTransactionModel->getBDSReportList($today, $today,0,0,0,'created_at',0,0,0,0,0,[]); - + if (empty($reportList)) { $this->myLogger->logme('error', "cronDailyBDSReport: No BDS records for {$today}"); return $this->respond(['status' => 'success', 'message' => 'No BDS records for today. No report sent.'], 200); @@ -6219,6 +6219,7 @@ class PolicyTransactionController extends BaseController ]; } + $today = date('d-m-Y', strtotime($today)); $fileName = 'BDS_Daily_Report_' . $today . '.xlsx'; $tmpDir = WRITEPATH . 'tmp' . DIRECTORY_SEPARATOR; if (!is_dir($tmpDir)) { @@ -6244,8 +6245,8 @@ class PolicyTransactionController extends BaseController ], 200); } - $subject = "Daily BDS Report - {$today}"; - $message = "

    Please find attached the daily BDS report for {$today}.

    "; + $subject = "BDS Report Up to - {$today}"; + $message = "

    Please find attached the BDS report up to {$today}.

    "; $message .= "

    Total records: " . count($reportList) . "

    "; $attachments = [ diff --git a/app/Controllers/TestingController.php b/app/Controllers/TestingController.php index daae4293..c83fe172 100644 --- a/app/Controllers/TestingController.php +++ b/app/Controllers/TestingController.php @@ -1097,10 +1097,12 @@ class TestingController extends BaseController 'dashboard' => $database_id ], 'exp' => time() + (10 * 60), // 10 minutes - 'params' => (object)[] + 'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase + ]; $token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256'); + // dd($token); if ($this->request->getGet('api') == 1) { return $this->respond([ diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 01109672..7c70ec06 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -1173,9 +1173,13 @@ class TicketController extends BaseController 'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).' ] ], - 'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [ - 'regex_match' => 'TPA ID can only contain letters and numbers.' - ]], + 'tpa_no' => [ + 'label' => 'TPA ID', + 'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]', + 'errors' => [ + 'regex_match' => 'TPA ID can only contain letters, numbers, and /.' + ] + ], 'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]', 'errors' => [ 'required' => 'Mobile number is required', @@ -1522,9 +1526,13 @@ class TicketController extends BaseController 'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).' ] ], - 'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [ - 'regex_match' => 'TPA ID can only contain letters and numbers.' - ]], + 'tpa_no' => [ + 'label' => 'TPA ID', + 'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]', + 'errors' => [ + 'regex_match' => 'TPA ID can only contain letters, numbers, and /.' + ] + ], 'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]', 'errors' => [ 'required' => 'Mobile number is required', diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index 137caf25..3ccbcadd 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -9,17 +9,30 @@ use App\Models\TicketMasterModel; use App\Models\EmployeeModel; use App\Models\ClaimDumpFileModel; use App\Models\ClaimsDumpFhplModel; +use App\Models\ClientPolicyModel; + use RuntimeException; abstract class BaseTpaClaimImportService { protected BaseConnection $db; protected $claimDumpFileModel; + protected $clientPolicyModel; + protected $policyNumberMapping; public function __construct() { $this->db = db_connect(); $this->claimDumpFileModel = new ClaimDumpFileModel(); + $this->clientPolicyModel = new ClientPolicyModel(); + $this->policyNumberMapping = [ + (int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'Insurer Policy Number', + (int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'Policy Number', + (int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'policy_no', + (int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'Policy No', + (int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'Policy Number', + (int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'POLICY_NO', + ]; } /** @@ -33,6 +46,8 @@ abstract class BaseTpaClaimImportService try { $fileData = $this->claimDumpFileModel->where('id', $fileId)->first(); + $client_policy_data = $this->clientPolicyModel->where('id', $fileData['client_policy_id'])->first(); + // Determine sheet name logic... if (env('FHPL_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) { $rows = $this->readExcelBySheetName($filePath, 'Claims&Preauth'); @@ -47,6 +62,18 @@ abstract class BaseTpaClaimImportService return ['status' => false, 'message' => 'Excel file contains no data or wrong file upload']; } + if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){ + $policy_number_column = $this->policyNumberMapping[$fileData['tpa_id']]; + }else{ + $policy_number_column = 'policy_no'; + } + + + if($client_policy_data['policy_no'] != ($rows[0][$policy_number_column] ?? '')){ + $this->db->transRollback(); + return ['status' => false, 'message' => 'Policy number mismatch in the file and in the system']; + } + $tpaInsertData = $this->mapTPAData($rows, $fileId); if (empty($tpaInsertData)) { diff --git a/app/Models/PolicyTransactionModel.php b/app/Models/PolicyTransactionModel.php index e9656ad6..20da8dec 100644 --- a/app/Models/PolicyTransactionModel.php +++ b/app/Models/PolicyTransactionModel.php @@ -3098,6 +3098,8 @@ if ($date_type === "policy_issue_date") { $conditions .= " AND pcsd.pt_policy_issue_date >= '$startDate' "; $conditions .= " AND pcsd.pt_policy_issue_date <= '$endDate' "; + } else if ($date_type === "created_at") { + $conditions .= " AND pt.created_at <= '$endDate' "; } else { $conditions .= " AND pt.$date_type >= '$startDate' "; $conditions .= " AND pt.$date_type <= '$endDate' "; diff --git a/app/Views/daily_report_email_template.php b/app/Views/daily_report_email_template.php new file mode 100644 index 00000000..0ba7c8ff --- /dev/null +++ b/app/Views/daily_report_email_template.php @@ -0,0 +1,347 @@ + + + + + + + + +
    +
    +

    Daily Activity Report

    +
    + Key operations, sales and claim metrics for the day +
    +
    + +
    +
    Date:
    + + +

    Inception & Endorsement (File Upload)

    +
    +
    +
    Total Inception
    +
    +
    Employee file uploads marked as inception
    +
    +
    +
    Total Endorsement
    +
    +
    All non‑inception successful uploads
    +
    +
    + + +

    Employee Enrollment

    +
    +
    +
    Draft / Enrolled
    +
    /
    +
    Employees in draft / enrolled status for the day
    +
    +
    +
    Open for Enrollment Policies
    +
    +
    Policies currently open for enrollment
    +
    +
    + + +

    TPA & Insurer Batch Summary

    +
    +
    +
    TPA (Inception / Endorsement)
    +
    /
    +
    Successful TPA events
    +
    +
    +
    Insurer (Inception / Endorsement)
    +
    /
    +
    Successful insurer events
    +
    +
    + + +

    Sales Funnel & Leads

    +
    +
    +
    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
    +
    +
    + + +

    BDS Policy Transactions

    +
    +
    +
    Total BDS Transactions
    +
    +
    All policy transactions processed
    +
    +
    +
    BDS (Policy / Endorsement)
    +
    /
    +
    Split of inceptions vs endorsements
    +
    +
    + + +

    Claims

    +
    +
    +
    Total Claims Registered for the day
    +
    +
    +
    +
    + + +

    BDS by Policy Type

    +
    + + + + + + + + + + + + + + + + + + + + + +
    Policy TypeCount
    + +
    + No BDS activity recorded for today. +
    +
    + + +

    Claim Status Breakdown

    +
    + The total claims received so far are listed ticket type-wise. +
    + + $total_gmc_status_wise_claim_count ?? [], + 'GPA Claims' => $total_gpa_status_wise_claim_count ?? [], + 'EDLI Claims' => $total_edli_status_wise_claim_count ?? [], + 'GTLI Claims' => $total_gtli_status_wise_claim_count ?? [], + ]; + $hasAnyClaimData = false; + ?> + + $rows): ?> + + +

    +
    + + + + + + + + + + + + + + + +
    StatusCount
    + +
    +
    + + + +
    + + +
    + + \ No newline at end of file From 746eb7d4b5eb177fab5498ebbca4414b4ac70502 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Wed, 4 Mar 2026 18:32:11 +0530 Subject: [PATCH 11/12] FIX_salesTracker12 --- app/Controllers/LeadsController.php | 30 + app/Controllers/SalesController.php | 1260 +++++++++++++---- app/Models/LeadsModel.php | 1 + app/Models/SalesActivityModel.php | 97 +- app/Models/SalesActualLeadModel.php | 33 +- app/Views/leads_form.php | 70 +- app/Views/leads_non_eb.php | 65 + app/Views/sales/activity_view.php | 190 ++- .../sales/branch_level_dashboard_view.php | 1002 +++++++++---- .../branch_level_dashboard_view_1mar.php | 294 ++++ .../sales/sales_manager_level_dashboard.php | 20 +- app/Views/sales/target_view.php | 4 +- app/Views/sales/tracker_view.php | 672 +++++---- 13 files changed, 2895 insertions(+), 843 deletions(-) create mode 100644 app/Views/sales/branch_level_dashboard_view_1mar.php diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index dfd99a43..2d3988ac 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -32,6 +32,8 @@ use App\Models\OccupancyMasterModel; use App\Models\LeadFilesModel; use App\Models\LeadInstallmentPaymentDetails; use App\Models\GmailSentHistoryModel; +use App\Models\SalesActualLeadModel; +use App\Models\SalesContactPersonModel; use App\Helpers\MailHelper; use App\Helpers\ExcelMergeHelper; @@ -83,6 +85,8 @@ class LeadsController extends BaseController protected $buisnessType; protected $member_data_excel_columns; protected $general_relationships; + protected $leadModel; + protected $contactModel; public function __construct() @@ -107,6 +111,8 @@ class LeadsController extends BaseController $this->leadFilesModel = new LeadFilesModel(); $this->leadInstallmentPaymentDetails = new LeadInstallmentPaymentDetails(); $this->gmailSentHistoryModel = new GmailSentHistoryModel(); + $this->leadModel = new SalesActualLeadModel(); + $this->contactModel = new SalesContactPersonModel(); $this->issuer = [1 => 'JIBS', 2 => 'Nhance']; $this->clientType = [1 => 'Group', 2 => 'Individual']; @@ -627,6 +633,7 @@ class LeadsController extends BaseController { $request_data = $this->request->getPost(); + print_r($request_data);die; $data = sanitizeInputArrayAdvanced($request_data); $data['client_type'] = 1; @@ -825,6 +832,7 @@ class LeadsController extends BaseController $last_3_years_claims = $data['finyear']; $processedData[] = [ + 'lost_reason' => $data['lost_reason'] ?? null, 'actual_lead_id' => $data['actual_lead_id'] ?? null, 'lead_type' => $data['lead_type'], 'issuer' => $data['issuer'], @@ -4372,6 +4380,28 @@ class LeadsController extends BaseController 'actual_lead_id' => $actual_lead_id, ]; + if ($actual_lead_id > 0) { + + // 🔹 Client Details (Single Row) + $data['actual_lead_client_details'] = $this->leadModel + ->select('company_name, email, phone, address, website, gst_number, status, assigned_to') + ->where('lead_id', $actual_lead_id) + ->first(); // first row only + + + // 🔹 Contact Person Details (First Row Only) + $data['actual_lead_contact_person_details'] = $this->contactModel + ->select('contact_id, name, mobile, designation, email, is_primary') + ->where('lead_id', $actual_lead_id) + ->where('is_primary',1) + ->orderBy('is_primary', 'DESC') // optional (primary first) + ->first(); // only first row + } + else { + $data['actual_lead_client_details'] = null; + $data['actual_lead_contact_person_details'] = null; + } + // Fetch sales team members who are active in team 5 $data['salse_team'] = $this->userModel ->select('user_profiles.*') diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 164444e9..39a0f672 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -53,7 +53,6 @@ class SalesController extends BaseController ->orderBy('lead_id', 'DESC') ->findAll(); - return $this->loadLayout('sales/activity_view', $data); } @@ -66,9 +65,14 @@ class SalesController extends BaseController return $this->loadLayout('sales/target_view', $data); } - /** - * HELPER: Fetches Sales Managers based on the logged-in user's role and branch - */ + /** + * HELPER: Fetches Sales Managers based on the logged-in user's role and branch + * sales_manager Current branch | Role 4 + Team 5 | Assign To dropdown + * sales_manager_ids Current branch | Role 4 + Team 5 | Query filter IDs + * sales_manager_with_head Current branch | Role 1,4,5 | Branch reporting dropdown + * sales_manager_with_head_ids Current branch | Role 1,4,5 | Branch reporting filter + * sales_team All branches | Role 1,4,5 | Admin/global reporting + */ private function getSalesStaffData(): array { $db = \Config\Database::connect(); @@ -76,54 +80,118 @@ class SalesController extends BaseController $role = get_role_id(); $team_id = user_team(); + // ── Get logged-in user's profile ──────────────────────────────── + $row = $db->table('user_profiles') + ->where('is_active', 1) + ->where('id', $logged_user_id) + ->get()->getRow(); + + $nhance_branch_id = $row ? $row->nhance_branch_id : null; + + // ── Base result structure ──────────────────────────────────────── $data = [ - 'users' => [], - 'sales_manager_ids'=> [], - 'sales_role' => '', - 'nhance_branch_id' => null, - 'assigned_ids' => [], + 'sales_role' => '', + 'sales_manager' => [], + 'sales_manager_ids' => [], + 'sales_manager_with_head' => [], + 'sales_manager_with_head_ids' => [], + 'sales_team' => [], + 'nhance_branch_id' => $nhance_branch_id, ]; - $row = $db->table('user_profiles')->select('*') - ->where('is_active', 1)->where('id', $logged_user_id) - ->get()->getRow(); + // ================================================================ + // QUERY 1: Get all MANAGERS in current branch + // Role = 4 AND Team = 5 AND same branch + // ================================================================ + $branch_managers = $db->table('user_profiles up') + ->select('up.id, up.first_name, up.role, up.nhance_branch_id') + ->join('user_teams ut', 'ut.user_id = up.id') + ->where('up.is_active', 1) + ->where('ut.is_active', 1) + ->where('up.role', 4) // Sales Manager role + ->where('ut.team_id', 5) // Sales team + ->where('up.nhance_branch_id', $nhance_branch_id) // same branch + ->groupBy('up.id') + ->get()->getResultArray(); - $nhance_branch_id = $row ? $row->nhance_branch_id : null; - $data['nhance_branch_id']= $nhance_branch_id; + // ================================================================ + // QUERY 2: Get all HEADS in current branch + // Role = 1 or 5 AND same branch + // ================================================================ + $branch_heads = $db->table('user_profiles') + ->select('id, first_name, role, nhance_branch_id') + ->where('is_active', 1) + ->whereIn('role', [1, 5]) // Sales Head roles + ->where('nhance_branch_id', $nhance_branch_id) // same branch + ->get()->getResultArray(); - // ── Sales Manager (Role 4, Team 5) ────────────────────────── + // Add "(Head)" label to heads so dropdown is clear + foreach ($branch_heads as &$head) { + $head['first_name'] = $head['first_name'] . ' (Head)'; + } + unset($head); + + // ================================================================ + // QUERY 3: Get ALL MANAGERS across ALL branches + // Role = 4 AND Team = 5 (no branch filter) + // ================================================================ + $all_managers = $db->table('user_profiles up') + ->select('up.id, up.first_name, up.role, up.nhance_branch_id') + ->join('user_teams ut', 'ut.user_id = up.id') + ->where('up.is_active', 1) + ->where('ut.is_active', 1) + ->where('up.role', 4) // Sales Manager role + ->where('ut.team_id', 5) // Sales team + ->groupBy('up.id') + ->get()->getResultArray(); + + // ================================================================ + // QUERY 4: Get ALL HEADS across ALL branches + // Role = 1 or 5 (no branch filter) + // ================================================================ + $all_heads = $db->table('user_profiles') + ->select('id, first_name, role, nhance_branch_id') + ->where('is_active', 1) + ->whereIn('role', [1, 5]) // Sales Head roles + ->get()->getResultArray(); + + // Add "(Head)" label to all heads + foreach ($all_heads as &$head) { + $head['first_name'] = $head['first_name'] . ' (Head)'; + } + unset($head); + + // ================================================================ + // BUILD: sales_manager_with_head = branch heads + branch managers + // ================================================================ + $data['sales_manager_with_head'] = array_merge($branch_heads, $branch_managers); + $data['sales_manager_with_head_ids'] = array_column($data['sales_manager_with_head'], 'id'); + + // ================================================================ + // BUILD: sales_team = all heads + all managers (every branch) + // ================================================================ + $data['sales_team'] = array_merge($all_heads, $all_managers); + + // ── Sales Manager (Role 4, Team 5) ────────────────────────────── if ($role == 4 && in_array(5, $team_id)) { $data['sales_role'] = 'Sales Manager'; + $data['sales_manager'] = [[ // only himself + 'id' => $row->id, + 'first_name' => $row->first_name, + 'role' => $role, + 'nhance_branch_id' => $nhance_branch_id, + ]]; $data['sales_manager_ids'] = [$logged_user_id]; - $data['assigned_ids'] = [$logged_user_id]; - $data['users'] = [ - [ - 'id' => $row->id, - 'first_name' => $row->first_name, - 'last_name' => $row->last_name ?? '', - 'nhance_branch_id' => $nhance_branch_id, - ] - ]; - // ── Sales Head (Role 1 or 5) ───────────────────────────────── + // ── Sales Head (Role 1 or 5) ───────────────────────────────────── } elseif (in_array($role, [1, 5])) { - $data['sales_role'] = 'Sales Head'; - $data['users'] = $db->table('user_profiles up') - ->select('up.id, up.first_name, up.last_name, up.nhance_branch_id') - ->join('user_teams ut', 'ut.user_id = up.id') - ->where('up.is_active', 1) - ->where('ut.is_active', 1) - ->where('up.role', 4) - ->where('ut.team_id', 5) - ->where('up.nhance_branch_id', $nhance_branch_id) - ->get() - ->getResultArray(); - - $ids = array_column($data['users'], 'id'); - $data['sales_manager_ids'] = $ids; - $data['assigned_ids'] = $ids; // same value, both available + $data['sales_role'] = 'Sales Head'; + $data['sales_manager'] = $branch_managers; // reuse QUERY 1 result + $data['sales_manager_ids'] = array_column($branch_managers, 'id'); + $data['sales_manager_with_head'] = array_merge($branch_heads, $branch_managers); // reuse + $data['sales_manager_with_head_ids'] = array_column($data['sales_manager_with_head'], 'id'); } return $data; @@ -149,6 +217,9 @@ class SalesController extends BaseController echo view('layout/footer', $data); } + /** + * GET /api/sales/activities/(:num)/complete + */ public function completeActivity($id) { try { $data = $this->request->getJSON(true); @@ -236,7 +307,8 @@ class SalesController extends BaseController 'data' => $result['data'], 'total' => $result['total'], 'limit' => $limit, - 'offset' => $offset + 'offset' => $offset, + 'counts' => $result['counts'], ]); } catch (\Exception $e) { return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); @@ -398,6 +470,15 @@ class SalesController extends BaseController $data = $this->request->getJSON(true); $data['updated_by'] = $this->getUserId(); + $contact = $this->contactModel->find($id); + + if (isset($data['is_primary']) && $data['is_primary'] == 1) { + // Reset all contacts for this lead to 0 primary + $this->contactModel->where('lead_id', $contact['lead_id']) + ->set(['is_primary' => 0]) + ->update(); + } + if (!$this->contactModel->update((int)$id, $data)) { return $this->fail($this->contactModel->errors(), ResponseInterface::HTTP_BAD_REQUEST); } @@ -487,7 +568,8 @@ class SalesController extends BaseController 'data' => $result['data'], 'total' => $result['total'], 'limit' => $limit, - 'offset' => $offset + 'offset' => $offset, + 'counts' => $result['counts'], ], 200); } catch (\Exception $e) { @@ -565,6 +647,11 @@ class SalesController extends BaseController $data['created_by'] = $this->getUserId(); $data['updated_by'] = $this->getUserId(); + // FIX: Convert the array to a JSON string so it fits in the VARCHAR column + if (isset($data['additional_assigned_ids']) && is_array($data['additional_assigned_ids'])) { + $data['additional_assigned_ids'] = json_encode($data['additional_assigned_ids']); + } + if (!$this->activityModel->insert($data)) { return $this->fail($this->activityModel->errors(), ResponseInterface::HTTP_BAD_REQUEST); } @@ -575,6 +662,11 @@ class SalesController extends BaseController $activityId = $this->activityModel->getInsertID(); $activity = $this->activityModel->find((int)$activityId); + // OPTIONAL: Decode it back to an array for the API response so the frontend gets a clean array + if (isset($activity['additional_assigned_ids'])) { + $activity['additional_assigned_ids'] = json_decode($activity['additional_assigned_ids'], true); + } + return $this->respondCreated([ 'status' => 'success', 'message' => 'Activity created successfully', @@ -600,12 +692,22 @@ class SalesController extends BaseController $data = $this->request->getJSON(true); $data['updated_by'] = $this->getUserId(); + // FIX: Convert the array to a JSON string for updating + if (isset($data['additional_assigned_ids']) && is_array($data['additional_assigned_ids'])) { + $data['additional_assigned_ids'] = json_encode($data['additional_assigned_ids']); + } + if (!$this->activityModel->update($id, $data)) { return $this->fail($this->activityModel->errors(), ResponseInterface::HTTP_BAD_REQUEST); } $activity = $this->activityModel->find((int)$id); + // OPTIONAL: Decode it back to an array for the API response + if (isset($activity['additional_assigned_ids'])) { + $activity['additional_assigned_ids'] = json_decode($activity['additional_assigned_ids'], true); + } + return $this->respond([ 'status' => 'success', 'message' => 'Activity updated successfully', @@ -927,252 +1029,896 @@ class SalesController extends BaseController // ==================== Dashboard ==================== - public function dashboard() - { - $payload = $this->request->getGet(); - $base = $this->getSalesStaffData(); - $salesRole = $base['sales_role']; - $salesManagerIds = $base['sales_manager_ids']; - $userId = get_session_userid(); - // Get branch id from users array - $nhanceBranchId = $base['users'][0]['nhance_branch_id'] ?? null; +// ───────────────────────────────────────────── +// HELPER: Build FY date range from fy_year string +// e.g. "2024-2025" → ['2024-04-01 00:00:00', '2025-03-31 23:59:59'] +// ───────────────────────────────────────────── +private function getFYDateRange(string $financialYear): array +{ + // Format: "2025-2026" — split on last hyphen to get start=2025, end=2026 + $pos = strrpos($financialYear, '-'); + $startYear = substr($financialYear, 0, $pos); // "2025" + $endYear = substr($financialYear, $pos + 1); // "2026" - if ($salesRole === 'Sales Head') { - $this->branchLevelDashboard($nhanceBranchId, $salesManagerIds); - } elseif ($salesRole === 'Sales Manager') { - $this->salesManagerLevelDashboard($userId, $salesManagerIds,$payload); - } - } + return [ + 'start' => $startYear . '-04-01 00:00:00', // 2025-04-01 00:00:00 + 'end' => $endYear . '-03-31 23:59:59', // 2026-03-31 23:59:59 + ]; +} - public function branchLevelDashboard($branchId,$sales_manager_ids) - { - // Hardcoded branch ID as requested - // $branchId = 1; +// ───────────────────────────────────────────── +// HELPER: FY quarters (Apr-Jun / Jul-Sep / Oct-Dec / Jan-Mar) +// ───────────────────────────────────────────── +private function getFYQuarters(string $financialYear): array +{ + $pos = strrpos($financialYear, '-'); + $sy = (int)substr($financialYear, 0, $pos); // 2025 + $ey = (int)substr($financialYear, $pos + 1); // 2026 - try { - $sales_manager_ids = array_values(array_map('intval', $sales_manager_ids)); - - // Final safe check - if (empty($sales_manager_ids)) { - // No valid IDs — skip queries or return empty - $total_leads = 0; - $total_activity = 0; - $total_completed_activity = 0; - $total_pending_activity = 0; - $pending_activities = []; - $recent_activities = []; - $teamPerformance = []; - - $leadsOverview = []; - } else { + return [ + ['name' => 'Q1', 'label' => "Q1 (Apr–Jun {$sy})", 'start' => "{$sy}-04-01", 'end' => "{$sy}-06-30"], + ['name' => 'Q2', 'label' => "Q2 (Jul–Sep {$sy})", 'start' => "{$sy}-07-01", 'end' => "{$sy}-09-30"], + ['name' => 'Q3', 'label' => "Q3 (Oct–Dec {$sy})", 'start' => "{$sy}-10-01", 'end' => "{$sy}-12-31"], + ['name' => 'Q4', 'label' => "Q4 (Jan–Mar {$ey})", 'start' => "{$ey}-01-01", 'end' => "{$ey}-03-31"], + ]; +} - if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) { - $sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0 - } - - // 1. Lead Statistics - $total_leads = $this->leadModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults(); // Use countAllResults, NOT countAll - // echo $this->leadModel->getLastQuery();die(); - - // 2. Total activity - $total_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults(); +// ───────────────────────────────────────────── +// dashboard() — entry point +// ───────────────────────────────────────────── +public function dashboard() +{ + $payload = $this->request->getGet(); + $base = $this->getSalesStaffData(); + $salesRole = $base['sales_role']; + $salesManagerIds = $base['sales_manager_ids']; + $userId = get_session_userid(); - // 3. Completed activity - $total_completed_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'completed')->countAllResults(); + // Get branch id + $nhanceBranchId = $base['sales_manager'][0]['nhance_branch_id'] ?? null; - // 4. Pending activity - $total_pending_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'pending')->countAllResults(); - - $db = \Config\Database::connect(); - - // 5. Team Performance - $teamPerformance = $db->table('user_profiles as u') - ->select('u.first_name, u.last_name, r.role, - (SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id) as total_acts, - (SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id AND status = "completed") as done_acts') - ->join('roles r', 'r.id = u.role') - ->where('u.nhance_branch_id', $branchId) - ->whereIn('u.id', $sales_manager_ids) - ->where('u.is_active', 1) - ->get()->getResultArray(); + $current_fin_year = $payload['fy'] ?? getCurrentFinancialYear(); - // 6. Recent Activities (Joining for Lead Names) - $recent_activities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name') - ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id') - ->orderBy('sales_activities.scheduled_date', 'DESC') - ->limit(6) - ->findAll(); - - // 7. Pending Activities (List) - $pending_activities = $db->table('sales_activities sa') - ->select('sa.activity_id,sa.lead_id,sa.activity_type,sa.scheduled_date,sa.status,sa.assigned_to,sal.company_name,up.first_name AS assigned_to_name,sa.notes') - ->join('user_profiles up', 'up.id = sa.assigned_to', 'left') - ->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') // ✅ ADD THIS - ->orderBy('sa.scheduled_date', 'DESC') - ->whereIn('sa.assigned_to', $sales_manager_ids) - ->where('sa.status', 'pending') - ->get()->getResultArray(); + $db = \Config\Database::connect(); - // 8. All Leads Overview - $leadsOverview = $db->table('sales_actual_leads sal') - ->select('sal.lead_id,sal.company_name,sal.status,up.first_name AS assigned_to, - COUNT(DISTINCT sa.activity_id) AS activities, - COUNT(DISTINCT l.id) AS opportunities - ') - ->join('user_profiles up', 'up.id = sal.assigned_to', 'left') - ->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left') - ->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left') - ->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name') - // ->having('COUNT(DISTINCT sa.activity_id) + COUNT(DISTINCT l.id) >', 0) // ← this line - ->orderBy('sal.created_at', 'DESC') - ->whereIn('sal.assigned_to', $sales_manager_ids) - ->get() - ->getResultArray(); - - // 9. Activity BrakDown - $activityBreakdown = $db->table('sales_activities') - ->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total_activity}, 0) AS percentage", false) - ->whereIn('assigned_to', $sales_manager_ids) - ->groupBy('activity_type') - ->orderBy('total', 'DESC') - ->get() - ->getResultArray(); - } + // Available FY years for dropdown + $fin_years_raw = $db->table('sales_target') + ->select('fy_year', false) // false = no backtick escaping + ->distinct() + ->orderBy('fy_year', 'DESC') + ->get() + ->getResultArray(); + + $fin_years = array_column($fin_years_raw, 'fy_year'); + + if (empty($fin_years)) { + $fin_years[] = $current_fin_year; + } + + // Ensure current FY is available in list + if (!in_array($current_fin_year, $fin_years)) { + array_unshift($fin_years, $current_fin_year); + } + + // Route by role + if ($salesRole === 'Sales Head') { + $this->branchLevelDashboard($nhanceBranchId, $salesManagerIds, $current_fin_year, $fin_years); + } elseif ($salesRole === 'Sales Manager') { + $this->salesManagerLevelDashboard($userId, $current_fin_year, $fin_years); + } +} + +// ───────────────────────────────────────────── +// branchLevelDashboard() +// ───────────────────────────────────────────── +public function branchLevelDashboard($branchId, $sales_manager_ids, $current_fin_year, $fin_years) +{ + try { + $sales_manager_ids = array_values(array_filter(array_map('intval', $sales_manager_ids))); + + $db = \Config\Database::connect(); + $fyRange = $this->getFYDateRange($current_fin_year); + $fyStart = $fyRange['start']; + $fyEnd = $fyRange['end']; + + if (empty($sales_manager_ids)) { + // ── No team members — return empty dashboard ── $data = [ - 'total_leads' => $total_leads, - 'total_acts' => $total_activity, - 'total_completed_acts' => $total_completed_activity, - 'total_pending_acts'=> $total_pending_activity, - 'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14] - 'team' => $teamPerformance, - 'recent_acts' => $recent_activities, - 'pending_acts' => $pending_activities, - 'leads_overview' => $leadsOverview, - 'activity_breakdown'=> $activityBreakdown, - 'tab_name' => "Sales Dashboard", - 'page_name' => "Sales Dashboard" + 'total_leads' => 0, + 'total_acts' => 0, + 'total_completed_acts' => 0, + 'total_pending_acts' => 0, + 'team' => [], + 'pending_acts' => [], + 'leads_overview' => [], + 'activity_breakdown' => [], + 'team_achievement' => [], + 'opp_achievement' => [], + 'fin_years' => $fin_years, + 'current_fin_year' => $current_fin_year, + 'tab_name' => 'Sales Dashboard', + 'page_name' => 'Sales Dashboard', ]; - - // dd($data); - $this->loadLayout('sales/branch_level_dashboard_view', $data); - - // return view('sales/dashboard_view', $data); - - } catch (\Exception $e) { - return $this->failServerError($e->getMessage()); + return; } + + // 1. Lead count — FY filtered by created_at + $total_leads = $this->leadModel + ->whereIn('assigned_to', $sales_manager_ids) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->countAllResults(); + + // 2. Total activities — FY filtered by scheduled_date + $total_activity = $this->activityModel + ->whereIn('assigned_to', $sales_manager_ids) + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->countAllResults(); + + // 3. Completed activities — FY filtered + $total_completed_activity = $this->activityModel + ->whereIn('assigned_to', $sales_manager_ids) + ->where('status', 'completed') + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->countAllResults(); + + // 4. Pending activities — FY filtered + $total_pending_activity = $this->activityModel + ->whereIn('assigned_to', $sales_manager_ids) + ->where('status', 'pending') + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->countAllResults(); + + // 5. Team Performance — subqueries FY filtered by scheduled_date + $teamPerformance = $db->table('user_profiles as u') + ->select("u.id, u.first_name, u.last_name, r.role, + (SELECT COUNT(*) FROM sales_activities + WHERE assigned_to = u.id + AND scheduled_date >= '{$fyStart}' + AND scheduled_date <= '{$fyEnd}') as total_acts, + (SELECT COUNT(*) FROM sales_activities + WHERE assigned_to = u.id AND status = 'completed' + AND scheduled_date >= '{$fyStart}' + AND scheduled_date <= '{$fyEnd}') as done_acts", false) + ->join('roles r', 'r.id = u.role') + ->where('u.nhance_branch_id', $branchId) + ->whereIn('u.id', $sales_manager_ids) + ->where('u.is_active', 1) + ->get() + ->getResultArray(); + + // 6. Pending Activities list — FY filtered by scheduled_date + $pending_activities = $db->table('sales_activities sa') + ->select('sa.activity_id, sa.lead_id, sa.activity_type, sa.scheduled_date, + sa.status, sa.assigned_to, sal.company_name, + up.first_name AS assigned_to_name, sa.notes') + ->join('user_profiles up', 'up.id = sa.assigned_to', 'left') + ->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') + ->whereIn('sa.assigned_to', $sales_manager_ids) + ->where('sa.status', 'pending') + ->where('sa.scheduled_date >=', $fyStart) + ->where('sa.scheduled_date <=', $fyEnd) + ->orderBy('sa.scheduled_date', 'DESC') + ->get() + ->getResultArray(); + + // 7. All Leads Overview — FY filtered by sal.created_at + // Activities & opportunities also scoped to FY via CASE WHEN + $leadsOverview = $db->table('sales_actual_leads sal') + ->select("sal.lead_id, sal.company_name, sal.status, + up.first_name AS assigned_to, + COUNT(DISTINCT CASE WHEN sa.scheduled_date >= '{$fyStart}' + AND sa.scheduled_date <= '{$fyEnd}' THEN sa.activity_id END) AS activities, + COUNT(DISTINCT CASE WHEN l.updated_at >= '{$fyStart}' + AND l.updated_at <= '{$fyEnd}' THEN l.id END) AS opportunities", false) + ->join('user_profiles up', 'up.id = sal.assigned_to', 'left') + ->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left') + ->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left') + ->whereIn('sal.assigned_to', $sales_manager_ids) + ->where('sal.created_at >=', $fyStart) + ->where('sal.created_at <=', $fyEnd) + ->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name') + ->orderBy('sal.created_at', 'DESC') + ->get() + ->getResultArray(); + + // Protect against division by zero in query #8 + $total_activity_safe = $total_activity > 0 ? $total_activity : 1; + + // 8. Activity Breakdown — FY filtered by scheduled_date + $activityBreakdown = $db->table('sales_activities') + ->select("activity_type, + COUNT(*) AS total, + ROUND(COUNT(*) * 100.0 / {$total_activity_safe}, 0) AS percentage", false) + ->whereIn('assigned_to', $sales_manager_ids) + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->groupBy('activity_type') + ->orderBy('total', 'DESC') + ->get() + ->getResultArray(); + + // 9. Team Achievement (for achievement list + modal) + $teamAchievement = $this->buildTeamAchievement( + $db, $sales_manager_ids, $branchId, $current_fin_year, $fyStart, $fyEnd + ); + + // 10. Opportunities Achievement per member + $oppAchievement = $this->buildOppAchievement( + $db, $sales_manager_ids, $branchId, $current_fin_year, $fyStart, $fyEnd + ); + + $data = [ + 'total_leads' => $total_leads, + 'total_acts' => $total_activity, + 'total_completed_acts' => $total_completed_activity, + 'total_pending_acts' => $total_pending_activity, + 'team' => $teamPerformance, + 'pending_acts' => $pending_activities, + 'leads_overview' => $leadsOverview, + 'activity_breakdown' => $activityBreakdown, + 'team_achievement' => $teamAchievement, // used by JS TEAM constant + 'opp_achievement' => $oppAchievement, // used by JS OPP_DATA constant + 'fin_years' => $fin_years, + 'current_fin_year' => $current_fin_year, + 'tab_name' => 'Sales Dashboard', + 'page_name' => 'Sales Dashboard', + ]; + + $this->loadLayout('sales/branch_level_dashboard_view', $data); + + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } +} + +// ───────────────────────────────────────────── +// buildTeamAchievement() +// Builds the TEAM array for the achievement list +// ───────────────────────────────────────────── +private function buildTeamAchievement($db, array $sales_manager_ids, $branchId, string $fy, string $fyStart, string $fyEnd): array +{ + $quarters = $this->getFYQuarters($fy); + + // Gradient palette (cycles) + $gradients = [ + ['grad' => 'linear-gradient(135deg,#10b981,#34d399)', 'color' => '#10b981'], + ['grad' => 'linear-gradient(135deg,#06b6d4,#67e8f9)', 'color' => '#06b6d4'], + ['grad' => 'linear-gradient(135deg,#4f46e5,#818cf8)', 'color' => '#4f46e5'], + ['grad' => 'linear-gradient(135deg,#ec4899,#f9a8d4)', 'color' => '#ec4899'], + ['grad' => 'linear-gradient(135deg,#f97316,#fbbf24)', 'color' => '#f97316'], + ]; + + $members = $db->table('user_profiles as u') + ->select('u.id, u.first_name, u.last_name, r.role') + ->join('roles r', 'r.id = u.role') + ->where('u.nhance_branch_id', $branchId) + ->whereIn('u.id', $sales_manager_ids) + ->where('u.is_active', 1) + ->get() + ->getResultArray(); + + $result = []; + + foreach ($members as $idx => $m) { + $uid = (int)$m['id']; + + // Target from sales_target + $targetRow = $db->table('sales_target') + ->where('user_id', $uid) + ->where('fy_year', $fy) + ->get() + ->getRowArray(); + $targetAmt = (float)($targetRow['target_amount'] ?? 0); + + // Achieved (won leads in FY) + $achievedAmt = (float)$this->getUserAchievedAmount($fy, $uid); + + // Activities — FY filtered by scheduled_date + $totalActs = $db->table('sales_activities') + ->where('assigned_to', $uid) + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->countAllResults(); + $doneActs = $db->table('sales_activities') + ->where('assigned_to', $uid) + ->where('status', 'completed') + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->countAllResults(); + + // Activity breakdown — FY filtered + $actRows = $db->table('sales_activities') + ->select('activity_type, COUNT(*) as cnt') + ->where('assigned_to', $uid) + ->where('scheduled_date >=', $fyStart) + ->where('scheduled_date <=', $fyEnd) + ->groupBy('activity_type') + ->get()->getResultArray(); + $activities = []; + foreach ($actRows as $ar) { + $activities[$ar['activity_type']] = (int)$ar['cnt']; + } + + // Quarter splits + $splits = []; + foreach ($quarters as $q) { + $qStart = $q['start'] . ' 00:00:00'; + $qEnd = $q['end'] . ' 23:59:59'; + + // Achievement = SUM(exp_amt) for won policies in this quarter + $qAchievedRow = $db->query(" + SELECT COALESCE(SUM(ptcs.exp_amt), 0) AS total + FROM policy_transaction pt + LEFT JOIN pt_co_share_details ptcs + ON ptcs.pt_id = pt.id + AND ptcs.is_active = 1 + WHERE pt.sales_generated_by = ? + AND pt.issuer_branch = ? + AND pt.created_at >= ? + AND pt.created_at <= ? + ", [$uid, $branchId, $qStart, $qEnd])->getRowArray(); + $qAchieved = (float)($qAchievedRow['total'] ?? 0); + + $qTarget = $targetAmt > 0 ? round($targetAmt / 4, 2) : 0; + + $qActs = $db->table('sales_activities') + ->where('assigned_to', $uid) + ->where('scheduled_date >=', $qStart) + ->where('scheduled_date <=', $qEnd) + ->countAllResults(); + + $qDone = $db->table('sales_activities') + ->where('assigned_to', $uid) + ->where('status', 'completed') + ->where('scheduled_date >=', $qStart) + ->where('scheduled_date <=', $qEnd) + ->countAllResults(); + + $qLeads = $db->table('sales_actual_leads') + ->where('assigned_to', $uid) + ->where('created_at >=', $qStart) + ->where('created_at <=', $qEnd) + ->countAllResults(); + + $splits[] = [ + 'name' => $q['label'], + 'start' => date('M Y', strtotime($q['start'])), + 'end' => date('M Y', strtotime($q['end'])), + 'target' => $qTarget, + 'achieved' => $qAchieved, + 'acts' => $qActs, + 'done' => $qDone, + 'leads' => $qLeads, + ]; + } + + $palette = $gradients[$idx % count($gradients)]; + + $result[] = [ + 'id' => $uid, + 'first_name' => $m['first_name'], + 'last_name' => $m['last_name'], + 'role' => $m['role'], + 'total_acts' => $totalActs, + 'done_acts' => $doneActs, + 'target_amt' => $targetAmt, + 'achieved_amt' => $achievedAmt, + 'grad' => $palette['grad'], + 'color' => $palette['color'], + 'splits' => $splits, + 'activities' => $activities, + ]; } - public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = []) - { - // $userId = get_session_userid(); - // $userId = 1; - $db = \Config\Database::connect(); + return $result; +} - try { +// ───────────────────────────────────────────── +// buildOppAchievement() +// Opportunities via policy_transaction + pt_co_share_details +// Returns per-member: totals + flat policy list (no quarterly grouping) +// ───────────────────────────────────────────── +private function buildOppAchievement($db, array $sales_manager_ids, $branchId, string $fy, string $fyStart, string $fyEnd): array +{ + $result = []; - // $payload = $this->request->getGet(); - $current_fin_year = $payload['fy'] ?? getCurrentFinancialYear(); + foreach ($sales_manager_ids as $uid) { - - $fin_years = $db->table('sales_target') - ->select('fy_year') - ->where('user_id', $userId) - ->orderBy('fy_year', 'desc') - ->get() - ->getResultArray(); + // ── Target ── + $targetRow = $db->table('sales_target') + ->where('user_id', $uid) + ->where('fy_year', $fy) + ->get() + ->getRowArray(); + $targetAmt = (float)($targetRow['target_amount'] ?? 0); - $fin_years = array_column($fin_years, 'fy_year'); + // ── All policy rows for this user in FY ── + // policy_no, issue_date (from pt), amount (exp_amt from child), created_at + // If no matching pt_co_share_details row exists, exp_amt = 0 + // Policy list for Tab 2: policy_transaction + exp_amt from pt_co_share_details + $policyRows = $db->query(" + SELECT + pt.id, + pt.policy_no, + pt.created_at AS issue_date, + COALESCE(ptcs.exp_amt, 0) AS amount, + pt.created_at AS created_at + FROM policy_transaction pt + LEFT JOIN pt_co_share_details ptcs + ON ptcs.pt_id = pt.id + AND ptcs.is_active = 1 + WHERE pt.sales_generated_by = ? + AND pt.issuer_branch = ? + AND pt.created_at >= ? + AND pt.created_at <= ? + ORDER BY pt.created_at DESC + ", [$uid, $branchId, $fyStart, $fyEnd])->getResultArray(); - if(empty($fin_years)){ - $fin_years[] = $current_fin_year; - } + // ── Totals derived from policy_transaction ── + $totalPolicies = count($policyRows); + $totalExpAmt = array_sum(array_column($policyRows, 'amount')); - $target = $db->table('sales_target') - ->where('user_id', $userId) - ->where('fy_year', $current_fin_year) - ->get() - ->getRowArray(); - - $targetAmount = $target['target_amount'] ?? 0.00; - - // get achieved amount from leads table - $achievedAmount = $this->getUserAchievedAmount($current_fin_year, $userId); - - $remainingAmount = $targetAmount - $achievedAmount; - // $achievementPercent = ($targetAmount > 0) ? round(($achievedAmount / $targetAmount) * 100) : 0; - $achievementPercent = ($targetAmount > 0) ? min(100, round(($achievedAmount / $targetAmount) * 100)) : 0; - - $activitySummary = [ - 'total' => $this->activityModel->where('assigned_to', $userId)->countAllResults(), - 'pending' => $this->activityModel->where(['assigned_to' => $userId, 'status' => 'pending'])->countAllResults(), - 'completed' => $this->activityModel->where(['assigned_to' => $userId, 'status' => 'completed'])->countAllResults(), + // Clean policy list for JS + $policies = array_map(function($row) { + return [ + 'policy_no' => $row['policy_no'], + 'issue_date' => $row['issue_date'], + 'amount' => (float)$row['amount'], + 'created_at' => $row['created_at'], ]; + }, $policyRows); - $myLeadsCount = $this->leadModel->where('assigned_to', $userId)->countAllResults(); + // ── Won Leads for this user in FY (Table 2 in modal) ── + // leads.actual_lead_id maps to sales_actual_leads.id + // leads.type: 1 = EB, else = Non-EB + $wonLeads = $db->query(" + SELECT + sal.lead_id, + sal.company_name AS company, + CASE WHEN l.lead_type = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_type, + l.created_at AS created_at, + l.status + FROM leads l + INNER JOIN sales_actual_leads sal + ON sal.lead_id = l.actual_lead_id + WHERE l.status = 'won' + AND sal.assigned_to = ? + AND l.created_at >= ? + AND l.created_at <= ? + ORDER BY l.created_at DESC + ", [$uid, $fyStart, $fyEnd])->getResultArray(); - $upcomingActivities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name') - ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id') - ->where(['sales_activities.assigned_to' => $userId, 'sales_activities.status' => 'pending']) - ->orderBy('scheduled_date', 'ASC') - ->limit(3) - ->findAll(); - - $recentLeads = $this->leadModel->where('assigned_to', $userId) - ->orderBy('created_at', 'DESC') - ->limit(5) - ->findAll(); - - $data = [ - 'target_amt' => $targetAmount, - 'achieved' => $achievedAmount, - 'remaining' => $remainingAmount, - 'percent' => $achievementPercent, - 'acts' => $activitySummary, - 'lead_count' => $myLeadsCount, - 'upcoming' => $upcomingActivities, - 'recent_leads' => $recentLeads, - 'fin_years' => $fin_years, - 'display_fin_years' => format_financial_year($current_fin_year), - 'user_name' => get_session_userdata()->first_namee ?? '', - 'tab_name' => "Sales Dashboard", - 'page_name' => "Sales Dashboard" - ]; - - // dd($data); - - // return view('sales/my_dashboard_view', $data); - $this->loadLayout('sales/sales_manager_level_dashboard', $data); - - } catch (\Exception $e) { - return $this->failServerError($e->getMessage()); - } + $result[$uid] = [ + 'total_policies' => $totalPolicies, + 'total_exp_amt' => $totalExpAmt, + 'target_amt' => $targetAmt, + 'policies' => $policies, + 'won_leads' => $wonLeads, // for Table 2 in modal Tab 2 + ]; } - public function getUserAchievedAmount($financialYear, $userId) - { - // Split the string into two years - $years = explode('-', $financialYear); - $startYear = $years[0]; // 2025 - $endYear = $years[1]; // 2026 + return $result; +} - // Create the timestamps - $startFY = $startYear . '-04-01 00:00:00'; - $endFY = $endYear . '-03-31 23:59:59'; +// ───────────────────────────────────────────── +// getUserAchievedAmount() +// ───────────────────────────────────────────── +public function getUserAchievedAmount($financialYear, $userId) +{ + // "2025-2026" → strrpos splits correctly into 2025 / 2026 + $pos = strrpos($financialYear, '-'); + $startYear = substr($financialYear, 0, $pos); // "2025" + $endYear = substr($financialYear, $pos + 1); // "2026" - $achievedAmountData = $this->leadModel - ->select('SUM(leads.premium_amount) as achieved_amount') - ->join('leads', 'sales_actual_leads.lead_id = leads.actual_lead_id') - ->where('sales_actual_leads.assigned_to', $userId) - ->where('leads.status', 'won') - ->where('leads.updated_at >=', $startFY) - ->where('leads.updated_at <=', $endFY) + $startFY = $startYear . '-04-01 00:00:00'; // 2025-04-01 + $endFY = $endYear . '-03-31 23:59:59'; // 2026-03-31 + + // Achievement = SUM(exp_amt) from policy_transaction + pt_co_share_details + $db = \Config\Database::connect(); + $row = $db->query(" + SELECT COALESCE(SUM(ptcs.exp_amt), 0) AS achieved_amount + FROM policy_transaction pt + LEFT JOIN pt_co_share_details ptcs + ON ptcs.pt_id = pt.id + AND ptcs.is_active = 1 + WHERE pt.sales_generated_by = ? + AND pt.created_at >= ? + AND pt.created_at <= ? + ", [$userId, $startFY, $endFY])->getRowArray(); + + return (float)($row['achieved_amount'] ?? 0.00); +} + +// ───────────────────────────────────────────── +// salesManagerLevelDashboard() +// ───────────────────────────────────────────── +public function salesManagerLevelDashboard($userId, $current_fin_year = null, $fin_years = []) +{ + $db = \Config\Database::connect(); + + try { + + // ------------------------------- + // Financial Year Handling + // ------------------------------- + if (empty($current_fin_year)) { + $current_fin_year = getCurrentFinancialYear(); + } + + $fyRange = $this->getFYDateRange($current_fin_year); + $fyStart = $fyRange['start']; + $fyEnd = $fyRange['end']; + + // ------------------------------- + // Target (FY Based) + // ------------------------------- + $target = $db->table('sales_target') + ->where('user_id', $userId) + ->where('fy_year', $current_fin_year) + ->get() + ->getRowArray(); + + $targetAmount = (float)($target['target_amount'] ?? 0); + + // ------------------------------- + // Achieved (FY Based) + // ------------------------------- + $achievedAmount = (float)$this->getUserAchievedAmount($current_fin_year, $userId); + + $remainingAmount = $targetAmount - $achievedAmount; + $achievementPercent = ($targetAmount > 0) + ? min(100, round(($achievedAmount / $targetAmount) * 100)) + : 0; + + // ------------------------------- + // Activity Summary (FY Based using created_at) + // ------------------------------- + $activitySummary = [ + 'total' => $this->activityModel + ->where('assigned_to', $userId) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->countAllResults(), + + 'pending' => $this->activityModel + ->where([ + 'assigned_to' => $userId, + 'status' => 'pending' + ]) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->countAllResults(), + + 'completed' => $this->activityModel + ->where([ + 'assigned_to' => $userId, + 'status' => 'completed' + ]) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->countAllResults(), + ]; + + // ------------------------------- + // Leads Count (FY Based) + // ------------------------------- + $myLeadsCount = $this->leadModel + ->where('assigned_to', $userId) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->countAllResults(); + + // ------------------------------- + // Upcoming Activities (FY Based) + // ------------------------------- + $upcomingActivities = $this->activityModel + ->select('sales_activities.*, sales_actual_leads.company_name') + ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id') + ->where([ + 'sales_activities.assigned_to' => $userId, + 'sales_activities.status' => 'pending' + ]) + ->where('sales_activities.created_at >=', $fyStart) + ->where('sales_activities.created_at <=', $fyEnd) + ->orderBy('scheduled_date', 'ASC') + ->limit(3) ->findAll(); - return $achievedAmountData[0]['achieved_amount'] ?? 0.00; + // ------------------------------- + // Recent Leads (FY Based) + // ------------------------------- + $recentLeads = $this->leadModel + ->where('assigned_to', $userId) + ->where('created_at >=', $fyStart) + ->where('created_at <=', $fyEnd) + ->orderBy('created_at', 'DESC') + ->limit(5) + ->findAll(); + + // ------------------------------- + // Final Data + // ------------------------------- + $data = [ + 'target_amt' => $targetAmount, + 'achieved' => $achievedAmount, + 'remaining' => $remainingAmount, + 'percent' => $achievementPercent, + 'acts' => $activitySummary, + 'lead_count' => $myLeadsCount, + 'upcoming' => $upcomingActivities, + 'recent_leads' => $recentLeads, + 'fin_years' => $fin_years, + 'current_fin_year' => $current_fin_year, + 'display_fin_years' => format_financial_year($current_fin_year), + 'user_name' => get_session_userdata()->first_name ?? '', + 'tab_name' => 'Sales Dashboard', + 'page_name' => 'Sales Dashboard', + ]; + + return $this->loadLayout('sales/sales_manager_level_dashboard', $data); + + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); } +} + // public function dashboard() + // { + // $payload = $this->request->getGet(); + // $base = $this->getSalesStaffData(); + // $salesRole = $base['sales_role']; + // $salesManagerIds = $base['sales_manager_ids']; + // $userId = get_session_userid(); + + // // Get branch id from users array + // $nhanceBranchId = $base['users'][0]['nhance_branch_id'] ?? null; + + // if ($salesRole === 'Sales Head') { + // $this->branchLevelDashboard($nhanceBranchId, $salesManagerIds); + // // $this->salesManagerLevelDashboard($userId, $salesManagerIds,$payload); + // } elseif ($salesRole === 'Sales Manager') { + // $this->salesManagerLevelDashboard($userId, $salesManagerIds,$payload); + // } + // } + + // public function branchLevelDashboard($branchId,$sales_manager_ids) + // { + // // Hardcoded branch ID as requested + // // $branchId = 1; + + // try { + // $sales_manager_ids = array_values(array_map('intval', $sales_manager_ids)); + + // // Final safe check + // if (empty($sales_manager_ids)) { + // // No valid IDs — skip queries or return empty + // $total_leads = 0; + // $total_activity = 0; + // $total_completed_activity = 0; + // $total_pending_activity = 0; + // $pending_activities = []; + // $recent_activities = []; + // $teamPerformance = []; + + // $leadsOverview = []; + // } else { + + // if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) { + // $sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0 + // } + + // // 1. Lead Statistics + // $total_leads = $this->leadModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults(); // Use countAllResults, NOT countAll + // // echo $this->leadModel->getLastQuery();die(); + + // // 2. Total activity + // $total_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults(); + + // // 3. Completed activity + // $total_completed_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'completed')->countAllResults(); + + // // 4. Pending activity + // $total_pending_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'pending')->countAllResults(); + + // $db = \Config\Database::connect(); + + // // 5. Team Performance + // $teamPerformance = $db->table('user_profiles as u') + // ->select('u.first_name, u.last_name, r.role, + // (SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id) as total_acts, + // (SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id AND status = "completed") as done_acts') + // ->join('roles r', 'r.id = u.role') + // ->where('u.nhance_branch_id', $branchId) + // ->whereIn('u.id', $sales_manager_ids) + // ->where('u.is_active', 1) + // ->get()->getResultArray(); + + // // 6. Recent Activities (Joining for Lead Names) + // $recent_activities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name') + // ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id') + // ->orderBy('sales_activities.scheduled_date', 'DESC') + // ->limit(6) + // ->findAll(); + + // // 7. Pending Activities (List) + // $pending_activities = $db->table('sales_activities sa') + // ->select('sa.activity_id,sa.lead_id,sa.activity_type,sa.scheduled_date,sa.status,sa.assigned_to,sal.company_name,up.first_name AS assigned_to_name,sa.notes') + // ->join('user_profiles up', 'up.id = sa.assigned_to', 'left') + // ->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') // ✅ ADD THIS + // ->orderBy('sa.scheduled_date', 'DESC') + // ->whereIn('sa.assigned_to', $sales_manager_ids) + // ->where('sa.status', 'pending') + // ->get()->getResultArray(); + + // // 8. All Leads Overview + // $leadsOverview = $db->table('sales_actual_leads sal') + // ->select('sal.lead_id,sal.company_name,sal.status,up.first_name AS assigned_to, + // COUNT(DISTINCT sa.activity_id) AS activities, + // COUNT(DISTINCT l.id) AS opportunities + // ') + // ->join('user_profiles up', 'up.id = sal.assigned_to', 'left') + // ->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left') + // ->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left') + // ->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name') + // // ->having('COUNT(DISTINCT sa.activity_id) + COUNT(DISTINCT l.id) >', 0) // ← this line + // ->orderBy('sal.created_at', 'DESC') + // ->whereIn('sal.assigned_to', $sales_manager_ids) + // ->get() + // ->getResultArray(); + + // // 9. Activity BrakDown + // $activityBreakdown = $db->table('sales_activities') + // ->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total_activity}, 0) AS percentage", false) + // ->whereIn('assigned_to', $sales_manager_ids) + // ->groupBy('activity_type') + // ->orderBy('total', 'DESC') + // ->get() + // ->getResultArray(); + // } + // $data = [ + // 'total_leads' => $total_leads, + // 'total_acts' => $total_activity, + // 'total_completed_acts' => $total_completed_activity, + // 'total_pending_acts'=> $total_pending_activity, + // 'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14] + // 'display_fin_years' => format_financial_year($current_fin_year), + // 'team' => $teamPerformance, + // 'recent_acts' => $recent_activities, + // 'pending_acts' => $pending_activities, + // 'leads_overview' => $leadsOverview, + // 'activity_breakdown'=> $activityBreakdown, + // 'tab_name' => "Sales Dashboard", + // 'page_name' => "Sales Dashboard" + // ]; + + // // dd($data); + + // $this->loadLayout('sales/branch_level_dashboard_view', $data); + + // // return view('sales/dashboard_view', $data); + + // } catch (\Exception $e) { + // return $this->failServerError($e->getMessage()); + // } + // } + + // public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = []) + // { + // // $userId = get_session_userid(); + // // $userId = 1; + // $db = \Config\Database::connect(); + + // try { + + // // $payload = $this->request->getGet(); + // $current_fin_year = $payload['fy'] ?? getCurrentFinancialYear(); + + + // $fin_years = $db->table('sales_target') + // ->select('fy_year') + // ->where('user_id', $userId) + // ->orderBy('fy_year', 'desc') + // ->get() + // ->getResultArray(); + + // $fin_years = array_column($fin_years, 'fy_year'); + + // if(empty($fin_years)){ + // $fin_years[] = $current_fin_year; + // } + + // $target = $db->table('sales_target') + // ->where('user_id', $userId) + // ->where('fy_year', $current_fin_year) + // ->get() + // ->getRowArray(); + + // $targetAmount = $target['target_amount'] ?? 0.00; + + // // get achieved amount from leads table + // $achievedAmount = $this->getUserAchievedAmount($current_fin_year, $userId); + + // $remainingAmount = $targetAmount - $achievedAmount; + // // $achievementPercent = ($targetAmount > 0) ? round(($achievedAmount / $targetAmount) * 100) : 0; + // $achievementPercent = ($targetAmount > 0) ? min(100, round(($achievedAmount / $targetAmount) * 100)) : 0; + + // $activitySummary = [ + // 'total' => $this->activityModel->where('assigned_to', $userId)->countAllResults(), + // 'pending' => $this->activityModel->where(['assigned_to' => $userId, 'status' => 'pending'])->countAllResults(), + // 'completed' => $this->activityModel->where(['assigned_to' => $userId, 'status' => 'completed'])->countAllResults(), + // ]; + + // $myLeadsCount = $this->leadModel->where('assigned_to', $userId)->countAllResults(); + + // $upcomingActivities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name') + // ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id') + // ->where(['sales_activities.assigned_to' => $userId, 'sales_activities.status' => 'pending']) + // ->orderBy('scheduled_date', 'ASC') + // ->limit(3) + // ->findAll(); + + // $recentLeads = $this->leadModel->where('assigned_to', $userId) + // ->orderBy('created_at', 'DESC') + // ->limit(5) + // ->findAll(); + + // $data = [ + // 'target_amt' => $targetAmount, + // 'achieved' => $achievedAmount, + // 'remaining' => $remainingAmount, + // 'percent' => $achievementPercent, + // 'acts' => $activitySummary, + // 'lead_count' => $myLeadsCount, + // 'upcoming' => $upcomingActivities, + // 'recent_leads' => $recentLeads, + // 'fin_years' => $fin_years, + // 'display_fin_years' => format_financial_year($current_fin_year), + // 'user_name' => get_session_userdata()->first_namee ?? '', + // 'tab_name' => "Sales Dashboard", + // 'page_name' => "Sales Dashboard", + // 'splits' => [], + // 'activity_breakdown'=> [], + // ]; + + + + // // dd($data); + + // // return view('sales/my_dashboard_view', $data); + // $this->loadLayout('sales/sales_manager_level_dashboard', $data); + + // } catch (\Exception $e) { + // return $this->failServerError($e->getMessage()); + // } + // } + + // public function getUserAchievedAmount($financialYear, $userId) + // { + // // Split the string into two years + // $years = explode('-', $financialYear); + // $startYear = $years[0]; // 2025 + // $endYear = $years[1]; // 2026 + + // // Create the timestamps + // $startFY = $startYear . '-04-01 00:00:00'; + // $endFY = $endYear . '-03-31 23:59:59'; + + // $achievedAmountData = $this->leadModel + // ->select('SUM(leads.premium_amount) as achieved_amount') + // ->join('leads', 'sales_actual_leads.lead_id = leads.actual_lead_id') + // ->where('sales_actual_leads.assigned_to', $userId) + // ->where('leads.status', 'won') + // ->where('leads.updated_at >=', $startFY) + // ->where('leads.updated_at <=', $endFY) + // ->findAll(); + + // return $achievedAmountData[0]['achieved_amount'] ?? 0.00; + // } public function addCalenderEvent($input) { diff --git a/app/Models/LeadsModel.php b/app/Models/LeadsModel.php index 72ac2f59..3ba0e2bd 100644 --- a/app/Models/LeadsModel.php +++ b/app/Models/LeadsModel.php @@ -47,6 +47,7 @@ class LeadsModel extends Model 'proposel_data', 'status', 'notes', + 'lost_reason', 'created_at', 'created_by', 'updated_at', diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php index e9a3801d..a51713dd 100644 --- a/app/Models/SalesActivityModel.php +++ b/app/Models/SalesActivityModel.php @@ -25,6 +25,7 @@ class SalesActivityModel extends Model 'completion_notes', 'completed_date', 'parent_activity_id', + 'additional_assigned_ids', 'created_by', 'updated_by' ]; @@ -64,8 +65,14 @@ class SalesActivityModel extends Model */ public function getActivitiesByLead($leadId, $status = null) { - $builder = $this->select('sales_activities.*, user_profiles.first_name as assigned_to_name') - ->join('user_profiles', 'user_profiles.id = sales_activities.assigned_to', 'left') + // Convert the INT id to a string, then wrap it in JSON quotes to match ["5", "11"] + $subQuery = "(SELECT GROUP_CONCAT(up2.first_name SEPARATOR ', ') + FROM user_profiles up2 + WHERE JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR))) + ) as additional_assigned_names"; + + $builder = $this->select("sales_activities.*, up1.first_name as assigned_to_name, $subQuery") + ->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left') ->where('sales_activities.lead_id', $leadId); if ($status) { @@ -78,7 +85,7 @@ class SalesActivityModel extends Model /** * Get all sales_activities with filters */ - public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0) + public function getActivitiesWithFiltersOLD($filters = [], $limit = 10, $offset = 0) { $this->select('sales_activities.*, sales_actual_leads.company_name, user_profiles.first_name as assigned_to_name') ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left') @@ -139,6 +146,90 @@ class SalesActivityModel extends Model // ]; } + public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0) + { + // 1. Initial Selection + $builder = $this->select(" + sales_activities.*, + sales_actual_leads.company_name, + up1.first_name as assigned_to_name, + GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names + ") + ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left') + ->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left'); + + // 2. The JSON Join for additional names + // Only attempts join if the string looks like a JSON array + $builder->join('user_profiles as up2', " + sales_activities.additional_assigned_ids IS NOT NULL + AND sales_activities.additional_assigned_ids != '' + AND sales_activities.additional_assigned_ids != '[]' + AND JSON_VALID(sales_activities.additional_assigned_ids) + AND JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR))) + ", 'left'); + + // 3. Apply Filters + if (!empty($filters['status'])) { + $builder->where('sales_activities.status', $filters['status']); + } + + if (!empty($filters['activity_type'])) { + $builder->where('sales_activities.activity_type', $filters['activity_type']); + } + + if (!empty($filters['assigned_to'])) { + $assignedToIds = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']); + $builder->whereIn('sales_activities.assigned_to', $assignedToIds); + } + + if (!empty($filters['search'])) { + $builder->groupStart() + ->like('sales_actual_leads.company_name', $filters['search']) + ->orLike('up1.first_name', $filters['search']) + ->groupEnd(); + } + + // 4. Grouping & Ordering + $builder->groupBy('sales_activities.activity_id'); + $builder->orderBy('sales_activities.created_at', 'DESC'); + + // 5. Calculate Counts (using a clean builder to avoid the syntax error) + $counts = $this->getActivityStatusCounts($filters); + + // 6. Get Data and Total + // Use true for countAllResults to get an accurate count of grouped rows + $totalCountQuery = clone $builder; + $total = $totalCountQuery->countAllResults(false); + + $data = $builder->findAll($limit, $offset); + + return [ + 'data' => $data, + 'total' => $total, + 'counts' => $counts + ]; + } + + /** + * Helper function to get counts without breaking the main query syntax + */ + private function getActivityStatusCounts($filters) + { + $validStatuses = ['pending', 'completed']; + $counts = ['all' => 0, 'pending' => 0, 'completed' => 0]; + + foreach ($validStatuses as $status) { + $query = $this->db->table('sales_activities')->where('status', $status); + if (!empty($filters['assigned_to'])) { + $ids = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']); + $query->whereIn('assigned_to', $ids); + } + $counts[$status] = $query->countAllResults(); + } + $counts['all'] = $counts['pending'] + $counts['completed']; + return $counts; + } + /** * Complete an activity */ diff --git a/app/Models/SalesActualLeadModel.php b/app/Models/SalesActualLeadModel.php index 22db6a2a..a5ca1026 100644 --- a/app/Models/SalesActualLeadModel.php +++ b/app/Models/SalesActualLeadModel.php @@ -123,7 +123,38 @@ class SalesActualLeadModel extends Model $data = $this->findAll($limit, $offset); - return ['data' => $data,'total' => $total]; + $counts = $this->getLeadStatusCounts($filters); + + return ['data' => $data,'total' => $total,'counts' => $counts]; + } + + /** + * Helper function to get counts without breaking the main query syntax + */ + private function getLeadStatusCounts($filters) + { + + $validStatuses = ['New', 'Potential', 'Prospects', 'Not a Prospects']; + $counts = ['all' => 0, 'New' => 0, 'Potential' => 0, 'Prospects' => 0, 'Not a Prospects' => 0]; + + foreach ($validStatuses as $status) { + $countQuery = $this->db->table('sales_actual_leads') + ->whereIn('status', $validStatuses); + + // Apply assigned_to filter to counts too + if (!empty($filters['assigned_to'])) { + $assignedToIds = is_array($filters['assigned_to']) + ? $filters['assigned_to'] + : explode(',', $filters['assigned_to']); + $countQuery->whereIn('assigned_to', $assignedToIds); + } + + $counts[$status] = $countQuery->where('status', $status)->countAllResults(); + } + + $counts['all'] = array_sum($counts); + + return $counts; } /** diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index eeef3d1b..089bff40 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -401,6 +401,12 @@ +
    @@ -778,7 +784,8 @@ $('#client_name').val(res.data.client_name); $('#client_short_name').val(res.data.client_short_name); $('#entity_type_id').val(res.data.entity_type_id); - $('#lead_status').val(res.data.status); + $('#lead_status').val(res.data.status).trigger('change'); + $('#lost_reason').val(res.data.lost_reason || ''); $('#notes').val(res.data.notes); setTimeout(function() { @@ -2431,5 +2438,66 @@ }); + $(document).ready(function() { + + var actual_lead_client_details = ; + var actual_lead_contact_person_details = ; + + // ------------------------------- + // CLIENT DETAILS AUTO FILL + // ------------------------------- + if (actual_lead_client_details) { + + $('#client_name').val(actual_lead_client_details.company_name || ''); + + $('#gst').val(actual_lead_client_details.gst_number || ''); + + // 🔥 Important: + // Only auto-generate short name IF empty (avoid overwrite in edit) + if (!$('#client_short_name').val()) { + $('#client_name').trigger('input'); + } else { + // Run duplicate validation once + validateInput($('#client_short_name')[0], "clients", "short_name"); + } + } + + // ------------------------------- + // CONTACT PERSON AUTO FILL + // ------------------------------- + if (actual_lead_contact_person_details) { + + $('#contact_person_name').val(actual_lead_contact_person_details.name || ''); + + $('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || ''); + + $('#contact_person_email').val(actual_lead_contact_person_details.email || ''); + } + + + $('#lead_status').change(function() { + if ($(this).val() === 'lost') { + $('#lost_reason_div').show(); + $('#lost_reason').attr('required', 'required'); + } else { + $('#lost_reason_div').hide(); + $('#lost_reason').val(""); + $('#lost_reason').removeAttr('required'); + } + }); + + // Check on page load + if ($('#lead_status').val() === 'lost') { + $('#lost_reason_div').show(); + $('#lost_reason').attr('required', 'required'); + } else { + // Ensure it's hidden and not required if the initial value is not 'lost' + $('#lost_reason_div').hide(); + $('#lost_reason').val(""); + $('#lost_reason').removeAttr('required'); + } + }); + + //----------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/app/Views/leads_non_eb.php b/app/Views/leads_non_eb.php index c6749816..c4caca5f 100644 --- a/app/Views/leads_non_eb.php +++ b/app/Views/leads_non_eb.php @@ -410,6 +410,12 @@
    +
    @@ -1258,4 +1264,63 @@ } }); + $(document).ready(function() { + + var actual_lead_client_details = ; + var actual_lead_contact_person_details = ; + + // ------------------------------- + // CLIENT DETAILS AUTO FILL + // ------------------------------- + if (actual_lead_client_details) { + + $('#client_name').val(actual_lead_client_details.company_name || ''); + + $('#gst').val(actual_lead_client_details.gst_number || ''); + + // 🔥 Important: + // Only auto-generate short name IF empty (avoid overwrite in edit) + if (!$('#client_short_name').val()) { + $('#client_name').trigger('input'); + } else { + // Run duplicate validation once + validateInput($('#client_short_name')[0], "clients", "short_name"); + } + } + + // ------------------------------- + // CONTACT PERSON AUTO FILL + // ------------------------------- + if (actual_lead_contact_person_details) { + + $('#contact_person_name').val(actual_lead_contact_person_details.name || ''); + + $('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || ''); + + $('#contact_person_email').val(actual_lead_contact_person_details.email || ''); + } + + + $('#lead_status').change(function() { + if ($(this).val() === 'lost') { + $('#lost_reason_div').show(); + $('#lost_reason').attr('required', 'required'); + } else { + $('#lost_reason_div').hide(); + $('#lost_reason').val(""); + $('#lost_reason').removeAttr('required'); + } + }); + + // Check on page load + if ($('#lead_status').val() === 'lost') { + $('#lost_reason_div').show(); + $('#lost_reason').attr('required', 'required'); + } else { + // Ensure it's hidden and not required if the initial value is not 'lost' + $('#lost_reason_div').hide(); + $('#lost_reason').val(""); + $('#lost_reason').removeAttr('required'); + } + }); \ No newline at end of file diff --git a/app/Views/sales/activity_view.php b/app/Views/sales/activity_view.php index 3edb0181..da457b03 100644 --- a/app/Views/sales/activity_view.php +++ b/app/Views/sales/activity_view.php @@ -10,6 +10,8 @@ .btn-complete:hover { background: #4caf50; transform: translateY(-1px); } .btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;} .btn-view:hover { background: #f0f0f0; transform: translateY(-1px); } + .btn-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background .2s; } + .btn-close:hover { background: #f0f0f0; color: #333; } /* Filter Tabs */ .filter-tabs { display: flex; gap: 10px; padding: 20px 30px; } @@ -68,6 +70,7 @@ .activity-meta { display: flex; gap: 20px; font-size: 13px; color: #999; margin-top: 10px;} .lead-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-top: 5px; } + .opportunity-status-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;} .status-new { background: #e3f2fd; color: #1976d2; } .status-potential { background: #fff3e0; color: #f57c00; } @@ -76,6 +79,10 @@ .status-pending { background: #fff3e0; color: #f57c00; } .status-completed { background: #e8f5e9; color: #388e3c; } .status-unknown { background: #000; color: #fff; } + .status-text-unknown { color: #000; font-weight: bold; } + .status-text-lost { color: #d32f2f; font-weight: bold; } + .status-text-won { color: #388e3c; font-weight: bold; } + /* Modals */ .modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; } @@ -124,6 +131,9 @@ .opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;} .opportunity-detail-item { font-size: 13px; color: #666;} .opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; } + .lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ } + .footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; } + .notes { color: #555; } /* Base style for both tabs */ .tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; } @@ -142,6 +152,21 @@ outline: none; } +.select2-container--default +.select2-selection--multiple +.select2-selection__choice { + background-color: #02a8b5 !important; + border: none !important; + border-color: #fff !important; + color: #fff !important; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #fff !important; +} +.modal .select2-container--default .select2-selection--multiple { + background-color: #fff !important; +} + @@ -159,7 +184,7 @@
    + placeholder="Search activities..." onkeyup="fetchActivities(false)" style="width: 300px !important;"> @@ -191,7 +216,7 @@

    Lead Detail

    - +

    @@ -368,7 +409,7 @@
    - @@ -133,6 +133,9 @@
    --> + + + '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ]; $icon = $activityIcons[$u['activity_type']] ?? '📌'; @@ -160,11 +163,19 @@
    + + +
    + No Upcoming Activities for this Financial Year. +
    + +

    My Recent Leads

    +
    + placeholder="Search leads..." onkeyup="fetchLeads(false)" style="width: 300px !important;"> @@ -166,7 +189,7 @@ @@ -228,7 +251,7 @@

    Lead Detail

    - +