Ver Fonte

feat: :sparkles: crud base de clientes

Heron Slovinski há 1 ano atrás
pai
commit
7b30849859

+ 26 - 0
src/api/clientes.js

@@ -0,0 +1,26 @@
+import { api } from "src/boot/axios";
+
+export const getCliente = async (id) => {
+  const { data } = await api.get(`/clientes/${id}`);
+  return data.payload;
+};
+
+export const getClientes = async () => {
+  const { data } = await api.get("/clientes");
+  return data.payload;
+};
+
+export const createCliente = async (cliente) => {
+  const { data } = await api.post("/clientes", cliente);
+  return data.payload;
+};
+
+export const updateCliente = async (cliente, id) => {
+  const { data } = await api.put(`/clientes/${id}`, cliente);
+  return data.payload;
+};
+
+export const getClientesInfos = async (id) => {
+  const { data } = await api.get(`/clientes-infos/${id}`);
+  return data.payload;
+}

+ 23 - 12
src/components/geral/DefaultTable.vue

@@ -93,21 +93,17 @@
         @click="onAddItem"
       >
       </q-btn>
-
-
     </template>
 
     <template #body-cell-status="{ value, row }">
       <q-td style="width: 8%">
-        <q-item-section>
-          <span class="text-center">
-            <div v-if="row.status && value" class="ativo body2 text-positive">
-              Ativo
-            </div>
-            <div v-if="!row.status" class="inativo body2 text-accent">
-              Inativo
-            </div>
-          </span>
+        <q-item-section class="flex items-center justify-center">
+          <span
+            :class="[
+              'circulo-status',
+              row.status == 'ativo' && value ? 'circulo-ativo' : 'circulo-inativo',
+            ]"
+          ></span>
         </q-item-section>
       </q-td>
     </template>
@@ -122,7 +118,9 @@
             <div v-if="row.ativo && !value" class="ativo body2 text-positive">
               Ativo
             </div>
-            <div v-if="!row.ativo" class="inativo body2 text-accent">Ativo</div>
+            <div v-if="!row.ativo" class="inativo body2 text-accent">
+              Inativo
+            </div>
           </span>
         </q-item-section>
       </q-td>
