Parcourir la source

refactor: permite alterar somente senha na parte de dados

Gustavo Mantovani il y a 1 mois
Parent
commit
96e365c0be

+ 1 - 1
package.json

@@ -13,7 +13,7 @@
     "test": "echo \"No test specified\" && exit 0",
     "dev": "quasar dev",
     "build": "quasar build",
-    "deploy": "aws --profile michel-softpar --region sa-east-1 s3 sync ./dist/spa s3://alugap-backoffice-test --delete"
+    "deploy": "aws --profile michel-softpar --region sa-east-1 s3 sync ./dist/spa s3://alugap-owner-office-test --delete"
   },
   "dependencies": {
     "@quasar/extras": "^1.16.17",

+ 195 - 0
src/composables/useFormUpdateTracker.js

@@ -0,0 +1,195 @@
+import { reactive, computed, toRaw, isReactive, watch } from "vue";
+import isEqual from "fast-deep-equal";
+
+export const useFormUpdateTracker = (initialFormValue) => {
+  const form = reactive(deepClone(initialFormValue));
+
+  let originalForm = deepClone(initialFormValue);
+
+  const updatedFields = reactive({});
+
+  const getUpdatedFields = computed(() => {
+    return updatedFields;
+  });
+
+  const hasUpdatedFields = computed(() => {
+    return Object.keys(updatedFields).length > 0;
+  });
+
+  watch(
+    form,
+    (newValue) => {
+      const changes = diff(toRaw(newValue), originalForm);
+
+      Object.keys(updatedFields).forEach((key) => delete updatedFields[key]);
+
+      Object.assign(updatedFields, changes);
+    },
+    { deep: true },
+  );
+
+  const resetUpdateForm = () => {
+    const newFormState = deepClone(originalForm);
+
+    Object.keys(form).forEach((key) => delete form[key]);
+
+    Object.assign(form, newFormState);
+  };
+
+  const setUpdateFormAsOriginal = () => {
+    originalForm = deepClone(toRaw(form));
+  };
+
+  const getFormAsFormData = () => {
+    const formData = new FormData();
+
+    buildFormData(formData, form);
+
+    return formData;
+  };
+
+  const getUpdatedFieldsAsFormData = (spoofMethod = null) => {
+    const formData = new FormData();
+
+    buildFormData(formData, updatedFields);
+
+    if (spoofMethod) {
+      formData.append("_method", spoofMethod.toUpperCase());
+    }
+
+    return formData;
+  };
+
+  return {
+    form,
+    getUpdatedFields,
+    hasUpdatedFields,
+    resetUpdateForm,
+    setUpdateFormAsOriginal,
+    getFormAsFormData,
+    getUpdatedFieldsAsFormData,
+  };
+};
+
+/**
+ * A recursive function to find the differences between two objects.
+ * It returns a new object containing only the keys that have been added,
+ * changed, or removed (set to null).
+ * @param {object} currentObj The current state of the object.
+ * @param {object} baseObj The original object to compare against.
+ * @returns {object} An object with only the changed, new, or deleted keys.
+ */
+function diff(currentObj, baseObj) {
+  const changes = {};
+
+  const currentKeys = Object.keys(currentObj);
+
+  const baseKeys = Object.keys(baseObj);
+
+  for (const key of currentKeys) {
+    const currentValue = currentObj[key];
+
+    const baseValue = baseObj[key];
+
+    if (!isEqual(currentValue, baseValue)) {
+      if (
+        currentValue &&
+        typeof currentValue === "object" &&
+        !Array.isArray(currentValue) &&
+        baseValue &&
+        typeof baseValue === "object" &&
+        !Array.isArray(baseValue)
+      ) {
+        const nestedChanges = diff(currentValue, baseValue);
+
+        if (Object.keys(nestedChanges).length > 0) {
+          changes[key] = nestedChanges;
+        }
+      } else {
+        changes[key] = deepClone(currentValue);
+      }
+    }
+  }
+
+  for (const key of baseKeys) {
+    if (!Object.prototype.hasOwnProperty.call(currentObj, key)) {
+      changes[key] = null;
+    }
+  }
+
+  return changes;
+}
+
+/**
+ * Recursively builds a FormData object from a nested plain object using bracket notation
+ * for keys, which is compatible with PHP/Laravel backends.
+ * @param {FormData} formData The FormData instance.
+ * @param {object} data The plain object to serialize.
+ * @param {string} parentKey The base key for nested properties.
+ */
+function buildFormData(formData, data, parentKey = "") {
+  if (data === undefined) {
+    return;
+  }
+
+  if (data == null) {
+    formData.append(parentKey, null);
+
+    return;
+  }
+
+  if (Array.isArray(data)) {
+    data.forEach((value, index) => {
+      buildFormData(formData, value, `${parentKey}[${index}]`);
+    });
+
+    return;
+  }
+
+  if (
+    typeof data === "object" &&
+    !(data instanceof File) &&
+    !(data instanceof Date)
+  ) {
+    Object.keys(data).forEach((key) => {
+      const propName = parentKey ? `${parentKey}[${key}]` : key;
+
+      buildFormData(formData, data[key], propName);
+    });
+
+    return;
+  }
+
+
+  let valueToAppend = data;
+
+  if (data instanceof Date) {
+    valueToAppend = data.toISOString().slice(0, 19).replace("T", " ");
+  }
+
+  formData.append(parentKey, valueToAppend);
+}
+
+/** * Deep clones an object using structuredClone if available,
+ * otherwise falls back to JSON methods.
+ * If the object is reactive, it converts it to a raw object first.
+ * @param {object} obj The object to clone.
+ * @returns {object} A deep clone of the input object.
+ */
+function deepClone(obj) {
+  if (obj && isReactive(obj)) {
+    obj = toRaw(obj);
+  }
+
+  if (typeof structuredClone === "function") {
+    try {
+      return structuredClone(obj);
+    } catch (e) {
+      console.warn(
+        "structuredClone not supported, using JSON methods instead: " + e,
+      );
+    }
+  }
+
+  return JSON.parse(JSON.stringify(obj));
+}

+ 51 - 21
src/pages/profile/ProfilePage.vue

@@ -80,7 +80,7 @@
 
     <ProfileEditDialog
       v-model="editDialog"
-      :form="editForm"
+      v-model:form="editForm"
       :input-rules="inputRules"
       :server-errors="serverErrors"
       :submitting="submitting"
@@ -91,10 +91,13 @@
 </template>
 
 <script setup>
-import { computed, onMounted, reactive, ref } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
+
 import { updateUser } from "src/api/user";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+
 import { userStore } from "src/stores/user";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
@@ -103,6 +106,7 @@ import ProfileEditDialog from "./components/ProfileEditDialog.vue";
 defineOptions({ name: "ProfilePage" });
 
 const { inputRules } = useInputRules();
+
 const currentUserStore = userStore();
 
 const {
@@ -111,10 +115,13 @@ const {
   serverErrors,
 } = useSubmitHandler(() => closeEditDialog());
 
-const loading = ref(false);
-const editDialog = ref(false);
-
-const editForm = reactive({
+const {
+  form: editForm,
+  getUpdatedFields,
+  hasUpdatedFields,
+  resetUpdateForm,
+  setUpdateFormAsOriginal,
+} = useFormUpdateTracker({
   name: "",
   document: "",
   email: "",
@@ -122,8 +129,13 @@ const editForm = reactive({
   confirmPassword: "",
 });
 
+const loading = ref(false);
+
+const editDialog = ref(false);
+
 const userData = computed(() => {
   const user = currentUserStore.user ?? {};
+
   return {
     id: user?.id ?? null,
     name: user?.name ?? user?.full_name ?? "",
@@ -134,6 +146,7 @@ const userData = computed(() => {
 
 const loadUser = async () => {
   loading.value = true;
+
   try {
     await currentUserStore.fetchUser();
   } finally {
@@ -141,7 +154,7 @@ const loadUser = async () => {
   }
 };
 
-const resetEditForm = () => {
+const syncFormWithUser = () => {
   Object.assign(editForm, {
     name: userData.value.name,
     document: userData.value.document,
@@ -149,42 +162,59 @@ const resetEditForm = () => {
     password: "",
     confirmPassword: "",
   });
-  serverErrors.value = {};
+
+  setUpdateFormAsOriginal();
 };
 
 const openEditDialog = () => {
-  resetEditForm();
+  syncFormWithUser();
+  serverErrors.value = {};
   editDialog.value = true;
 };
 
 const closeEditDialog = () => {
   editDialog.value = false;
-  resetEditForm();
+  resetUpdateForm();
 };
 
-const buildPayload = (formValues) => {
-  const payload = {
-    name: formValues.name,
-    email: formValues.email,
-    document_number: formValues.document,
-  };
+const buildPayload = () => {
+  const updated = { ...getUpdatedFields.value };
 
-  if (formValues.password) {
-    payload.password = formValues.password;
+  if (updated.document) {
+    updated.document_number = updated.document;
+    delete updated.document;
   }
 
-  return payload;
+  if (!updated.password) {
+    delete updated.password;
+  }
+
+  delete updated.confirmPassword;
+
+  return updated;
 };
 
-const submitProfileUpdate = async (formValues) => {
+const submitProfileUpdate = async () => {
   if (!userData.value.id) return;
 
+  if (!hasUpdatedFields.value) {
+    closeEditDialog();
+    return;
+  }
+
   await execute(async () => {
-    await updateUser(buildPayload(formValues), userData.value.id);
+    const payload = buildPayload();
+
+    await updateUser(payload, userData.value.id);
+
     await loadUser();
+
+    setUpdateFormAsOriginal();
   });
 };
 
+watch(userData, syncFormWithUser, { immediate: true });
+
 onMounted(loadUser);
 </script>
 

+ 29 - 41
src/pages/profile/components/ProfileEditDialog.vue

@@ -1,6 +1,6 @@
 <template>
   <q-dialog v-model="dialogModel" persistent>
-    <q-card style="max-width: 100%; border-radius: 8px">
+    <q-card style="width: 900px; max-width: 95vw; border-radius: 8px">
       <DefaultDialogHeader title="Editar meus dados" @close="closeDialog" />
 
       <q-form
@@ -21,7 +21,7 @@
 
             <div class="grid-2">
               <q-input
-                v-model="localForm.name"
+                v-model="form.name"
                 bg-color="white"
                 label="Nome"
                 outlined
@@ -32,7 +32,7 @@
               />
 
               <q-input
-                v-model="localForm.document"
+                v-model="form.document"
                 bg-color="white"
                 label="Número de documento"
                 outlined
@@ -52,7 +52,7 @@
 
             <div class="grid-3">
               <q-input
-                v-model="localForm.email"
+                v-model="form.email"
                 bg-color="white"
                 label="Email"
                 outlined
@@ -63,7 +63,7 @@
               />
 
               <DefaultPasswordInput
-                v-model="localForm.password"
+                v-model="form.password"
                 bg-color="white"
                 label="Senha"
                 outlined
@@ -74,7 +74,7 @@
               />
 
               <DefaultPasswordInput
-                v-model="localForm.confirmPassword"
+                v-model="form.confirmPassword"
                 bg-color="white"
                 label="Confirmar Senha"
                 outlined
@@ -107,62 +107,50 @@
 </template>
 
 <script setup>
-import { computed, reactive, ref, watch } from "vue";
+import { ref, computed } from "vue";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
 
 const dialogModel = defineModel({ default: false });
 
+const form = defineModel("form", {
+  default: () => ({
+    name: "",
+    document: "",
+    email: "",
+    password: "",
+    confirmPassword: "",
+  }),
+});
+
 const emit = defineEmits(["submit", "close"]);
 
-const { form, inputRules, serverErrors, submitting } = defineProps({
-  // eslint-disable-next-line vue/require-default-prop
-  form: Object,
+const props = defineProps({
   // eslint-disable-next-line vue/require-default-prop
   inputRules: Object,
-  serverErrors: { type: Object, default: () => ({}) },
+  serverErrors: {
+    type: Object,
+    default: () => ({}),
+  },
   submitting: Boolean,
 });
 
 const editFormRef = ref(null);
 
-const localForm = reactive({
-  name: "",
-  document: "",
-  email: "",
-  password: "",
-  confirmPassword: "",
-});
-
 const passwordRules = computed(() =>
-  localForm.password ? [inputRules.min(6)] : [],
+  form.value.password ? [props.inputRules.min(6)] : [],
 );
 
 const confirmPasswordRules = computed(() =>
-  localForm.password
-    ? [inputRules.required, inputRules.samePassword(localForm.password)]
+  form.value.password
+    ? [
+        props.inputRules.required,
+        props.inputRules.samePassword(form.value.password),
+      ]
     : [],
 );
 
-const syncLocalForm = () => {
-  Object.assign(localForm, {
-    name: form.name ?? "",
-    document: form.document ?? "",
-    email: form.email ?? "",
-    password: form.password ?? "",
-    confirmPassword: form.confirmPassword ?? "",
-  });
-};
-
-watch(
-  () => [dialogModel.value, form],
-  () => {
-    if (dialogModel.value) syncLocalForm();
-  },
-  { deep: true, immediate: true },
-);
-
 const closeDialog = () => {
   dialogModel.value = false;
   emit("close");
@@ -172,7 +160,7 @@ const submitForm = async () => {
   const isValid = await editFormRef.value?.validate();
   if (!isValid) return;
 
-  emit("submit", { ...localForm });
+  emit("submit", form.value);
 };
 </script>