LoginPage.vue 17 KB

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