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: `