diff --git a/application/Geocoding/GeocodingController.php b/application/Geocoding/GeocodingController.php
new file mode 100644
index 000000000..a434901c1
--- /dev/null
+++ b/application/Geocoding/GeocodingController.php
@@ -0,0 +1,48 @@
+needlogin = true;
+ $me = new User();
+ $me->loadMe();
+ $this->me = $me;
+ }
+
+ protected function autocompleteAction() {
+ $search = urlencode($this->request->q);
+ $url = TT_GEOCODING_API_URL . "?address={$search}&key=" . TT_GEOCODING_API_SECRET . "®ion=at&language=de&components=country:AT";
+
+ if (!$response = @file_get_contents($url)) {
+ self::returnJson([]);
+ return;
+ }
+
+ $data = json_decode($response, true);
+ var_dump($data); // Debugging line, can be removed in production
+ if ($data['status'] !== 'OK' || empty($data['results'])) {
+ self::returnJson([]);
+ return;
+ }
+
+ $out = [];
+ foreach ($data['results'] as $entry) {
+ $hasHouseNumber = false;
+ foreach ($entry['address_components'] as $component) {
+ if (in_array('street_number', $component['types'])) {
+ $hasHouseNumber = true;
+ break;
+ }
+ }
+
+ $text = $entry['formatted_address'];
+ $value = "{$entry['geometry']['location']['lat']},{$entry['geometry']['location']['lng']}" . ($hasHouseNumber ? '' : ', area');
+
+ $out[$text] = ['value' => $value, 'text' => $text];
+ }
+
+ self::returnJson(array_values($out));
+ }
+
+
+}
\ No newline at end of file
diff --git a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
index b697ab26c..a628484a5 100644
--- a/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
+++ b/public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
@@ -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') ?
- `` :
+ `` :
'';
return `
-
+
${reason.text}
${otherInput}`;
@@ -342,15 +343,20 @@ Vue.component('PreorderRimoTypeMap', {
return `
Fehler melden/bearbeiten:
${formInputs}
- Fehler Speichern `;
+ Fehler Speichern `;
},
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 ?
- ` Dieser Fehler wurde von ${this.userIdToNameMap.get(String(faultData.done_by)) || `User #${faultData.done_by}`} am ${new Date(faultData.done_at).toLocaleDateString()} als erledigt markiert.
` :
- this._generateBuildingFaultDisplayHtml(faultData);
+ 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 ``;
},
- 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', {
>
-
Filter:
-
-
-
-
-
-
-
-
-
-
+
Filter:
+
+
+
+
+
+
+
+
+
+
diff --git a/public/plugins/vue/tt-components/css/tt-map.css b/public/plugins/vue/tt-components/css/tt-map.css
index 61b021f8c..5992ac184 100644
--- a/public/plugins/vue/tt-components/css/tt-map.css
+++ b/public/plugins/vue/tt-components/css/tt-map.css
@@ -29,7 +29,7 @@
.tt-map-top-controls {
position: absolute;
top: 10px;
- left: 55px; /* Moved to avoid overlapping with zoom controls */
+ left: 10px;
z-index: 401;
}
@@ -44,7 +44,7 @@
/* Container for built-in map buttons (zoom, layer toggle) */
.tt-map-builtin-controls {
position: absolute;
- top: 10px;
+ top: 60px; /* Pushed down to make space for the search bar */
right: 10px;
z-index: 401;
display: flex;
@@ -74,4 +74,142 @@
.popup-loader {
text-align: center;
padding: 10px;
+}
+
+/* --- NEW MAP SEARCH STYLES --- */
+.tt-map-search-wrapper {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 402;
+ width: 320px;
+ max-width: calc(100% - 20px);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.tt-map-search-input-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ 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);
+ padding: 0.25rem;
+ height: 42px;
+}
+
+.tt-map-search-input {
+ width: 100%;
+ height: 100%;
+ padding: 0 30px 0 32px;
+ border: none;
+ background-color: transparent;
+ font-size: 14px;
+ outline: none;
+}
+
+.tt-map-search-input-container .search-icon {
+ position: absolute;
+ left: 12px;
+ color: #888;
+ pointer-events: none;
+}
+
+.tt-map-search-input-container .clear-icon {
+ position: absolute;
+ right: 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #aaa;
+ font-size: 16px;
+ padding: 5px;
+}
+
+.tt-map-search-input-container .clear-icon:hover {
+ color: #333;
+}
+
+.tt-map-search-results {
+ list-style: none;
+ padding: 0;
+ margin: 8px 0 0 0;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid #ddd;
+}
+
+.tt-map-search-results .result-item,
+.tt-map-search-results .result-item-info {
+ padding: 10px 15px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ transition: background-color 0.15s;
+ font-size: 14px;
+}
+
+.tt-map-search-results .result-item:hover {
+ background-color: #f5f5f5;
+}
+
+.tt-map-search-results .result-item:not(:last-child) {
+ border-bottom: 1px solid #eee;
+}
+
+.tt-map-search-results .result-item i {
+ color: #6c757d;
+}
+
+.tt-map-search-results .result-item-info {
+ color: #6c757d;
+ cursor: default;
+}
+
+/* Fade animation for the results list */
+.fade-enter-active, .fade-leave-active {
+ transition: opacity 0.2s ease;
+}
+
+.fade-enter, .fade-leave-to {
+ opacity: 0;
+}
+
+/* --- NEW CUSTOM MARKER STYLE --- */
+.custom-map-marker {
+ background-color: rgba(0, 123, 255, 0.9);
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: 2px solid white;
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.9);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ animation: pulse 1.5s infinite;
+}
+
+.custom-map-marker i {
+ color: white;
+ font-size: 12px;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(0.9);
+ box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.7);
+ }
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 10px rgba(0, 123, 255, 0);
+ }
+ 100% {
+ transform: scale(0.9);
+ box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
+ }
}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-map.js b/public/plugins/vue/tt-components/tt-map.js
index 1253679bd..4036de12e 100644
--- a/public/plugins/vue/tt-components/tt-map.js
+++ b/public/plugins/vue/tt-components/tt-map.js
@@ -27,6 +27,12 @@ Vue.component('tt-map', {
internalLoading: true,
scriptsLoaded: false,
showSettings: false,
+ searchQuery: '',
+ searchResults: [],
+ isSearchLoading: false,
+ showSearchResults: false,
+ searchDebounce: null,
+ selectedMarker: null, // To hold the temporary marker
};
},
computed: {
@@ -248,6 +254,80 @@ Vue.component('tt-map', {
if (this.map) {
this.map.invalidateSize();
}
+ },
+ searchLocations() {
+ if (this.searchDebounce) clearTimeout(this.searchDebounce);
+ if (this.searchQuery.length < 3) {
+ this.searchResults = [];
+ this.showSearchResults = true;
+ return;
+ }
+ this.isSearchLoading = true;
+ this.searchDebounce = setTimeout(async () => {
+ try {
+ const response = await axios.get('/Geocoding/autocomplete', {
+ params: { q: this.searchQuery }
+ });
+ this.searchResults = response.data;
+ } catch (error) {
+ console.error("Geocoding search failed:", error);
+ this.searchResults = [];
+ } finally {
+ this.isSearchLoading = false;
+ }
+ }, 350);
+ },
+ selectLocation(location) {
+ this.searchQuery = location.text;
+ this.showSearchResults = false;
+ const [lat, lng] = location.value.split(',').map(Number);
+
+ if (this.map && !isNaN(lat) && !isNaN(lng)) {
+ this.map.flyTo([lat, lng], 17, { duration: 0.5 });
+
+ if (this.selectedMarker) {
+ this.selectedMarker.remove();
+ }
+
+ const customIcon = L.divIcon({
+ html: `
`,
+ className: '', // Important to keep it empty
+ iconSize: [24, 24],
+ iconAnchor: [12, 12],
+ popupAnchor: [0, -14]
+ });
+
+ this.selectedMarker = L.marker([lat, lng], { icon: customIcon }).addTo(this.map)
+ .bindPopup(`${location.text} `)
+ .openPopup();
+
+ this.searchQuery = '';
+ this.searchResults = [];
+
+ setTimeout(() => {
+ if (this.selectedMarker) {
+ this.selectedMarker.remove();
+ this.selectedMarker = null;
+ }
+ }, 10000);
+ }
+ },
+ clearSearch() {
+ this.searchQuery = '';
+ this.searchResults = [];
+ this.showSearchResults = false;
+ if (this.selectedMarker) {
+ this.selectedMarker.remove();
+ this.selectedMarker = null;
+ }
+ },
+ handleSearchFocus() {
+ this.showSearchResults = true;
+ },
+ handleSearchBlur() {
+ setTimeout(() => {
+ this.showSearchResults = false;
+ }, 200);
}
},
watch: {
@@ -273,6 +353,47 @@ Vue.component('tt-map', {
+
+
+
+
+
+
+
+
+
+
+
+ Suche läuft...
+
+
+ Bitte mind. 3 Zeichen eingeben
+
+
+ Keine Ergebnisse gefunden
+
+
+
+ {{ result.text }}
+
+
+
+
+