| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- <template>
- <q-page class="login-page column items-center justify-center">
- <div class="login-overlay" />
- <div class="login-card column items-center">
- <div class="column items-center fields-card">
- <q-img :src="Logo" class="login-logo q-mb-md" />
- <p class="login-title">{{ $t("auth.first_access.form_title") }}</p>
- <p class="login-description">{{ $t("auth.first_access.form_hint") }}</p>
- <div class="flex flex-center q-mb-md">
- <div class="avatar-wrap">
- <q-avatar size="96px" class="first-access-avatar">
- <img v-if="photoPreview" :src="photoPreview" class="avatar-img" />
- <q-icon v-else name="mdi-account" size="52px" color="white" />
- </q-avatar>
- <q-btn
- round
- unelevated
- size="sm"
- icon="mdi-camera-outline"
- class="avatar-edit-btn"
- color="violet-normal"
- text-color="white"
- @click="photoInputRef.click()"
- />
- <input
- ref="photoInputRef"
- type="file"
- accept="image/*"
- capture="user"
- style="display: none"
- @change="onPhotoSelected"
- />
- </div>
- </div>
- <div
- class="text-center text-caption q-mb-md"
- :class="photoError ? 'text-negative' : 'text-grey-7'"
- >
- {{ photoError || $t("auth.first_access.photo_hint") }}
- </div>
- <q-form
- ref="formRef"
- class="full-width"
- autocomplete="off"
- @submit="onSubmit"
- >
- <DefaultInput
- v-model="form.registration"
- v-model:error="validationErrors.registration"
- readonly
- bg-color="grey-2"
- :label="$t('auth.first_access.registration')"
- />
- <DefaultInput
- v-model="form.name"
- v-model:error="validationErrors.name"
- :readonly="isLocked('name')"
- :bg-color="isLocked('name') ? 'grey-2' : undefined"
- :label="$t('common.terms.name')"
- :rules="[inputRules.required]"
- />
- <DefaultInput
- v-model="form.cpf"
- v-model:error="validationErrors.cpf"
- :readonly="isLocked('cpf')"
- :bg-color="isLocked('cpf') ? 'grey-2' : undefined"
- :mask="masks.Brasil.cpf"
- :label="$t('common.terms.cpf')"
- :rules="[inputRules.required, inputRules.cpf]"
- />
- <DefaultInput
- v-model="form.email"
- v-model:error="validationErrors.email"
- type="email"
- :readonly="isLocked('email')"
- :bg-color="isLocked('email') ? 'grey-2' : undefined"
- :label="$t('common.terms.email')"
- :rules="[inputRules.required, inputRules.email]"
- />
- <DefaultInput
- v-model="form.phone"
- v-model:error="validationErrors.phone"
- :readonly="isLocked('phone')"
- :bg-color="isLocked('phone') ? 'grey-2' : undefined"
- mask="(##) #####-####"
- :label="$t('auth.first_access.whatsapp')"
- :rules="[inputRules.required]"
- />
- <DefaultSelect
- v-model="form.position_id"
- v-model:error="validationErrors.position_id"
- :options="positionOptions"
- option-value="id"
- option-label="name"
- emit-value
- map-options
- :readonly="isLocked('position_id')"
- :bg-color="isLocked('position_id') ? 'grey-2' : undefined"
- :label="$t('associado.position')"
- :rules="[inputRules.required]"
- />
- <DefaultSelect
- v-model="form.sector_id"
- v-model:error="validationErrors.sector_id"
- :options="sectorOptions"
- option-value="id"
- option-label="name"
- emit-value
- map-options
- :readonly="isLocked('sector_id')"
- :bg-color="isLocked('sector_id') ? 'grey-2' : undefined"
- :label="$t('associado.sector')"
- :rules="[inputRules.required]"
- />
- <DefaultPasswordInput
- v-model="form.password"
- v-model:error="validationErrors.password"
- :label="$t('common.terms.password')"
- :rules="[inputRules.required, inputRules.password]"
- />
- <DefaultPasswordInput
- v-model="form.password_confirmation"
- v-model:error="validationErrors.password_confirmation"
- :label="$t('auth.confirm_password')"
- :rules="[inputRules.required, inputRules.samePassword(form.password)]"
- />
- <p class="password-hint">{{ $t("auth.password_hint") }}</p>
- <q-btn
- class="full-width q-mt-md login-btn"
- color="violet-normal"
- :label="$t('auth.first_access.finish')"
- type="submit"
- unelevated
- :loading
- >
- <template #loading>
- <q-spinner />
- </template>
- </q-btn>
- </q-form>
- <a href="#" class="forgot-link q-mt-md" @click.prevent="goBack">
- <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
- {{ $t("auth.first_access.back_to_login") }}
- </a>
- </div>
- </div>
- </q-page>
- </template>
- <script setup>
- import { ref, onBeforeMount, useTemplateRef } from "vue";
- import { useQuasar } from "quasar";
- import { useI18n } from "vue-i18n";
- import { useRouter } from "vue-router";
- import { useAuth } from "src/composables/useAuth";
- import { useInputRules } from "src/composables/useInputRules";
- import { useSubmitHandler } from "src/composables/useSubmitHandler";
- import {
- registerFirstAccess,
- getPublicPositions,
- getPublicSectors,
- } from "src/api/firstAccess";
- import { getFirstAccessData, clearFirstAccessData } from "./firstAccessStorage";
- import masks from "src/helpers/masks";
- import Logo from "src/assets/logo_serprati.svg";
- import DefaultInput from "src/components/defaults/DefaultInput.vue";
- import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
- import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
- const router = useRouter();
- const $q = useQuasar();
- const { t } = useI18n();
- const { inputRules } = useInputRules();
- const { login } = useAuth();
- const formRef = useTemplateRef("formRef");
- const photoInputRef = useTemplateRef("photoInputRef");
- const token = ref(null);
- const lockedFields = ref([]);
- const photoFile = ref(null);
- const photoPreview = ref(null);
- const photoError = ref(null);
- const positionOptions = ref([]);
- const sectorOptions = ref([]);
- const form = ref({
- registration: null,
- name: null,
- cpf: null,
- email: null,
- phone: null,
- position_id: null,
- sector_id: null,
- password: null,
- password_confirmation: null,
- });
- const isLocked = (field) => lockedFields.value.includes(field);
- const {
- loading,
- validationErrors,
- execute: submitForm,
- } = useSubmitHandler({ formRef });
- const onPhotoSelected = (event) => {
- const file = event.target.files?.[0];
- event.target.value = "";
- if (!file) return;
- photoFile.value = file;
- photoPreview.value = URL.createObjectURL(file);
- photoError.value = null;
- };
- const onSubmit = async () => {
- if (!photoFile.value) {
- photoError.value = t("auth.first_access.photo_required");
- return;
- }
- const password = form.value.password;
- const user = await submitForm(() =>
- registerFirstAccess({
- ...form.value,
- token: token.value,
- photo: photoFile.value,
- }),
- );
- if (!user) return;
- clearFirstAccessData();
- try {
- await login(form.value.registration, password);
- $q.notify({ type: "positive", message: t("auth.first_access.success") });
- router.push({ name: "CarteirinhaPage" });
- } catch {
- $q.notify({ type: "positive", message: t("auth.first_access.success_login") });
- router.push({ name: "LoginPage" });
- }
- };
- const goBack = () => {
- clearFirstAccessData();
- router.push({ name: "LoginPage" });
- };
- onBeforeMount(async () => {
- const data = getFirstAccessData();
- if (!data?.token) {
- router.replace({ name: "FirstAccessPage" });
- return;
- }
- token.value = data.token;
- form.value.registration = data.registration;
- if (data.user) {
- const { photo_url, ...fields } = data.user;
- Object.entries(fields).forEach(([key, value]) => {
- if (value === null || value === "") return;
- form.value[key] = value;
- lockedFields.value.push(key);
- });
- if (photo_url) {
- photoPreview.value = photo_url;
- }
- }
- try {
- const [positions, sectors] = await Promise.all([
- getPublicPositions(),
- getPublicSectors(),
- ]);
- positionOptions.value = positions ?? [];
- sectorOptions.value = sectors ?? [];
- } catch {
- $q.notify({ type: "negative", message: t("http.errors.failed") });
- }
- });
- </script>
- <style lang="scss" scoped>
- @use "sass:map";
- @use "src/css/quasar.variables.scss" as *;
- .login-page {
- min-height: 100dvh;
- background-image: url("src/assets/pessoas_fundo.jpg");
- background-size: cover;
- background-position: center;
- position: relative;
- }
- .login-overlay {
- position: absolute;
- inset: 0;
- background: rgba(74, 20, 140, 0.48);
- z-index: 0;
- }
- .login-card {
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- width: calc(100% - 32px);
- min-height: calc(100dvh - 32px);
- margin: 16px 0;
- background: rgba(255, 255, 255, 0.747);
- border-radius: 16px;
- box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
- padding-top: env(safe-area-inset-top);
- padding-bottom: env(safe-area-inset-bottom);
- }
- .fields-card {
- width: 100%;
- max-width: 400px;
- padding: 32px;
- border-radius: 12px;
- :deep(.q-field__native),
- :deep(.q-field__input) {
- color: $color-text !important;
- }
- :deep(.q-field__control) {
- background: $surface !important;
- }
- :deep(.q-field__label) {
- color: $color-text-2;
- }
- :deep(.q-field--focused .q-field__label) {
- color: $violet-normal;
- }
- :deep(.q-field--float:not(.q-field--focused) .q-field__label) {
- color: $neutral-dark-active;
- }
- }
- .login-logo {
- width: 220px;
- max-width: 100%;
- }
- .login-title {
- color: map.get($colors, "violet-normal");
- font-size: 18px;
- font-weight: 700;
- margin: 0 0 8px;
- text-align: center;
- }
- .login-description {
- color: map.get($colors, "text-2");
- font-size: 13px;
- text-align: center;
- margin: 0 0 16px;
- }
- .password-hint {
- color: map.get($colors, "text-2");
- font-size: 12px;
- margin: 0;
- }
- .avatar-wrap {
- position: relative;
- display: inline-block;
- }
- .first-access-avatar {
- background: rgba(112, 32, 130, 0.18) !important;
- }
- .avatar-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .avatar-edit-btn {
- position: absolute;
- bottom: -2px;
- right: -2px;
- }
- .login-btn {
- height: 44px;
- font-size: 15px;
- font-weight: 600;
- letter-spacing: 0.5px;
- }
- .forgot-link {
- color: map.get($colors, "violet-normal");
- font-size: 13px;
- text-decoration: none;
- cursor: pointer;
- &:hover {
- color: map.get($colors, "violet-normal-hover");
- text-decoration: underline;
- }
- }
- </style>
|