Преглед изворни кода

feat: adiciona user action page

ebagabee пре 2 недеља
родитељ
комит
ce37a9da13

+ 5 - 0
src/api/user.js

@@ -10,6 +10,11 @@ export const getUsers = async () => {
   return data.payload;
 };
 
+export const getUserById = async (id) => {
+  const { data } = await api.get(`/user/${id}`);
+  return data.payload;
+};
+
 export const createUser = async (formData) => {
   const { data } = await api.post("/user", formData, {
     headers: { "Content-Type": "multipart/form-data" },

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

@@ -23,7 +23,9 @@
         </template>
 
         <template #append>
-          <q-icon v-if="icon" :name="icon" size="sm" color="secondary" />
+          <slot name="append">
+            <q-icon v-if="icon" :name="icon" size="sm" color="secondary" />
+          </slot>
         </template>
       </q-input>
     </div>

+ 102 - 15
src/pages/users/UserActionPage.vue

@@ -1,11 +1,16 @@
 <template>
   <div>
-    <DefaultHeaderPage title="Cadastro de Usuário" />
+    <DefaultHeaderPage
+      :title="isEdit ? 'Editar Usuário' : 'Cadastro de Usuário'"
+    />
 
     <div class="q-pa-md">
       <q-form ref="formRef">
         <div class="column justify-center items-center q-mb-lg">
-          <AvatarImageComponent @update:file="(f) => (avatarFile = f)" />
+          <AvatarImageComponent
+            ref="avatarRef"
+            @update:file="(f) => (avatarFile = f)"
+          />
 
           <div class="row full-width q-mt-md q-col-gutter-sm">
             <StateSelect
@@ -13,6 +18,7 @@
               class="col-6"
               outlined
               label="Estado / UF"
+              :initial-id="editStateId"
             />
 
             <UnitSelect
@@ -28,6 +34,7 @@
               outlined
               label="Tipo de Usuário"
               :rules="[inputRules.required]"
+              :type="editUserTypeValue"
             />
 
             <DefaultInput
@@ -61,21 +68,56 @@
               label="Senha"
               class="col-6"
               outlined
-              type="password"
-              :rules="[inputRules.required, inputRules.min(8)]"
-            />
+              :type="showPassword ? 'text' : 'password'"
+              :rules="
+                isEdit
+                  ? [(v) => !v || v.length >= 8 || 'Mínimo 8 caracteres']
+                  : [inputRules.required, inputRules.min(8)]
+              "
+            >
+              <template #append>
+                <q-icon
+                  :name="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
+                  class="cursor-pointer"
+                  color="secondary"
+                  @click="showPassword = !showPassword"
+                />
+              </template>
+            </DefaultInput>
 
             <DefaultInput
               v-model="form.password_confirmation"
               label="Repetir Senha"
               class="col-6"
               outlined
-              type="password"
-              :rules="[
-                inputRules.required,
-                inputRules.samePassword(form.password),
-              ]"
-            />
+              :type="showPasswordConfirm ? 'text' : 'password'"
+              :rules="
+                isEdit
+                  ? [
+                      (v) =>
+                        !form.password ||
+                        v === form.password ||
+                        'Senhas não conferem',
+                    ]
+                  : [
+                      inputRules.required,
+                      inputRules.samePassword(form.password),
+                    ]
+              "
+            >
+              <template #append>
+                <q-icon
+                  :name="showPasswordConfirm ? 'mdi-eye-off' : 'mdi-eye'"
+                  class="cursor-pointer"
+                  color="secondary"
+                  @click="showPasswordConfirm = !showPasswordConfirm"
+                />
+              </template>
+            </DefaultInput>
+
+            <div v-if="isEdit" class="col-12 text-caption text-grey-6">
+              Deixe os campos de senha em branco para manter a senha atual.
+            </div>
 
             <div class="col-6 flex items-center">
               <q-btn
@@ -103,8 +145,8 @@
 </template>
 
 <script setup>
-import { ref } from "vue";
-import { useRouter } from "vue-router";
+import { ref, computed, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
@@ -113,14 +155,24 @@ import UnitSelect from "src/components/selects/UnitSelect.vue";
 import UserTypeSelect from "src/components/selects/UserTypeSelect.vue";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { createUser } from "src/api/user";
+import { createUser, updateUser, getUserById } from "src/api/user";
 
 const router = useRouter();
+const route = useRoute();
 const { inputRules } = useInputRules();
 
 const formRef = ref(null);
+const avatarRef = ref(null);
 const avatarFile = ref(null);
 
+const showPassword = ref(false);
+const showPasswordConfirm = ref(false);
+
+const editUserTypeValue = ref(null);
+const editStateId = ref(null);
+
+const isEdit = computed(() => !!route.params.id);
+
 const form = ref({
   state: null,
   unit: null,
@@ -139,6 +191,23 @@ const { loading, execute } = useSubmitHandler({
   },
 });
 
+onMounted(async () => {
+  if (!isEdit.value) return;
+  try {
+    const user = await getUserById(route.params.id);
+    form.value.name = user.name;
+    form.value.email = user.email;
+    form.value.cpf = user.cpf;
+    editUserTypeValue.value = user.user_type;
+    editStateId.value = user.state_id ?? null;
+    if (user.avatar_url) {
+      avatarRef.value?.setImageUrl(user.avatar_url);
+    }
+  } catch (error) {
+    console.error("Failed to load user:", error);
+  }
+});
+
 function buildFormData() {
   const fd = new FormData();
 
@@ -156,8 +225,26 @@ function buildFormData() {
   return fd;
 }
 
+function buildUpdateData() {
+  const data = {};
+
+  if (form.value.name) data.name = form.value.name;
+  if (form.value.email) data.email = form.value.email;
+  if (form.value.cpf) data.cpf = form.value.cpf;
+  if (form.value.user_type?.value) data.user_type = form.value.user_type.value;
+  if (form.value.state?.value) data.state_id = form.value.state.value;
+  if (form.value.unit?.value) data.unit_id = form.value.unit.value;
+  if (form.value.password) data.password = form.value.password;
+
+  return data;
+}
+
 async function onSave() {
-  await execute(() => createUser(buildFormData()));
+  if (isEdit.value) {
+    await execute(() => updateUser(buildUpdateData(), route.params.id));
+  } else {
+    await execute(() => createUser(buildFormData()));
+  }
 }
 
 function generatePassword() {

+ 17 - 3
src/pages/users/UserPage.vue

@@ -31,7 +31,7 @@
         add-item-route="UserAddPage"
       >
         <template #body-cell-user_type="{ row }">
-          <q-td align="left">{{ row.user_type }}</q-td>
+          <q-td align="left">{{ userTypeLabel(row.user_type) }}</q-td>
         </template>
 
         <template #body-cell-status="{ row }">
@@ -43,7 +43,7 @@
           </q-td>
         </template>
 
-        <template #body-cell-actions>
+        <template #body-cell-actions="{ row }">
           <q-td auto-width>
             <q-item-section class="no-wrap" style="flex-direction: row">
               <q-btn
@@ -51,7 +51,7 @@
                 icon="mdi-account-edit-outline"
                 style="width: 36px"
                 class="q-mr-sm"
-                @click.prevent.stop="() => {}"
+                @click.prevent.stop="onEdit(row)"
               />
               <q-btn
                 outline
@@ -70,16 +70,26 @@
 
 <script setup>
 import { ref, computed, onMounted } from "vue";
+import { useRouter } from "vue-router";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import UserTypeSelect from "src/components/selects/UserTypeSelect.vue";
 import UnitSelect from "src/components/selects/UnitSelect.vue";
 import { getUsers } from "src/api/user";
 
+const router = useRouter();
+
 const funcaoSelected = ref(null);
 const unitSelected = ref(null);
 const allUsers = ref([]);
 
+const USER_TYPE_LABELS = {
+  ADMIN: "Administrador (Franqueadora)",
+  ADMIN_FRANCHISEE: "Administrador (Franqueada)",
+};
+
+const userTypeLabel = (type) => USER_TYPE_LABELS[type] ?? type;
+
 const filteredRows = computed(() => {
   return allUsers.value.filter((row) => {
     if (funcaoSelected.value && row.user_type !== funcaoSelected.value.value)
@@ -96,6 +106,10 @@ const columns = ref([
   { name: "actions", label: "Ações", field: null, align: "center" },
 ]);
 
+function onEdit(row) {
+  router.push({ name: "UserEditPage", params: { id: row.id } });
+}
+
 onMounted(async () => {
   try {
     allUsers.value = await getUsers();

+ 23 - 0
src/router/routes/user.route.js

@@ -22,6 +22,29 @@ export default [
       ],
     },
   },
+  {
+    path: "/users/:id",
+    name: "UserEditPage",
+    component: () => import("pages/users/UserActionPage.vue"),
+    meta: {
+      title: {
+        value: "Editar Usuário",
+        translate: false,
+      },
+      requireAuth: true,
+      requiredPermission: "config.city",
+      breadcrumbs: [
+        {
+          name: "UserPage",
+          title: "Usuários",
+        },
+        {
+          name: "UserEditPage",
+          title: "Editar Usuário",
+        },
+      ],
+    },
+  },
   {
     path: "/users",
     name: "UserPage",