From 8261aea835cf62cc467fe7af377caa66fbfc29d8 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 12 May 2026 16:03:47 +0530 Subject: [PATCH] FIX_Sales Tracker Fixes --- app/Config/Routes.php | 6 + app/Controllers/LeadsController.php | 15 +- app/Controllers/SalesController.php | 304 +++++++++++++++++- app/Views/leads_form.php | 36 ++- app/Views/leads_non_eb.php | 33 +- app/Views/sales/activity_view.php | 100 ++++-- .../sales/branch_level_dashboard_view.php | 15 +- app/Views/sales/tracker_view.php | 191 ++++++++--- 8 files changed, 617 insertions(+), 83 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 081b44cd..908dfe62 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1010,6 +1010,9 @@ $routes->group('sales', function($routes) { // Get all leads with filters $routes->get('leads', 'SalesController::getLeads'); + + // Export leads by created date range + $routes->get('leads/export', 'SalesController::exportLeads'); // Get lead statistics $routes->get('leads/stats', 'SalesController::getLeadStats'); @@ -1056,6 +1059,9 @@ $routes->group('sales', function($routes) { // Get all activities with filters $routes->get('activities', 'SalesController::getActivities'); + + // Export activities by created date range + $routes->get('activities/export', 'SalesController::exportActivities'); // Get upcoming activities $routes->get('activities/upcoming', 'SalesController::getUpcomingActivities'); diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index ce2dc8f0..d4750154 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -680,7 +680,7 @@ class LeadsController extends BaseController $data['client_code'] = generate_client_code('IC'); } - if (isset($data['lead_form_type'])) { + if ((int) ($data['lead_form_type'] ?? 1) === 2) { $data = $this->prepareSingleLeadData($data); } else { $data = $this->prepareMultipleLeadData($data); @@ -4774,7 +4774,18 @@ class LeadsController extends BaseController // 🔹 Client Details (Single Row) $data['actual_lead_client_details'] = $this->leadModel - ->select('sales_actual_leads.company_name, clients.short_name, clients.client_type, sales_actual_leads.email, sales_actual_leads.phone, sales_actual_leads.address, sales_actual_leads.website, sales_actual_leads.gst_number, sales_actual_leads.status, sales_actual_leads.assigned_to') + ->select(" + sales_actual_leads.company_name, + clients.short_name, + clients.client_type, + sales_actual_leads.email, + sales_actual_leads.phone, + sales_actual_leads.address, + sales_actual_leads.website, + COALESCE(NULLIF(sales_actual_leads.gst_number, ''), clients.gst) AS gst_number, + sales_actual_leads.status, + sales_actual_leads.assigned_to + ", false) ->join('clients', 'clients.id = sales_actual_leads.client_id', 'left') ->where('sales_actual_leads.lead_id', $actual_lead_id) ->first(); // first row only diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 195ace88..659299cd 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -371,6 +371,102 @@ class SalesController extends BaseController } } + /** + * Export leads filtered by created_at date range. + * GET /sales/leads/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD + */ + public function exportLeads() + { + try { + $range = $this->getExportDateRange(); + } catch (\InvalidArgumentException $e) { + return $this->failValidationErrors($e->getMessage()); + } + + try { + $db = \Config\Database::connect(); + $builder = $db->table('sales_actual_leads sal') + ->select(' + sal.company_name, + c.short_name, + sal.address, + sal.website, + sal.gst_number, + sal.email, + sal.phone, + sal.status, + sal.created_at, + up.first_name as assigned_to_name, + COUNT(DISTINCT sa.activity_id) as total_activity + ') + ->join('clients c', 'c.id = sal.client_id', 'left') + ->join('user_profiles up', 'up.id = sal.assigned_to', 'left') + ->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left') + ->where("sal.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false); + + $status = $this->request->getGet('status'); + if (!empty($status)) { + $builder->where('sal.status', $status); + } + + $assignedToIds = $this->getExportAssignedToIds(); + if (!empty($assignedToIds)) { + $builder->whereIn('sal.assigned_to', $assignedToIds); + } + + $search = $this->request->getGet('search'); + if (!empty($search)) { + $builder->groupStart() + ->like('sal.company_name', $search) + ->orLike('c.short_name', $search) + ->orLike('sal.address', $search) + ->orLike('sal.website', $search) + ->orLike('sal.gst_number', $search) + ->orLike('sal.email', $search) + ->orLike('sal.phone', $search) + ->orLike('up.first_name', $search) + ->groupEnd(); + } + + $leads = $builder + ->groupBy('sal.lead_id, sal.company_name, c.short_name, sal.address, sal.website, sal.gst_number, sal.email, sal.phone, sal.status, sal.created_at, up.first_name') + ->orderBy('sal.created_at', 'DESC') + ->get() + ->getResultArray(); + + if (empty($leads)) { + return $this->exportNotFoundResponse("Leads Not Found {$range['display']}"); + } + + $rows = []; + $serialNo = 1; + foreach ($leads as $lead) { + $rows[] = [ + $serialNo++, + $lead['company_name'] ?? '', + $lead['short_name'] ?? '', + $lead['address'] ?? '', + $lead['website'] ?? '', + $lead['gst_number'] ?? '', + $lead['email'] ?? '', + $lead['phone'] ?? '', + $lead['status'] ?? '', + $lead['assigned_to_name'] ?? '', + $lead['total_activity'] ?? 0, + $this->formatExportDate($lead['created_at'] ?? ''), + ]; + } + + return $this->streamCsvDownload( + "sales-leads-{$range['from_label']}-to-{$range['to_label']}.csv", + ['S.No', 'Company', 'Company Short Name', 'Address', 'Website', 'GST', 'Email', 'Phone', 'Status', 'Assigned To', 'Total Activity', 'Created At'], + $rows + ); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + /** * Get single lead with complete details * GET /api/sales/leads/{id} @@ -757,6 +853,104 @@ class SalesController extends BaseController } } + /** + * Export activities filtered by created_at date range. + * GET /sales/activities/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD + */ + public function exportActivities() + { + try { + $range = $this->getExportDateRange(); + } catch (\InvalidArgumentException $e) { + return $this->failValidationErrors($e->getMessage()); + } + + try { + $db = \Config\Database::connect(); + $builder = $db->table('sales_activities sa') + ->select(" + sa.activity_type, + sa.status, + sa.scheduled_date, + sa.created_at, + sa.notes, + sa.completion_notes, + sal.company_name, + c.short_name, + up1.first_name as assigned_to_name, + GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names + ") + ->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') + ->join('clients c', 'c.id = sal.client_id', 'left') + ->join('user_profiles up1', 'up1.id = sa.assigned_to', 'left') + ->join('user_profiles up2', " + sa.additional_assigned_ids IS NOT NULL + AND sa.additional_assigned_ids != '' + AND sa.additional_assigned_ids != '[]' + AND JSON_VALID(sa.additional_assigned_ids) + AND JSON_CONTAINS(sa.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR))) + ", 'left') + ->where("sa.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false); + + $status = $this->request->getGet('status'); + if (!empty($status)) { + $builder->where('sa.status', $status); + } + + $assignedToIds = $this->getExportAssignedToIds(); + if (!empty($assignedToIds)) { + $builder->whereIn('sa.assigned_to', $assignedToIds); + } + + $search = $this->request->getGet('search'); + if (!empty($search)) { + $builder->groupStart() + ->like('sal.company_name', $search) + ->orLike('c.short_name', $search) + ->orLike('sa.status', $search) + ->orLike('sa.activity_type', $search) + ->orLike('up1.first_name', $search) + ->orLike('up2.first_name', $search) + ->groupEnd(); + } + + $activities = $builder + ->groupBy('sa.activity_id') + ->orderBy('sa.created_at', 'DESC') + ->get() + ->getResultArray(); + + if (empty($activities)) { + return $this->exportNotFoundResponse("Activities not Found {$range['display']}"); + } + + $rows = []; + $serialNo = 1; + foreach ($activities as $activity) { + $rows[] = [ + $serialNo++, + $activity['company_name'] ?? '', + $activity['short_name'] ?? '', + $activity['activity_type'] ?? '', + ucfirst((string) ($activity['status'] ?? '')), + $activity['assigned_to_name'] ?? '', + $activity['additional_assigned_names'] ?? '', + $this->formatExportDate($activity['scheduled_date'] ?? ''), + $this->formatExportDate($activity['created_at'] ?? ''), + $activity['notes'] ?: ($activity['completion_notes'] ?? ''), + ]; + } + + return $this->streamCsvDownload( + "sales-activities-{$range['from_label']}-to-{$range['to_label']}.csv", + ['S.No', 'Company', 'Company Short Name', 'Activity Type', 'Status', 'Assigned To', 'Additional Members', 'Scheduled Date', 'Created At', 'Notes'], + $rows + ); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + /** * Get activities for a specific lead * GET /api/sales/leads/{leadId}/activities @@ -1251,6 +1445,104 @@ class SalesController extends BaseController return $filters; } + private function getExportDateRange(): array + { + $fromDate = trim((string) $this->request->getGet('from_date')); + $toDate = trim((string) $this->request->getGet('to_date')); + + if ($fromDate === '' || $toDate === '') { + throw new \InvalidArgumentException('Please select from date and to date.'); + } + + $from = $this->normalizeExportDate($fromDate, '00:00:00'); + $to = $this->normalizeExportDate($toDate, '23:59:59'); + + if ($from === null || $to === null) { + throw new \InvalidArgumentException('Invalid date range. Use YYYY-MM-DD format.'); + } + + if (strtotime($from) > strtotime($to)) { + throw new \InvalidArgumentException('From date cannot be after to date.'); + } + + return [ + 'from' => $from, + 'to' => $to, + 'from_label' => date('Y-m-d', strtotime($from)), + 'to_label' => date('Y-m-d', strtotime($to)), + 'display' => date('d-m-Y', strtotime($from)) . ' to ' . date('d-m-Y', strtotime($to)), + ]; + } + + private function normalizeExportDate(string $date, string $time): ?string + { + $dateTime = \DateTime::createFromFormat('Y-m-d H:i:s', $date . ' ' . $time); + + if (!$dateTime || $dateTime->format('Y-m-d') !== $date) { + return null; + } + + return $dateTime->format('Y-m-d H:i:s'); + } + + private function getExportAssignedToIds(): array + { + $assignedTo = (string) $this->request->getGet('assigned_to'); + + if ($assignedTo === '') { + return []; + } + + return array_values(array_filter(array_map('trim', explode(',', $assignedTo)), static function ($id) { + return ctype_digit($id); + })); + } + + private function streamCsvDownload(string $filename, array $headers, array $rows) + { + $handle = fopen('php://temp', 'r+'); + fwrite($handle, "\xEF\xBB\xBF"); + fputcsv($handle, $headers); + + foreach ($rows as $row) { + fputcsv($handle, $row); + } + + rewind($handle); + $csv = stream_get_contents($handle); + fclose($handle); + + return $this->response + ->setHeader('Content-Type', 'text/csv; charset=UTF-8') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate') + ->setBody($csv); + } + + private function exportNotFoundResponse(string $message) + { + return $this->response + ->setStatusCode(ResponseInterface::HTTP_NOT_FOUND) + ->setJSON([ + 'status' => 'error', + 'message' => $message, + ]); + } + + private function formatExportDate(?string $value): string + { + if (empty($value)) { + return ''; + } + + $timestamp = strtotime($value); + if ($timestamp === false) { + return $value; + } + + return date('d/m/Y h:i A', $timestamp); + } + // ==================== Dashboard ==================== @@ -1707,15 +1999,23 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s // ── 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 + // leads.lead_form_type: 1 = EB, else = Non-EB + // leads.lead_type: 1 = Fresh, 2 = Renewal, 3 = Roll Over $wonLeads = $db->query(" SELECT l.id AS opportunities_id, l.actual_lead_id, + COALESCE(l.lead_form_type, 1) AS lead_form_type_id, l.lead_type AS lead_type_id, sal.company_name AS company, l.client_name AS client_name, - CASE WHEN l.lead_type = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_type, + CASE WHEN COALESCE(l.lead_form_type, 1) = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_form_type, + CASE + WHEN l.lead_type = 1 THEN 'Fresh' + WHEN l.lead_type = 2 THEN 'Renewal' + WHEN l.lead_type = 3 THEN 'Roll Over' + ELSE '' + END AS lead_type, l.created_at AS created_at, l.status FROM leads l diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 939d4b57..0aafa1ec 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -166,6 +166,7 @@ enctype="multipart/form-data"> + @@ -480,8 +481,24 @@