Browse Source

feat: add possibilidade de cadastrar foto no momento de registro do cliente

Gustavo Mantovani 2 weeks ago
parent
commit
3ffbc5d1d7

+ 3 - 1
.gitignore

@@ -49,4 +49,6 @@ Thumbs.db
 
 /src-capacitor/android/.gradle/
 /src-capacitor/android/**/build/
-/src-capacitor/android/**/release/
+/src-capacitor/android/**/release/
+
+quasar.config.js.temporary.compiled.*

+ 0 - 221
quasar.config.js.temporary.compiled.1773333788777.mjs

@@ -1,221 +0,0 @@
-/* eslint-disable */
-/**
- * THIS FILE IS GENERATED AUTOMATICALLY.
- * 1. DO NOT edit this file directly as it won't do anything.
- * 2. EDIT the original quasar.config file INSTEAD.
- * 3. DO NOT git commit this file. It should be ignored.
- *
- * This file is still here because there was an error in
- * the original quasar.config file and this allows you to
- * investigate the Node.js stack error.
- *
- * After you fix the original file, this file will be
- * deleted automatically.
- **/
-
-
-// quasar.config.js
-import { defineConfig } from "@quasar/app-vite/wrappers";
-import { fileURLToPath } from "node:url";
-var __quasar_inject_import_meta_url__ = "file:///home/softpar/projetos/spas/sfp_front_vue_diarista_cliente/quasar.config.js";
-var quasar_config_default = defineConfig((ctx) => {
-  return {
-    bin: {
-      linuxAndroidStudio: "/snap/bin/android-studio"
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
-    // preFetch: true,
-    // app boot file (/src/boot)
-    // --> boot files are part of "main.js"
-    // https://v2.quasar.dev/quasar-cli-vite/boot-files
-    boot: [
-      "axios",
-      "i18n",
-      "defaultPropsComponents"
-      // "socket.io",
-    ],
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
-    css: ["app.scss"],
-    // https://github.com/quasarframework/quasar/tree/dev/extras
-    extras: [
-      // 'ionicons-v4',
-      "mdi-v7",
-      // 'fontawesome-v6',
-      // 'eva-icons',
-      // 'themify',
-      // "line-awesome",
-      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
-      "roboto-font",
-      // optional, you are not bound to it
-      "material-icons"
-      // optional, you are not bound to it
-    ],
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
-    build: {
-      target: {
-        browser: ["es2022", "firefox115", "chrome115", "safari14"],
-        node: "node22"
-      },
-      vueRouterMode: "history",
-      // available values: 'hash', 'history'
-      // vueRouterBase,
-      // vueDevtools,
-      // vueOptionsAPI: false,
-      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
-      // publicPath: '/',
-      // analyze: true,
-      env: {
-        API_URL: ctx.dev ? "http://localhost:3000" : "http://localhost:3000",
-        PASSWORD: ctx.dev ? "S@ft2080." : "",
-        WEBSOCKET_API: ctx.dev ? "http://localhost:4321/" : "http://localhost:4321/",
-        WEBSOCKET_PATH: ctx.dev ? "/socket.io" : "/socket.io",
-        WEBSOCKET_ROOM: ctx.dev ? "LARAVEL" : "LARAVEL",
-        WEBSOCKET_API_KEY: "7wArC/kl0nTbt4zBu0agw.NXLyjA96I6x1XmBcuokwPqfo3/CIxzqYw.PTthh5eqa08Uf4ubFlOqatpShoz1CRRID9pZReEFvBk3il6E9u"
-      },
-      // rawDefine: {}
-      // ignorePublicFolder: true,
-      // minify: false,
-      // polyfillModulePreload: true,
-      // distDir
-      // extendViteConf (viteConf) {},
-      // viteVuePluginOptions: {},
-      vitePlugins: [
-        [
-          "@intlify/unplugin-vue-i18n/vite",
-          {
-            include: [fileURLToPath(new URL("./src/i18n", __quasar_inject_import_meta_url__))],
-            ssr: ctx.modeName === "ssr"
-          }
-        ],
-        [
-          "vite-plugin-checker",
-          {
-            eslint: {
-              lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
-              useFlatConfig: true
-            }
-          },
-          { server: false }
-        ]
-      ]
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
-    devServer: {
-      // https: true
-      open: true
-      // opens browser window automatically
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
-    framework: {
-      lang: "pt-BR",
-      config: {
-        dark: "auto",
-        notify: {
-          position: "top-right"
-        }
-      },
-      // iconSet: 'material-icons', // Quasar icon set
-      // lang: 'en-US', // Quasar language pack
-      // For special cases outside of where the auto-import strategy can have an impact
-      // (like functional components as one of the examples),
-      // you can manually specify Quasar components/directives to be available everywhere:
-      //
-      // components: [],
-      // directives: [],
-      // Quasar plugins
-      plugins: ["Dialog", "Notify", "Loading", "Cookies", "Dark"]
-    },
-    // animations: 'all', // --- includes all animations
-    // https://v2.quasar.dev/options/animations
-    animations: [],
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
-    // sourceFiles: {
-    //   rootComponent: 'src/App.vue',
-    //   router: 'src/router/index',
-    //   store: 'src/store/index',
-    //   pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
-    //   pwaServiceWorker: 'src-pwa/custom-service-worker',
-    //   pwaManifestFile: 'src-pwa/manifest.json',
-    //   electronMain: 'src-electron/electron-main',
-    //   electronPreload: 'src-electron/electron-preload'
-    //   bexManifestFile: 'src-bex/manifest.json
-    // },
-    // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
-    ssr: {
-      prodPort: 3e3,
-      // The default port that the production server should use
-      // (gets superseded if process.env.PORT is specified at runtime)
-      middlewares: [
-        "render"
-        // keep this as last one
-      ],
-      // extendPackageJson (json) {},
-      // extendSSRWebserverConf (esbuildConf) {},
-      // manualStoreSerialization: true,
-      // manualStoreSsrContextInjection: true,
-      // manualStoreHydration: true,
-      // manualPostHydrationTrigger: true,
-      pwa: false
-      // pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
-      // will mess up SSR
-      // pwaExtendGenerateSWOptions (cfg) {},
-      // pwaExtendInjectManifestOptions (cfg) {}
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
-    pwa: {
-      workboxMode: "GenerateSW"
-      // 'GenerateSW' or 'InjectManifest'
-      // swFilename: 'sw.js',
-      // manifestFilename: 'manifest.json'
-      // extendManifestJson (json) {},
-      // useCredentialsForManifestTag: true,
-      // injectPwaMetaTags: false,
-      // extendPWACustomSWConf (esbuildConf) {},
-      // extendGenerateSWOptions (cfg) {},
-      // extendInjectManifestOptions (cfg) {}
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
-    cordova: {
-      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
-    capacitor: {
-      hideSplashscreen: true
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
-    electron: {
-      // extendElectronMainConf (esbuildConf) {},
-      // extendElectronPreloadConf (esbuildConf) {},
-      // extendPackageJson (json) {},
-      // Electron preload scripts (if any) from /src-electron, WITHOUT file extension
-      preloadScripts: ["electron-preload"],
-      // specify the debugging port to use for the Electron app when running in development mode
-      inspectPort: 5858,
-      bundler: "packager",
-      // 'packager' or 'builder'
-      packager: {
-        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
-        // OS X / Mac App Store
-        // appBundleId: '',
-        // appCategoryType: '',
-        // osxSign: '',
-        // protocol: 'myapp://path',
-        // Windows only
-        // win32metadata: { ... }
-      },
-      builder: {
-        // https://www.electron.build/configuration/configuration
-        appId: "quasar-skeleton"
-      }
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
-    bex: {
-      // extendBexScriptsConf (esbuildConf) {},
-      // extendBexManifestJson (json) {},
-      contentScripts: ["my-content-script"]
-    }
-  };
-});
-export {
-  quasar_config_default as default
-};

+ 0 - 226
quasar.config.js.temporary.compiled.1780406523031.mjs

@@ -1,226 +0,0 @@
-/* eslint-disable */
-/**
- * THIS FILE IS GENERATED AUTOMATICALLY.
- * 1. DO NOT edit this file directly as it won't do anything.
- * 2. EDIT the original quasar.config file INSTEAD.
- * 3. DO NOT git commit this file. It should be ignored.
- *
- * This file is still here because there was an error in
- * the original quasar.config file and this allows you to
- * investigate the Node.js stack error.
- *
- * After you fix the original file, this file will be
- * deleted automatically.
- **/
-
-
-// quasar.config.js
-import { defineConfig } from "@quasar/app-vite/wrappers";
-import { fileURLToPath } from "node:url";
-import { config as loadEnv } from "dotenv";
-var __quasar_inject_import_meta_url__ = "file:///home/softpar/projetos/spas/sfp_front_vue_diarista_cliente/quasar.config.js";
-loadEnv();
-var quasar_config_default = defineConfig((ctx) => {
-  return {
-    bin: {
-      linuxAndroidStudio: "/snap/bin/android-studio"
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
-    // preFetch: true,
-    // app boot file (/src/boot)
-    // --> boot files are part of "main.js"
-    // https://v2.quasar.dev/quasar-cli-vite/boot-files
-    boot: [
-      "axios",
-      "i18n",
-      "defaultPropsComponents",
-      "push-notifications"
-      // "socket.io",
-    ],
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
-    css: ["app.scss"],
-    // https://github.com/quasarframework/quasar/tree/dev/extras
-    extras: [
-      // 'ionicons-v4',
-      "mdi-v7",
-      // 'fontawesome-v6',
-      // 'eva-icons',
-      // 'themify',
-      // "line-awesome",
-      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
-      "roboto-font",
-      // optional, you are not bound to it
-      "material-icons"
-      // optional, you are not bound to it
-    ],
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
-    build: {
-      target: {
-        browser: ["es2022", "firefox115", "chrome115", "safari14"],
-        node: "node22"
-      },
-      vueRouterMode: "history",
-      // available values: 'hash', 'history'
-      // vueRouterBase,
-      // vueDevtools,
-      // vueOptionsAPI: false,
-      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
-      // publicPath: '/',
-      // analyze: true,
-      env: {
-        API_URL: ctx.mode.capacitor ? "http://10.0.2.2:3000" : ctx.dev ? "http://localhost:3000" : "http://localhost:3000",
-        PASSWORD: ctx.dev ? "S@ft2080." : "",
-        WEBSOCKET_API: ctx.dev ? "http://localhost:4321/" : "http://localhost:4321/",
-        WEBSOCKET_PATH: ctx.dev ? "/socket.io" : "/socket.io",
-        WEBSOCKET_ROOM: ctx.dev ? "LARAVEL" : "LARAVEL",
-        WEBSOCKET_API_KEY: "7wArC/kl0nTbt4zBu0agw.NXLyjA96I6x1XmBcuokwPqfo3/CIxzqYw.PTthh5eqa08Uf4ubFlOqatpShoz1CRRID9pZReEFvBk3il6E9u",
-        GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY ?? "",
-        PAGARME_PUBLIC_KEY: process.env.PAGARME_PUBLIC_KEY ?? "pk_test_1VRWkbvu43Tyk7qG"
-      },
-      // rawDefine: {}
-      // ignorePublicFolder: true,
-      // minify: false,
-      // polyfillModulePreload: true,
-      // distDir
-      // extendViteConf (viteConf) {},
-      // viteVuePluginOptions: {},
-      vitePlugins: [
-        [
-          "@intlify/unplugin-vue-i18n/vite",
-          {
-            include: [fileURLToPath(new URL("./src/i18n", __quasar_inject_import_meta_url__))],
-            ssr: ctx.modeName === "ssr"
-          }
-        ],
-        [
-          "vite-plugin-checker",
-          {
-            eslint: {
-              lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
-              useFlatConfig: true
-            }
-          },
-          { server: false }
-        ]
-      ]
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
-    devServer: {
-      // https: true
-      open: false
-      // opens browser window automatically
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
-    framework: {
-      lang: "pt-BR",
-      config: {
-        dark: "auto",
-        notify: {
-          position: "top-right"
-        }
-      },
-      // iconSet: 'material-icons', // Quasar icon set
-      // lang: 'en-US', // Quasar language pack
-      // For special cases outside of where the auto-import strategy can have an impact
-      // (like functional components as one of the examples),
-      // you can manually specify Quasar components/directives to be available everywhere:
-      //
-      // components: [],
-      // directives: [],
-      // Quasar plugins
-      plugins: ["Dialog", "Notify", "Loading", "Cookies", "Dark"]
-    },
-    // animations: 'all', // --- includes all animations
-    // https://v2.quasar.dev/options/animations
-    animations: [],
-    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
-    // sourceFiles: {
-    //   rootComponent: 'src/App.vue',
-    //   router: 'src/router/index',
-    //   store: 'src/store/index',
-    //   pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
-    //   pwaServiceWorker: 'src-pwa/custom-service-worker',
-    //   pwaManifestFile: 'src-pwa/manifest.json',
-    //   electronMain: 'src-electron/electron-main',
-    //   electronPreload: 'src-electron/electron-preload'
-    //   bexManifestFile: 'src-bex/manifest.json
-    // },
-    // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
-    ssr: {
-      prodPort: 3e3,
-      // The default port that the production server should use
-      // (gets superseded if process.env.PORT is specified at runtime)
-      middlewares: [
-        "render"
-        // keep this as last one
-      ],
-      // extendPackageJson (json) {},
-      // extendSSRWebserverConf (esbuildConf) {},
-      // manualStoreSerialization: true,
-      // manualStoreSsrContextInjection: true,
-      // manualStoreHydration: true,
-      // manualPostHydrationTrigger: true,
-      pwa: false
-      // pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
-      // will mess up SSR
-      // pwaExtendGenerateSWOptions (cfg) {},
-      // pwaExtendInjectManifestOptions (cfg) {}
-    },
-    // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
-    pwa: {
-      workboxMode: "GenerateSW"
-      // 'GenerateSW' or 'InjectManifest'
-      // swFilename: 'sw.js',
-      // manifestFilename: 'manifest.json'
-      // extendManifestJson (json) {},
-      // useCredentialsForManifestTag: true,
-      // injectPwaMetaTags: false,
-      // extendPWACustomSWConf (esbuildConf) {},
-      // extendGenerateSWOptions (cfg) {},
-      // extendInjectManifestOptions (cfg) {}
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
-    cordova: {
-      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
-    capacitor: {
-      hideSplashscreen: true
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
-    electron: {
-      // extendElectronMainConf (esbuildConf) {},
-      // extendElectronPreloadConf (esbuildConf) {},
-      // extendPackageJson (json) {},
-      // Electron preload scripts (if any) from /src-electron, WITHOUT file extension
-      preloadScripts: ["electron-preload"],
-      // specify the debugging port to use for the Electron app when running in development mode
-      inspectPort: 5858,
-      bundler: "packager",
-      // 'packager' or 'builder'
-      packager: {
-        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
-        // OS X / Mac App Store
-        // appBundleId: '',
-        // appCategoryType: '',
-        // osxSign: '',
-        // protocol: 'myapp://path',
-        // Windows only
-        // win32metadata: { ... }
-      },
-      builder: {
-        // https://www.electron.build/configuration/configuration
-        appId: "quasar-skeleton"
-      }
-    },
-    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
-    bex: {
-      // extendBexScriptsConf (esbuildConf) {},
-      // extendBexManifestJson (json) {},
-      contentScripts: ["my-content-script"]
-    }
-  };
-});
-export {
-  quasar_config_default as default
-};

+ 212 - 0
src/components/login/LoginStepAvatarPanel.vue

@@ -0,0 +1,212 @@
+<template>
+  <q-card-section class="avatar-step no-padding">
+    <div class="column items-center q-gutter-y-xl q-pt-md">
+      <div class="column items-center q-gutter-y-sm">
+        <div
+          class="photo-circle"
+          :class="form.avatar ? 'photo-circle--done' : 'photo-circle--pending'"
+          @click="openDefaultPicker"
+        >
+          <img
+            v-if="avatarPreview"
+            :src="avatarPreview"
+            class="avatar-preview__image"
+          />
+
+          <q-icon v-else color="white" name="photo_camera" size="52px" />
+        </div>
+
+        <input
+          ref="uploadInputRef"
+          accept="image/jpeg,image/png,image/webp"
+          class="hidden"
+          type="file"
+          @change="onFileSelected"
+        />
+
+        <input
+          ref="cameraInputRef"
+          accept="image/jpeg,image/png,image/webp"
+          capture="user"
+          class="hidden"
+          type="file"
+          @change="onFileSelected"
+        />
+
+        <template v-if="isNativeApp && !form.avatar">
+          <q-btn
+            color="primary-button"
+            no-caps
+            padding="10px 28px"
+            rounded
+            :label="$t('auth.avatar_step_take_photo')"
+            @click="cameraInputRef.click()"
+          />
+
+          <q-btn
+            color="primary"
+            flat
+            no-caps
+            padding="8px 16px"
+            rounded
+            :label="$t('auth.avatar_step_choose_photo')"
+            @click="uploadInputRef.click()"
+          />
+        </template>
+
+        <q-btn
+          v-else-if="!form.avatar"
+          color="primary-button"
+          no-caps
+          padding="10px 28px"
+          rounded
+          :label="$t('auth.avatar_step_button')"
+          @click="uploadInputRef.click()"
+        />
+
+        <q-btn
+          v-if="form.avatar"
+          color="grey-6"
+          flat
+          no-caps
+          padding="10px 16px"
+          rounded
+          :label="$t('auth.avatar_step_remove')"
+          @click="removeAvatar"
+        />
+
+        <div class="text-text text-center font12">
+          {{ $t("auth.avatar_step_title") }}
+        </div>
+      </div>
+    </div>
+  </q-card-section>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, ref, watch } from "vue";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+
+const form = defineModel({ type: Object, required: true });
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const uploadInputRef = ref(null);
+const cameraInputRef = ref(null);
+const avatarPreview = ref(null);
+const maxAvatarSize = 5 * 1024 * 1024;
+const acceptedAvatarTypes = ["image/jpeg", "image/png", "image/webp"];
+
+const isNativeApp = computed(() => {
+  return $q.platform.is.capacitor || $q.platform.is.cordova;
+});
+
+const revokePreview = () => {
+  if (!avatarPreview.value) return;
+
+  URL.revokeObjectURL(avatarPreview.value);
+
+  avatarPreview.value = null;
+};
+
+const openDefaultPicker = () => {
+  if (isNativeApp.value) {
+    cameraInputRef.value?.click();
+
+    return;
+  }
+
+  uploadInputRef.value?.click();
+};
+
+const onFileSelected = (event) => {
+  const file = event.target.files?.[0] ?? null;
+
+  if (!file) return;
+
+  if (!acceptedAvatarTypes.includes(file.type) || file.size > maxAvatarSize) {
+    $q.notify({
+      type: "negative",
+      message: t("auth.avatar_step_invalid"),
+    });
+
+    event.target.value = "";
+
+    return;
+  }
+
+  revokePreview();
+
+  form.value.avatar = file;
+
+  avatarPreview.value = URL.createObjectURL(file);
+};
+
+const removeAvatar = () => {
+  revokePreview();
+
+  form.value.avatar = null;
+
+  if (uploadInputRef.value) {
+    uploadInputRef.value.value = "";
+  }
+
+  if (cameraInputRef.value) {
+    cameraInputRef.value.value = "";
+  }
+};
+
+watch(
+  () => form.value.avatar,
+  (avatar) => {
+    if (!avatar) {
+      revokePreview();
+    }
+  },
+);
+
+onBeforeUnmount(() => {
+  revokePreview();
+});
+</script>
+
+<style lang="scss" scoped>
+.avatar-step {
+  min-height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.photo-circle {
+  width: 130px;
+  height: 130px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  overflow: hidden;
+  transition: transform 0.15s ease;
+
+  &:active {
+    transform: scale(0.95);
+  }
+
+  &--pending {
+    background-color: rgba(139, 92, 246, 0.18);
+  }
+
+  &--done {
+    background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
+  }
+}
+
+.avatar-preview__image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+</style>

+ 8 - 1
src/i18n/locales/en.json

@@ -150,6 +150,13 @@
     "address_type_home": "Home",
     "address_type_commercial": "Commercial",
     "address_type_other": "Other",
+    "avatar_step_title": "Add a profile photo",
+    "avatar_step_button": "Add photo",
+    "avatar_step_take_photo": "Take photo",
+    "avatar_step_choose_photo": "Choose photo",
+    "avatar_step_remove": "Remove photo",
+    "avatar_step_hint": "Optional. You can continue without a photo.",
+    "avatar_step_invalid": "Upload a JPG, PNG, or WEBP image up to 5 MB.",
     "step4_title": "Add your ZIP code and find the nearest cleaners",
     "use_location": "use my location",
     "back_to_register": "Back to registration",
@@ -934,4 +941,4 @@
       "user_message_header": "--- User message ---"
     }
   }
-}
+}

+ 8 - 1
src/i18n/locales/es.json

@@ -150,6 +150,13 @@
     "address_type_home": "Casa",
     "address_type_commercial": "Comercial",
     "address_type_other": "Otro",
+    "avatar_step_title": "Agrega una foto de perfil",
+    "avatar_step_button": "Agregar foto",
+    "avatar_step_take_photo": "Tomar foto",
+    "avatar_step_choose_photo": "Elegir foto",
+    "avatar_step_remove": "Eliminar foto",
+    "avatar_step_hint": "Opcional. Puedes continuar sin foto.",
+    "avatar_step_invalid": "Sube una imagen JPG, PNG o WEBP de hasta 5 MB.",
     "step4_title": "Agrega tu código postal y encuentra los limpiadores más cercanos",
     "use_location": "usar mi ubicación",
     "back_to_register": "Volver al registro",
@@ -930,4 +937,4 @@
       "user_message_header": "--- Mensaje del usuario ---"
     }
   }
-}
+}

+ 8 - 1
src/i18n/locales/pt.json

@@ -150,6 +150,13 @@
     "address_type_home": "Casa",
     "address_type_commercial": "Comercial",
     "address_type_other": "Outro",
+    "avatar_step_title": "Adicione uma foto de perfil",
+    "avatar_step_button": "Cadastrar foto",
+    "avatar_step_take_photo": "Tirar foto",
+    "avatar_step_choose_photo": "Escolher foto",
+    "avatar_step_remove": "Remover foto",
+    "avatar_step_hint": "Opcional. Você pode continuar sem foto.",
+    "avatar_step_invalid": "Envie uma imagem JPG, PNG ou WEBP de até 5 MB.",
     "step4_title": "Adicione seu CEP e veja os diaristas mais próximos",
     "use_location": "usar minha localização",
     "back_to_register": "Voltar para o cadastro",
@@ -947,4 +954,4 @@
       "user_message_header": "--- Mensagem do usuário ---"
     }
   }
-}
+}

+ 257 - 121
src/pages/LoginPage.vue

@@ -1,68 +1,135 @@
 <template>
-  <q-page class="login-page bg-surface-dark">
-    <Transition name="fade-slide" mode="out-in">
-
-      <div v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
-        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
-        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
-        <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
+  <q-page class="bg-surface-dark login-page">
+    <Transition mode="out-in" name="fade-slide">
+      <div
+        v-if="!clicked"
+        key="splash"
+        class="splash-screen"
+        @click="clicked = true"
+      >
+        <img
+          class="splash-layer splash-layer--bg"
+          :src="BackgroundLogin"
+        />
+
+        <img
+          class="splash-layer splash-layer--photo"
+          :src="FotoDiarista"
+        />
+
+        <img
+          class="splash-layer splash-layer--logo"
+          :src="LogoLogin"
+        />
       </div>
 
-      <div v-else-if="steps === 4" key="step4" class="splash-screen">
-        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
-        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
-        <img :src="LogoLogin" class="splash-layer splash-layer--logo-small" />
+      <div
+        v-else-if="steps === 5"
+        key="step5"
+        class="splash-screen"
+      >
+        <img
+          class="splash-layer splash-layer--bg"
+          :src="BackgroundLogin"
+        />
+
+        <img
+          class="splash-layer splash-layer--photo"
+          :src="FotoDiarista"
+        />
+
+        <img
+          class="splash-layer splash-layer--logo-small"
+          :src="LogoLogin"
+        />
+
         <div class="step4-card-wrapper">
           <LoginStepFourPanel
-            @back="steps = 3"
+            @back="goBack"
             @cep-resolved="onCepResolved"
             @device-location="onDeviceLocation"
           />
         </div>
       </div>
 
-      <div v-else key="flow" class="flow-screen">
-
-        <div class="flow-header">
+      <div
+        v-else
+        key="flow"
+        class="flow-screen"
+      >
+        <div class="flow-header items-center row">
           <q-btn
-            v-if="steps === 3"
-            flat
+            v-if="steps >= 4"
+            color="primary"
             dense
+            flat
+            icon="mdi-arrow-left"
+            round
+            @click="goBack"
+          />
+
+          <q-btn
+            v-else-if="steps === 3"
+            class="q-ml-auto"
             color="primary"
-            :label="$t('auth.register_later')"
+            dense
+            flat
             icon-right="mdi-chevron-right-circle-outline"
-            class=""
-            @click="steps = 4"
+            :label="$t('auth.register_later')"
+            @click="steps = 5"
           />
         </div>
 
         <div class="flow-logo">
-          <q-img :src="LogoDiariaCampos" style="max-width: 180px;" />
+          <q-img
+            style="max-width: 180px"
+            :src="LogoDiariaCampos"
+          />
         </div>
 
         <q-form
           ref="loginForm"
-          class="flow-form"
-          autocorrect="off"
           autocapitalize="off"
           autocomplete="off"
+          autocorrect="off"
+          class="flow-form"
           spellcheck="false"
           @submit="onSubmit"
         >
-          <div class="flow-content" :class="{ 'flow-content--centered': steps < 3 }">
-            <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
-            <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
-            <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
+          <div
+            class="flow-content"
+            :class="{ 'flow-content--centered': steps < 3 }"
+          >
+            <LoginStepOnePanel
+              v-if="steps === 1"
+              v-model:email="email"
+              v-model:phone="phone"
+            />
+
+            <LoginStepTwoPanel
+              v-else-if="steps === 2"
+              v-model:code="code"
+            />
+
+            <LoginStepThreePanel
+              v-else-if="steps === 3"
+              v-model="stepThreeForm"
+            />
+
+            <LoginStepAvatarPanel
+              v-else-if="steps === 4"
+              v-model="stepAvatarForm"
+            />
           </div>
 
           <div class="flow-footer">
             <q-btn
+              class="full-width"
               color="primary-button"
-              :label="$t('auth.continue')"
-              rounded
               padding="8px 12px"
+              rounded
               type="submit"
-              class="full-width"
+              :label="$t('auth.continue')"
               :loading="submitting"
             >
               <template #loading>
@@ -71,113 +138,78 @@
             </q-btn>
           </div>
         </q-form>
-
       </div>
-
     </Transition>
   </q-page>
 </template>
 
 <script setup>
-import { onMounted, ref } from 'vue';
-import { createUserAndClient, sendCode, validateCode } from 'src/api/user';
-import { useAuth } from 'src/composables/useAuth';
-import { useRouter } from 'vue-router';
-import { useRegistrationFlowStore } from 'src/stores/registrationFlow';
-
-import BackgroundLogin from 'src/assets/background-login.svg';
-import FotoDiarista from 'src/assets/foto_diarista_login.svg';
-import LogoLogin from 'src/assets/logo_diaria_login.svg';
-import LogoDiariaCampos from 'src/assets/logo_diaria_campos_login.svg';
-
-import LoginStepOnePanel from 'src/components/login/LoginStepOnePanel.vue';
-import LoginStepTwoPanel from 'src/components/login/LoginStepTwoPanel.vue';
-import LoginStepThreePanel from 'src/components/login/LoginStepThreePanel.vue';
-import LoginStepFourPanel from 'src/components/login/LoginStepFourPanel.vue';
+import { createUserAndClient, sendCode, validateCode } from "src/api/user";
+import { useAuth } from "src/composables/useAuth";
+import { useRegistrationFlowStore } from "src/stores/registrationFlow";
+import { onMounted, ref } from "vue";
+import { useRouter } from "vue-router";
+
+import BackgroundLogin from "src/assets/background-login.svg";
+import FotoDiarista from "src/assets/foto_diarista_login.svg";
+import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
+import LogoLogin from "src/assets/logo_diaria_login.svg";
+
+import LoginStepAvatarPanel from "src/components/login/LoginStepAvatarPanel.vue";
+import LoginStepFourPanel from "src/components/login/LoginStepFourPanel.vue";
+import LoginStepOnePanel from "src/components/login/LoginStepOnePanel.vue";
+import LoginStepThreePanel from "src/components/login/LoginStepThreePanel.vue";
+import LoginStepTwoPanel from "src/components/login/LoginStepTwoPanel.vue";
 
-const router = useRouter();
 const { setAuthDataFromPayload } = useAuth();
 const flowStore = useRegistrationFlowStore();
+const router = useRouter();
 
-const email = ref('');
-const phone = ref('');
-const code = ref('');
+const clicked = ref(false);
+const code = ref("");
+const email = ref("");
+const isLogin = ref(false);
 
-const stepThreeForm = ref({
-  name: '',
-  document: '',
-  zip_code: '',
-  address: '',
-  number: '',
-  district: '',
-  complement: '',
-  nickname: '',
-  instructions: '',
-  address_type: 'home',
-  no_complement: false,
-  city: '',
-  state: '',
-});
+const loginForm = ref(null);
 
-const steps = ref(1); // 1 = credentials | 2 = code | 3 = user fields | 4 = cep only
-const clicked = ref(false);
-const submitting = ref(false);
+const phone = ref("");
 
-onMounted(() => {
-  setTimeout(() => {
-    clicked.value = true;
-  }, 1500);
+const stepAvatarForm = ref({
+  avatar: null,
 });
-const loginForm = ref(null);
-const isLogin = ref(false);
 
-const sendValidationCode = async () => {
-  const response = await sendCode(email.value, phone.value);
-  if (response.status === 201) {
-    steps.value = 2;
-  } else {
-    console.error('Failed to send validation code');
-  }
-  return response;
-};
+const steps = ref(1);
 
-const validateCodeInput = async () => {
-  const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
-  if (response.status === 200) {
-    if(isLogin.value == true) {
-      await setAuthDataFromPayload(response.data.payload);
-      router.push({ name: 'DashboardPage' });
-    } else {
-      steps.value = 3;
-    }
-  } else {
-    console.error('Invalid validation code');
-  }
-};
+const stepThreeForm = ref({
+  address: "",
+  address_type: "home",
+  city: "",
+  complement: "",
+  district: "",
+  document: "",
+  instructions: "",
+  name: "",
+  nickname: "",
+  no_complement: false,
+  number: "",
+  state: "",
+  zip_code: "",
+});
 
-const registerUserAndClient = async () => {
-  const payload = {
-    ...stepThreeForm.value,
-    email: email.value,
-    phone: phone.value,
-    code: code.value,
-    has_complement: !stepThreeForm.value.no_complement,
-  };
-  // delete payload.no_complement;
+const submitting = ref(false);
 
-  const response = await createUserAndClient(payload);
-  if (response.status === 200) {
-    await setAuthDataFromPayload(response.data.payload);
-    router.push({ name: 'DashboardPage' });
-  } else {
-    console.error('Failed to create user and client');
+const goBack = () => {
+  if (steps.value === 4) {
+    steps.value = 3;
+  } else if (steps.value === 5) {
+    steps.value = 4;
   }
 };
 
 const navigateToMap = (lat, lng) => {
   flowStore.setCredentials(email.value, phone.value, code.value);
   flowStore.setInitialLocation(lat, lng);
-  router.push({ name: 'LocationMapPage' });
+  router.push({ name: "LocationMapPage" });
 };
 
 const onCepResolved = (geoData) => {
@@ -190,30 +222,135 @@ const onDeviceLocation = ({ lat, lng }) => {
 
 const onSubmit = async () => {
   submitting.value = true;
+
   try {
     switch (steps.value) {
       case 1: {
         const response = await sendValidationCode();
+
         isLogin.value = response.data.payload.isLogin;
         break;
       }
-      case 2: await validateCodeInput(); break;
-      case 3: await registerUserAndClient(); break;
-      default: break;
+
+      case 2:
+        await validateCodeInput();
+        break;
+
+      case 3:
+        steps.value = 4;
+        break;
+
+      case 4:
+        await registerUserAndClient();
+        break;
+
+      default:
+        break;
     }
   } finally {
     submitting.value = false;
   }
 };
+
+const registerUserAndClient = async () => {
+  const payload = new FormData();
+
+  const append = (key, value) => {
+    if (value === null || value === undefined || value === "") {
+      return;
+    }
+
+    if (typeof value === "boolean") {
+      payload.append(key, value ? "1" : "0");
+    } else {
+      payload.append(key, value);
+    }
+  };
+
+  Object.entries(stepThreeForm.value).forEach(([key, value]) => {
+    if (key === "no_complement") {
+      return;
+    }
+
+    if (key === "complement" && stepThreeForm.value.no_complement) {
+      return;
+    }
+
+    append(key, value);
+  });
+
+  append("avatar", stepAvatarForm.value.avatar);
+  append("code", code.value);
+  append("email", email.value);
+  append("has_complement", !stepThreeForm.value.no_complement);
+  append("phone", phone.value);
+
+  const response = await createUserAndClient(payload);
+
+  if (response.status === 200) {
+    await setAuthDataFromPayload(response.data.payload);
+    router.push({ name: "DashboardPage" });
+  } else {
+    console.error("Failed to create user and client");
+  }
+};
+
+const sendValidationCode = async () => {
+  const response = await sendCode(email.value, phone.value);
+
+  if (response.status === 201) {
+    steps.value = 2;
+  } else {
+    console.error("Failed to send validation code");
+  }
+
+  return response;
+};
+
+const validateCodeInput = async () => {
+  const response = await validateCode(
+    email.value,
+    phone.value,
+    code.value,
+    isLogin.value,
+  );
+
+  if (response.status === 200) {
+    if (isLogin.value === true) {
+      await setAuthDataFromPayload(response.data.payload);
+      router.push({ name: "DashboardPage" });
+    } else {
+      steps.value = 3;
+    }
+  } else {
+    console.error("Invalid validation code");
+  }
+};
+
+onMounted(() => {
+  setTimeout(() => {
+    clicked.value = true;
+  }, 1500);
+});
 </script>
 
 <style lang="scss" scoped>
 .fade-slide-enter-active,
 .fade-slide-leave-active {
-  transition: opacity 0.35s ease, transform 0.35s ease;
+  transition:
+    opacity 0.35s ease,
+    transform 0.35s ease;
+}
+
+.fade-slide-enter-from {
+  opacity: 0;
+  transform: translateY(6px);
+}
+
+.fade-slide-leave-to {
+  opacity: 0;
+  transform: translateY(-6px);
 }
-.fade-slide-enter-from { opacity: 0; transform: translateY(6px); }
-.fade-slide-leave-to   { opacity: 0; transform: translateY(-6px); }
 
 .login-page {
   min-height: 100vh;
@@ -290,7 +427,6 @@ const onSubmit = async () => {
   min-height: 36px;
   display: flex;
   align-items: center;
-  justify-content: flex-end;
 }
 
 .flow-logo {
@@ -318,4 +454,4 @@ const onSubmit = async () => {
 .flow-footer {
   padding: 20px 0 calc(12px + env(safe-area-inset-bottom));
 }
-</style>
+</style>