Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
0cfd5fb8ba
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -166,6 +166,7 @@
|
||||
enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" name="id" id="leads_primarykey">
|
||||
<input type="hidden" name="lead_form_type" id="lead_form_type_id" value="<?= isset($selected_lead_type) ? $selected_lead_type : 1 ?>">
|
||||
<input type="hidden" name="actual_lead_id" id="actual_lead_id" value="<?= isset($actual_lead_id) ? $actual_lead_id : 0 ?>">
|
||||
|
||||
|
||||
@ -480,8 +481,24 @@
|
||||
<script>
|
||||
var actualLeadClientName = '';
|
||||
var actualLeadShortName = '';
|
||||
var actualLeadGstNumber = '';
|
||||
var actualLeadContactDetails = null;
|
||||
let claimIndex = 1;
|
||||
|
||||
function restoreActualLeadGstNumber() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) {
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreActualLeadContactDetails() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadContactDetails) {
|
||||
$('#contact_person_name').val(actualLeadContactDetails.name || '');
|
||||
$('#contact_person_mobile').val(actualLeadContactDetails.mobile || '');
|
||||
$('#contact_person_email').val(actualLeadContactDetails.email || '');
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
var policyStart = $(".policy_start_date");
|
||||
var policyEnd = $(".policy_end_date");
|
||||
@ -1947,6 +1964,8 @@
|
||||
// --- Re-populate Actual Lead data if it was cleared ---
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0) {
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
// Re-generate short name from the stored client name to ensure consistency
|
||||
if (actualLeadClientName.trim() !== '') {
|
||||
let baseName = actualLeadClientName
|
||||
@ -2311,6 +2330,8 @@
|
||||
}
|
||||
|
||||
toggleRequiredFields();
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
@ -2516,6 +2537,8 @@
|
||||
}
|
||||
|
||||
toggleRequiredFields();
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
function toggleRequiredFields() {
|
||||
@ -2629,6 +2652,8 @@
|
||||
$("#client_name").prop("required", true);
|
||||
$('#client_name').closest('.form-group').show();
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
});
|
||||
|
||||
|
||||
@ -2644,7 +2669,8 @@
|
||||
actualLeadClientName = actual_lead_client_details.company_name || '';
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || '';
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') {
|
||||
$('#client_type').val(String(actual_lead_client_details.client_type));
|
||||
}
|
||||
@ -2695,12 +2721,8 @@
|
||||
// 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 || '');
|
||||
actualLeadContactDetails = actual_lead_contact_person_details;
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -493,6 +493,22 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
<script>
|
||||
var actualLeadClientName = '';
|
||||
var actualLeadShortName = '';
|
||||
var actualLeadGstNumber = '';
|
||||
var actualLeadContactDetails = null;
|
||||
|
||||
function restoreActualLeadGstNumber() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) {
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreActualLeadContactDetails() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadContactDetails) {
|
||||
$('#contact_person_name').val(actualLeadContactDetails.name || '');
|
||||
$('#contact_person_mobile').val(actualLeadContactDetails.mobile || '');
|
||||
$('#contact_person_email').val(actualLeadContactDetails.email || '');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#rfq_qcr_viewers').parsley({
|
||||
@ -871,6 +887,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
// --- Re-populate Actual Lead data if it was cleared ---
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0) {
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
if (actualLeadShortName.trim() !== '') {
|
||||
$('#client_short_name').val(actualLeadShortName);
|
||||
} else if (actualLeadClientName.trim() !== '') {
|
||||
@ -971,6 +989,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
}
|
||||
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
var allContacts = "";
|
||||
function getBranchData(input,inputType) {
|
||||
@ -1364,6 +1384,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
$("#client_id").prop("required", false);
|
||||
$("#client_branch_id").prop("required", false);
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
@ -1378,7 +1400,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
actualLeadClientName = actual_lead_client_details.company_name || '';
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || '';
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') {
|
||||
$('#client_type').val(String(actual_lead_client_details.client_type));
|
||||
}
|
||||
@ -1427,12 +1450,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
// 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 || '');
|
||||
actualLeadContactDetails = actual_lead_contact_person_details;
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -224,7 +224,7 @@
|
||||
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="toolbar-icon-btn export-btn" onclick="exportActivitiesCsv()" title="Export" aria-label="Export activities">
|
||||
<button class="toolbar-icon-btn export-btn" id="activityExportExcelBtn" title="Export Excel" aria-label="Export activities">
|
||||
<i class="mdi mdi-file-excel"></i>
|
||||
</button>
|
||||
<button class="toolbar-icon-btn add-btn" onclick="openMainActivityModal()" title="Add Activity" aria-label="Add activity">
|
||||
@ -1426,37 +1426,95 @@ function downloadCsv(filename, rows) {
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
async function exportActivitiesCsv() {
|
||||
function getExportFilename(response, fallbackFilename) {
|
||||
const disposition = response.headers.get('content-disposition') || '';
|
||||
const match = disposition.match(/filename="?([^"]+)"?/i);
|
||||
return match ? match[1] : fallbackFilename;
|
||||
}
|
||||
|
||||
async function downloadExportFile(url, fallbackFilename) {
|
||||
const response = await fetch(url);
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (!response.ok || contentType.includes('application/json')) {
|
||||
let message = 'Export failed. Please try again.';
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.message || json.messages?.error || message;
|
||||
} catch (err) {
|
||||
// Keep the generic message when the response cannot be parsed.
|
||||
}
|
||||
|
||||
return response.status === 404 ? toastr.warning(message) : toastr.error(message);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = getExportFilename(response, fallbackFilename);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function initActivityExportDateRangePicker() {
|
||||
const $button = $('#activityExportExcelBtn');
|
||||
if (!$button.length) return;
|
||||
|
||||
if (typeof moment === 'undefined' || typeof $.fn.daterangepicker === 'undefined') {
|
||||
$button.on('click', () => toastr.error('Date range picker is not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
$button.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
startDate: moment().startOf('month'),
|
||||
endDate: moment(),
|
||||
maxDate: moment(),
|
||||
opens: 'left',
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: 'DD-MM-YYYY',
|
||||
applyLabel: 'Generate Excel',
|
||||
cancelLabel: 'Cancel'
|
||||
},
|
||||
ranges: {
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment()],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
}
|
||||
});
|
||||
|
||||
$button.on('apply.daterangepicker', function (ev, picker) {
|
||||
exportActivitiesCsv(
|
||||
picker.startDate.format('YYYY-MM-DD'),
|
||||
picker.endDate.format('YYYY-MM-DD')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportActivitiesCsv(fromDate, toDate) {
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
|
||||
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
|
||||
if (!fromDate || !toDate) return toastr.warning('Please select a date range');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: document.getElementById('mainSearch')?.value || '',
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
financial_year: getSelectedFinancialYear()
|
||||
from_date: fromDate,
|
||||
to_date: toDate
|
||||
});
|
||||
|
||||
const res = await fetch(`${API}/activities?${params.toString()}`);
|
||||
const json = await res.json();
|
||||
const rows = [
|
||||
['Company', 'Activity Type', 'Status', 'Assigned To', 'Additional Members', 'Scheduled Date', 'Created At', 'Notes'],
|
||||
...(json.data || []).map(a => [
|
||||
a.company_name || '',
|
||||
a.activity_type || '',
|
||||
capitalize(a.status),
|
||||
a.assigned_to_name || '',
|
||||
a.additional_assigned_names || '',
|
||||
formatIndianDate(a.scheduled_date),
|
||||
formatIndianDate(a.created_at),
|
||||
a.notes || a.completion_notes || ''
|
||||
])
|
||||
];
|
||||
downloadCsv(`sales-activities-${getSelectedFinancialYear()}.csv`, rows);
|
||||
await downloadExportFile(
|
||||
`${API}/activities/export?${params.toString()}`,
|
||||
`sales-activities-${fromDate}-to-${toDate}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('load', initActivityExportDateRangePicker);
|
||||
initSalesToolbarFilters();
|
||||
fetchActivities();
|
||||
|
||||
|
||||
@ -942,20 +942,27 @@ function openModal(id) {
|
||||
/* ── Won Leads Table (Table 2) ── */
|
||||
const wonLeads = opp.won_leads || [];
|
||||
const wonRows = wonLeads.map(function(l) {
|
||||
const typeBadgeClass = l.lead_type === 'EB'
|
||||
// const opportunityType = [
|
||||
// l.lead_form_type || '',
|
||||
// l.lead_type || ''
|
||||
// ].filter(Boolean).join('/').toUpperCase();
|
||||
const opportunityType = [
|
||||
l.lead_form_type || '',
|
||||
].filter(Boolean).join('').toUpperCase();
|
||||
const typeBadgeClass = l.lead_form_type === 'EB'
|
||||
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
|
||||
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
|
||||
const canRedirect = l.lead_type_id && l.actual_lead_id && l.opportunities_id;
|
||||
const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id;
|
||||
const rowAttrs = canRedirect
|
||||
? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity('
|
||||
+ JSON.stringify(String(l.lead_type_id)) + ','
|
||||
+ JSON.stringify(String(l.lead_form_type_id)) + ','
|
||||
+ JSON.stringify(String(l.actual_lead_id)) + ','
|
||||
+ JSON.stringify(String(l.opportunities_id)) + ')\''
|
||||
: '';
|
||||
|
||||
return '<tr' + rowAttrs + '>'
|
||||
+ '<td style="font-weight:700;color:#0f172a;">' + (l.company || '—') + '</td>'
|
||||
+ '<td><span ' + typeBadgeClass + '>' + (l.lead_type || '—') + '</span></td>'
|
||||
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
|
||||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
|
||||
@ -200,7 +200,7 @@
|
||||
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="toolbar-icon-btn export-btn" onclick="exportLeadsCsv()" title="Export" aria-label="Export leads">
|
||||
<button class="toolbar-icon-btn export-btn" id="leadExportExcelBtn" title="Export Excel" aria-label="Export leads">
|
||||
<i class="mdi mdi-file-excel"></i>
|
||||
</button>
|
||||
<button class="toolbar-icon-btn add-btn" onclick="openModal('addLeadModal')" title="Add Lead" aria-label="Add lead">
|
||||
@ -672,31 +672,64 @@
|
||||
/* Reset Bootstrap's default position: absolute if present */
|
||||
position: static;
|
||||
}
|
||||
.contact-person-entry-row .form-control-sm {
|
||||
width: 100% !important;
|
||||
height: 31px;
|
||||
}
|
||||
.contact-person-entry-row {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.contact-person-entry-row .contact-field-col {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
.contact-person-entry-row .form-label {
|
||||
min-height: 18px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.contact-person-entry-row .contact-field-col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.contact-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 31px;
|
||||
}
|
||||
.contact-action-buttons .btn {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="col-12 ml-1">
|
||||
<label class="form-label fw-bold small ml-1">Contact Persons</label>
|
||||
<input type="hidden" id="editing_contact_id" value="">
|
||||
<div class="row g-0-5 align-items-end ml-1">
|
||||
<div class="col-md-3 col-12 mb-2 mb-md-0">
|
||||
<div class="row g-0-5 align-items-end ml-1 contact-person-entry-row">
|
||||
<div class="col-md-3 col-12 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Person Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control form-control-sm" id="contact_name" placeholder="Person Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, ''); validateContactField('name')">
|
||||
<small id="contact_name_error" class="text-danger d-block mt-1 field-error"></small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-3 col-6 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Mobile <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control form-control-sm" id="contact_mobile" placeholder="Mobile" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10); validateContactField('mobile')">
|
||||
<small id="contact_mobile_error" class="text-danger d-block mt-1 field-error"></small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-3 col-6 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Designation </label>
|
||||
<input type="text" class="form-control form-control-sm" id="contact_designation" placeholder="Designation" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 col-3">
|
||||
<div class="col-md-1 col-3 mb-2 mb-md-0">
|
||||
<label class="form-label"> </label>
|
||||
<div class="primary-container">
|
||||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||||
@ -710,13 +743,13 @@
|
||||
✔ Save
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="col-md-2 col-6">
|
||||
<div class="col-md-2 col-6 mb-2 mb-md-0">
|
||||
<label class="form-label"> </label>
|
||||
<div style="display:flex; gap:2px;">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:42%" title="Save">
|
||||
<div class="contact-action-buttons">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" title="Save">
|
||||
✔
|
||||
</button>
|
||||
<button type="button" id="btnClearContact" class="btn btn-sm btn-danger text-white" style="width:42%" title="Clear">
|
||||
<button type="button" id="btnClearContact" class="btn btn-sm btn-danger text-white" title="Clear">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@ -1261,8 +1294,9 @@ async function viewDetail(id) {
|
||||
function renderCard(opps) {
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
const opportunityFormType = Number(o.lead_form_type || 1);
|
||||
let lead_form_type = opportunityFormType === 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
@ -1287,7 +1321,7 @@ function renderCard(opps) {
|
||||
<div class="opportunities-list" id="leadOpportunitiesList">
|
||||
<div class="opportunity-card" style="bottom:2px;">
|
||||
<div class="opportunity-header">
|
||||
<div class="opportunity-title">${lead_type}</div>
|
||||
<div class="opportunity-title">${lead_form_type}</div>
|
||||
</div>
|
||||
<div class="opportunity-details">
|
||||
<div class="opportunity-detail-item">
|
||||
@ -2191,12 +2225,12 @@ function submitToRedirectwithactualLeadIDUrl(){
|
||||
|
||||
|
||||
let actual_lead_id = document.getElementById('opp_lead_id').value;
|
||||
let lead_type = $('input[name="lead_form_type"]:checked').val();
|
||||
let lead_form_type = $('input[name="lead_form_type"]:checked').val();
|
||||
|
||||
// URL: /type / actual_lead_id
|
||||
let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_type + '/' + actual_lead_id ;
|
||||
let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_form_type + '/' + actual_lead_id ;
|
||||
|
||||
console.log('lead_type ', lead_type);
|
||||
console.log('lead_form_type ', lead_form_type);
|
||||
console.log('opp_lead_id ', actual_lead_id);
|
||||
console.log('url ', url);
|
||||
|
||||
@ -2245,7 +2279,8 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
});
|
||||
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
async function openEditLeadModal(id, preservedFields = {}) {
|
||||
preservedFields = preservedFields || {};
|
||||
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
@ -2290,9 +2325,12 @@ async function openEditLeadModal(id) {
|
||||
form.querySelector('[name="company_name"]').value = lead.company_name || '';
|
||||
form.querySelector('[name="email"]').value = lead.email || '';
|
||||
form.querySelector('[name="phone"]').value = lead.phone || '';
|
||||
form.querySelector('[name="address"]').value = lead.address || '';
|
||||
form.querySelector('[name="website"]').value = lead.website || '';
|
||||
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
|
||||
form.querySelector('[name="address"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'address') ? preservedFields.address : (lead.address || '');
|
||||
form.querySelector('[name="website"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'website') ? preservedFields.website : (lead.website || '');
|
||||
form.querySelector('[name="gst_number"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'gst_number') ? preservedFields.gst_number : (lead.gst_number || '');
|
||||
// form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
@ -2384,15 +2422,27 @@ document.getElementById('contactPersonsList').onclick = async (e) => {
|
||||
if (!confirm('Are you sure you want to remove this contact?')) return;
|
||||
|
||||
try {
|
||||
const preservedFields = getEditLeadPreservedFields();
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
toastr.success('Removed successfully');
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value, preservedFields); // Refresh
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
};
|
||||
|
||||
function getEditLeadPreservedFields() {
|
||||
const form = document.getElementById('editLeadForm');
|
||||
if (!form) return {};
|
||||
|
||||
return {
|
||||
address: form.querySelector('[name="address"]')?.value || '',
|
||||
website: form.querySelector('[name="website"]')?.value || '',
|
||||
gst_number: form.querySelector('[name="gst_number"]')?.value || ''
|
||||
};
|
||||
}
|
||||
|
||||
// --- 2. SAVE / UPDATE LOGIC ---
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
@ -2417,6 +2467,7 @@ if (btnSaveContact) {
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const preservedFields = getEditLeadPreservedFields();
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -2430,7 +2481,7 @@ if (btnSaveContact) {
|
||||
resetContactForm();
|
||||
|
||||
// Refresh List
|
||||
openEditLeadModal(leadId);
|
||||
openEditLeadModal(leadId, preservedFields);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
if (err.messages?.name) setContactFieldError('name', err.messages.name);
|
||||
@ -2537,35 +2588,95 @@ function downloadCsv(filename, rows) {
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
async function exportLeadsCsv() {
|
||||
function getExportFilename(response, fallbackFilename) {
|
||||
const disposition = response.headers.get('content-disposition') || '';
|
||||
const match = disposition.match(/filename="?([^"]+)"?/i);
|
||||
return match ? match[1] : fallbackFilename;
|
||||
}
|
||||
|
||||
async function downloadExportFile(url, fallbackFilename) {
|
||||
const response = await fetch(url);
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (!response.ok || contentType.includes('application/json')) {
|
||||
let message = 'Export failed. Please try again.';
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.message || json.messages?.error || message;
|
||||
} catch (err) {
|
||||
// Keep the generic message when the response cannot be parsed.
|
||||
}
|
||||
|
||||
return response.status === 404 ? toastr.warning(message) : toastr.error(message);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = getExportFilename(response, fallbackFilename);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function initLeadExportDateRangePicker() {
|
||||
const $button = $('#leadExportExcelBtn');
|
||||
if (!$button.length) return;
|
||||
|
||||
if (typeof moment === 'undefined' || typeof $.fn.daterangepicker === 'undefined') {
|
||||
$button.on('click', () => toastr.error('Date range picker is not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
$button.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
startDate: moment().startOf('month'),
|
||||
endDate: moment(),
|
||||
maxDate: moment(),
|
||||
opens: 'left',
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: 'DD-MM-YYYY',
|
||||
applyLabel: 'Generate Excel',
|
||||
cancelLabel: 'Cancel'
|
||||
},
|
||||
ranges: {
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment()],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
}
|
||||
});
|
||||
|
||||
$button.on('apply.daterangepicker', function (ev, picker) {
|
||||
exportLeadsCsv(
|
||||
picker.startDate.format('YYYY-MM-DD'),
|
||||
picker.endDate.format('YYYY-MM-DD')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportLeadsCsv(fromDate, toDate) {
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
|
||||
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
|
||||
if (!fromDate || !toDate) return toastr.warning('Please select a date range');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: document.getElementById('mainSearch')?.value || '',
|
||||
limit: 10000,
|
||||
offset: 0,
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
financial_year: getSelectedFinancialYear()
|
||||
from_date: fromDate,
|
||||
to_date: toDate
|
||||
});
|
||||
|
||||
const res = await fetch(`${API}/leads?${params.toString()}`);
|
||||
const json = await res.json();
|
||||
const rows = [
|
||||
['Company', 'Email', 'Phone', 'Status', 'Assigned To', 'Created At'],
|
||||
...(json.data || []).map(l => [
|
||||
l.company_name || '',
|
||||
l.email || '',
|
||||
l.phone || '',
|
||||
capitalizeStatus(l.status),
|
||||
l.assigned_to_name || '',
|
||||
formatIndianDate(l.created_at)
|
||||
])
|
||||
];
|
||||
downloadCsv(`sales-leads-${getSelectedFinancialYear()}.csv`, rows);
|
||||
await downloadExportFile(
|
||||
`${API}/leads/export?${params.toString()}`,
|
||||
`sales-leads-${fromDate}-to-${toDate}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('load', initLeadExportDateRangePicker);
|
||||
initSalesToolbarFilters();
|
||||
fetchLeads().then(() => {
|
||||
const leadId = new URLSearchParams(window.location.search).get('lead_id');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user