Kaynağa Gözat

feat: ✨ feat (termos condicoes) associado obrigatorio aceitar termos

adicionada funcao que obriga associado a aceitar termos e condicoes ao realizar fluxo de primeiro acesso

fase:dev | origin:prod
Gustavo Zanatta 1 hafta önce
ebeveyn
işleme
cbd1469341

+ 12 - 0
src/components/CompleteProfileDialog.vue

@@ -151,6 +151,12 @@
 
         </div>
 
+        <TermsAcceptCheckbox
+          v-if="!initialUser.terms_accepted_at"
+          v-model="termsAccepted"
+          class="q-mt-md"
+        />
+
       </q-card-section>
 
       <q-card-actions class="q-px-md q-pb-md">
@@ -178,6 +184,7 @@ import { userStore } from "src/stores/user";
 import { getPositions } from "src/api/position";
 import { getSectors } from "src/api/sector";
 import { uploadMyAvatar, updateMyProfile } from "src/api/user";
+import TermsAcceptCheckbox from "src/components/TermsAcceptCheckbox.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
@@ -193,6 +200,7 @@ const localPhotoUrl = ref(null);
 const uploadingPhoto = ref(false);
 const saving = ref(false);
 const positionOptions = ref([]);
+const termsAccepted = ref(false);
 const sectorOptions = ref([]);
 
 const form = ref({
@@ -208,6 +216,7 @@ const form = ref({
 
 const canSave = computed(() => {
   return (
+    (!!initialUser.value.terms_accepted_at || termsAccepted.value) &&
     !!localPhotoUrl.value &&
     !!form.value.name?.trim() &&
     !!form.value.cpf?.trim() &&
@@ -246,6 +255,8 @@ const onSave = async () => {
     if (!initialUser.value.position_id) payload.position_id = form.value.position_id;
     if (!initialUser.value.sector_id)   payload.sector_id   = form.value.sector_id;
 
+    if (!initialUser.value.terms_accepted_at) payload.terms_accepted = 1;
+
     if (Object.keys(payload).length > 0) {
       const updated = await updateMyProfile(payload);
       store.user = updated;
@@ -270,6 +281,7 @@ onMounted(async () => {
     sector_id:   user.sector_id   ?? user.sector?.id   ?? null,
     registration:  user.registration  || null,
     admission_date: user.admission_date || null,
+    terms_accepted_at: user.terms_accepted_at || null,
   };
 
   form.value = {

+ 62 - 0
src/components/TermsAcceptCheckbox.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="terms-accept">
+    <q-checkbox
+      :model-value="modelValue"
+      dense
+      color="violet-normal"
+      @update:model-value="$emit('update:modelValue', $event)"
+    >
+      <span class="terms-text">
+        {{ $t("auth.terms.accept_prefix") }}
+        <a
+          class="terms-link"
+          href="#"
+          @click.stop.prevent="openUrl(PRIVACY_POLICY_URL)"
+        >
+          {{ $t("auth.terms.link") }}
+        </a>
+      </span>
+    </q-checkbox>
+
+    <div v-if="error" class="terms-error text-negative">
+      {{ $t("auth.terms.required") }}
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { PRIVACY_POLICY_URL, openUrl } from "src/helpers/links";
+
+defineProps({
+  modelValue: { type: Boolean, default: false },
+  error: { type: Boolean, default: false },
+});
+
+defineEmits(["update:modelValue"]);
+</script>
+
+<style lang="scss" scoped>
+@use "sass:map";
+@use "src/css/quasar.variables.scss" as *;
+
+.terms-text {
+  color: map.get($colors, "text-2");
+  font-size: 13px;
+}
+
+.terms-link {
+  color: map.get($colors, "violet-normal");
+  font-weight: 600;
+  text-decoration: underline;
+  cursor: pointer;
+
+  &:hover {
+    color: map.get($colors, "violet-normal-hover");
+  }
+}
+
+.terms-error {
+  font-size: 12px;
+  margin-left: 32px;
+}
+</style>

+ 4 - 0
src/helpers/links.js

@@ -0,0 +1,4 @@
+export const PRIVACY_POLICY_URL =
+  "https://politicas.softpar.inf.br/politicas/politicaSerpRatiAssociado.html";
+
+export const openUrl = (url) => window.open(url, "_blank", "noopener");

+ 5 - 2
src/i18n/locales/en.json

@@ -229,8 +229,11 @@
     "logout": "Logout",
     "registration": "Registration",
     "confirm_password": "Confirm Password",
-    "agreed_terms": "I agree with the terms",
-    "agreed_privacy": "I agree with the privacy policy",
+    "terms": {
+      "accept_prefix": "I have read and accept the",
+      "link": "terms and conditions",
+      "required": "You must accept the terms and conditions to complete the registration."
+    },
     "select_type_hint": "Select your profile and sign in",
     "type": {
       "administrador": "Administrator",

+ 5 - 2
src/i18n/locales/es.json

@@ -229,8 +229,11 @@
     "logout": "Cerrar sesión",
     "registration": "Registro",
     "confirm_password": "Confirmar contraseña",
-    "agreed_terms": "Acepto los términos",
-    "agreed_privacy": "Acepto la política de privacidad",
+    "terms": {
+      "accept_prefix": "He leído y acepto los",
+      "link": "términos y condiciones",
+      "required": "Es necesario aceptar los términos y condiciones para completar el registro."
+    },
     "select_type_hint": "Seleccione su perfil e inicie sesión",
     "type": {
       "administrador": "Administrador",

+ 5 - 2
src/i18n/locales/pt.json

@@ -229,8 +229,11 @@
     "logout": "Sair",
     "registration": "Cadastro",
     "confirm_password": "Confirmar Senha",
-    "agreed_terms": "Eu concordo com os termos",
-    "agreed_privacy": "Eu concordo com a política de privacidade",
+    "terms": {
+      "accept_prefix": "Li e aceito os",
+      "link": "termos e condições",
+      "required": "É necessário aceitar os termos e condições para concluir o cadastro."
+    },
     "select_type_hint": "Selecione seu perfil e faça o Login",
     "type": {
       "administrador": "Administrador",

+ 1 - 0
src/layouts/MainLayout.vue

@@ -79,6 +79,7 @@ const toggleLeftDrawer = () => {
 
 const isProfileIncomplete = (user) => {
   return (
+    !user.terms_accepted_at ||
     !user.photo_url ||
     !user.name ||
     !user.cpf ||

+ 1 - 3
src/pages/associado/profile/ProfilePage.vue

@@ -152,9 +152,7 @@ import { useI18n } from "vue-i18n";
 import { userStore } from "src/stores/user";
 import { getDependentsByUser } from "src/api/profile";
 import { uploadMyAvatar, deleteMyAvatar } from "src/api/user";
-
-const PRIVACY_POLICY_URL = "https://politicas.softpar.inf.br/politicas/politicaSerpRatiAssociado.html";
-const openUrl = (url) => window.open(url, "_blank");
+import { PRIVACY_POLICY_URL, openUrl } from "src/helpers/links";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 

+ 18 - 2
src/pages/login/FirstAccessFormPage.vue

@@ -151,6 +151,13 @@
 
           <p class="password-hint">{{ $t("auth.password_hint") }}</p>
 
+          <TermsAcceptCheckbox
+            v-model="termsAccepted"
+            class="q-mt-md"
+            :error="termsError"
+            @update:model-value="termsError = false"
+          />
+
           <q-btn
             class="full-width q-mt-md login-btn"
             color="violet-normal"
@@ -195,6 +202,7 @@ 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";
+import TermsAcceptCheckbox from "src/components/TermsAcceptCheckbox.vue";
 
 const MAX_PHOTO_SIZE_MB = 5;
 const MAX_PHOTO_SIZE_BYTES = MAX_PHOTO_SIZE_MB * 1024 * 1024;
@@ -213,6 +221,8 @@ const lockedFields = ref([]);
 const photoFile = ref(null);
 const photoPreview = ref(null);
 const photoError = ref(null);
+const termsAccepted = ref(false);
+const termsError = ref(false);
 const positionOptions = ref([]);
 const sectorOptions = ref([]);
 
@@ -329,11 +339,16 @@ const onPhotoSelected = async (event) => {
 };
 
 const onSubmit = async () => {
-  if (!photoFile.value && !photoPreview.value) {
+  const missingPhoto = !photoFile.value && !photoPreview.value;
+
+  if (missingPhoto) {
     photoError.value = t("auth.first_access.photo_required");
-    return;
   }
 
+  termsError.value = !termsAccepted.value;
+
+  if (missingPhoto || termsError.value) return;
+
   const password = form.value.password;
 
   const result = await submitForm(() =>
@@ -341,6 +356,7 @@ const onSubmit = async () => {
       ...form.value,
       token: token.value,
       photo: photoFile.value,
+      terms_accepted: 1,
     }),
   );
 

+ 1 - 3
src/pages/login/LoginPage.vue

@@ -94,14 +94,12 @@ import { useAuth } from "src/composables/useAuth";
 import { useRouter } from "vue-router";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { PRIVACY_POLICY_URL, openUrl } from "src/helpers/links";
 
 import Logo from "src/assets/logo_serprati.svg";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
 
-const PRIVACY_POLICY_URL = "https://politicas.softpar.inf.br/politicas/politicaSerpRatiAssociado.html";
-const openUrl = (url) => window.open(url, "_blank");
-
 const router = useRouter();
 const $q = useQuasar();
 const { inputRules } = useInputRules();