diff --git a/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js b/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js index 5d2fd4747..d376237f5 100644 --- a/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js +++ b/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js @@ -17,59 +17,48 @@ Vue.component('ADBRimoFcpMap', { this.error = null; try { const response = await axios.get(this.fetchUrl); - if (response.data && response.data.success && Array.isArray(response.data.data)) { - this.mapMarkers = response.data.data - .filter(fcp => fcp.gps_lat != null && fcp.gps_long != null) - .map(fcp => ({ - lat: fcp.gps_lat, - lng: fcp.gps_long, - options: { - asyncPopupContent: async (markerData) => { - const response = await axios.get(`${this.window.TT_CONFIG.BASE_PATH}/ADBRimoFcp/getById?id=${fcp.id}`); - const fullFcpData = response.data; - return ` -
-
${fullFcpData.name}
- - - - - - - - - - - - - - - - - - - - - - - -
RIMO ID:${fullFcpData.rimo_id}
RIMO Ex State:${fullFcpData.rimo_ex_state}
RIMO Op State:${fullFcpData.rimo_op_state}
Building Type:${fullFcpData.building_type}
Coordinates: - - ${fullFcpData.gps_lat},
${fullFcpData.gps_long} -
-
-
-`; }, - } - })); - } else { + if (!response.data?.success || !Array.isArray(response.data.data)) { console.error("Invalid data format from API:", response.data); - this.error = "Invalid data format received."; + this.error = "Ungültiges Datenformat von der API empfangen."; this.mapMarkers = []; + return; } + + this.mapMarkers = response.data.data + .filter(fcp => fcp.gps_lat != null && fcp.gps_long != null) + .map(fcp => ({ + lat: fcp.gps_lat, + lng: fcp.gps_long, + options: { + asyncPopupContent: async () => { + const res = await axios.get(`${this.window.TT_CONFIG.BASE_PATH}/ADBRimoFcp/getById?id=${fcp.id}`); + const fullFcpData = res.data; + return ` +
+
${fullFcpData.name}
+ + + + + + + + + + +
RIMO ID:${fullFcpData.rimo_id}
RIMO Ex State:${fullFcpData.rimo_ex_state}
RIMO Op State:${fullFcpData.rimo_op_state}
Building Type:${fullFcpData.building_type}
Koordinaten: + + ${fullFcpData.gps_lat},
${fullFcpData.gps_long} +
+
+
`; + }, + } + })); + } catch (err) { console.error("Error fetching FCP data:", err); - this.error = "Failed to load FCP locations."; + this.error = "Laden der FCP-Standorte fehlgeschlagen."; this.mapMarkers = []; } finally { this.isLoading = false; @@ -78,15 +67,11 @@ Vue.component('ADBRimoFcpMap', { }, template: ` - -
- {{ error }} -
+ +
{{ error }}
-
- 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 ` - - - - - - - - - ${tableRows} -
TypGEBWEBE
`; + return `${tableRows}
TypGEBWEBE
`; }, 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 `
-
FCP: ${fcp.text}
- In Google Maps anzeigen -
- Zusammenfassung - ${summaryHtml} -
- ${this._generateFcpStatsTable(fcpStat)} -
`; + 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 `
FCP: ${fcp.text}
In Google Maps anzeigen
Zusammenfassung${summaryHtml}
${this._generateFcpStatsTable(fcpStat)}
`; }, 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 `
    - Gemeldeter Fehler: -
      ${reasonsList}${otherText}
    -
    `; + return `
    Gemeldeter Fehler:
      ${reasonsList}${otherText}
    `; }, _generateBuildingFaultFormHtml(itemGroup, faultData) { const formInputs = this.faultReasons.map(reason => { const isChecked = faultData.reasons.includes(reason.value); - const otherInput = (reason.value === 'other') ? - `` : - ''; - - return ` - ${otherInput}`; + const otherInput = (reason.value === 'other') ? `` : ''; + return `${otherInput}`; }).join(''); - - return `
    Fehler melden/bearbeiten:
    - ${formInputs} - `; + return `
    Fehler melden/bearbeiten:
    ${formInputs}`; }, 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 `
    - ${detailsHtml} - ${faultDisplayHtml} -
    -
    ${faultFormHtml}
    -
    `; + 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 `
    ${detailsHtml}${faultDisplayHtml}
    ${faultFormHtml}
    `; }, 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: ` -
    - +
    + -