LoginStep3Panel.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <template>
  2. <q-card-section class="no-padding">
  3. <div>
  4. <div class="text-text">
  5. <span class="font14 fontbold">
  6. {{ $t("auth.full_name") }}
  7. </span>
  8. </div>
  9. <q-input
  10. v-model="form.name"
  11. bg-color="surface"
  12. class="q-mt-sm q-mb-md"
  13. hide-bottom-space
  14. input-class="text-text"
  15. lazy-rules
  16. no-error-icon
  17. outlined
  18. rounded
  19. :error="!!serverErrors.name"
  20. :error-message="serverErrors.name"
  21. :placeholder="$t('auth.full_name')"
  22. :rules="[inputRules.required]"
  23. @update:model-value="clearServerError('name')"
  24. />
  25. </div>
  26. <div>
  27. <div class="text-text">
  28. <span class="font14 fontbold">
  29. {{ $t("common.terms.cpf") }}
  30. </span>
  31. </div>
  32. <q-input
  33. v-model="form.document"
  34. bg-color="surface"
  35. class="q-mt-sm q-mb-md"
  36. hide-bottom-space
  37. input-class="text-text"
  38. lazy-rules
  39. mask="###.###.###-##"
  40. no-error-icon
  41. outlined
  42. placeholder="000.000.000-00"
  43. rounded
  44. type="tel"
  45. unmasked-value
  46. :error="!!serverErrors.document"
  47. :error-message="serverErrors.document"
  48. :rules="[inputRules.required, inputRules.cpf]"
  49. @update:model-value="clearServerError('document')"
  50. />
  51. </div>
  52. <div>
  53. <div class="text-text">
  54. <span class="font14 fontbold">
  55. {{ $t("common.terms.phone") }}
  56. </span>
  57. </div>
  58. <q-input
  59. v-model="form.phone"
  60. bg-color="surface"
  61. class="q-mt-sm q-mb-md"
  62. hide-bottom-space
  63. input-class="text-text"
  64. lazy-rules
  65. mask="(##) #####-####"
  66. no-error-icon
  67. outlined
  68. rounded
  69. :error="!!serverErrors.phone"
  70. :error-message="serverErrors.phone"
  71. :placeholder="$t('common.terms.phone')"
  72. :rules="[inputRules.required, inputRules.phone]"
  73. @update:model-value="clearServerError('phone')"
  74. />
  75. </div>
  76. <div>
  77. <div class="text-text">
  78. <span class="font14 fontbold">
  79. {{ $t("common.terms.cep") }}
  80. </span>
  81. </div>
  82. <q-input
  83. v-model="form.zip_code"
  84. bg-color="surface"
  85. class="q-mt-sm q-mb-md"
  86. hide-bottom-space
  87. input-class="text-text"
  88. lazy-rules
  89. mask="#####-###"
  90. no-error-icon
  91. outlined
  92. placeholder="00000-000"
  93. rounded
  94. :error="!!serverErrors.zip_code"
  95. :error-message="serverErrors.zip_code"
  96. :loading="loadingCep"
  97. :rules="[inputRules.required, inputRules.cep]"
  98. @update:model-value="(clearServerError('zip_code'), onCepChange($event))"
  99. />
  100. </div>
  101. <q-btn
  102. class="full-width q-mb-md"
  103. color="primary-button"
  104. padding="8px 12px"
  105. rounded
  106. :label="$t('auth.use_location')"
  107. :loading="loadingLocation"
  108. @click="useLocation"
  109. />
  110. <div>
  111. <div class="text-text">
  112. <span class="font14 fontbold">
  113. {{ $t("common.terms.address") }}
  114. </span>
  115. </div>
  116. <q-input
  117. v-model="form.address"
  118. bg-color="surface"
  119. class="q-mt-sm q-mb-md"
  120. hide-bottom-space
  121. input-class="text-text"
  122. lazy-rules
  123. no-error-icon
  124. outlined
  125. readonly
  126. rounded
  127. :error="!!serverErrors.address"
  128. :error-message="serverErrors.address"
  129. :placeholder="`${$t('common.terms.address')}...`"
  130. :rules="[inputRules.required]"
  131. />
  132. </div>
  133. <div class="row q-col-gutter-sm">
  134. <div class="col-4">
  135. <div class="text-text">
  136. <span class="font14 fontbold">
  137. {{ $t("common.terms.address_number") }}
  138. </span>
  139. </div>
  140. <q-input
  141. v-model="form.number"
  142. bg-color="surface"
  143. class="q-mt-sm q-mb-md"
  144. hide-bottom-space
  145. input-class="text-text"
  146. lazy-rules
  147. no-error-icon
  148. outlined
  149. placeholder="0000"
  150. rounded
  151. :error="!!serverErrors.number"
  152. :error-message="serverErrors.number"
  153. :rules="[inputRules.required]"
  154. @update:model-value="clearServerError('number')"
  155. />
  156. </div>
  157. <div class="col-8">
  158. <div class="text-text">
  159. <span class="font14 fontbold">
  160. {{ $t("common.terms.district") }}
  161. </span>
  162. </div>
  163. <q-input
  164. v-model="form.district"
  165. bg-color="surface"
  166. class="q-mt-sm q-mb-md"
  167. hide-bottom-space
  168. input-class="text-text"
  169. lazy-rules
  170. no-error-icon
  171. outlined
  172. readonly
  173. rounded
  174. :error="!!serverErrors.district"
  175. :error-message="serverErrors.district"
  176. :placeholder="`${$t('common.terms.district')}...`"
  177. :rules="[inputRules.required]"
  178. />
  179. </div>
  180. </div>
  181. <div class="row q-col-gutter-sm">
  182. <div class="col-8">
  183. <div class="text-text">
  184. <span class="font14 fontbold">
  185. {{ $t("common.terms.city") }}
  186. </span>
  187. </div>
  188. <q-input
  189. v-model="form.city"
  190. bg-color="surface"
  191. class="q-mt-sm q-mb-md"
  192. hide-bottom-space
  193. input-class="text-text"
  194. lazy-rules
  195. no-error-icon
  196. outlined
  197. readonly
  198. rounded
  199. :error="!!serverErrors.city"
  200. :error-message="serverErrors.city"
  201. :rules="[inputRules.required]"
  202. />
  203. </div>
  204. <div class="col-4">
  205. <div class="text-text">
  206. <span class="font14 fontbold">
  207. {{ $t("common.terms.state") }}
  208. </span>
  209. </div>
  210. <q-input
  211. v-model="form.state"
  212. bg-color="surface"
  213. class="q-mt-sm q-mb-md"
  214. hide-bottom-space
  215. input-class="text-text"
  216. lazy-rules
  217. no-error-icon
  218. outlined
  219. readonly
  220. rounded
  221. :error="!!serverErrors.state"
  222. :error-message="serverErrors.state"
  223. :rules="[inputRules.required]"
  224. />
  225. </div>
  226. </div>
  227. <div>
  228. <div class="text-text">
  229. <span class="font14 fontbold">
  230. {{ $t("common.terms.complement") }}
  231. </span>
  232. </div>
  233. <q-input
  234. v-model="form.complement"
  235. bg-color="surface"
  236. class="q-mt-sm q-mb-md"
  237. hide-bottom-space
  238. input-class="text-text"
  239. lazy-rules
  240. no-error-icon
  241. outlined
  242. rounded
  243. :error="!!serverErrors.complement"
  244. :error-message="serverErrors.complement"
  245. :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
  246. :rules="[inputRules.required]"
  247. @update:model-value="clearServerError('complement')"
  248. />
  249. </div>
  250. <div>
  251. <div class="text-text">
  252. <span class="font14 fontbold">
  253. {{ $t("auth.address_nickname") }}
  254. </span>
  255. </div>
  256. <q-input
  257. v-model="form.nickname"
  258. bg-color="surface"
  259. class="q-mt-sm q-mb-md"
  260. hide-bottom-space
  261. input-class="text-text"
  262. lazy-rules
  263. no-error-icon
  264. outlined
  265. rounded
  266. :placeholder="`${$t('common.ui.misc.example')}: Casa`"
  267. />
  268. </div>
  269. <div>
  270. <div class="row q-gutter-sm q-mt-xs">
  271. <q-chip
  272. v-for="type in addressTypes"
  273. :key="type.value"
  274. :icon="type.icon"
  275. :icon-selected="type.icon"
  276. :outline="form.address_type !== type.value"
  277. :selected="form.address_type === type.value"
  278. clickable
  279. color="primary"
  280. text-color="surface"
  281. @click="form.address_type = type.value"
  282. >
  283. {{ $t(type.label) }}
  284. </q-chip>
  285. </div>
  286. </div>
  287. </q-card-section>
  288. </template>
  289. <script setup>
  290. import axios from "axios";
  291. import { inject, ref } from "vue";
  292. import { useGeolocation } from "src/composables/useGeolocation";
  293. import { useI18n } from "vue-i18n";
  294. import { useInputRules } from "src/composables/useInputRules";
  295. import { useQuasar } from "quasar";
  296. import LocationMapDialog from "src/components/shared/LocationMapDialog.vue";
  297. const form = defineModel({
  298. required: true,
  299. type: Object,
  300. });
  301. defineProps({
  302. serverErrors: {
  303. type: Object,
  304. default: () => ({}),
  305. },
  306. });
  307. const clearServerError = inject("clearServerError", () => {});
  308. const { requestPermission, getCurrentPosition } = useGeolocation();
  309. const { inputRules } = useInputRules();
  310. const { t } = useI18n();
  311. const $q = useQuasar();
  312. const loadingCep = ref(false);
  313. const loadingLocation = ref(false);
  314. const addressTypes = [
  315. {
  316. icon: "mdi-home-outline",
  317. label: "auth.address_type_home",
  318. value: "home",
  319. },
  320. {
  321. icon: "mdi-briefcase-variant-outline",
  322. label: "auth.address_type_commercial",
  323. value: "commercial",
  324. },
  325. {
  326. icon: "mdi-map-marker-outline",
  327. label: "auth.address_type_other",
  328. value: "other",
  329. },
  330. ];
  331. const fetchCep = async (rawCep) => {
  332. const cleaned = rawCep.replace(/\D/g, "");
  333. if (cleaned.length !== 8) {
  334. return;
  335. }
  336. loadingCep.value = true;
  337. try {
  338. const { data } = await axios.get(
  339. `https://viacep.com.br/ws/${cleaned}/json/`,
  340. );
  341. if (!data.erro) {
  342. form.value.address = data.logradouro ?? "";
  343. form.value.district = data.bairro ?? "";
  344. form.value.city = data.localidade ?? "";
  345. form.value.state = data.uf ?? "";
  346. } else {
  347. form.value.address = "";
  348. form.value.district = "";
  349. form.value.city = "";
  350. form.value.state = "";
  351. }
  352. } catch {
  353. form.value.address = "";
  354. form.value.district = "";
  355. form.value.city = "";
  356. form.value.state = "";
  357. } finally {
  358. loadingCep.value = false;
  359. }
  360. };
  361. const onCepChange = (val) => {
  362. const cleaned = val ? val.replace(/\D/g, "") : "";
  363. form.value.latitude = null;
  364. form.value.longitude = null;
  365. if (cleaned.length === 8) {
  366. fetchCep(val);
  367. } else {
  368. form.value.address = "";
  369. form.value.district = "";
  370. form.value.city = "";
  371. form.value.state = "";
  372. }
  373. };
  374. const showCurrentLocationOnMap = ({ lat, lng }) => {
  375. $q.dialog({
  376. component: LocationMapDialog,
  377. componentProps: {
  378. initialLat: lat,
  379. initialLng: lng,
  380. },
  381. }).onOk((geoData) => {
  382. form.value.zip_code = geoData.zip_code ?? "";
  383. form.value.address = geoData.address ?? "";
  384. form.value.number = geoData.number ?? "";
  385. form.value.district = geoData.district ?? "";
  386. form.value.city = geoData.city ?? "";
  387. form.value.state = geoData.state ?? "";
  388. form.value.latitude = geoData.lat;
  389. form.value.longitude = geoData.lng;
  390. });
  391. };
  392. const useLocation = async () => {
  393. loadingLocation.value = true;
  394. try {
  395. const granted = await requestPermission();
  396. if (!granted) {
  397. $q.notify({
  398. message: t("auth.location_permission_denied"),
  399. type: "warning",
  400. });
  401. return;
  402. }
  403. const position = await getCurrentPosition();
  404. showCurrentLocationOnMap(position);
  405. } catch (err) {
  406. const isPermissionError =
  407. err?.code === 1 || err?.message?.toLowerCase().includes("denied");
  408. $q.notify({
  409. message: isPermissionError
  410. ? t("auth.location_permission_denied")
  411. : t("auth.location_error"),
  412. type: "warning",
  413. });
  414. } finally {
  415. loadingLocation.value = false;
  416. }
  417. };
  418. </script>