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 ``;
},
- _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 ` ${reason.text} ${otherInput}`;
+ return ` ${reason.text} ${otherInput}`;
}).join('');
- return `Fehler melden/bearbeiten: ${formInputs}Fehler Speichern `;
+
+ // --- FIX: Updated logic to only show button if fault exists and is not done ---
+ const doneButton = isExistingFault && !faultData.done
+ ? `Als erledigt markieren `
+ : '';
+
+ return `Fehler melden/bearbeiten: ${formInputs}Änderungen Speichern ${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 ``;
},
+ // 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 ``; },
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: `
-
+
@@ -387,6 +634,12 @@ Vue.component('PreorderRimoTypeMap', {
Not2Connect
+