diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 8561a7318..5d0fe7974 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,50 +1,42 @@ -# Use Debian Bookworm as base image -FROM debian:bookworm +# Use Debian 13 “Trixie” as base image +FROM debian:trixie -# Install wkhtmltopdf -RUN apt update -RUN apt install wget libfontenc1 xfonts-75dpi xfonts-base xfonts-encodings xfonts-utils openssl build-essential libssl-dev libxrender-dev git-core libx11-dev libxext-dev libfontconfig1-dev libfreetype6-dev fontconfig -y -RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb -RUN dpkg --force-all -i wkhtmltox_0.12.6-1.stretch_amd64.deb -RUN wget https://www.mytaxexpress.com/download/libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb -RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb -RUN wget https://archive.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8b-1_amd64.deb -RUN dpkg -i libjpeg8_8b-1_amd64.deb - -# Install apache2 and PHP and PHP modules +# Install ALL native packages from Debian 13 first RUN apt update && \ - apt install -y poppler-utils apache2 curl cron unzip php8.2 php8.2-imap php8.2-curl php8.2-cli php8.2-mysqli php8.2-gd php8.2-zip php8.2-dom php8.2-mbstring && \ + apt install -y \ + # wkhtmltopdf prerequisites + wget libfontenc1 xfonts-75dpi xfonts-base xfonts-encodings xfonts-utils openssl build-essential libssl-dev libxrender-dev git-core libx11-dev libxext-dev libfontconfig1-dev libfreetype6-dev fontconfig \ + \ + # Apache + PHP + Utils + poppler-utils apache2 curl cron unzip \ + php8.4 php8.4-curl php8.4-cli php8.4-mysqli php8.4-gd php8.4-zip php8.4-dom php8.4-mbstring && \ + \ + # Install Composer curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ - apt clean && \ - rm -rf /var/lib/apt/lists/* + \ + # Clean up apt cache + apt clean && rm -rf /var/lib/apt/lists/* -# Enable PHP in Apache2 -RUN a2enmod php8.2 -RUN a2enmod rewrite +# --- Now, install the old/insecure libraries for wkhtmltopdf --- -# Composer install +RUN wget https://www.mytaxexpress.com/download/libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb && \ + dpkg -i libssl1.1_1.1.1f-1ubuntu2.17_amd64.deb + +RUN wget https://archive.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8b-1_amd64.deb && \ + dpkg -i libjpeg8_8b-1_amd64.deb + +# Finally, install wkhtmltopdf itself, forcing it over the broken dependencies +RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb && \ + dpkg --force-all -i wkhtmltox_0.12.6-1.stretch_amd64.deb + +# Enable PHP in Apache2 and enable rewrite +RUN a2enmod php8.4 \ + && a2enmod rewrite + +# Set working directory and copy composer.json, then run composer install WORKDIR /var/www/html COPY ../../composer.json ./ RUN composer install --no-interaction -COPY ./docker/php/clean_logs.sh /root/clean_logs.sh -RUN chmod +x /root/clean_logs.sh - -# Add cron job for log cleanup -RUN echo "* * * * * /root/clean_old_logs.sh" > /etc/cron.d/clean_old_logs && \ - chmod 0644 /etc/cron.d/clean_old_logs && \ - crontab /etc/cron.d/clean_old_logs - -# Start Apache in the foreground -CMD ["apachectl", "-D", "FOREGROUND"] - - -# Install XDEBUG -# apt install -y php8.2-xdebug -# -# cat <<'EOF' > /etc/php/8.2/apache2/conf.d/99-xdebug-custom.ini - #[xdebug] - #xdebug.mode=profile - #xdebug.start_with_request=trigger - #xdebug.output_dir="/tmp/xdebug_profiles" - #EOF \ No newline at end of file +# Expose port 80 and start Apache in foreground +CMD ["apachectl", "-D", "FOREGROUND"] \ No newline at end of file diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css index 15c27b513..ecebcc660 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css @@ -215,6 +215,14 @@ div.leaflet-marker-icon.custom-div-icon { animation: pulse-red 2s infinite; } +/* New style for missing building markers */ +.marker-missing-building .rimo-marker { + border-style: dashed; + border-width: 3px; + box-shadow: 0 0 8px rgba(220, 53, 69, 0.8); +} + + @keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); @@ -348,4 +356,4 @@ div.leaflet-marker-icon.custom-div-icon { .marker-multiple-dwelling { background-color: #6f42c1; } .marker-public { background-color: #17a2b8; } .marker-other { background-color: #bf2d69; } -.marker-not-to-connect { background-color: #6c757d !important; } \ No newline at end of file +.marker-not-to-connect { background-color: #6c757d !important; } diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js index 2e8e79cb5..ecb29c303 100644 --- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js +++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js @@ -3,6 +3,7 @@ Vue.component('PreorderRimoTypeMap', { rawRimoData: [], mapMarkers: [], fcpMarkers: [], + missingBuildingMarkers: [], // For manually added faults faults: {}, isLoading: false, window, @@ -19,12 +20,15 @@ Vue.component('PreorderRimoTypeMap', { showOnlyFaults: false, showFcps: true, showFaultsModal: false, + showMissingBuildingModal: false, // For new context menu modal + missingBuildingData: null, // For new context menu modal userIdToNameMap: new Map(), 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: 'missing_building', text: 'Gebäude fehlt (manuell markiert)'}, // New reason {value: 'other', text: 'Sonstiges/Bemerkung'} ], rimoTypeDefs: { @@ -44,6 +48,10 @@ Vue.component('PreorderRimoTypeMap', { filterOptions() { return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({value, ...defs})); }, + // Options for the new "missing building" modal + rimoTypeOptionsForMissing() { + return Object.entries(this.rimoTypeDefs).map(([value, defs]) => ({value, text: defs.text})); + }, filteredMapMarkers() { let rimoMarkers = this.mapMarkers; @@ -52,24 +60,48 @@ Vue.component('PreorderRimoTypeMap', { const fault = this.faults[marker.hausnummerId]; return fault && !fault.done; }); + // Also filter missing building markers if showOnlyFaults is true + this.missingBuildingMarkers = this.missingBuildingMarkers.filter(marker => { + const fault = this.faults[marker.hausnummerId]; + return fault && !fault.done; + }); } if (this.activeFilters.length > 0) { rimoMarkers = rimoMarkers.filter(marker => this.activeFilters.includes(marker.rimoType)); + // We don't filter missingBuildingMarkers by rimoType unless we want to + this.missingBuildingMarkers = this.missingBuildingMarkers.filter(marker => { + const fault = this.faults[marker.hausnummerId]; + return fault && this.activeFilters.includes(fault.rimo_type); + }); } - return this.showFcps ? [...rimoMarkers, ...this.fcpMarkers] : rimoMarkers; + const allMarkers = [...rimoMarkers, ...this.missingBuildingMarkers]; + return this.showFcps ? [...allMarkers, ...this.fcpMarkers] : allMarkers; }, faultsForModal() { - if (!this.rawRimoData.length) return []; + if (!this.rawRimoData && !this.faults) return []; return Object.entries(this.faults).map(([hausnummerId, faultData]) => { - const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId); - if (!rimoItem) return null; + let rimoItem = null; + let address = ''; + let rimo_id = ''; + + if (hausnummerId.startsWith('missing-')) { + const typeDef = this.rimoTypeDefs[faultData.rimo_type] || this.rimoTypeDefs.other; + address = `Fehlendes Gebäude (${typeDef.text}) bei ${faultData.lat.toFixed(5)}, ${faultData.lng.toFixed(5)}`; + rimo_id = 'N/A (Fehlend)'; + } else { + rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId); + if (!rimoItem) return null; // Don't show faults for buildings not in the current dataset + address = `${rimoItem.strasse_name} ${rimoItem.hausnummer}, ${rimoItem.plz_name} ${rimoItem.ortschaft_name}`; + rimo_id = rimoItem.rimo_id; + } + return { ...faultData, hausnummerId, - rimo_id: rimoItem.rimo_id, - address: `${rimoItem.strasse_name} ${rimoItem.hausnummer}, ${rimoItem.plz_name} ${rimoItem.ortschaft_name}`, + rimo_id, + address, 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}` }; @@ -87,7 +119,7 @@ Vue.component('PreorderRimoTypeMap', { this.fetchAllMapData(); } else { localStorage.removeItem('rimoMapSelectedCampaign'); - this.mapMarkers = []; this.fcpMarkers = []; this.faults = {}; this.activeFilters = []; + this.mapMarkers = []; this.fcpMarkers = []; this.faults = {}; this.activeFilters = []; this.missingBuildingMarkers = []; } }, showFcps(newVal) { @@ -114,28 +146,45 @@ Vue.component('PreorderRimoTypeMap', { } const storedShowFcps = localStorage.getItem('rimoMapShowFcps'); this.showFcps = storedShowFcps !== null ? JSON.parse(storedShowFcps) : true; + + // Register window functions for popup buttons window.updateEditingFault = this.updateTempFault.bind(this); - window.saveEditingFault = this.saveFaults.bind(this); + window.saveEditingFault = this.saveEditingFault.bind(this); + // --- FIX: Renamed function and window property to avoid reference errors --- + window.markFaultDonePopup = this.markFaultDonePopup.bind(this); }, beforeDestroy() { + // Unregister window functions delete window.updateEditingFault; delete window.saveEditingFault; + // --- FIX: Use matching name for deletion --- + delete window.markFaultDonePopup; }, methods: { 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 = []; this.missingBuildingMarkers = []; try { - await this.fetchFaultData(); + await this.fetchFaultData(); // This will now also populate missingBuildingMarkers await Promise.all([this.fetchRimoData(), this.fetchFCPData()]); } finally { this.isLoading = false; } }, async fetchFaultData() { + this.missingBuildingMarkers = []; // Reset missing markers 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 : {}; + if (res.data.success) { + this.faults = res.data.faults && typeof res.data.faults === 'object' && !Array.isArray(res.data.faults) ? res.data.faults : {}; + + // Process missing building faults into markers + Object.entries(this.faults).forEach(([faultId, faultData]) => { + if (faultId.startsWith('missing-')) { + this.addMissingBuildingMarker(faultId, faultData); + } + }); + } }, async fetchRimoData() { const res = await axios.post(this.fetchUrl, {campaignId: this.selectedCampaign}); @@ -224,24 +273,62 @@ Vue.component('PreorderRimoTypeMap', { const otherText = faultData.other ? `
  • Sonstiges: ${faultData.other}
  • ` : ''; return `
    Gemeldeter Fehler:
    `; }, - _generateBuildingFaultFormHtml(itemGroup, faultData) { + _generateBuildingFaultFormHtml(itemGroup, faultData, isExistingFault) { const formInputs = this.faultReasons.map(reason => { const isChecked = faultData.reasons.includes(reason.value); + const isDisabled = reason.value === 'missing_building'; // Disable 'missing_building' checkbox const otherInput = (reason.value === 'other') ? `` : ''; - return `${otherInput}`; + return `${otherInput}`; }).join(''); - return `
    Fehler melden/bearbeiten:
    ${formInputs}`; + + // --- FIX: Updated logic to only show button if fault exists and is not done --- + const doneButton = isExistingFault && !faultData.done + ? `` + : ''; + + return `
    Fehler melden/bearbeiten:
    ${formInputs}${doneButton}`; }, generateBuildingPopupHtml(itemGroup) { + // --- FIX: Check if the fault actually exists --- + const isExistingFault = !!this.faults[itemGroup.hausnummer_id]; 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); + // --- FIX: Pass the isExistingFault flag --- + const faultFormHtml = this._generateBuildingFaultFormHtml(itemGroup, this.editingFault.data, isExistingFault); + 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}
    `; }, + // New method for "missing building" popups + generateMissingBuildingPopupHtml(faultId) { + const currentFault = this.faults[faultId] || {reasons: [], other: '', done: false}; + const isExistingFault = !!this.faults[faultId]; + this.editingFault = {hausnummerId: faultId, data: JSON.parse(JSON.stringify(currentFault))}; + + const typeDef = this.rimoTypeDefs[currentFault.rimo_type] || this.rimoTypeDefs.other; + const detailsHtml = ` +
    +
    Fehlendes Gebäude
    + Gemeldeter Typ: ${typeDef.text}
    + Koordinaten: ${currentFault.lat.toFixed(6)}, ${currentFault.lng.toFixed(6)}
    + Links: + Karte +
    `; + +// --- FIX: Pass the isExistingFault flag --- + const faultFormHtml = this._generateBuildingFaultFormHtml({}, this.editingFault.data, isExistingFault); // Pass empty itemGroup + 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 user interacts with a "done" fault, mark it as "not done" again if (this.editingFault.data.done) { this.editingFault.data.done = false; this.editingFault.data.done_by = null; this.editingFault.data.done_at = null; } @@ -251,6 +338,8 @@ Vue.component('PreorderRimoTypeMap', { const index = fault.reasons.indexOf(reason); if (value && index === -1) fault.reasons.push(reason); else if (!value && index > -1) fault.reasons.splice(index, 1); + + // Toggle visibility of 'other' textarea if (reason === 'other') { const popup = this.$refs.ttMap?.map?._popup; if (popup?.isOpen()) { @@ -264,54 +353,202 @@ Vue.component('PreorderRimoTypeMap', { } } }, - async saveFaults(hausnummerIdToUpdate) { + // --- FIX: Renamed this method --- + async markFaultDonePopup() { + if (!this.editingFault) return; + const hausnummerId = this.editingFault.hausnummerId; + const faultData = { + ...this.editingFault.data, + done: true, + done_by: window.TT_CONFIG.USER_ID, + done_at: new Date().toISOString() + }; + + this.$set(this.faults, hausnummerId, faultData); + this.editingFault.data = faultData; // Update local state for popup refresh + + await this.saveFaults(hausnummerId, true); // Save and stay in place + }, + // Modified saveFaults to handle "stayInPlace" and "missing" buildings + async saveFaults(hausnummerIdToUpdate, stayInPlace = false) { 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); + + // Store current view to prevent map from moving + const center = this.$refs.ttMap?.map.getCenter(); + const zoom = this.$refs.ttMap?.map.getZoom(); + + 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(); + + // Refetch fault data to be in sync (backend might have changed something) + await this.fetchFaultData(); + + // Update marker icon in place 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 isNot2Connect = rimoItem.rimo_op_state === 'Not2Connect'; - const rimoType = this.getNormalizedRimoType(rimoItem.rimo_type); - const fault = this.faults[hausnummerId]; - const hasFault = fault && !fault.done; + const fault = this.faults[hausnummerId]; + const hasFault = fault && !fault.done; + + if (marker.options.isMissingBuilding) { + // Logic for missing building marker + const rimoType = fault.rimo_type || 'other'; const markerIconDef = this.getMarkerIcon(rimoType); const newIcon = L.divIcon({ - className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`, - html: `
    `, + className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''} marker-missing-building`, + html: `
    `, iconSize: [30, 30], iconAnchor: [15, 30] }); marker.setIcon(newIcon); + + } else { + // Logic for existing building marker + const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId); + if (rimoItem) { + const isNot2Connect = rimoItem.rimo_op_state === 'Not2Connect'; + const rimoType = this.getNormalizedRimoType(rimoItem.rimo_type); + 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] + }); + marker.setIcon(newIcon); + } } } }); } - } else window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.'); - this.editingFault = null; + + // Handle popup state + if (stayInPlace) { + const markerInstance = mapComponent?.markerLayer?.getLayers().find(m => m.tt_hausnummerId == hausnummerId); + + if (markerInstance && mapComponent.map._popup && mapComponent.map._popup.isOpen() && mapComponent.map._popup._source === markerInstance) { + let newPopupContent; + if(markerInstance.options.isMissingBuilding) { + newPopupContent = await this.generateMissingBuildingPopupHtml(hausnummerId); + } else { + const rimoItem = this.rawRimoData.find(item => item.hausnummer_id == hausnummerId); + if(rimoItem) newPopupContent = await this.generateBuildingPopupHtml(rimoItem); + } + + if (newPopupContent) { + markerInstance.setPopupContent(newPopupContent); // Refresh popup content + } else { + mapComponent.map.closePopup(); // Close if we can't refresh + } + } else if (!markerInstance) { + mapComponent.map.closePopup(); // Close if marker somehow disappeared (e.g. filtered out) + } + } else { + this.$refs.ttMap?.map.closePopup(); + } + + // Restore view + if (center && zoom) { + this.$refs.ttMap?.map.setView(center, zoom, { animate: false, noMoveStart: true }); + } + + } else { + window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.'); + } + + if(!stayInPlace) { + 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()}); - await this.saveFaults(hausnummerId); + await this.saveFaults(hausnummerId, false); // Save, don't stay in place (closes modal) }, 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.'); + + // Find marker (works for normal and missing building markers) 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}); setTimeout(() => markerLayer.zoomToShowLayer(markerInstance, () => markerInstance.openPopup()), 1100); }, + // New method to handle right-click context menu + handleMapContextMenu(e) { + if (!this.selectedCampaign) return; // Don't allow if no campaign is selected + e.originalEvent.preventDefault(); + this.missingBuildingData = { lat: e.latlng.lat, lng: e.latlng.lng, rimo_type: null }; + this.showMissingBuildingModal = true; + }, + // New method to save the missing building + async saveMissingBuilding() { + if (!this.missingBuildingData || !this.missingBuildingData.rimo_type) { + window.notify('warning', 'Bitte einen RIMO-Typ auswählen.'); + return; + } + const newFaultId = 'missing-' + Math.random().toString(36).substr(2, 9); + const newFaultData = { + reasons: ['missing_building'], + other: 'Vom Benutzer als fehlend markiert.', + done: false, + created_by: window.TT_CONFIG.USER_ID, + created_at: new Date().toISOString(), + lat: this.missingBuildingData.lat, + lng: this.missingBuildingData.lng, + rimo_type: this.missingBuildingData.rimo_type + }; + + this.$set(this.faults, newFaultId, newFaultData); + this.addMissingBuildingMarker(newFaultId, newFaultData); // Add marker locally + + this.showMissingBuildingModal = false; + this.missingBuildingData = null; + + await this.saveFaults(newFaultId, false); // Save to backend + }, + // New method to add marker for missing building + addMissingBuildingMarker(faultId, faultData) { + const rimoType = faultData.rimo_type || 'other'; + const markerIconDef = this.getMarkerIcon(rimoType); + const hasFault = faultData && !faultData.done; + const newIcon = L.divIcon({ + className: `custom-div-icon marker-missing-building marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''}`, + html: `
    `, + iconSize: [30, 30], + iconAnchor: [15, 30] + }); + const newMarker = { + lat: faultData.lat, + lng: faultData.lng, + hausnummerId: faultId, // Root property for tt-map to find + options: { + icon: newIcon, + isMissingBuilding: true, // Custom flag + asyncPopupContent: () => this.generateMissingBuildingPopupHtml(faultId) + } + }; + + // Avoid duplicates - update existing if found + const existingIndex = this.missingBuildingMarkers.findIndex(m => m.hausnummerId === faultId); + if (existingIndex > -1) { + this.$set(this.missingBuildingMarkers, existingIndex, newMarker); + } else { + this.missingBuildingMarkers.push(newMarker); + } + }, getNormalizedRimoType(type) { const lowerType = (type || '').toLowerCase(); if (lowerType.includes('greenfield')) return 'greenfield'; @@ -336,10 +573,20 @@ Vue.component('PreorderRimoTypeMap', { const color = this.rimoTypeDefs[filterValue]?.color || '#6c757d'; return this.isFilterActive(filterValue) ? {backgroundColor: color, borderColor: color, color: 'white'} : {color: color, backgroundColor: 'white', borderColor: color}; }, + saveEditingFault() { + this.saveFaults(); + }, }, template: `
    - +