Jelajahi Sumber

feat(users): adiciona usuarios e get

ebagabee 2 minggu lalu
induk
melakukan
df0199e175

+ 5 - 0
src/api/unit.js

@@ -30,3 +30,8 @@ export const deleteUnit = async (id) => {
   const { data } = await api.delete(`/unit/${id}`);
   return data;
 };
+
+export const getUnitsForSelect = async () => {
+  const { data } = await api.get("/unit/all/select");
+  return data.payload;
+};

+ 4 - 2
src/api/user.js

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

+ 75 - 0
src/components/selects/UnitSelect.vue

@@ -0,0 +1,75 @@
+<template>
+  <DefaultSelect
+    v-model="selectedUnit"
+    v-bind="$attrs"
+    use-input
+    hide-selected
+    fill-input
+    clearable
+    :options="filteredOptions"
+    :loading="isLoading"
+    :placeholder
+    :label
+    @filter="filterFn"
+  >
+    <template #no-option>
+      <q-item>
+        <q-item-section class="text-grey">
+          {{ $t("http.errors.no_records_found") }}
+        </q-item-section>
+      </q-item>
+    </template>
+  </DefaultSelect>
+</template>
+
+<script setup>
+import { onMounted, ref } from "vue";
+import { getUnitsForSelect } from "src/api/unit";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+const { placeholder, label, initialId } = defineProps({
+  placeholder: { type: String, default: "Buscar unidade" },
+  label: { type: String, default: "Unidade" },
+  initialId: { type: Number, default: null },
+});
+
+const selectedUnit = defineModel({ type: Object });
+const unitOptions = ref([]);
+const filteredOptions = ref([]);
+const isLoading = ref(true);
+
+const selectUnitById = (id) => {
+  selectedUnit.value = unitOptions.value.find((o) => o.value === id) ?? null;
+};
+
+const filterFn = (val, update) => {
+  update(() => {
+    if (val === "") {
+      filteredOptions.value = unitOptions.value;
+    } else {
+      const needle = val.toLowerCase();
+      filteredOptions.value = unitOptions.value.filter((v) =>
+        v.label.toLowerCase().includes(needle),
+      );
+    }
+  });
+};
+
+onMounted(async () => {
+  try {
+    const response = await getUnitsForSelect();
+    unitOptions.value = response.map((unit) => ({
+      label: unit.fantasy_name,
+      value: unit.id,
+    }));
+    filteredOptions.value = unitOptions.value;
+    if (initialId) selectUnitById(initialId);
+  } catch (error) {
+    console.error("Failed to load units:", error);
+  } finally {
+    isLoading.value = false;
+  }
+});
+
+defineExpose({ selectUnitById });
+</script>

+ 131 - 129
src/pages/users/UserActionPage.vue

@@ -3,155 +3,161 @@
     <DefaultHeaderPage title="Cadastro de Usuário" />
 
     <div class="q-pa-md">
-      <div class="column justify-center items-center q-mb-lg">
-        <AvatarImageComponent @update:file="onAvatarChange" />
-
-        <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"
-            class="col-6"
-            outlined
-            emit-value
-            map-options
-            :options="roleOptions"
-          />
-
-          <DefaultInput
-            v-model="form.cpf"
-            label="CPF"
-            class="col-6"
-            outlined
-          />
-
-          <DefaultInput
-            v-model="form.name"
-            label="Nome"
-            class="col-6"
-            outlined
-          />
-
-          <DefaultInput
-            v-model="form.password"
-            label="Senha"
-            class="col-6"
-            outlined
-            type="password"
-          />
-
-          <DefaultInput
-            v-model="form.surname"
-            label="Sobrenome"
-            class="col-6"
-            outlined
-          />
-
-          <DefaultInput
-            v-model="form.password_confirmation"
-            label="Repetir Senha"
-            class="col-6"
-            outlined
-            type="password"
-          />
-
-          <DefaultInput
-            v-model="form.email"
-            label="E-mail"
-            class="col-6"
-            outlined
-            type="email"
-          />
-
-          <div class="col-6 flex items-center">
+      <q-form ref="formRef">
+        <div class="column justify-center items-center q-mb-lg">
+          <AvatarImageComponent @update:file="(f) => (avatarFile = f)" />
+
+          <div class="row full-width q-mt-md q-col-gutter-sm">
+            <StateSelect
+              v-model="form.state"
+              class="col-6"
+              outlined
+              label="Estado / UF"
+            />
+
+            <UnitSelect
+              v-model="form.unit"
+              class="col-6"
+              outlined
+              label="Unidade"
+            />
+
+            <UserTypeSelect
+              v-model="form.user_type"
+              class="col-6"
+              outlined
+              label="Tipo de Usuário"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.cpf"
+              label="CPF"
+              class="col-6"
+              outlined
+              mask="###.###.###-##"
+              :rules="[inputRules.cpf]"
+            />
+
+            <DefaultInput
+              v-model="form.name"
+              label="Nome"
+              class="col-6"
+              outlined
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.email"
+              label="E-mail"
+              class="col-6"
+              outlined
+              type="email"
+              :rules="[inputRules.required, inputRules.email]"
+            />
+
+            <DefaultInput
+              v-model="form.password"
+              label="Senha"
+              class="col-6"
+              outlined
+              type="password"
+              :rules="[inputRules.required, inputRules.min(8)]"
+            />
+
+            <DefaultInput
+              v-model="form.password_confirmation"
+              label="Repetir Senha"
+              class="col-6"
+              outlined
+              type="password"
+              :rules="[
+                inputRules.required,
+                inputRules.samePassword(form.password),
+              ]"
+            />
+
+            <div class="col-6 flex items-center">
+              <q-btn
+                label="Gerar senha segura"
+                icon="mdi-thumb-up-outline"
+                color="primary"
+                @click="generatePassword"
+              />
+            </div>
+          </div>
+
+          <div class="row justify-end q-mt-md full-width">
             <q-btn
