diff --git a/src/app/analytics/dashboard-widget/dashboard-widget.component.scss b/src/app/analytics/dashboard-widget/dashboard-widget.component.scss index 5c5223790f..18c6a059f8 100644 --- a/src/app/analytics/dashboard-widget/dashboard-widget.component.scss +++ b/src/app/analytics/dashboard-widget/dashboard-widget.component.scss @@ -580,102 +580,104 @@ border-color: rgb(255 255 255 / 4%); } - .leaflet-map-element { - filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%); - } - - .leaflet-custom-popup { - .leaflet-popup-content-wrapper { - background: rgb(30 30 30 / 85%); - border-color: rgb(255 255 255 / 8%); - box-shadow: - 0 10px 25px -5px rgb(0 0 0 / 40%), - 0 8px 10px -6px rgb(0 0 0 / 40%); + ::ng-deep { + .leaflet-tile-pane { + filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%); } - .leaflet-popup-tip { - background: rgb(30 30 30 / 85%); - border-color: rgb(255 255 255 / 8%); - } + .leaflet-custom-popup { + .leaflet-popup-content-wrapper { + background: rgb(30 30 30 / 85%); + border-color: rgb(255 255 255 / 8%); + box-shadow: + 0 10px 25px -5px rgb(0 0 0 / 40%), + 0 8px 10px -6px rgb(0 0 0 / 40%); + } - .map-tooltip { - .tooltip-header { - .tooltip-icon { - color: #38bdf8; + .leaflet-popup-tip { + background: rgb(30 30 30 / 85%); + border-color: rgb(255 255 255 / 8%); + } + + .map-tooltip { + .tooltip-header { + .tooltip-icon { + color: #38bdf8; + } + + .tooltip-title { + color: #f8fafc; + } + + .tooltip-subtitle { + color: #94a3b8; + } } - .tooltip-title { - color: #f8fafc; + .tooltip-divider { + background: rgb(255 255 255 / 8%); } - .tooltip-subtitle { - color: #94a3b8; + .tooltip-row { + .tooltip-label { + color: #94a3b8; + } + + .tooltip-value { + color: #e2e8f0; + } } } + } - .tooltip-divider { - background: rgb(255 255 255 / 8%); + .custom-map-pin { + .pin-pulse { + background: rgb(56 189 248 / 24%); } - .tooltip-row { - .tooltip-label { - color: #94a3b8; + &:hover { + .pin-svg { + filter: drop-shadow(0 3px 6px rgb(56 189 248 / 45%)); } + } - .tooltip-value { - color: #e2e8f0; + &.active { + .pin-pulse { + background: rgb(249 115 22 / 30%); } } } - } - .custom-map-pin { - .pin-pulse { - background: rgb(56 189 248 / 24%); - } - - &:hover { - .pin-svg { - filter: drop-shadow(0 3px 6px rgb(56 189 248 / 45%)); + .custom-marker-cluster { + .cluster-inner { + background: rgb(56 189 248 / 20%); + border-color: rgb(56 189 248 / 40%); + box-shadow: 0 4px 14px rgb(56 189 248 / 35%); + + span { + background: #38bdf8; + color: #0f172a !important; + border: 2px solid #fff; + outline: 2px solid rgb(56 189 248 / 60%); + font-weight: 800; + } } } - &.active { - .pin-pulse { - background: rgb(249 115 22 / 30%); - } + .custom-leaflet-tooltip { + background: rgb(30 30 30 / 90%) !important; + border-color: rgb(255 255 255 / 8%) !important; + color: #e2e8f0 !important; + box-shadow: 0 4px 10px rgb(0 0 0 / 30%) !important; } - } - .custom-marker-cluster { - .cluster-inner { - background: rgb(56 189 248 / 20%); - border-color: rgb(56 189 248 / 40%); - box-shadow: 0 4px 14px rgb(56 189 248 / 35%); - - span { - background: #38bdf8; - color: #0f172a !important; - border: 2px solid #fff; - outline: 2px solid rgb(56 189 248 / 60%); - font-weight: 800; - } + .leaflet-tooltip.custom-tooltip-card { + background: #1e293b !important; + border-color: rgb(255 255 255 / 12%) !important; + box-shadow: 0 10px 25px -5px rgb(0 0 0 / 50%) !important; } } - .custom-leaflet-tooltip { - background: rgb(30 30 30 / 90%) !important; - border-color: rgb(255 255 255 / 8%) !important; - color: #e2e8f0 !important; - box-shadow: 0 4px 10px rgb(0 0 0 / 30%) !important; - } - - .leaflet-tooltip.custom-tooltip-card { - background: #1e293b !important; - border-color: rgb(255 255 255 / 12%) !important; - box-shadow: 0 10px 25px -5px rgb(0 0 0 / 50%) !important; - } - .map-empty-state { background-color: #1a1a1a; color: #94a3b8; diff --git a/src/app/analytics/dashboard-widget/dashboard-widget.component.ts b/src/app/analytics/dashboard-widget/dashboard-widget.component.ts index 6a65d28af3..fd7c80982a 100644 --- a/src/app/analytics/dashboard-widget/dashboard-widget.component.ts +++ b/src/app/analytics/dashboard-widget/dashboard-widget.component.ts @@ -288,22 +288,25 @@ export class DashboardWidgetComponent implements AfterViewInit, OnChanges, OnDes const savingsVal = pin.savings ? pin.savings.toLocaleString('en-US') : '0'; const collectedVal = pin.collected ? pin.collected.toLocaleString('en-US') : '0'; + const safeOfficeName = this.escapeHtml(pin.officeName); + const safeCountry = this.escapeHtml(pin.country); + const popupHtml = `
- ${pin.officeName} + ${safeOfficeName} ${flagUrl ? `` : ''} - ${pin.country || ''} + ${safeCountry}
${countryLabel}: - ${pin.country || ''} + ${safeCountry}
${activeClientsLabel}: @@ -351,7 +354,7 @@ export class DashboardWidgetComponent implements AfterViewInit, OnChanges, OnDes marker.closeTooltip(); }); - marker.on('click', (e: any) => { + marker.on('click', (e: L.LeafletMouseEvent) => { if (e && e.originalEvent) { L.DomEvent.stopPropagation(e.originalEvent); } @@ -398,14 +401,22 @@ export class DashboardWidgetComponent implements AfterViewInit, OnChanges, OnDes }); if (this.selectedOfficeId !== null && this.markersMap.has(this.selectedOfficeId) && this.map) { - const marker = this.markersMap.get(this.selectedOfficeId); + const targetOfficeId = this.selectedOfficeId; + const marker = this.markersMap.get(targetOfficeId); if (marker) { - setTimeout(() => { - if (this.map && marker) { - marker.openPopup(); - this.map.panTo(marker.getLatLng()); + const showMarkerPopup = () => { + if (this.selectedOfficeId !== targetOfficeId || !this.map || !marker) { + return; } - }, 50); + marker.openPopup(); + this.map.panTo(marker.getLatLng()); + }; + + if (this.markerClusterGroup) { + this.markerClusterGroup.zoomToShowLayer(marker, showMarkerPopup); + } else { + setTimeout(showMarkerPopup, 50); + } } } } @@ -426,6 +437,16 @@ export class DashboardWidgetComponent implements AfterViewInit, OnChanges, OnDes * - Percent adapters: format as 12.3% * - Default: plain integer */ + private escapeHtml(text?: string | null): string { + if (!text) return ''; + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + formatMetricValue(widgetId: string, value?: number): string { if (value === undefined || value === null) { return '—'; diff --git a/src/app/analytics/services/analytics-data-source.service.ts b/src/app/analytics/services/analytics-data-source.service.ts index 057acdb9b7..13a7bfd115 100644 --- a/src/app/analytics/services/analytics-data-source.service.ts +++ b/src/app/analytics/services/analytics-data-source.service.ts @@ -1164,8 +1164,13 @@ export class AnalyticsDataSourceService { }; } + private normalizeOfficeId(officeId: number): number { + const id = Math.abs(Math.floor(Number(officeId))); + return Number.isSafeInteger(id) && id > 0 ? id : 1; + } + private getOfficeClients(officeId: number, filters: AnalyticsFilters): number { - const safeId = Math.abs(Number(officeId)) || 1; + const safeId = this.normalizeOfficeId(officeId); const base = ((safeId * 149) % 300) + 150; const scale = filters.productId ? 0.25 : 1.0; return Math.max(50, Math.floor(base * scale)); @@ -1178,14 +1183,16 @@ export class AnalyticsDataSourceService { private getOfficeSavings(officeId: number, filters: AnalyticsFilters): number { const clients = this.getOfficeClients(officeId, filters); - const basePerClient = ((officeId * 73) % 500) + 800; + const safeId = this.normalizeOfficeId(officeId); + const basePerClient = ((safeId * 73) % 500) + 800; const productScale = filters.productId ? 0.4 : 1.0; return Math.floor(clients * basePerClient * productScale); } private getOfficeCollected(officeId: number, filters: AnalyticsFilters): number { const loansCount = this.getOfficeLoans(officeId, filters); - const avgLoanSize = ((officeId * 41) % 1000) + 1500; + const safeId = this.normalizeOfficeId(officeId); + const avgLoanSize = ((safeId * 41) % 1000) + 1500; const basePortfolio = loansCount * avgLoanSize; let periodScale = 0.08;