enhanced preorder map
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class GeocodingController extends mfBaseController {
|
||||||
|
|
||||||
|
protected function init() {
|
||||||
|
$this->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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
showFaultsModal: false,
|
showFaultsModal: false,
|
||||||
logoControlAdded: false,
|
logoControlAdded: false,
|
||||||
userIdToNameMap: new Map(),
|
userIdToNameMap: new Map(),
|
||||||
|
editingFault: null, // Temporary state for the fault being edited
|
||||||
faultReasons: [
|
faultReasons: [
|
||||||
{ value: 'building_type', text: 'Gebäudetyp ist falsch' },
|
{ value: 'building_type', text: 'Gebäudetyp ist falsch' },
|
||||||
{ value: 'home_count', text: 'Anzahl der Wohneinheiten ist falsch' },
|
{ value: 'home_count', text: 'Anzahl der Wohneinheiten ist falsch' },
|
||||||
@@ -112,12 +113,12 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
}
|
}
|
||||||
const storedShowFcps = localStorage.getItem('rimoMapShowFcps');
|
const storedShowFcps = localStorage.getItem('rimoMapShowFcps');
|
||||||
this.showFcps = storedShowFcps !== null ? JSON.parse(storedShowFcps) : true;
|
this.showFcps = storedShowFcps !== null ? JSON.parse(storedShowFcps) : true;
|
||||||
window.updateMapFault = this.handleFaultUpdate.bind(this);
|
window.updateEditingFault = this.updateTempFault.bind(this);
|
||||||
window.saveMapFaults = this.saveFaults.bind(this);
|
window.saveEditingFault = this.saveFaults.bind(this);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
delete window.updateMapFault;
|
delete window.updateEditingFault;
|
||||||
delete window.saveMapFaults;
|
delete window.saveEditingFault;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addLogoToMap() {
|
addLogoToMap() {
|
||||||
@@ -263,7 +264,7 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
const rimoType = this.getNormalizedRimoType(group.rimo_type);
|
const rimoType = this.getNormalizedRimoType(group.rimo_type);
|
||||||
const markerIcon = this.getMarkerIcon(rimoType);
|
const markerIcon = this.getMarkerIcon(rimoType);
|
||||||
const fault = this.faults[group.hausnummer_id];
|
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 = '';
|
let tooltipInnerClass = '';
|
||||||
if (rimoType !== 'greenfield' && group.wohneinheit_count > 0 && group.wohneinheit_count === group.preorder_count)
|
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 formInputs = this.faultReasons.map(reason => {
|
||||||
const isChecked = faultData.reasons.includes(reason.value);
|
const isChecked = faultData.reasons.includes(reason.value);
|
||||||
const otherInput = (reason.value === 'other') ?
|
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>
|
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}
|
${reason.text}
|
||||||
</label>
|
</label>
|
||||||
${otherInput}`;
|
${otherInput}`;
|
||||||
@@ -342,15 +343,20 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
|
|
||||||
return `<h6>Fehler melden/bearbeiten:</h6>
|
return `<h6>Fehler melden/bearbeiten:</h6>
|
||||||
${formInputs}
|
${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) {
|
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 detailsHtml = this._generateBuildingDetailsHtml(itemGroup);
|
||||||
const faultFormHtml = this._generateBuildingFaultFormHtml(itemGroup, faultData);
|
const faultFormHtml = this._generateBuildingFaultFormHtml(itemGroup, this.editingFault.data);
|
||||||
const faultDisplayHtml = faultData.done ?
|
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(faultData.done_by)) || `User #${faultData.done_by}`}</strong> am ${new Date(faultData.done_at).toLocaleDateString()} als erledigt markiert.</div>` :
|
`<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(faultData);
|
this._generateBuildingFaultDisplayHtml(this.editingFault.data);
|
||||||
|
|
||||||
return `<div class="building-popup-content">
|
return `<div class="building-popup-content">
|
||||||
${detailsHtml}
|
${detailsHtml}
|
||||||
@@ -359,36 +365,49 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
<div class="fault-reporting-form">${faultFormHtml}</div>
|
<div class="fault-reporting-form">${faultFormHtml}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
handleFaultUpdate(hausnummerId, reason, value, from_mark_as_done = false) {
|
updateTempFault(reason, value) {
|
||||||
if (!this.faults[hausnummerId])
|
if (!this.editingFault) return;
|
||||||
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 });
|
|
||||||
|
|
||||||
|
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') {
|
if (reason === 'other_text') {
|
||||||
fault.other = value;
|
fault.other = value;
|
||||||
} else if (reason) {
|
} else { // It's a checkbox change
|
||||||
const index = fault.reasons.indexOf(reason);
|
const index = fault.reasons.indexOf(reason);
|
||||||
if (value && index === -1) fault.reasons.push(reason);
|
if (value && index === -1) { // checked
|
||||||
else if (!value && index > -1) fault.reasons.splice(index, 1);
|
fault.reasons.push(reason);
|
||||||
|
} else if (!value && index > -1) { // unchecked
|
||||||
const popup = this.$refs.ttMap?.map?._popup;
|
fault.reasons.splice(index, 1);
|
||||||
if (popup?.isOpen()) {
|
|
||||||
const otherTextarea = popup.getElement().querySelector('.fault-other-textarea');
|
|
||||||
if (otherTextarea) otherTextarea.classList.toggle('hidden', !fault.reasons.includes('other'));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const markerInstance = this.$refs.ttMap.markerLayer.getLayers().find(m => m.tt_hausnummerId == hausnummerId);
|
if (reason === 'other') {
|
||||||
if (markerInstance?.getElement) {
|
const popup = this.$refs.ttMap?.map?._popup;
|
||||||
const hasOpenFault = fault && !fault.done && (fault.reasons.length > 0 || fault.other);
|
if (popup?.isOpen()) {
|
||||||
markerInstance.getElement().classList.toggle('marker-has-fault', hasOpenFault);
|
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() {
|
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`, {
|
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Preorder/RimoTypeMapSaveFaults`, {
|
||||||
campaignId: this.selectedCampaign,
|
campaignId: this.selectedCampaign,
|
||||||
faults: this.faults
|
faults: this.faults
|
||||||
@@ -396,9 +415,11 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
window.notify('success', 'Fehlerbericht gespeichert.');
|
window.notify('success', 'Fehlerbericht gespeichert.');
|
||||||
this.$refs.ttMap?.map.closePopup();
|
this.$refs.ttMap?.map.closePopup();
|
||||||
|
this.mapMarkers = this.processData(this.rawRimoData);
|
||||||
} else {
|
} else {
|
||||||
window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.');
|
window.notify('error', 'Fehlerbericht konnte nicht gespeichert werden.');
|
||||||
}
|
}
|
||||||
|
this.editingFault = null;
|
||||||
},
|
},
|
||||||
async markFaultAsDone(hausnummerId) {
|
async markFaultAsDone(hausnummerId) {
|
||||||
if (!hausnummerId || !this.faults[hausnummerId]) return;
|
if (!hausnummerId || !this.faults[hausnummerId]) return;
|
||||||
@@ -411,7 +432,6 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.saveFaults();
|
await this.saveFaults();
|
||||||
this.handleFaultUpdate(hausnummerId, null, null, true);
|
|
||||||
},
|
},
|
||||||
zoomToFaultMarker(hausnummerId) {
|
zoomToFaultMarker(hausnummerId) {
|
||||||
const map = this.$refs.ttMap?.map;
|
const map = this.$refs.ttMap?.map;
|
||||||
@@ -475,28 +495,28 @@ Vue.component('PreorderRimoTypeMap', {
|
|||||||
></tt-select>
|
></tt-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-filter-container" v-if="selectedCampaign">
|
<div class="map-filter-container" v-if="selectedCampaign">
|
||||||
<h6 class="mr-2 font-weight-bold align-self-center">Filter:</h6>
|
<h6 class="mr-2 font-weight-bold align-self-center">Filter:</h6>
|
||||||
<button v-for="filter in filterOptions"
|
<button v-for="filter in filterOptions"
|
||||||
:key="filter.value"
|
:key="filter.value"
|
||||||
@click="toggleFilter(filter.value)"
|
@click="toggleFilter(filter.value)"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
:style="getFilterButtonStyle(filter.value)"
|
:style="getFilterButtonStyle(filter.value)"
|
||||||
:title="filter.text">
|
:title="filter.text">
|
||||||
<i :class="filter.icon"></i>
|
<i :class="filter.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="filter-separator"></div>
|
<div class="filter-separator"></div>
|
||||||
<button @click="showOnlyFaults = !showOnlyFaults"
|
<button @click="showOnlyFaults = !showOnlyFaults"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
:class="showOnlyFaults ? 'btn-danger' : 'btn-outline-danger'"
|
:class="showOnlyFaults ? 'btn-danger' : 'btn-outline-danger'"
|
||||||
title="Nur Gebäude mit Fehlern anzeigen">
|
title="Nur Gebäude mit Fehlern anzeigen">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleShowFcps"
|
<button @click="toggleShowFcps"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
:class="showFcps ? 'btn-info' : 'btn-outline-info'"
|
:class="showFcps ? 'btn-info' : 'btn-outline-info'"
|
||||||
title="FCPs anzeigen/ausblenden">
|
title="FCPs anzeigen/ausblenden">
|
||||||
<i class="fas fa-broadcast-tower"></i>
|
<i class="fas fa-broadcast-tower"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:legend>
|
<template v-slot:legend>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
.tt-map-top-controls {
|
.tt-map-top-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 55px; /* Moved to avoid overlapping with zoom controls */
|
left: 10px;
|
||||||
z-index: 401;
|
z-index: 401;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
/* Container for built-in map buttons (zoom, layer toggle) */
|
/* Container for built-in map buttons (zoom, layer toggle) */
|
||||||
.tt-map-builtin-controls {
|
.tt-map-builtin-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 60px; /* Pushed down to make space for the search bar */
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 401;
|
z-index: 401;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -74,4 +74,142 @@
|
|||||||
.popup-loader {
|
.popup-loader {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,12 @@ Vue.component('tt-map', {
|
|||||||
internalLoading: true,
|
internalLoading: true,
|
||||||
scriptsLoaded: false,
|
scriptsLoaded: false,
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
|
searchQuery: '',
|
||||||
|
searchResults: [],
|
||||||
|
isSearchLoading: false,
|
||||||
|
showSearchResults: false,
|
||||||
|
searchDebounce: null,
|
||||||
|
selectedMarker: null, // To hold the temporary marker
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -248,6 +254,80 @@ Vue.component('tt-map', {
|
|||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.invalidateSize();
|
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: `<div class="custom-map-marker"><i class="fas fa-map-marker-alt"></i></div>`,
|
||||||
|
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(`<b>${location.text}</b>`)
|
||||||
|
.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: {
|
watch: {
|
||||||
@@ -273,6 +353,47 @@ Vue.component('tt-map', {
|
|||||||
</div>
|
</div>
|
||||||
<div ref="mapContainer" class="tt-map-container" :style="{ visibility: internalLoading ? 'hidden' : 'visible' }"></div>
|
<div ref="mapContainer" class="tt-map-container" :style="{ visibility: internalLoading ? 'hidden' : 'visible' }"></div>
|
||||||
|
|
||||||
|
<div class="tt-map-search-wrapper">
|
||||||
|
<div class="tt-map-search-input-container">
|
||||||
|
<i class="fas fa-search search-icon"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="tt-map-search-input"
|
||||||
|
placeholder="Adresse suchen..."
|
||||||
|
v-model="searchQuery"
|
||||||
|
@input="searchLocations"
|
||||||
|
@focus="handleSearchFocus"
|
||||||
|
@blur="handleSearchBlur"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button v-if="searchQuery" @click="clearSearch" class="clear-icon">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<transition name="fade">
|
||||||
|
<ul v-if="showSearchResults" class="tt-map-search-results">
|
||||||
|
<li v-if="isSearchLoading" class="result-item-info">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> Suche läuft...
|
||||||
|
</li>
|
||||||
|
<li v-else-if="searchQuery.length < 3" class="result-item-info">
|
||||||
|
Bitte mind. 3 Zeichen eingeben
|
||||||
|
</li>
|
||||||
|
<li v-else-if="searchResults.length === 0" class="result-item-info">
|
||||||
|
Keine Ergebnisse gefunden
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="result in searchResults"
|
||||||
|
:key="result.value"
|
||||||
|
class="result-item"
|
||||||
|
@mousedown="selectLocation(result)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
<span>{{ result.text }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tt-map-top-controls">
|
<div class="tt-map-top-controls">
|
||||||
<slot name="tools"></slot>
|
<slot name="tools"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user