From 76a9fd5a025377685deb82db03b3329e09fd5c75 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 16 Sep 2025 12:19:31 +0200 Subject: [PATCH] added new adbrimofcp map --- application/Preorder/PreorderModel.php | 2 +- .../PreorderRimoTypeMap.css | 43 +++-- .../PreorderRimoTypeMap.js | 172 +++++++++++++----- 3 files changed, 153 insertions(+), 64 deletions(-) diff --git a/application/Preorder/PreorderModel.php b/application/Preorder/PreorderModel.php index 04ebaa0b0..32321710e 100644 --- a/application/Preorder/PreorderModel.php +++ b/application/Preorder/PreorderModel.php @@ -1359,7 +1359,7 @@ ORDER BY h.id AS hausnummer_id, h.gps_lat, h.gps_long, h.rimo_type, h.rimo_op_state, h.rimo_ex_state, h.hausnummer, s.name AS strasse_name, plz.plz AS plz_name, o.name AS ortschaft_name, COUNT(DISTINCT we.id) AS wohneinheit_count, - COUNT(DISTINCT pr.id) AS preorder_count + COUNT(DISTINCT ps.id) AS preorder_count FROM `{$addressDbName}`.`Hausnummer` AS h LEFT JOIN `{$addressDbName}`.`Wohneinheit` AS we ON h.id = we.hausnummer_id LEFT JOIN `{$fronkDbName}`.`Preorder` AS pr ON we.id = pr.adb_wohneinheit_id AND pr.preordercampaign_id = {$safeCampaignId} AND pr.deleted = 0 diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css index c3c259938..fe0e088fb 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css @@ -1,5 +1,5 @@ .preorder-map-container { - height: 100%; /* Changed from 85vh to fill parent */ + height: 100%; width: 100%; } @@ -12,21 +12,25 @@ flex-wrap: wrap; } +/* Added hover effect for custom-styled buttons */ +.map-filter-container .btn { + transition: opacity 0.15s ease-in-out; +} +.map-filter-container .btn:hover { + opacity: 0.85; +} + + .marker-label { - /* This is now the outer, transparent container provided by Leaflet. */ - /* We remove its background and border to let the inner div's style show through. */ background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; - - /* These properties are for Leaflet's positioning and events */ z-index: 1000; pointer-events: none; transition: visibility 0.2s, opacity 0.2s linear; } -/* Base style for the new inner div that holds the content */ .tooltip-content-wrapper { background-color: rgba(255, 255, 255, 0.85); border: 1px solid #ccc; @@ -40,29 +44,24 @@ white-space: nowrap; } -/* Style for greenfield markers with at least one preorder */ .tooltip-content-wrapper.marker-label-highlight { - background-color: rgba(248, 215, 218, 0.9); /* Soft red */ + background-color: rgba(248, 215, 218, 0.9); border-color: #e57373; color: #721c24; } -/* Style for markers where preorders match housing units */ .tooltip-content-wrapper.marker-label-saturated { - background-color: rgba(212, 237, 218, 0.9); /* Light green */ + background-color: rgba(212, 237, 218, 0.9); border-color: #81c784; color: #155724; } -/* * This rule is now more specific to ensure it overrides Leaflet's default styles. - * It targets the DIV element that has BOTH .leaflet-marker-icon AND .custom-div-icon classes. - */ div.leaflet-marker-icon.custom-div-icon { background: transparent !important; border: none !important; } -/* Base style for all markers */ +/* Base style for all RIMO markers */ .rimo-marker { display: flex; justify-content: center; @@ -80,6 +79,22 @@ 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 */ + color: #333; + font-size: 11px; + font-weight: bold; + border-radius: 50%; + border: 2px solid white; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); +} + /* Specific styles for each rimo_type */ .marker-greenfield { background-color: #28a745; } .marker-residential { background-color: #007bff; } diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js index 9d8cacc78..2fffbd6a7 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js @@ -2,6 +2,7 @@ Vue.component('PreorderRimoTypeMap', { data() { return { mapMarkers: [], + fcpMarkers: [], // For FCP data isLoading: false, window, fetchUrl: window.TT_CONFIG.BASE_PATH + '/Preorder/RimoTypeMapData', @@ -12,35 +13,47 @@ Vue.component('PreorderRimoTypeMap', { disableClusteringAtZoom: 17, } }, - mapInstance: null, // Reference to the map instance - activeFilters: [], // To store active rimo_type filters - filterOptions: [ - { value: 'greenfield', text: 'Greenfield', icon: 'fas fa-tree' }, - { value: 'residential', text: 'Wohngebiet', icon: 'fas fa-home' }, - { value: 'company', text: 'Gewerbe', icon: 'fas fa-building' }, - { value: 'multiple-dwelling', text: 'Mehrfamilienhaus', icon: 'fas fa-city' }, - { value: 'public', text: 'Öffentlich', icon: 'fas fa-school' }, - { value: 'other', text: 'Andere', icon: 'fas fa-question-circle' } - ] + 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' }, + company: { text: 'Gewerbe', icon: 'fas fa-building', color: '#ffc107' }, + 'multiple-dwelling': { text: 'Mehrfamilienhaus', icon: 'fas fa-city', color: '#6f42c1' }, + public: { text: 'Öffentlich', icon: 'fas fa-school', color: '#17a2b8' }, + other: { text: 'Andere', icon: 'fas fa-question-circle', color: '#6c757d' } + } }; }, computed: { /** - * Filters markers based on the activeFilters array. - * If no filters are active, all markers are shown. + * Dynamically generates filter options from the rimoTypeDefs object. + */ + filterOptions() { + return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({ + value, + text: defs.text, + icon: defs.icon + })); + }, + /** + * Combines filtered RIMO markers with all FCP markers for map display. + * FCPs are always visible, regardless of filters. */ filteredMapMarkers() { - if (this.activeFilters.length === 0) { - return this.mapMarkers; - } - return this.mapMarkers.filter(marker => this.activeFilters.includes(marker.rimoType)); + const rimoMarkers = this.activeFilters.length === 0 + ? this.mapMarkers + : this.mapMarkers.filter(marker => this.activeFilters.includes(marker.rimoType)); + + return [...rimoMarkers, ...this.fcpMarkers]; } }, async created() { const urlParams = new URLSearchParams(window.location.search); this.selectedCampaign = urlParams.get('preordercampaign_id'); if (this.selectedCampaign) { - await this.fetchAndPrepareData(); + await this.fetchAllMapData(); } }, mounted() { @@ -59,22 +72,75 @@ Vue.component('PreorderRimoTypeMap', { } }, methods: { - async fetchAndPrepareData() { + /** + * Main data fetching orchestrator. Fetches RIMO and FCP data in parallel. + */ + async fetchAllMapData() { if (!this.selectedCampaign) return; - this.isLoading = true; this.mapMarkers = []; + this.fcpMarkers = []; + try { + await Promise.all([ + this.fetchRimoData(), + this.fetchFCPData() + ]); + } 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 }); if (response.data.success && Array.isArray(response.data.data)) { this.mapMarkers = this.processData(response.data.data); } else { - window.notify('error', 'Ungültiges Datenformat von der API empfangen.'); + window.notify('error', 'Ungültiges RIMO-Datenformat von der API empfangen.'); } } catch (err) { - window.notify('error', 'Laden der Kartendaten fehlgeschlagen.'); - } finally { - this.isLoading = false; + window.notify('error', 'Laden der RIMO-Kartendaten fehlgeschlagen.'); + throw err; // Re-throw to be caught by the orchestrator + } + }, + + /** + * 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 => ({ + lat: fcp.lat, + lng: fcp.lng, + options: { + icon: { + className: 'custom-div-icon', + html: `
${fcp.text}
`, + iconSize: [28, 28], + iconAnchor: [14, 14], + }, + tooltip: { + content: `FCP: ${fcp.text}`, + direction: 'top', + }, + zIndexOffset: 500 // Keep FCPs slightly above other markers + }, + })); + } 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. } }, @@ -86,13 +152,13 @@ Vue.component('PreorderRimoTypeMap', { if (!groupedData[latLngKey]) { groupedData[latLngKey] = { ...item, - wohneinheit_count: parseInt(item.wohneinheit_count, 10), - preorder_count: parseInt(item.preorder_count, 10), + wohneinheit_count: parseInt(item.wohneinheit_count, 10) || 0, + preorder_count: parseInt(item.preorder_count, 10) || 0, original_items: [item], }; } else { - groupedData[latLngKey].wohneinheit_count += parseInt(item.wohneinheit_count, 10); - groupedData[latLngKey].preorder_count += parseInt(item.preorder_count, 10); + groupedData[latLngKey].wohneinheit_count += parseInt(item.wohneinheit_count, 10) || 0; + groupedData[latLngKey].preorder_count += parseInt(item.preorder_count, 10) || 0; groupedData[latLngKey].original_items.push(item); } }); @@ -111,7 +177,7 @@ Vue.component('PreorderRimoTypeMap', { return { lat: group.gps_lat, lng: group.gps_long, - rimoType: rimoType, // Add normalized rimoType for filtering + rimoType: rimoType, options: { icon: { className: `custom-div-icon marker-${rimoType}`, @@ -137,7 +203,7 @@ Vue.component('PreorderRimoTypeMap', { Wohn. gesamt: ${item.wohneinheit_count}
Bestellungen: ${item.preorder_count}
Koordinaten: - + Karte @@ -145,7 +211,7 @@ Vue.component('PreorderRimoTypeMap', {
`; }); - return content.slice(0, -16) + ``; // Remove last
+ return content.slice(0, -16) + ``; }, }, }; @@ -154,6 +220,7 @@ Vue.component('PreorderRimoTypeMap', { getNormalizedRimoType(type) { const lowerType = (type || '').toLowerCase(); + if (lowerType.includes('2/3')) return 'residential'; // Catches '2/3' as residential if (lowerType.includes('greenfield')) return 'greenfield'; if (lowerType.includes('residential')) return 'residential'; if (lowerType.includes('multiple dwelling')) return 'multiple-dwelling'; @@ -163,15 +230,11 @@ Vue.component('PreorderRimoTypeMap', { }, getMarkerIcon(rimoType) { - const icons = { - greenfield: { class: 'marker-greenfield', icon: 'fas fa-tree' }, - residential: { class: 'marker-residential', icon: 'fas fa-home' }, - company: { class: 'marker-company', icon: 'fas fa-building' }, - 'multiple-dwelling': { class: 'marker-multiple-dwelling', icon: 'fas fa-city' }, - public: { class: 'marker-public', icon: 'fas fa-school' }, - other: { class: 'marker-other', icon: 'fas fa-question-circle' } + const def = this.rimoTypeDefs[rimoType] || this.rimoTypeDefs.other; + return { + class: `marker-${rimoType}`, + icon: def.icon, }; - return icons[rimoType] || icons.other; }, checkZoomLevel() { @@ -187,10 +250,6 @@ Vue.component('PreorderRimoTypeMap', { }); }, - /** - * Toggles a filter on or off. - * @param {string} filterValue - The rimo_type to toggle. - */ toggleFilter(filterValue) { const index = this.activeFilters.indexOf(filterValue); if (index > -1) { @@ -200,14 +259,28 @@ Vue.component('PreorderRimoTypeMap', { } }, - /** - * Checks if a filter is currently active. - * @param {string} filterValue - The rimo_type to check. - * @returns {boolean} - */ isFilterActive(filterValue) { return this.activeFilters.includes(filterValue); - } + }, + + /** + * Generates the style for a filter button based on its state (active/inactive). + */ + getFilterButtonStyle(filterValue) { + const color = this.rimoTypeDefs[filterValue]?.color || '#6c757d'; + if (this.isFilterActive(filterValue)) { + return { + backgroundColor: color, + borderColor: color, + color: 'white', + }; + } + return { + color: color, + backgroundColor: 'white', + borderColor: color, + }; + }, }, template: ` @@ -219,7 +292,8 @@ Vue.component('PreorderRimoTypeMap', {