Переглянути джерело

Merge branch 'feature/GC-GAB-cadastro-colaboradores' of Softpar/sfp_vue_franchisee_ginastica_cerebro into development

Gabriel Alves 1 місяць тому
батько
коміт
e8f60301ad

+ 15 - 0
src/api/user.js

@@ -10,12 +10,27 @@ 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;
+};
+
 export const createUser = async (user) => {
   const { data } = await api.post("/user", user);
   return data.payload;
 };
 
 export const updateUser = async (user, id) => {
+  if (user instanceof FormData) {
+    user.append("_method", "PUT");
+    const { data } = await api.post(`/user/${id}`, user);
+    return data.payload;
+  }
   const { data } = await api.put(`/user/${id}`, user);
   return data.payload;
 };

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

@@ -1,6 +1,6 @@
 <template>
   <DefaultSelect
-    v-model="selectedUserType"
+    v-model="selectedOption"
     v-bind="$attrs"
     use-input
     hide-selected
@@ -15,12 +15,12 @@
 </template>
 
 <script setup>
-import { onMounted, ref } from "vue";
+import { onMounted, ref, computed } 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"),
@@ -29,24 +29,31 @@ const { placeholder, label, type } = defineProps({
     type: String,
     default: () => useI18n().t("common.ui.misc.type"),
   },
-  type: {
-    type: String,
-    default: null,
+  excludeTypes: {
+    type: Array,
+    default: () => [],
   },
 });
 
-const selectedUserType = defineModel({
-  type: Object,
-});
+const { placeholder, label } = props;
+
 const userTypeOptions = ref([]);
 const filteredOptions = ref([]);
 const isLoading = ref(true);
 
-const selectUserByValue = (value) => {
-  selectedUserType.value = userTypeOptions.value.find(
-    (option) => option.value === value,
-  );
-};
+const modelValue = defineModel({ type: String, default: null });
+
+const selectedOption = computed({
+  get() {
+    return (
+      userTypeOptions.value.find((opt) => opt.value === modelValue.value) ??
+      null
+    );
+  },
+  set(option) {
+    modelValue.value = option?.value ?? null;
+  },
+});
 
 const filterFn = (val, update) => {
   update(() => {
@@ -64,25 +71,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) {
-      selectedUserType.value = userTypeOptions.value.find(
-        (option) => option.value === type,
-      );
-    }
   } catch (error) {
     console.error("Failed to load user types:", error);
   } finally {
     isLoading.value = false;
   }
 });
-
-defineExpose({
-  selectUserByValue,
-});
 </script>

+ 122 - 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,163 @@
             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]"
+            :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 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;
+    form.value.user_type = data.user_type;
+    originalUserType.value = data.user_type;
+    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) fd.append("user_type", form.value.user_type);
+  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 !== 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>

+ 30 - 141
src/pages/users/UsersPage.vue

@@ -1,68 +1,27 @@
 <template>
   <div>
-    <DefaultHeaderPage title="Usuários" show-filter-icon />
-
-    <div class="row q-col-gutter-x-md q-pa-sm">
-      <q-select
-        v-model="funcaoSelected"
-        label="Selecione a Função"
-        class="col-3"
-        color="secondary"
-        emit-value
-        map-options
-        :options="funcaoOptions"
-      />
-
-      <q-select
-        v-model="unitSelected"
-        label="Selecione a Unidade"
-        class="col-3"
-        color="secondary"
-        emit-value
-        map-options
-        :options="unitOptions"
-      />
-    </div>
+    <DefaultHeaderPage title="Usuários" />
 
     <div class="q-px-sm">
       <DefaultTable
-        v-model:rows="rows"
         title="Lista de Usuários"
-        :columns
-        descricao="Usuários"
-        no-api-call
+        :columns="columns"
+        :api-call="getUsersByUnit"
+        descricao="usuários"
+        :feminino="false"
         add-item
         add-item-route="UserAddPage"
       >
-        <template #body-cell-status="{ row }">
+        <template #body-cell-actions="{ row }">
           <q-td align="center">
-            <q-badge
-              :color="row.status === 'active' ? 'positive' : 'warning'"
-              :label="row.status === 'active' ? 'Ativo' : 'Inativo'"
+            <q-btn
+              outline
+              icon="mdi-account-edit-outline"
+              style="width: 36px"
+              @click.prevent.stop="onEdit(row)"
             />
           </q-td>
         </template>
