LoginPage.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
  2. <template>
  3. <q-page class="login-page bg-surface-dark">
  4. <Transition mode="out-in" name="fade-slide">
  5. <div
  6. v-if="!clicked"
  7. key="splash"
  8. class="splash-screen"
  9. @click="clicked = true"
  10. >
  11. <img class="splash-layer splash-layer--bg" :src="BackgroundLogin" />
  12. <img class="splash-layer splash-layer--photo" :src="FotoDiarista" />
  13. <img class="splash-layer splash-layer--logo" :src="LogoLogin" />
  14. </div>
  15. <div v-else key="flow" class="flow-screen">
  16. <div
  17. v-if="!showSubStep"
  18. class="flow-header row items-center"
  19. >
  20. <q-btn
  21. v-if="steps >= 4"
  22. class="flow-back-btn"
  23. color="primary"
  24. dense
  25. flat
  26. icon="mdi-arrow-left"
  27. round
  28. @click="goBack"
  29. />
  30. </div>
  31. <div v-if="!showSubStep" class="flow-logo q-my-xl">
  32. <q-img style="max-width: 180px" :src="LogoDiariaCampos" />
  33. </div>
  34. <q-form
  35. v-if="!registrationCompleted"
  36. ref="loginForm"
  37. autocapitalize="off"
  38. autocomplete="off"
  39. autocorrect="off"
  40. class="flow-form"
  41. spellcheck="false"
  42. @submit="onSubmit"
  43. >
  44. <div
  45. class="flow-content"
  46. :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }"
  47. >
  48. <LoginStep1Panel
  49. v-if="steps === 1"
  50. v-model:email="email"
  51. v-model:phone="phone"
  52. />
  53. <LoginStep2Panel v-else-if="steps === 2" v-model:code="code" />
  54. <LoginStep3Panel
  55. v-else-if="steps === 3"
  56. v-model="stepThreeForm"
  57. />
  58. <LoginStep4Panel
  59. v-else-if="steps === 4"
  60. v-model="stepFourForm"
  61. @update:show-sub-step="showSubStep = $event"
  62. />
  63. <LoginStep5Panel
  64. v-else-if="steps === 5"
  65. v-model="stepFiveForm"
  66. />
  67. <LoginStep6Panel v-else-if="steps === 6" v-model="stepSixForm" />
  68. </div>
  69. <div v-if="!showSubStep" class="flow-footer">
  70. <q-btn
  71. class="full-width"
  72. color="primary-button"
  73. padding="8px 16px"
  74. rounded
  75. type="submit"
  76. :label="actionLabel"
  77. :loading="submitting"
  78. >
  79. <template #loading>
  80. <q-spinner />
  81. </template>
  82. </q-btn>
  83. </div>
  84. </q-form>
  85. <div v-else class="registration-complete">
  86. <q-icon
  87. class="q-mb-md"
  88. color="positive"
  89. name="mdi-check-circle-outline"
  90. size="52px"
  91. />
  92. <div class="registration-complete__title text-text fontbold">
  93. {{ t("provider.login.registration_complete.title") }}
  94. </div>
  95. <div class="registration-complete__message text-grey-7 font12">
  96. {{ t("provider.login.registration_complete.message") }}
  97. </div>
  98. </div>
  99. </div>
  100. </Transition>
  101. </q-page>
  102. </template>
  103. <script setup>
  104. import { computed, onMounted, ref } from "vue";
  105. import { createUserAndProvider, sendCode, validateCode } from "src/api/user";
  106. import { useAuth } from "src/composables/useAuth";
  107. import { useI18n } from "vue-i18n";
  108. import { useQuasar } from "quasar";
  109. import { useRouter } from "vue-router";
  110. import BackgroundLogin from "src/assets/background-login.svg";
  111. import FotoDiarista from "src/assets/foto_diarista_login.svg";
  112. import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
  113. import LogoLogin from "src/assets/logo_diaria_login.svg";
  114. import LoginStep1Panel from "src/components/login/LoginStep1Panel.vue";
  115. import LoginStep2Panel from "src/components/login/LoginStep2Panel.vue";
  116. import LoginStep3Panel from "src/components/login/LoginStep3Panel.vue";
  117. import LoginStep4Panel from "src/components/login/LoginStep4Panel.vue";
  118. import LoginStep5Panel from "src/components/login/LoginStep5Panel.vue";
  119. import LoginStep6Panel from "src/components/login/LoginStep6Panel.vue";
  120. const $q = useQuasar();
  121. const router = useRouter();
  122. const { setAuthDataFromPayload } = useAuth();
  123. const { t } = useI18n();
  124. const clicked = ref(false);
  125. const code = ref("");
  126. const email = ref("");
  127. const isLogin = ref(false);
  128. const loginForm = ref(null);
  129. const phone = ref("");
  130. const registrationCompleted = ref(false);
  131. const showSubStep = ref(false);
  132. const stepFiveForm = ref({
  133. daily_price_2h: null,
  134. daily_price_4h: null,
  135. daily_price_6h: null,
  136. daily_price_8h: null,
  137. services_types_ids: [],
  138. });
  139. const stepFourForm = ref({
  140. document_back: null,
  141. document_front: null,
  142. selfie: null,
  143. });
  144. const stepSixForm = ref({
  145. account_check_digit: "",
  146. account_number: "",
  147. account_type: "checking",
  148. bank: "",
  149. branch_check_digit: "",
  150. branch_number: "",
  151. pix_key: "",
  152. working_days: {},
  153. });
  154. const stepThreeForm = ref({
  155. address: "",
  156. address_type: "home",
  157. birth_date: "",
  158. city: "",
  159. complement: "",
  160. document: "",
  161. email: "",
  162. instructions: "",
  163. name: "",
  164. nickname: "Principal",
  165. no_complement: false,
  166. phone: "",
  167. rg: "",
  168. state: "",
  169. zip_code: "",
  170. });
  171. const steps = ref(1);
  172. const submitting = ref(false);
  173. const actionLabel = computed(() => {
  174. if (steps.value === 1) return t("provider.login.steps.step_1.action");
  175. if (steps.value === 2) return t("provider.login.steps.step_2.action");
  176. if (steps.value === 6) return t("provider.login.steps.step_6.action");
  177. return t("provider.login.steps.step_3.action");
  178. });
  179. const goBack = () => {
  180. if (steps.value === 4) {
  181. steps.value = 3;
  182. } else if (steps.value === 5) {
  183. steps.value = 4;
  184. } else if (steps.value === 6) {
  185. steps.value = 5;
  186. }
  187. };
  188. const hasWorkingDaySelected = () => mapWorkingDays().length > 0;
  189. const mapWorkingDays = () => {
  190. const mapped = [];
  191. const workingDays = stepSixForm.value.working_days || {};
  192. Object.entries(workingDays).forEach(([dayKey, periods]) => {
  193. if (periods?.morning) {
  194. mapped.push({ day: Number(dayKey), period: "morning" });
  195. }
  196. if (periods?.afternoon) {
  197. mapped.push({ day: Number(dayKey), period: "afternoon" });
  198. }
  199. });
  200. return mapped;
  201. };
  202. const onSubmit = async () => {
  203. if (registrationCompleted.value) return;
  204. if (showSubStep.value) return;
  205. const isValid = await loginForm.value.validate();
  206. if (!isValid) return;
  207. submitting.value = true;
  208. try {
  209. switch (steps.value) {
  210. case 1: {
  211. const response = await sendValidationCode();
  212. isLogin.value = response?.data?.payload?.isLogin === true;
  213. break;
  214. }
  215. case 2:
  216. await validateCodeInput();
  217. break;
  218. case 3: {
  219. if (await validateCurrentStep()) {
  220. steps.value = 4;
  221. }
  222. break;
  223. }
  224. case 4: {
  225. if (await validateCurrentStep()) {
  226. steps.value = 5;
  227. }
  228. break;
  229. }
  230. case 5: {
  231. if (await validateCurrentStep()) {
  232. steps.value = 6;
  233. }
  234. break;
  235. }
  236. case 6: {
  237. if (await validateCurrentStep()) {
  238. await registerUserAndProvider();
  239. }
  240. break;
  241. }
  242. default:
  243. break;
  244. }
  245. } catch (error) {
  246. console.error(error);
  247. const msg =
  248. error?.response?.data?.message ??
  249. error?.message ??
  250. "Erro ao concluir cadastro. Tente novamente.";
  251. $q.notify({ message: msg, type: "negative" });
  252. } finally {
  253. submitting.value = false;
  254. }
  255. };
  256. const registerUserAndProvider = async () => {
  257. const workingDays = mapWorkingDays();
  258. const form = new FormData();
  259. const hasValue = (val) =>
  260. val !== null && val !== undefined && String(val).trim() !== "";
  261. const append = (key, val) => {
  262. if (val === null || val === undefined || val === "") return;
  263. if (typeof val === "boolean") {
  264. form.append(key, val ? "1" : "0");
  265. } else {
  266. form.append(key, val);
  267. }
  268. };
  269. const appendIfFilled = (key, val) => {
  270. if (!hasValue(val)) return;
  271. form.append(key, val);
  272. };
  273. append("name", stepThreeForm.value.name);
  274. append("email", stepThreeForm.value.email || email.value);
  275. append("phone", stepThreeForm.value.phone || phone.value);
  276. append("code", code.value);
  277. append("rg", stepThreeForm.value.rg);
  278. append("document", stepThreeForm.value.document);
  279. append("birth_date", toISODate(stepThreeForm.value.birth_date));
  280. append("zip_code", stepThreeForm.value.zip_code);
  281. append("address", stepThreeForm.value.address);
  282. append("has_complement", !stepThreeForm.value.no_complement);
  283. append(
  284. "complement",
  285. stepThreeForm.value.no_complement ? null : stepThreeForm.value.complement,
  286. );
  287. append("nickname", stepThreeForm.value.nickname);
  288. append("instructions", stepThreeForm.value.instructions);
  289. append("city", stepThreeForm.value.city);
  290. append("state", stepThreeForm.value.state);
  291. append("address_type", stepThreeForm.value.address_type);
  292. append("daily_price_8h", Number(stepFiveForm.value.daily_price_8h));
  293. append("daily_price_6h", Number(stepFiveForm.value.daily_price_6h));
  294. append("daily_price_4h", Number(stepFiveForm.value.daily_price_4h));
  295. append("daily_price_2h", Number(stepFiveForm.value.daily_price_2h));
  296. (stepFiveForm.value.services_types_ids ?? []).forEach((id) => {
  297. form.append("services_types_ids[]", id);
  298. });
  299. workingDays.forEach((workingDay, index) => {
  300. form.append(`working_days[${index}][day]`, workingDay.day);
  301. form.append(`working_days[${index}][period]`, workingDay.period);
  302. });
  303. form.append("selfie", stepFourForm.value.selfie);
  304. form.append("document_front", stepFourForm.value.document_front);
  305. form.append("document_back", stepFourForm.value.document_back);
  306. append("recipient_name", stepThreeForm.value.name);
  307. append("recipient_email", stepThreeForm.value.email || email.value);
  308. append("recipient_document", stepThreeForm.value.document);
  309. append("recipient_type", "individual");
  310. const hasBankAccountData = [
  311. stepSixForm.value.bank,
  312. stepSixForm.value.branch_number,
  313. stepSixForm.value.account_number,
  314. stepSixForm.value.account_check_digit,
  315. ].every(hasValue);
  316. if (hasBankAccountData) {
  317. append("recipient_payment_mode", "bank_transfer");
  318. append(
  319. "recipient_default_bank_account[type]",
  320. stepSixForm.value.account_type,
  321. );
  322. append("recipient_default_bank_account[bank]", stepSixForm.value.bank);
  323. append(
  324. "recipient_default_bank_account[branch_number]",
  325. stepSixForm.value.branch_number,
  326. );
  327. appendIfFilled(
  328. "recipient_default_bank_account[branch_check_digit]",
  329. stepSixForm.value.branch_check_digit,
  330. );
  331. append(
  332. "recipient_default_bank_account[account_number]",
  333. stepSixForm.value.account_number,
  334. );
  335. append(
  336. "recipient_default_bank_account[account_check_digit]",
  337. stepSixForm.value.account_check_digit,
  338. );
  339. appendIfFilled(
  340. "recipient_default_bank_account[pix_key]",
  341. stepSixForm.value.pix_key,
  342. );
  343. append(
  344. "recipient_default_bank_account[holder_name]",
  345. stepThreeForm.value.name,
  346. );
  347. append(
  348. "recipient_default_bank_account[holder_document]",
  349. stepThreeForm.value.document,
  350. );
  351. append("recipient_default_bank_account[holder_type]", "individual");
  352. }
  353. const response = await createUserAndProvider(form);
  354. if (response.status >= 200 && response.status < 300) {
  355. registrationCompleted.value = true;
  356. }
  357. };
  358. const sendValidationCode = async () => {
  359. const response = await sendCode(email.value, phone.value);
  360. if (response.status === 201) {
  361. steps.value = 2;
  362. }
  363. return response;
  364. };
  365. const toISODate = (value) => {
  366. const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || "");
  367. if (!matches) return null;
  368. return `${matches[3]}-${matches[2]}-${matches[1]}`;
  369. };
  370. const validateCodeInput = async () => {
  371. const response = await validateCode(
  372. email.value,
  373. phone.value,
  374. code.value,
  375. isLogin.value,
  376. );
  377. if (response.status === 200) {
  378. if (isLogin.value === true) {
  379. await setAuthDataFromPayload(response.data.payload);
  380. router.push({ name: "DashboardPage" });
  381. return;
  382. }
  383. stepThreeForm.value.email = email.value;
  384. stepThreeForm.value.phone = phone.value;
  385. steps.value = 3;
  386. }
  387. };
  388. const validateCurrentStep = async () => {
  389. const isValid = await loginForm.value?.validate();
  390. if (!isValid) return false;
  391. if (steps.value === 4) {
  392. const hasDocumentBack = !!stepFourForm.value.document_back;
  393. const hasDocumentFront = !!stepFourForm.value.document_front;
  394. const hasSelfie = !!stepFourForm.value.selfie;
  395. if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
  396. $q.notify({
  397. message: t("provider.login.steps.step_4.upload_all_photos"),
  398. type: "negative",
  399. });
  400. return false;
  401. }
  402. }
  403. if (steps.value === 6 && !hasWorkingDaySelected()) {
  404. $q.notify({
  405. message: t("provider.login.steps.step_6.select_at_least_one"),
  406. type: "negative",
  407. });
  408. return false;
  409. }
  410. return true;
  411. };
  412. onMounted(() => {
  413. setTimeout(() => {
  414. clicked.value = true;
  415. }, 1500);
  416. });
  417. </script>
  418. <style lang="scss" scoped>
  419. .fade-slide-enter-active,
  420. .fade-slide-leave-active {
  421. transition:
  422. opacity 0.35s ease,
  423. transform 0.35s ease;
  424. }
  425. .fade-slide-enter-from {
  426. opacity: 0;
  427. transform: translateY(6px);
  428. }
  429. .fade-slide-leave-to {
  430. opacity: 0;
  431. transform: translateY(-6px);
  432. }
  433. .login-page {
  434. min-height: 100vh;
  435. display: flex;
  436. justify-content: center;
  437. background: var(--q-surface-dark);
  438. }
  439. .splash-screen {
  440. position: relative;
  441. width: 100vw;
  442. min-height: 100vh;
  443. overflow: hidden;
  444. cursor: pointer;
  445. .splash-layer {
  446. position: absolute;
  447. &--bg {
  448. inset: 0;
  449. width: 100%;
  450. height: 100%;
  451. object-fit: cover;
  452. }
  453. &--photo {
  454. inset: 0;
  455. width: 100%;
  456. height: 100%;
  457. object-fit: cover;
  458. opacity: 0.15;
  459. mix-blend-mode: multiply;
  460. }
  461. &--logo {
  462. top: 50%;
  463. left: 50%;
  464. transform: translate(-50%, -50%);
  465. width: 180px;
  466. z-index: 1;
  467. }
  468. }
  469. }
  470. .flow-screen {
  471. display: flex;
  472. flex-direction: column;
  473. width: 100%;
  474. max-width: 500px;
  475. min-height: 100vh;
  476. padding: 16px 20px;
  477. background: var(--q-surface-dark);
  478. }
  479. .flow-header {
  480. min-height: 36px;
  481. padding-left: max(4px, env(safe-area-inset-left));
  482. }
  483. .flow-back-btn {
  484. margin-left: -4px;
  485. }
  486. .flow-logo {
  487. display: flex;
  488. justify-content: center;
  489. padding: 12px 0 20px;
  490. }
  491. .flow-form {
  492. flex: 1;
  493. display: flex;
  494. flex-direction: column;
  495. min-height: 0;
  496. }
  497. .flow-content {
  498. flex: 1;
  499. overflow-y: auto;
  500. &--centered {
  501. display: flex;
  502. align-items: center;
  503. justify-content: center;
  504. }
  505. }
  506. .flow-footer {
  507. padding: 20px 0 calc(12px + env(safe-area-inset-bottom));
  508. }
  509. .registration-complete {
  510. flex: 1;
  511. min-height: 0;
  512. display: flex;
  513. flex-direction: column;
  514. align-items: center;
  515. justify-content: center;
  516. padding: 24px 8px 80px;
  517. text-align: center;
  518. &__title {
  519. font-size: 18px;
  520. line-height: 1.25;
  521. margin-bottom: 10px;
  522. }
  523. &__message {
  524. max-width: 320px;
  525. line-height: 1.45;
  526. }
  527. }
  528. </style>