@@ -374,4 +372,17 @@ onMounted(async () => {
   background: #f7cfbb;
   border-radius: 24px;
 }
+
+.circulo-status {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  display: inline-block;
+}
+.circulo-ativo {
+  background-color: #80F680; /* Verde */
+}
+.circulo-inativo {
+  background-color: #919191; /* Cinza */
+}
 </style>

+ 9 - 0
src/components/geral/LeftMenuLayout.vue

@@ -223,6 +223,15 @@ const menus = ref([
     permission: false,
     permissionScope: "dashboard",
   },
+  {
+    type: "single",
+    title: "Clientes",
+    name: "ClientesPage",
+    icon: "mdi-account",
+    disable: false,
+    permission: false,
+    permissionScope: "clientes",
+  },
   {
     type: "expansive",
     title: "Configurações",

+ 21 - 14
src/css/app.scss

@@ -6,42 +6,42 @@ body,
   font-size: 14px;
 }
 
-.font-h1 {
+.h1 {
   font-family: "Montserrat";
   font-size: 96px;
   font-weight: 300;
   line-height: 144px;
 }
 
-.font-h2 {
+.h2 {
   font-family: "Montserrat";
   font-size: 60px;
   font-weight: 300;
   line-height: 90px;
 }
 
-.font-h3 {
+.h3 {
   font-family: "Montserrat";
   font-size: 48px;
   font-weight: 400;
   line-height: 72px;
 }
 
-.font-h4 {
+.h4 {
   font-family: "Montserrat";
   font-size: 34px;
   font-weight: 400;
   line-height: 51px;
 }
 
-.font-h5 {
+.h5 {
   font-family: "Montserrat";
   font-size: 24px;
   font-weight: 400;
   line-height: 36px;
 }
 
-.font-h6 {
+.h6 {
   font-family: "Montserrat";
   font-size: 20px;
   font-weight: 500;
@@ -49,7 +49,7 @@ body,
   letter-spacing: 0.7px;
 }
 
-.font-subtitle-1 {
+.subtitle-1 {
   font-family: "Montserrat";
   font-size: 16px;
   font-weight: 400;
@@ -57,7 +57,7 @@ body,
   letter-spacing: 0.15px;
 }
 
-.font-subtitle-2 {
+.subtitle-2 {
   font-family: "Montserrat";
   font-size: 14px;
   font-weight: 500;
@@ -66,7 +66,7 @@ body,
   text-align: left;
 }
 
-.font-body-1 {
+.body-1 {
   font-family: "Montserrat";
   font-size: 16px;
   font-weight: 400;
@@ -74,7 +74,7 @@ body,
   letter-spacing: 0.5px;
 }
 
-.font-body-2 {
+.body-2 {
   font-family: "Montserrat";
   font-size: 14px;
   font-weight: 400;
@@ -82,7 +82,7 @@ body,
   letter-spacing: 0.25px;
 }
 
-.font-overline {
+.overline {
   font-family: "Montserrat";
   font-size: 10px;
   font-weight: 400;
@@ -90,7 +90,7 @@ body,
   letter-spacing: 1.5px;
 }
 
-.font-caption {
+.caption {
   font-family: "Montserrat";
   font-size: 12px;
   font-weight: 400;
@@ -98,7 +98,7 @@ body,
   letter-spacing: 0.4px;
 }
 
-.font-button {
+.button {
   font-family: "Montserrat";
   font-size: 14px;
   font-weight: 500;
@@ -149,11 +149,18 @@ body,
   }
 }
 
+.q-input-remove-all-border {
+  .q-field__control::before {
+    border: none !important;
+  }
+}
+
+
 .select-rounded {
   border-radius: 4px;
 }
 
-.default-button-padding {
+.q-btn {
   border-radius: 4px;
 }
 

+ 124 - 0
src/pages/clientes/ClientesPage.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="q-pa-md">
+    <DefaultHeaderPage />
+    <DefaultTable
+      :key="tableKey"
+      :columns="columns"
+      :mostrar-selecao-de-colunas="false"
+      :mostrar-botao-fullscreen="false"
+      :mostrar-toggle-inativos="false"
+      style="padding-right: 16px"
+      :api-route="getClientes"
+      open-item
+      add-item
+      @on-row-click="onRowClick"
+      @on-add-item="onAddItem"
+    />
+  </div>
+</template>
+
+<script setup>
+import DefaultTable from "src/components/geral/DefaultTable.vue";
+import { ref, defineAsyncComponent } from "vue";
+import { useQuasar } from "quasar";
+import { permissionStore } from "src/stores/permission";
+import { getClientes, createCliente, updateCliente } from "src/api/clientes";
+import { format, parseISO } from "date-fns";
+
+import DefaultHeaderPage from "src/components/geral/DefaultHeaderPage.vue";
+
+const AddEditClientesDialog = defineAsyncComponent(
+  () => import("src/pages/clientes/components/AddEditClientesDialog.vue"),
+);
+
+const permission_store = permissionStore();
+const $q = useQuasar();
+const tableKey = ref(0);
+
+const columns = [
+  {
+    name: "id",
+    label: "Código",
+    field: "id",
+    align: "left",
+    style: "width: 1%",
+    required: true,
+  },
+  {
+    name: "nome_fantasia",
+    label: "Nome Fantasia",
+    field: "nome_fantasia",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "cnpj",
+    label: "CNPJ",
+    field: "cnpj",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "created_at",
+    label: "Cadastro",
+    field: "created_at",
+    format: (val) => format(parseISO(val), "dd/MM/yyyy"),
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "status",
+    label: "Status",
+    field: "status",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+];
+
+const onRowClick = ({ row }) => {
+  if (permission_store.getAccess("clientes", "view") === false) {
+    $q.loading.hide();
+    $q.notify({
+      type: "negative",
+      message: "Você não tem permissão para visualizar clientes",
+    });
+    return;
+  }
+  $q.dialog({
+    component: AddEditClientesDialog,
+    componentProps: {
+      cliente: row,
+      clienteId: row.id,
+      title: 'Editar Cliente',
+    },
+  }).onOk(async (payload) => {
+    await updateCliente(payload, row.id);
+    tableKey.value = tableKey.value + 1;
+  });
+};
+
+const onAddItem = () => {
+  if (permission_store.getAccess("clientes", "add") === false) {
+    $q.loading.hide();
+    $q.notify({
+      type: "negative",
+      message: "Você não tem permissão para adicionar clientes",
+    });
+    return;
+  }
+  $q.dialog({
+    component: AddEditClientesDialog,
+
+    componentProps: {
+      title: 'Novo Cliente',
+    },
+  }).onOk(async (payload) => {
+    await createCliente(payload);
+    tableKey.value = tableKey.value + 1;
+  });
+};
+</script>

+ 187 - 0
src/pages/clientes/components/AddEditClientesDialog.vue

@@ -0,0 +1,187 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="width: 1000px">
+      <DefaultDialogHeader :title="props.title" @close="onDialogCancel" />
+      <q-card-section>
+        <q-form ref="formRef" class="row q-col-gutter-sm">
+          <q-input
+            v-model="form.nome_fantasia"
+            label="Nome Fantasia"
+            hint="Obrigatório"
+            :rules="[inputRules.required]"
+            outlined
+            class="col-12 q-input-border"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+          <q-input
+            v-model="form.razao_social"
+            label="Razão Social"
+            outlined
+            class="col-12 q-input-border q-mb-sm"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+          <q-input
+            v-model="form.cnpj"
+            label="CNPJ"
+            hint="Obrigatório"
+            outlined
+            class="col-4 q-input-border q"
+            :mask="masks.Brasil.docEmpresa"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+          <q-input
+            v-model="form.inscricao_estadual"
+            label="Inscrição Estadual"
+            mask="########################"
+            outlined
+            class="col-4 q-input-border q-mb-sm"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+          <q-input
+            v-model="form.inscricao_municipal"
+            label="Inscrição Municipal"
+            mask="########################"
+            outlined
+            class="col-4 q-input-border q-mb-sm"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+          <q-input
+            v-model="form.email"
+            label="Email"
+            :rules="[inputRules.email]"
+            outlined
+            class="col-12 q-input-border q-mb-sm"
+            dense
+            bg-color="white"
+            hide-bottom-space
+          />
+
+          <q-input
+            v-model="form.observacoes"
+            dense
+            outlined
+            label="Observações"
+            class="col-12 q-input-border q-mb-sm"
+            bg-color="white"
+            hide-bottom-space
+            autogrow
+            type="textarea"
+          />
+        </q-form>
+      </q-card-section>
+
+      <q-card-section>
+        <div class="row items-center">
+          <!-- Input com Toggle e textos Ativo/Inativo -->
+          <div v-if="props.clienteId" class="col-auto row items-center">
+            <span class="q-mr-sm text-body-2">Inativo</span>
+            <q-toggle
+              v-model="form.status"
+              color="primary"
+              size="sm"
+              true-value="ativo"
+              false-value="inativo"
+              dense
+              outlined
+            />
+            <span class="q-ml-sm text-body-2">Ativo</span>
+          </div>
+
+          <div class="col">
+            <q-card-actions align="right" class="flex justify-end">
+              <q-btn
+                outline
+                padding="10px 20px"
+                label="Cancelar"
+                color="dark"
+                @click="onDialogCancel"
+              />
+              <q-btn
+                padding="10px 20px"
+                color="primary"
+                label="Salvar"
+                @click="onOKClick"
+              />
+            </q-card-actions>
+          </div>
+        </div>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+<script setup>
+import { onMounted, ref } from "vue";
+import { inputRules } from "src/helpers/utils";
+import { useDialogPluginComponent } from "quasar";
+
+import masks from "src/helpers/masks.js";
+import DefaultDialogHeader from "src/components/geral/DefaultDialogHeader.vue";
+import { getCliente } from "src/api/clientes";
+// import { getClientesInfos } from "src/api/clientes";
+
+defineEmits([
+  // REQUIRED; need to specify some events that your
+  // component will emit through useDialogPluginComponent()
+  ...useDialogPluginComponent.emits,
+]);
+
+const props = defineProps({
+  cliente: {
+    type: Object,
+    default: null,
+  },
+  clienteId: {
+    type: Number,
+    default: null,
+  },
+  title: {
+    type: String,
+    default: "Novo Usuário",
+  },
+});
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const formRef = ref(null);
+
+const form = ref({
+  nome_fantasia: null,
+  razao_social: null,
+  cnpj: null,
+  inscricao_estadual: null,
+  inscricao_municipal: null,
+  email: null,
+  observacoes: null,
+  status: "ativo",
+});
+
+const onOKClick = () => {
+  if (!formRef.value.validate()) {
+    return;
+  }
+
+  onDialogOK(form.value);
+};
+
+const fetchClientes = async () => {
+  const response = await getCliente(props.clienteId);
+  form.value = response;
+};
+
+onMounted(async () => {
+  if (props.clienteId) {
+    fetchClientes();
+  }
+});
+</script>

+ 24 - 0
src/router/routes/clientes.route.js

@@ -0,0 +1,24 @@
+const routes = [
+  {
+    path: "/clientes",
+    name: "ClientesPage",
+    component: () => import("pages/clientes/ClientesPage.vue"),
+    meta: {
+      title: "Clientes",
+      requireAuth: true,
+      requiredPermission: "clientes",
+      breadcrumbs: [
+        {
+          name: "HomePage",
+          title: "Início",
+        },
+        {
+          name: "ClientesPage",
+          title: "Clientes",
+        },
+      ],
+    },
+  },
+];
+
+export default routes;