-              label="Gerar senha segura"
-              icon="mdi-thumb-up-outline"
+              label="SALVAR"
+              icon-right="mdi-check"
               color="primary"
-              @click="generatePassword"
+              :loading="loading"
+              @click="onSave"
             />
           </div>
         </div>
-
-        <div class="row justify-end q-mt-md full-width">
-          <q-btn
-            label="SALVAR"
-            icon-right="mdi-check"
-            color="primary"
-            @click="onSave"
-          />
-        </div>
-      </div>
+      </q-form>
     </div>
   </div>
 </template>
 
 <script setup>
+import { ref } from "vue";
+import { useRouter } from "vue-router";
 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 AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
-import { ref } from "vue";
+import StateSelect from "src/components/selects/StateSelect.vue";
+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";
+
+const router = useRouter();
+const { inputRules } = useInputRules();
+
+const formRef = ref(null);
+const avatarFile = ref(null);
 
 const form = ref({
   state: null,
   unit: null,
-  role: null,
+  user_type: null,
   cpf: null,
   name: null,
-  surname: 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 onAvatarChange(file) {
-  console.log("Avatar file selected:", file);
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: () => {
+    router.push({ name: "UserPage" });
+  },
+});
+
+function buildFormData() {
+  const fd = new FormData();
+
+  if (avatarFile.value) fd.append("avatar", avatarFile.value);
+  if (form.value.state?.value) fd.append("state_id", form.value.state.value);
+  if (form.value.unit?.value) fd.append("unit_id", form.value.unit.value);
+  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);
+
+  fd.append("name", form.value.name ?? "");
+  fd.append("email", form.value.email ?? "");
+  fd.append("password", form.value.password ?? "");
+
+  return fd;
+}
+
+async function onSave() {
+  await execute(() => createUser(buildFormData()));
 }
 
 function generatePassword() {
@@ -163,8 +169,4 @@ function generatePassword() {
   form.value.password = password;
   form.value.password_confirmation = password;
 }
-
-function onSave() {
-  console.log("Saving user:", form.value);
-}
 </script>

+ 37 - 116
src/pages/users/UserPage.vue

@@ -3,30 +3,26 @@
     <DefaultHeaderPage title="Usuários" show-filter-icon />
 
     <div class="row q-col-gutter-x-md q-pa-sm">
-      <q-select
+      <UserTypeSelect
         v-model="funcaoSelected"
         label="Selecione a Função"
         class="col-3"
-        color="secondary"
-        emit-value
-        map-options
-        :options="funcaoOptions"
+        outlined
+        clearable
       />
 
-      <q-select
+      <UnitSelect
         v-model="unitSelected"
         label="Selecione a Unidade"
         class="col-3"
-        color="secondary"
-        emit-value
-        map-options
-        :options="unitOptions"
+        outlined
+        clearable
       />
     </div>
 
     <div class="q-px-sm">
       <DefaultTable
-        v-model:rows="rows"
+        v-model:rows="filteredRows"
         title="Lista de Usuários"
         :columns
         descricao="Usuários"
@@ -34,11 +30,15 @@
         add-item
         add-item-route="UserAddPage"
       >
+        <template #body-cell-user_type="{ row }">
+          <q-td align="left">{{ row.user_type }}</q-td>
+        </template>
+
         <template #body-cell-status="{ row }">
           <q-td align="center">
             <q-badge
-              :color="row.status === 'active' ? 'positive' : 'warning'"
-              :label="row.status === 'active' ? 'Ativo' : 'Inativo'"
+              :color="row.status === 'ACTIVE' ? 'positive' : 'warning'"
+              :label="row.status === 'ACTIVE' ? 'Ativo' : 'Inativo'"
             />
           </q-td>
         </template>
@@ -69,117 +69,38 @@
 </template>
 
 <script setup>
+import { ref, computed, onMounted } from "vue";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
-import { ref } from "vue";
+import UserTypeSelect from "src/components/selects/UserTypeSelect.vue";
+import UnitSelect from "src/components/selects/UnitSelect.vue";
+import { getUsers } from "src/api/user";
 
 const funcaoSelected = ref(null);
 const unitSelected = ref(null);
+const allUsers = ref([]);
 
-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 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 filteredRows = computed(() => {
+  return allUsers.value.filter((row) => {
+    if (funcaoSelected.value && row.user_type !== funcaoSelected.value.value)
+      return false;
+    return true;
+  });
+});
 
 const columns = ref([
-  {
-    name: "name",
-    label: "Nome",
-    field: "name",
-    align: "left",
-  },
-  {
-    name: "unit",
-    label: "Unidade",
-    field: "unit",
-    align: "left",
-  },
-  {
-    name: "role",
-    label: "Função",
-    field: "role",
-    align: "left",
-  },
-  {
-    name: "status",
-    label: "Status",
-    field: "status",
-    align: "center",
-  },
-  {
-    name: "actions",
-    label: "Ações",
-    field: null,
-    align: "center",
-  },
+  { name: "name", label: "Nome", field: "name", align: "left" },
+  { name: "email", label: "E-mail", field: "email", align: "left" },
+  { name: "user_type", label: "Função", field: "user_type", align: "left" },
+  { name: "status", label: "Status", field: "status", align: "center" },
+  { name: "actions", label: "Ações", field: null, align: "center" },
 ]);
 
-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",
-  },
-]);
+onMounted(async () => {
+  try {
+    allUsers.value = await getUsers();
+  } catch (error) {
+    console.error("Failed to load users:", error);
+  }
+});
 </script>