diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css index fe0e088fb..d48227030 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css @@ -12,7 +12,6 @@ flex-wrap: wrap; } -/* Added hover effect for custom-styled buttons */ .map-filter-container .btn { transition: opacity 0.15s ease-in-out; } @@ -20,7 +19,6 @@ opacity: 0.85; } - .marker-label { background: transparent !important; border: none !important; @@ -61,7 +59,6 @@ div.leaflet-marker-icon.custom-div-icon { border: none !important; } -/* Base style for all RIMO markers */ .rimo-marker { display: flex; justify-content: center; @@ -79,14 +76,13 @@ div.leaflet-marker-icon.custom-div-icon { color: white; } -/* --- New Styles for FCP Markers --- */ .fcp-marker { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; - background-color: #ffc107; /* Yellow */ + background-color: #ffc107; color: #333; font-size: 11px; font-weight: bold; @@ -95,7 +91,67 @@ div.leaflet-marker-icon.custom-div-icon { box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); } -/* Specific styles for each rimo_type */ +/* --- Styles for FCP Popup --- */ +.fcp-popup-content { + font-family: Arial, sans-serif; + width: 320px; + font-size: 0.8rem; + line-height: 1.4; +} +.fcp-popup-content h5 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + border-bottom: 1px solid #ddd; + padding-bottom: 5px; + font-size: 1rem; + font-weight: 600; +} +.fcp-popup-content a { + color: #007bff; + text-decoration: none; + margin-bottom: 15px; + display: inline-block; +} +.fcp-popup-content a:hover { + text-decoration: underline; +} +.fcp-popup-content .summary-block { + margin-bottom: 15px; +} +.fcp-popup-content .summary-block strong { + display: block; + margin-bottom: 5px; + color: #555; + font-weight: 600; +} +.fcp-popup-content table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + margin-top: 10px; +} +.fcp-popup-content th, +.fcp-popup-content td { + padding: 6px 8px; + border: 1px solid #ddd; + text-align: left; + vertical-align: middle; +} +.fcp-popup-content th { + font-weight: bold; +} +.fcp-popup-content thead tr { + background-color: #f2f2f2; +} +.fcp-popup-content tbody tr:nth-child(even) { + background-color: #f9f9f9; +} +.fcp-popup-content .text-center { + text-align: center; +} + +/* RIMO Marker Colors */ .marker-greenfield { background-color: #28a745; } .marker-residential { background-color: #007bff; } .marker-company { background-color: #ffc107; } diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js index 2fffbd6a7..e5d31483e 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js @@ -2,7 +2,7 @@ Vue.component('PreorderRimoTypeMap', { data() { return { mapMarkers: [], - fcpMarkers: [], // For FCP data + fcpMarkers: [], isLoading: false, window, fetchUrl: window.TT_CONFIG.BASE_PATH + '/Preorder/RimoTypeMapData', @@ -15,7 +15,6 @@ Vue.component('PreorderRimoTypeMap', { }, mapInstance: null, activeFilters: [], - // Single source of truth for RIMO type definitions rimoTypeDefs: { greenfield: { text: 'Greenfield', icon: 'fas fa-tree', color: '#28a745' }, residential: { text: 'Wohngebiet', icon: 'fas fa-home', color: '#007bff' }, @@ -27,9 +26,6 @@ Vue.component('PreorderRimoTypeMap', { }; }, computed: { - /** - * Dynamically generates filter options from the rimoTypeDefs object. - */ filterOptions() { return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({ value, @@ -37,10 +33,6 @@ Vue.component('PreorderRimoTypeMap', { icon: defs.icon })); }, - /** - * Combines filtered RIMO markers with all FCP markers for map display. - * FCPs are always visible, regardless of filters. - */ filteredMapMarkers() { const rimoMarkers = this.activeFilters.length === 0 ? this.mapMarkers @@ -72,9 +64,6 @@ Vue.component('PreorderRimoTypeMap', { } }, methods: { - /** - * Main data fetching orchestrator. Fetches RIMO and FCP data in parallel. - */ async fetchAllMapData() { if (!this.selectedCampaign) return; this.isLoading = true; @@ -87,15 +76,11 @@ Vue.component('PreorderRimoTypeMap', { ]); } catch (err) { console.error("Failed to load map data:", err); - // A general error message is sufficient as specific ones are shown by sub-methods. } finally { this.isLoading = false; } }, - /** - * Fetches and processes RIMO location data. - */ async fetchRimoData() { try { const response = await axios.post(this.fetchUrl, { campaignId: this.selectedCampaign }); @@ -106,19 +91,33 @@ Vue.component('PreorderRimoTypeMap', { } } catch (err) { window.notify('error', 'Laden der RIMO-Kartendaten fehlgeschlagen.'); - throw err; // Re-throw to be caught by the orchestrator + throw err; } }, - /** - * Fetches and processes FCP (Fiber Connection Point) data. - */ async fetchFCPData() { try { - const url = `${window.TT_CONFIG.BASE_PATH}/Preorder/Api?do=getFCPsForCampaign&campaign_id=${this.selectedCampaign}`; - const response = await axios.get(url); - if (response.data.status === 'OK' && Array.isArray(response.data.result)) { - this.fcpMarkers = response.data.result.map(fcp => ({ + // Step 1: Fetch FCP locations + const fcpLocationUrl = `${window.TT_CONFIG.BASE_PATH}/Preorder/Api?do=getFCPsForCampaign&campaign_id=${this.selectedCampaign}`; + const fcpResponse = await axios.get(fcpLocationUrl); + + if (fcpResponse.data.status !== "OK" || !fcpResponse.data.result?.length) { + console.warn('No FCP locations found or API error.'); + return; + } + const fcpLocations = fcpResponse.data.result; + + // Step 2: Fetch FCP stats using the IDs from the first call + const fcpIds = fcpLocations.map(fcp => fcp.real_id); + const statsUrl = `${window.TT_CONFIG.BASE_PATH}/Preorder/Api?do=getRimoFcpStats`; + const statsResponse = await axios.post(statsUrl, { fcp_ids: fcpIds }); + const fcpStats = statsResponse.data.status === "OK" ? statsResponse.data.result : []; + const statsMap = new Map(fcpStats.map(s => [s.fcp_id, s])); + + // Step 3: Create markers with detailed popup content + this.fcpMarkers = fcpLocations.map(fcp => { + const stat = statsMap.get(String(fcp.real_id)); + return { lat: fcp.lat, lng: fcp.lng, options: { @@ -128,22 +127,71 @@ Vue.component('PreorderRimoTypeMap', { iconSize: [28, 28], iconAnchor: [14, 14], }, - tooltip: { - content: `FCP: ${fcp.text}`, - direction: 'top', - }, - zIndexOffset: 500 // Keep FCPs slightly above other markers + asyncPopupContent: () => this.generateFcpPopupHtml(fcp, stat), + zIndexOffset: 500 }, - })); - } else { - console.warn('Could not retrieve FCP data or data format is invalid.'); - } + }; + }); } catch (err) { window.notify('warning', 'Laden der FCP-Daten fehlgeschlagen. Die Karte wird ohne sie angezeigt.'); - // Do not re-throw; failing to load FCPs should not block the entire map. + console.error("FCP data fetch failed:", err); } }, + generateFcpPopupHtml(fcp, fcpStat) { + const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${fcp.lat},${fcp.lng}`; + let statsHtml; + + if (!fcpStat) { + statsHtml = `

Keine Statistiken für diesen FCP gefunden.

`; + } else { + const tableRows = Object.entries(fcpStat.counts_by_rimo_type || {}) + .map(([type, counts]) => { + const normalizedType = this.getNormalizedRimoType(type); + const typeDef = this.rimoTypeDefs[normalizedType] || this.rimoTypeDefs.other; + const typeDisplay = `${typeDef.text}`; + return ` + + ${typeDisplay} + ${counts.hausnummer_count} + ${counts.wohneinheit_count} + ${counts.preorder_count} + `; + }).join(''); + + const table = tableRows ? ` + + + + + + + + + + ${tableRows} +
TypGEBWEBE
` : '

Keine Detail-Statistiken verfügbar.

'; + + statsHtml = ` +
+ Zusammenfassung + Gebäude: ${fcpStat.total_hausnummer_count}
+ Wohneinheiten: ${fcpStat.total_wohneinheit_count}
+ Bestellungen: ${fcpStat.total_active_preorders} +
+ ${table}`; + } + + return ` +
+
FCP: ${fcp.text}
+ + In Google Maps anzeigen + + ${statsHtml} +
`; + }, + processData(data) { const groupedData = {}; @@ -220,12 +268,13 @@ Vue.component('PreorderRimoTypeMap', { getNormalizedRimoType(type) { const lowerType = (type || '').toLowerCase(); - if (lowerType.includes('2/3')) return 'residential'; // Catches '2/3' as residential + if (lowerType.includes('2/3')) return 'residential'; if (lowerType.includes('greenfield')) return 'greenfield'; if (lowerType.includes('residential')) return 'residential'; - if (lowerType.includes('multiple dwelling')) return 'multiple-dwelling'; + if (lowerType.includes('multiple dwelling') || lowerType.includes('multiple dwellings')) return 'multiple-dwelling'; if (lowerType.includes('company') || lowerType.includes('commercial')) return 'company'; if (lowerType.includes('public') || lowerType.includes('school')) return 'public'; + if (lowerType.includes('unknown')) return 'other'; return 'other'; }, @@ -299,7 +348,7 @@ Vue.component('PreorderRimoTypeMap', {
-
+
Keine Standorte für die ausgewählte Kampagne gefunden.