Add context menu for missing buildings and update fault handling
This commit is contained in:
+32
-40
@@ -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
|
||||
# Expose port 80 and start Apache in 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ? `<li>Sonstiges: ${faultData.other}</li>` : '';
|
||||
return `<div class="fault-indicator-popup"><strong><i class="fas fa-exclamation-triangle"></i> Gemeldeter Fehler:</strong><ul>${reasonsList}${otherText}</ul></div>`;
|
||||
},
|
||||
_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') ? `<textarea class="fault-other-textarea ${isChecked ? '' : 'hidden'}" oninput="window.updateEditingFault('other_text', this.value)">${faultData.other || ''}</textarea>` : '';
|
||||
return `<label><input type="checkbox" onchange="window.updateEditingFault('${reason.value}', this.checked)" ${isChecked ? 'checked' : ''}> ${reason.text}</label>${otherInput}`;
|
||||
return `<label><input type="checkbox" onchange="window.updateEditingFault('${reason.value}', this.checked)" ${isChecked ? 'checked' : ''} ${isDisabled ? 'disabled' : ''}> ${reason.text}</label>${otherInput}`;
|
||||
}).join('');
|
||||
return `<h6>Fehler melden/bearbeiten:</h6>${formInputs}<button class="btn btn-primary btn-sm mt-2" onclick="window.saveEditingFault()">Fehler Speichern</button>`;
|
||||
|
||||
// --- FIX: Updated logic to only show button if fault exists and is not done ---
|
||||
const doneButton = isExistingFault && !faultData.done
|
||||
? `<button class="btn btn-success btn-sm mt-2 ml-2" onclick="window.markFaultDonePopup()">Als erledigt markieren</button>`
|
||||
: '';
|
||||
|
||||
return `<h6>Fehler melden/bearbeiten:</h6>${formInputs}<button class="btn btn-primary btn-sm mt-2" onclick="window.saveEditingFault()">Änderungen Speichern</button>${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 ? `<div class="alert alert-success"><i class="fas fa-check-circle"></i> Dieser Fehler wurde von <strong>${this.userIdToNameMap.get(String(this.editingFault.data.done_by)) || `User #${this.editingFault.data.done_by}`}</strong> am ${new Date(this.editingFault.data.done_at).toLocaleDateString()} als erledigt markiert.</div>` : 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
|
||||
? `<div class="alert alert-success"><i class="fas fa-check-circle"></i> Dieser Fehler wurde von <strong>${this.userIdToNameMap.get(String(this.editingFault.data.done_by)) || `User #${this.editingFault.data.done_by}`}</strong> am ${new Date(this.editingFault.data.done_at).toLocaleDateString()} als erledigt markiert.</div>`
|
||||
: this._generateBuildingFaultDisplayHtml(this.editingFault.data);
|
||||
|
||||
return `<div class="building-popup-content">${detailsHtml}${faultDisplayHtml}<hr class="my-2"><div class="fault-reporting-form">${faultFormHtml}</div></div>`;
|
||||
},
|
||||
// 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 = `
|
||||
<div class="mb-2">
|
||||
<h5 class="mb-2 mt-1"><i class="fas fa-map-pin mr-2"></i>Fehlendes Gebäude</h5>
|
||||
<strong>Gemeldeter Typ:</strong> ${typeDef.text}<br>
|
||||
<strong>Koordinaten:</strong> ${currentFault.lat.toFixed(6)}, ${currentFault.lng.toFixed(6)}<br>
|
||||
<strong>Links:</strong>
|
||||
<a href="https://www.google.com/maps?q=${currentFault.lat},${currentFault.lng}" target="_blank" class="text-primary"><i class="fas fa-map-marker-alt mr-1"></i>Karte</a>
|
||||
</div>`;
|
||||
|
||||
// --- FIX: Pass the isExistingFault flag ---
|
||||
const faultFormHtml = this._generateBuildingFaultFormHtml({}, this.editingFault.data, isExistingFault); // Pass empty itemGroup
|
||||
const faultDisplayHtml = this.editingFault.data.done
|
||||
? `<div class="alert alert-success"><i class="fas fa-check-circle"></i> Dieser Fehler wurde von <strong>${this.userIdToNameMap.get(String(this.editingFault.data.done_by)) || `User #${this.editingFault.data.done_by}`}</strong> am ${new Date(this.editingFault.data.done_at).toLocaleDateString()} als erledigt markiert.</div>`
|
||||
: this._generateBuildingFaultDisplayHtml(this.editingFault.data);
|
||||
|
||||
return `<div class="building-popup-content">${detailsHtml}${faultDisplayHtml}<hr class="my-2"><div class="fault-reporting-form">${faultFormHtml}</div></div>`; },
|
||||
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: `<div class="rimo-marker ${markerIconDef.class} ${isNot2Connect ? 'marker-not-to-connect' : ''}"><i class="${markerIconDef.icon} rimo-icon"></i></div>`,
|
||||
className: `custom-div-icon marker-${rimoType} ${hasFault ? 'marker-has-fault' : ''} marker-missing-building`,
|
||||
html: `<div class="rimo-marker ${markerIconDef.class}"><i class="${markerIconDef.icon} rimo-icon"></i></div>`,
|
||||
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: `<div class="rimo-marker ${markerIconDef.class} ${isNot2Connect ? 'marker-not-to-connect' : ''}"><i class="${markerIconDef.icon} rimo-icon"></i></div>`,
|
||||
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: `<div class="rimo-marker ${markerIconDef.class}"><i class="${markerIconDef.icon} rimo-icon"></i></div>`,
|
||||
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: `
|
||||
<div id="PreorderRimoTypeMap">
|
||||
<tt-map ref="ttMap" :markers-data="filteredMapMarkers" :loading="isLoading" :config="mapConfig" :show-logo="true">
|
||||
<tt-map ref="ttMap"
|
||||
:markers-data="filteredMapMarkers"
|
||||
:loading="isLoading"
|
||||
:config="mapConfig"
|
||||
:show-logo="true"
|
||||
:contextmenu="true"
|
||||
@contextmenu="handleMapContextMenu"
|
||||
>
|
||||
<template v-slot:tools>
|
||||
<div class="main-filter-container">
|
||||
<div class="map-filter-container">
|
||||
@@ -387,6 +634,12 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
</div>
|
||||
<span class="legend-text">Not2Connect</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon">
|
||||
<div class="rimo-marker marker-residential marker-missing-building" style="width: 24px; height: 24px; border-width: 2px;"><i class="fas fa-home rimo-icon" style="font-size: 12px;"></i></div>
|
||||
</div>
|
||||
<span class="legend-text">Fehlendes Gebäude</span>
|
||||
</div>
|
||||
|
||||
<div class="mobile-only-legend-buttons mt-3" v-if="selectedCampaign">
|
||||
<tt-button :href="window.TT_CONFIG.BASE_PATH + '/Preorder/RimoTypeMapFaultsPDFAction?preordercampaign_id=' + selectedCampaign"
|
||||
@@ -397,6 +650,7 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
</template>
|
||||
</tt-map>
|
||||
|
||||
<!-- Faults List Modal -->
|
||||
<div v-if="showFaultsModal" class="modal fade show" style="display: block; background-color: rgba(0,0,0,0.5);" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
@@ -411,7 +665,7 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="pr-3">
|
||||
<strong :class="{ 'text-muted': fault.done }">{{ fault.address }}</strong>
|
||||
<div class="text-muted mb-1">Rimo ID: {{ fault.rimo_id }}</div>
|
||||
<div class="text-muted mb-1" v-if="!fault.hausnummerId.startsWith('missing-')">Rimo ID: {{ fault.rimo_id }}</div>
|
||||
<div v-if="!fault.done" class="mt-1">
|
||||
<ul class="mb-0 small pl-3 text-danger font-weight-bold">
|
||||
<li v-for="reason in fault.translated_reasons" :key="reason">{{ reason }}</li>
|
||||
@@ -437,6 +691,34 @@ Vue.component('PreorderRimoTypeMap', {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Missing Building Modal -->
|
||||
<div v-if="showMissingBuildingModal" class="modal fade show" style="display: block; background-color: rgba(0,0,0,0.5);" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Fehlendes Gebäude markieren</h5>
|
||||
<button type="button" class="close" @click="showMissingBuildingModal = false; missingBuildingData = null;" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Markieren Sie ein neues, fehlendes Gebäude an der Position:</p>
|
||||
<p v-if="missingBuildingData"><strong>Lat:</strong> {{ missingBuildingData.lat.toFixed(6) }}, <strong>Lng:</strong> {{ missingBuildingData.lng.toFixed(6) }}</p>
|
||||
<tt-select v-if="missingBuildingData"
|
||||
v-model="missingBuildingData.rimo_type"
|
||||
:options="rimoTypeOptionsForMissing"
|
||||
label="RIMO-Typ des fehlenden Gebäudes"
|
||||
:row="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<tt-button text="Abbrechen" @click="showMissingBuildingModal = false; missingBuildingData = null;" additional-class="btn-secondary"/>
|
||||
<tt-button text="Speichern" @click="saveMissingBuilding" additional-class="btn-primary" icon="fas fa-save"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ Vue.component('tt-map', {
|
||||
markersData: { type: Array, default: () => [] },
|
||||
config: { type: Object, default: () => ({}) },
|
||||
loading: { type: Boolean, default: false },
|
||||
showLogo: { type: Boolean, default: false }
|
||||
showLogo: { type: Boolean, default: false },
|
||||
contextmenu: { type: Boolean, default: false }
|
||||
},
|
||||
data: () => ({
|
||||
map: null,
|
||||
@@ -61,6 +62,7 @@ Vue.component('tt-map', {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
if (this.map) {
|
||||
this.map.off('zoomend moveend', this.updateTooltipVisibility);
|
||||
if (this.contextmenu) this.map.off('contextmenu', this.onMapContextMenu);
|
||||
this.map.remove();
|
||||
this.map = null;
|
||||
}
|
||||
@@ -115,6 +117,7 @@ Vue.component('tt-map', {
|
||||
this.map.addLayer(this.markerLayer);
|
||||
this.map.addLayer(this.nonClusteredLayer);
|
||||
this.map.on('zoomend moveend', this.updateTooltipVisibility);
|
||||
if (this.contextmenu) { this.map.on('contextmenu', this.onMapContextMenu); }
|
||||
if (this.showLogo) {
|
||||
const LogoControl = L.Control.extend({ onAdd: map => {
|
||||
const container = L.DomUtil.create('div', 'leaflet-control-logo');
|
||||
@@ -225,6 +228,7 @@ Vue.component('tt-map', {
|
||||
},
|
||||
handleSearchFocus() { this.showSearchResults = true; },
|
||||
handleSearchBlur() { setTimeout(() => { this.showSearchResults = false; }, 200); },
|
||||
onMapContextMenu(e) { e.originalEvent.preventDefault(); this.$emit('contextmenu', e); },
|
||||
},
|
||||
watch: {
|
||||
markersData: { handler() { this.updateMarkers(); }, deep: true },
|
||||
|
||||
Reference in New Issue
Block a user