FirstAccessFormPage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <q-page class="login-page column items-center justify-center">
  3. <div class="login-overlay" />
  4. <div class="login-card column items-center">
  5. <div class="column items-center fields-card">
  6. <q-img :src="Logo" class="login-logo q-mb-md" />
  7. <p class="login-title">{{ $t("auth.first_access.form_title") }}</p>
  8. <p class="login-description">{{ $t("auth.first_access.form_hint") }}</p>
  9. <div class="flex flex-center q-mb-md">
  10. <div class="avatar-wrap">
  11. <q-avatar size="96px" class="first-access-avatar">
  12. <img v-if="photoPreview" :src="photoPreview" class="avatar-img" />
  13. <q-icon v-else name="mdi-account" size="52px" color="white" />
  14. </q-avatar>
  15. <q-btn
  16. round
  17. unelevated
  18. size="sm"
  19. icon="mdi-camera-outline"
  20. class="avatar-edit-btn"
  21. color="violet-normal"
  22. text-color="white"
  23. @click="photoInputRef.click()"
  24. />
  25. <input
  26. ref="photoInputRef"
  27. type="file"
  28. accept="image/*"
  29. capture="user"
  30. style="display: none"
  31. @change="onPhotoSelected"
  32. />
  33. </div>
  34. </div>
  35. <div
  36. class="text-center text-caption q-mb-md"
  37. :class="photoError ? 'text-negative' : 'text-grey-7'"
  38. >
  39. {{ photoError || $t("auth.first_access.photo_hint") }}
  40. </div>
  41. <q-form
  42. ref="formRef"
  43. class="full-width"
  44. autocomplete="off"
  45. @submit="onSubmit"
  46. >
  47. <DefaultInput
  48. v-model="form.registration"
  49. v-model:error="validationErrors.registration"
  50. readonly
  51. bg-color="grey-2"
  52. :label="$t('auth.first_access.registration')"
  53. />
  54. <DefaultInput
  55. v-model="form.name"
  56. v-model:error="validationErrors.name"
  57. :readonly="isLocked('name')"
  58. :bg-color="isLocked('name') ? 'grey-2' : undefined"
  59. :label="$t('common.terms.name')"
  60. :rules="[inputRules.required]"
  61. />
  62. <DefaultInput
  63. v-model="form.cpf"
  64. v-model:error="validationErrors.cpf"
  65. :readonly="isLocked('cpf')"
  66. :bg-color="isLocked('cpf') ? 'grey-2' : undefined"
  67. :mask="masks.Brasil.cpf"
  68. :label="$t('common.terms.cpf')"
  69. :rules="[inputRules.required, inputRules.cpf]"
  70. />
  71. <DefaultInput
  72. v-model="form.email"
  73. v-model:error="validationErrors.email"
  74. type="email"
  75. :readonly="isLocked('email')"
  76. :bg-color="isLocked('email') ? 'grey-2' : undefined"
  77. :label="$t('common.terms.email')"
  78. :rules="[inputRules.required, inputRules.email]"
  79. />
  80. <DefaultInput
  81. v-model="form.phone"
  82. v-model:error="validationErrors.phone"
  83. :readonly="isLocked('phone')"
  84. :bg-color="isLocked('phone') ? 'grey-2' : undefined"
  85. mask="(##) #####-####"
  86. :label="$t('auth.first_access.whatsapp')"
  87. :rules="[inputRules.required]"
  88. />
  89. <DefaultSelect
  90. v-model="form.position_id"
  91. v-model:error="validationErrors.position_id"
  92. :options="positionOptions"
  93. option-value="id"
  94. option-label="name"
  95. emit-value
  96. map-options
  97. :readonly="isLocked('position_id')"
  98. :bg-color="isLocked('position_id') ? 'grey-2' : undefined"
  99. :label="$t('associado.position')"
  100. :rules="[inputRules.required]"
  101. />
  102. <DefaultSelect
  103. v-model="form.sector_id"
  104. v-model:error="validationErrors.sector_id"
  105. :options="sectorOptions"
  106. option-value="id"
  107. option-label="name"
  108. emit-value
  109. map-options
  110. :readonly="isLocked('sector_id')"
  111. :bg-color="isLocked('sector_id') ? 'grey-2' : undefined"
  112. :label="$t('associado.sector')"
  113. :rules="[inputRules.required]"
  114. />
  115. <DefaultPasswordInput
  116. v-model="form.password"
  117. v-model:error="validationErrors.password"
  118. :label="$t('common.terms.password')"
  119. :rules="[inputRules.required, inputRules.password]"
  120. />
  121. <DefaultPasswordInput
  122. v-model="form.password_confirmation"
  123. v-model:error="validationErrors.password_confirmation"
  124. :label="$t('auth.confirm_password')"
  125. :rules="[inputRules.required, inputRules.samePassword(form.password)]"
  126. />
  127. <p class="password-hint">{{ $t("auth.password_hint") }}</p>
  128. <q-btn
  129. class="full-width q-mt-md login-btn"
  130. color="violet-normal"
  131. :label="$t('auth.first_access.finish')"
  132. type="submit"
  133. unelevated
  134. :loading
  135. >
  136. <template #loading>
  137. <q-spinner />
  138. </template>
  139. </q-btn>
  140. </q-form>
  141. <a href="#" class="forgot-link q-mt-md" @click.prevent="goBack">
  142. <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
  143. {{ $t("auth.first_access.back_to_login") }}
  144. </a>
  145. </div>
  146. </div>
  147. </q-page>
  148. </template>
  149. <script setup>
  150. import { ref, onBeforeMount, useTemplateRef } from "vue";
  151. import { useQuasar } from "quasar";
  152. import { useI18n } from "vue-i18n";
  153. import { useRouter } from "vue-router";
  154. import { useAuth } from "src/composables/useAuth";
  155. import { useInputRules } from "src/composables/useInputRules";
  156. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  157. import {
  158. registerFirstAccess,
  159. getPublicPositions,
  160. getPublicSectors,
  161. } from "src/api/firstAccess";
  162. import { getFirstAccessData, clearFirstAccessData } from "./firstAccessStorage";
  163. import masks from "src/helpers/masks";
  164. import Logo from "src/assets/logo_serprati.svg";
  165. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  166. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  167. import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
  168. const router = useRouter();
  169. const $q = useQuasar();
  170. const { t } = useI18n();
  171. const { inputRules } = useInputRules();
  172. const { login } = useAuth();
  173. const formRef = useTemplateRef("formRef");
  174. const photoInputRef = useTemplateRef("photoInputRef");
  175. const token = ref(null);
  176. const lockedFields = ref([]);
  177. const photoFile = ref(null);
  178. const photoPreview = ref(null);
  179. const photoError = ref(null);
  180. const positionOptions = ref([]);
  181. const sectorOptions = ref([]);
  182. const form = ref({
  183. registration: null,
  184. name: null,
  185. cpf: null,
  186. email: null,
  187. phone: null,
  188. position_id: null,
  189. sector_id: null,
  190. password: null,
  191. password_confirmation: null,
  192. });
  193. const isLocked = (field) => lockedFields.value.includes(field);
  194. const {
  195. loading,
  196. validationErrors,
  197. execute: submitForm,
  198. } = useSubmitHandler({ formRef });
  199. const onPhotoSelected = (event) => {
  200. const file = event.target.files?.[0];
  201. event.target.value = "";
  202. if (!file) return;
  203. photoFile.value = file;
  204. photoPreview.value = URL.createObjectURL(file);
  205. photoError.value = null;
  206. };
  207. const onSubmit = async () => {
  208. if (!photoFile.value) {
  209. photoError.value = t("auth.first_access.photo_required");
  210. return;
  211. }
  212. const password = form.value.password;
  213. const user = await submitForm(() =>
  214. registerFirstAccess({
  215. ...form.value,
  216. token: token.value,
  217. photo: photoFile.value,
  218. }),
  219. );
  220. if (!user) return;
  221. clearFirstAccessData();
  222. try {
  223. await login(form.value.registration, password);
  224. $q.notify({ type: "positive", message: t("auth.first_access.success") });
  225. router.push({ name: "CarteirinhaPage" });
  226. } catch {
  227. $q.notify({ type: "positive", message: t("auth.first_access.success_login") });
  228. router.push({ name: "LoginPage" });
  229. }
  230. };
  231. const goBack = () => {
  232. clearFirstAccessData();
  233. router.push({ name: "LoginPage" });
  234. };
  235. onBeforeMount(async () => {
  236. const data = getFirstAccessData();
  237. if (!data?.token) {
  238. router.replace({ name: "FirstAccessPage" });
  239. return;
  240. }
  241. token.value = data.token;
  242. form.value.registration = data.registration;
  243. if (data.user) {
  244. const { photo_url, ...fields } = data.user;
  245. Object.entries(fields).forEach(([key, value]) => {
  246. if (value === null || value === "") return;
  247. form.value[key] = value;
  248. lockedFields.value.push(key);
  249. });
  250. if (photo_url) {
  251. photoPreview.value = photo_url;
  252. }
  253. }
  254. try {
  255. const [positions, sectors] = await Promise.all([
  256. getPublicPositions(),
  257. getPublicSectors(),
  258. ]);
  259. positionOptions.value = positions ?? [];
  260. sectorOptions.value = sectors ?? [];
  261. } catch {
  262. $q.notify({ type: "negative", message: t("http.errors.failed") });
  263. }
  264. });
  265. </script>
  266. <style lang="scss" scoped>
  267. @use "sass:map";
  268. @use "src/css/quasar.variables.scss" as *;
  269. .login-page {
  270. min-height: 100dvh;
  271. background-image: url("src/assets/pessoas_fundo.jpg");
  272. background-size: cover;
  273. background-position: center;
  274. position: relative;
  275. }
  276. .login-overlay {
  277. position: absolute;
  278. inset: 0;
  279. background: rgba(74, 20, 140, 0.48);
  280. z-index: 0;
  281. }
  282. .login-card {
  283. z-index: 1;
  284. display: flex;
  285. align-items: center;
  286. justify-content: center;
  287. width: calc(100% - 32px);
  288. min-height: calc(100dvh - 32px);
  289. margin: 16px 0;
  290. background: rgba(255, 255, 255, 0.747);
  291. border-radius: 16px;
  292. box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
  293. padding-top: env(safe-area-inset-top);
  294. padding-bottom: env(safe-area-inset-bottom);
  295. }
  296. .fields-card {
  297. width: 100%;
  298. max-width: 400px;
  299. padding: 32px;
  300. border-radius: 12px;
  301. :deep(.q-field__native),
  302. :deep(.q-field__input) {
  303. color: $color-text !important;
  304. }
  305. :deep(.q-field__control) {
  306. background: $surface !important;
  307. }
  308. :deep(.q-field__label) {
  309. color: $color-text-2;
  310. }
  311. :deep(.q-field--focused .q-field__label) {
  312. color: $violet-normal;
  313. }
  314. :deep(.q-field--float:not(.q-field--focused) .q-field__label) {
  315. color: $neutral-dark-active;
  316. }
  317. }
  318. .login-logo {
  319. width: 220px;
  320. max-width: 100%;
  321. }
  322. .login-title {
  323. color: map.get($colors, "violet-normal");
  324. font-size: 18px;
  325. font-weight: 700;
  326. margin: 0 0 8px;
  327. text-align: center;
  328. }
  329. .login-description {
  330. color: map.get($colors, "text-2");
  331. font-size: 13px;
  332. text-align: center;
  333. margin: 0 0 16px;
  334. }
  335. .password-hint {
  336. color: map.get($colors, "text-2");
  337. font-size: 12px;
  338. margin: 0;
  339. }
  340. .avatar-wrap {
  341. position: relative;
  342. display: inline-block;
  343. }
  344. .first-access-avatar {
  345. background: rgba(112, 32, 130, 0.18) !important;
  346. }
  347. .avatar-img {
  348. width: 100%;
  349. height: 100%;
  350. object-fit: cover;
  351. }
  352. .avatar-edit-btn {
  353. position: absolute;
  354. bottom: -2px;
  355. right: -2px;
  356. }
  357. .login-btn {
  358. height: 44px;
  359. font-size: 15px;
  360. font-weight: 600;
  361. letter-spacing: 0.5px;
  362. }
  363. .forgot-link {
  364. color: map.get($colors, "violet-normal");
  365. font-size: 13px;
  366. text-decoration: none;
  367. cursor: pointer;
  368. &:hover {
  369. color: map.get($colors, "violet-normal-hover");
  370. text-decoration: underline;
  371. }
  372. }
  373. </style>