LoginPage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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 v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
  6. <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
  7. <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
  8. <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
  9. </div>
  10. <div v-else key="flow" class="flow-screen">
  11. <div v-if="!showSubStep" class="flow-logo q-my-xl">
  12. <q-img :src="LogoDiariaCampos" style="max-width: 180px" />
  13. </div>
  14. <q-form
  15. ref="loginForm"
  16. class="flow-form"
  17. autocorrect="off"
  18. autocapitalize="off"
  19. autocomplete="off"
  20. spellcheck="false"
  21. @submit="onSubmit"
  22. >
  23. <div class="flow-content" :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }">
  24. <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
  25. <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
  26. <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
  27. <LoginStepFourPanel
  28. v-else-if="steps === 4"
  29. v-model="stepFourForm"
  30. @update:show-sub-step="showSubStep = $event"
  31. />
  32. <LoginStepFivePanel v-else-if="steps === 5" v-model="stepFiveForm" />
  33. <LoginStepSixPanel v-else-if="steps === 6" v-model="stepSixForm" />
  34. <div v-else-if="steps === 7" class="column items-center justify-center q-gutter-md text-center">
  35. <q-icon name="mdi-clock-outline" size="64px" color="warning" />
  36. <div class="text-h6 text-text">{{ $t('provider.login.pending_approval.title') }}</div>
  37. <div class="text-body2 text-grey-5">{{ $t('provider.login.pending_approval.description') }}</div>
  38. </div>
  39. </div>
  40. <div v-if="!showSubStep && steps !== 7" class="flow-footer">
  41. <q-btn
  42. color="primary-button"
  43. :label="actionLabel"
  44. rounded
  45. padding="8px 16px"
  46. type="submit"
  47. class="full-width"
  48. :loading="submitting"
  49. >
  50. <template #loading>
  51. <q-spinner />
  52. </template>
  53. </q-btn>
  54. </div>
  55. </q-form>
  56. </div>
  57. </Transition>
  58. </q-page>
  59. </template>
  60. <script setup>
  61. import { computed, ref } from 'vue';
  62. import { useQuasar } from 'quasar';
  63. import { useRouter } from 'vue-router';
  64. import { useI18n } from 'vue-i18n';
  65. import { createUserAndProvider, sendCode, validateCode } from 'src/api/user';
  66. import { useAuth } from 'src/composables/useAuth';
  67. import BackgroundLogin from 'src/assets/background-login.svg';
  68. import FotoDiarista from 'src/assets/foto_diarista_login.svg';
  69. import LogoLogin from 'src/assets/logo_diaria_login.svg';
  70. import LogoDiariaCampos from 'src/assets/logo_diaria_campos_login.svg';
  71. import LoginStepOnePanel from 'src/components/login/LoginStepOnePanel.vue';
  72. import LoginStepTwoPanel from 'src/components/login/LoginStepTwoPanel.vue';
  73. import LoginStepThreePanel from 'src/components/login/LoginStepThreePanel.vue';
  74. import LoginStepFourPanel from 'src/components/login/LoginStepFourPanel.vue';
  75. import LoginStepFivePanel from 'src/components/login/LoginStepFivePanel.vue';
  76. import LoginStepSixPanel from 'src/components/login/LoginStepSixPanel.vue';
  77. const { t } = useI18n();
  78. const $q = useQuasar();
  79. const router = useRouter();
  80. const { setAuthDataFromPayload } = useAuth();
  81. const clicked = ref(false);
  82. const showSubStep = ref(false);
  83. const steps = ref(1);
  84. const submitting = ref(false);
  85. const loginForm = ref(null);
  86. const isLogin = ref(false);
  87. const email = ref('');
  88. const phone = ref('');
  89. const code = ref('');
  90. const stepThreeForm = ref({
  91. name: '',
  92. phone: '',
  93. email: '',
  94. rg: '',
  95. document: '',
  96. birth_date: '',
  97. zip_code: '',
  98. address: '',
  99. complement: '',
  100. no_complement: false,
  101. city: '',
  102. state: '',
  103. address_type: 'home',
  104. nickname: 'Principal',
  105. instructions: '',
  106. });
  107. const stepFourForm = ref({
  108. selfie: null,
  109. document_front: null,
  110. document_back: null,
  111. });
  112. const stepFiveForm = ref({
  113. daily_price_8h: null,
  114. daily_price_6h: null,
  115. daily_price_4h: null,
  116. daily_price_2h: null,
  117. services_types_ids: [],
  118. });
  119. const stepSixForm = ref({
  120. working_days: {},
  121. });
  122. const actionLabel = computed(() => {
  123. if (steps.value === 1) return t('provider.login.steps.step_1.action');
  124. if (steps.value === 2) return t('provider.login.steps.step_2.action');
  125. if (steps.value === 6) return t('provider.login.steps.step_6.action');
  126. return t('provider.login.steps.step_3.action');
  127. });
  128. const toISODate = (value) => {
  129. const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || '');
  130. if (!matches) return null;
  131. return `${matches[3]}-${matches[2]}-${matches[1]}`;
  132. };
  133. const mapWorkingDays = () => {
  134. const mapped = [];
  135. const workingDays = stepSixForm.value.working_days || {};
  136. Object.entries(workingDays).forEach(([dayKey, periods]) => {
  137. if (periods?.morning) {
  138. mapped.push({ day: Number(dayKey), period: 'morning' });
  139. }
  140. if (periods?.afternoon) {
  141. mapped.push({ day: Number(dayKey), period: 'afternoon' });
  142. }
  143. });
  144. return mapped;
  145. };
  146. const hasWorkingDaySelected = () => {
  147. return mapWorkingDays().length > 0;
  148. };
  149. const validateCurrentStep = async () => {
  150. const isValid = await loginForm.value?.validate();
  151. if (!isValid) {
  152. return false;
  153. }
  154. if(steps.value === 4) {
  155. const hasSelfie = !!stepFourForm.value.selfie;
  156. const hasDocumentFront = !!stepFourForm.value.document_front;
  157. const hasDocumentBack = !!stepFourForm.value.document_back;
  158. if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
  159. $q.notify({
  160. type: 'negative',
  161. message: t('provider.login.steps.step_4.upload_all_photos'),
  162. });
  163. return false;
  164. }
  165. }
  166. if (steps.value === 6 && !hasWorkingDaySelected()) {
  167. $q.notify({
  168. type: 'negative',
  169. message: t('provider.login.steps.step_6.select_at_least_one'),
  170. });
  171. return false;
  172. }
  173. return true;
  174. };
  175. const sendValidationCode = async () => {
  176. const response = await sendCode(email.value, phone.value);
  177. if (response.status === 201) {
  178. steps.value = 2;
  179. }
  180. return response;
  181. };
  182. const validateCodeInput = async () => {
  183. const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
  184. if (response.status === 200) {
  185. if (isLogin.value === true) {
  186. await setAuthDataFromPayload(response.data.payload);
  187. router.push({ name: 'DashboardPage' });
  188. return;
  189. }
  190. stepThreeForm.value.email = email.value;
  191. stepThreeForm.value.phone = phone.value;
  192. steps.value = 3;
  193. }
  194. };
  195. const registerUserAndProvider = async () => {
  196. const workingDays = mapWorkingDays();
  197. const form = new FormData();
  198. const append = (key, val) => {
  199. if (val === null || val === undefined) return;
  200. if (typeof val === 'boolean') form.append(key, val ? '1' : '0');
  201. else form.append(key, val);
  202. };
  203. append('name', stepThreeForm.value.name);
  204. append('email', stepThreeForm.value.email || email.value);
  205. append('phone', stepThreeForm.value.phone || phone.value);
  206. append('code', code.value);
  207. append('rg', stepThreeForm.value.rg);
  208. append('document', stepThreeForm.value.document);
  209. append('birth_date', toISODate(stepThreeForm.value.birth_date));
  210. append('zip_code', stepThreeForm.value.zip_code);
  211. append('address', stepThreeForm.value.address);
  212. append('has_complement', !stepThreeForm.value.no_complement);
  213. append('complement', stepThreeForm.value.no_complement ? null : stepThreeForm.value.complement);
  214. append('nickname', stepThreeForm.value.nickname);
  215. append('instructions', stepThreeForm.value.instructions);
  216. append('city', stepThreeForm.value.city);
  217. append('state', stepThreeForm.value.state);
  218. append('address_type', stepThreeForm.value.address_type);
  219. append('daily_price_8h', Number(stepFiveForm.value.daily_price_8h));
  220. append('daily_price_6h', Number(stepFiveForm.value.daily_price_6h));
  221. append('daily_price_4h', Number(stepFiveForm.value.daily_price_4h));
  222. append('daily_price_2h', Number(stepFiveForm.value.daily_price_2h));
  223. (stepFiveForm.value.services_types_ids ?? []).forEach(id => form.append('services_types_ids[]', id));
  224. workingDays.forEach((wd, i) => {
  225. form.append(`working_days[${i}][day]`, wd.day);
  226. form.append(`working_days[${i}][period]`, wd.period);
  227. });
  228. form.append('selfie', stepFourForm.value.selfie);
  229. form.append('document_front', stepFourForm.value.document_front);
  230. form.append('document_back', stepFourForm.value.document_back);
  231. const response = await createUserAndProvider(form);
  232. if (response.status === 200) {
  233. steps.value = 7;
  234. }
  235. };
  236. const onSubmit = async () => {
  237. if (showSubStep.value) return;
  238. const isValid = await loginForm.value.validate();
  239. if (!isValid) return;
  240. submitting.value = true;
  241. try {
  242. switch (steps.value) {
  243. case 1: {
  244. const response = await sendValidationCode();
  245. isLogin.value = response?.data?.payload?.isLogin === true;
  246. break;
  247. }
  248. case 2:
  249. await validateCodeInput();
  250. break;
  251. case 3: {
  252. if (await validateCurrentStep()) {
  253. steps.value = 4;
  254. }
  255. break;
  256. }
  257. case 4: {
  258. if (await validateCurrentStep()) {
  259. steps.value = 5;
  260. }
  261. break;
  262. }
  263. case 5: {
  264. if (await validateCurrentStep()) {
  265. steps.value = 6;
  266. }
  267. break;
  268. }
  269. case 6: {
  270. if (await validateCurrentStep()) {
  271. await registerUserAndProvider();
  272. }
  273. break;
  274. }
  275. default:
  276. break;
  277. }
  278. } catch (error) {
  279. console.error(error);
  280. } finally {
  281. submitting.value = false;
  282. }
  283. };
  284. </script>
  285. <style lang="scss" scoped>
  286. .fade-slide-enter-active,
  287. .fade-slide-leave-active {
  288. transition: opacity 0.35s ease, transform 0.35s ease;
  289. }
  290. .fade-slide-enter-from {
  291. opacity: 0;
  292. transform: translateY(6px);
  293. }
  294. .fade-slide-leave-to {
  295. opacity: 0;
  296. transform: translateY(-6px);
  297. }
  298. .login-page {
  299. min-height: 100vh;
  300. display: flex;
  301. justify-content: center;
  302. background: var(--q-surface-dark);
  303. }
  304. .splash-screen {
  305. position: relative;
  306. width: 100vw;
  307. min-height: 100vh;
  308. overflow: hidden;
  309. cursor: pointer;
  310. .splash-layer {
  311. position: absolute;
  312. &--bg {
  313. inset: 0;
  314. width: 100%;
  315. height: 100%;
  316. object-fit: cover;
  317. }
  318. &--photo {
  319. inset: 0;
  320. width: 100%;
  321. height: 100%;
  322. object-fit: cover;
  323. opacity: 0.15;
  324. mix-blend-mode: multiply;
  325. }
  326. &--logo {
  327. top: 50%;
  328. left: 50%;
  329. transform: translate(-50%, -50%);
  330. width: 180px;
  331. z-index: 1;
  332. }
  333. }
  334. }
  335. .flow-screen {
  336. display: flex;
  337. flex-direction: column;
  338. width: 100%;
  339. max-width: 500px;
  340. min-height: 100vh;
  341. padding: 16px 20px;
  342. background: var(--q-surface-dark);
  343. }
  344. .flow-header {
  345. min-height: 36px;
  346. }
  347. .flow-logo {
  348. display: flex;
  349. justify-content: center;
  350. padding: 12px 0 20px;
  351. }
  352. .flow-form {
  353. flex: 1;
  354. display: flex;
  355. flex-direction: column;
  356. min-height: 0;
  357. }
  358. .flow-content {
  359. flex: 1;
  360. overflow-y: auto;
  361. &--centered {
  362. display: flex;
  363. align-items: center;
  364. justify-content: center;
  365. }
  366. }
  367. .flow-footer {
  368. padding: 20px 0 12px;
  369. }
  370. </style>