Просмотр исходного кода

refactor: rules nas validacoes e composables de submit

Gustavo Mantovani 1 день назад
Родитель
Сommit
426ea398ad

+ 6 - 12
src/components/defaults/DefaultCurrencyInput.vue

@@ -57,18 +57,12 @@ const { options, label, rules, errorMessage } = defineProps({
 const { inputRef, formattedValue, numberValue, setValue } =
   useCurrencyInput(options);
 
-const minRule = inputRules.minValue(0);
-
-const wrapRule = (rule) => {
-  const wrapped = (value) => rule(numberValue.value, value);
-  if (rule?.$id) wrapped.$id = rule.$id;
-  return wrapped;
-};
-
-const finalRules = computed(() => [
-  ...rules.map(wrapRule),
-  () => minRule(numberValue.value),
-]);
+const finalRules = computed(() =>
+  inputRules.forValue(
+    [...rules, inputRules.minValue(0)],
+    () => numberValue.value,
+  ),
+);
 
 watch(
   () => model.value,

+ 0 - 1
src/components/defaults/DefaultSelect.vue

@@ -155,7 +155,6 @@ const required = computed(() =>
 );
 
 const selectAttrs = computed(() => {
-  // eslint-disable-next-line
   const {
     // eslint-disable-next-line no-unused-vars
     class: _,

+ 11 - 0
src/composables/useAuth.js

@@ -1,16 +1,23 @@
 import api from "src/api";
+
 import { permissionStore } from "src/stores/permission";
 import { userStore } from "src/stores/user";
 
 export const useAuth = () => {
   const setAuthDataFromPayload = async (tokens) => {
     const { access_token, user } = tokens;
+
     const store = userStore();
+
     store.setUser(user);
+
     store.accessToken = access_token;
+
     await permissionStore().fetchScopes();
   };
 
+  //
+
   const login = async (email, password) => {
     try {
       const response = await api.post("/login", {
@@ -22,6 +29,7 @@ export const useAuth = () => {
       if (response.status === 200) {
         await setAuthDataFromPayload(response.data.payload);
       }
+
       return response;
     } catch (error) {
       return Promise.reject(error);
@@ -31,6 +39,7 @@ export const useAuth = () => {
   const logout = async () => {
     try {
       const response = await api.post("/logout");
+
       if (response.status === 200) {
         userStore().resetUser();
       }
@@ -42,9 +51,11 @@ export const useAuth = () => {
   const refresh = async () => {
     try {
       const response = await api.post("/refresh");
+
       if (response.status === 200) {
         await setAuthDataFromPayload(response.data.payload);
       }
+
       return response;
     } catch (error) {
       return Promise.reject(error);

+ 5 - 0
src/composables/useForm.js

@@ -77,6 +77,7 @@ const buildFormData = (formData, data, parentKey = "") => {
 
   if (data == null) {
     formData.append(parentKey, null);
+
     return;
   }
 
@@ -84,6 +85,7 @@ const buildFormData = (formData, data, parentKey = "") => {
     data.forEach((value, index) => {
       buildFormData(formData, value, `${parentKey}[${index}]`);
     });
+
     return;
   }
 
@@ -94,8 +96,10 @@ const buildFormData = (formData, data, parentKey = "") => {
   ) {
     Object.keys(data).forEach((key) => {
       const propName = parentKey ? `${parentKey}[${key}]` : key;
+
       buildFormData(formData, data[key], propName);
     });
+
     return;
   }
 
@@ -146,6 +150,7 @@ const diff = (currentObj, baseObj) => {
         !Array.isArray(baseValue)
       ) {
         const nestedChanges = diff(currentValue, baseValue);
+
         if (Object.keys(nestedChanges).length > 0) {
           changes[key] = nestedChanges;
         }

+ 124 - 13
src/composables/useInputRules.js

@@ -1,3 +1,4 @@
+import { format, isValid, parse } from "date-fns";
 import { useI18n } from "vue-i18n";
 
 export const useInputRules = () => {
@@ -19,6 +20,18 @@ export const useInputRules = () => {
   const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
 
   const inputRules = {
+    afterTime: (getStartTime) => (value) => {
+      const startTime =
+        typeof getStartTime === "function" ? getStartTime() : getStartTime;
+
+      return (
+        !hasValue(startTime) ||
+        !hasValue(value) ||
+        value > startTime ||
+        t("validation.rules.after_time")
+      );
+    },
+
     cep: (value) => {
       if (!value) return true;
 
@@ -26,8 +39,35 @@ export const useInputRules = () => {
     },
 
     cnpj: (value) => !value || isValidCNPJ(value) || t("validation.rules.cnpj"),
+
     cpf: (value) => !value || isValidCPF(value) || t("validation.rules.cpf"),
 
+    date: (value) => {
+      if (!hasValue(value)) return true;
+
+      const formats = ["dd/MM/yyyy", "yyyy-MM-dd"];
+
+      const valid = formats.some((dateFormat) => {
+        const parsedDate = parse(value, dateFormat, new Date());
+
+        return (
+          isValid(parsedDate) &&
+          format(parsedDate, dateFormat) === value
+        );
+      });
+
+      return valid || t("validation.rules.date");
+    },
+
+    durationHours: (value) => {
+      const minutes = Number(value) * 60;
+
+      return (
+        (Number.isInteger(minutes) && minutes >= 1 && minutes <= 1440) ||
+        t("validation.rules.duration_hours")
+      );
+    },
+
     email: (value) =>
       !value || emailPattern.test(value) || t("validation.rules.email"),
 
@@ -42,20 +82,51 @@ export const useInputRules = () => {
       );
     },
 
-    min: (min) => (value) =>
-      value.length >= min ||
-      `${t("validation.rules.min")} ${min} ${t("validation.rules.characters")}`,
+    forValue: (rules, getValue) =>
+      rules.map((rule) => {
+        const wrapped = (...args) => rule(getValue(), ...args);
+
+        if (rule?.$id) wrapped.$id = rule.$id;
+
+        return wrapped;
+      }),
+
+    integer: (value) =>
+      !hasValue(value) ||
+      Number.isInteger(Number(value)) ||
+      t("validation.rules.integer"),
+
+    integerRange:
+      (min, max = null) =>
+      (value) => {
+        if (!hasValue(value)) return true;
+
+        const number = Number(value);
+        const withinRange = number >= min && (max == null || number <= max);
+
+        if (Number.isInteger(number) && withinRange) return true;
+
+        return max == null
+          ? t("validation.rules.integer_min", { min })
+          : t("validation.rules.integer_range", { min, max });
+      },
 
     max: (max) => (value) =>
-      value.length <= max ||
+      !hasValue(value) ||
+      String(value).length <= max ||
       `${t("validation.rules.max")} ${max} ${t("validation.rules.characters")}`,
 
-    minValue: (min) => (value) =>
-      value >= min || `${t("validation.rules.min")} ${min}`,
-
     maxValue: (max) => (value) =>
       value <= max || `${t("validation.rules.max")} ${max}`,
 
+    min: (min) => (value) =>
+      !hasValue(value) ||
+      String(value).length >= min ||
+      `${t("validation.rules.min")} ${min} ${t("validation.rules.characters")}`,
+
+    minValue: (min) => (value) =>
+      value >= min || `${t("validation.rules.min")} ${min}`,
+
     notSameDocument: (allDocuments) => (value) => {
       if (!value) return true;
 
@@ -74,20 +145,56 @@ export const useInputRules = () => {
       return true;
     },
 
+    oneOf: (allowedValues) => (value) =>
+      !hasValue(value) ||
+      allowedValues.includes(value) ||
+      t("validation.rules.one_of"),
+
     password: (value) =>
       !value || passwordPattern.test(value) || t("validation.rules.password"),
 
+    positive: (value) =>
+      Number(value) > 0 || t("validation.rules.positive"),
+
     samePassword: (otherValue) => (value) =>
       value === otherValue || t("validation.rules.same_password"),
 
+    slug: (value) =>
+      !hasValue(value) ||
+      /^[A-Z0-9_]+$/.test(value) ||
+      t("validation.rules.slug"),
+
+    time: (value) => {
+      if (!hasValue(value)) return true;
+
+      const match = /^(\d{2}):(\d{2})$/.exec(value);
+
+      return (
+        (!!match && Number(match[1]) <= 23 && Number(match[2]) <= 59) ||
+        t("validation.rules.time")
+      );
+    },
+
+    whenValue: (getDependency, rule) => (value) =>
+      !hasValue(getDependency()) || rule(value),
+
     //
 
     required: (value) => hasValue(value) || t("validation.rules.required"),
-    requiredHideMessage: (value) => !!value,
-    requiredNumber: (value) => !isNaN(value) || t("validation.rules.required"),
+
+    requiredHideMessage: (value) => hasValue(value),
+
+    requiredNumber: (value) =>
+      (value !== null &&
+        value !== undefined &&
+        value !== "" &&
+        !Number.isNaN(Number(value))) ||
+      t("validation.rules.required"),
   };
 
   inputRules.required.$id = "required";
+  inputRules.requiredHideMessage.$id = "required";
+  inputRules.requiredNumber.$id = "required";
 
   return {
     inputRules,
@@ -134,7 +241,7 @@ const isValidCNPJ = (cnpj) => {
   if (result !== parseInt(digits.charAt(1))) return false;
 
   return true;
-}
+};
 
 const isValidCPF = (cpf) => {
   if (!cpf) return false;
@@ -146,7 +253,9 @@ const isValidCPF = (cpf) => {
 
   let sum = 0;
 
-  for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i);
+  for (let i = 0; i < 9; i++) {
+    sum += parseInt(cpf.charAt(i)) * (10 - i);
+  }
 
   let rev = 11 - (sum % 11);
 
@@ -155,7 +264,9 @@ const isValidCPF = (cpf) => {
 
   sum = 0;
 
-  for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i);
+  for (let i = 0; i < 10; i++) {
+    sum += parseInt(cpf.charAt(i)) * (11 - i);
+  }
 
   rev = 11 - (sum % 11);
 
@@ -163,4 +274,4 @@ const isValidCPF = (cpf) => {
   if (rev !== parseInt(cpf.charAt(10))) return false;
 
   return true;
-}
+};

+ 9 - 0
src/i18n/locales/en.json

@@ -176,6 +176,7 @@
       "required": "This field is required",
       "email": "This field must be a valid email | These fields must be valid emails",
       "date": "This field must be a valid date",
+      "duration_hours": "Enter a valid duration between 1 minute and 24 hours",
       "min": "This field must have at least",
       "max": "This field must have at most",
       "characters": "characters",
@@ -185,6 +186,14 @@
       "cpf": "This field must be a valid CPF",
       "cnpj": "This field must be a valid CNPJ",
       "cep": "This field must be a valid ZIP code",
+      "positive": "Enter a value greater than zero",
+      "integer": "Enter a valid integer",
+      "integer_min": "Enter an integer greater than or equal to {min}",
+      "integer_range": "Enter an integer between {min} and {max}",
+      "time": "Enter a valid time",
+      "after_time": "The end time must be after the start time",
+      "one_of": "Select a valid option",
+      "slug": "Use only uppercase letters, numbers, and underscores",
       "value_smaller_than_zero": "Value cannot be less than zero"
     },
     "permissions": {

+ 9 - 0
src/i18n/locales/es.json

@@ -176,6 +176,7 @@
       "required": "Este campo es obligatorio",
       "email": "Este campo debe ser un correo electrónico válido | Estos campos deben ser correos electrónicos válidos",
       "date": "Este campo debe ser una fecha válida",
+      "duration_hours": "Ingrese una duración válida entre 1 minuto y 24 horas",
       "min": "Este campo debe tener al menos",
       "max": "Este campo debe tener como máximo",
       "characters": "caracteres",
@@ -185,6 +186,14 @@
       "cpf": "Este campo debe ser un CPF válido",
       "cnpj": "Este campo debe ser un CNPJ válido",
       "cep": "Este campo debe ser un código postal válido",
+      "positive": "Ingrese un valor mayor que cero",
+      "integer": "Ingrese un número entero válido",
+      "integer_min": "Ingrese un número entero mayor o igual a {min}",
+      "integer_range": "Ingrese un número entero entre {min} y {max}",
+      "time": "Ingrese una hora válida",
+      "after_time": "La hora de finalización debe ser posterior a la hora de inicio",
+      "one_of": "Seleccione una opción válida",
+      "slug": "Use solo letras mayúsculas, números y guiones bajos",
       "value_smaller_than_zero": "El valor no puede ser menor que cero"
     },
     "permissions": {

+ 9 - 0
src/i18n/locales/pt.json

@@ -176,6 +176,7 @@
       "required": "Este campo é obrigatório",
       "email": "Este campo deve ser um e-mail válido | Estes campos devem ser e-mails válidos",
       "date": "Este campo deve ser uma data válida",
+      "duration_hours": "Informe uma duração válida entre 1 minuto e 24 horas",
       "min": "Este campo deve ter no mínimo",
       "max": "Este campo deve ter no máximo",
       "characters": "caracteres",
@@ -185,6 +186,14 @@
       "cpf": "Este campo deve ser um CPF válido",
       "cnpj": "Este campo deve ser um CNPJ válido",
       "cep": "Este campo deve ser um CEP válido",
+      "positive": "Informe um valor maior que zero",
+      "integer": "Informe um número inteiro válido",
+      "integer_min": "Informe um número inteiro maior ou igual a {min}",
+      "integer_range": "Informe um número inteiro entre {min} e {max}",
+      "time": "Informe um horário válido",
+      "after_time": "O horário final deve ser após o inicial",
+      "one_of": "Selecione uma opção válida",
+      "slug": "Use apenas letras maiúsculas, números e underline",
       "value_smaller_than_zero": "O valor não pode ser menor que zero"
     },
     "permissions": {

+ 11 - 54
src/pages/classes/components/AddEditClassDialog.vue

@@ -19,7 +19,7 @@
                   v-model:error="validationErrors.title"
                   class="col-12"
                   label="Nome da Atividade"
-                  :rules="titleRules"
+                  :rules="[inputRules.required, inputRules.max(255)]"
                 />
 
                 <DefaultSelect
@@ -33,7 +33,7 @@
                   option-value="id"
                   :loading="loadingPackages"
                   :options="packageOptions"
-                  :rules="packageRules"
+                  :rules="[inputRules.required, inputRules.integer]"
                 />
 
                 <DefaultInput
@@ -43,7 +43,7 @@
                   label="Hora de Início"
                   mask="##:##"
                   placeholder="08:40"
-                  :rules="startTimeRules"
+                  :rules="[inputRules.required, inputRules.time]"
                 >
                   <template #append>
                     <q-icon
@@ -81,7 +81,11 @@
                   label="Hora de Fim"
                   mask="##:##"
                   placeholder="09:40"
-                  :rules="endTimeRules"
+                  :rules="[
+                    inputRules.required,
+                    inputRules.time,
+                    inputRules.afterTime(() => form.start_time),
+                  ]"
                 >
                   <template #append>
                     <q-icon
@@ -120,7 +124,7 @@
                   option-value="name"
                   :loading="loadingInstructors"
                   :options="instructorOptions"
-                  :rules="[optionalMaxLengthRule(255)]"
+                  :rules="[inputRules.max(255)]"
                 />
 
                 <DefaultInput
@@ -128,7 +132,7 @@
                   v-model:error="validationErrors.room"
                   class="col-6"
                   label="Número da Sala"
-                  :rules="[optionalMaxLengthRule(255)]"
+                  :rules="[inputRules.max(255)]"
                 />
 
                 <DefaultInputDatePicker
@@ -137,7 +141,7 @@
                   v-model:untreated-date="form.date"
                   class="col-12"
                   label="Data de Início"
-                  :rules="dateRules"
+                  :rules="[inputRules.required, inputRules.date]"
                 />
               </div>
             </q-card-section>
@@ -170,7 +174,6 @@
 import { createClass, updateClass } from "src/api/class";
 import { getUnitPackagesForSelect } from "src/api/package";
 import { getUsersByUnit } from "src/api/user";
-import { format, isValid, parse } from "date-fns";
 import { onMounted, ref, useTemplateRef, watch } from "vue";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useForm } from "src/composables/useForm";
@@ -224,52 +227,6 @@ const { form, getUpdatedFields } = useForm({
   ),
 });
 
-const emptyOptionalValue = (value) => value == null || value === "";
-
-const endAfterStartRule = (value) =>
-  !form.start_time ||
-  !value ||
-  value > form.start_time ||
-  "Fim deve ser após o início.";
-
-const integerRule = (value) =>
-  emptyOptionalValue(value) ||
-  Number.isInteger(Number(value)) ||
-  "Informe um número inteiro válido.";
-
-const optionalMaxLengthRule = (max) => (value) =>
-  emptyOptionalValue(value) ||
-  String(value).length <= max ||
-  `Informe no máximo ${max} caracteres.`;
-
-const validDateRule = (value) => {
-  if (emptyOptionalValue(value)) return true;
-
-  const parsedDate = parse(value, "dd/MM/yyyy", new Date());
-
-  return (
-    (isValid(parsedDate) && format(parsedDate, "dd/MM/yyyy") === value) ||
-    "Informe uma data válida."
-  );
-};
-
-const validTimeRule = (value) => {
-  if (emptyOptionalValue(value)) return true;
-
-  const match = /^(\d{2}):(\d{2})$/.exec(value);
-
-  return (
-    (!!match && Number(match[1]) <= 23 && Number(match[2]) <= 59) ||
-    "Informe um horário válido."
-  );
-};
-
-const titleRules = [inputRules.required, optionalMaxLengthRule(255)];
-const packageRules = [inputRules.required, integerRule];
-const startTimeRules = [inputRules.required, validTimeRule];
-const endTimeRules = [inputRules.required, validTimeRule, endAfterStartRule];
-const dateRules = [inputRules.required, validDateRule];
-
 const trimTime = (time) => (time ? time.slice(0, 5) : "");
 
 const applyPackageSchedule = () => {

+ 2 - 5
src/pages/financial/AccountsPayablePage.vue

@@ -215,7 +215,7 @@
                   v-model:error="createValidationErrors.value"
                   label="Valor"
                   outlined
-                  :rules="[inputRules.required, positiveAmountRule]"
+                  :rules="[inputRules.required, inputRules.positive]"
                 />
 
                 <DefaultInputDatePicker
@@ -503,8 +503,5 @@ const openSettle = (row) => {
   settleDialog.value = true;
 };
 
-const positiveAmountRule = (value) =>
-  Number(value) > 0 || "Informe um valor maior que zero.";
-
 onMounted(loadData);
-</script>
+</script>

+ 2 - 5
src/pages/financial/AccountsReceivablePage.vue

@@ -149,7 +149,7 @@
                   v-model:error="createValidationErrors.value"
                   label="Valor"
                   outlined
-                  :rules="[inputRules.required, positiveAmountRule]"
+                  :rules="[inputRules.required, inputRules.positive]"
                 />
 
                 <DefaultInputDatePicker
@@ -628,8 +628,5 @@ const openSettle = (row) => {
   settleDialog.value = true;
 };
 
-const positiveAmountRule = (value) =>
-  Number(value) > 0 || "Informe um valor maior que zero.";
-
 onMounted(loadData);
-</script>
+</script>

+ 5 - 10
src/pages/financial/components/AddEditTreasuryAccountDialog.vue

@@ -21,7 +21,7 @@
               class="col-12"
               label="Nome (ex.: Banco do Brasil, Caixa Loja)"
               outlined
-              :rules="[inputRules.required, optionalMaxLengthRule(255)]"
+              :rules="[inputRules.required, inputRules.max(255)]"
             />
 
             <DefaultInput
@@ -30,7 +30,7 @@
               class="col-12 col-sm-6"
               label="Banco"
               outlined
-              :rules="[optionalMaxLengthRule(255)]"
+              :rules="[inputRules.max(255)]"
             />
 
             <DefaultInput
@@ -39,7 +39,7 @@
               class="col-12 col-sm-6"
               label="Agência"
               outlined
-              :rules="[optionalMaxLengthRule(50)]"
+              :rules="[inputRules.max(50)]"
             />
 
             <DefaultInput
@@ -48,7 +48,7 @@
               class="col-12 col-sm-6"
               label="Conta"
               outlined
-              :rules="[optionalMaxLengthRule(50)]"
+              :rules="[inputRules.max(50)]"
             />
 
             <DefaultSelect
@@ -152,11 +152,6 @@ const { form, getUpdatedFields } = useForm({
   treasury_type: "bank",
 });
 
-const optionalMaxLengthRule = (max) => (value) =>
-  !value ||
-  String(value).length <= max ||
-  `Informe no máximo ${max} caracteres.`;
-
 const {
   loading,
   validationErrors,
@@ -189,4 +184,4 @@ const onOKClick = async () => {
   width: 520px;
   max-width: calc(100vw - 32px);
 }
-</style>
+</style>

+ 3 - 10
src/pages/financial/components/AddTreasuryLaunchDialog.vue

@@ -33,7 +33,7 @@
               class="col-12"
               label="Descrição"
               outlined
-              :rules="[inputRules.required, maxLengthRule(1000)]"
+              :rules="[inputRules.required, inputRules.max(1000)]"
             />
 
             <DefaultCurrencyInput
@@ -42,7 +42,7 @@
               class="col-12 col-sm-6"
               label="Valor"
               outlined
-              :rules="[inputRules.required, positiveAmountRule]"
+              :rules="[inputRules.required, inputRules.positive]"
             />
 
             <DefaultInputDatePicker
@@ -147,13 +147,6 @@ const { form, getUpdatedFields } = useForm({
   transaction_type: props.launch?.transaction_type ?? "entrada",
 });
 
-const maxLengthRule = (max) => (value) =>
-  String(value ?? "").length <= max ||
-  `Informe no máximo ${max} caracteres.`;
-
-const positiveAmountRule = (value) =>
-  Number(value) > 0 || "Informe um valor maior que zero.";
-
 const {
   loading,
   validationErrors,
@@ -216,4 +209,4 @@ const onOKClick = async () => {
   width: 480px;
   max-width: calc(100vw - 32px);
 }
-</style>
+</style>

+ 4 - 1
src/pages/forgot-password/NewPasswordStep.vue

@@ -33,7 +33,10 @@
         v-model="passwordConfirmation"
         label="Confirmar nova senha"
         lazy-rules
-        :rules="[inputRules.required, (v) => v === password || 'As senhas não conferem']"
+        :rules="[
+          inputRules.required,
+          inputRules.samePassword(password),
+        ]"
       />
 
       <q-btn

+ 0 - 192
src/pages/packages/components/AddEditPackageDialog.vue

@@ -58,80 +58,11 @@
                   necessário. É possível configurar no máximo dois dias.
                 </div>
 
-                <DefaultSelect
-                  v-model="form.weekday"
-                  class="col-4"
-                  clearable
-                  emit-value
-                  label="1º Dia da Semana"
-                  map-options
-                  option-label="label"
-                  option-value="value"
-                  :options="weekdays"
-                  :rules="form.start_time ? [inputRules.required] : []"
-                />
-
-                <DefaultInput
-                  v-model="form.start_time"
-                  class="col-4"
-                  label="Horário Inicial"
-                  mask="##:##"
-                  :rules="
-                    form.weekday !== null
-                      ? [inputRules.required]
-                      : []
-                  "
-                />
-
-                <DefaultInput
-                  :model-value="firstEndTime"
-                  class="col-4"
-                  disable
-                  label="Horário Final"
-                />
-
-                <DefaultSelect
-                  v-model="form.second_weekday"
-                  class="col-4"
-                  clearable
-                  emit-value
-                  label="2º Dia da Semana"
-                  map-options
-                  option-label="label"
-                  option-value="value"
-                  :options="secondWeekdayOptions"
-                  :rules="
-                    form.second_start_time
-                      ? [inputRules.required]
-                      : []
-                  "
-                />
-
-                <DefaultInput
-                  v-model="form.second_start_time"
-                  class="col-4"
-                  label="Horário Inicial do 2º dia"
-                  mask="##:##"
-                  :rules="
-                    form.second_weekday !== null
-                      ? [inputRules.required]
-                      : []
-                  "
-                />
-
-                <DefaultInput
-                  :model-value="secondEndTime"
-                  class="col-4"
-                  disable
-                  label="Horário Final do 2º dia"
-                />
-
                 <DefaultCurrencyInput
                   v-model="form.contract_register_value"
                   v-model:error="validationErrors.contract_register_value"
                   class="col-4"
                   label="R$ Matrícula"
-                  :rules="[inputRules.required]"
                 />
 
                 <DefaultCurrencyInput
@@ -139,7 +70,6 @@
                   v-model:error="validationErrors.contract_value"
                   class="col-4"
                   label="R$ Total do Contrato"
-                  :rules="[inputRules.required]"
                 />
 
                 <DefaultInput
@@ -261,7 +191,6 @@ import {
   onMounted,
   ref,
   useTemplateRef,
-  watch,
 } from "vue";
 
 import {
@@ -303,13 +232,6 @@ const scrollAreaRef = useTemplateRef("scrollAreaRef");
 
 const products = ref([]);
 
-const firstEndTime = computed(() =>
-  calculateEndTime(
-    form.start_time,
-    form.class_duration_hours,
-  ),
-);
-
 const productOptions = computed(() =>
   products.value.map((product) => ({
     label: product.name,
@@ -318,53 +240,6 @@ const productOptions = computed(() =>
   })),
 );
 
-const secondEndTime = computed(() =>
-  calculateEndTime(
-    form.second_start_time,
-    form.class_duration_hours,
-  ),
-);
-
-const secondWeekdayOptions = computed(() =>
-  weekdays.filter(
-    (weekday) => weekday.value !== form.weekday,
-  ),
-);
-
-const weekdays = [
-  {
-    label: "Segunda",
-    value: 1,
-  },
-  {
-    label: "Terça",
-    value: 2,
-  },
-  {
-    label: "Quarta",
-    value: 3,
-  },
-  {
-    label: "Quinta",
-    value: 4,
-  },
-  {
-    label: "Sexta",
-    value: 5,
-  },
-  {
-    label: "Sábado",
-    value: 6,
-  },
-  {
-    label: "Domingo",
-    value: 0,
-  },
-];
-
-const trimTime = (time) =>
-  time ? time.slice(0, 5) : null;
-
 const { form, getUpdatedFields } = useForm({
   class_duration_hours:
     (props.package?.class_duration_minutes ?? 120) / 60,
@@ -387,14 +262,6 @@ const { form, getUpdatedFields } = useForm({
 
   name: props.package?.name ?? null,
   quantity_classes: props.package?.quantity_classes ?? null,
-
-  second_start_time: trimTime(
-    props.package?.second_start_time,
-  ),
-
-  second_weekday: props.package?.second_weekday ?? null,
-  start_time: trimTime(props.package?.start_time),
-  weekday: props.package?.weekday ?? null,
 });
 
 const {
@@ -418,34 +285,6 @@ const addMaterial = () => {
   });
 };
 
-const calculateEndTime = (startTime, durationHours) => {
-  if (!/^\d{2}:\d{2}$/.test(startTime ?? "")) {
-    return null;
-  }
-
-  const [hours, minutes] = startTime
-    .split(":")
-    .map(Number);
-
-  if (hours > 23 || minutes > 59) {
-    return null;
-  }
-
-  const durationMinutes = Math.round(
-    Number(durationHours) * 60,
-  );
-
-  const endMinutes =
-    (hours * 60 + minutes + durationMinutes) %
-    (24 * 60);
-
-  return `${String(
-    Math.floor(endMinutes / 60),
-  ).padStart(2, "0")}:${String(
-    endMinutes % 60,
-  ).padStart(2, "0")}`;
-};
-
 const onOKClick = async () => {
   const validMaterials = form.materials.filter(
     (material) => material.product_id,
@@ -472,13 +311,6 @@ const onOKClick = async () => {
 
     name: form.name,
     quantity_classes: form.quantity_classes,
-
-    second_start_time:
-      form.second_start_time || null,
-
-    second_weekday: form.second_weekday,
-    start_time: form.start_time || null,
-    weekday: form.weekday,
   };
 
   await execute(() => {
@@ -495,10 +327,6 @@ const onOKClick = async () => {
       "contract_value",
       "contract_register_value",
       "contrat_discount_value",
-      "weekday",
-      "start_time",
-      "second_weekday",
-      "second_start_time",
     ];
 
     for (const key of directKeys) {
@@ -537,15 +365,6 @@ const removeMaterial = (index) => {
   form.materials.splice(index, 1);
 };
 
-watch(
-  () => form.weekday,
-  (weekday) => {
-    if (form.second_weekday === weekday) {
-      form.second_weekday = null;
-    }
-  },
-);
-
 onMounted(async () => {
   const productsResponse = await getProductsForSelect();
 
@@ -560,17 +379,6 @@ onMounted(async () => {
   form.class_duration_hours =
     (packageData.class_duration_minutes ?? 120) / 60;
 
-  form.second_start_time = trimTime(
-    packageData.second_start_time,
-  );
-
-  form.second_weekday =
-    packageData.second_weekday ?? null;
-
-  form.start_time = trimTime(packageData.start_time);
-
-  form.weekday = packageData.weekday ?? null;
-
   if (packageData.materials?.length) {
     form.materials = packageData.materials.map(
       (material) => ({

+ 2 - 6
src/pages/permissions/components/AddEditPermissionGroupDialog.vue

@@ -34,7 +34,7 @@
             label="Código"
             :error="!!validationErrors.slug"
             :error-message="validationErrors.slug"
-            :rules="[inputRules.required, slugRule]"
+            :rules="[inputRules.required, inputRules.slug]"
             @update:model-value="onSlugTouched"
           />
         </q-card-section>
@@ -135,10 +135,6 @@ const onSlugTouched = (value) => {
   form.slug = toSlug(value);
 };
 
-const slugRule = (value) =>
-  /^[A-Z0-9_]+$/.test(value ?? "") ||
-  "Use apenas letras maiúsculas, números e underline.";
-
 const toSlug = (value) =>
   (value ?? "")
     .normalize("NFD")
@@ -152,4 +148,4 @@ const toSlug = (value) =>
 .permission-group-fields {
   min-height: 220px;
 }
-</style>
+</style>

+ 28 - 103
src/pages/students/components/AddEditContractDialog.vue

@@ -81,7 +81,7 @@
                   v-if="props.selectStudent"
                   class="col-12"
                 >
-                  <div class="row items-start no-wrap q-col-gutter-sm">
+                  <div class="row items-start no-wrap q-col-gutter-sm" @keydown.enter.prevent>
                     <DefaultSelect
                       v-model="selectedStudent"
                       v-model:error="validationErrors.student_id"
@@ -151,7 +151,7 @@
                     v-model="form.protocol"
                     v-model:error="validationErrors.protocol"
                     label="Protocolo"
-                    :rules="[optionalMaxLengthRule(255)]"
+                    :rules="[inputRules.max(255)]"
                   />
                 </div>
 
@@ -160,7 +160,7 @@
                     v-model="form.signature_date"
                     v-model:error="validationErrors.signature_date"
                     label="Data Assinatura"
-                    :rules="[optionalDateRule]"
+                    :rules="[inputRules.date]"
                   />
                 </div>
 
@@ -169,7 +169,7 @@
                     v-model="form.end_date"
                     v-model:error="validationErrors.end_date"
                     label="Data Encerramento"
-                    :rules="[optionalDateRule]"
+                    :rules="[inputRules.date]"
                   />
                 </div>
 
@@ -183,6 +183,7 @@
                     option-label="name"
                     option-value="id"
                     :options="packages"
+                    :rules="[inputRules.required]"
                   />
                 </div>
 
@@ -193,7 +194,7 @@
                     disable
                     label="Qtd. Aulas"
                     type="number"
-                    :rules="[optionalIntegerRangeRule(1)]"
+                    :rules="[inputRules.integerRange(1)]"
                   />
                 </div>
 
@@ -207,7 +208,7 @@
                     option-label="label"
                     option-value="value"
                     :options="weekdays"
-                    :rules="[optionalIntegerRangeRule(0, 6)]"
+                    :rules="[inputRules.integerRange(0, 6)]"
                   />
                 </div>
 
@@ -217,7 +218,7 @@
                     v-model:error="validationErrors.start_time"
                     label="Hora de Início"
                     mask="##:##"
-                    :rules="[optionalTimeRule]"
+                    :rules="[inputRules.time]"
                   >
                     <template #append>
                       <q-icon name="mdi-clock-outline" />
@@ -231,7 +232,7 @@
                     v-model:error="validationErrors.end_time"
                     label="Hora de Término"
                     mask="##:##"
-                    :rules="[optionalTimeRule]"
+                    :rules="[inputRules.time]"
                   >
                     <template #append>
                       <q-icon name="mdi-clock-outline" />
@@ -249,7 +250,7 @@
                     option-label="label"
                     option-value="value"
                     :options="weekdays"
-                    :rules="[optionalIntegerRangeRule(0, 6)]"
+                    :rules="[inputRules.integerRange(0, 6)]"
                   />
                 </div>
 
@@ -259,7 +260,7 @@
                     v-model:error="validationErrors.second_start_time"
                     label="Hora de Início"
                     mask="##:##"
-                    :rules="[optionalTimeRule]"
+                    :rules="[inputRules.time]"
                   >
                     <template #append>
                       <q-icon name="mdi-clock-outline" />
@@ -273,7 +274,7 @@
                     v-model:error="validationErrors.second_end_time"
                     label="Hora de Término"
                     mask="##:##"
-                    :rules="[optionalTimeRule]"
+                    :rules="[inputRules.time]"
                   >
                     <template #append>
                       <q-icon name="mdi-clock-outline" />
@@ -293,7 +294,7 @@
                     v-model:error="validationErrors.due_day"
                     label="Dia de Vencimento"
                     type="number"
-                    :rules="[optionalIntegerRangeRule(1, 31)]"
+                    :rules="[inputRules.integerRange(1, 31)]"
                   />
                 </div>
               </div>
@@ -322,7 +323,7 @@
                     option-label="label"
                     option-value="value"
                     :options="enrollmentInstallmentOptions"
-                    :rules="[optionalIntegerRangeRule(1, 12)]"
+                    :rules="[inputRules.integerRange(1, 12)]"
                   />
                 </div>
 
@@ -340,7 +341,7 @@
                     v-model="form.enrollment_due_date"
                     v-model:error="validationErrors.enrollment_due_date"
                     label="Data Vencimento"
-                    :rules="[optionalDateRule]"
+                    :rules="[inputRules.date]"
                   />
                 </div>
               </div>
@@ -369,7 +370,7 @@
                     option-label="label"
                     option-value="value"
                     :options="packageInstallmentOptions"
-                    :rules="[optionalIntegerRangeRule(1, 13)]"
+                    :rules="[inputRules.integerRange(1, 13)]"
                   />
                 </div>
 
@@ -387,7 +388,7 @@
                     v-model="form.package_due_date"
                     v-model:error="validationErrors.package_due_date"
                     label="Data 1ª Parcela"
-                    :rules="[optionalDateRule]"
+                    :rules="[inputRules.date]"
                   />
                 </div>
               </div>
@@ -399,7 +400,10 @@
                     v-model:error="validationErrors.early_payment_discount"
                     label="Desconto até o vencimento (%)"
                     type="number"
-                    :rules="[optionalNumberRangeRule(0, 100)]"
+                    :rules="[
+                      inputRules.minValue(0),
+                      inputRules.maxValue(100),
+                    ]"
                   />
                 </div>
 
@@ -414,7 +418,7 @@
                     option-value="value"
                     :options="paymentMethods"
                     :rules="[
-                      optionalOneOfRule([
+                      inputRules.oneOf([
                         'pix',
                         'credit_card',
                         'debit_card',
@@ -429,7 +433,7 @@
                     v-model:error="validationErrors.interest_rate"
                     label="Juros (%) a.m"
                     type="number"
-                    :rules="[optionalNumberRangeRule(0)]"
+                    :rules="[inputRules.minValue(0)]"
                   />
                 </div>
 
@@ -439,7 +443,10 @@
                     v-model:error="validationErrors.fine_cancelled"
                     label="Multa (%)"
                     type="number"
-                    :rules="[optionalNumberRangeRule(0, 100)]"
+                    :rules="[
+                      inputRules.minValue(0),
+                      inputRules.maxValue(100),
+                    ]"
                   />
                 </div>
               </div>
@@ -514,7 +521,7 @@
 </template>
 
 <script setup>
-import { addYears, format, isValid, parse } from "date-fns";
+import { addYears, format } from "date-fns";
 
 import {
   createStudentContract,
@@ -919,9 +926,6 @@ const calculateEndTime = (
   ).padStart(2, "0")}`;
 };
 
-const emptyOptionalValue = (value) =>
-  value == null || value === "";
-
 const fetchMedias = async () => {
   if (!props.contract) return;
 
@@ -1075,85 +1079,6 @@ const openFile = (url) => {
   window.open(url, "_blank");
 };
 
-const optionalDateRule = (value) => {
-  if (emptyOptionalValue(value)) return true;
-
-  const parsedDate = parse(
-    value,
-    "dd/MM/yyyy",
-    new Date(),
-  );
-
-  return (
-    (isValid(parsedDate) &&
-      format(parsedDate, "dd/MM/yyyy") === value) ||
-    "Informe uma data válida."
-  );
-};
-
-const optionalIntegerRangeRule =
-  (min, max = null) =>
-  (value) => {
-    if (emptyOptionalValue(value)) return true;
-
-    const number = Number(value);
-
-    const withinRange =
-      number >= min &&
-      (max == null || number <= max);
-
-    if (Number.isInteger(number) && withinRange) {
-      return true;
-    }
-
-    return max == null
-      ? `Informe um número inteiro maior ou igual a ${min}.`
-      : `Informe um número inteiro entre ${min} e ${max}.`;
-  };
-
-const optionalMaxLengthRule = (max) => (value) =>
-  emptyOptionalValue(value) ||
-  String(value).length <= max ||
-  `Informe no máximo ${max} caracteres.`;
-
-const optionalNumberRangeRule =
-  (min, max = null) =>
-  (value) => {
-    if (emptyOptionalValue(value)) return true;
-
-    const number = Number(value);
-
-    const withinRange =
-      number >= min &&
-      (max == null || number <= max);
-
-    if (Number.isFinite(number) && withinRange) {
-      return true;
-    }
-
-    return max == null
-      ? `Informe um valor maior ou igual a ${min}.`
-      : `Informe um valor entre ${min} e ${max}.`;
-  };
-
-const optionalOneOfRule = (allowedValues) => (value) =>
-  emptyOptionalValue(value) ||
-  allowedValues.includes(value) ||
-  "Selecione uma opção válida.";
-
-const optionalTimeRule = (value) => {
-  if (emptyOptionalValue(value)) return true;
-
-  const match = /^(\d{2}):(\d{2})$/.exec(value);
-
-  return (
-    (!!match &&
-      Number(match[1]) <= 23 &&
-      Number(match[2]) <= 59) ||
-    "Informe um horário válido."
-  );
-};
-
 const restoreDefaultContractDates = () => {
   restoringDefaultDates.value = true;
 

+ 2 - 7
src/pages/users/UserActionPage.vue

@@ -87,12 +87,7 @@
             :error-message="validationErrors.password"
             :rules="
               isEdit
-                ? [
-                    (value) =>
-                      !value ||
-                      value.length >= 8 ||
-                      'Mínimo 8 caracteres',
-                  ]
+                ? [inputRules.min(8)]
                 : [
                     inputRules.required,
                     inputRules.min(8),
@@ -370,4 +365,4 @@ onMounted(async () => {
     );
   }
 });
-</script>
+</script>