Merge branch 'Preorder/add-new-map' into 'master'
added new adbrimofcp map See merge request fronk/thetool!1751
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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: `<div class="fcp-marker">${fcp.text}</div>`,
|
||||
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', {
|
||||
<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">
|
||||
<a href="https://www.google.com/maps?q=${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">
|
||||
@@ -145,7 +211,7 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
</a>
|
||||
</div><hr class="my-1">`;
|
||||
});
|
||||
return content.slice(0, -16) + `</div>`; // Remove last <hr>
|
||||
return content.slice(0, -16) + `</div>`;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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: `
|
||||
<tt-card style="height: 75vh; position: relative; display: flex; flex-direction: column;">
|
||||
@@ -219,7 +292,8 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
<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']"
|
||||
class="btn btn-sm"
|
||||
:style="getFilterButtonStyle(filter.value)"
|
||||
:title="filter.text">
|
||||
<i :class="filter.icon"></i>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user