Sales tracker Changes
This commit is contained in:
parent
11b47b2dff
commit
67d89e1fd5
@ -2028,6 +2028,35 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
|
||||
ORDER BY l.created_at DESC
|
||||
", [$uid, $fyStart, $fyEnd])->getResultArray();
|
||||
|
||||
// ── Lost Leads for this user in FY (Opportunities tab — toggle off) ──
|
||||
$lossLeads = $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 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,
|
||||
l.lost_reason AS lost_reason
|
||||
FROM leads l
|
||||
INNER JOIN sales_actual_leads sal
|
||||
ON sal.lead_id = l.actual_lead_id
|
||||
WHERE l.status = 'lost'
|
||||
AND sal.assigned_to = ?
|
||||
AND l.created_at >= ?
|
||||
AND l.created_at <= ?
|
||||
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,
|
||||
@ -2035,6 +2064,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
|
||||
'target_amt' => $targetAmt,
|
||||
'policies' => $policies,
|
||||
'won_leads' => $wonLeads, // for Table 2 in modal Tab 2
|
||||
'loss_leads' => $lossLeads,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -230,6 +230,17 @@
|
||||
.opp-card { background: #f8fafd; border-radius: 12px; padding: 16px; border: 1px solid #e4eaf5; text-align: center; }
|
||||
.opp-card-val { font-size: 22px; font-weight: 800; color: #0f172a; }
|
||||
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
|
||||
.opp-section-head { display: flex; justify-content: space-between; align-items: center; margin: 24px 0 12px; gap: 12px; flex-wrap: wrap; }
|
||||
.opp-section-head .split-section-title { margin: 0; }
|
||||
.opp-toggle-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||||
.opp-toggle-lbl { font-size: 12px; font-weight: 700; color: #94a3b8; transition: color .2s; }
|
||||
.opp-toggle-lbl.active { color: #0f172a; }
|
||||
.opp-toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
|
||||
.opp-toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.opp-toggle-slider { position: absolute; cursor: pointer; inset: 0; background: #cbd5e1; border-radius: 99px; transition: background .2s; }
|
||||
.opp-toggle-slider::before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform .2s; box-shadow: 0 1px 3px rgba(15,23,42,0.15); }
|
||||
.opp-toggle-switch input:checked + .opp-toggle-slider { background: #4f46e5; }
|
||||
.opp-toggle-switch input:checked + .opp-toggle-slider::before { transform: translateX(20px); }
|
||||
|
||||
@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); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
|
||||
@ -502,7 +513,9 @@ const TEAM = <?php echo json_encode($team_achievement ?? []); ?>;
|
||||
const OPP_DATA = <?php echo json_encode($opp_achievement ?? []); ?>;
|
||||
/* OPP_DATA shape per userId:
|
||||
{ total_policies, total_exp_amt, target_amt,
|
||||
policies: [{ policy_no, issue_date, amount, created_at }] } */
|
||||
policies: [{ policy_no, issue_date, amount, created_at }],
|
||||
won_leads: [...], loss_leads: [...] } */
|
||||
let currentModalOppUserId = null;
|
||||
|
||||
/* ── JS Helpers ── */
|
||||
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
|
||||
@ -545,6 +558,89 @@ function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) {
|
||||
+ encodeURIComponent(opportunityId);
|
||||
}
|
||||
|
||||
function escOppCell(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function updateOppTableHeader(showWon) {
|
||||
const headRow = document.getElementById('oppTableHead');
|
||||
if (!headRow) return;
|
||||
|
||||
if (showWon) {
|
||||
headRow.innerHTML =
|
||||
'<th>Company</th>'
|
||||
+ '<th>Opportunity Type</th>'
|
||||
+ '<th>Date</th>';
|
||||
} else {
|
||||
headRow.innerHTML =
|
||||
'<th>Company</th>'
|
||||
+ '<th>Opportunity Type</th>'
|
||||
+ '<th>Lost Reason</th>'
|
||||
+ '<th>Date</th>';
|
||||
}
|
||||
}
|
||||
|
||||
function buildOpportunityTableRows(leads, showWon) {
|
||||
return (leads || []).map(function(l) {
|
||||
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_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_form_type_id)) + ','
|
||||
+ JSON.stringify(String(l.actual_lead_id)) + ','
|
||||
+ JSON.stringify(String(l.opportunities_id)) + ')\''
|
||||
: '';
|
||||
|
||||
const lostReasonCell = showWon ? '' :
|
||||
'<td style="font-size:12px;color:#64748b;max-width:220px;white-space:normal;">'
|
||||
+ escOppCell((l.lost_reason || '').trim() || '—')
|
||||
+ '</td>';
|
||||
|
||||
return '<tr' + rowAttrs + '>'
|
||||
+ '<td style="font-weight:700;color:#0f172a;">' + escOppCell(l.company || '—') + '</td>'
|
||||
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
|
||||
+ lostReasonCell
|
||||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateOppToggleLabels(showWon) {
|
||||
const lostLbl = document.getElementById('oppToggleLblLost');
|
||||
const wonLbl = document.getElementById('oppToggleLblWon');
|
||||
if (lostLbl) lostLbl.classList.toggle('active', !showWon);
|
||||
if (wonLbl) wonLbl.classList.toggle('active', showWon);
|
||||
}
|
||||
|
||||
function renderOpportunitiesTable(userId, showWon) {
|
||||
const opp = OPP_DATA[userId];
|
||||
const tbody = document.getElementById('oppTableBody');
|
||||
if (!tbody || !opp) return;
|
||||
|
||||
const leads = showWon ? (opp.won_leads || []) : (opp.loss_leads || []);
|
||||
const colSpan = showWon ? 3 : 4;
|
||||
updateOppTableHeader(showWon);
|
||||
const rows = buildOpportunityTableRows(leads, showWon);
|
||||
const emptyMsg = showWon ? 'No won opportunities found' : 'No lost opportunities found';
|
||||
tbody.innerHTML = rows || '<tr><td colspan="' + colSpan + '" style="text-align:center;color:#aaa;padding:24px;">' + emptyMsg + '</td></tr>';
|
||||
updateOppToggleLabels(showWon);
|
||||
}
|
||||
|
||||
function onOppStatusToggle(checked) {
|
||||
if (currentModalOppUserId) {
|
||||
renderOpportunitiesTable(currentModalOppUserId, checked);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
@ -939,48 +1035,33 @@ function openModal(id) {
|
||||
// + '</table>'
|
||||
// + '</div>';
|
||||
|
||||
/* ── Won Leads Table (Table 2) ── */
|
||||
const wonLeads = opp.won_leads || [];
|
||||
const wonRows = wonLeads.map(function(l) {
|
||||
// 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_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_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 + '>' + (opportunityType || '—') + '</span></td>'
|
||||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
currentModalOppUserId = id;
|
||||
|
||||
tab2Html +=
|
||||
'<div class="split-section-title" style="margin-top:24px;"> Opportunities </div>'
|
||||
'<div class="opp-section-head">'
|
||||
+ '<div class="split-section-title">Opportunities</div>'
|
||||
+ '<div class="opp-toggle-wrap">'
|
||||
+ '<span class="opp-toggle-lbl" id="oppToggleLblLost">Lost</span>'
|
||||
+ '<label class="opp-toggle-switch" title="Toggle between won and lost opportunities">'
|
||||
+ '<input type="checkbox" id="oppStatusToggle" checked onchange="onOppStatusToggle(this.checked)">'
|
||||
+ '<span class="opp-toggle-slider"></span>'
|
||||
+ '</label>'
|
||||
+ '<span class="opp-toggle-lbl active" id="oppToggleLblWon">Won</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div style="overflow-x:auto;">'
|
||||
+ '<table class="split-table">'
|
||||
+ '<thead><tr>'
|
||||
+ '<thead><tr id="oppTableHead">'
|
||||
+ '<th>Company</th>'
|
||||
+ '<th>Opportunity Type</th>'
|
||||
+ '<th>Date</th>'
|
||||
+ '</tr></thead>'
|
||||
+ '<tbody>' + (wonRows || '<tr><td colspan="3" style="text-align:center;color:#aaa;padding:24px;">No leads found</td></tr>') + '</tbody>'
|
||||
+ '<tbody id="oppTableBody"></tbody>'
|
||||
+ '</table>'
|
||||
+ '</div>';
|
||||
|
||||
} else {
|
||||
currentModalOppUserId = null;
|
||||
tab2Html = '<div class="empty-state"><div class="empty-icon">📋</div>No opportunities data available</div>';
|
||||
}
|
||||
|
||||
@ -996,6 +1077,14 @@ function openModal(id) {
|
||||
|
||||
document.getElementById('modalOverlay').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
if (currentModalOppUserId && OPP_DATA[currentModalOppUserId]) {
|
||||
const toggle = document.getElementById('oppStatusToggle');
|
||||
if (toggle) toggle.checked = true;
|
||||
renderOpportunitiesTable(currentModalOppUserId, true);
|
||||
} else {
|
||||
currentModalOppUserId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(idx, btn) {
|
||||
@ -1007,6 +1096,7 @@ function switchTab(idx, btn) {
|
||||
function closeModal() {
|
||||
document.getElementById('modalOverlay').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
currentModalOppUserId = null;
|
||||
}
|
||||
|
||||
document.getElementById('modalOverlay').addEventListener('click', function(e) { if (e.target === e.currentTarget) closeModal(); });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user