- No FCP locations found.
+
+ Keine FCP-Standorte gefunden.
`
diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css
index 087836799..ddd526f1c 100644
--- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css
+++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css
@@ -1,27 +1,51 @@
-.map-filter-container,
-.map-legend-container {
+.main-filter-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
padding: 0.5rem 0.75rem;
background-color: rgba(255, 255, 255, 0.9);
- border: 1px solid rgba(0,0,0,0.1);
+ border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.25);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+}
+
+.main-filter-container .map-filter-container {
+ padding: 0;
+ background: none;
+ border: none;
+ box-shadow: none;
}
.map-filter-container {
- margin-left: 40px;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
-.map-filter-container h6 {
- line-height: 1; /* Align header text with buttons */
+.filter-label {
+ font-size: 0.8rem;
+ font-weight: bold;
+ color: #6c757d;
+ margin: 0;
+ line-height: 1;
+}
+
+.filters-wrapper {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.filters-wrapper .no-user-select {
+ user-select: none;
}
.map-filter-container .btn {
transition: all 0.15s ease-in-out;
}
+
.map-filter-container .btn:hover {
transform: translateY(-1px);
opacity: 0.9;
@@ -38,25 +62,15 @@
margin: 0 0.25rem;
}
-/* Move legend to the left to make space for the logo */
-.tt-map-bottom-controls {
- bottom: 90px !important;
-}
-
-/* Style for the custom logo control container */
-.leaflet-control-logo {
- background-color: rgba(255, 255, 255, 0.7);
- padding: 2px 5px;
- border-radius: 5px;
- box-shadow: 0 1px 5px rgba(0,0,0,0.4);
-}
-.leaflet-control-logo img {
- display: block;
-}
-
-.map-legend-container {
- font-size: 0.8rem;
- line-height: 1.4;
+.map-actions-card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background-color: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
.map-legend-container h6 {
@@ -66,6 +80,51 @@
padding-bottom: 0.25rem;
}
+.map-legend-container .legend-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.35rem;
+}
+
+.map-legend-container .legend-item .legend-icon {
+ margin-right: 0.5rem;
+ flex-shrink: 0;
+}
+
+.map-legend-container .legend-item .legend-text {
+ font-size: 0.8rem;
+}
+
+.not2connect-icons {
+ display: flex;
+ align-items: center;
+ padding-left: 20px;
+}
+
+.desktop-only-legend-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+.mobile-only-legend-buttons {
+ display: none;
+}
+#PreorderRimoTypeMap {
+ width: 100%;
+ height: 80vh;
+ display: flex;
+ flex-direction: column
+}
+
+@media (max-width: 991px) {
+ .desktop-only-legend-buttons { display: none; }
+ .mobile-only-legend-buttons { display: block; }
+ .page-title-box { display: none; }
+ body > .wrapper > .content-page > footer { display: none; }
+ .container-fluid { padding: 0 !important; margin: 0 !important; }
+ #PreorderRimoTypeMap { height: calc(100vh - 70px) !important; width: 100vw !important; }
+}
+
.marker-label {
background: transparent !important;
border: none !important;
@@ -89,16 +148,15 @@
}
.tooltip-content-wrapper.marker-label-highlight {
- background-color: #dc3545; /* A solid, vibrant red */
- border-color: #c82333; /* A slightly darker border for definition */
- color: white; /* White text for maximum contrast */
+ background-color: #dc3545;
+ border-color: #c82333;
+ color: white;
}
-
.tooltip-content-wrapper.marker-label-saturated {
- background-color: #28a745; /* A solid, vibrant green */
- border-color: #218838; /* A slightly darker border for definition */
- color: white; /* White text for maximum contrast */
+ background-color: #28a745;
+ border-color: #218838;
+ color: white;
}
div.leaflet-marker-icon.custom-div-icon {
@@ -136,7 +194,7 @@ div.leaflet-marker-icon.custom-div-icon {
font-weight: bold;
border-radius: 50%;
border: 3px solid white;
- box-shadow: 0 4px 8px rgba(0,0,0,0.3);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.fcp-marker::after {
@@ -150,27 +208,32 @@ div.leaflet-marker-icon.custom-div-icon {
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 12px solid #ffc107;
- filter: drop-shadow(0 4px 2px rgba(0,0,0,0.3));
+ filter: drop-shadow(0 4px 2px rgba(0, 0, 0, 0.3));
}
-/* --- Fault Indicator on Marker --- */
.marker-has-fault .rimo-marker {
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
- 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); }
- 70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); }
- 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
+ 0% {
+ box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(220, 53, 69, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
+ }
}
-/* --- Styles for Building & FCP Popups --- */
.building-popup-content, .fcp-popup-content {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
width: 320px;
font-size: 0.8rem;
line-height: 1.4;
}
+
.building-popup-content h5, .fcp-popup-content h5 {
margin-top: 0;
margin-bottom: 10px;
@@ -180,30 +243,36 @@ div.leaflet-marker-icon.custom-div-icon {
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;
@@ -211,20 +280,23 @@ div.leaflet-marker-icon.custom-div-icon {
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;
}
-/* --- Fault Reporting Specific Styles --- */
.fault-indicator-popup {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
@@ -234,24 +306,29 @@ div.leaflet-marker-icon.custom-div-icon {
font-size: 0.8rem;
margin-top: 0.5rem;
}
+
.fault-indicator-popup ul {
margin: 0.5rem 0 0 1rem;
padding: 0;
}
+
.fault-reporting-form h6 {
font-size: 0.9rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
+
.fault-reporting-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: normal;
cursor: pointer;
}
+
.fault-reporting-form input[type="checkbox"] {
margin-right: 0.5rem;
}
+
.fault-reporting-form .fault-other-textarea {
width: 100%;
padding: 0.25rem 0.5rem;
@@ -260,12 +337,11 @@ div.leaflet-marker-icon.custom-div-icon {
border: 1px solid #ccc;
margin-top: 0.25rem;
}
+
.fault-reporting-form .fault-other-textarea.hidden {
display: none;
}
-
-/* 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 af4c219f9..665229c08 100644
--- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
+++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
@@ -19,106 +19,94 @@ Vue.component('PreorderRimoTypeMap', {
showOnlyFaults: false,
showFcps: true,
showFaultsModal: false,
- logoControlAdded: false,
userIdToNameMap: new Map(),
- editingFault: null, // Temporary state for the fault being edited
+ editingFault: null,
faultReasons: [
- { value: 'building_type', text: 'Gebäudetyp ist falsch' },
- { value: 'home_count', text: 'Anzahl der Wohneinheiten ist falsch' },
- { value: 'not_existent', text: 'Gebäude existiert nicht' },
- { value: 'other', text: 'Sonstiges/Bemerkung' }
+ {value: 'building_type', text: 'Gebäudetyp ist falsch'},
+ {value: 'home_count', text: 'Anzahl der Wohneinheiten ist falsch'},
+ {value: 'not_existent', text: 'Gebäude existiert nicht'},
+ {value: 'other', text: 'Sonstiges/Bemerkung'}
],
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: '#bf2d69' }
+ 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: '#bf2d69'},
+ gross: {text: 'Großanschluss', icon: 'fas fa-industry', color: '#6c757d'}
}
}),
computed: {
campaignOptions() {
if (!this.allCampaigns) return [];
- return this.allCampaigns.map(campaign => ({
- value: campaign.id,
- text: campaign.name
- }));
+ return this.allCampaigns.map(c => ({value: c.id, text: c.name}));
},
filterOptions() {
- return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({ value, ...defs }));
+ return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({value, ...defs}));
},
filteredMapMarkers() {
let rimoMarkers = this.mapMarkers;
- if (this.showOnlyFaults)
+ if (this.showOnlyFaults) {
rimoMarkers = rimoMarkers.filter(marker => {
const fault = this.faults[marker.hausnummerId];
return fault && !fault.done;
});
+ }
- if (this.activeFilters.length > 0)
+ if (this.activeFilters.length > 0) {
rimoMarkers = rimoMarkers.filter(marker => this.activeFilters.includes(marker.rimoType));
+ }
return this.showFcps ? [...rimoMarkers, ...this.fcpMarkers] : rimoMarkers;
},
faultsForModal() {
if (!this.rawRimoData.length) return [];
-
- return Object.entries(this.faults)
- .map(([hausnummerId, faultData]) => {
- const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId);
- if (!rimoItem) return null;
-
- return {
- ...faultData,
- hausnummerId,
- rimo_id: rimoItem.rimo_id,
- address: `${rimoItem.strasse_name} ${rimoItem.hausnummer}, ${rimoItem.plz_name} ${rimoItem.ortschaft_name}`,
- translated_reasons: faultData.reasons.map(r => this.faultReasons.find(fr => fr.value === r)?.text || r),
- done_by_user: this.userIdToNameMap.get(String(faultData.done_by)) || `User #${faultData.done_by}`
- };
- })
- .filter(Boolean)
- .sort((a, b) => {
- if (a.done && !b.done) return 1;
- if (!a.done && b.done) return -1;
- return a.address.localeCompare(b.address);
- });
+ return Object.entries(this.faults).map(([hausnummerId, faultData]) => {
+ const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId);
+ if (!rimoItem) return null;
+ return {
+ ...faultData,
+ hausnummerId,
+ rimo_id: rimoItem.rimo_id,
+ address: `${rimoItem.strasse_name} ${rimoItem.hausnummer}, ${rimoItem.plz_name} ${rimoItem.ortschaft_name}`,
+ translated_reasons: faultData.reasons.map(r => this.faultReasons.find(fr => fr.value === r)?.text || r),
+ done_by_user: this.userIdToNameMap.get(String(faultData.done_by)) || `User #${faultData.done_by}`
+ };
+ }).filter(Boolean).sort((a, b) => {
+ if (a.done && !b.done) return 1;
+ if (!a.done && b.done) return -1;
+ return a.address.localeCompare(b.address);
+ });
}
},
watch: {
selectedCampaign(newCampaignId) {
- const localStorageKey = 'rimoMapSelectedCampaign';
if (newCampaignId) {
- localStorage.setItem(localStorageKey, newCampaignId);
+ localStorage.setItem('rimoMapSelectedCampaign', newCampaignId);
this.fetchAllMapData();
} else {
- localStorage.removeItem(localStorageKey);
- this.mapMarkers = [];
- this.fcpMarkers = [];
- this.faults = {};
- this.activeFilters = [];
+ localStorage.removeItem('rimoMapSelectedCampaign');
+ this.mapMarkers = []; this.fcpMarkers = []; this.faults = {}; this.activeFilters = [];
}
+ },
+ showFcps(newVal) {
+ localStorage.setItem('rimoMapShowFcps', JSON.stringify(newVal));
}
},
created() {
this.allCampaigns = window.TT_CONFIG?.ALL_CAMPAIGNS || [];
const urlParams = new URLSearchParams(window.location.search);
const campaignIdParam = urlParams.get('preordercampaign_id');
- const localStorageKey = 'rimoMapSelectedCampaign';
+ const savedCampaignId = localStorage.getItem('rimoMapSelectedCampaign');
if (campaignIdParam && this.allCampaigns.some(c => String(c.id) === campaignIdParam)) {
- // URL parameter has priority and will be saved by the watcher
this.selectedCampaign = campaignIdParam;
- } else {
- const savedCampaignId = localStorage.getItem(localStorageKey);
- if (savedCampaignId && this.allCampaigns.some(c => String(c.id) === savedCampaignId)) {
- this.selectedCampaign = savedCampaignId;
- } else if (this.allCampaigns.length === 1) {
- // Fallback for first time or if saved campaign is invalid
- this.selectedCampaign = this.allCampaigns[0].id;
- }
+ } else if (savedCampaignId && this.allCampaigns.some(c => String(c.id) === savedCampaignId)) {
+ this.selectedCampaign = savedCampaignId;
+ } else if (this.allCampaigns.length === 1) {
+ this.selectedCampaign = this.allCampaigns[0].id;
}
},
mounted() {
@@ -135,271 +123,132 @@ Vue.component('PreorderRimoTypeMap', {
delete window.saveEditingFault;
},
methods: {
- addLogoToMap() {
- if (this.$refs.ttMap?.map && !this.logoControlAdded) {
- const LogoControl = L.Control.extend({
- onAdd: function(map) {
- const container = L.DomUtil.create('div', 'leaflet-control-logo');
- container.innerHTML = `
`;
- L.DomEvent.disableClickPropagation(container);
- return container;
- }
- });
- new LogoControl({ position: 'bottomright' }).addTo(this.$refs.ttMap.map);
- this.logoControlAdded = true;
- }
- },
async fetchAllMapData() {
if (!this.selectedCampaign) return;
this.isLoading = true;
- this.mapMarkers = [];
- this.fcpMarkers = [];
- this.faults = {};
- this.activeFilters = [];
-
-
+ this.mapMarkers = []; this.fcpMarkers = []; this.faults = {}; this.activeFilters = [];
try {
await this.fetchFaultData();
await Promise.all([this.fetchRimoData(), this.fetchFCPData()]);
} finally {
this.isLoading = false;
- this.$nextTick(() => this.addLogoToMap());
}
},
async fetchFaultData() {
- const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapGetFaults`, {
- params: { preordercampaign_id: this.selectedCampaign }
- });
- if (response.data.success) {
- this.faults = response.data.faults && typeof response.data.faults === 'object' && !Array.isArray(response.data.faults) ? response.data.faults : {};
- }
+ const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapGetFaults`, {params: {preordercampaign_id: this.selectedCampaign}});
+ if (res.data.success) this.faults = res.data.faults && typeof res.data.faults === 'object' && !Array.isArray(res.data.faults) ? res.data.faults : {};
},
async fetchRimoData() {
- const response = await axios.post(this.fetchUrl, { campaignId: this.selectedCampaign });
- if (response.data.success && Array.isArray(response.data.data)) {
- this.rawRimoData = response.data.data;
+ const res = await axios.post(this.fetchUrl, {campaignId: this.selectedCampaign});
+ if (res.data.success && Array.isArray(res.data.data)) {
+ this.rawRimoData = res.data.data;
this.mapMarkers = this.processData(this.rawRimoData);
- } else {
- window.notify('error', 'Ungültiges RIMO-Datenformat von der API empfangen.');
- }
+ } else window.notify('error', 'Ungültiges RIMO-Datenformat von der API empfangen.');
},
async fetchFCPData() {
- 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) return;
-
- const fcpLocations = fcpResponse.data.result;
+ const fcpRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Preorder/Api?do=getFCPsForCampaign&campaign_id=${this.selectedCampaign}`);
+ if (fcpRes.data.status !== "OK" || !fcpRes.data.result?.length) return;
+ const fcpLocations = fcpRes.data.result;
const fcpIds = fcpLocations.map(fcp => fcp.real_id);
if (fcpIds.length === 0) return;
-
- 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]));
-
+ const statsRes = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Preorder/Api?do=getRimoFcpStats`, {fcp_ids: fcpIds});
+ const statsMap = new Map(statsRes.data.status === "OK" ? statsRes.data.result.map(s => [s.fcp_id, s]) : []);
this.fcpMarkers = fcpLocations.map(fcp => ({
- lat: fcp.lat,
- lng: fcp.lng,
+ lat: fcp.lat, lng: fcp.lng,
options: {
noCluster: true,
- icon: {
- className: 'custom-div-icon',
- html: `
${fcp.text}
`,
- iconSize: [30, 42],
- iconAnchor: [15, 42],
- },
+ icon: {className: 'custom-div-icon', html: `
${fcp.text}
`, iconSize: [30, 42], iconAnchor: [15, 42]},
asyncPopupContent: () => this.generateFcpPopupHtml(fcp, statsMap.get(fcp.real_id))
},
}));
},
_generateFcpStatsTable(fcpStat) {
if (!fcpStat?.counts_by_rimo_type) return '
Keine Detail-Statistiken verfügbar.
';
-
- 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 tableRows = Object.entries(fcpStat.counts_by_rimo_type).map(([type, counts]) => {
+ const typeDef = this.rimoTypeDefs[this.getNormalizedRimoType(type)] || this.rimoTypeDefs.other;
+ return `
${typeDef.text}${counts.hausnummer_count} ${counts.wohneinheit_count} ${counts.preorder_count} `;
+ }).join('');
if (!tableRows) return '
Keine Detail-Statistiken verfügbar.
';
-
- return `
-
-
- Typ
- GEB
- WE
- BE
-
-
- ${tableRows}
-
`;
+ return `
`;
},
generateFcpPopupHtml(fcp, fcpStat) {
- const googleMapsLink = `http://googleusercontent.com/maps.google.com/4{fcp.lat},${fcp.lng}`;
- const summaryHtml = fcpStat ?
- `
Gebäude: ${fcpStat.total_hausnummer_count}
-
Wohneinheiten: ${fcpStat.total_wohneinheit_count}
-
Bestellungen: ${fcpStat.total_active_preorders} ` :
- 'Keine Statistiken für diesen FCP gefunden.';
-
- return ``;
+ const summaryHtml = fcpStat ? `
Gebäude: ${fcpStat.total_hausnummer_count} Wohneinheiten: ${fcpStat.total_wohneinheit_count} Bestellungen: ${fcpStat.total_active_preorders} ` : 'Keine Statistiken für diesen FCP gefunden.';
+ return ``;
},
processData(data) {
const groupedData = {};
data.forEach(item => {
if (!item) return;
const latLngKey = `${item.gps_lat},${item.gps_long}`;
- if (!groupedData[latLngKey]) {
- groupedData[latLngKey] = { ...item, wohneinheit_count: 0, preorder_count: 0, original_items: [] };
- }
+ if (!groupedData[latLngKey]) groupedData[latLngKey] = {...item, wohneinheit_count: 0, preorder_count: 0, original_items: []};
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);
});
-
return Object.values(groupedData).map(group => {
- const rimoType = this.getNormalizedRimoType(group.rimo_type);
+ const rimoType = group.rimo_op_state === 'Not2Connect' ? 'gross' : this.getNormalizedRimoType(group.rimo_type);
const markerIcon = this.getMarkerIcon(rimoType);
const fault = this.faults[group.hausnummer_id];
const hasFault = fault && !fault.done && (fault.reasons.length > 0 || (fault.other && fault.other.trim() !== ''));
-
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';
-
- if (group.rimo_op_state === 'Not2Connect')
- markerIcon.class = 'marker-gross';
-
+ 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,
- hausnummerId: group.hausnummer_id,
+ lat: group.gps_lat, lng: group.gps_long, rimoType, hausnummerId: group.hausnummer_id,
options: {
- icon: {
- className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`,
- html: `
`,
- iconSize: [30, 30],
- iconAnchor: [15, 30],
- },
- tooltip: {
- content: `
H: ${group.wohneinheit_count} B: ${group.preorder_count}
`,
- direction: 'bottom',
- className: 'marker-label',
- permanent: true,
- minZoom: 18
- },
+ icon: {className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`, html: `
`, iconSize: [30, 30], iconAnchor: [15, 30]},
+ tooltip: {content: `
H: ${group.wohneinheit_count} B: ${group.preorder_count}
`, direction: 'bottom', className: 'marker-label', permanent: true, minZoom: 18},
asyncPopupContent: async () => this.generateBuildingPopupHtml(group),
},
};
});
},
_generateBuildingDetailsHtml(itemGroup) {
- return itemGroup.original_items.map(item => {
- const googleMapsLink = `http://googleusercontent.com/maps/contrib/117320308544975743452/reviews/@${item.gps_lat},${item.gps_long}`;
- const addressDbLink = `https://thetool.xinon.at/AddressDB/View?id=${item.hausnummer_id}`;
- return `
+ return itemGroup.original_items.map(item => `
+
${item.strasse_name} ${item.hausnummer}, ${item.plz_name} ${item.ortschaft_name}
Rimo Type: ${item.rimo_type || 'N/A'}
Rimo Op State: ${item.rimo_op_state || 'N/A'}
-
Rimo Ex State: ${item.rimo_ex_state || 'N/A'}
-
Wohneinheiten gesamt: ${item.wohneinheit_count}
-
Bestellungen: ${item.preorder_count}
+
Wohneinheiten: ${item.wohneinheit_count} |
Bestellungen: ${item.preorder_count}
Links:
-
Karte
-
AddressDB
-
`;
- }).join('
');
+
Karte
+
AddressDB
+
`
+ ).join('
');
},
_generateBuildingFaultDisplayHtml(faultData) {
if (!faultData.reasons?.length && !faultData.other) return '';
-
const reasonsList = faultData.reasons.map(r => `
${this.faultReasons.find(fr => fr.value === r)?.text || r} `).join('');
const otherText = faultData.other ? `
Sonstiges: ${faultData.other} ` : '';
-
- return ``;
+ return ``;
},
_generateBuildingFaultFormHtml(itemGroup, faultData) {
const formInputs = this.faultReasons.map(reason => {
const isChecked = faultData.reasons.includes(reason.value);
- const otherInput = (reason.value === 'other') ?
- `
` :
- '';
-
- return `
-
- ${reason.text}
-
- ${otherInput}`;
+ const otherInput = (reason.value === 'other') ? `
` : '';
+ return `
${reason.text}${otherInput}`;
}).join('');
-
- return `
Fehler melden/bearbeiten:
- ${formInputs}
-
Fehler Speichern `;
+ return `
Fehler melden/bearbeiten: ${formInputs}
Fehler Speichern `;
},
generateBuildingPopupHtml(itemGroup) {
- const currentFault = this.faults[itemGroup.hausnummer_id] || { reasons: [], other: '', done: false };
- this.editingFault = {
- hausnummerId: itemGroup.hausnummer_id,
- data: JSON.parse(JSON.stringify(currentFault))
- };
-
+ const currentFault = this.faults[itemGroup.hausnummer_id] || {reasons: [], other: '', done: false};
+ this.editingFault = {hausnummerId: itemGroup.hausnummer_id, data: JSON.parse(JSON.stringify(currentFault))};
const detailsHtml = this._generateBuildingDetailsHtml(itemGroup);
const faultFormHtml = this._generateBuildingFaultFormHtml(itemGroup, this.editingFault.data);
- const faultDisplayHtml = this.editingFault.data.done ?
- `
Dieser Fehler wurde von ${this.userIdToNameMap.get(String(this.editingFault.data.done_by)) || `User #${this.editingFault.data.done_by}`} am ${new Date(this.editingFault.data.done_at).toLocaleDateString()} als erledigt markiert.
` :
- this._generateBuildingFaultDisplayHtml(this.editingFault.data);
-
- return ``;
+ const faultDisplayHtml = this.editingFault.data.done ? `
Dieser Fehler wurde von ${this.userIdToNameMap.get(String(this.editingFault.data.done_by)) || `User #${this.editingFault.data.done_by}`} am ${new Date(this.editingFault.data.done_at).toLocaleDateString()} als erledigt markiert.
` : this._generateBuildingFaultDisplayHtml(this.editingFault.data);
+ return ``;
},
updateTempFault(reason, value) {
if (!this.editingFault) return;
-
if (this.editingFault.data.done) {
- this.editingFault.data.done = false;
- this.editingFault.data.done_by = null;
- this.editingFault.data.done_at = null;
+ this.editingFault.data.done = false; this.editingFault.data.done_by = null; this.editingFault.data.done_at = null;
}
-
const fault = this.editingFault.data;
-
- if (reason === 'other_text') {
- fault.other = value;
- } else { // It's a checkbox change
+ if (reason === 'other_text') fault.other = value;
+ else {
const index = fault.reasons.indexOf(reason);
- if (value && index === -1) { // checked
- fault.reasons.push(reason);
- } else if (!value && index > -1) { // unchecked
- fault.reasons.splice(index, 1);
- }
-
+ if (value && index === -1) fault.reasons.push(reason);
+ else if (!value && index > -1) fault.reasons.splice(index, 1);
if (reason === 'other') {
const popup = this.$refs.ttMap?.map?._popup;
if (popup?.isOpen()) {
@@ -407,10 +256,7 @@ Vue.component('PreorderRimoTypeMap', {
if (otherTextarea) {
const hasOther = fault.reasons.includes('other');
otherTextarea.classList.toggle('hidden', !hasOther);
- if (!hasOther) {
- otherTextarea.value = '';
- fault.other = '';
- }
+ if (!hasOther) { otherTextarea.value = ''; fault.other = ''; }
}
}
}
@@ -418,81 +264,46 @@ Vue.component('PreorderRimoTypeMap', {
},
async saveFaults(hausnummerIdToUpdate) {
const hausnummerId = hausnummerIdToUpdate || this.editingFault?.hausnummerId;
- if (!hausnummerId) {
- if (this.editingFault) this.editingFault = null;
- return;
- }
-
- if (this.editingFault && this.editingFault.hausnummerId === hausnummerId) {
- this.$set(this.faults, hausnummerId, this.editingFault.data);
- }
-
- const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapSaveFaults`, {
- campaignId: this.selectedCampaign,
- faults: this.faults
- });
-
- if (response.data.success) {
+ if (!hausnummerId) { if (this.editingFault) this.editingFault = null; return; }
+ if (this.editingFault && this.editingFault.hausnummerId === hausnummerId) this.$set(this.faults, hausnummerId, this.editingFault.data);
+ const res = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapSaveFaults`, {campaignId: this.selectedCampaign, faults: this.faults});
+ if (res.data.success) {
window.notify('success', 'Fehlerbericht gespeichert.');
this.$refs.ttMap?.map.closePopup();
-
const mapComponent = this.$refs.ttMap;
if (mapComponent && mapComponent.markerLayer) {
mapComponent.markerLayer.eachLayer(marker => {
if (marker.tt_hausnummerId == hausnummerId) {
const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId);
if (rimoItem) {
- const rimoType = this.getNormalizedRimoType(rimoItem.rimo_type);
+ const rimoType = rimoItem.rimo_op_state === 'Not2Connect' ? 'gross' : this.getNormalizedRimoType(rimoItem.rimo_type);
const fault = this.faults[hausnummerId];
const hasFault = fault && !fault.done;
const markerIconDef = this.getMarkerIcon(rimoType);
-
- const newIcon = L.divIcon({
- className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`,
- html: `
`,
- iconSize: [30, 30],
- iconAnchor: [15, 30],
- });
+ const newIcon = L.divIcon({className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`, html: `
`, iconSize: [30, 30], iconAnchor: [15, 30]});
marker.setIcon(newIcon);
}
}
});
}
- } else {
- window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.');
- }
+ } else window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.');
this.editingFault = null;
},
async markFaultAsDone(hausnummerId) {
if (!hausnummerId || !this.faults[hausnummerId]) return;
-
- this.$set(this.faults, hausnummerId, {
- ...this.faults[hausnummerId],
- done: true,
- done_by: window.TT_CONFIG.USER_ID,
- done_at: new Date().toISOString()
- });
-
+ this.$set(this.faults, hausnummerId, {...this.faults[hausnummerId], done: true, done_by: window.TT_CONFIG.USER_ID, done_at: new Date().toISOString()});
await this.saveFaults(hausnummerId);
},
zoomToFaultMarker(hausnummerId) {
const map = this.$refs.ttMap?.map;
const markerLayer = this.$refs.ttMap?.markerLayer;
-
if (!map || !markerLayer) return window.notify('error', 'Kartenkomponente ist nicht bereit.');
-
const markerInstance = markerLayer.getLayers().find(m => m.tt_hausnummerId == hausnummerId);
if (!markerInstance) return window.notify('warning', 'Marker konnte nicht auf der Karte gefunden werden.');
-
this.showFaultsModal = false;
- map.flyTo(markerInstance.getLatLng(), 19, { duration: 1 });
-
+ map.flyTo(markerInstance.getLatLng(), 19, {duration: 1});
setTimeout(() => markerLayer.zoomToShowLayer(markerInstance, () => markerInstance.openPopup()), 1100);
},
- toggleShowFcps() {
- this.showFcps = !this.showFcps;
- localStorage.setItem('rimoMapShowFcps', JSON.stringify(this.showFcps));
- },
getNormalizedRimoType(type) {
const lowerType = (type || '').toLowerCase();
if (lowerType.includes('greenfield')) return 'greenfield';
@@ -500,83 +311,80 @@ Vue.component('PreorderRimoTypeMap', {
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';
+ if (lowerType.includes('grossanschluss')) return 'gross';
return 'other';
},
getMarkerIcon(rimoType) {
const def = this.rimoTypeDefs[rimoType] || this.rimoTypeDefs.other;
- return { class: `marker-${rimoType}`, icon: def.icon };
+ return {class: `marker-${rimoType}`, icon: def.icon};
},
toggleFilter(filterValue) {
const index = this.activeFilters.indexOf(filterValue);
if (index > -1) this.activeFilters.splice(index, 1);
else this.activeFilters.push(filterValue);
},
- isFilterActive(filterValue) {
- return this.activeFilters.includes(filterValue);
- },
+ isFilterActive(filterValue) { return this.activeFilters.includes(filterValue); },
getFilterButtonStyle(filterValue) {
const color = this.rimoTypeDefs[filterValue]?.color || '#6c757d';
- return this.isFilterActive(filterValue) ?
- { backgroundColor: color, borderColor: color, color: 'white' } :
- { color: color, backgroundColor: 'white', borderColor: color };
+ return this.isFilterActive(filterValue) ? {backgroundColor: color, borderColor: color, color: 'white'} : {color: color, backgroundColor: 'white', borderColor: color};
},
},
template: `
-
-
+
+
-
-
-
-
-
Filter:
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
Filter:
+
+
+
+
+ Nur Fehler
+
+
+ FCPs
+
+
+
-
-
-
Legende
-
H: Homes (Wohneinheiten)
-
B: Bestellungen
-
-
-
- Fehlerliste ({{ faultsForModal.filter(f => !f.done).length }})
-
+
+
+
+
+
+
+
+
+
+ H:
+ Wohneinheiten
+
+
+ B:
+ Bestellungen
+
+
+
+
+
+
@@ -586,9 +394,7 @@ Vue.component('PreorderRimoTypeMap', {
Keine Fehler für diese Kampagne gemeldet.
@@ -606,24 +412,19 @@ Vue.component('PreorderRimoTypeMap', {
-
- Auf Karte zeigen
-
+
Erledigt
- von {{ fault.done_by_user }}
- {{ new Date(fault.done_at).toLocaleDateString() }}
+ von {{ fault.done_by_user }} am {{ new Date(fault.done_at).toLocaleDateString() }}
-
- Als erledigt markieren
-
+
diff --git a/public/plugins/vue/tt-components/css/tt-map.css b/public/plugins/vue/tt-components/css/tt-map.css
index 5992ac184..471b5952c 100644
--- a/public/plugins/vue/tt-components/css/tt-map.css
+++ b/public/plugins/vue/tt-components/css/tt-map.css
@@ -1,9 +1,8 @@
-/* tt-map.css */
.tt-map-wrapper {
position: relative;
width: 100%;
height: 100%;
- background-color: #f0f0f0; /* Placeholder color while map loads */
+ background-color: #f0f0f0;
}
.tt-map-container {
@@ -25,95 +24,105 @@
align-items: center;
}
-/* Slot for top controls like filters */
-.tt-map-top-controls {
+.tt-map-top-container {
position: absolute;
top: 10px;
left: 10px;
- z-index: 401;
+ right: 10px;
+ z-index: 402;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+ pointer-events: none;
+ flex-wrap: wrap;
}
-/* Slot for bottom controls like legend */
-.tt-map-bottom-controls {
+.tt-map-top-container > * {
+ pointer-events: auto;
+}
+
+.tt-map-top-left-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-start;
+}
+
+.tt-map-top-right-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-end;
+ margin-right: 50px;
+}
+
+.tt-map-bottom-right-container {
position: absolute;
bottom: 30px;
right: 10px;
z-index: 401;
-}
-
-/* Container for built-in map buttons (zoom, layer toggle) */
-.tt-map-builtin-controls {
- position: absolute;
- top: 60px; /* Pushed down to make space for the search bar */
- right: 10px;
- z-index: 401;
display: flex;
- gap: 0.5rem;
flex-direction: column;
align-items: flex-end;
+ gap: 10px;
}
-.tt-map-builtin-controls .btn {
- min-width: 120px;
- text-align: left;
- box-shadow: 0 1px 5px rgba(0,0,0,0.4);
+.tt-map-bottom-right-container.with-logo {
+ bottom: 90px;
}
-.tt-map-settings-panel {
- background-color: white;
- padding: 0.75rem;
- border-radius: 5px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.25);
- border: 1px solid rgba(0,0,0,0.1);
+.leaflet-control-container .leaflet-top.leaflet-right {
+ z-index: 403;
}
-.tt-map-settings-panel .btn-group .btn {
- font-size: 0.8rem;
+.leaflet-control-zoom {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
-.popup-loader {
- text-align: center;
- padding: 10px;
-}
-
-/* --- NEW MAP SEARCH STYLES --- */
.tt-map-search-wrapper {
+ width: 100%;
+ max-width: 350px;
+}
+
+.tt-input-with-icon {
+ position: relative;
+ width: 100%;
+}
+
+.tt-input-with-icon .prefix-icon {
position: absolute;
- top: 10px;
- right: 10px;
- z-index: 402;
- width: 320px;
- max-width: calc(100% - 20px);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #888;
+ pointer-events: none;
+ z-index: 5;
+}
+
+.tt-input-with-icon input.has-prefix-icon {
+ padding-left: 32px !important;
}
.tt-map-search-input-container {
position: relative;
display: flex;
align-items: center;
- background-color: rgba(255, 255, 255, 0.9);
- border: 1px solid rgba(0,0,0,0.1);
+ background-color: white;
border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.25);
- padding: 0.25rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
height: 42px;
+ min-width: 250px;
+ padding: 12px;
}
-.tt-map-search-input {
+.tt-map-search-input-container .tt-input {
width: 100%;
- height: 100%;
- padding: 0 30px 0 32px;
- border: none;
- background-color: transparent;
- font-size: 14px;
- outline: none;
}
-.tt-map-search-input-container .search-icon {
- position: absolute;
- left: 12px;
- color: #888;
- pointer-events: none;
+.tt-map-search-input-container .tt-input input {
+ padding-right: 30px !important;
+ border: 1px solid rgba(0, 0, 0, 0.1) !important;
}
.tt-map-search-input-container .clear-icon {
@@ -125,10 +134,7 @@
color: #aaa;
font-size: 16px;
padding: 5px;
-}
-
-.tt-map-search-input-container .clear-icon:hover {
- color: #333;
+ z-index: 5;
}
.tt-map-search-results {
@@ -137,14 +143,13 @@
margin: 8px 0 0 0;
background-color: #fff;
border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
}
-.tt-map-search-results .result-item,
-.tt-map-search-results .result-item-info {
+.tt-map-search-results .result-item, .tt-map-search-results .result-item-info {
padding: 10px 15px;
cursor: pointer;
display: flex;
@@ -171,7 +176,128 @@
cursor: default;
}
-/* Fade animation for the results list */
+.tt-map-builtin-controls {
+ position: relative;
+}
+
+.tt-map-builtin-controls .btn {
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
+}
+
+.tt-map-settings-panel {
+ position: absolute;
+ top: calc(100% + 5px);
+ right: 0;
+ z-index: 10;
+ background-color: white;
+ padding: 0.75rem;
+ border-radius: 5px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.map-legend-wrapper {
+ background-color: rgba(255, 255, 255, 0.9);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ max-width: 300px;
+}
+
+.map-legend-header {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.map-legend-header h6 {
+ margin: 0;
+ font-weight: bold;
+}
+
+.map-legend-header i.fa-chevron-down {
+ transition: transform 0.2s ease;
+}
+
+.map-legend-wrapper:not(.collapsed) .map-legend-header i.fa-chevron-down {
+ transform: rotate(180deg);
+}
+
+.map-legend-content {
+ padding: 0 0.75rem 0.5rem;
+}
+
+.tt-map-mobile-fab .btn {
+ width: 56px;
+ height: 56px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+.tt-map-mobile-fab {
+ position: absolute;
+ bottom: calc(90px + env(safe-area-inset-bottom));
+ right: 10px;
+ z-index: 401;
+}
+
+.tt-map-mobile-controls-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 1050;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+}
+
+.tt-map-mobile-controls-panel {
+ background-color: #fff;
+ width: 100%;
+ max-height: 75vh;
+ border-top-left-radius: 1rem;
+ border-top-right-radius: 1rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-header {
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid #e9ecef;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.panel-header h5 {
+ margin-bottom: 0;
+ font-size: 1.1rem;
+}
+
+.panel-content {
+ padding: 1rem;
+ overflow-y: auto;
+}
+
+.panel-section:not(:last-child) {
+ margin-bottom: 1.5rem;
+}
+
+.panel-section h6 {
+ font-weight: bold;
+ margin-bottom: 0.75rem;
+}
+
+.panel-section .map-filter-container {
+ flex-direction: column;
+ align-items: stretch;
+ margin-left: 0;
+ padding: 0;
+ background: none;
+ border: none;
+ box-shadow: none;
+}
+
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s ease;
}
@@ -180,7 +306,6 @@
opacity: 0;
}
-/* --- NEW CUSTOM MARKER STYLE --- */
.custom-map-marker {
background-color: rgba(0, 123, 255, 0.9);
width: 24px;
@@ -212,4 +337,15 @@
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
}
+}
+
+.leaflet-control-logo {
+ background-color: rgba(255, 255, 255, 0.7);
+ padding: 2px 5px;
+ border-radius: 5px;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
+}
+
+.leaflet-control-logo img {
+ display: block;
}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-input.js b/public/plugins/vue/tt-components/tt-input.js
index feded25c1..17add5e96 100644
--- a/public/plugins/vue/tt-components/tt-input.js
+++ b/public/plugins/vue/tt-components/tt-input.js
@@ -11,6 +11,7 @@ Vue.component('tt-input', {
additionalProps: Object,
sm: {type: Boolean, default: false},
noFormGroup: {type: Boolean, default: false},
+ prefixIcon: {type: String, default: null}
},
data() {
return {
@@ -23,25 +24,26 @@ Vue.component('tt-input', {
}
},
template: `
-
+
{{ label }}
+
+ @blur="$emit('blur', $event)" />
{{ hint }}
`
-});
+});
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-map.js b/public/plugins/vue/tt-components/tt-map.js
index 4036de12e..dce9093e3 100644
--- a/public/plugins/vue/tt-components/tt-map.js
+++ b/public/plugins/vue/tt-components/tt-map.js
@@ -1,49 +1,35 @@
Vue.component('tt-map', {
props: {
- markersData: {
- type: Array,
- default: () => [] // Expecting [{ lat: Number, lng: Number, options: { ..., noCluster?: Boolean, tooltip: { content: '...', minZoom: 18, ... } } }, ...]
- },
- config: {
- type: Object,
- default: () => ({}) // User overrides for defaults
- },
- loading: {
- type: Boolean,
- default: false
- }
- },
- data() {
- return {
- map: null,
- markerLayer: null, // For clustered markers
- nonClusteredLayer: null, // For important, always-visible markers
- tileLayers: {
- mapbox: { streets: null, satellite: null },
- basemap: { streets: null, satellite: null }
- },
- mapProvider: localStorage.getItem('tt-map-provider') || 'basemap',
- mapType: localStorage.getItem('tt-map-type') || 'streets',
- internalLoading: true,
- scriptsLoaded: false,
- showSettings: false,
- searchQuery: '',
- searchResults: [],
- isSearchLoading: false,
- showSearchResults: false,
- searchDebounce: null,
- selectedMarker: null, // To hold the temporary marker
- };
+ markersData: { type: Array, default: () => [] },
+ config: { type: Object, default: () => ({}) },
+ loading: { type: Boolean, default: false },
+ showLogo: { type: Boolean, default: false }
},
+ data: () => ({
+ map: null,
+ markerLayer: null,
+ nonClusteredLayer: null,
+ tileLayers: { mapbox: { streets: null, satellite: null }, basemap: { streets: null, satellite: null } },
+ mapProvider: localStorage.getItem('tt-map-provider') || 'basemap',
+ mapType: localStorage.getItem('tt-map-type') || 'streets',
+ internalLoading: true,
+ scriptsLoaded: false,
+ showSettings: false,
+ searchQuery: '',
+ searchResults: [],
+ isSearchLoading: false,
+ showSearchResults: false,
+ searchDebounce: null,
+ selectedMarker: null,
+ isLegendOpen: false,
+ isMobile: window.innerWidth < 992,
+ showMobileControls: false
+ }),
computed: {
- isLoading() {
- return this.internalLoading || this.loading;
- },
+ isLoading() { return this.internalLoading || this.loading; },
mapConfig() {
const defaults = {
- center: [47.07, 15.44], // Centered on Graz, Styria
- zoom: 9,
- mapboxKey: window.TT_CONFIG?.MAPBOX_KEY,
+ center: [47.07, 15.44], zoom: 9, mapboxKey: window.TT_CONFIG?.MAPBOX_KEY,
streetsTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
streetsTileId: 'mapbox/streets-v11',
satelliteTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
@@ -51,11 +37,11 @@ Vue.component('tt-map', {
tileAttribution: '©
Mapbox ©
OpenStreetMap ',
basemapStreetsTileUrl: 'https://maps.wien.gv.at/basemap/geolandbasemap/normal/google3857/{z}/{y}/{x}.png',
basemapSatelliteTileUrl: 'https://maps.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/{z}/{y}/{x}.jpeg',
- basemapAttribution: 'Datenquelle: basemap.at, Stadt Wien - data.wien.gv.at',
+ basemapAttribution: 'Datenquelle: basemap.at',
clusterOptions: {},
makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" }
};
- return { ...defaults, ...this.config }; // Merge user config over defaults
+ return { ...defaults, ...this.config };
}
},
async mounted() {
@@ -69,6 +55,15 @@ Vue.component('tt-map', {
console.error("Map Initialization Error:", error);
this.internalLoading = false;
}
+ window.addEventListener('resize', this.handleResize);
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.handleResize);
+ if (this.map) {
+ this.map.off('zoomend moveend', this.updateTooltipVisibility);
+ this.map.remove();
+ this.map = null;
+ }
},
methods: {
loadScripts() {
@@ -80,7 +75,6 @@ Vue.component('tt-map', {
{ type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css' },
{ type: 'script', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js' }
];
-
const promises = scripts.map(s => new Promise((resolve, reject) => {
let el;
if (s.type === 'script') {
@@ -88,156 +82,107 @@ Vue.component('tt-map', {
el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject;
} else {
el = document.createElement('link');
- el.rel = 'stylesheet'; el.href = s.url;
- el.onload = resolve;
- el.onerror = () => {
- console.warn(`Could not load stylesheet: ${s.url}`);
- resolve();
- };
+ el.rel = 'stylesheet'; el.href = s.url; el.onload = resolve;
+ el.onerror = () => { console.warn(`Could not load stylesheet: ${s.url}`); resolve(); };
}
document.head.appendChild(el);
}));
return Promise.all(promises);
},
initializeMap() {
- if (!this.scriptsLoaded || typeof L === 'undefined' || typeof L.MarkerClusterGroup === 'undefined' || !this.mapConfig.mapboxKey) {
- console.error("Leaflet or MarkerCluster is not loaded. Cannot initialize map.");
- return;
- }
-
- this.map = L.map(this.$refs.mapContainer, { preferCanvas: true }).setView(this.mapConfig.center, this.mapConfig.zoom);
- if (typeof L.MakiMarkers !== 'undefined') {
- L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
- }
-
- this.map.createPane('fcpPane');
- this.map.getPane('fcpPane').style.zIndex = 599; // Default markerPane is 600
-
+ if (!this.scriptsLoaded || typeof L === 'undefined' || !this.mapConfig.mapboxKey) return;
+ this.map = L.map(this.$refs.mapContainer, { preferCanvas: true, zoomControl: false }).setView(this.mapConfig.center, this.mapConfig.zoom);
+ L.control.zoom({ position: 'topright' }).addTo(this.map);
+ if (typeof L.MakiMarkers !== 'undefined') L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
+ this.map.createPane('fcpPane'); this.map.getPane('fcpPane').style.zIndex = 599;
this.tileLayers.mapbox.streets = L.tileLayer(this.mapConfig.streetsTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.streetsTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey });
this.tileLayers.mapbox.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.satelliteTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey });
this.tileLayers.basemap.streets = L.tileLayer(this.mapConfig.basemapStreetsTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 19 });
this.tileLayers.basemap.satellite = L.tileLayer(this.mapConfig.basemapSatelliteTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 19 });
this.setActiveTileLayer();
- this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions);
+ // --- PERFORMANCE OPTIMIZATION ---
+ const clusterOptions = {
+ ...this.mapConfig.clusterOptions, // Keep existing user-defined options
+ chunkedLoading: true, // Add markers in chunks to avoid freezing
+ maxClusterRadius: 100, // Increase clustering radius for better performance when zoomed out
+ disableClusteringAtZoom: 18 // Disable clustering at high zoom levels
+ };
+ this.markerLayer = L.markerClusterGroup(clusterOptions);
+ // --- END PERFORMANCE OPTIMIZATION ---
+
this.nonClusteredLayer = L.layerGroup();
this.map.addLayer(this.markerLayer);
this.map.addLayer(this.nonClusteredLayer);
-
this.map.on('zoomend moveend', this.updateTooltipVisibility);
-
+ if (this.showLogo) {
+ const LogoControl = L.Control.extend({ onAdd: map => {
+ const container = L.DomUtil.create('div', 'leaflet-control-logo');
+ container.innerHTML = `
`;
+ L.DomEvent.disableClickPropagation(container);
+ return container;
+ }});
+ new LogoControl({ position: 'bottomright' }).addTo(this.map);
+ }
this.$nextTick(() => { this.map.invalidateSize(); });
- window.addEventListener('resize', this.handleResize);
},
setActiveTileLayer() {
if (!this.map) return;
- Object.values(this.tileLayers).forEach(provider => {
- Object.values(provider).forEach(layer => {
- if (layer && this.map.hasLayer(layer)) {
- this.map.removeLayer(layer);
- }
- });
- });
-
+ Object.values(this.tileLayers).forEach(p => Object.values(p).forEach(l => { if (l && this.map.hasLayer(l)) this.map.removeLayer(l); }));
const activeLayer = this.tileLayers[this.mapProvider]?.[this.mapType];
- if (activeLayer) {
- activeLayer.addTo(this.map);
- } else {
- this.tileLayers.mapbox.streets.addTo(this.map);
- }
+ if (activeLayer) activeLayer.addTo(this.map); else this.tileLayers.mapbox.streets.addTo(this.map);
},
updateMarkers() {
if (!this.map || !this.markerLayer || !this.scriptsLoaded) return;
- this.markerLayer.clearLayers();
- this.nonClusteredLayer.clearLayers();
+ this.markerLayer.clearLayers(); this.nonClusteredLayer.clearLayers();
const markersToCluster = [];
-
this.markersData.forEach(data => {
if (data.lat == null || data.lng == null) return;
-
let icon;
if (data.options?.icon instanceof L.Icon) icon = data.options.icon;
else if (data.options?.icon) icon = L.divIcon(data.options.icon);
- else if (typeof L.MakiMarkers !== 'undefined') {
- const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) };
- icon = L.MakiMarkers.icon(makiOptions);
- }
-
+ else if (typeof L.MakiMarkers !== 'undefined') icon = L.MakiMarkers.icon({ ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) });
const markerOptions = { icon };
if (data.options?.zIndexOffset) markerOptions.zIndexOffset = data.options.zIndexOffset;
-
- if (data.options?.noCluster) {
- markerOptions.pane = 'fcpPane';
- }
-
+ if (data.options?.noCluster) markerOptions.pane = 'fcpPane';
const marker = L.marker([data.lat, data.lng], markerOptions);
-
- // Attach a unique identifier if provided
- if (data.hausnummerId) {
- marker.tt_hausnummerId = data.hausnummerId;
- }
-
- if (data.options?.tooltip) {
- marker.tt_tooltip_options = data.options.tooltip;
- }
-
+ if (data.hausnummerId) marker.tt_hausnummerId = data.hausnummerId;
+ if (data.options?.tooltip) marker.tt_tooltip_options = data.options.tooltip;
if (data.options?.popup) marker.bindPopup(data.options.popup);
else if (data.options?.asyncPopupContent) {
marker.bindPopup(() => '');
- marker.on('popupopen', async (e) => {
- const content = await data.options.asyncPopupContent(data);
- e.popup.setContent(content).update();
- });
- }
-
- if (data.options?.noCluster) {
- this.nonClusteredLayer.addLayer(marker);
- } else {
- markersToCluster.push(marker);
+ marker.on('popupopen', async e => e.popup.setContent(await data.options.asyncPopupContent(data)).update());
}
+ if (data.options?.noCluster) this.nonClusteredLayer.addLayer(marker);
+ else markersToCluster.push(marker);
});
- if (markersToCluster.length > 0) {
- this.markerLayer.addLayers(markersToCluster);
- }
+ // The chunkedLoading option will automatically handle adding these without freezing
+ if (markersToCluster.length > 0) this.markerLayer.addLayers(markersToCluster);
this.updateTooltipVisibility();
-
- const allMarkers = this.markerLayer.getLayers().concat(this.nonClusteredLayer.getLayers());
+ const allMarkers = [...this.markerLayer.getLayers(), ...this.nonClusteredLayer.getLayers()];
if (allMarkers.length > 0) {
- const bounds = L.latLngBounds(allMarkers.map(marker => marker.getLatLng()));
- if (bounds.isValid()) {
- this.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 17 });
- }
+ const bounds = L.latLngBounds(allMarkers.map(m => m.getLatLng()));
+ if (bounds.isValid()) this.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 17 });
}
},
updateTooltipVisibility() {
if (!this.map) return;
const currentZoom = this.map.getZoom();
-
- const processMarker = (marker) => {
+ const processMarker = marker => {
const tooltipOptions = marker.tt_tooltip_options;
if (!tooltipOptions || !tooltipOptions.minZoom) return;
-
const isBound = marker.getTooltip();
-
if (currentZoom >= tooltipOptions.minZoom) {
if (!isBound) {
const { content, minZoom, ...options } = tooltipOptions;
marker.bindTooltip(content, options);
- if (options.permanent) {
- marker.openTooltip();
- }
+ if (options.permanent) marker.openTooltip();
}
- } else {
- if (isBound) {
- marker.unbindTooltip();
- }
- }
+ } else if (isBound) marker.unbindTooltip();
};
-
- this.markerLayer.eachLayer(processMarker);
- this.nonClusteredLayer.eachLayer(processMarker);
+ this.markerLayer.eachLayer(processMarker); this.nonClusteredLayer.eachLayer(processMarker);
},
toggleMapType() {
this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets';
@@ -250,178 +195,120 @@ Vue.component('tt-map', {
this.setActiveTileLayer();
this.showSettings = false;
},
- handleResize() {
- if (this.map) {
- this.map.invalidateSize();
- }
- },
+ handleResize() { this.isMobile = window.innerWidth < 992; if (this.map) this.map.invalidateSize(); },
searchLocations() {
if (this.searchDebounce) clearTimeout(this.searchDebounce);
- if (this.searchQuery.length < 3) {
- this.searchResults = [];
- this.showSearchResults = true;
- return;
- }
+ if (this.searchQuery.length < 3) { this.searchResults = []; this.showSearchResults = true; return; }
this.isSearchLoading = true;
this.searchDebounce = setTimeout(async () => {
try {
- const response = await axios.get('/Geocoding/autocomplete', {
- params: { q: this.searchQuery }
- });
- this.searchResults = response.data;
- } catch (error) {
- console.error("Geocoding search failed:", error);
- this.searchResults = [];
- } finally {
- this.isSearchLoading = false;
- }
+ const res = await axios.get('/Geocoding/autocomplete', { params: { q: this.searchQuery } });
+ this.searchResults = res.data;
+ } catch (e) { this.searchResults = []; } finally { this.isSearchLoading = false; }
}, 350);
},
selectLocation(location) {
- this.searchQuery = location.text;
- this.showSearchResults = false;
+ this.searchQuery = location.text; this.showSearchResults = false;
const [lat, lng] = location.value.split(',').map(Number);
-
if (this.map && !isNaN(lat) && !isNaN(lng)) {
this.map.flyTo([lat, lng], 17, { duration: 0.5 });
-
- if (this.selectedMarker) {
- this.selectedMarker.remove();
- }
-
- const customIcon = L.divIcon({
- html: `
`,
- className: '', // Important to keep it empty
- iconSize: [24, 24],
- iconAnchor: [12, 12],
- popupAnchor: [0, -14]
- });
-
- this.selectedMarker = L.marker([lat, lng], { icon: customIcon }).addTo(this.map)
- .bindPopup(`
${location.text} `)
- .openPopup();
-
- this.searchQuery = '';
- this.searchResults = [];
-
- setTimeout(() => {
- if (this.selectedMarker) {
- this.selectedMarker.remove();
- this.selectedMarker = null;
- }
- }, 10000);
+ if (this.selectedMarker) this.selectedMarker.remove();
+ const icon = L.divIcon({ html: `
`, className: '', iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -14] });
+ this.selectedMarker = L.marker([lat, lng], { icon }).addTo(this.map).bindPopup(`
${location.text} `).openPopup();
+ this.searchQuery = ''; this.searchResults = [];
+ setTimeout(() => { if (this.selectedMarker) { this.selectedMarker.remove(); this.selectedMarker = null; } }, 10000);
}
},
clearSearch() {
- this.searchQuery = '';
- this.searchResults = [];
- this.showSearchResults = false;
- if (this.selectedMarker) {
- this.selectedMarker.remove();
- this.selectedMarker = null;
- }
+ this.searchQuery = ''; this.searchResults = []; this.showSearchResults = false;
+ if (this.selectedMarker) { this.selectedMarker.remove(); this.selectedMarker = null; }
},
- handleSearchFocus() {
- this.showSearchResults = true;
- },
- handleSearchBlur() {
- setTimeout(() => {
- this.showSearchResults = false;
- }, 200);
- }
+ handleSearchFocus() { this.showSearchResults = true; },
+ handleSearchBlur() { setTimeout(() => { this.showSearchResults = false; }, 200); },
},
watch: {
markersData: { handler() { this.updateMarkers(); }, deep: true },
- loading(newVal) {
- if (!newVal && this.map) {
- this.$nextTick(() => this.map.invalidateSize());
- }
- }
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.handleResize);
- if (this.map) {
- this.map.off('zoomend moveend', this.updateTooltipVisibility);
- this.map.remove();
- this.map = null;
- }
+ loading(newVal) { if (!newVal && this.map) this.$nextTick(() => this.map.invalidateSize()); }
},
template: `