|
|
@@ -0,0 +1,419 @@
|
|
|
+<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/*"
|
|
|
+ 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"
|
|
|
+ >
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.registration"
|
|
|
+ v-model:error="validationErrors.registration"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ readonly
|
|
|
+ bg-color="grey-2"
|
|
|
+ :label="$t('auth.first_access.registration')"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.name"
|
|
|
+ v-model:error="validationErrors.name"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ 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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :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"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :label="$t('common.terms.password')"
|
|
|
+ :rules="[inputRules.required, inputRules.password]"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultPasswordInput
|
|
|
+ v-model="form.password_confirmation"
|
|
|
+ v-model:error="validationErrors.password_confirmation"
|
|
|
+ class="col-12 col-sm-6"
|
|
|
+ :label="$t('auth.confirm_password')"
|
|
|
+ :rules="[inputRules.required, inputRules.samePassword(form.password)]"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p class="password-hint q-mt-sm">{{ $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="login-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, "associado");
|
|
|
+ $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);
|
|
|
+}
|
|
|
+
|
|
|
+.fields-card {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 600px;
|
|
|
+ padding: 32px;
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-logo {
|
|
|
+ width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+.login-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>
|