added new adbrimofcp map

This commit is contained in:
2025-09-16 11:51:25 +02:00
parent e79b2392c9
commit 5db3a64e34
7 changed files with 445 additions and 25 deletions
@@ -0,0 +1,89 @@
.preorder-map-container {
height: 100%; /* Changed from 85vh to fill parent */
width: 100%;
}
.map-filter-container {
display: flex;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
flex-wrap: wrap;
}
.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;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
color: #333;
font-size: 12px;
font-weight: bold;
padding: 2px 5px;
text-align: center;
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 */
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 */
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 */
.rimo-marker {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
clip-path: inset(0 round 50%);
}
.rimo-icon {
font-size: 16px;
color: white;
}
/* Specific styles for each rimo_type */
.marker-greenfield { background-color: #28a745; }
.marker-residential { background-color: #007bff; }
.marker-company { background-color: #ffc107; }
.marker-multiple-dwelling { background-color: #6f42c1; }
.marker-public { background-color: #17a2b8; }
.marker-other { background-color: #6c757d; }
@@ -0,0 +1,236 @@
Vue.component('PreorderRimoTypeMap', {
data() {
return {
mapMarkers: [],
isLoading: false,
window,
fetchUrl: window.TT_CONFIG.BASE_PATH + '/Preorder/RimoTypeMapData',
selectedCampaign: null,
mapConfig: {
clusterOptions: {
spiderfyOnMaxZoom: false,
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' }
]
};
},
computed: {
/**
* Filters markers based on the activeFilters array.
* If no filters are active, all markers are shown.
*/
filteredMapMarkers() {
if (this.activeFilters.length === 0) {
return this.mapMarkers;
}
return this.mapMarkers.filter(marker => this.activeFilters.includes(marker.rimoType));
}
},
async created() {
const urlParams = new URLSearchParams(window.location.search);
this.selectedCampaign = urlParams.get('preordercampaign_id');
if (this.selectedCampaign) {
await this.fetchAndPrepareData();
}
},
mounted() {
this.$nextTick(() => {
const ttMapComponent = this.$refs.ttMap;
if (ttMapComponent && ttMapComponent.map) {
this.mapInstance = ttMapComponent.map;
this.mapInstance.on('zoomend', this.checkZoomLevel);
this.checkZoomLevel();
}
});
},
beforeDestroy() {
if (this.mapInstance) {
this.mapInstance.off('zoomend', this.checkZoomLevel);
}
},
methods: {
async fetchAndPrepareData() {
if (!this.selectedCampaign) return;
this.isLoading = true;
this.mapMarkers = [];
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.');
}
} catch (err) {
window.notify('error', 'Laden der Kartendaten fehlgeschlagen.');
} finally {
this.isLoading = false;
}
},
processData(data) {
const groupedData = {};
data.forEach(item => {
const latLngKey = `${item.gps_lat},${item.gps_long}`;
if (!groupedData[latLngKey]) {
groupedData[latLngKey] = {
...item,
wohneinheit_count: parseInt(item.wohneinheit_count, 10),
preorder_count: parseInt(item.preorder_count, 10),
original_items: [item],
};
} else {
groupedData[latLngKey].wohneinheit_count += parseInt(item.wohneinheit_count, 10);
groupedData[latLngKey].preorder_count += parseInt(item.preorder_count, 10);
groupedData[latLngKey].original_items.push(item);
}
});
return Object.values(groupedData).map(group => {
const rimoType = this.getNormalizedRimoType(group.rimo_type);
const markerIcon = this.getMarkerIcon(rimoType);
let tooltipInnerClass = '';
if (rimoType !== 'greenfield' && group.wohneinheit_count > 0 && group.wohneinheit_count === group.preorder_count) {
tooltipInnerClass = ' marker-label-saturated';
} else if (rimoType === 'greenfield' && group.preorder_count > 0) {
tooltipInnerClass = ' marker-label-highlight';
}
return {
lat: group.gps_lat,
lng: group.gps_long,
rimoType: rimoType, // Add normalized rimoType for filtering
options: {
icon: {
className: `custom-div-icon marker-${rimoType}`,
html: `<div class="rimo-marker ${markerIcon.class}"><i class="${markerIcon.icon} rimo-icon"></i></div>`,
iconSize: [30, 30],
iconAnchor: [15, 30],
},
tooltip: {
content: `<div class="tooltip-content-wrapper${tooltipInnerClass}">H: ${group.wohneinheit_count}<br>B: ${group.preorder_count}</div>`,
direction: 'bottom',
className: 'marker-label',
permanent: true,
},
asyncPopupContent: async () => {
let content = `<div style="font-size: 0.85rem;">`;
group.original_items.forEach(item => {
content += `
<div class="mb-2">
<h5 class="mb-2 mt-1">${item.strasse_name} ${item.hausnummer}, ${item.plz_name} ${item.ortschaft_name}</h5>
<strong>Rimo Type:</strong> ${item.rimo_type || 'N/A'}<br>
<strong>Rimo Op State:</strong> ${item.rimo_op_state || 'N/A'}<br>
<strong>Rimo Ex State:</strong> ${item.rimo_ex_state || 'N/A'}<br>
<strong>Wohn. gesamt:</strong> ${item.wohneinheit_count}<br>
<strong>Bestellungen:</strong> ${item.preorder_count}<br>
<strong>Koordinaten:</strong>
<a href="https://www.google.com/maps/search/?api=1&query=${item.gps_lat},${item.gps_long}" target="_blank" class="text-primary">
<i class="fas fa-map-marker-alt mr-1"></i>Karte
</a>
<a href="https://thetool.xinon.at/AddressDB/View?id=${item.hausnummer_id}" target="_blank" class="text-primary ml-2">
<i class="fas fa-info-circle mr-1"></i>AddressDB
</a>
</div><hr class="my-1">`;
});
return content.slice(0, -16) + `</div>`; // Remove last <hr>
},
},
};
});
},
getNormalizedRimoType(type) {
const lowerType = (type || '').toLowerCase();
if (lowerType.includes('greenfield')) return 'greenfield';
if (lowerType.includes('residential')) return 'residential';
if (lowerType.includes('multiple dwelling')) return 'multiple-dwelling';
if (lowerType.includes('company') || lowerType.includes('commercial')) return 'company';
if (lowerType.includes('public') || lowerType.includes('school')) return 'public';
return 'other';
},
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' }
};
return icons[rimoType] || icons.other;
},
checkZoomLevel() {
if (!this.mapInstance) return;
const currentZoom = this.mapInstance.getZoom();
const minZoomForLabel = 16;
const visibility = currentZoom >= minZoomForLabel ? 'visible' : 'hidden';
const opacity = currentZoom >= minZoomForLabel ? '1' : '0';
document.querySelectorAll('.marker-label').forEach(el => {
el.style.visibility = visibility;
el.style.opacity = opacity;
});
},
/**
* 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) {
this.activeFilters.splice(index, 1);
} else {
this.activeFilters.push(filterValue);
}
},
/**
* Checks if a filter is currently active.
* @param {string} filterValue - The rimo_type to check.
* @returns {boolean}
*/
isFilterActive(filterValue) {
return this.activeFilters.includes(filterValue);
}
},
template: `
<tt-card style="height: 75vh; position: relative; display: flex; flex-direction: column;">
<div v-if="!selectedCampaign" class="alert alert-warning m-3">
Bitte eine Kampagne über den URL-Parameter 'preordercampaign_id' auswählen (z.B. ?preordercampaign_id=44).
</div>
<template v-else>
<div class="map-filter-container">
<button v-for="filter in filterOptions"
:key="filter.value"
@click="toggleFilter(filter.value)"
:class="['btn', 'btn-sm', isFilterActive(filter.value) ? 'btn-primary' : 'btn-outline-secondary']"
:title="filter.text">
<i :class="filter.icon"></i>
</button>
</div>
<div style="height: 100%; position: relative; flex-grow: 1;">
<div v-if="!isLoading && mapMarkers.length === 0" class="alert alert-info m-3">
Keine Standorte für die ausgewählte Kampagne gefunden.
</div>
<tt-map ref="ttMap" :markers-data="filteredMapMarkers" :loading="isLoading" :config="mapConfig" class="preorder-map-container"></tt-map>
</div>
</template>
</tt-card>
`
});