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/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', 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 @@ -