Forráskód Böngészése

feat: following latest quasar recommendations, better useFormUpdateTracker, useScroll, useSubmitHandler and other small improvements in the layout and code overall

Denis 4 hónapja
szülő
commit
9ee1521056

+ 0 - 7
.eslintignore

@@ -1,7 +0,0 @@
-/dist
-/src-capacitor
-/src-cordova
-/.quasar
-/node_modules
-.eslintrc.cjs
-/quasar.config.*.temporary.compiled*

+ 0 - 73
.eslintrc.cjs

@@ -1,73 +0,0 @@
-module.exports = {
-  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
-  // This option interrupts the configuration hierarchy at this file
-  // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
-  root: true,
-
-  parserOptions: {
-    ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features
-  },
-
-  env: {
-    node: true,
-    browser: true,
-  },
-
-  // Rules order is important, please avoid shuffling them
-  extends: [
-    // Base ESLint recommended rules
-    "eslint:recommended",
-
-    // Uncomment any of the lines below to choose desired strictness,
-    // but leave only one uncommented!
-    // See https://eslint.vuejs.org/rules/#available-rules
-    "plugin:vue/essential", // Priority A: Essential (Error Prevention)
-    "plugin:vue/strongly-recommended", // Priority B: Strongly Recommended (Improving Readability)
-    "plugin:vue/recommended", // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
-    "plugin:@intlify/vue-i18n/recommended",
-
-    // https://github.com/prettier/eslint-config-prettier#installation
-    // usage with Prettier, provided by 'eslint-config-prettier'.
-    "prettier",
-  ],
-
-  settings: {
-    "vue-i18n": {
-      localeDir: "./src/i18n/locales/*.json",
-    },
-  },
-
-  plugins: [
-    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
-    // required to lint *.vue files
-    "vue",
-
-    // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
-    // Prettier has not been included as plugin to avoid performance impact
-    // add it as an extension for your IDE
-  ],
-
-  globals: {
-    ga: "readonly", // Google Analytics
-    cordova: "readonly",
-    __statics: "readonly",
-    __QUASAR_SSR__: "readonly",
-    __QUASAR_SSR_SERVER__: "readonly",
-    __QUASAR_SSR_CLIENT__: "readonly",
-    __QUASAR_SSR_PWA__: "readonly",
-    process: "readonly",
-    Capacitor: "readonly",
-    chrome: "readonly",
-  },
-
-  // add your custom rules here
-  rules: {
-    "prefer-promise-reject-errors": "off",
-    "vue/require-prop-types": "off",
-    "vue/no-v-model-argument": "off",
-    "vue/no-unused-vars": "warn",
-    "vue/no-unused-components": "warn",
-    // allow debugger during development only
-    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
-  },
-};

+ 101 - 0
eslint.config.js

@@ -0,0 +1,101 @@
+import js from "@eslint/js";
+import globals from "globals";
+import pluginVue from "eslint-plugin-vue";
+import pluginQuasar from "@quasar/app-vite/eslint";
+import vueI18n from "@intlify/eslint-plugin-vue-i18n";
+
+// the following is optional, if you want prettier too:
+import prettierSkipFormatting from "@vue/eslint-config-prettier/skip-formatting";
+
+export default [
+  {
+    /**
+     * Ignore the following files.
+     * Please note that pluginQuasar.configs.recommended() already ignores
+     * the "node_modules" folder for you (and all other Quasar project
+     * relevant folders and files).
+     *
+     * ESLint requires "ignores" key to be the only one in this object
+     */
+    ignores: [
+      "/dist",
+      "/src-capacitor",
+      "/src-cordova",
+      "/.quasar",
+      "/node_modules",
+      "/quasar.config.*.temporary.compiled*",
+    ],
+  },
+  ...pluginQuasar.configs.recommended(),
+  js.configs.recommended,
+
+  /**
+   * https://eslint.vuejs.org
+   *
+   * pluginVue.configs.base
+   *   -> Settings and rules to enable correct ESLint parsing.
+   * pluginVue.configs[ 'flat/essential']
+   *   -> base, plus rules to prevent errors or unintended behavior.
+   * pluginVue.configs["flat/strongly-recommended"]
+   *   -> Above, plus rules to considerably improve code readability and/or dev experience.
+   * pluginVue.configs["flat/recommended"]
+   *   -> Above, plus rules to enforce subjective community defaults to ensure consistency.
+   */
+  ...pluginVue.configs["flat/essential"],
+  ...pluginVue.configs["flat/strongly-recommended"],
+  ...pluginVue.configs["flat/recommended"],
+  ...vueI18n.configs.recommended,
+
+  {
+    languageOptions: {
+      ecmaVersion: "latest",
+      sourceType: "module",
+
+      globals: {
+        ...globals.browser,
+        ...globals.node, // SSR, Electron, config files
+        process: "readonly", // process.env.*
+        ga: "readonly", // Google Analytics
+        cordova: "readonly",
+        Capacitor: "readonly",
+        chrome: "readonly", // BEX related
+        browser: "readonly", // BEX related
+      },
+    },
+
+    // add your custom rules here
+    rules: {
+      "vue/no-unused-vars": "warn",
+      "vue/no-unused-components": "warn",
+      "@intlify/vue-i18n/no-dynamic-keys": "off",
+      "@intlify/vue-i18n/no-unused-keys": [
+        "error",
+        {
+          extensions: [".js", ".vue"],
+        },
+      ],
+      // allow debugger during development only
+      "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
+    },
+  },
+
+  {
+    settings: {
+      "vue-i18n": {
+        localeDir: "./src/i18n/locales/*.{json,json5,yaml,yml}",
+        messageSyntaxVersion: "^11.0.0",
+      },
+    },
+  },
+
+  {
+    files: ["src-pwa/custom-service-worker.js"],
+    languageOptions: {
+      globals: {
+        ...globals.serviceworker,
+      },
+    },
+  },
+
+  prettierSkipFormatting, // optional, if you want prettier
+];

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 358 - 220
package-lock.json


