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

fix fluxo primeiro acesso + version 1.0.6

Gustavo Zanatta 16 часов назад
Родитель
Сommit
31aa85f20f

+ 2 - 2
src-capacitor/android/app/build.gradle

@@ -11,8 +11,8 @@ android {
         applicationId "br.com.serprati.associado"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 6
-        versionName "1.0.5"
+        versionCode 7
+        versionName "1.0.6"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 1 - 1
src-capacitor/www/index.html

@@ -1,3 +1,3 @@
-<!DOCTYPE html><html><head><title>SerPrati-Digital-App</title><meta charset=utf-8><meta name=description content="App Associado SerPrati"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width,viewport-fit=cover"><link rel=icon type=image/png sizes=128x128 href=icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=icons/favicon-16x16.png><link rel=icon type=image/ico href=favicon.ico>  <script type="module" crossorigin src="./assets/index-CVbq7qsp.js"></script>
+<!DOCTYPE html><html><head><title>SerPrati-Digital-App</title><meta charset=utf-8><meta name=description content="App Associado SerPrati"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width,viewport-fit=cover"><link rel=icon type=image/png sizes=128x128 href=icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=icons/favicon-16x16.png><link rel=icon type=image/ico href=favicon.ico>  <script type="module" crossorigin src="./assets/index-KiNnXcNo.js"></script>
   <link rel="stylesheet" crossorigin href="./assets/index-C8WB2mbd.css">
 </head><body><div id=q-app></div></body></html>

+ 56 - 0
src/helpers/compressImage.js

@@ -0,0 +1,56 @@
+
+const compressImage = (file, { maxSize = 1024, quality = 0.8 } = {}) =>
+  new Promise((resolve) => {
+    if (!file?.type?.startsWith("image/")) {
+      resolve(file);
+      return;
+    }
+
+    const objectUrl = URL.createObjectURL(file);
+    const image = new Image();
+
+    const finish = (result) => {
+      URL.revokeObjectURL(objectUrl);
+      resolve(result);
+    };
+
+    image.onerror = () => finish(file);
+
+    image.onload = () => {
+      const scale = Math.min(1, maxSize / Math.max(image.width, image.height));
+      const canvas = document.createElement("canvas");
+      canvas.width = Math.round(image.width * scale);
+      canvas.height = Math.round(image.height * scale);
+
+      const context = canvas.getContext("2d");
+      if (!context) {
+        finish(file);
+        return;
+      }
+
+      context.drawImage(image, 0, 0, canvas.width, canvas.height);
+
+      canvas.toBlob(
+        (blob) => {
+          if (!blob || blob.size >= file.size) {
+            finish(file);
+            return;
+          }
+
+          const name = file.name.replace(/\.[^.]+$/, "") || "photo";
+          finish(
+            new File([blob], `${name}.jpg`, {
+              type: "image/jpeg",
+              lastModified: Date.now(),
+            }),
+          );
+        },
+        "image/jpeg",
+        quality,
+      );
+    };
+
+    image.src = objectUrl;
+  });
+
+export default compressImage;

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

@@ -271,6 +271,7 @@
       "whatsapp": "WhatsApp",
       "photo_hint": "Tap the icon to add your photo",
       "photo_required": "The photo is required",
+      "photo_too_large": "Photo is too large ({size} MB). The limit is {max} MB — take another photo or choose a smaller image.",
       "finish": "Finish Registration",
       "success": "Registration completed successfully!",
       "success_login": "Registration completed! Sign in to access the system.",
@@ -315,6 +316,8 @@
     "errors": {
       "404": "Page not found",
       "failed": "The action failed",
+      "unexpected": "Could not complete: {detail}",
+      "no_connection": "No connection to the server: {detail}",
       "no_records_found": "No records found"
     },
     "success": "The action was successful"

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

@@ -271,6 +271,7 @@
       "whatsapp": "WhatsApp",
       "photo_hint": "Toque el ícono para agregar su foto",
       "photo_required": "La foto es obligatoria",
+      "photo_too_large": "Foto demasiado grande ({size} MB). El límite es {max} MB — tome otra foto o elija una imagen más pequeña.",
       "finish": "Finalizar Registro",
       "success": "¡Registro completado con éxito!",
       "success_login": "¡Registro completado! Inicie sesión para acceder al sistema.",
@@ -315,6 +316,8 @@
     "errors": {
       "404": "Página no encontrada",
       "failed": "La acción falló",
+      "unexpected": "No fue posible concluir: {detail}",
+      "no_connection": "Sin conexión con el servidor: {detail}",
       "no_records_found": "No se encontraron registros"
     },
     "success": "La acción fue exitosa"

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

@@ -271,6 +271,7 @@
       "whatsapp": "WhatsApp",
       "photo_hint": "Toque no ícone para adicionar sua foto",
       "photo_required": "A foto é obrigatória",
+      "photo_too_large": "Foto muito grande ({size} MB). O limite é {max} MB — tire outra foto ou escolha uma imagem menor.",
       "finish": "Concluir Cadastro",
       "success": "Cadastro concluído com sucesso!",
       "success_login": "Cadastro concluído! Faça login para acessar o sistema.",
@@ -315,6 +316,8 @@
     "errors": {
       "404": "Página não encontrada",
       "failed": "A ação falhou",
+      "unexpected": "Não foi possível concluir: {detail}",
+      "no_connection": "Sem conexão com o servidor: {detail}",
       "no_records_found": "Nenhum registro encontrado"
     },
     "success": "A ação foi bem-sucedida"

+ 67 - 6
src/pages/login/FirstAccessFormPage.vue

@@ -134,7 +134,10 @@
             v-model="form.password_confirmation"
             v-model:error="validationErrors.password_confirmation"
             :label="$t('auth.confirm_password')"
-            :rules="[inputRules.required, inputRules.samePassword(form.password)]"
+            :rules="[
+              inputRules.required,
+              inputRules.samePassword(form.password),
+            ]"
           />
 
           <p class="password-hint">{{ $t("auth.password_hint") }}</p>