-
-        <template #body-cell-actions>
-          <q-td auto-width>
-            <q-item-section class="no-wrap" style="flex-direction: row">
-              <q-btn
-                outline
-                icon="mdi-account-edit-outline"
-                style="width: 36px"
-                class="q-mr-sm"
-                @click.prevent.stop="() => {}"
-              />
-              <q-btn
-                outline
-                icon="mdi-trash-can-outline"
-                style="width: 36px"
-                class="q-mr-sm"
-                @click.prevent.stop="() => {}"
-              />
-            </q-item-section>
-          </q-td>
-        </template>
       </DefaultTable>
     </div>
   </div>
@@ -71,115 +30,45 @@
 <script setup>
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
-import { ref } from "vue";
+import { getUsersByUnit } from "src/api/user";
 
-const funcaoSelected = ref(null);
-const unitSelected = ref(null);
+import { useRouter } from "vue-router";
+import { formatDateYMDtoDMY } from "src/helpers/utils";
 
-const funcaoOptions = ref([
-  { label: "Todas", value: null },
-  { 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" },
-]);
+const router = useRouter();
 
-const unitOptions = ref([
-  { label: "Todas", value: null },
-  { 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 columns = ref([
+const columns = [
   {
     name: "name",
     label: "Nome",
     field: "name",
     align: "left",
+    sortable: true,
   },
   {
-    name: "unit",
-    label: "Unidade",
-    field: "unit",
+    name: "type",
+    label: "Tipo de Usuário",
+    field: (row) => row.user_type_label ?? "-",
     align: "left",
+    sortable: true,
   },
   {
-    name: "role",
-    label: "Função",
-    field: "role",
+    name: "created_at",
+    label: "Data de Cadastro",
+    field: (row) => formatDateYMDtoDMY(row.created_at),
     align: "left",
-  },
-  {
-    name: "status",
-    label: "Status",
-    field: "status",
-    align: "center",
+    sortable: true,
   },
   {
     name: "actions",
     label: "Ações",
     field: null,
     align: "center",
+    required: true,
   },
-]);
+];
 
-const rows = ref([
-  {
-    id: 1,
-    name: "Heloisa Faria",
-    unit: "Toledo-PR",
-    role: "Neurotainer",
-    status: "inactive",
-  },
-  {
-    id: 2,
-    name: "Carol",
-    unit: "Arapongas-PR",
-    role: "Assessor",
-    status: "active",
-  },
-  {
-    id: 3,
-    name: "Marcelo Souza",
-    unit: "Curitiba-PR",
-    role: "Marketing",
-    status: "active",
-  },
-  {
-    id: 4,
-    name: "Ana Lúcia",
-    unit: "Londrina-PR",
-    role: "Comercial",
-    status: "active",
-  },
-  {
-    id: 5,
-    name: "Ricardo Silva",
-    unit: "Ponta Grossa-PR",
-    role: "Gestor",
-    status: "active",
-  },
-  {
-    id: 6,
-    name: "Juliana Civita",
-    unit: "Maringá-PR",
-    role: "Administrativo",
-    status: "active",
-  },
-  {
-    id: 7,
-    name: "Fernando Almeida",
-    unit: "Cascavel-PR",
-    role: "Recepção",
-    status: "active",
-  },
-]);
+const onEdit = (row) => {
+  router.push({ name: "UserEditPage", params: { id: row.id } });
+};
 </script>

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

@@ -106,21 +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,
-      requiredPermission: "config.user",
       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" },
       ],
     },
   },
@@ -138,7 +141,6 @@ export default [
         translate: true,
       },
       requireAuth: true,
-      requiredPermission: "config.user",
       breadcrumbs: [
         {
           name: "UsersPage",

+ 8 - 8
src/stores/navigation.js

@@ -36,14 +36,14 @@ export const navigationStore = defineStore("navigation", () => {
       disable: false,
       permission: true,
     },
-    // {
-    //   type: "single",
-    //   title: "Usuários",
-    //   name: "UsersPage",
-    //   icon: "mdi-account-multiple-outline",
-    //   disable: false,
-    //   permission: true,
-    // },
+    {
+      type: "single",
+      title: "Usuários",
+      name: "UsersPage",
+      icon: "mdi-account-multiple-outline",
+      disable: false,
+      permission: true,
+    },
     {
       type: "single",
       title: "Dados da Unidade",