-
+
All
- Pending
- Completed
+ Pending
+ Completed
-
@@ -448,12 +482,15 @@ document.addEventListener('DOMContentLoaded', function () {
// Single selects
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
let parentModal = el.closest('.modal');
- $(el).select2({
- placeholder: "Select..",
+ const select2Options = {
allowClear: false,
minimumResultsForSearch: 0,
dropdownParent: parentModal ? $(parentModal) : $(document.body)
- });
+ };
+ if (el.id !== 'memberFilter') {
+ select2Options.placeholder = "Select..";
+ }
+ $(el).select2(select2Options);
});
// Multi selects
@@ -509,6 +546,7 @@ const activityIcons = {
const salesManagerWithHeadIds = = json_encode($sales_manager_with_head_ids ?? []) ?>;
const API = '= base_url('sales') ?>';
+const defaultFinancialYear = '= esc($current_fin_year ?? '') ?>';
let filter = 'all';
let lead_id = null;
let global_lead_assigned_to = null;
@@ -519,6 +557,58 @@ let limit = 10;
let currentOffset = 0;
const department = '= $sales_role ?>';
+function getCurrentFY() {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = today.getMonth();
+ const startYear = month >= 3 ? year : year - 1;
+ return startYear + '-' + String(startYear + 1);
+}
+
+function initSalesToolbarFilters() {
+ const fySelect = document.getElementById('financialYearFilter');
+ const params = new URLSearchParams(window.location.search);
+
+ if (fySelect) {
+ let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
+ const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
+ if (!matched && fySelect.options.length > 0) {
+ fy = fySelect.options[0].value;
+ }
+ fySelect.value = fy;
+ }
+
+ const statusParam = (params.get('status') || 'all').toLowerCase();
+ const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
+ .find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
+ if (statusTab) {
+ document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
+ statusTab.classList.add('active');
+ filter = statusParam;
+ }
+
+ const memberSelect = document.getElementById('memberFilter');
+ const memberId = params.get('member');
+ if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
+ memberSelect.value = memberId;
+ if (window.jQuery) {
+ $(memberSelect).trigger('change.select2');
+ }
+ }
+}
+
+function getSelectedMemberIds(defaultIds) {
+ const memberFilter = document.getElementById('memberFilter');
+ if (memberFilter && memberFilter.value) {
+ return [memberFilter.value];
+ }
+ return Array.isArray(defaultIds) ? defaultIds : [];
+}
+
+function getSelectedFinancialYear() {
+ return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
+}
+
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
@@ -583,7 +673,11 @@ function setFilter(val, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
filter = val;
- fetchActivities();
+ const searchInput = document.getElementById('mainSearch');
+ if (searchInput) {
+ searchInput.value = '';
+ }
+ fetchActivities(false);
}
async function fetchActivities(isLoadMore = false) {
@@ -592,6 +686,7 @@ async function fetchActivities(isLoadMore = false) {
const btnLoadMore = document.getElementById('btn-load-more');
const spinner = document.getElementById('load-more-spinner');
const text = document.getElementById('load-more-text');
+ const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
// 1. Define the empty state HTML early so we can use it immediately if needed
@@ -606,7 +701,7 @@ async function fetchActivities(isLoadMore = false) {
console.log(salesManagerWithHeadIds);
// return;
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
- if (typeof salesManagerWithHeadIds === 'undefined' || salesManagerWithHeadIds.length === 0) {
+ if (selectedMemberIds.length === 0) {
console.log("No Sales Manager IDs found. Skipping API call.");
grid.innerHTML = emptyStateHTML;
btnLoadMore.style.display = 'none'; // Hide the load more button
@@ -631,11 +726,15 @@ async function fetchActivities(isLoadMore = false) {
}
// 5. Build URL with dynamic offset
- let url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
- // url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
- if (typeof salesManagerWithHeadIds !== 'undefined' && salesManagerWithHeadIds.length > 0) {
- url += `&assigned_to=${salesManagerWithHeadIds.join(',')}`;
- }
+ const params = new URLSearchParams({
+ status: filter === 'all' ? '' : filter,
+ search: q,
+ limit,
+ offset: currentOffset,
+ assigned_to: selectedMemberIds.join(','),
+ financial_year: getSelectedFinancialYear()
+ });
+ let url = `${API}/activities?${params.toString()}`;
console.log("Fetching API:", url);
@@ -1289,6 +1388,68 @@ function convertDBFormatted(input) {
);
}
+function csvEscape(value) {
+ const text = String(value ?? '').replace(/"/g, '""');
+ return `"${text}"`;
+}
+
+function formatIndianDate(value) {
+ if (!value) return '';
+ const date = new Date(String(value).replace(' ', 'T'));
+ if (Number.isNaN(date.getTime())) return value;
+
+ return date.toLocaleString('en-IN', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true
+ }).replace(',', '').toUpperCase();
+}
+
+function downloadCsv(filename, rows) {
+ const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+}
+
+async function exportActivitiesCsv() {
+ const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
+ if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
+
+ const params = new URLSearchParams({
+ status: filter === 'all' ? '' : filter,
+ search: document.getElementById('mainSearch')?.value || '',
+ limit: 10000,
+ offset: 0,
+ assigned_to: selectedMemberIds.join(','),
+ financial_year: getSelectedFinancialYear()
+ });
+
+ 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);
+}
+
+initSalesToolbarFilters();
fetchActivities();
\ No newline at end of file
diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php
index 6fec92ed..74b44c21 100644
--- a/app/Views/sales/branch_level_dashboard_view.php
+++ b/app/Views/sales/branch_level_dashboard_view.php
@@ -67,6 +67,7 @@
.card-hero:nth-child(3)::before { background: linear-gradient(90deg, #fda085, #f6d365); }
.card-hero:nth-child(4)::before { background: linear-gradient(90deg, #43e97b, #38f9d7); }
.card-hero:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.09); }
+ .card-hero.stat-link { cursor: pointer; }
.stat-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-bottom: 14px; }
.stat-val { font-size: 30px; font-weight: 800; color: #1a202c; line-height: 1; }
.stat-label { color: #8896aa; font-size: 13px; margin-top: 6px; font-weight: 600; }
@@ -114,6 +115,8 @@
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
.leads-table-wrap { max-height: 480px; overflow-y: auto; }
.leads-table-wrap thead th { position: sticky; top: 0; background: white; z-index: 1; }
+ .activity-count-link { display: inline-block; color: #2563eb; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
+ .activity-count-muted { display: inline-block; color: #8896aa; cursor: help; }
.empty-state { text-align: center; padding: 50px 20px; color: #aaa; }
.empty-icon { width: 70px; height: 70px; margin: 0 auto 16px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32px; }
@@ -178,6 +181,14 @@
.ms-num { font-size: 19px; font-weight: 800; }
.ms-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; letter-spacing: .05em; margin-top: 2px; }
.modal-body { padding: 22px 24px; overflow-y: auto; flex: 1; }
+ .target-edit-panel { display: grid; grid-template-columns: 1fr 170px 120px; gap: 12px; align-items: end; padding: 16px; margin-bottom: 18px; border: 1px solid #bfdbfe; border-radius: 14px; background: #eff6ff; }
+ .target-edit-title { font-size: 13px; font-weight: 800; color: #1e3a8a; margin-bottom: 4px; }
+ .target-edit-help { font-size: 11px; color: #64748b; font-weight: 600; }
+ .target-edit-input { width: 100%; height: 38px; border: 1px solid #93c5fd; border-radius: 9px; padding: 8px 12px; font-size: 14px; font-weight: 700; color: #0f172a; background: #fff; }
+ .target-edit-input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14); }
+ .target-edit-btn { height: 38px; border: none; border-radius: 9px; background: #2563eb; color: #fff; font-size: 13px; font-weight: 800; cursor: pointer; transition: background .2s, transform .2s; }
+ .target-edit-btn:hover { background: #1d4ed8; transform: translateY(-1px); }
+ .target-edit-btn:disabled { background: #94a3b8; cursor: not-allowed; transform: none; }
/* ── Modal Tabs ── */
.modal-tabs { display: flex; gap: 4px; background: #f1f5f9; border-radius: 10px; padding: 4px; margin-bottom: 20px; }
@@ -219,7 +230,7 @@
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
@media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } }
- @media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
+ @media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
@@ -243,22 +254,22 @@
-
+
-
👥
Total Leads
+
-
⚡
Total Activities
+
-
⏳
Pending Activities
+
- = esc($lead['company_name']) ?>
+
+
+
+ = esc($lead['company_name']) ?>
+
+
+
= esc($lead['status']) ?>
= esc($lead['assigned_to'] ?? 'Unassigned') ?>
-
+
+
@@ -470,11 +505,155 @@ const OPP_DATA = ;
/* ── JS Helpers ── */
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
const ACT_COLORS = {Call:'#4f46e5',Email:'#10b981',Meeting:'#06b6d4',Visit:'#f59e0b',Demo:'#ec4899','Share Docs':'#f97316','To Do':'#64748b'};
-const fmt = v => '₹' + (v / 100000).toFixed(1) + 'L';
+const fmtIndianFull = value => '₹' + (Number(value) || 0).toLocaleString('en-IN', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+});
+const fmt = value => {
+ const amount = Number(value) || 0;
+ let display = '₹0';
+ if (Math.abs(amount) >= 100000) {
+ display = '₹' + (amount / 100000).toFixed(1) + 'L';
+ } else if (Math.abs(amount) >= 1000) {
+ display = '₹' + (amount / 1000).toFixed(1) + 'K';
+ } else if (amount !== 0) {
+ display = '₹' + amount.toLocaleString('en-IN', { maximumFractionDigits: 0 });
+ }
+ return '' + display + '';
+};
const fmtFull = v => '₹' + Number(v).toLocaleString('en-IN');
const pct = (a, t) => t > 0 ? Math.min(100, Math.round(a / t * 100)) : 0;
const badge = p => p >= 100 ? ['b-achieved','🏆 Achieved'] : p >= 75 ? ['b-ontrack','🎯 On Track'] : p >= 50 ? ['b-behind','⚡ Behind'] : ['b-atrisk','⚠️ At Risk'];
const progColor= p => p >= 100 ? 'linear-gradient(90deg,#10b981,#34d399)' : p >= 75 ? 'linear-gradient(90deg,#06b6d4,#67e8f9)' : p >= 50 ? 'linear-gradient(90deg,#f97316,#fbbf24)' : 'linear-gradient(90deg,#ef4444,#fca5a5)';
+const SALES_LEADS_URL = '= base_url('sales') ?>';
+const SALES_ACTIVITIES_URL = '= base_url('sales/loadactivities') ?>';
+const SALES_TARGETS_URL = '= base_url('sales/targets') ?>';
+
+function getSelectedDashboardFY() {
+ return document.getElementById('financial_year')?.value || getCurrentFY();
+}
+
+function goSalesPage(page, status, memberId) {
+ const url = new URL(page === 'activities' ? SALES_ACTIVITIES_URL : SALES_LEADS_URL, window.location.origin);
+ url.searchParams.set('fy', getSelectedDashboardFY());
+ url.searchParams.set('status', status || 'all');
+ if (memberId) {
+ url.searchParams.set('member', memberId);
+ }
+ window.location.href = url.toString();
+}
+
+function goLeadActivities(memberId) {
+ if (!memberId) return;
+ goSalesPage('activities', 'all', memberId);
+}
+
+function goLeadDetail(leadId) {
+ if (!leadId) return;
+ const url = new URL(SALES_LEADS_URL, window.location.origin);
+ url.searchParams.set('fy', getSelectedDashboardFY());
+ url.searchParams.set('status', 'all');
+ url.searchParams.set('lead_id', leadId);
+ window.location.href = url.toString();
+}
+
+function showToast(type, message) {
+ if (window.toastr && typeof toastr[type] === 'function') {
+ toastr[type](message);
+ return;
+ }
+ alert(message);
+}
+
+function refreshTargetInDashboard(userId, targetId, targetAmount) {
+ const member = TEAM.find(item => item.id == userId);
+ if (!member) return;
+
+ member.target_id = targetId || member.target_id;
+ member.target_amt = targetAmount;
+ if (Array.isArray(member.splits)) {
+ member.splits.forEach(split => {
+ split.target = Number((targetAmount / 4).toFixed(2));
+ });
+ }
+
+ if (OPP_DATA[userId]) {
+ OPP_DATA[userId].target_amt = targetAmount;
+ }
+
+ renderBranchSummary();
+ renderList();
+}
+
+async function getSavedTargetRecord(userId, fyYear, fallbackTarget, forceFetch = false) {
+ if (!forceFetch && fallbackTarget?.id && fallbackTarget?.target_amount) {
+ return fallbackTarget;
+ }
+
+ const res = await fetch(`${SALES_TARGETS_URL}/user/${userId}`);
+ if (!res.ok) {
+ return fallbackTarget || {};
+ }
+
+ const json = await res.json();
+ return (json.data || []).find(record => record.fy_year === fyYear) || fallbackTarget || {};
+}
+
+async function saveModalTarget(targetId, userId, fyYear) {
+ const input = document.getElementById('modalTargetAmount');
+ const button = document.getElementById('btnSaveModalTarget');
+ const amount = parseInt(input?.value || '0', 10);
+
+ if (!amount || amount <= 0) {
+ input?.focus();
+ showToast('warning', 'Please enter a valid target amount.');
+ return;
+ }
+
+ const originalText = button ? button.textContent : '';
+ if (button) {
+ button.disabled = true;
+ button.textContent = 'Saving...';
+ }
+
+ try {
+ const isUpdate = !!targetId;
+ const res = await fetch(isUpdate ? `${SALES_TARGETS_URL}/${targetId}` : SALES_TARGETS_URL, {
+ method: isUpdate ? 'PUT' : 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ user_id: userId,
+ fy_year: fyYear,
+ target_amount: amount
+ })
+ });
+ const result = await res.json();
+
+ if (!res.ok) {
+ throw new Error(result?.message || 'Unable to update target amount.');
+ }
+
+ const activeTabIndex = Array.from(document.querySelectorAll('.modal-tab'))
+ .findIndex(tab => tab.classList.contains('active'));
+ const updatedTarget = await getSavedTargetRecord(userId, fyYear, result?.data || {}, !isUpdate);
+ const updatedAmount = Number(updatedTarget.target_amount ?? amount);
+
+ refreshTargetInDashboard(userId, updatedTarget.id || targetId, updatedAmount);
+ openModal(userId);
+ if (activeTabIndex > 0) {
+ const tabButton = document.querySelectorAll('.modal-tab')[activeTabIndex];
+ if (tabButton) switchTab(activeTabIndex, tabButton);
+ }
+
+ showToast('success', result?.message || (isUpdate ? 'Target amount updated successfully.' : 'Target amount created successfully.'));
+ } catch (err) {
+ showToast('error', err.message || 'Unable to save target amount.');
+ if (button) {
+ button.disabled = false;
+ button.textContent = originalText || 'Save Target';
+ }
+ }
+}
/* ── Date formatters ── */
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
@@ -608,6 +787,19 @@ function openModal(id) {
+ ''
+ '';
+ const hasTargetRecord = !!m.target_id;
+ const targetEditorHtml =
+ '
✅
Completed Activities
@@ -349,9 +360,14 @@
= $lead['activities'] ?>
+ 0): ?>
+ = $lead['activities'] ?>
+
+ = $lead['activities'] ?>
+
+
+ = $lead['opportunities'] ?>
' + fmt(rem) + '
REMAINING
' + p + '%
ACHIEVED %
'
+ + '' + (hasTargetRecord ? 'Update Target' : 'Create Target') + ' '
+ + '
';
+
/* ── Overall progress block (shared) ── */
const overallHtml =
''
+ + '
'
+ + '' + (hasTargetRecord ? 'Edit Target Amount' : 'Create Target Amount') + '
'
+ + '' + (hasTargetRecord ? 'Update' : 'Create') + ' target for ' + m.first_name + ' - ' + currentFY + '
'
+ + ''
+ + ''
+ + '
'
+ + ''
@@ -767,7 +959,8 @@ function openModal(id) {
/* ── Inject tabs ── */
document.getElementById('modalBody').innerHTML =
- '
-
'
+ targetEditorHtml
+ + '
-->
'
+ '📊 ' + fyLabel + ' Split-wise Achievement '
+ '🏆 Opportunities Achievement '
+ '
'
diff --git a/app/Views/sales/tracker_view.php b/app/Views/sales/tracker_view.php
index a8a755ed..392c3f99 100644
--- a/app/Views/sales/tracker_view.php
+++ b/app/Views/sales/tracker_view.php
@@ -12,12 +12,32 @@
/* Filter Tabs */
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
- .tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
+ .tab { padding: 10px 14px; border-radius: 20px; cursor: pointer; font-size: 12px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; }
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
/* Leads Grid */
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
- .lead-actions { display: flex; align-items: center; gap: 10px; }
+ .lead-actions { display: flex; align-items: center; gap: 4px; }
+ .sales-toolbar { flex-wrap: nowrap; padding: 16px 30px; gap: 12px; overflow-x: auto; }
+ .sales-toolbar .filter-tabs { padding: 0; flex: 1 1 auto; min-width: 0; overflow-x: auto; }
+ .sales-toolbar .lead-actions { flex: 0 0 auto; flex-wrap: nowrap; }
+ .sales-toolbar .search-input { width: 230px !important; }
+ .sales-toolbar .toolbar-select { width: 175px !important; min-width: 175px; padding: 9px 12px; }
+ .sales-toolbar .toolbar-select + .select2-container { width: 175px !important; min-width: 175px; }
+ .sales-toolbar .financial-year-select { width: 108px !important; min-width: 108px; }
+ .sales-toolbar .select2-container .select2-selection--single { height: 38px; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; }
+ .sales-toolbar .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 36px; }
+ .sales-toolbar .select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
+ .sales-toolbar #memberFilter + .select2-container { width: 175px !important; min-width: 175px; flex: 0 0 175px; }
+ .sales-toolbar #memberFilter + .select2-container .select2-selection--single { height: 38px !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 6px !important; display: flex; align-items: center; }
+ .sales-toolbar #memberFilter + .select2-container .select2-selection__rendered { flex: 1; line-height: 36px !important; padding-left: 12px; padding-right: 28px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .sales-toolbar #memberFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; }
+ .toolbar-icon-btn { width: 38px; height: 38px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.2s; }
+ .toolbar-icon-btn:hover { transform: translateY(-1px); filter: brightness(0.96); }
+ .toolbar-icon-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(2, 168, 181, 0.25); }
+ .toolbar-icon-btn i { font-size: 20px; line-height: 1; }
+ .toolbar-icon-btn.export-btn { background: #1f9d55; }
+ .toolbar-icon-btn.add-btn { background: #02a8b5; }
.leads-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 0 30px 30px; overflow-y: auto; }
.lead-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; cursor: pointer; transition: all 0.2s; }
.lead-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
@@ -146,7 +166,7 @@
Not a Prospects
-
+
@@ -160,9 +180,23 @@
-
- + Add Lead
+ placeholder="Search leads..." onkeyup="fetchLeads(false)">
+
+
+
+
+
+
+
@@ -729,12 +763,15 @@ document.addEventListener('DOMContentLoaded', function () {
// Single selects
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
let parentModal = el.closest('.modal');
- $(el).select2({
- placeholder: "Select..",
+ const select2Options = {
allowClear: false,
minimumResultsForSearch: 0,
dropdownParent: parentModal ? $(parentModal) : $(document.body)
- });
+ };
+ if (el.id !== 'memberFilter') {
+ select2Options.placeholder = "Select..";
+ }
+ $(el).select2(select2Options);
});
// Multi selects
@@ -771,6 +808,7 @@ const API = '= base_url('sales') ?>';
const SALES_CHECK_DUPLICATE_URL = '= base_url('sales/checkDuplicate') ?>';
/** Normalized company name when edit modal opened — same name retyped after clear must not count as duplicate */
let trackerEditLeadOriginalCompanyNorm = '';
+const defaultFinancialYear = '= esc($current_fin_year ?? '') ?>';
let filter = 'all';
let lead_id = null;
let global_lead_assigned_to = null;
@@ -780,6 +818,58 @@ let currentPage = 1;
let limit = 9;
let currentOffset = 0;
+function getCurrentFY() {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = today.getMonth();
+ const startYear = month >= 3 ? year : year - 1;
+ return startYear + '-' + String(startYear + 1);
+}
+
+function initSalesToolbarFilters() {
+ const fySelect = document.getElementById('financialYearFilter');
+ const params = new URLSearchParams(window.location.search);
+
+ if (fySelect) {
+ let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
+ const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
+ if (!matched && fySelect.options.length > 0) {
+ fy = fySelect.options[0].value;
+ }
+ fySelect.value = fy;
+ }
+
+ const statusParam = (params.get('status') || 'all').toLowerCase();
+ const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
+ .find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
+ if (statusTab) {
+ document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
+ statusTab.classList.add('active');
+ filter = statusTab.dataset.filter || 'all';
+ }
+
+ const memberSelect = document.getElementById('memberFilter');
+ const memberId = params.get('member');
+ if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
+ memberSelect.value = memberId;
+ if (window.jQuery) {
+ $(memberSelect).trigger('change.select2');
+ }
+ }
+}
+
+function getSelectedMemberIds(defaultIds) {
+ const memberFilter = document.getElementById('memberFilter');
+ if (memberFilter && memberFilter.value) {
+ return [memberFilter.value];
+ }
+ return Array.isArray(defaultIds) ? defaultIds : [];
+}
+
+function getSelectedFinancialYear() {
+ return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
+}
+
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
// function closeModal(id) { document.getElementById(id).classList.remove('active'); }
@@ -940,7 +1030,11 @@ function setFilter(val, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
filter = val;
- fetchLeads();
+ const searchInput = document.getElementById('mainSearch');
+ if (searchInput) {
+ searchInput.value = '';
+ }
+ fetchLeads(false);
}
// 1. Leads Grid Logic
@@ -970,6 +1064,7 @@ async function fetchLeads(isLoadMore = false) {
const btnLoadMore = document.getElementById('btn-load-more');
const spinner = document.getElementById('load-more-spinner');
const text = document.getElementById('load-more-text');
+ const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
// 1. Define the empty state HTML early so we can use it immediately if needed
const emptyStateHTML = `
@@ -980,7 +1075,7 @@ async function fetchLeads(isLoadMore = false) {
`;
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
- if (typeof salesManagerHeadIds === 'undefined' || salesManagerHeadIds.length === 0) {
+ if (selectedMemberIds.length === 0) {
console.log("No Sales Manager IDs found. Skipping API call.");
grid.innerHTML = emptyStateHTML;
btnLoadMore.style.display = 'none'; // Hide the load more button
@@ -1005,11 +1100,15 @@ async function fetchLeads(isLoadMore = false) {
}
// 5. Build URL with dynamic offset
- let url = `${API}/leads?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
- // url += `&assigned_to=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
- if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
- url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
- }
+ const params = new URLSearchParams({
+ status: filter === 'all' ? '' : filter,
+ search: q,
+ limit,
+ offset: currentOffset,
+ assigned_to: selectedMemberIds.join(','),
+ financial_year: getSelectedFinancialYear()
+ });
+ let url = `${API}/leads?${params.toString()}`;
console.log("Fetching API:", url);
@@ -2394,7 +2493,78 @@ function convertDBFormatted(input) {
seconds.padStart(2, '0')
);
}
-fetchLeads();
+
+function csvEscape(value) {
+ const text = String(value ?? '').replace(/"/g, '""');
+ return `"${text}"`;
+}
+
+function capitalizeStatus(value) {
+ const text = String(value ?? '').trim();
+ return text ? text.charAt(0).toUpperCase() + text.slice(1).toLowerCase() : '';
+}
+
+function formatIndianDate(value) {
+ if (!value) return '';
+ const date = new Date(String(value).replace(' ', 'T'));
+ if (Number.isNaN(date.getTime())) return value;
+
+ return date.toLocaleString('en-IN', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true
+ }).replace(',', '').toUpperCase();
+}
+
+function downloadCsv(filename, rows) {
+ const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+}
+
+async function exportLeadsCsv() {
+ const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
+ if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
+
+ const params = new URLSearchParams({
+ status: filter === 'all' ? '' : filter,
+ search: document.getElementById('mainSearch')?.value || '',
+ limit: 10000,
+ offset: 0,
+ assigned_to: selectedMemberIds.join(','),
+ financial_year: getSelectedFinancialYear()
+ });
+
+ 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);
+}
+
+initSalesToolbarFilters();
+fetchLeads().then(() => {
+ const leadId = new URLSearchParams(window.location.search).get('lead_id');
+ if (leadId) {
+ viewDetail(leadId);
+ }
+});
// ================================================================
// COMPANY SEARCH — Odoo-style autocomplete