-
+
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 = ; const API = ''; +const defaultFinancialYear = ''; 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 = ''; +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 @@
-
+ -
+ -
+ -
+
- + + + + + + + -
+ +
+ 0): ?> + + + + +
+
@@ -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 = ''; +const SALES_ACTIVITIES_URL = ''; +const SALES_TARGETS_URL = ''; + +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) { + '
' + fmt(rem) + '
REMAINING
' + '
' + p + '%
ACHIEVED %
'; + const hasTargetRecord = !!m.target_id; + const targetEditorHtml = + '
' + + '
' + + '
' + (hasTargetRecord ? 'Edit Target Amount' : 'Create Target Amount') + '
' + + '
' + (hasTargetRecord ? 'Update' : 'Create') + ' target for ' + m.first_name + ' - ' + currentFY + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '
'; + /* ── Overall progress block (shared) ── */ const overallHtml = '
' @@ -767,7 +959,8 @@ function openModal(id) { /* ── Inject tabs ── */ document.getElementById('modalBody').innerHTML = - ' -->
-
+
@@ -160,9 +180,23 @@
- +
@@ -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 = ''; const SALES_CHECK_DUPLICATE_URL = ''; /** Normalized company name when edit modal opened — same name retyped after clear must not count as duplicate */ let trackerEditLeadOriginalCompanyNorm = ''; +const defaultFinancialYear = ''; 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 From a41d390768f9d3ef7f637cca49742d60e26460d8 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 11 May 2026 17:51:52 +0530 Subject: [PATCH 2/6] GWM : megre pdf --- app/Helpers/merge_pdf_helper.php | 101 ++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index 9316017f..063de050 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -13,14 +13,14 @@ if (! defined('MERGED_CLAIM_FILE_TYPE')) { if (! function_exists('merge_ticket_pdfs')) { /** - * Merge all active PDF rows in claim_files for a given ticket_master id + * Merge all active PDF/image rows in claim_files for a given ticket_master id * into one combined PDF and register that PDF as a new claim_files row * with file_type = MERGED_CLAIM_FILE_TYPE. * * Source rows are picked from claim_files where: * - ticket_id = $ticket_master_id * - is_active = 1 - * - mime_type = 'application/pdf' + * - mime_type IN $opts['include_mime_types'] (default PDF/JPG/PNG) * - file_type IN $opts['include_file_types'] (default [2, 3]) * * @param int $ticket_master_id @@ -29,6 +29,7 @@ if (! function_exists('merge_ticket_pdfs')) { * @var int $created_by Override created_by user id on the inserted row. * @var int $ticket_type Default 1. Stored on the inserted claim_files row. * @var array $include_file_types Default [2, 3]. + * @var array $include_mime_types Default ['application/pdf', 'image/jpeg', 'image/png']. * } * @return array {status, merged_file_id, file_name, pages, source_count, message} */ @@ -39,6 +40,7 @@ if (! function_exists('merge_ticket_pdfs')) { 'created_by' => null, 'ticket_type' => 1, 'include_file_types' => [2, 3], + 'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'], ]; $result = [ @@ -60,14 +62,14 @@ if (! function_exists('merge_ticket_pdfs')) { $rows = $claimFiles ->where('ticket_id', $ticket_master_id) ->where('is_active', 1) - ->where('mime_type', 'application/pdf') + ->whereIn('mime_type', $opts['include_mime_types']) ->whereIn('file_type', $opts['include_file_types']) ->orderBy('id', 'ASC') ->findAll(); if (empty($rows)) { $result['status'] = true; - $result['message'] = 'No PDF files to merge'; + $result['message'] = 'No PDF/image files to merge'; return $result; } @@ -75,7 +77,7 @@ if (! function_exists('merge_ticket_pdfs')) { . 'uploads' . DIRECTORY_SEPARATOR . 'claim_files' . DIRECTORY_SEPARATOR; - $sourcePaths = []; + $sourceFiles = []; foreach ($rows as $row) { $name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? ''); if (empty($name)) { @@ -84,18 +86,22 @@ if (! function_exists('merge_ticket_pdfs')) { // url column may sometimes hold a full URL; we only care about the file basename on disk. $full = $uploadDir . basename($name); if (is_file($full) && is_readable($full)) { - $sourcePaths[] = $full; + $sourceFiles[] = [ + 'path' => $full, + 'mime' => strtolower((string) ($row['mime_type'] ?? '')), + 'id' => $row['id'] ?? null, + ]; } else { - log_message('error', "merge_ticket_pdfs | missing PDF on disk | claim_file_id={$row['id']} | path={$full}"); + log_message('error', "merge_ticket_pdfs | missing file on disk | claim_file_id={$row['id']} | path={$full}"); } } - if (empty($sourcePaths)) { - $result['message'] = 'No readable PDF files on disk'; + if (empty($sourceFiles)) { + $result['message'] = 'No readable PDF/image files on disk'; return $result; } - $result['source_count'] = count($sourcePaths); + $result['source_count'] = count($sourceFiles); $tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($tempDir)) { @@ -112,18 +118,24 @@ if (! function_exists('merge_ticket_pdfs')) { ]); $totalPages = 0; - foreach ($sourcePaths as $src) { + foreach ($sourceFiles as $sourceFile) { + $src = $sourceFile['path']; try { - $pageCount = $mpdf->setSourceFile($src); - for ($p = 1; $p <= $pageCount; $p++) { - $tplId = $mpdf->importPage($p); - $size = $mpdf->getTemplateSize($tplId); - $mpdf->AddPageByArray([ - 'orientation' => ($size['width'] > $size['height']) ? 'L' : 'P', - 'sheet-size' => [$size['width'], $size['height']], - ]); - $mpdf->useTemplate($tplId); - $totalPages++; + if ($sourceFile['mime'] === 'application/pdf') { + $pageCount = $mpdf->setSourceFile($src); + for ($p = 1; $p <= $pageCount; $p++) { + $tplId = $mpdf->importPage($p); + $mpdf->AddPage(); + // adjustPageSize=true makes the output page match the imported page. + $mpdf->useTemplate($tplId, 0, 0, null, null, true); + $totalPages++; + } + } elseif (in_array($sourceFile['mime'], ['image/jpeg', 'image/png'], true)) { + if (merge_ticket_pdf_add_image_page($mpdf, $src)) { + $totalPages++; + } + } else { + log_message('error', "merge_ticket_pdfs | unsupported source mime {$sourceFile['mime']} | {$src}"); } } catch (\Throwable $e) { log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage()); @@ -131,7 +143,7 @@ if (! function_exists('merge_ticket_pdfs')) { } if ($totalPages === 0) { - $result['message'] = 'All source PDFs failed to import'; + $result['message'] = 'All source PDF/image files failed to import'; return $result; } @@ -192,3 +204,48 @@ if (! function_exists('merge_ticket_pdfs')) { return $result; } } + +if (! function_exists('merge_ticket_pdf_add_image_page')) { + /** + * Add an uploaded image as a single PDF page, preserving portrait/landscape + * orientation and fitting the image proportionally inside the page. + */ + function merge_ticket_pdf_add_image_page(Mpdf $mpdf, string $imagePath): bool + { + $imageInfo = @getimagesize($imagePath); + if (empty($imageInfo[0]) || empty($imageInfo[1])) { + log_message('error', "merge_ticket_pdfs | invalid image | {$imagePath}"); + return false; + } + + $imageWidthPx = (int) $imageInfo[0]; + $imageHeightPx = (int) $imageInfo[1]; + $isLandscape = $imageWidthPx > $imageHeightPx; + + $pageWidth = $isLandscape ? 297 : 210; + $pageHeight = $isLandscape ? 210 : 297; + $margin = 0; + + $availableWidth = $pageWidth - ($margin * 2); + $availableHeight = $pageHeight - ($margin * 2); + $scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx); + $drawWidth = $imageWidthPx * $scale; + $drawHeight = $imageHeightPx * $scale; + $x = ($pageWidth - $drawWidth) / 2; + $y = ($pageHeight - $drawHeight) / 2; + + $mpdf->AddPageByArray([ + 'orientation' => $isLandscape ? 'L' : 'P', + 'sheet-size' => 'A4', + 'margin-left' => 0, + 'margin-right' => 0, + 'margin-top' => 0, + 'margin-bottom' => 0, + 'margin-header' => 0, + 'margin-footer' => 0, + ]); + + $mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight); + return true; + } +} From d516a66c94ffce28fb2dde366827aa476c55ba78 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 11 May 2026 18:17:21 +0530 Subject: [PATCH 3/6] GWM : megre pdf --- app/Helpers/merge_pdf_helper.php | 50 ++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index 063de050..fdfe1b26 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -62,7 +62,6 @@ if (! function_exists('merge_ticket_pdfs')) { $rows = $claimFiles ->where('ticket_id', $ticket_master_id) ->where('is_active', 1) - ->whereIn('mime_type', $opts['include_mime_types']) ->whereIn('file_type', $opts['include_file_types']) ->orderBy('id', 'ASC') ->findAll(); @@ -86,9 +85,15 @@ if (! function_exists('merge_ticket_pdfs')) { // url column may sometimes hold a full URL; we only care about the file basename on disk. $full = $uploadDir . basename($name); if (is_file($full) && is_readable($full)) { + $mime = merge_ticket_pdf_resolve_mime($full, (string) ($row['mime_type'] ?? '')); + if (! in_array($mime, $opts['include_mime_types'], true)) { + log_message('error', "merge_ticket_pdfs | unsupported source mime {$mime} | claim_file_id={$row['id']} | path={$full}"); + continue; + } + $sourceFiles[] = [ 'path' => $full, - 'mime' => strtolower((string) ($row['mime_type'] ?? '')), + 'mime' => $mime, 'id' => $row['id'] ?? null, ]; } else { @@ -205,6 +210,47 @@ if (! function_exists('merge_ticket_pdfs')) { } } +if (! function_exists('merge_ticket_pdf_resolve_mime')) { + /** + * Resolve file type from stored MIME, filesystem MIME, and extension. + * Some uploads can be saved as image/jpg, empty MIME, or octet-stream in DB. + */ + function merge_ticket_pdf_resolve_mime(string $path, string $storedMime = ''): string + { + $storedMime = strtolower(trim($storedMime)); + if ($storedMime === 'image/jpg') { + return 'image/jpeg'; + } + if (in_array($storedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) { + return $storedMime; + } + + $detectedMime = ''; + if (function_exists('mime_content_type')) { + $detectedMime = strtolower((string) @mime_content_type($path)); + if ($detectedMime === 'image/jpg') { + return 'image/jpeg'; + } + if (in_array($detectedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) { + return $detectedMime; + } + } + + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + if ($ext === 'pdf') { + return 'application/pdf'; + } + if (in_array($ext, ['jpg', 'jpeg'], true)) { + return 'image/jpeg'; + } + if ($ext === 'png') { + return 'image/png'; + } + + return $detectedMime ?: ($storedMime ?: 'application/octet-stream'); + } +} + if (! function_exists('merge_ticket_pdf_add_image_page')) { /** * Add an uploaded image as a single PDF page, preserving portrait/landscape From 524806f0eb30be750db939aa0ce8ed3b1c50825d Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 11 May 2026 18:42:03 +0530 Subject: [PATCH 4/6] GWM : megre pdf --- app/Helpers/merge_pdf_helper.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index fdfe1b26..2ae442ed 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -21,14 +21,14 @@ if (! function_exists('merge_ticket_pdfs')) { * - ticket_id = $ticket_master_id * - is_active = 1 * - mime_type IN $opts['include_mime_types'] (default PDF/JPG/PNG) - * - file_type IN $opts['include_file_types'] (default [2, 3]) + * - file_type IN $opts['include_file_types'] (default [1, 2, 3]) * * @param int $ticket_master_id * @param array $opts { * @var bool $replace Default true. Soft-delete previous merged row before re-creating. * @var int $created_by Override created_by user id on the inserted row. * @var int $ticket_type Default 1. Stored on the inserted claim_files row. - * @var array $include_file_types Default [2, 3]. + * @var array $include_file_types Default [1, 2, 3]. * @var array $include_mime_types Default ['application/pdf', 'image/jpeg', 'image/png']. * } * @return array {status, merged_file_id, file_name, pages, source_count, message} @@ -39,7 +39,7 @@ if (! function_exists('merge_ticket_pdfs')) { 'replace' => true, 'created_by' => null, 'ticket_type' => 1, - 'include_file_types' => [2, 3], + 'include_file_types' => [1, 2], 'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'], ]; @@ -107,6 +107,16 @@ if (! function_exists('merge_ticket_pdfs')) { } $result['source_count'] = count($sourceFiles); + log_message( + 'error', + 'merge_ticket_pdfs | source files resolved | ticket_id=' . $ticket_master_id . ' | sources=' . json_encode(array_map(static function ($sourceFile) { + return [ + 'id' => $sourceFile['id'], + 'mime' => $sourceFile['mime'], + 'file' => basename($sourceFile['path']), + ]; + }, $sourceFiles)) + ); $tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($tempDir)) { @@ -135,9 +145,11 @@ if (! function_exists('merge_ticket_pdfs')) { $mpdf->useTemplate($tplId, 0, 0, null, null, true); $totalPages++; } + log_message('error', "merge_ticket_pdfs | added PDF | file={$src} | pages={$pageCount}"); } elseif (in_array($sourceFile['mime'], ['image/jpeg', 'image/png'], true)) { if (merge_ticket_pdf_add_image_page($mpdf, $src)) { $totalPages++; + log_message('error', "merge_ticket_pdfs | added image | file={$src} | mime={$sourceFile['mime']}"); } } else { log_message('error', "merge_ticket_pdfs | unsupported source mime {$sourceFile['mime']} | {$src}"); @@ -266,6 +278,7 @@ if (! function_exists('merge_ticket_pdf_add_image_page')) { $imageWidthPx = (int) $imageInfo[0]; $imageHeightPx = (int) $imageInfo[1]; + $imageType = ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg'; $isLandscape = $imageWidthPx > $imageHeightPx; $pageWidth = $isLandscape ? 297 : 210; @@ -291,7 +304,7 @@ if (! function_exists('merge_ticket_pdf_add_image_page')) { 'margin-footer' => 0, ]); - $mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight); + $mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight, $imageType); return true; } } From 1d812d9a936f7393f9662b75f093727db3fa1fcc Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 12 May 2026 11:21:31 +0530 Subject: [PATCH 5/6] GWM : merge pdf --- app/Helpers/merge_pdf_helper.php | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index 2ae442ed..4498b9f8 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -122,6 +122,10 @@ if (! function_exists('merge_ticket_pdfs')) { if (! is_dir($tempDir)) { @mkdir($tempDir, 0775, true); } + $mpdfCacheDir = $tempDir . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfCacheDir)) { + @mkdir($mpdfCacheDir, 0775, true); + } $mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf'; $mergedPath = $uploadDir . $mergedName; @@ -271,15 +275,13 @@ if (! function_exists('merge_ticket_pdf_add_image_page')) { function merge_ticket_pdf_add_image_page(Mpdf $mpdf, string $imagePath): bool { $imageInfo = @getimagesize($imagePath); - if (empty($imageInfo[0]) || empty($imageInfo[1])) { - log_message('error', "merge_ticket_pdfs | invalid image | {$imagePath}"); - return false; - } + $ext = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)); + $imageType = $ext === 'png' || ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg'; - $imageWidthPx = (int) $imageInfo[0]; - $imageHeightPx = (int) $imageInfo[1]; - $imageType = ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg'; - $isLandscape = $imageWidthPx > $imageHeightPx; + $imageWidthPx = (int) ($imageInfo[0] ?? 0); + $imageHeightPx = (int) ($imageInfo[1] ?? 0); + $hasSize = $imageWidthPx > 0 && $imageHeightPx > 0; + $isLandscape = $hasSize && $imageWidthPx > $imageHeightPx; $pageWidth = $isLandscape ? 297 : 210; $pageHeight = $isLandscape ? 210 : 297; @@ -287,11 +289,21 @@ if (! function_exists('merge_ticket_pdf_add_image_page')) { $availableWidth = $pageWidth - ($margin * 2); $availableHeight = $pageHeight - ($margin * 2); - $scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx); - $drawWidth = $imageWidthPx * $scale; - $drawHeight = $imageHeightPx * $scale; - $x = ($pageWidth - $drawWidth) / 2; - $y = ($pageHeight - $drawHeight) / 2; + if ($hasSize) { + $scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx); + $drawWidth = $imageWidthPx * $scale; + $drawHeight = $imageHeightPx * $scale; + $x = ($pageWidth - $drawWidth) / 2; + $y = ($pageHeight - $drawHeight) / 2; + } else { + // Some PNGs fail getimagesize(), but mPDF can still render them. + // Use a portrait A4 fallback and let mPDF calculate image height. + log_message('error', "merge_ticket_pdfs | image size unavailable, using fallback page | {$imagePath}"); + $drawWidth = $availableWidth; + $drawHeight = 0; + $x = $margin; + $y = $margin; + } $mpdf->AddPageByArray([ 'orientation' => $isLandscape ? 'L' : 'P', From 70064b6ea6ebe075dd9f5b6c630135c78d43c778 Mon Sep 17 00:00:00 2001 From: "sanjeev.p" Date: Tue, 12 May 2026 11:23:19 +0530 Subject: [PATCH 6/6] FIX_SalesTracker --- app/Controllers/SalesController.php | 6 ++++- app/Views/sales/activity_view.php | 26 ++++++++++++------- .../sales/branch_level_dashboard_view.php | 22 +++++++++++++++- app/Views/sales/tracker_view.php | 26 ++++++++++++------- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 100ff01a..195ace88 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -1710,8 +1710,11 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s // leads.type: 1 = EB, else = Non-EB $wonLeads = $db->query(" SELECT - sal.lead_id, + l.id AS opportunities_id, + l.actual_lead_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, l.created_at AS created_at, l.status @@ -1725,6 +1728,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s ORDER BY l.created_at DESC ", [$uid, $fyStart, $fyEnd])->getResultArray(); + // -- NOTE: AND TRIM(sal.company_name) = TRIM(l.client_name) $result[$uid] = [ 'total_policies' => $totalPolicies, 'total_exp_amt' => $totalExpAmt, diff --git a/app/Views/sales/activity_view.php b/app/Views/sales/activity_view.php index 8e0d0765..3df4d15c 100644 --- a/app/Views/sales/activity_view.php +++ b/app/Views/sales/activity_view.php @@ -21,20 +21,28 @@ /* 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; } - .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 { flex-wrap: nowrap; padding: 16px 15px; gap: 12px; overflow-x: auto; } + .sales-toolbar .filter-tabs { padding: 0; flex: 0 0 auto; min-width: max-content; overflow: visible; } + .sales-toolbar .lead-actions { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; } + .sales-toolbar .search-input { width: 230px !important; height: 38px; padding: 9px 12px; border: 1px solid #ddd !important; border-radius: 6px !important; flex: 0 0 230px; } .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 .financial-year-select { width: 122px !important; min-width: 122px; } + .sales-toolbar .financial-year-select + .select2-container { width: 122px !important; min-width: 122px; flex: 0 0 122px; } .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; } + .sales-toolbar #memberFilter + .select2-container .select2-selection--single, + .sales-toolbar #financialYearFilter + .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, + .sales-toolbar #financialYearFilter + .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, + .sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; } + .sales-toolbar #memberFilter + .select2-container .select2-selection__arrow b, + .sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow b { border-width: 4px 3px 0 3px !important; margin-left: -3px; margin-top: -2px; } + .sales-toolbar #memberFilter + .select2-container--open .select2-selection__arrow b, + .sales-toolbar #financialYearFilter + .select2-container--open .select2-selection__arrow b { border-width: 0 3px 4px 3px !important; } .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); } @@ -211,7 +219,7 @@ - diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php index 74b44c21..e73902a5 100644 --- a/app/Views/sales/branch_level_dashboard_view.php +++ b/app/Views/sales/branch_level_dashboard_view.php @@ -104,6 +104,8 @@ th { text-align: left; padding: 10px 12px; color: #8896aa; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; border-bottom: 1px solid #edf2f7; background: white; font-weight: 700; } td { padding: 13px 12px; border-bottom: 1px solid #f4f6fb; font-size: 14px; vertical-align: middle; } tbody tr:hover { background: #fafbfe; } + .won-opportunity-row { cursor: pointer; } + .won-opportunity-row:hover { background: #eff6ff !important; } /* ── Status Pills ── */ .status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; } @@ -528,11 +530,21 @@ const progColor= p => p >= 100 ? 'linear-gradient(90deg,#10b981,#34d399)' : p >= const SALES_LEADS_URL = ''; const SALES_ACTIVITIES_URL = ''; const SALES_TARGETS_URL = ''; +const OPPORTUNITY_DETAIL_BASE = ''; function getSelectedDashboardFY() { return document.getElementById('financial_year')?.value || getCurrentFY(); } +function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) { + if (!leadTypeId || !actualLeadId || !opportunityId) return; + + window.location.href = OPPORTUNITY_DETAIL_BASE + + encodeURIComponent(leadTypeId) + '/' + + encodeURIComponent(actualLeadId) + '/' + + encodeURIComponent(opportunityId); +} + 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()); @@ -933,7 +945,15 @@ function openModal(id) { const typeBadgeClass = l.lead_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;"'; - return '' + const canRedirect = l.lead_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.actual_lead_id)) + ',' + + JSON.stringify(String(l.opportunities_id)) + ')\'' + : ''; + + return '' + '' + (l.company || '—') + '' + '' + (l.lead_type || '—') + '' + '' + fmtCreatedAt(l.created_at) + '' diff --git a/app/Views/sales/tracker_view.php b/app/Views/sales/tracker_view.php index 392c3f99..cd35c28c 100644 --- a/app/Views/sales/tracker_view.php +++ b/app/Views/sales/tracker_view.php @@ -18,20 +18,28 @@ /* 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: 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 { flex-wrap: nowrap; padding: 16px 15px; gap: 12px; overflow-x: auto; } + .sales-toolbar .filter-tabs { padding: 0; flex: 0 0 auto; min-width: max-content; overflow: visible; } + .sales-toolbar .lead-actions { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; } + .sales-toolbar .search-input { width: 230px !important; height: 38px; padding: 9px 12px; border: 1px solid #ddd !important; border-radius: 6px !important; flex: 0 0 230px; } .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 .financial-year-select { width: 122px !important; min-width: 122px; } + .sales-toolbar .financial-year-select + .select2-container { width: 122px !important; min-width: 122px; flex: 0 0 122px; } .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; } + .sales-toolbar #memberFilter + .select2-container .select2-selection--single, + .sales-toolbar #financialYearFilter + .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, + .sales-toolbar #financialYearFilter + .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, + .sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; } + .sales-toolbar #memberFilter + .select2-container .select2-selection__arrow b, + .sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow b { border-width: 4px 3px 0 3px !important; margin-left: -3px; margin-top: -2px; } + .sales-toolbar #memberFilter + .select2-container--open .select2-selection__arrow b, + .sales-toolbar #financialYearFilter + .select2-container--open .select2-selection__arrow b { border-width: 0 3px 4px 3px !important; } .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); } @@ -187,7 +195,7 @@ -