diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 566935a0..42aab688 100755
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -1011,6 +1011,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');
@@ -1057,6 +1060,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 66337c36..06876db1 100644
--- a/app/Controllers/LeadsController.php
+++ b/app/Controllers/LeadsController.php
@@ -744,7 +744,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);
@@ -4840,7 +4840,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 836ec3b8..fae264c6 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 @@