|
|
@@ -0,0 +1,206 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" persistent maximized @show="initMap">
|
|
|
+ <div class="location-map-dialog">
|
|
|
+ <div ref="mapRef" class="location-map-container" />
|
|
|
+
|
|
|
+ <q-btn
|
|
|
+ flat
|
|
|
+ round
|
|
|
+ color="white"
|
|
|
+ icon="mdi-arrow-left"
|
|
|
+ class="location-map-back-btn"
|
|
|
+ @click="handleCancel"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="location-map-bottom-card">
|
|
|
+ <p class="location-map-address-label">{{ $t('common.terms.address') }}</p>
|
|
|
+ <q-input
|
|
|
+ :model-value="currentAddress"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ readonly
|
|
|
+ class="location-map-address-input q-mb-md"
|
|
|
+ bg-color="white"
|
|
|
+ input-class="text-text"
|
|
|
+ :loading="reversing"
|
|
|
+ :placeholder="reversing ? '' : $t('auth.geocoding_failed_short')"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ color="primary-button"
|
|
|
+ :label="$t('auth.confirm_location')"
|
|
|
+ rounded
|
|
|
+ padding="14px 16px"
|
|
|
+ class="full-width"
|
|
|
+ :loading="reversing"
|
|
|
+ :disable="!currentGeoData"
|
|
|
+ @click="handleConfirm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onUnmounted } from 'vue';
|
|
|
+import { useDialogPluginComponent, useQuasar } from 'quasar';
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
+import { GoogleMap } from '@capacitor/google-maps';
|
|
|
+import { useGeocodingApi } from 'src/composables/useGeocodingApi';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ initialLat: {
|
|
|
+ type: Number,
|
|
|
+ default: -15.7801,
|
|
|
+ },
|
|
|
+ initialLng: {
|
|
|
+ type: Number,
|
|
|
+ default: -47.9292,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
+
|
|
|
+const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
|
+const $q = useQuasar();
|
|
|
+const { t } = useI18n();
|
|
|
+const { reverseGeocode } = useGeocodingApi();
|
|
|
+
|
|
|
+const mapRef = ref(null);
|
|
|
+const reversing = ref(false);
|
|
|
+const currentAddress = ref('');
|
|
|
+const currentGeoData = ref(null);
|
|
|
+
|
|
|
+let googleMap = null;
|
|
|
+const markerId = ref(null);
|
|
|
+
|
|
|
+const handleCancel = async () => {
|
|
|
+ await destroyMap();
|
|
|
+ onDialogCancel();
|
|
|
+};
|
|
|
+
|
|
|
+const handleConfirm = async () => {
|
|
|
+ if (!currentGeoData.value) return;
|
|
|
+ await destroyMap();
|
|
|
+ onDialogOK(currentGeoData.value);
|
|
|
+};
|
|
|
+
|
|
|
+const destroyMap = async () => {
|
|
|
+ if (googleMap) {
|
|
|
+ await googleMap.destroy();
|
|
|
+ googleMap = null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const updateMarkerPosition = async (lat, lng) => {
|
|
|
+ reversing.value = true;
|
|
|
+ try {
|
|
|
+ const geoData = await reverseGeocode(lat, lng);
|
|
|
+ if (geoData) {
|
|
|
+ currentGeoData.value = { ...geoData, lat, lng };
|
|
|
+ const parts = [geoData.address, geoData.number, geoData.district, geoData.city, geoData.state].filter(Boolean);
|
|
|
+ currentAddress.value = parts.join(', ');
|
|
|
+ } else {
|
|
|
+ currentGeoData.value = { lat, lng, address: '', number: '', district: '', city: '', state: '', zip_code: '' };
|
|
|
+ currentAddress.value = '';
|
|
|
+ $q.notify({ type: 'warning', message: t('auth.geocoding_failed') });
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ currentGeoData.value = { lat, lng, address: '', number: '', district: '', city: '', state: '', zip_code: '' };
|
|
|
+ currentAddress.value = '';
|
|
|
+ $q.notify({ type: 'warning', message: t('auth.geocoding_failed') });
|
|
|
+ } finally {
|
|
|
+ reversing.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const moveMarkerTo = async (lat, lng) => {
|
|
|
+ if (markerId.value) {
|
|
|
+ await googleMap.removeMarker(markerId.value);
|
|
|
+ }
|
|
|
+ markerId.value = await googleMap.addMarker({
|
|
|
+ coordinate: { lat, lng },
|
|
|
+ draggable: true,
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const initMap = async () => {
|
|
|
+ googleMap = await GoogleMap.create({
|
|
|
+ id: 'location-map-dialog',
|
|
|
+ element: mapRef.value,
|
|
|
+ apiKey: process.env.GOOGLE_MAPS_API_KEY,
|
|
|
+ config: {
|
|
|
+ center: { lat: props.initialLat, lng: props.initialLng },
|
|
|
+ zoom: 17,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ markerId.value = await googleMap.addMarker({
|
|
|
+ coordinate: { lat: props.initialLat, lng: props.initialLng },
|
|
|
+ draggable: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ await updateMarkerPosition(props.initialLat, props.initialLng);
|
|
|
+
|
|
|
+ await googleMap.setOnMarkerDragEndListener(async (event) => {
|
|
|
+ const { latitude, longitude } = event;
|
|
|
+ await updateMarkerPosition(latitude, longitude);
|
|
|
+ });
|
|
|
+
|
|
|
+ await googleMap.setOnMapClickListener(async (event) => {
|
|
|
+ const { latitude, longitude } = event;
|
|
|
+ await moveMarkerTo(latitude, longitude);
|
|
|
+ await updateMarkerPosition(latitude, longitude);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+onUnmounted(async () => {
|
|
|
+ await destroyMap();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.location-map-dialog {
|
|
|
+ position: relative;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100dvh;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #e5e3df;
|
|
|
+}
|
|
|
+
|
|
|
+.location-map-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.location-map-back-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: calc(16px + env(safe-area-inset-top));
|
|
|
+ left: 16px;
|
|
|
+ z-index: 10;
|
|
|
+ background: rgba(0, 0, 0, 0.4);
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.location-map-bottom-card {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 10;
|
|
|
+ background: white;
|
|
|
+ border-radius: 24px 24px 0 0;
|
|
|
+ padding: 20px 20px calc(20px + env(safe-area-inset-bottom));
|
|
|
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.location-map-address-label {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--q-text);
|
|
|
+ margin: 0 0 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.location-map-address-input {
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+</style>
|