-
+
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 06/23] 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 07/23] 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 08/23] 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 09/23] 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 10/23] 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 @@ - From f1bc4fddea394b47a6a9e3ecf56335f8806487f0 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 12 May 2026 11:27:12 +0530 Subject: [PATCH 11/23] FIX_STATEMENT_UPLOAD_ISSUE --- app/Config/Routes.php | 1 + app/Controllers/FhplApiController.php | 2 +- .../PolicyTransactionController.php | 374 +++++++++++++++++- app/Models/PTCOShareDetailsModel.php | 36 ++ app/Views/insurer_statement_list.php | 10 +- 5 files changed, 418 insertions(+), 5 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 081b44cd..566935a0 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -508,6 +508,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { $routes->post("saveInvoicePaymentDetails", "PolicyTransactionController::saveInvoicePaymentDetails"); $routes->get("deletePaymentEntry/(:any)", "PolicyTransactionController::deletePaymentEntry/$1"); $routes->get("downloadSampleInsurerStatement", "PolicyTransactionController::downloadSampleInsurerStatement"); + $routes->get("downloadInsurerStatement/(:num)", "PolicyTransactionController::downloadInsurerStatement/$1"); $routes->get("getFileErr/(:any)", "PolicyTransactionController::getFileErr/$1"); $routes->get("getInsurerStatementMonth", "PolicyTransactionController::getInsurerStatementMonth"); $routes->get("deleteStatement/(:any)", "PolicyTransactionController::deleteStatement/$1"); diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php index dfa7db1b..2d6cc36c 100644 --- a/app/Controllers/FhplApiController.php +++ b/app/Controllers/FhplApiController.php @@ -1068,7 +1068,7 @@ class FhplApiController extends BaseController 'is_active' => 1, 'created_by' => $file_info[0]['created_by'] ?? null, - 'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D' + 'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D', 'si' => $row['BASE_SUMINSURED'], 'doj' => change_date_format($row['DATE_OF_JOINING'],'Y-m-d\TH:i:s'), 'endorsement_no' => $row['ENDORSEMENT_NO'] diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 1c4c347b..0f15f31d 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -4236,7 +4236,7 @@ class PolicyTransactionController extends BaseController return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); } - public function validateInsurerStatement($params) + public function validateInsurerStatementOLD1($params) { helper('excel_util_helper'); @@ -4392,8 +4392,7 @@ class PolicyTransactionController extends BaseController } } - - public function updateInsurerStatement($params) + public function updateInsurerStatementOLD($params) { helper('excel_util_helper'); //get file info @@ -4559,6 +4558,375 @@ class PolicyTransactionController extends BaseController return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); } + public function validateInsurerStatement($params) + { + /* + * Changes made: 12-06-2024 + * - Added safe file_id handling and DB file record validation before using file details. + * - Fixed physical file missing response and error message. + * - Skips empty Excel rows and collects unique policy numbers from the uploaded statement. + * - Fetches NHance source records only for those uploaded policy numbers. + * - Sanitizes policy and endorsement numbers before comparison to avoid hidden-space mismatch. + * - Validates each row by policy number + endorsement number combination. + * - Detects duplicate policy + endorsement rows and returns row-wise validation errors. + */ + helper('excel_util_helper'); + + $file_id = $params['file_id'] ?? 0; + + try { + + $error_data = ['error_code' => '', 'error_data' => []]; + $status = 'success'; + $ret_status = true; + + //get file info + $file = $this->insurerStatements->find((int)$file_id); + // dd($file); + + if (empty($file)) { + //file not found in DB + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => 'statement file not found in DB'])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name']; + + //check physical file + if (!file_exists($file_name_with_path)) { + //file not found update status and reason + $message = "Physical file not found"; + // echo $message; + $this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRow = $sheet->getHighestRow(); + $highestColumn = $sheet->getHighestColumn(); + + $excel_data = $sheet->rangeToArray('A1:' . $highestColumn . $highestRow); + unset($excel_data[0]); + $excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data); + // dd($excel_data); + + //get no of line items and update in DB + $line_items = 0; + + $policyNos = []; + + foreach ($excel_data as $row) { + if (check_row_is_empty_or_null($row)) { + continue; + } + + // row[1] => second column (B column) + $policyNo = $this->sanitizeStatementLookupValue($row[1] ?? ''); + + if ($policyNo !== '') { + $policyNos[] = $policyNo; + } + } + $policyNos = array_values(array_unique($policyNos)); + + // get uploaded month transactions data + // $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']); + $source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : []; + // dd($source_data); + + $source_lookup = []; + $source_policy_lookup = []; + $source_endorsement_lookup = []; + + foreach ($source_data as $source_row) { + $source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? ''); + $source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? ''); + $source_entry_key = $source_policy_no . '|' . $source_endorsement_no; + + $source_lookup[$source_entry_key] = true; + $source_policy_lookup[$source_policy_no] = true; + $source_endorsement_lookup[$source_policy_no][$source_endorsement_no] = true; + } + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + $matched_entry = []; + $error_messages = []; // row-wise error storage + + foreach ($excel_data as $excel_key => $excel_row) { + + $row_number = $excel_key; + $is_row_empty = check_row_is_empty_or_null($excel_row); + $policy_source_found = 0; + $endorsement_source_found = 0; + $duplicate_found = 0; + + if (!$is_row_empty) { + + $policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel + $endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel + $entry_key = $policy_no . '|' . $endorsement_no; + + if (isset($matched_entry[$entry_key])) { + $duplicate_found = 1; + } elseif (isset($source_lookup[$entry_key])) { + $line_items = $line_items + 1; + $matched_entry[$entry_key] = true; + continue; + } + + $policy_source_found = isset($source_policy_lookup[$policy_no]) ? 1 : 0; + $endorsement_source_found = isset($source_endorsement_lookup[$policy_no][$endorsement_no]) ? 1 : 0; + + // Policy number mismatch + if ($policy_source_found == 0) { + $error_messages[$row_number]['policy_no_mismatch'] = + "Policy number ({$policy_no}) not in NHance."; + } + + // Endorsement number mismatch + if ($policy_source_found == 1 && $endorsement_source_found == 0) { + $error_messages[$row_number]['endorsement_no_mismatch'] = + "Endorsement number ({$endorsement_no}) not in NHance for policy ({$policy_no})."; + } + + // Duplicate check + if ($duplicate_found == 1) { + $error_messages[$row_number]['duplicate'] = + "Duplicate entry found. This policy and endorsement ({$policy_no} | {$endorsement_no}) combination has already been matched."; + } + } + + } + // print_rr($error_messages);die; + + if (!empty($error_messages)) { + $error_data['error_code'] = 2; + $error_data['error_data'] = $error_messages; + $status = 'failed'; + $ret_status = false; + } + + //update in DB + $this->insurerStatements->where('id', $file_id)->set(['line_items' => $line_items, 'file_status' => $status, 'reason' => json_encode($error_data)])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); + } catch (\Throwable $th) { + + $errorData = [ + 'message' => $th->getMessage(), + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'code' => $th->getCode(), + 'trace' => $th->getTraceAsString(), + 'trace_array' => $th->getTrace(), // full array version (optional) + 'function' => $th->getTrace()[0]['function'] ?? null, + 'class' => $th->getTrace()[0]['class'] ?? null, + ]; + + $this->myLogger->logme("error", "POLICY-TRANSACTION-CONTROLLER - validateInsurerStatement: Exception: " . json_encode($errorData ?? [])); + $this->insurerStatements->where('id', $file_id)->set(['line_items' => 0, 'file_status' => 'failed', 'reason' => json_encode($errorData)])->update(); + return array('status' => false, 'error_code' => [], 'error_data' => $errorData); + } + } + + private function sanitizeStatementLookupValue($value): string + { + $value = trim((string)($value ?? '')); + $value = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $value); + return preg_replace('/[\x{200B}\x{200C}\x{200D}\x{FEFF}\x{00A0}\x{200E}\x{200F}\x{202A}-\x{202E}]/u', '', $value); + } + + public function updateInsurerStatement($params) + { + /* + * Changes made: 12-06-2024 + * - Added safe file_id handling and DB file record validation before using file details. + * - Fixed physical file missing response and error message. + * - Skips empty Excel rows and collects unique policy numbers from the uploaded statement. + * - Fetches NHance source records only for those uploaded policy numbers. + * - Uses the same sanitized policy number + endorsement number matching as validation. + * - Inserts statement details only for matched rows and skips empty or unmatched Excel rows. + * - Added safe default handling for amount and reward columns before calculation/insert. + */ + helper('excel_util_helper'); + $file_id = $params['file_id'] ?? 0; + + $error_data = ['error_code' => '', 'error_data' => []]; + $status = 'success'; + $ret_status = true; + + //get file info + $file = $this->insurerStatements->find((int)$file_id); + // dd($file); + + if (empty($file)) { + //file not found in DB + return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name']; + + //check physical file + if (!file_exists($file_name_with_path)) { + //file not found update status and reason + $message = "Physical file not found"; + // echo $message; + $this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRowAndColumn = $sheet->getHighestRowAndColumn(); + // dd($highestRowAndColumn); + $excel_data = $sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']); + unset($excel_data[0]); + $excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data); + // dd($excel_data); + + $policyNos = []; + + foreach ($excel_data as $row) { + if (check_row_is_empty_or_null($row)) { + continue; + } + + $policyNo = $this->sanitizeStatementLookupValue($row[1] ?? ''); + + if ($policyNo !== '') { + $policyNos[] = $policyNo; + } + } + $policyNos = array_values(array_unique($policyNos)); + + // get uploaded month transactions data + // $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']); + $source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : []; + // Kint::dump($source_data);//die; + // Kint::dump($excel_data); + // die; + $source_lookup = []; + + foreach ($source_data as $source_row) { + $source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? ''); + $source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? ''); + $source_lookup[$source_policy_no . '|' . $source_endorsement_no][] = $source_row; + } + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + $data_to_update = []; + try { + foreach ($excel_data as $excel_row) { + if (check_row_is_empty_or_null($excel_row)) { + continue; + } + + // $policy_start_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[4]); //policy_start_date from excel + // $policy_end_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[5]); //policy_end_date from excel + $policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel + // $client_name = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[3]); //clientname from excel + $endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel + $entry_key = $policy_no . '|' . $endorsement_no; + + if (empty($source_lookup[$entry_key])) { + continue; + } + + $source_row = array_shift($source_lookup[$entry_key]); + + //calculate percentage first + $total_amt = 0; + + // $actual_bp_per = trim($excel_row[9]); //commented becoz this filed removed tfrom excel file + $actual_bp_per = 0; //set default value 0 for maintaining existing code flow + $actual_bp_brokerage = (int) trim((string)($excel_row[5] ?? 0)); + $actual_bp_amt = (int) trim((string)($excel_row[3] ?? 0)); + + if (($actual_bp_brokerage && $actual_bp_brokerage != 0 && $actual_bp_brokerage != "")) { + $total_amt += $actual_bp_brokerage; + //percentage reverse calculation + if (($actual_bp_per == 0 || $actual_bp_per == 0) && !empty($actual_bp_amt)) { + $actual_bp_per = (int) round(($actual_bp_brokerage / $actual_bp_amt) * 100, 2); + } + } else { + $actual_bp_brokerage = $actual_bp_amt * ($actual_bp_per / 100); + $total_amt += $actual_bp_brokerage; + } + + // $actual_tp_per = trim($excel_row[10]);//commented becoz this filed removed tfrom excel file + $actual_tp_per = 0;//set default value 0 for maintaining existing code flow + $actual_tp_brokerage = (int) trim((string)($excel_row[6] ?? 0)); + $actual_tp_amt = (int) trim((string)($excel_row[4] ?? 0)); + + if ($actual_tp_brokerage && $actual_tp_brokerage != 0 && $actual_tp_brokerage != "") { + $total_amt += $actual_tp_brokerage; + //percentage reverse calculation + if (($actual_tp_per == 0 || $actual_tp_per == "") && !empty($actual_tp_amt)) { + $actual_tp_per = (int) round(($actual_tp_brokerage / $actual_tp_amt) * 100, 2); + } + } else { + $actual_tp_brokerage = $actual_tp_amt * ($actual_tp_per / 100); + $total_amt += $actual_tp_brokerage; + } + + // $actual_tep_per = trim($excel_row[11]); + // $actual_tep_brokerage = trim($excel_row[14]); + // $actual_tep_amt = trim($excel_row[8]); + + $actual_tep_per = 0; + $actual_tep_brokerage = 0; + $actual_tep_amt = 0; + + if ($actual_tep_brokerage && $actual_tep_brokerage != 0 && $actual_tep_brokerage != "") { + $total_amt += $actual_tep_brokerage; + //percentage reverse calculation + if (($actual_tep_per == 0 || $actual_tep_per == "") && !empty($actual_tep_amt)) { + $actual_tep_per = ($actual_tep_brokerage / $actual_tep_amt) * 100; + } + } else { + $actual_tep_brokerage = $actual_tep_amt * ($actual_tep_per / 100); + $total_amt += $actual_tep_brokerage; + } + + //find variance + $variance_amt = $source_row['exp_amt'] - $total_amt; + + $data_to_update[] = ['co_share_id' => $source_row['id'], 'actual_bp_amt' => $actual_bp_amt, 'actual_tp_amt' => $actual_tp_amt, 'actual_tep_amt' => $actual_tep_amt, 'actual_bp_per' => $actual_bp_per, 'actual_tp_per' => $actual_tp_per, 'actual_tep_per' => $actual_tep_per, 'variance' => $variance_amt, 'actual_tep_brokerage_amt' => $actual_tep_brokerage, 'actual_tp_brokerage_amt' => $actual_tp_brokerage, 'actual_bp_brokerage_amt' => $actual_bp_brokerage, 'reward' => trim((string)($excel_row[7] ?? '')), 'statement_id' => $file_id]; + } + } catch (\Throwable $th) { + + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - updateInsurerStatement: Exception: " . $th->getMessage() . " --- Line: " . $th->getLine() . " --- Trace: " . $th->getTraceAsString()); + $errorData = [ + 'message' => $th->getMessage(), + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'code' => $th->getCode(), + 'trace' => $th->getTraceAsString(), + 'trace_array' => $th->getTrace(), // full array version (optional) + 'function' => $th->getTrace()[0]['function'] ?? null, + 'class' => $th->getTrace()[0]['class'] ?? null, + ]; + return ['status' => 'failed', 'code' => 500, 'message' => $th->getMessage(), 'error_data' => $errorData]; + } + // dd($data_to_update); + $this->coShareStmtDetailsModel->insertBatch($data_to_update, 'id'); + // dd($data_to_update); + // if($error_data['error_code']) + // { + // $status = 'failed'; + // $ret_status = false; + // } + //update in DB + $this->insurerStatements->where('id', $file_id)->set(['file_status' => $status, 'reason' => json_encode($error_data), 'invoice_status' => 'pending'])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); + } + public function getInvoicePaymentDetails() { $statement_id = $this->request->getUri()->getSegment(4); diff --git a/app/Models/PTCOShareDetailsModel.php b/app/Models/PTCOShareDetailsModel.php index 59d53c9c..e619c69d 100644 --- a/app/Models/PTCOShareDetailsModel.php +++ b/app/Models/PTCOShareDetailsModel.php @@ -121,5 +121,41 @@ class PTCOShareDetailsModel extends Model ->get() ->getResultArray(); } + + public function getNonReconcileredPolicyTransactionByPolicyAndEndorsement(string $insurer_id, string $insurer_branch_id, array $policy_no) + { + $builder = $this->db->table('pt_co_share_details pt_co') + ->select(" + pt_co.id, + pt_co.pt_id, + pt_co.exp_amt, + pt.id AS policy_transaction_id, + pt.endorsement_no, + c.client_name, + pt.created_at, + pt_co.insurer_id, + pt_co.insurer_branch_id, + pt.policy_no, + pt.policy_issue_date, + pt.policy_start_date, + pt.policy_end_date, + pt.status, + pt_co.bp_amt, + pt_co.tp_amt, + pt_co.tep_amt, + pt_co.exp_amt, + pt_co.statement_id + ") + ->join('policy_transaction pt', 'pt_co.pt_id = pt.id') + ->join('clients c', 'pt.client_id = c.id') + ->where('pt_co.is_active', 1) + ->where('pt.is_active', 1) + ->where('pt_co.insurer_id', $insurer_id) + ->where('pt_co.insurer_branch_id', $insurer_branch_id) + ->whereIn('pt.policy_no', $policy_no); + + + return $builder->get()->getResultArray(); + } } diff --git a/app/Views/insurer_statement_list.php b/app/Views/insurer_statement_list.php index e6d03e1a..5834ac98 100644 --- a/app/Views/insurer_statement_list.php +++ b/app/Views/insurer_statement_list.php @@ -302,7 +302,15 @@ $isl_col_width_px = nhance_dt_column_widths_px($isl_header_labels, $isl_col_max_ - + + + + + + + - + + Date: Tue, 12 May 2026 15:08:35 +0530 Subject: [PATCH 12/23] CHANGE_EB_CLAIMS_HISTORY_BASED_ON_THE_TOGGLE --- app/Controllers/LeadsController.php | 74 ++++++++++++++++++++++-- app/Views/leads_form.php | 87 ++++++++++++++++++++++++----- app/Views/leads_form_handler.php | 10 +++- app/Views/rfq/gpa.php | 33 +++++++---- app/Views/view_rfq.php | 23 ++++++-- 5 files changed, 194 insertions(+), 33 deletions(-) diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index ce2dc8f0..9eb1da12 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -571,6 +571,70 @@ class LeadsController extends BaseController } + // $leadFormType = (int) ($postData['lead_form_type'] ?? 1); + // $postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? [])); + // $isEbRenewalGpaClaimHistory = $leadFormType === 1 + // && (int) ($postData['lead_type'] ?? 0) === 2 + // && in_array(1, $postedPolicyTypeIds, true) + // && (int) ($postData['claim_history'] ?? 0) === 1; + + // if ($isEbRenewalGpaClaimHistory) { + // $rules['first_year.*'] = [ + // 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', + // 'errors' => [ + // 'required' => 'Claim Year is required for all entries.', + // 'regex_match' => 'Year must be in format YYYY-YYYY.', + // ], + // ]; + // $rules['emp_id.*'] = [ + // 'rules' => 'required', + // 'errors' => ['required' => 'Employee ID is required in Claim History.'], + // ]; + // $rules['emp_name.*'] = [ + // 'rules' => 'required', + // 'errors' => ['required' => 'Employee Name is required in Claim History.'], + // ]; + // $rules['gender.*'] = [ + // 'rules' => 'required|in_list[Female,Male]', + // 'errors' => [ + // 'required' => 'Gender is required in Claim History.', + // 'in_list' => 'Gender must be Female or Male.', + // ], + // ]; + // $rules['designation.*'] = [ + // 'rules' => 'required', + // 'errors' => ['required' => 'Designation is required in Claim History.'], + // ]; + // $rules['sum_insured.*'] = [ + // 'rules' => 'required|numeric', + // 'errors' => [ + // 'required' => 'Sum Insured is required in Claim History.', + // 'numeric' => 'Sum Insured must be a number.', + // ], + // ]; + // $rules['first_death_date.*'] = [ + // 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', + // 'errors' => [ + // 'required' => 'Date of Death is required.', + // 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', + // ], + // ]; + // $rules['first_cause_of_death.*'] = [ + // 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', + // 'errors' => [ + // 'required' => 'Nature/Cause Of Death is required.', + // 'in_list' => 'Nature/Cause Of Death is invalid.', + // ], + // ]; + // $rules['first_claim_amount.*'] = [ + // 'rules' => 'required|numeric', + // 'errors' => [ + // 'required' => 'Claim/Settled Amount is required.', + // 'numeric' => 'Claim/Settled Amount must be a number.', + // ], + // ]; + // } + // 1. MANUALLY VALIDATE FILES BEFORE PROCESSING // $allFiles = $this->request->getFiles(); // foreach ($allFiles as $inputName => $files) { @@ -2140,6 +2204,8 @@ class LeadsController extends BaseController { helper('excel_util_helper'); $rfq_data = $this->RFQModel->getRFQTableDataWithLeadIDAndType($lead_id, $type); + $claim_history = $rfq_data['claim_history'] ?? 0; + $is_placement = false; $length = 0; @@ -2176,7 +2242,7 @@ class LeadsController extends BaseController 'No of Dependents' => $rfq_data['incept_dept_count'], 'Total Lives' => $rfq_data['incept_no_of_lives'], - 'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", + // 'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", // 'Policy Run Days' => $rfq_data['policy_run_days'], @@ -2186,7 +2252,7 @@ class LeadsController extends BaseController 'Insured' => $rfq_data['client_name'], 'No of Employees at Inception' => $rfq_data['incept_emp_count'], // 'Total Sum Insured at Inception ' => $rfq_data['total_si_at_incept'], - 'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", + // 'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", 'Policy Type' => $this->leadType[$rfq_data['lead_type']] ?? "", @@ -2279,7 +2345,7 @@ class LeadsController extends BaseController 'Existing Insurer' => $rfq_data['insurer_name'], 'No of Employees at Renewal' => $rfq_data['renewal_emp_count'], 'Total Sum Insured at Renewal' => $rfq_data['total_si_at_renewal'], - 'Claims Experience for last 3 years' => "Mentioned in Claims sheet", + 'Claims Experience for last 3 years' => $claim_history == "1" ? "Mentioned in Claims sheet" : "Nil", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", // 'Total Lives at Inception' => $rfq_data['total_lives_at_incept'], @@ -2796,7 +2862,7 @@ class LeadsController extends BaseController } //claim history new sheet; - if (! empty($rfq_data['fin_years_claims'])) { + if ($claim_history == 1 && !empty($rfq_data['fin_years_claims'])) { $claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? []; diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 939d4b57..ddd850a6 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -744,7 +744,7 @@ } - leadTypeBsedHideAndShow(lead_type) + leadTypeBsedHideAndShow(lead_type, false, policy_type_id) if (lead_type == 1) { @@ -851,6 +851,7 @@ .contact_person_email); $('#policy_type_id_1').val(res.data.policy_type_id) .select2(); + togglePolicyDateFields(lead_type, res.data.policy_type_id); $('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow'); }, 1000) @@ -1093,6 +1094,7 @@ var lead_type = $('#lead_type').val(); console.log("lead_type", lead_type); + togglePolicyDateFields(lead_type); let url = ''; @@ -1281,7 +1283,7 @@
-
+
@@ -1290,7 +1292,7 @@
-
+
@@ -1776,6 +1778,8 @@ const finyearJsonString = gatherClaimExperienceData(policy_type_ids); console.log('finyearJsonString', finyearJsonString); formData.append('finyear', finyearJsonString); + let claimHistoryStatus = $("#claim_history").length > 0 && $("#claim_history").prop("checked") ? 1 : 0; + formData.set('claim_history', claimHistoryStatus); $.ajax({ data: formData, @@ -1898,6 +1902,7 @@ $('#policy_start_date').val(""); $('#policy_end_date').val(""); $("#appendArea_1").empty(); + togglePolicyDateFields(value); // $('#source_policy_start_date').val(""); // $('#source_policy_end_date').val(""); @@ -2019,6 +2024,12 @@ let claimData = []; + if ($("#claim_history").length > 0 && !$("#claim_history").prop("checked")) { + return JSON.stringify({ + "finyear": claimData + }); + } + $(".claim-row").each(function() { let year = $(this).find("[name='first_year[]']").val(); @@ -2156,15 +2167,23 @@ let policy_type_id = $('#policy_type_id_' + count).val() console.log('policy_type_id', policy_type_id); + let lead_type = $('#lead_type').val(); + let showClaimHistorySwitch = lead_type == 2 && policy_type_id == 1; + console.log('claimIndex from parent', claimIndex); let increment = claimIndex; - let claimsFields = ` + let claimsFields = `${showClaimHistorySwitch && !document.querySelector('#claim_history') ? `
+ + +

` : ``}`; + + claimsFields += `
- $year"; @@ -2174,18 +2193,18 @@
- +
- +
- @@ -2194,25 +2213,25 @@
- +
- +
- +
- $death_value) { echo ""; @@ -2222,7 +2241,7 @@
- +
-
+
-
- -
@@ -1261,8 +1294,9 @@ async function viewDetail(id) { function renderCard(opps) { const opp_cont = document.getElementById('opportunitiesContainer'); - opp_cont.innerHTML = opps.length ? opps.map(o => { - let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB'; + opp_cont.innerHTML = opps.length ? opps.map(o => { + const opportunityFormType = Number(o.lead_form_type || 1); + let lead_form_type = opportunityFormType === 1 ? 'EB' : 'Non-EB'; let status = o.status?.toLowerCase(); let statusClass = { won: 'status-text-won', @@ -1287,7 +1321,7 @@ function renderCard(opps) {
-
${lead_type}
+
${lead_form_type}
@@ -2191,12 +2225,12 @@ function submitToRedirectwithactualLeadIDUrl(){ let actual_lead_id = document.getElementById('opp_lead_id').value; - let lead_type = $('input[name="lead_form_type"]:checked').val(); + let lead_form_type = $('input[name="lead_form_type"]:checked').val(); // URL: /type / actual_lead_id - let url = '' + lead_type + '/' + actual_lead_id ; + let url = '' + lead_form_type + '/' + actual_lead_id ; - console.log('lead_type ', lead_type); + console.log('lead_form_type ', lead_form_type); console.log('opp_lead_id ', actual_lead_id); console.log('url ', url); @@ -2245,7 +2279,8 @@ document.getElementById('do_follow').addEventListener('change', function () { }); -async function openEditLeadModal(id) { +async function openEditLeadModal(id, preservedFields = {}) { + preservedFields = preservedFields || {}; // 1. Reset UI State document.getElementById('hidden_lead_id').value = id; @@ -2290,9 +2325,12 @@ async function openEditLeadModal(id) { form.querySelector('[name="company_name"]').value = lead.company_name || ''; form.querySelector('[name="email"]').value = lead.email || ''; form.querySelector('[name="phone"]').value = lead.phone || ''; - form.querySelector('[name="address"]').value = lead.address || ''; - form.querySelector('[name="website"]').value = lead.website || ''; - form.querySelector('[name="gst_number"]').value = lead.gst_number || ''; + form.querySelector('[name="address"]').value = + Object.prototype.hasOwnProperty.call(preservedFields, 'address') ? preservedFields.address : (lead.address || ''); + form.querySelector('[name="website"]').value = + Object.prototype.hasOwnProperty.call(preservedFields, 'website') ? preservedFields.website : (lead.website || ''); + form.querySelector('[name="gst_number"]').value = + Object.prototype.hasOwnProperty.call(preservedFields, 'gst_number') ? preservedFields.gst_number : (lead.gst_number || ''); // form.querySelector('[name="status"]').value = lead.status || 'New'; form.querySelector('[name="assigned_to"]').value = lead.assigned_to || ''; form.querySelector('[name="assigned_to"]').value = lead.assigned_to || ''; @@ -2384,15 +2422,27 @@ document.getElementById('contactPersonsList').onclick = async (e) => { if (!confirm('Are you sure you want to remove this contact?')) return; try { + const preservedFields = getEditLeadPreservedFields(); const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' }); if (res.ok) { toastr.success('Removed successfully'); - openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh + openEditLeadModal(document.getElementById('hidden_lead_id').value, preservedFields); // Refresh } } catch (err) { console.error(err); } } }; +function getEditLeadPreservedFields() { + const form = document.getElementById('editLeadForm'); + if (!form) return {}; + + return { + address: form.querySelector('[name="address"]')?.value || '', + website: form.querySelector('[name="website"]')?.value || '', + gst_number: form.querySelector('[name="gst_number"]')?.value || '' + }; +} + // --- 2. SAVE / UPDATE LOGIC --- const btnSaveContact = document.getElementById('btnSaveContact'); if (btnSaveContact) { @@ -2417,6 +2467,7 @@ if (btnSaveContact) { const method = editingId ? 'PUT' : 'POST'; try { + const preservedFields = getEditLeadPreservedFields(); const res = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, @@ -2430,7 +2481,7 @@ if (btnSaveContact) { resetContactForm(); // Refresh List - openEditLeadModal(leadId); + openEditLeadModal(leadId, preservedFields); } else { const err = await res.json(); if (err.messages?.name) setContactFieldError('name', err.messages.name); @@ -2537,35 +2588,95 @@ function downloadCsv(filename, rows) { URL.revokeObjectURL(link.href); } -async function exportLeadsCsv() { +function getExportFilename(response, fallbackFilename) { + const disposition = response.headers.get('content-disposition') || ''; + const match = disposition.match(/filename="?([^"]+)"?/i); + return match ? match[1] : fallbackFilename; +} + +async function downloadExportFile(url, fallbackFilename) { + const response = await fetch(url); + const contentType = response.headers.get('content-type') || ''; + + if (!response.ok || contentType.includes('application/json')) { + let message = 'Export failed. Please try again.'; + try { + const json = await response.json(); + message = json.message || json.messages?.error || message; + } catch (err) { + // Keep the generic message when the response cannot be parsed. + } + + return response.status === 404 ? toastr.warning(message) : toastr.error(message); + } + + const blob = await response.blob(); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = getExportFilename(response, fallbackFilename); + link.click(); + URL.revokeObjectURL(link.href); +} + +function initLeadExportDateRangePicker() { + const $button = $('#leadExportExcelBtn'); + if (!$button.length) return; + + if (typeof moment === 'undefined' || typeof $.fn.daterangepicker === 'undefined') { + $button.on('click', () => toastr.error('Date range picker is not available')); + return; + } + + $button.daterangepicker({ + autoUpdateInput: false, + startDate: moment().startOf('month'), + endDate: moment(), + maxDate: moment(), + opens: 'left', + drops: 'down', + locale: { + format: 'DD-MM-YYYY', + applyLabel: 'Generate Excel', + cancelLabel: 'Cancel' + }, + ranges: { + 'Today': [moment(), moment()], + 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + 'Last 7 Days': [moment().subtract(6, 'days'), moment()], + 'Last 30 Days': [moment().subtract(29, 'days'), moment()], + 'This Month': [moment().startOf('month'), moment()], + 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] + } + }); + + $button.on('apply.daterangepicker', function (ev, picker) { + exportLeadsCsv( + picker.startDate.format('YYYY-MM-DD'), + picker.endDate.format('YYYY-MM-DD') + ); + }); +} + +async function exportLeadsCsv(fromDate, toDate) { const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds); if (selectedMemberIds.length === 0) return toastr.warning('No members available to export'); + if (!fromDate || !toDate) return toastr.warning('Please select a date range'); const params = new URLSearchParams({ status: filter === 'all' ? '' : filter, search: document.getElementById('mainSearch')?.value || '', - limit: 10000, - offset: 0, assigned_to: selectedMemberIds.join(','), - financial_year: getSelectedFinancialYear() + from_date: fromDate, + to_date: toDate }); - const res = await fetch(`${API}/leads?${params.toString()}`); - const json = await res.json(); - const rows = [ - ['Company', 'Email', 'Phone', 'Status', 'Assigned To', 'Created At'], - ...(json.data || []).map(l => [ - l.company_name || '', - l.email || '', - l.phone || '', - capitalizeStatus(l.status), - l.assigned_to_name || '', - formatIndianDate(l.created_at) - ]) - ]; - downloadCsv(`sales-leads-${getSelectedFinancialYear()}.csv`, rows); + await downloadExportFile( + `${API}/leads/export?${params.toString()}`, + `sales-leads-${fromDate}-to-${toDate}.csv` + ); } +window.addEventListener('load', initLeadExportDateRangePicker); initSalesToolbarFilters(); fetchLeads().then(() => { const leadId = new URLSearchParams(window.location.search).get('lead_id'); From 3b56b0d1f83575d505b5a1358481ed64aa926c9b Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 12 May 2026 17:17:01 +0530 Subject: [PATCH 16/23] FIX_Opportunities_TOGGLE_ISSUE --- app/Controllers/LeadsController.php | 20 ++++++++++---------- app/Views/leads_form.php | 10 +++++++--- app/Views/leads_form_handler.php | 15 ++++++++++++--- app/Views/leads_non_eb.php | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index 9eb1da12..66337c36 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -1014,10 +1014,10 @@ class LeadsController extends BaseController $insertCount[] = $insert; $this->insertLeadStatus($insert, $value['status'], 3); - if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') { - $leadDataForClient = $this->leadsModel->find($insert); - $this->createClientWithLeadData($leadDataForClient); - } + // if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') { + // $leadDataForClient = $this->leadsModel->find($insert); + // $this->createClientWithLeadData($leadDataForClient); + // } if ($value['lead_form_type'] == 1) { //for this push the job to the calculateMembersDemography() function @@ -1045,12 +1045,12 @@ class LeadsController extends BaseController $this->insertMultiFilesData($data[0]['multi_file_data'], $id, $data[0]['lead_form_type']); $this->insertLeadStatus($id, $data[0]['status'], 3); - if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') { - $leadDataForClient = $this->leadsModel->find($id); - if (empty($leadDataForClient['is_client_created'])) { - $this->createClientWithLeadData($leadDataForClient); - } - } + // if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') { + // $leadDataForClient = $this->leadsModel->find($id); + // if (empty($leadDataForClient['is_client_created'])) { + // $this->createClientWithLeadData($leadDataForClient); + // } + // } return $this->respond(['status' => true, 'lead_id' => $id, 'message' => "Opportunity updated successfully", 'data' => $data], 200); } diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index e2242c4f..836ec3b8 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -822,11 +822,15 @@ $('#lost_reason').val(res.data.lost_reason || ''); $('#notes').val(res.data.notes); - let existingClientId = res.data.client_id || res.data.is_client_created || 0; + const isExistingClient = res.data.client_id !== undefined + && res.data.client_id !== null + && String(res.data.client_id) !== '' + && String(res.data.client_id) !== '0'; + let existingClientId = isExistingClient ? res.data.client_id : 0; console.log('existingClientId', existingClientId); - if (lead_type == 3) { - if (existingClientId && existingClientId != 0) { + if (lead_type == 1 || lead_type == 3) { + if (isExistingClient) { $('#exixting_client').prop('checked', true).trigger('change'); } else { $('#exixting_client').prop('checked', false).trigger('change'); diff --git a/app/Views/leads_form_handler.php b/app/Views/leads_form_handler.php index 96987f29..426f43fe 100644 --- a/app/Views/leads_form_handler.php +++ b/app/Views/leads_form_handler.php @@ -831,6 +831,10 @@ if (isset($selected_lead_type)) { let policy_type_id = data.policy_type_id || null; let lead_type = data.lead_type || null; + const isExistingClient = data.client_id !== undefined + && data.client_id !== null + && String(data.client_id) !== '' + && String(data.client_id) !== '0'; if ([1, 6, 7].includes(policy_type_id)) { let newId = 'appendAreaForClaim_' + dataIncrement; @@ -849,7 +853,7 @@ if (isset($selected_lead_type)) { if (lead_type == 1 || lead_type == 3) { $('.claim-row').hide(); - if(data.is_client_created == null || data.is_client_created == 0 || data.is_client_created == ""){ + if(!isExistingClient){ $('#exixting_client').prop('checked', false); $('#exixting_client').prop('disabled', true); @@ -1117,7 +1121,12 @@ if (isset($selected_lead_type)) { leadTypeBsedHideAndShow(lead_type); - if(data.client_id == 0 || data.client_id == null){ + const isExistingClient = data.client_id !== undefined + && data.client_id !== null + && String(data.client_id) !== '' + && String(data.client_id) !== '0'; + + if(!isExistingClient){ temp_client_id = 0; }else{ temp_client_id = data.client_id @@ -1125,7 +1134,7 @@ if (isset($selected_lead_type)) { if(lead_type == 1 || lead_type == 3){ - if(data.is_client_created == null || data.is_client_created == 0 || data.is_client_created == ""){ + if(!isExistingClient){ $('#exixting_client').prop('checked', false); $('#exixting_client').prop('disabled', true); diff --git a/app/Views/leads_non_eb.php b/app/Views/leads_non_eb.php index a0d9946e..0bd1f5ab 100644 --- a/app/Views/leads_non_eb.php +++ b/app/Views/leads_non_eb.php @@ -561,6 +561,22 @@ var pageBackButton = ' json_encode(['error_data' => 'API failed: Empty member data for this pull request.']), + 'status' => 'failed-7', + ]; + $file_model->where('id', $requestData['file_id'])->set($bfData)->update(); if($function_calling_type == "job"){ - return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request']; + return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request']; }else{ - return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request']); + return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request']); } } diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 62b702f8..278fec55 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -2197,7 +2197,7 @@ console.log('policy_type_id', policy_type_id); let lead_type = $('#lead_type').val(); - let showClaimHistorySwitch = lead_type == 2 && policy_type_id == 1; + let showClaimHistorySwitch = lead_type == 2 && (policy_type_id == 1 || policy_type_id == 6 || policy_type_id == 7); console.log('claimIndex from parent', claimIndex); let increment = claimIndex; diff --git a/app/Views/rfq/gpa.php b/app/Views/rfq/gpa.php index bc5cb726..8d29fd05 100644 --- a/app/Views/rfq/gpa.php +++ b/app/Views/rfq/gpa.php @@ -46,7 +46,7 @@ $isClaimHistoryChecked = (isset($lead_edit_data['claim_history']) && (int) $lead_edit_data['claim_history'] === 1) || !empty($savedClaims); $showClaimHistorySwitch = isset($lead_edit_data['lead_type'], $lead_edit_data['policy_type_id']) && (int) $lead_edit_data['lead_type'] === 2 - && (int) $lead_edit_data['policy_type_id'] === 1; + && in_array((int) $lead_edit_data['policy_type_id'], [1, 6, 7], true); ?>
From 20329dfa20a9d4a68692dab7a62db8971b270821 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Wed, 13 May 2026 13:00:24 +0530 Subject: [PATCH 19/23] FIX_DOWNLOAD_ISSUE --- .../PolicyTransactionController.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 0f15f31d..b0a7cb12 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -5065,6 +5065,27 @@ class PolicyTransactionController extends BaseController } } + public function downloadInsurerStatement($id) + { + $statement = $this->insurerStatements + ->where('id', (int) $id) + ->where('is_active', 1) + ->first(); + + if (empty($statement) || empty($statement['file_name'])) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found'); + } + + $fileName = basename($statement['file_name']); + $filePath = WRITEPATH . 'uploads/statements/' . $fileName; + + if (!is_file($filePath)) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found'); + } + + return $this->response->download($filePath, null)->setFileName($fileName); + } + public function getFileErr() { $file_id = $this->request->getUri()->getSegment(4); From 0577fe4efd6249177a95b7a9dca01cb5c593801b Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Wed, 13 May 2026 18:15:28 +0530 Subject: [PATCH 20/23] FIX_VIE_CLAIM_ACCESS --- app/Config/Acl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Config/Acl.php b/app/Config/Acl.php index 67edbd35..54c64819 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -43,6 +43,7 @@ class Acl '#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], '#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], + '#^/viewClaimFile#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID]], From 041f700a65280070d7c647692b9c3cb38f19a6e0 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Fri, 15 May 2026 11:17:33 +0530 Subject: [PATCH 21/23] Samul token api --- app/Config/Routes.php | 6 ++++++ .../RestAuthenticationController.php | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 566935a0..2321083f 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -673,6 +673,9 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu $routes->post("getVerifiedRetailUserData", "RestAuthenticationController::getVerifiedRetailUserData"); $routes->post("updateRetailUserAuthDetails", "RestAuthenticationController::updateRetailUserAuthDetails"); + //samulss oauth login api's + $routes->get("getTokenforSamulssOAuthLogin", "RestAuthenticationController::getTokenforSamulssOAuthLogin"); + }); $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) { @@ -1115,3 +1118,6 @@ $routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controller }); + + + diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index ae85137b..5ef73c7b 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -2425,4 +2425,25 @@ class RestAuthenticationController extends AdminController return; } + + public function getTokenforSamulssOAuthLogin() + { + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Function called"); + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Received payload = " . json_encode($this->request->getJSON() ?? [])); + + $id = $this->request->getGet('id') ?? null; + if(!$id){ + return $this->respond(['status' => 'failed','code' => 400,'message' => 'ID is required'], 200); + } + + $user = $this->employeeModel->where('id', $id)->first(); + if(!$user){ + return $this->respond(['status' => 'failed','code' => 400,'message' => 'User not found'], 200); + } + + $token = JWTToken::encode($user); + return $this->respond(['status' => 'success','code' => 200,'data' => $token], 200); + + } + } \ No newline at end of file From 528d8dcb267be4d93feb62fb3cf296e38ec742fe Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Sat, 16 May 2026 09:55:02 +0530 Subject: [PATCH 22/23] FIX_CLAIM_HISTORY_SHOW_ROLL_OVER --- .../PolicyTransactionController.php | 3 +++ app/Views/leads_form.php | 21 ++++++++++++++++--- app/Views/leads_form_handler.php | 4 +++- app/Views/rfq/gpa.php | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index b0a7cb12..26da53b2 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -5321,6 +5321,8 @@ class PolicyTransactionController extends BaseController try { + $bdsInstallmentData = []; + try { $bdsInstallmentData = $this->BdsPlacementModel->getClientInstallmentDetails(); @@ -5328,6 +5330,7 @@ class PolicyTransactionController extends BaseController $this->myLogger->logme('error', 'Exception: ' . $e->getMessage() . 'Page: ' . $e->getFile() . ' Line: ' . $e->getLine()); } + $this->myLogger->logme("error", "Data for BDS Installment " . json_encode($bdsInstallmentData)); // dd($bdsInstallmentData); diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 278fec55..36d930c8 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -1162,7 +1162,7 @@ if (lead_type == 1) { $('.claim-row').hide(); - } else { + } else if (!shouldShowClaimHistorySwitch(lead_type, policy_type_id)) { $('.claim-row').show(); if (policy_type_id == 1) { @@ -1174,6 +1174,10 @@ } } + if ($('#claim_history').length && typeof claimHistoryToggle === 'function') { + claimHistoryToggle(); + } + if (lead_type != 1 && policy_type_id != 1 && policy_type_id != 6 && policy_type_id != 7) { if (lead_type == 1) { updateRenewalFields(dataIncrement); @@ -2188,6 +2192,14 @@ // toggleRequiredFields(); // } + function isClaimHistoryPolicyType(policyTypeId) { + return ['1', '6', '7'].includes(String(policyTypeId)); + } + + function shouldShowClaimHistorySwitch(leadType, policyTypeId) { + return (leadType == 2 || leadType == 3) && isClaimHistoryPolicyType(policyTypeId); + } + function appendThreeYearsClaims(count) { // let count = $('#appendAreaForClaim').data('count'); @@ -2197,7 +2209,7 @@ console.log('policy_type_id', policy_type_id); let lead_type = $('#lead_type').val(); - let showClaimHistorySwitch = lead_type == 2 && (policy_type_id == 1 || policy_type_id == 6 || policy_type_id == 7); + let showClaimHistorySwitch = shouldShowClaimHistorySwitch(lead_type, policy_type_id); console.log('claimIndex from parent', claimIndex); let increment = claimIndex; @@ -2432,7 +2444,6 @@ } else if (value == 3) { $('.btnDiv').hide(); - $('.claim-row').show(); $('.emp_title_text').text('No of Employees at Inception') $('.depnd_title_text').text(' No of Dependents at Inception') @@ -2546,6 +2557,10 @@ } + if ($('#claim_history').length && typeof claimHistoryToggle === 'function') { + claimHistoryToggle(); + } + toggleRequiredFields(); restoreActualLeadGstNumber(); restoreActualLeadContactDetails(); diff --git a/app/Views/leads_form_handler.php b/app/Views/leads_form_handler.php index 426f43fe..0b12c3af 100644 --- a/app/Views/leads_form_handler.php +++ b/app/Views/leads_form_handler.php @@ -851,7 +851,9 @@ if (isset($selected_lead_type)) { if (lead_type == 1 || lead_type == 3) { - $('.claim-row').hide(); + if (lead_type == 1 || !shouldShowClaimHistorySwitch(lead_type, policy_type_id)) { + $('.claim-row').hide(); + } if(!isExistingClient){ $('#exixting_client').prop('checked', false); diff --git a/app/Views/rfq/gpa.php b/app/Views/rfq/gpa.php index 8d29fd05..7fe37fb4 100644 --- a/app/Views/rfq/gpa.php +++ b/app/Views/rfq/gpa.php @@ -45,7 +45,7 @@ $savedClaims = !empty($lead_edit_data['fin_years_claims_array']) ? $lead_edit_data['fin_years_claims_array'] : []; $isClaimHistoryChecked = (isset($lead_edit_data['claim_history']) && (int) $lead_edit_data['claim_history'] === 1) || !empty($savedClaims); $showClaimHistorySwitch = isset($lead_edit_data['lead_type'], $lead_edit_data['policy_type_id']) - && (int) $lead_edit_data['lead_type'] === 2 + && in_array((int) $lead_edit_data['lead_type'], [2, 3], true) && in_array((int) $lead_edit_data['policy_type_id'], [1, 6, 7], true); ?> From 21594469bc8462283f6007f5587810d66f1d61cc Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Sat, 16 May 2026 10:20:36 +0530 Subject: [PATCH 23/23] FIX_ISSUE --- app/Controllers/LeadsController.php | 126 ++++++++++++++-------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index 06876db1..7f347d6f 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -571,69 +571,71 @@ class LeadsController extends BaseController } - // $leadFormType = (int) ($postData['lead_form_type'] ?? 1); - // $postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? [])); - // $isEbRenewalGpaClaimHistory = $leadFormType === 1 - // && (int) ($postData['lead_type'] ?? 0) === 2 - // && in_array(1, $postedPolicyTypeIds, true) - // && (int) ($postData['claim_history'] ?? 0) === 1; + $leadFormType = (int) ($postData['lead_form_type'] ?? 1); + $postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? [])); + $claimHistoryPolicyTypes = [1, 6, 7]; + $hasClaimHistoryPolicy = count(array_intersect($postedPolicyTypeIds, $claimHistoryPolicyTypes)) > 0; + $isEbClaimHistory = $leadFormType === 1 + && in_array((int) ($postData['lead_type'] ?? 0), [2, 3], true) + && $hasClaimHistoryPolicy + && (int) ($postData['claim_history'] ?? 0) === 1; - // if ($isEbRenewalGpaClaimHistory) { - // $rules['first_year.*'] = [ - // 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', - // 'errors' => [ - // 'required' => 'Claim Year is required for all entries.', - // 'regex_match' => 'Year must be in format YYYY-YYYY.', - // ], - // ]; - // $rules['emp_id.*'] = [ - // 'rules' => 'required', - // 'errors' => ['required' => 'Employee ID is required in Claim History.'], - // ]; - // $rules['emp_name.*'] = [ - // 'rules' => 'required', - // 'errors' => ['required' => 'Employee Name is required in Claim History.'], - // ]; - // $rules['gender.*'] = [ - // 'rules' => 'required|in_list[Female,Male]', - // 'errors' => [ - // 'required' => 'Gender is required in Claim History.', - // 'in_list' => 'Gender must be Female or Male.', - // ], - // ]; - // $rules['designation.*'] = [ - // 'rules' => 'required', - // 'errors' => ['required' => 'Designation is required in Claim History.'], - // ]; - // $rules['sum_insured.*'] = [ - // 'rules' => 'required|numeric', - // 'errors' => [ - // 'required' => 'Sum Insured is required in Claim History.', - // 'numeric' => 'Sum Insured must be a number.', - // ], - // ]; - // $rules['first_death_date.*'] = [ - // 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', - // 'errors' => [ - // 'required' => 'Date of Death is required.', - // 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', - // ], - // ]; - // $rules['first_cause_of_death.*'] = [ - // 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', - // 'errors' => [ - // 'required' => 'Nature/Cause Of Death is required.', - // 'in_list' => 'Nature/Cause Of Death is invalid.', - // ], - // ]; - // $rules['first_claim_amount.*'] = [ - // 'rules' => 'required|numeric', - // 'errors' => [ - // 'required' => 'Claim/Settled Amount is required.', - // 'numeric' => 'Claim/Settled Amount must be a number.', - // ], - // ]; - // } + if ($isEbClaimHistory) { + $rules['first_year.*'] = [ + 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', + 'errors' => [ + 'required' => 'Claim Year is required for all entries.', + 'regex_match' => 'Year must be in format YYYY-YYYY.', + ], + ]; + $rules['emp_id.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee ID is required in Claim History.'], + ]; + $rules['emp_name.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee Name is required in Claim History.'], + ]; + $rules['gender.*'] = [ + 'rules' => 'required|in_list[Female,Male]', + 'errors' => [ + 'required' => 'Gender is required in Claim History.', + 'in_list' => 'Gender must be Female or Male.', + ], + ]; + $rules['designation.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Designation is required in Claim History.'], + ]; + $rules['sum_insured.*'] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Sum Insured is required in Claim History.', + 'numeric' => 'Sum Insured must be a number.', + ], + ]; + $rules['first_death_date.*'] = [ + 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', + 'errors' => [ + 'required' => 'Date of Death is required.', + 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', + ], + ]; + $rules['first_cause_of_death.*'] = [ + 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', + 'errors' => [ + 'required' => 'Nature/Cause Of Death is required.', + 'in_list' => 'Nature/Cause Of Death is invalid.', + ], + ]; + $rules['first_claim_amount.*'] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Claim/Settled Amount is required.', + 'numeric' => 'Claim/Settled Amount must be a number.', + ], + ]; + } // 1. MANUALLY VALIDATE FILES BEFORE PROCESSING // $allFiles = $this->request->getFiles();