+ 24 - 23
package.json

@@ -7,7 +7,7 @@
   "type": "module",
   "private": true,
   "scripts": {
-    "lint": "eslint --ext .js,.vue ./",
+    "lint": "eslint -c ./eslint.config.js \"./src*/**/*.{js,cjs,mjs,vue}\"",
     "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
     "test": "echo \"No test specified\" && exit 0",
     "dev": "quasar dev",
@@ -16,35 +16,36 @@
   "dependencies": {
     "@bufbuild/protobuf": "^2.5.1",
     "@quasar/cli": "^2.5.0",
-    "@quasar/extras": "^1.16.15",
-    "axios": "^1.7.9",
-    "chart.js": "^4.4.7",
+    "@quasar/extras": "^1.17.0",
+    "axios": "^1.13.2",
+    "chart.js": "^4.5.1",
     "chartjs-plugin-datalabels": "^2.2.0",
     "date-fns": "^3.6.0",
     "fast-deep-equal": "^3.1.3",
-    "pinia": "^2.3.0",
-    "quasar": "^2.17.4",
+    "pinia": "^3.0.2",
+    "quasar": "^2.18.6",
     "socket.io-client": "^4.8.1",
     "vue": "^3.5.13",
-    "vue-chartjs": "^5.3.2",
-    "vue-currency-input": "^3.1.0",
-    "vue-i18n": "^9.14.2",
-    "vue-router": "^4.5.0"
+    "vue-chartjs": "^5.3.3",
+    "vue-currency-input": "^3.2.1",
+    "vue-i18n": "^11.1.4",
+    "vue-router": "^4.6.4"
   },
   "devDependencies": {
-    "@bufbuild/buf": "^1.54.0",
-    "@bufbuild/protoc-gen-es": "^2.5.1",
-    "@intlify/eslint-plugin-vue-i18n": "^3.2.0",
-    "@intlify/unplugin-vue-i18n": "^2.0.0",
-    "@intlify/vue-i18n-loader": "^4.2.0",
-    "@quasar/app-vite": "^2.2.1",
-    "autoprefixer": "^10.4.20",
-    "eslint": "^8.57.1",
-    "eslint-config-prettier": "^9.1.0",
-    "eslint-plugin-vue": "^9.33.0",
-    "postcss": "^8.4.49",
-    "prettier": "^3.4.2",
-    "vite-plugin-checker": "^0.6.4"
+    "@bufbuild/buf": "^1.61.0",
+    "@bufbuild/protoc-gen-es": "^2.10.2",
+    "@intlify/eslint-plugin-vue-i18n": "^4.0.1",
+    "@intlify/unplugin-vue-i18n": "^6.0.8",
+    "@eslint/js": "^9.27.0",
+    "@quasar/app-vite": "^2.4.0",
+    "@vue/eslint-config-prettier": "^10.2.0",
+    "autoprefixer": "^10.4.21",
+    "eslint": "^9.31.0",
+    "eslint-config-prettier": "^10.1.5",
+    "eslint-plugin-vue": "^10.1.0",
+    "postcss": "^8.5.3",
+    "prettier": "^3.5.3",
+    "vite-plugin-checker": "^0.9.3"
   },
   "engines": {
     "node": "^24 || ^22 || ^20 || ^18",

+ 27 - 0
postcss.config.js

@@ -0,0 +1,27 @@
+import autoprefixer from 'autoprefixer'
+// import rtlcss from 'postcss-rtlcss'
+
+export default {
+  plugins: [
+    // https://github.com/postcss/autoprefixer
+    autoprefixer({
+      overrideBrowserslist: [
+        'last 4 Chrome versions',
+        'last 4 Firefox versions',
+        'last 4 Edge versions',
+        'last 4 Safari versions',
+        'last 4 Android versions',
+        'last 4 ChromeAndroid versions',
+        'last 4 FirefoxAndroid versions',
+        'last 4 iOS versions'
+      ]
+    }),
+
+    // https://github.com/elchininet/postcss-rtlcss
+    // If you want to support RTL css, then
+    // 1. yarn/pnpm/bun/npm install postcss-rtlcss
+    // 2. optionally set quasar.config.js > framework > lang to an RTL language
+    // 3. uncomment the following line (and its import statement above):
+    // rtlcss()
+  ]
+}

+ 6 - 13
quasar.config.js

@@ -3,10 +3,10 @@
 // Configuration for your app
 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
 
-import { configure } from "quasar/wrappers";
+import { defineConfig } from "#q-app/wrappers";
 import { fileURLToPath } from "node:url";
 
-export default configure((ctx) => {
+export default defineConfig((ctx) => {
   return {
     // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
     // preFetch: true,
@@ -78,24 +78,17 @@ export default configure((ctx) => {
         [
           "@intlify/unplugin-vue-i18n/vite",
           {
-            // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
-            // compositionOnly: false,
-
-            // if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
-            // you need to set `runtimeOnly: false`
-            // runtimeOnly: false,
-
-            ssr: ctx.modeName === "ssr",
-
-            // you need to set i18n resource including paths !
             include: [fileURLToPath(new URL("./src/i18n", import.meta.url))],
+            ssr: ctx.modeName === "ssr",
           },
         ],
         [
           "vite-plugin-checker",
           {
             eslint: {
-              lintCommand: 'eslint "./**/*.{js,mjs,cjs,vue}"',
+              lintCommand:
+                'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
+              useFlatConfig: true,
             },
           },
           { server: false },

+ 16 - 12
src/boot/axios.js

@@ -1,4 +1,4 @@
-import { boot } from "quasar/wrappers";
+import { defineBoot } from "#q-app/wrappers";
 import { Cookies, Notify } from "quasar";
 import axios from "axios";
 import { useRouter } from "vue-router";
@@ -52,18 +52,22 @@ const errorInterceptor = async (error, router) => {
     error.response?.status !== 401 ||
     originalRequest.url.includes("/refresh")
   ) {
-    Notify.create({
-      message: error?.response?.data?.message ?? error.message,
-      type: "negative",
-    });
+    if (error?.response?.data?.message) {
+      Notify.create({
+        message: error?.response?.data?.message,
+        type: "negative",
+      });
+    }
     return Promise.reject(error);
   }
 
   if (error?.config?.url === "/login") {
-    Notify.create({
-      message: error.response.data.message,
-      type: "negative",
-    });
+    if (error?.response?.data?.message) {
+      Notify.create({
+        message: error?.response?.data?.message,
+        type: "negative",
+      });
+    }
     return Promise.reject(error);
   }
 
@@ -92,16 +96,16 @@ const errorInterceptor = async (error, router) => {
 };
 
 const successInterceptor = (response) => {
-  if (response.data.message) {
+  if (response?.data?.message) {
     Notify.create({
-      message: response.data.message,
+      message: response?.data?.message,
       type: "positive",
     });
   }
   return response;
 };
 
-export default boot(({ app }) => {
+export default defineBoot(({ app }) => {
   const router = useRouter();
 
   api.interceptors.response.use(

+ 2 - 2
src/boot/defaultPropsComponents.js

@@ -1,5 +1,5 @@
 import { QDialog, QInput, QSelect, QScrollArea, QCard } from "quasar";
-import { boot } from "quasar/wrappers";
+import { defineBoot } from "#q-app/wrappers";
 
 /**
  * Set some default properties on a component
@@ -14,7 +14,7 @@ const SetComponentDefaults = (component, defaults) => {
   });
 };
 
-export default boot(() => {
+export default defineBoot(() => {
   SetComponentDefaults(QDialog, {
     transitionShow: "slide-up",
     transitionHide: "slide-down",

+ 2 - 2
src/boot/i18n.js

@@ -1,4 +1,4 @@
-import { boot } from "quasar/wrappers";
+import { defineBoot } from "#q-app/wrappers";
 import { createI18n } from "vue-i18n";
 import { Cookies } from "quasar";
 import messages from "src/i18n";
@@ -11,7 +11,7 @@ const i18n = createI18n({
   messages,
 });
 
-export default boot(({ app }) => {
+export default defineBoot(({ app }) => {
   app.use(i18n);
 });
 

+ 2 - 2
src/boot/socket.io.js

@@ -1,4 +1,4 @@
-import { boot } from "quasar/wrappers";
+import { defineBoot } from "#q-app/wrappers";
 import { io } from "socket.io-client";
 import { reactive } from "vue";
 
@@ -73,7 +73,7 @@ const sendEvent = (room, eventName, data) => {
   });
 };
 
-export default boot(async () => {
+export default defineBoot(async () => {
   socket.on("connect", () => {
     console.log("Connected to websocket server!");
     state.isConnected = true;

+ 3 - 1
src/components/defaults/DefaultCepInput.vue

@@ -23,7 +23,9 @@ import masks from "src/helpers/masks.js";
 
 const $q = useQuasar();
 
-const model = defineModel();
+const model = defineModel({
+  type: Number,
+});
 
 const { disable, readonly, label, rules } = defineProps({
   disable: {

+ 3 - 1
src/components/defaults/DefaultCurrencyInput.vue

@@ -17,7 +17,9 @@ import { useCurrencyInput } from "vue-currency-input";
 import { computed, watch } from "vue";
 import { useI18n } from "vue-i18n";
 
-const model = defineModel();
+const model = defineModel({
+  type: Number,
+});
 
 const { options, disable, readonly, label, error, errorMessage } = defineProps({
   options: {

+ 3 - 3
src/components/defaults/DefaultInputDatePicker.vue

@@ -85,10 +85,10 @@ const { label, rules, time, dense } = defineProps({
   },
 });
 
-const qInputRef = useTemplateRef("inputRef");
+const qInputRef = useTemplateRef("inputRef", { type: String });
 
-const treatedDate = defineModel();
-const untreatedDate = defineModel("untreatedDate");
+const treatedDate = defineModel({ type: String });
+const untreatedDate = defineModel("untreatedDate", { type: String });
 
 const activePanel = ref("date");
 const date = ref();

+ 6 - 2
src/components/defaults/DefaultPasswordInput.vue

@@ -33,6 +33,10 @@ const { rules } = defineProps({
   },
 });
 
-const password = defineModel();
-const seePassword = defineModel("seePassword", { default: false });
+const password = defineModel({ type: String });
+const seePassword = defineModel("seePassword", {
+  default: false,
+  type: Boolean,
+});
+
 </script>

+ 1 - 1
src/components/defaults/DefaultTabs.vue

@@ -28,5 +28,5 @@ const { tabsItems } = defineProps({
   },
 });
 
-const tab = defineModel();
+const tab = defineModel({ type: String });
 </script>

+ 4 - 2
src/components/layout/LeftMenuLayout.vue

@@ -68,7 +68,7 @@
               >
             </q-item>
             <!-- Expansive Menu with children -->
-            <div v-else :key="item.name">
+            <div v-else :key="item.title">
               <template v-if="!miniState">
                 <q-tooltip
                   v-if="miniState"
@@ -269,7 +269,9 @@ const { navigationItems } = navigationStore();
 
 const version = "0.0.1";
 
-const miniState = ref(Cookies.get("miniState") === "true" ?? false);
+const miniStateCookies = Cookies.get("miniState")
+
+const miniState = ref(miniStateCookies === "true" ? true : false);
 
 const childrenAreActive = (children) => {
   if (!children) return false;

+ 1 - 1
src/components/layout/LeftMenuLayoutMobile.vue

@@ -91,7 +91,7 @@ import LogoSoftparLight from "src/assets/softpar_logo_light.svg";
 import LogoSoftparDark from "src/assets/softpar_logo_dark.svg";
 const route = useRoute();
 
-const leftDrawerOpen = defineModel();
+const leftDrawerOpen = defineModel({ type: Boolean });
 
 const { navigationItems } = navigationStore();
 

+ 31 - 41
src/components/regions/CitySelect.vue

@@ -28,11 +28,12 @@
 <script setup>
 import { getCities } from "src/api/city";
 import { ref, onMounted, watch } from "vue";
+import { normalizeString } from "src/helpers/utils";
 import { useI18n } from "vue-i18n";
 
 const emit = defineEmits(["selectedStateId"]);
 
-const { state, label, rules, initialId } = defineProps({
+const { state, label, rules, initialId, country } = defineProps({
   // This country prop is here for future use, maybe
   country: {
     type: Object,
@@ -72,32 +73,21 @@ const { state, label, rules, initialId } = defineProps({
   },
 });
 
-const selectedCity = defineModel();
+const selectedCity = defineModel({ type: Object });
 
 const loading = ref(false);
-const filteredCities = ref([]);
-const baseCities = ref([]);
+const baseOptions = ref([]);
 const cityOptions = ref([]);
 
 const filterFn = async (val, update) => {
-  if (!val) {
-    cityOptions.value = filteredCities.value.map((city) => ({
-      label: city.name,
-      value: city.id,
-      state_id: city.state_id,
-    }));
-  } else {
-    const needle = val.toLowerCase();
-    const cities = filteredCities.value.filter(
-      (v) => v.name.toLowerCase().indexOf(needle) > -1,
+  ensureOnlyPossibleOptions(country?.value, state?.value);
+  const needle = normalizeString(val);
+  cityOptions.value = cityOptions.value.filter((v) => {
+    return (
+      normalizeString(v.label).includes(needle) ||
+      normalizeString(v.code).includes(needle)
     );
-    cityOptions.value = cities.map((city) => ({
-      label: city.name,
-      value: city.id,
-      state_id: city.state_id,
-    }));
-  }
-
+  });
   update();
 };
 
@@ -105,14 +95,28 @@ const selectCityByName = (name) => {
   if (selectedCity.value?.label === name) {
     return;
   }
-  selectedCity.value = cityOptions.value.find((city) => city.label === name);
+  selectedCity.value = baseOptions.value.find((city) => city.label === name);
 };
 
 const selectCityById = (id) => {
   if (selectedCity.value?.value === id) {
     return;
   }
-  selectedCity.value = cityOptions.value.find((city) => city.value === id);
+  selectedCity.value = baseOptions.value.find((city) => city.value === id);
+};
+
+const ensureOnlyPossibleOptions = (country_id, state_id) => {
+  if (state_id) {
+    cityOptions.value = baseOptions.value.filter((city) => {
+      if (country_id) {
+        return city.country_id === country_id && city.state_id === state_id;
+      }
+      return city.state_id === state_id;
+    });
+  }
+  if (!!state_id && !country_id) {
+    cityOptions.value = baseOptions.value;
+  }
 };
 
 watch(
@@ -125,21 +129,7 @@ watch(
       selectedCity.value = null;
     }
     if (value) {
-      filteredCities.value = baseCities.value.filter(
-        (city) => city.state_id === value.value,
-      );
-      cityOptions.value = filteredCities.value.map((city) => ({
-        label: city.name,
-        value: city.id,
-        state_id: city.state_id,
-      }));
-    } else {
-      filteredCities.value = baseCities.value;
-      cityOptions.value = baseCities.value.map((city) => ({
-        label: city.name,
-        value: city.id,
-        state_id: city.state_id,
-      }));
+      ensureOnlyPossibleOptions(country?.value, value.value);
     }
   },
   { immediate: true },
@@ -154,13 +144,13 @@ watch(selectedCity, () => {
 onMounted(async () => {
   try {
     loading.value = true;
-    baseCities.value = await getCities();
-    filteredCities.value = baseCities.value;
-    cityOptions.value = baseCities.value.map((city) => ({
+    const baseCities = await getCities();
+    baseOptions.value = baseCities.map((city) => ({
       label: city.name,
       value: city.id,
       state_id: city.state_id,
     }));
+    cityOptions.value = baseOptions.value;
     if (initialId) {
       selectCityById(initialId);
     }

+ 1 - 1
src/components/regions/CountrySelect.vue

@@ -56,7 +56,7 @@ const { label, rules, initialId } = defineProps({
   },
 });
 
-const selectedCountry = defineModel();
+const selectedCountry = defineModel({ type: Object });
 
 const loading = ref(false);
 const baseCountry = ref([]);

+ 27 - 47
src/components/regions/StateSelect.vue

@@ -28,6 +28,7 @@
 <script setup>
 import { getStates } from "src/api/state";
 import { ref, onMounted, watch } from "vue";
+import { normalizeString } from "src/helpers/utils";
 import { useI18n } from "vue-i18n";
 
 const emit = defineEmits(["selectedCountryId"]);
@@ -66,34 +67,21 @@ const { country, label, rules, initialId } = defineProps({
   },
 });
 
-const selectedState = defineModel();
+const selectedState = defineModel({ type: Object });
 
 const loading = ref(false);
-const filteredStates = ref([]);
-const baseStates = ref([]);
+const baseOptions = ref([]);
 const stateOptions = ref([]);
 
 const filterFn = (val, update) => {
-  if (!val) {
-    stateOptions.value = filteredStates.value.map((state) => ({
-      label: state.name,
-      value: state.id,
-      code: state.code,
-      country_id: state.country_id,
-    }));
-  } else {
-    const needle = val.toLowerCase();
-    filteredStates.value = filteredStates.value.filter(
-      (v) => v.name.toLowerCase().indexOf(needle) > -1,
+  ensureOnlyPossibleOptions(country?.value);
+  const needle = normalizeString(val);
+  stateOptions.value = stateOptions.value.filter((v) => {
+    return (
+      normalizeString(v.label).includes(needle) ||
+      normalizeString(v.code).includes(needle)
     );
-    stateOptions.value = filteredStates.value.map((state) => ({
-      label: state.name,
-      value: state.id,
-      code: state.code,
-      country_id: state.country_id,
-    }));
-  }
-
+  });
   update();
 };
 
@@ -101,23 +89,31 @@ const selectStateById = async (id) => {
   if (selectedState.value?.value === id) {
     return;
   }
-  selectedState.value = stateOptions.value.find((state) => state.value === id);
+  selectedState.value = baseOptions.value.find((state) => state.value === id);
 };
 
 const selectStateByName = (name) => {
   if (selectedState.value?.label === name) {
     return;
   }
-  selectedState.value = stateOptions.value.find(
-    (state) => state.label === name,
-  );
+  selectedState.value = baseOptions.value.find((state) => state.label === name);
 };
 
 const selectStateByCode = (code) => {
   if (selectedState.value?.code === code) {
     return;
   }
-  selectedState.value = stateOptions.value.find((state) => state.code === code);
+  selectedState.value = baseOptions.value.find((state) => state.code === code);
+};
+
+const ensureOnlyPossibleOptions = (country_id) => {
+  if (country_id) {
+    stateOptions.value = baseOptions.value.filter(
+      (state) => state.country_id === country_id,
+    );
+  } else {
+    stateOptions.value = baseOptions.value;
+  }
 };
 
 watch(
@@ -130,23 +126,7 @@ watch(
       selectedState.value = null;
     }
     if (value) {
-      filteredStates.value = baseStates.value.filter(
-        (state) => state.country_id === value.value,
-      );
-      stateOptions.value = filteredStates.value.map((state) => ({
-        label: state.name,
-        value: state.id,
-        code: state.code,
-        country_id: state.country_id,
-      }));
-    } else {
-      filteredStates.value = baseStates.value;
-      stateOptions.value = baseStates.value.map((state) => ({
-        label: state.name,
-        value: state.id,
-        code: state.code,
-        country_id: state.country_id,
-      }));
+      ensureOnlyPossibleOptions(value.value);
     }
   },
   { immediate: true },
@@ -161,14 +141,14 @@ watch(selectedState, () => {
 onMounted(async () => {
   try {
     loading.value = true;
-    baseStates.value = await getStates();
-    filteredStates.value = baseStates.value;
-    stateOptions.value = baseStates.value.map((state) => ({
+    const baseStates = await getStates();
+    baseOptions.value = baseStates.map((state) => ({
       label: state.name,
       value: state.id,
       code: state.code,
       country_id: state.country_id,
     }));
+    stateOptions.value = baseOptions.value;
     if (initialId) {
       selectStateById(initialId);
     }

+ 10 - 3
src/composables/useFormUpdateTracker.js

@@ -1,5 +1,5 @@
 import { reactive, computed, toRaw, isReactive, watch } from "vue";
-import isEqual from 'fast-deep-equal';
+import isEqual from "fast-deep-equal";
 
 export const useFormUpdateTracker = (initialFormValue) => {
   const form = reactive(deepClone(initialFormValue));
@@ -114,7 +114,12 @@ function diff(currentObj, baseObj) {
  * @param {string} parentKey The base key for nested properties.
  */
 function buildFormData(formData, data, parentKey = "") {
-  if (data === null || data === undefined) {
+  if (data === undefined) {
+    return;
+  }
+
+  if (data == null) {
+    formData.append(parentKey, null);
     return;
   }
 
@@ -159,7 +164,9 @@ function deepClone(obj) {
     try {
       return structuredClone(obj);
     } catch (e) {
-      console.warn("structuredClone not supported, using JSON methods instead");
+      console.warn(
+        "structuredClone not supported, using JSON methods instead: " + e,
+      );
     }
   }
   return JSON.parse(JSON.stringify(obj));

+ 57 - 35
src/composables/useScroll.js

@@ -1,59 +1,81 @@
-import { unref } from 'vue'
+import { unref } from "vue";
 
 export const useScroll = () => {
   /**
-   * Scrolls to a specific component within a scroll container
-   * @param {Ref<Component>} targetRef - Reference to the component to scroll to
-   * @param {Ref<Component>} containerRef - Reference to the Quasar ScrollArea component
-   * @param {Object} options - Scroll options
-   * @param {number} options.offset - Offset from the top in pixels (default: 50)
-   * @param {number} options.duration - Animation duration in milliseconds (default: 150)
-   * @returns {boolean} - Whether the scroll was successful
+   * Scrolls to a target element within a scroll container.
+   * The function is agnostic to whether the container is a QScrollArea or a native DOM element.
+   *
+   * @param {Ref<Component|HTMLElement>} targetRef - Ref to the target component or DOM element.
+   * @param {Ref<Component|HTMLElement>} containerRef - Ref to the container (QScrollArea or a scrollable DOM element like body).
+   * @param {Object} options - Scroll options.
+   * @param {number} options.offset - Offset from the top in pixels (default: 50).
+   * @param {number} options.duration - Animation duration for QScrollArea (default: 150). Note: duration is not supported for native scrolling.
+   * @returns {boolean} - Whether the scroll was successful.
    */
   const scrollToComponent = (targetRef, containerRef, options = {}) => {
-    const {
-      offset = 50,
-      duration = 150
-    } = options
+    const { offset = 150, duration = 150 } = options;
 
-    const target = unref(targetRef)
-    const container = unref(containerRef)
+    const targetInstance = unref(targetRef);
+    const containerInstance = unref(containerRef);
 
-    const targetElement = target?.$el
-    const containerElement = container?.$el
+    const targetElement = targetInstance?.$el ?? targetInstance;
+
+    const containerElement = containerInstance?.$el ?? containerInstance;
 
     if (!targetElement || !containerElement) {
-      console.warn('useScroll: Target or container element not found')
-      return false
+      console.warn("useScroll: Target or container element not found");
+      return false;
     }
 
     try {
-      let currentElement = targetElement
-      let offsetTop = 0
+      let currentElement = targetElement;
+      let offsetTop = 0;
 
-      // Calculate total offset up to the scroll container
       while (currentElement && currentElement !== containerElement) {
-        offsetTop += currentElement.offsetTop
-        currentElement = currentElement.offsetParent
+        offsetTop += currentElement.offsetTop;
+        if (
+          containerElement.contains(currentElement.offsetParent) ||
+          containerElement === document.body
+        ) {
+          currentElement = currentElement.offsetParent;
+        } else {
+          currentElement = null;
+        }
       }
 
-      if (!currentElement) {
-        console.warn('useScroll: Target is not a child of the container')
-        return false
+      if (
+        containerElement !== document.body &&
+        !containerElement.contains(targetElement)
+      ) {
+        console.warn("useScroll: Target is not a child of the container");
+        return false;
       }
 
-      const targetPosition = Math.max(0, offsetTop - offset)
+      const targetPosition = Math.max(0, offsetTop - offset);
 
-      container.setScrollPosition('vertical', targetPosition, duration)
+      if (typeof containerInstance.setScrollPosition === "function") {
+        containerInstance.setScrollPosition(
+          "vertical",
+          targetPosition,
+          duration,
+        );
+      } else {
+        const scrollable =
+          containerElement === document.body ? window : containerElement;
+        scrollable.scrollTo({
+          top: targetPosition,
+          behavior: "smooth",
+        });
+      }
 
-      return true
+      return true;
     } catch (error) {
-      console.error('useScroll: Error while scrolling', error)
-      return false
+      console.error("useScroll: Error while scrolling", error);
+      return false;
     }
-  }
+  };
 
   return {
-    scrollToComponent
-  }
-}
+    scrollToComponent,
+  };
+};

+ 75 - 21
src/composables/useSubmitHandler.js

@@ -1,40 +1,94 @@
-import { ref } from "vue";
+import { ref, nextTick } from "vue";
+
+export function useSubmitHandler(options = {}) {
+  const { onSuccess, onError, formRef, scrollFn, containerRef } = options;
 
-export function useSubmitHandler(onSuccess) {
   const loading = ref(false);
-  const serverErrors = ref({});
+  const validationErrors = ref({});
+
+  const getFormRefs = () => {
+    const refs = formRef?.value;
+    if (!refs) return [];
+    return Array.isArray(refs) ? refs : [refs];
+  };
+
+  const scrollToFirstError = async () => {
+    if (!formRef?.value || !scrollFn) return;
+    await nextTick();
+
+    const refsToSearch = getFormRefs();
+
+    for (const ref of refsToSearch) {
+      if (!ref) continue;
+      const components = ref.getValidationComponents();
+      if (!components) continue;
+
+      const firstErrorComponent = components.find((c) => c.hasError);
+
+      if (firstErrorComponent) {
+        const container = containerRef?.value || containerRef;
+        firstErrorComponent.focus();
+        scrollFn(firstErrorComponent, container);
+        return;
+      }
+    }
+  };
 
   const execute = async (apiCallThunk) => {
     loading.value = true;
-    serverErrors.value = {};
+    validationErrors.value = {};
 
-    try {
-      const response = await apiCallThunk();
+    let allValid = true;
+    const refsToValidate = getFormRefs();
 
-      if (onSuccess && typeof onSuccess === "function") {
-        onSuccess(response);
-      }
-    } catch (error) {
-      if (error.response?.status === 422) {
-        const errors = error.response.data.errors || {};
-        for (const key in errors) {
-          serverErrors.value[key] = errors[key][0];
+    if (refsToValidate.length > 0) {
+      for (const ref of refsToValidate) {
+        if (ref) {
+          const success = await ref.validate(true);
+          if (!success) {
+            allValid = false;
+          }
         }
-      } else {
-        console.error(
-          "An unexpected error occurred in useSubmitHandler:",
-          error,
-        );
-        throw error;
       }
+    }
+
+    if (!allValid) {
+      loading.value = false;
+      await scrollToFirstError();
+      throw new Error("Frontend validation failed.");
+    }
+
+    try {
+      const response = await apiCallThunk();
+      if (typeof onSuccess === "function") await onSuccess(response);
+    } catch (error) {
+      await handleError(error);
     } finally {
       loading.value = false;
     }
   };
 
+  const handleError = async (error) => {
+    if (error?.response?.status === 422) {
+      const errors = error.response.data.errors || {};
+      for (const key in errors) {
+        const message = errors[key][0];
+        validationErrors.value[key] = message;
+      }
+      await scrollToFirstError();
+    }
+
+    await nextTick();
+    if (typeof onError === "function") {
+      await onError(error);
+    } else {
+      throw error;
+    }
+  };
+
   return {
     loading,
-    serverErrors,
+    validationErrors,
     execute,
   };
 }

+ 9 - 126
src/helpers/utils.js

@@ -79,129 +79,12 @@ const formatDateYMDtoDMY = (dateTime) => {
   return formattedDate;
 };
 
-/**
- * @description Checa a moeda selecionada.
- * @param {number} moeda moeda selecionada.
- * @returns {object} opções de moeda.
- */
-const checaMoeda = (moeda) => {
-  let currencyOptions = {};
-  if (moeda == 1) {
-    currencyOptions = {
-      locale: "pt-BR",
-      currency: "BRL",
-      currencyDisplay: "symbol",
-      hideCurrencySymbolOnFocus: false,
-      hideGroupingSeparatorOnFocus: false,
-      hideNegligibleDecimalDigitsOnFocus: false,
-      autoDecimalDigits: true,
-      useGrouping: true,
-      accountingSign: false,
-    };
-  } else if (moeda == 2) {
-    currencyOptions = {
-      currency: "PYG",
-      locale: "es-PY",
-      valueAsInteger: true,
-      distractionFree: true,
-      precision: 0,
-      autoDecimalMode: true,
-      valueRange: { min: 0 },
-      allowNegative: true,
-    };
-  } else if (moeda == 3) {
-    currencyOptions = {
-      locale: "en-US",
-      currency: "USD",
-      currencyDisplay: "symbol",
-      hideCurrencySymbolOnFocus: true,
-      hideGroupingSeparatorOnFocus: true,
-      hideNegligibleDecimalDigitsOnFocus: false,
-      autoDecimalDigits: true,
-      useGrouping: true,
-      accountingSign: false,
-    };
-  }
-  return currencyOptions;
-};
-
-/**
- * @description Filtra a moeda.
- * @param {number} value valor.
- * @returns {string} valor formatado.
- */
-const filterCurrency = (value) => {
-  if (value) {
-    value = parseFloat(value);
-    return value.toLocaleString("pt-BR", {
-      style: "currency",
-      currency: "BRL",
-    });
-  }
-  return value;
-};
-
-/**
- * @description Filtra a unidade de medida.
- * @param {number} value valor.
- * @returns {string} valor formatado.
- */
-const filterUnidadeMedida = (value) => {
-  return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
-};
-
-/**
- * @description Valida se a data é válida.
- * @param {string} date data.
- * @returns {boolean} true se a data é válida, false caso contrário.
- */
-const validaData = (date) => {
-  const regex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
-  return regex.test(date);
-};
-
-/**
- * @description Valida se a hora é válida.
- * @param {string} time hora.
- * @returns {boolean} true se a hora é válida, false caso contrário.
- */
-const validaHora = (time) => {
-  const regex = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/;
-  return regex.test(time);
-};
-
-/**
- * @description Valida se a data e hora são válidas.
- * @param {string} dataHora data e hora.
- * @returns {boolean} true se a data e hora são válidas, false caso contrário.
- */
-const validaDataHora = (dataHora) => {
-  const regex =
-    /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}\s([0-1][0-9]|2[0-3]):[0-5][0-9]$/;
-  return regex.test(dataHora);
-};
-
-/**
- * @description Formata a quantidade.
- * @param {number} value valor.
- * @returns {string} valor formatado.
- */
-const formatQuantity = (value) => {
-  if (value) {
-    return value
-      .toString()
-      .replace(/[^0-9]/g, "")
-      .replace(/\B(?=(\d{3})+(?!\d))/g, ".");
-  }
-  return value;
-};
-
 /**
  * @description Formata a moeda.
  * @param {number} value valor.
  * @returns {string} valor formatado.
  */
-const formatCurrency = (value) => {
+const formatToBRLCurrency = (value) => {
   if (value != null) {
     value = parseFloat(value);
     return value.toLocaleString("pt-BR", {
@@ -213,17 +96,17 @@ const formatCurrency = (value) => {
   return value;
 };
 
+const normalizeString = (val) =>
+  val
+    .toLowerCase()
+    .normalize("NFKD")
+    .replace(/[\u0300-\u036f~]/g, "");
+
 export {
   formatDateDMYtoYMD,
   formatDateYMDtoDMY,
   excerpt,
   convertDateTime,
-  checaMoeda,
-  filterCurrency,
-  filterUnidadeMedida,
-  validaData,
-  validaHora,
-  validaDataHora,
-  formatQuantity,
-  formatCurrency,
+  formatToBRLCurrency,
+  normalizeString,
 };

+ 12 - 13
src/pages/city/components/AddEditCityDialog.vue

@@ -8,8 +8,8 @@
             v-model="form.name"
             :label="$t('common.terms.name')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.name"
-            :error-message="serverErrors.name"
+            :error="!!serverErrors?.name"
+            :error-message="serverErrors?.name"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.name = null"
           />
@@ -18,8 +18,8 @@
             v-model="selectedCountry"
             :label="$t('ui.navigation.country')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.country_id"
-            :error-message="serverErrors.country_id"
+            :error="!!serverErrors?.country_id"
+            :error-message="serverErrors?.country_id"
             :initial-id="city ? city.country_id : null"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.country_id = null"
@@ -30,8 +30,8 @@
             :initial-id="form.state_id"
             :label="$t('ui.navigation.state')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.state_id"
-            :error-message="serverErrors.state_id"
+            :error="!!serverErrors?.state_id"
+            :error-message="serverErrors?.state_id"
             class="col-md-6 col-12"
             @selected-country-id="countrySelectRef.selectCountryById($event)"
             @update:model-value="serverErrors.state_id = null"
@@ -41,8 +41,8 @@
             :label="$t('common.terms.status')"
             :options="statusOptions"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.status"
-            :error-message="serverErrors.status"
+            :error="!!serverErrors?.status"
+            :error-message="serverErrors?.status"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.status = null"
           />
@@ -112,7 +112,10 @@ const {
   loading,
   serverErrors,
   execute: submitForm,
-} = useSubmitHandler(() => onDialogOK(true));
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
 
 const selectedCountry = ref(null);
 const selectedState = ref(null);
@@ -126,10 +129,6 @@ const statusOptions = ref([
 ]);
 
 const onOKClick = async () => {
-  if (!(await formRef.value.validate())) {
-    return;
-  }
-
   if (city) {
     await submitForm(() => updateCity(getUpdatedFields.value, city.id));
   } else {

+ 10 - 11
src/pages/country/components/AddEditCountryDialog.vue

@@ -8,8 +8,8 @@
             v-model="form.name"
             :label="$t('common.terms.name')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.name"
-            :error-message="serverErrors.name"
+            :error="!!serverErrors?.name"
+            :error-message="serverErrors?.name"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.name = null"
           />
@@ -17,8 +17,8 @@
             v-model="form.code"
             :label="$t('common.terms.code')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.code"
-            :error-message="serverErrors.code"
+            :error="!!serverErrors?.code"
+            :error-message="serverErrors?.code"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.code = null"
           />
@@ -27,8 +27,8 @@
             :label="$t('common.terms.status')"
             :options="statusOptions"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.status"
-            :error-message="serverErrors.status"
+            :error="!!serverErrors?.status"
+            :error-message="serverErrors?.status"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.status = null"
           />
@@ -94,7 +94,10 @@ const {
   loading,
   serverErrors,
   execute: submitForm,
-} = useSubmitHandler(() => onDialogOK(true));
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
 
 const selectedStatus = ref({
   label: t("common.status.active"),
@@ -106,10 +109,6 @@ const statusOptions = ref([
 ]);
 
 const onOKClick = async () => {
-  if (!(await formRef.value.validate())) {
-    return;
-  }
-
   if (country) {
     await submitForm(() => updateCountry(getUpdatedFields.value, country.id));
   } else {

+ 4 - 1
src/pages/dashboard/components/DatePeriodSelector.vue

@@ -26,7 +26,10 @@ const selectedPeriod = defineModel("selectedPeriod", {
   type: String,
   default: "month",
 });
-const selectedEventId = defineModel("selectedEventId");
+
+const selectedEventId = defineModel("selectedEventId", {
+  type: Number,
+});
 
 const selectedEvent = ref(null);
 

+ 12 - 12
src/pages/state/components/AddEditStateDialog.vue

@@ -8,8 +8,8 @@
             v-model="form.name"
             :label="$t('common.terms.name')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.name"
-            :error-message="serverErrors.name"
+            :error="!!serverErrors?.name"
+            :error-message="serverErrors?.name"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.name = null"
           />
@@ -17,8 +17,8 @@
             v-model="form.code"
             :label="$t('common.terms.code')"
             :rules="[inputRules.required, inputRules.max(2)]"
-            :error="!!serverErrors.code"
-            :error-message="serverErrors.code"
+            :error="!!serverErrors?.code"
+            :error-message="serverErrors?.code"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.code = null"
           />
@@ -27,8 +27,8 @@
             :label="$t('ui.navigation.country')"
             :rules="[inputRules.required]"
             :initial-id="form.country_id"
-            :error="!!serverErrors.country_id"
-            :error-message="serverErrors.country_id"
+            :error="!!serverErrors?.country_id"
+            :error-message="serverErrors?.country_id"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.country_id = null"
           />
@@ -37,8 +37,8 @@
             :label="$t('common.terms.status')"
             :options="statusOptions"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.status"
-            :error-message="serverErrors.status"
+            :error="!!serverErrors?.status"
+            :error-message="serverErrors?.status"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.status = null"
           />
@@ -106,7 +106,10 @@ const {
   loading,
   serverErrors,
   execute: submitForm,
-} = useSubmitHandler(() => onDialogOK(true));
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
 
 const selectedCountry = ref(null);
 const selectedStatus = ref({
@@ -119,9 +122,6 @@ const statusOptions = ref([
 ]);
 
 const onOKClick = async () => {
-  if (!(await formRef.value.validate())) {
-    return;
-  }
   if (state) {
     await submitForm(() => updateState(getUpdatedFields.value, state.id));
   } else {

+ 12 - 12
src/pages/users/components/AddEditUserDialog.vue

@@ -9,8 +9,8 @@
             :label="$t('common.terms.name')"
             :hint="$t('user.profile.name_and_surname')"
             :rules="[inputRules.required]"
-            :error="!!serverErrors.name"
-            :error-message="serverErrors.name"
+            :error="!!serverErrors?.name"
+            :error-message="serverErrors?.name"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.name = null"
           />
@@ -18,8 +18,8 @@
             v-model="selectedUserType"
             :rules="[inputRules.required]"
             :type="form.type"
-            :error="!!serverErrors.email"
-            :error-message="serverErrors.email"
+            :error="!!serverErrors?.email"
+            :error-message="serverErrors?.email"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.email = null"
           />
@@ -27,8 +27,8 @@
             v-model="form.email"
             label="Email"
             :rules="[inputRules.email]"
-            :error="!!serverErrors.type"
-            :error-message="serverErrors.type"
+            :error="!!serverErrors?.type"
+            :error-message="serverErrors?.type"
             class="col-12"
             @update:model-value="serverErrors.type = null"
           />
@@ -39,8 +39,8 @@
                 ? [inputRules.password]
                 : [inputRules.required, inputRules.password]
             "
-            :error="!!serverErrors.password"
-            :error-message="serverErrors.password"
+            :error="!!serverErrors?.password"
+            :error-message="serverErrors?.password"
             class="col-md-6 col-12"
             @update:model-value="serverErrors.password = null"
           />
@@ -121,12 +121,12 @@ const {
   loading,
   serverErrors,
   execute: submitForm,
-} = useSubmitHandler(() => onDialogOK(true));
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+});
 
 const onOKClick = async () => {
-  if (!(await formRef.value.validate())) {
-    return;
-  }
   if (user) {
     await submitForm(() => updateUser(getUpdatedFields.value, user.id));
   } else {

+ 3 - 1
src/pages/users/components/UserTypeSelect.vue

@@ -52,7 +52,9 @@ const { label, rules, type } = defineProps({
   },
 });
 
-const selectedUserType = defineModel();
+const selectedUserType = defineModel({
+  type: Object,
+});
 const userTypeOptions = ref([]);
 const filteredOptions = ref([]);
 const isLoading = ref(false);

+ 2 - 2
src/router/index.js

@@ -1,4 +1,4 @@
-import { route } from "quasar/wrappers";
+import { defineRouter } from "#q-app/wrappers";
 import {
   createRouter,
   createMemoryHistory,
@@ -20,7 +20,7 @@ import { useAuth } from "src/composables/useAuth";
  * with the Router instance.
  */
 
-export default route(function (/* { store, ssrContext } */) {
+export default defineRouter(function (/* { store, ssrContext } */) {
   const createHistory = process.env.SERVER
     ? createMemoryHistory
     : process.env.VUE_ROUTER_MODE === "history"

+ 2 - 2
src/stores/index.js

@@ -1,4 +1,4 @@
-import { store } from 'quasar/wrappers'
+import { defineStore } from '#q-app/wrappers'
 import { createPinia } from 'pinia'
 
 /*
@@ -10,7 +10,7 @@ import { createPinia } from 'pinia'
  * with the Store instance.
  */
 
-export default store((/* { ssrContext } */) => {
+export default defineStore((/* { ssrContext } */) => {
   const pinia = createPinia()
 
   // You can add Pinia plugins here

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott