| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- <template>
- <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
- <div class="bg-page full-height no-shadow">
- <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
- <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
- <q-space />
- <span class="text-subtitle1 text-weight-bold text-primary">
- {{ isEditing ? $t('profile.address.edit_address') : $t('profile.address.add_new_address') }}
- </span>
- <q-space />
- <div style="width: 32px"></div>
- </div>
- <q-card-section class="col">
- <div class="q-px-md q-pt-lg text-text">
- <q-card class="q-pa-lg bg-white shadow-card" style="border-radius: 25px;" :flat="false">
- <div class="text-weight-bold text-text q-mb-xs">{{ $t('profile.address.cep') }}</div>
- <q-input
- v-model="form.zip_code"
- input-class="text-text"
- outlined
- dense
- mask="#####-###"
- unmasked-value
- placeholder="00000-000"
- class="q-mb-md"
- @update:model-value="onCepChange"
- >
- <template #append>
- <q-spinner v-if="loadingCep" size="xs" color="primary" />
- </template>
- </q-input>
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.address_label') }}</div>
- <q-input
- v-model="form.address"
- outlined
- dense
- class="q-mb-md"
- input-class="text-text"
- :placeholder="$t('profile.address.address_placeholder')"
- />
- <div class="row q-col-gutter-sm q-mb-md">
- <div class="col-4">
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.number') }}</div>
- <q-input v-model="form.number" outlined dense input-class="text-text" placeholder="0000" />
- </div>
- <div class="col-8">
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.complement') }}</div>
- <q-input
- v-model="form.complement"
- outlined
- dense
- input-class="text-text"
- :placeholder="$t('profile.address.complement_placeholder')"
- />
- </div>
- </div>
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.district_label') }}</div>
- <q-input v-model="form.district" outlined dense class="q-mb-md" input-class="text-text" />
- <div class="row q-col-gutter-sm q-mb-lg">
- <div class="col-8">
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.city_label') }}</div>
- <q-input :model-value="form.city?.name" readonly outlined dense input-class="text-text" />
- </div>
- <div class="col-4">
- <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.address.state_label') }}</div>
- <q-input :model-value="form.state?.name" readonly outlined dense input-class="text-text" />
- </div>
- </div>
- <div class="q-mb-lg">
- <div class="row q-gutter-sm">
- <q-chip
- v-for="type in addressTypes"
- :key="type.value"
- :selected="form.address_type === type.value"
- clickable
- color="primary"
- :outline="form.address_type !== type.value"
- text-color="surface"
- :icon="type.icon"
- :icon-selected="type.icon"
- @click="form.address_type = type.value"
- >
- {{ $t(type.label) }}
- </q-chip>
- </div>
- </div>
- <div v-if="missingCoords" class="q-mb-md">
- <q-banner rounded dense class="bg-orange-1 text-orange-9 q-mb-sm">
- <template #avatar>
- <q-icon name="mdi-map-marker-off" color="orange-7" />
- </template>
- {{ $t('profile.address.missing_coords') }}
- </q-banner>
- <q-btn
- unelevated
- rounded
- no-caps
- outline
- color="primary"
- icon="mdi-map-marker"
- class="full-width"
- :label="$t('profile.address.update_on_map')"
- :loading="geocodingCep"
- @click="openMapDialog"
- />
- </div>
- <q-btn
- unelevated
- rounded
- no-caps
- color="primary"
- class="full-width q-py-md text-weight-bold"
- padding="8px 16px"
- style="font-size: 1.1rem;"
- :label="$t('common.actions.save')"
- :loading="saving"
- :disable="!hasUpdatedFields"
- @click="save"
- />
- </q-card>
- </div>
- <div class="q-pb-xl"></div>
- </q-card-section>
- </div>
- </q-dialog>
- </template>
- <script setup>
- import { ref, computed, onMounted } from 'vue';
- import { useDialogPluginComponent, useQuasar } from 'quasar';
- import { searchAddressByCEP, updateAddress, createAddress } from 'src/api/address';
- import { userStore } from 'src/stores/user';
- import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
- import { useGeocodingApi } from 'src/composables/useGeocodingApi';
- import { useI18n } from 'vue-i18n';
- import LocationMapDialog from 'src/components/shared/LocationMapDialog.vue';
- const props = defineProps({
- isEditing: {
- type: Boolean,
- default: false,
- },
- addressData: {
- type: Object,
- default: null,
- },
- });
- defineEmits([...useDialogPluginComponent.emits]);
- const { dialogRef, onDialogOK } = useDialogPluginComponent();
- const $q = useQuasar();
- const { t } = useI18n();
- const user = userStore();
- const clientId = user.user.client.id;
- const { geocodeFullAddress } = useGeocodingApi();
- const initialFormData = {
- zip_code: '',
- address: '',
- number: '',
- complement: '',
- district: '',
- city_id: null,
- state_id: null,
- city: null,
- state: null,
- source: 'client',
- source_id: clientId,
- address_type: 'home',
- latitude: null,
- longitude: null,
- };
- const { form, hasUpdatedFields, getUpdatedFields, setUpdateFormAsOriginal } =
- useFormUpdateTracker(initialFormData);
- const loadingCep = ref(false);
- const saving = ref(false);
- const addressId = ref(null);
- const geocodingCep = ref(false);
- const missingCoords = computed(() =>
- props.isEditing && form.latitude == null && form.longitude == null
- );
- const addressTypes = [
- { value: 'home', label: 'profile.address.type.home', icon: 'mdi-home-outline' },
- { value: 'commercial', label: 'profile.address.type.commercial', icon: 'mdi-briefcase-variant-outline' },
- { value: 'other', label: 'profile.address.type.other', icon: 'mdi-map-marker-outline' },
- ];
- const onCepChange = async (val) => {
- if (val?.length === 8) {
- loadingCep.value = true;
- try {
- const data = await searchAddressByCEP(val);
- if (data) {
- form.address = data.address;
- form.district = data.district;
- form.city_id = data.city_id;
- form.state_id = data.state_id;
- form.city = data.city;
- form.state = data.state;
- form.latitude = null;
- form.longitude = null;
- } else {
- $q.notify({ type: 'negative', message: t('profile.address.cep_not_found') });
- }
- } finally {
- loadingCep.value = false;
- }
- }
- };
- const openMapDialog = async () => {
- let initialLat = null;
- let initialLng = null;
- const hasAddress = form.address || form.zip_code;
- if (hasAddress) {
- geocodingCep.value = true;
- try {
- const geo = await geocodeFullAddress({
- address: form.address,
- number: form.number,
- district: form.district,
- zip_code: form.zip_code,
- city: form.city?.name,
- state: form.state?.name,
- });
- if (geo) {
- initialLat = geo.lat;
- initialLng = geo.lng;
- }
- } catch {
- // fallback to default
- } finally {
- geocodingCep.value = false;
- }
- }
- const dialogProps = initialLat !== null
- ? { initialLat, initialLng }
- : {};
- $q.dialog({
- component: LocationMapDialog,
- componentProps: dialogProps,
- }).onOk(async (geoData) => {
- form.latitude = geoData.lat;
- form.longitude = geoData.lng;
- form.address = geoData.address || form.address;
- form.number = geoData.number || form.number;
- form.district = geoData.district || form.district;
- if (geoData.zip_code) {
- form.zip_code = geoData.zip_code.replace(/\D/g, '');
- try {
- const addressData = await searchAddressByCEP(form.zip_code);
- if (addressData) {
- form.city_id = addressData.city_id;
- form.state_id = addressData.state_id;
- form.city = addressData.city;
- form.state = addressData.state;
- }
- } catch {
- // mantém cidade/estado atual se lookup falhar
- }
- }
- });
- };
- const save = async () => {
- saving.value = true;
- try {
- let response;
- if (props.isEditing && addressId.value) {
- response = await updateAddress(getUpdatedFields.value, addressId.value);
- } else {
- response = await createAddress({ ...form });
- }
- if (response) {
- setUpdateFormAsOriginal();
- onDialogOK(response);
- }
- } catch (error) {
- console.error('Erro ao salvar endereço:', error);
- $q.notify({ type: 'negative', message: t('profile.address.error_saving') });
- } finally {
- saving.value = false;
- }
- };
- onMounted(() => {
- if (props.isEditing && props.addressData) {
- addressId.value = props.addressData.id;
- const initialData = {
- zip_code: props.addressData.zip_code || '',
- address: props.addressData.address || '',
- number: props.addressData.number || '',
- complement: props.addressData.complement || '',
- district: props.addressData.district || '',
- city_id: props.addressData.city_id || null,
- state_id: props.addressData.state_id || null,
- city: props.addressData.city || null,
- state: props.addressData.state || null,
- source: 'client',
- source_id: clientId,
- address_type: props.addressData.address_type || 'home',
- latitude: props.addressData.latitude ?? null,
- longitude: props.addressData.longitude ?? null,
- };
- Object.assign(form, initialData);
- setUpdateFormAsOriginal();
- }
- });
- </script>
- <style scoped>
- </style>
|