enhanced preorder map

This commit is contained in:
2025-09-18 12:55:26 +02:00
parent b69bfc3afe
commit f05ff28394
4 changed files with 384 additions and 57 deletions
@@ -21,6 +21,7 @@ Vue.component('PreorderRimoTypeMap', {
showFaultsModal: false,
logoControlAdded: false,
userIdToNameMap: new Map(),
editingFault: null, // Temporary state for the fault being edited
faultReasons: [
{ value: 'building_type', text: 'Gebäudetyp ist falsch' },
{ value: 'home_count', text: 'Anzahl der Wohneinheiten ist falsch' },
@@ -112,12 +113,12 @@ Vue.component('PreorderRimoTypeMap', {
}
const storedShowFcps = localStorage.getItem('rimoMapShowFcps');
this.showFcps = storedShowFcps !== null ? JSON.parse(storedShowFcps) : true;
window.updateMapFault = this.handleFaultUpdate.bind(this);
window.saveMapFaults = this.saveFaults.bind(this);
window.updateEditingFault = this.updateTempFault.bind(this);
window.saveEditingFault = this.saveFaults.bind(this);
},
beforeDestroy() {
delete window.updateMapFault;
delete window.saveMapFaults;
delete window.updateEditingFault;
delete window.saveEditingFault;
},
methods: {
addLogoToMap() {
@@ -263,7 +264,7 @@ Vue.component('PreorderRimoTypeMap', {
const rimoType = this.getNormalizedRimoType(group.rimo_type);
const markerIcon = this.getMarkerIcon(rimoType);
const fault = this.faults[group.hausnummer_id];
const hasFault = fault && !fault.done;
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)
@@ -330,11 +331,11 @@ Vue.component('PreorderRimoTypeMap', {
const formInputs = this.faultReasons.map(reason => {
const isChecked = faultData.reasons.includes(reason.value);
const otherInput = (reason.value === 'other') ?
`<textarea class="fault-other-textarea ${isChecked ? '' : 'hidden'}" oninput="window.updateMapFault(${itemGroup.hausnummer_id}, 'other_text', this.value, false)">${faultData.other || ''}</textarea>` :
`<textarea class="fault-other-textarea ${isChecked ? '' : 'hidden'}" oninput="window.updateEditingFault('other_text', this.value)">${faultData.other || ''}</textarea>` :
'';
return `<label>
<input type="checkbox" onchange="window.updateMapFault(${itemGroup.hausnummer_id}, '${reason.value}', this.checked, false)" ${isChecked ? 'checked' : ''}>
<input type="checkbox" onchange="window.updateEditingFault('${reason.value}', this.checked)" ${isChecked ? 'checked' : ''}>
${reason.text}
</label>
${otherInput}`;
@@ -342,15 +343,20 @@ Vue.component('PreorderRimoTypeMap', {
return `<h6>Fehler melden/bearbeiten:</h6>
${formInputs}
<button class="btn btn-primary btn-sm mt-2" onclick="window.saveMapFaults()">Fehler Speichern</button>`;
<button class="btn btn-primary btn-sm mt-2" onclick="window.saveEditingFault()">Fehler Speichern</button>`;
},
generateBuildingPopupHtml(itemGroup) {
const faultData = this.faults[itemGroup.hausnummer_id] || { reasons: [], other: '' };
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, faultData);
const faultDisplayHtml = faultData.done ?
`<div class="alert alert-success"><i class="fas fa-check-circle"></i> Dieser Fehler wurde von <strong>${this.userIdToNameMap.get(String(faultData.done_by)) || `User #${faultData.done_by}`}</strong> am ${new Date(faultData.done_at).toLocaleDateString()} als erledigt markiert.</div>` :
this._generateBuildingFaultDisplayHtml(faultData);
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);
return `<div class="building-popup-content">
${detailsHtml}
@@ -359,36 +365,49 @@ Vue.component('PreorderRimoTypeMap', {
<div class="fault-reporting-form">${faultFormHtml}</div>
</div>`;
},
handleFaultUpdate(hausnummerId, reason, value, from_mark_as_done = false) {
if (!this.faults[hausnummerId])
this.$set(this.faults, hausnummerId, { reasons: [], other: '', done: false });
else if (this.faults[hausnummerId].done && !from_mark_as_done)
this.$set(this.faults, hausnummerId, { reasons: [], other: '', done: false });
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;
}
const fault = this.faults[hausnummerId];
const fault = this.editingFault.data;
if (reason === 'other_text') {
fault.other = value;
} else if (reason) {
} else { // It's a checkbox change
const index = fault.reasons.indexOf(reason);
if (value && index === -1) fault.reasons.push(reason);
else if (!value && index > -1) fault.reasons.splice(index, 1);
const popup = this.$refs.ttMap?.map?._popup;
if (popup?.isOpen()) {
const otherTextarea = popup.getElement().querySelector('.fault-other-textarea');
if (otherTextarea) otherTextarea.classList.toggle('hidden', !fault.reasons.includes('other'));
if (value && index === -1) { // checked
fault.reasons.push(reason);
} else if (!value && index > -1) { // unchecked
fault.reasons.splice(index, 1);
}
}
const markerInstance = this.$refs.ttMap.markerLayer.getLayers().find(m => m.tt_hausnummerId == hausnummerId);
if (markerInstance?.getElement) {
const hasOpenFault = fault && !fault.done && (fault.reasons.length > 0 || fault.other);
markerInstance.getElement().classList.toggle('marker-has-fault', hasOpenFault);
if (reason === 'other') {
const popup = this.$refs.ttMap?.map?._popup;
if (popup?.isOpen()) {
const otherTextarea = popup.getElement().querySelector('.fault-other-textarea');
if (otherTextarea) {
const hasOther = fault.reasons.includes('other');
otherTextarea.classList.toggle('hidden', !hasOther);
if (!hasOther) {
otherTextarea.value = '';
fault.other = '';
}
}
}
}
}
},
async saveFaults() {
if (!this.editingFault) return;
const { hausnummerId, data } = this.editingFault;
this.$set(this.faults, hausnummerId, data);
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapSaveFaults`, {
campaignId: this.selectedCampaign,
faults: this.faults
@@ -396,9 +415,11 @@ Vue.component('PreorderRimoTypeMap', {
if (response.data.success) {
window.notify('success', 'Fehlerbericht gespeichert.');
this.$refs.ttMap?.map.closePopup();
this.mapMarkers = this.processData(this.rawRimoData);
} else {
window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.');
}
this.editingFault = null;
},
async markFaultAsDone(hausnummerId) {
if (!hausnummerId || !this.faults[hausnummerId]) return;
@@ -411,7 +432,6 @@ Vue.component('PreorderRimoTypeMap', {
});
await this.saveFaults();
this.handleFaultUpdate(hausnummerId, null, null, true);
},
zoomToFaultMarker(hausnummerId) {
const map = this.$refs.ttMap?.map;
@@ -475,28 +495,28 @@ Vue.component('PreorderRimoTypeMap', {
></tt-select>
</div>
<div class="map-filter-container" v-if="selectedCampaign">
<h6 class="mr-2 font-weight-bold align-self-center">Filter:</h6>
<button v-for="filter in filterOptions"
:key="filter.value"
@click="toggleFilter(filter.value)"
class="btn btn-sm"
:style="getFilterButtonStyle(filter.value)"
:title="filter.text">
<i :class="filter.icon"></i>
</button>
<div class="filter-separator"></div>
<button @click="showOnlyFaults = !showOnlyFaults"
class="btn btn-sm"
:class="showOnlyFaults ? 'btn-danger' : 'btn-outline-danger'"
title="Nur Gebäude mit Fehlern anzeigen">
<i class="fas fa-exclamation-triangle"></i>
</button>
<button @click="toggleShowFcps"
class="btn btn-sm"
:class="showFcps ? 'btn-info' : 'btn-outline-info'"
title="FCPs anzeigen/ausblenden">
<i class="fas fa-broadcast-tower"></i>
</button>
<h6 class="mr-2 font-weight-bold align-self-center">Filter:</h6>
<button v-for="filter in filterOptions"
:key="filter.value"
@click="toggleFilter(filter.value)"
class="btn btn-sm"
:style="getFilterButtonStyle(filter.value)"
:title="filter.text">
<i :class="filter.icon"></i>
</button>
<div class="filter-separator"></div>
<button @click="showOnlyFaults = !showOnlyFaults"
class="btn btn-sm"
:class="showOnlyFaults ? 'btn-danger' : 'btn-outline-danger'"
title="Nur Gebäude mit Fehlern anzeigen">
<i class="fas fa-exclamation-triangle"></i>
</button>
<button @click="toggleShowFcps"
class="btn btn-sm"
:class="showFcps ? 'btn-info' : 'btn-outline-info'"
title="FCPs anzeigen/ausblenden">
<i class="fas fa-broadcast-tower"></i>
</button>
</div>
</template>
<template v-slot:legend>