Explorar o código

feat: adiciona criacao e edicao de usuario

ebagabee hai 1 mes
pai
achega
8fd51128cd

+ 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 getUsersByUnit = async () => {
   const { data } = await api.get("/user/unit");
   return data.payload;

+ 24 - 8
src/components/selects/UserTypeSelect.vue

@@ -15,12 +15,12 @@
 </template>
 
 <script setup>
-import { onMounted, ref } from "vue";
+import { onMounted, ref, watch } from "vue";
 import { userTypes } from "src/api/user";
 import { useI18n } from "vue-i18n";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 
-const { placeholder, label, type } = defineProps({
+const props = defineProps({
   placeholder: {
     type: String,
     default: () => useI18n().t("common.ui.misc.type"),
@@ -33,8 +33,14 @@ const { placeholder, label, type } = defineProps({
     type: String,
     default: null,
   },
+  excludeTypes: {
+    type: Array,
+    default: () => [],
+  },
 });
 
+const { placeholder, label } = props;
+
 const selectedUserType = defineModel({
   type: Object,
 });
@@ -64,15 +70,14 @@ const filterFn = (val, update) => {
 onMounted(async () => {
   try {
     const response = await userTypes();
-    userTypeOptions.value = Object.entries(response).map(([key, value]) => ({
-      label: value,
-      value: key,
-    }));
+    userTypeOptions.value = Object.entries(response)
+      .map(([key, value]) => ({ label: value, value: key }))
+      .filter((opt) => !props.excludeTypes.includes(opt.value));
     filteredOptions.value = userTypeOptions.value;
 
-    if (type) {
+    if (props.type) {
       selectedUserType.value = userTypeOptions.value.find(
-        (option) => option.value === type,
+        (option) => option.value === props.type,
       );
     }
   } catch (error) {
@@ -82,6 +87,17 @@ onMounted(async () => {
   }
 });
 
+watch(
+  () => props.type,
+  (newType) => {
+    if (newType && userTypeOptions.value.length > 0) {
+      selectedUserType.value = userTypeOptions.value.find(
+        (option) => option.value === newType,
+      );
+    }
+  },
+);
+
 defineExpose({
   selectUserByValue,
 });

+ 124 - 130
src/pages/users/UserActionPage.vue

@@ -1,60 +1,23 @@
 <template>
   <div>
-    <DefaultHeaderPage title="Cadastro de Usuário" />
+    <DefaultHeaderPage :title="isEdit ? 'Editar Usuário' : 'Cadastro de Usuário'" />
 
     <div class="q-pa-md">
-      <div class="column justify-center items-center q-mb-lg">
-        <div class="q-mb-md">
-          <q-avatar size="80px" color="grey-3">
-            <q-icon name="mdi-account" size="48px" color="grey-6" />
-            <q-btn
-              round
-              dense
-              color="primary"
-              icon="mdi-camera"
-              size="xs"
-              style="position: absolute; bottom: 0; right: 0"
-              @click="triggerFileInput"
-            />
-          </q-avatar>
-          <input
-            ref="fileInputRef"
-            type="file"
-            accept="image/*"
-            style="display: none"
-            @change="onAvatarChange"
+      <q-form ref="formRef">
+        <div class="column items-center q-mb-lg">
+          <AvatarImageComponent
+            ref="avatarRef"
+            @update:file="(f) => (avatarFile = f)"
           />
         </div>
 
-        <div class="row full-width q-mt-md q-col-gutter-sm">
-          <DefaultSelect
-            v-model="form.state"
-            label="Estado / UF"
-            class="col-6"
-            outlined
-            emit-value
-            map-options
-            :options="stateOptions"
-          />
-
-          <DefaultSelect
-            v-model="form.unit"
-            label="Unidade"
-            class="col-6"
-            outlined
-            emit-value
-            map-options
-            :options="unitOptions"
-          />
-
-          <DefaultSelect
-            v-model="form.role"
-            label="Tipo de Usuário"
+        <div class="row q-col-gutter-sm">
+          <DefaultInput
+            v-model="form.name"
+            label="Nome completo"
             class="col-6"
             outlined
-            emit-value
-            map-options
-            :options="roleOptions"
+            :rules="[inputRules.required]"
           />
 
           <DefaultInput
@@ -62,134 +25,165 @@
             label="CPF"
             class="col-6"
             outlined
+            :mask="masks.Brasil.cpf"
+            :rules="[inputRules.cpf]"
           />
 
-          <DefaultInput
-            v-model="form.name"
-            label="Nome"
+          <UserTypeSelect
+            v-model="form.user_type"
             class="col-6"
             outlined
+            label="Função"
+            :rules="[inputRules.required]"
+            :type="editUserTypeValue"
+            :disable="!canChangeUserType"
+            :exclude-types="['ADMIN']"
           />
 
-          <DefaultPasswordInput
-            v-model="form.password"
-            label="Senha"
+          <DefaultInput
+            v-model="form.phone"
+            label="Telefone"
             class="col-6"
             outlined
+            :mask="masks.Brasil.celular"
           />
 
           <DefaultInput
-            v-model="form.surname"
-            label="Sobrenome"
+            v-model="form.email"
+            label="E-mail"
             class="col-6"
             outlined
+            type="email"
+            :rules="[inputRules.required, inputRules.email]"
           />
 
           <DefaultPasswordInput
-            v-model="form.password_confirmation"
-            label="Repetir Senha"
-            class="col-6"
-            outlined
-          />
-
-          <DefaultInput
-            v-model="form.email"
-            label="E-mail"
+            v-model="form.password"
+            label="Senha"
             class="col-6"
             outlined
-            type="email"
+            :rules="
+              isEdit
+                ? [(v) => !v || v.length >= 8 || 'Mínimo 8 caracteres']
+                : [inputRules.required, inputRules.min(8)]
+            "
           />
 
-          <div class="col-6 flex items-center">
-            <q-btn
-              label="Gerar senha segura"
-              icon="mdi-thumb-up-outline"
-              color="primary"
-              @click="generatePassword"
-            />
+          <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>
 
-        <div class="row justify-end q-mt-md full-width">
+        <div class="row justify-end q-mt-lg q-gutter-sm">
+          <q-btn
+            label="Cancelar"
+            color="primary"
+            outline
+            @click="router.push({ name: 'UsersPage' })"
+          />
           <q-btn
-            label="SALVAR"
-            icon-right="mdi-check"
+            label="Salvar"
             color="primary"
+            :loading="loading"
             @click="onSave"
           />
         </div>
-      </div>
+      </q-form>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, useTemplateRef } from "vue";
+import { ref, computed, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { storeToRefs } from "pinia";
+import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
-
-const fileInputRef = useTemplateRef("fileInputRef");
+import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
+import UserTypeSelect from "src/components/selects/UserTypeSelect.vue";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { createUser, updateUser, getUserById } from "src/api/user";
+import { userStore } from "src/stores/user";
+import masks from "src/helpers/masks";
+
+const router = useRouter();
+const route = useRoute();
+const $q = useQuasar();
+const { inputRules } = useInputRules();
+const { user } = storeToRefs(userStore());
+
+const formRef = ref(null);
+const avatarRef = ref(null);
+const avatarFile = ref(null);
+const editUserTypeValue = ref(null);
+const originalUserType = ref(null);
+
+const isEdit = computed(() => !!route.params.id);
+const isEditingSelf = computed(() => isEdit.value && parseInt(route.params.id) === user.value?.id);
+const canChangeUserType = computed(() => user.value?.user_type === "ADMIN_FRANCHISEE");
 
 const form = ref({
-  state: null,
-  unit: null,
-  role: null,
-  cpf: null,
   name: null,
-  surname: null,
+  cpf: null,
+  phone: null,
+  user_type: null,
   email: null,
   password: null,
-  password_confirmation: null,
 });
 
-const stateOptions = ref([
-  { label: "Paraná", value: "PR" },
-  { label: "São Paulo", value: "SP" },
-  { label: "Santa Catarina", value: "SC" },
-]);
-
-const unitOptions = ref([
-  { label: "Toledo-PR", value: "toledo" },
-  { label: "Arapongas-PR", value: "arapongas" },
-  { label: "Curitiba-PR", value: "curitiba" },
-  { label: "Londrina-PR", value: "londrina" },
-  { label: "Ponta Grossa-PR", value: "ponta_grossa" },
-  { label: "Maringá-PR", value: "maringa" },
-  { label: "Cascavel-PR", value: "cascavel" },
-]);
-
-const roleOptions = ref([
-  { label: "Neurotainer", value: "neurotainer" },
-  { label: "Assessor", value: "assessor" },
-  { label: "Marketing", value: "marketing" },
-  { label: "Comercial", value: "comercial" },
-  { label: "Gestor", value: "gestor" },
-  { label: "Administrativo", value: "administrativo" },
-  { label: "Recepção", value: "recepcao" },
-]);
-
-function triggerFileInput() {
-  fileInputRef.value?.click();
-}
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: () => router.push({ name: "UsersPage" }),
+});
 
-function onAvatarChange(event) {
-  const file = event.target.files[0];
-  console.log("Avatar file selected:", file);
-}
+onMounted(async () => {
+  if (!isEdit.value) return;
+  try {
+    const data = await getUserById(route.params.id);
+    form.value.name = data.name;
+    form.value.email = data.email;
+    form.value.cpf = data.cpf;
+    form.value.phone = data.phone;
+    editUserTypeValue.value = data.user_type?.value ?? data.user_type;
+    originalUserType.value = editUserTypeValue.value;
+    if (data.avatar_url) avatarRef.value?.setImageUrl(data.avatar_url);
+  } catch (error) {
+    console.error("Failed to load user:", error);
+  }
+});
+
+function buildPayload() {
+  const fd = new FormData();
+
+  if (avatarFile.value) fd.append("avatar", avatarFile.value);
+
+  if (user.value?.unit_id) fd.append("unit_id", user.value.unit_id);
+  if (form.value.user_type?.value) fd.append("user_type", form.value.user_type.value);
+  if (form.value.cpf) fd.append("cpf", form.value.cpf);
+  if (form.value.phone) fd.append("phone", form.value.phone);
+  if (form.value.name) fd.append("name", form.value.name);
+  if (form.value.email) fd.append("email", form.value.email);
+  if (form.value.password) fd.append("password", form.value.password);
 
-function generatePassword() {
-  const chars =
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
-  const password = Array.from({ length: 12 }, () =>
-    chars.charAt(Math.floor(Math.random() * chars.length)),
-  ).join("");
-  form.value.password = password;
-  form.value.password_confirmation = password;
+  return fd;
 }
 
-function onSave() {
-  console.log("Saving user:", form.value);
+async function onSave() {
+  if (isEditingSelf.value && form.value.user_type?.value !== originalUserType.value) {
+    $q.notify({
+      type: "warning",
+      message: "Não é possível alterar o seu próprio tipo de usuário. Entre em contato com o suporte.",
+    });
+    return;
+  }
+
+  if (isEdit.value) {
+    await execute(() => updateUser(buildPayload(), route.params.id));
+  } else {
+    await execute(() => createUser(buildPayload()));
+  }
 }
 </script>

+ 2 - 0
src/pages/users/UsersPage.vue

@@ -9,6 +9,8 @@
         :api-call="getUsersByUnit"
         descricao="usuários"
         :feminino="false"
+        add-item
+        add-item-route="UserAddPage"
       >
         <template #body-cell-actions="{ row }">
           <q-td align="center">

+ 16 - 12
src/router/routes/config.route.js

@@ -106,20 +106,24 @@ export default [
     name: "UserAddPage",
     component: () => import("pages/users/UserActionPage.vue"),
     meta: {
-      title: {
-        value: "Cadastro de Usuário",
-        translate: false,
-      },
+      title: "Cadastro de Usuário",
       requireAuth: true,
       breadcrumbs: [
-        {
-          name: "UsersPage",
-          title: "Usuários",
-        },
-        {
-          name: "UserAddPage",
-          title: "Cadastro de Usuário",
-        },
+        { name: "UsersPage", title: "Usuários" },
+        { name: "UserAddPage", title: "Cadastro de Usuário" },
+      ],
+    },
+  },
+  {
+    path: "/users/:id/edit",
+    name: "UserEditPage",
+    component: () => import("pages/users/UserActionPage.vue"),
+    meta: {
+      title: "Editar Usuário",
+      requireAuth: true,
+      breadcrumbs: [
+        { name: "UsersPage", title: "Usuários" },
+        { name: "UserEditPage", title: "Editar Usuário" },
       ],
     },
   },