@@ -177,12 +180,16 @@ import {
 } from "src/api/firstAccess";
 import { getFirstAccessData, clearFirstAccessData } from "./firstAccessStorage";
 import masks from "src/helpers/masks";
+import compressImage from "src/helpers/compressImage";
 
 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 MAX_PHOTO_SIZE_MB = 5;
+const MAX_PHOTO_SIZE_BYTES = MAX_PHOTO_SIZE_MB * 1024 * 1024;
+
 const router = useRouter();
 const $q = useQuasar();
 const { t } = useI18n();
@@ -218,11 +225,49 @@ const unlockField = (field) => {
   lockedFields.value = lockedFields.value.filter((locked) => locked !== field);
 };
 
+const notifyUnmappedError = (error) => {
+  const response = error?.response;
+
+  if (!response) {
+    $q.notify({
+      type: "negative",
+      timeout: 10000,
+      message: t("http.errors.no_connection", {
+        detail: error?.message ?? "erro desconhecido",
+      }),
+    });
+    return;
+  }
+
+  const unmapped = Object.entries(response.data?.errors ?? {})
+    .filter(([field]) => !(field in form.value))
+    .map(([field, messages]) => `${field}: ${[messages].flat()[0]}`);
+
+  if (unmapped.length) {
+    $q.notify({
+      type: "negative",
+      timeout: 10000,
+      message: t("http.errors.unexpected", { detail: unmapped.join(" | ") }),
+    });
+    return;
+  }
+
+  if (!response.data?.message && response.status !== 422) {
+    $q.notify({
+      type: "negative",
+      timeout: 10000,
+      message: t("http.errors.unexpected", {
+        detail: `HTTP ${response.status}`,
+      }),
+    });
+  }
+};
+
 const {
   loading,
   validationErrors,
   execute: submitForm,
-} = useSubmitHandler({ formRef });
+} = useSubmitHandler({ formRef, onError: notifyUnmappedError });
 
 const prefillRules = {
   name: [inputRules.required],
@@ -249,13 +294,26 @@ watch(
   { deep: true },
 );
 
-const onPhotoSelected = (event) => {
+const onPhotoSelected = async (event) => {
   const file = event.target.files?.[0];
   event.target.value = "";
   if (!file) return;
 
-  photoFile.value = file;
-  photoPreview.value = URL.createObjectURL(file);
+  const photo = await compressImage(file);
+
+  if (photo.size > MAX_PHOTO_SIZE_BYTES) {
+    const message = t("auth.first_access.photo_too_large", {
+      size: (photo.size / 1024 / 1024).toFixed(1),
+      max: MAX_PHOTO_SIZE_MB,
+    });
+
+    photoError.value = message;
+    $q.notify({ type: "negative", message, timeout: 8000 });
+    return;
+  }
+
+  photoFile.value = photo;
+  photoPreview.value = URL.createObjectURL(photo);
   photoError.value = null;
 };
 
@@ -284,7 +342,10 @@ const onSubmit = async () => {
     $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") });
+    $q.notify({
+      type: "positive",
+      message: t("auth.first_access.success_login"),
+    });
     router.push({ name: "LoginPage" });
   }
 };