ProfileAddressDialog.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <template>
  2. <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
  3. <div class="bg-page full-height no-shadow">
  4. <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
  5. <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
  6. <q-space />
  7. <span class="font16 fontbold gradient-diarista">{{ $t('profile.address.title') }}</span>
  8. <q-space />
  9. <div style="width: 32px"></div>
  10. </div>
  11. <q-card-section class="col q-pa-lg">
  12. <div class="text-text">
  13. <div class="gradient-diarista font16 fontbold">{{ $t('profile.address.address_subtitle') }}</div>
  14. <div class="text-grey-7 q-mb-lg font12">{{ $t('profile.address.address_description') }}</div>
  15. <q-card class="q-pa-lg bg-white shadow-card" style="border-radius: 25px;" :flat="false">
  16. <q-input
  17. v-model="search"
  18. outlined
  19. dense
  20. class="q-mb-md bg-white card-border"
  21. input-class="text-text"
  22. :placeholder="$t('profile.address.search_placeholder')"
  23. >
  24. <template #append>
  25. <q-icon name="mdi-magnify" color="grey-7"/>
  26. </template>
  27. </q-input>
  28. <div class="text-text q-mb-xs font14 fontbold">{{ $t('profile.address.cep') }}</div>
  29. <q-input
  30. v-model="form.zip_code"
  31. input-class="text-text"
  32. outlined
  33. dense
  34. mask="#####-###"
  35. unmasked-value
  36. placeholder="00000-000"
  37. class="q-mb-md"
  38. @update:model-value="onCepChange"
  39. >
  40. <template #append>
  41. <q-spinner v-if="loadingCep" size="xs" color="primary" />
  42. </template>
  43. </q-input>
  44. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.address_label') }}</div>
  45. <q-input
  46. v-model="form.address"
  47. outlined
  48. dense
  49. class="q-mb-md"
  50. input-class="text-text"
  51. :placeholder="$t('profile.address.address_placeholder')"
  52. />
  53. <div class="row q-col-gutter-sm q-mb-md">
  54. <div class="col-4">
  55. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.number') }}</div>
  56. <q-input v-model="form.number" outlined dense input-class="text-text" placeholder="0000"/>
  57. </div>
  58. <div class="col-8">
  59. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.complement') }}</div>
  60. <q-input v-model="form.complement" outlined dense input-class="text-text" :placeholder="$t('profile.address.complement_placeholder')"/>
  61. </div>
  62. </div>
  63. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.district_label') }}</div>
  64. <q-input v-model="form.district" outlined dense class="q-mb-md" input-class="text-text"/>
  65. <div class="row q-col-gutter-sm q-mb-lg">
  66. <div class="col-8">
  67. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.city_label') }}</div>
  68. <q-input :model-value="form.city?.name" readonly outlined dense input-class="text-text"/>
  69. </div>
  70. <div class="col-4">
  71. <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.state_label') }}</div>
  72. <q-input :model-value="form.state?.name" readonly outlined dense input-class="text-text"/>
  73. </div>
  74. </div>
  75. <div v-if="missingCoords" class="q-mb-md">
  76. <q-banner rounded dense class="bg-orange-1 text-warning q-mb-sm">
  77. <template #avatar>
  78. <q-icon name="mdi-map-marker-off" color="orange-7" />
  79. </template>
  80. {{ $t('profile.address.missing_coords') }}
  81. </q-banner>
  82. <q-btn
  83. unelevated
  84. rounded
  85. no-caps
  86. outline
  87. color="primary"
  88. icon="mdi-map-marker"
  89. class="full-width q-mb-md"
  90. :label="$t('profile.address.update_on_map')"
  91. :loading="geocodingCep"
  92. @click="openMapDialog"
  93. />
  94. </div>
  95. <q-btn
  96. unelevated
  97. rounded
  98. no-caps
  99. color="primary"
  100. class="full-width q-py-md"
  101. padding="8px 16px"
  102. :label="$t('profile.address.update_address')"
  103. :loading="saving"
  104. :disable="!hasUpdatedFields"
  105. @click="save"
  106. />
  107. </q-card>
  108. </div>
  109. <div class="q-pb-xl"></div>
  110. </q-card-section>
  111. </div>
  112. </q-dialog>
  113. </template>
  114. <script setup>
  115. import { ref, computed, onMounted } from 'vue';
  116. import { useDialogPluginComponent, useQuasar } from 'quasar';
  117. import { searchAddressByCEP, updateAddress, createAddress, getAddresses } from 'src/api/address';
  118. import { userStore } from 'src/stores/user';
  119. import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
  120. import { useGeocodingApi } from 'src/composables/useGeocodingApi';
  121. import LocationMapDialog from 'src/components/shared/LocationMapDialog.vue';
  122. defineEmits([...useDialogPluginComponent.emits]);
  123. const { dialogRef, onDialogOK } = useDialogPluginComponent();
  124. const $q = useQuasar();
  125. const user = userStore();
  126. const providerId = user.user.provider.id;
  127. const { geocodeFullAddress } = useGeocodingApi();
  128. const {
  129. form,
  130. hasUpdatedFields,
  131. getUpdatedFields,
  132. setUpdateFormAsOriginal,
  133. } = useFormUpdateTracker({
  134. zip_code: '',
  135. address: '',
  136. number: '',
  137. complement: '',
  138. district: '',
  139. city_id: null,
  140. state_id: null,
  141. city: null,
  142. state: null,
  143. source: 'provider',
  144. source_id: providerId,
  145. address_type: 'home',
  146. latitude: null,
  147. longitude: null,
  148. });
  149. const search = ref('');
  150. const loadingCep = ref(false);
  151. const saving = ref(false);
  152. const addressId = ref(null);
  153. const geocodingCep = ref(false);
  154. const missingCoords = computed(() =>
  155. addressId.value !== null && form.latitude == null && form.longitude == null
  156. );
  157. const onCepChange = async (val) => {
  158. if (val?.length === 8) {
  159. loadingCep.value = true;
  160. try {
  161. const data = await searchAddressByCEP(val);
  162. if (data) {
  163. form.address = data.address;
  164. form.district = data.district;
  165. form.city_id = data.city_id;
  166. form.state_id = data.state_id;
  167. form.city = data.city;
  168. form.state = data.state;
  169. form.latitude = null;
  170. form.longitude = null;
  171. } else {
  172. $q.notify({ type: 'negative', message: 'CEP não encontrado' });
  173. }
  174. } finally {
  175. loadingCep.value = false;
  176. }
  177. }
  178. };
  179. const openMapDialog = async () => {
  180. let initialLat = null;
  181. let initialLng = null;
  182. const hasAddress = form.address || form.zip_code;
  183. if (hasAddress) {
  184. geocodingCep.value = true;
  185. try {
  186. const geo = await geocodeFullAddress({
  187. address: form.address,
  188. number: form.number,
  189. district: form.district,
  190. zip_code: form.zip_code,
  191. city: form.city?.name,
  192. state: form.state?.name,
  193. });
  194. if (geo) {
  195. initialLat = geo.lat;
  196. initialLng = geo.lng;
  197. }
  198. } catch (err) {
  199. console.error('geocodeFullAddress falhou, usando coordenada default:', err);
  200. } finally {
  201. geocodingCep.value = false;
  202. }
  203. }
  204. const dialogProps = initialLat !== null
  205. ? { initialLat, initialLng }
  206. : {};
  207. $q.dialog({
  208. component: LocationMapDialog,
  209. componentProps: dialogProps,
  210. }).onOk(async (geoData) => {
  211. form.latitude = geoData.lat;
  212. form.longitude = geoData.lng;
  213. form.address = geoData.address || form.address;
  214. form.number = geoData.number || form.number;
  215. form.district = geoData.district || form.district;
  216. if (geoData.zip_code) {
  217. form.zip_code = geoData.zip_code.replace(/\D/g, '');
  218. try {
  219. const addressData = await searchAddressByCEP(form.zip_code);
  220. if (addressData) {
  221. form.city_id = addressData.city_id;
  222. form.state_id = addressData.state_id;
  223. form.city = addressData.city;
  224. form.state = addressData.state;
  225. }
  226. } catch {
  227. // mantém cidade/estado atual se lookup falhar
  228. }
  229. }
  230. });
  231. };
  232. const save = async () => {
  233. saving.value = true;
  234. try {
  235. let response;
  236. if (addressId.value) {
  237. response = await updateAddress(getUpdatedFields.value, addressId.value);
  238. } else {
  239. response = await createAddress({ ...form });
  240. }
  241. if (response) {
  242. setUpdateFormAsOriginal();
  243. onDialogOK(response);
  244. }
  245. } catch (error) {
  246. console.error('Erro ao salvar endereço:', error);
  247. } finally {
  248. saving.value = false;
  249. }
  250. };
  251. onMounted(async () => {
  252. const providersAddresses = await getAddresses('provider', providerId);
  253. if (providersAddresses && providersAddresses.length > 0) {
  254. const current = providersAddresses[0];
  255. addressId.value = current.id;
  256. const initialData = {
  257. zip_code: current.zip_code || '',
  258. address: current.address || '',
  259. number: current.number || '',
  260. complement: current.complement || '',
  261. district: current.district || '',
  262. city_id: current.city_id || null,
  263. state_id: current.state_id || null,
  264. city: current.city || null,
  265. state: current.state || null,
  266. source: 'provider',
  267. source_id: providerId,
  268. address_type: current.address_type || 'home',
  269. latitude: current.latitude ?? null,
  270. longitude: current.longitude ?? null,
  271. };
  272. Object.assign(form, initialData);
  273. setUpdateFormAsOriginal();
  274. }
  275. });
  276. </script>
  277. <style scoped>
  278. </style>