Explorar el Código

feat: :sparkles: criacao do crud Clientes Endereco

Heron Slovinski hace 1 año
padre
commit
07cbf14599

+ 36 - 0
src/api/clientes-enderecos.js

@@ -0,0 +1,36 @@
+import { api } from "src/boot/axios";
+
+export const getEndereco = async (id) => {
+  const { data } = await api.get(`/clientes-enderecos/${id}`);
+  return data.payload;
+};
+
+export const getEnderecos = async () => {
+  const { data } = await api.get("/clientes-enderecos");
+  return data.payload;
+};
+
+export const createEndereco = async (endereco) => {
+  const { data } = await api.post("/clientes-enderecos", endereco);
+  return data.payload;
+};
+
+export const updateEndereco = async (endereco, id) => {
+  const { data } = await api.put(`/clientes-enderecos/${id}`, endereco);
+  return data.payload;
+};
+
+export const getEnderecosInfos = async (id) => {
+  const { data } = await api.get(`/clientes-enderecos-infos/${id}`);
+  return data.payload;
+}
+
+export const updateEnderecoPrincipal = async (id) => {
+  const { data } = await api.put(`/clientes-enderecos-principal/${id}`);
+  return data.payload;
+}
+
+export const getAllEnderecosCliente = async (id) => {
+  const { data } = await api.get(`/clientes-enderecos/cliente/${id}`);
+  return data.payload;
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 57 - 0
src/assets/sem_items.svg


+ 37 - 3
src/components/geral/DefaultTable.vue

@@ -101,7 +101,9 @@
           <span
             :class="[
               'circulo-status',
-              row.status == 'ativo' && value ? 'circulo-ativo' : 'circulo-inativo',
+              row.status == 'ativo' && value
+                ? 'circulo-ativo'
+                : 'circulo-inativo',
             ]"
           ></span>
         </q-item-section>
@@ -126,6 +128,33 @@
       </q-td>
     </template>
 
+    <template #body-cell-principal="{ value, row }">
+      <q-td style="width: 1%">
+        <q-item-section>
+          <span class="text-center">
+            <q-icon
+              v-if="row.principal && value"
+              name="mdi-star"
+              size="1.5rem"
+              style="color: #385873"
+              onmouseover="this.style.color='#688FAF';"
+              onmouseout="this.style.color='#385873';"
+              @click.stop="togglePrincipal(row)"
+            />
+            <q-icon
+              v-if="!row.principal"
+              name="mdi-star-outline"
+              size="1.5rem"
+              style="color: #385873"
+              onmouseover="this.style.color='#688FAF';"
+              onmouseout="this.style.color='#385873';"
+              @click.stop="togglePrincipal(row)"
+            />
+          </span>
+        </q-item-section>
+      </q-td>
+    </template>
+
     <template v-if="!props.hideNoDataLabel" #no-data>
       <div class="q-my-md row justify-center full-width">
         <q-spinner v-if="loading" color="primary" size="30px" />
@@ -143,7 +172,7 @@
 import { ref, onMounted, toRaw, watch } from "vue";
 import { useRouter } from "vue-router";
 
-const emit = defineEmits(["onRowClick", "onAddItem", "noRows"]);
+const emit = defineEmits(["onRowClick", "onAddItem", "noRows", "togglePrincipal"]);
 
 const props = defineProps({
   // colunas de configuração da tabela
@@ -331,6 +360,11 @@ const onRequest = async () => {
   loading.value = false;
 };
 
+// funcao exclusiva para contatos table, para alterar o contato principal
+const togglePrincipal = async (row) => {
+  emit("togglePrincipal", row);
+};
+
 onMounted(async () => {
   // faz a primeira requisição
   await onRequest({
@@ -380,7 +414,7 @@ onMounted(async () => {
   display: inline-block;
 }
 .circulo-ativo {
-  background-color: #80F680; /* Verde */
+  background-color: #80f680; /* Verde */
 }
 .circulo-inativo {
   background-color: #919191; /* Cinza */

+ 39 - 0
src/components/geral/TabsGlobal.vue

@@ -0,0 +1,39 @@
+<template>
+  <q-tabs
+    class="button bg-background-2 text-font"
+    indicator-color="transparent"
+    active-color="primary"
+    :model-value="tab"
+    v-bind="$attrs"
+    align="justify"
+    active-bg-color="white"
+  >
+    <q-tab
+      v-for="(q_tab, i) in props.tabsItems"
+      :key="i"
+      :name="q_tab.name"
+      :label="q_tab.label"
+      :disable="q_tab.disable"
+      :class="{ hidden: q_tab.hide }"
+      @update:model-value="(value) => $emit('update:tab', value)"
+    />
+  </q-tabs>
+</template>
+
+<script setup>
+defineEmits(["update:tab"]);
+
+const props = defineProps({
+  tabsItems: {
+    type: Array,
+    required: false,
+    default: () => [],
+  },
+
+  tab: {
+    type: String,
+    required: false,
+    default: "",
+  },
+});
+</script>

+ 2 - 2
src/pages/clientes/ClientesPage.vue

@@ -93,7 +93,7 @@ const onRowClick = ({ row }) => {
     componentProps: {
       cliente: row,
       clienteId: row.id,
-      title: 'Editar Cliente',
+      title: "Editar Cliente",
     },
   }).onOk(async (payload) => {
     await updateCliente(payload, row.id);
@@ -114,7 +114,7 @@ const onAddItem = () => {
     component: AddEditClientesDialog,
 
     componentProps: {
-      title: 'Novo Cliente',
+      title: "Novo Cliente",
     },
   }).onOk(async (payload) => {
     await createCliente(payload);

+ 122 - 93
src/pages/clientes/components/AddEditClientesDialog.vue

@@ -1,89 +1,110 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin" style="width: 1000px">
+    <q-card
+      class="q-dialog-plugin bg-background-2 column no-wrap"
+      :style="props.clienteId ? 'min-width: 1000px; min-height: 500px' : 'min-width: 800px; min-height: 350px'"
+    >
       <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>
+      <TabsGlobal
+        v-if="props.clienteId"
+        v-model="activeTab"
+        :tabs-items="tabsItems"
+      />
+
+      <q-separator />
 
-      <q-card-section>
+      <q-card-section class="q-py-none q-px-sm" >
+        <q-tab-panels v-model="activeTab" animated class="bg-background-2 full-height">
+          <q-tab-panel name="principal">
+            <q-form ref="formRef" class="row q-col-gutter-sm bg-background-2">
+              <q-input
+                v-model="form.nome_fantasia"
+                label="Nome Fantasia"
+                hint="Obrigatório"
+                dense
+                :rules="[inputRules.required]"
+                outlined
+                class="col-12 q-input-border"
+                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-tab-panel>
+          <q-tab-panel name="enderecos">
+            <EnderecosComponent
+              :enderecos="enderecos"
+              :cliente="props.cliente"
+            />
+          </q-tab-panel>
+        </q-tab-panels>
+      </q-card-section>
+      <q-space />
+      <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">
+          <div v-if="props.clienteId && activeTab == 'principal'" class="col-auto row items-center">
             <span class="q-mr-sm text-body-2">Inativo</span>
             <q-toggle
               v-model="form.status"
@@ -101,13 +122,13 @@
             <q-card-actions align="right" class="flex justify-end">
               <q-btn
                 outline
-                padding="10px 20px"
+                padding="10px 40px"
                 label="Cancelar"
                 color="dark"
                 @click="onDialogCancel"
               />
               <q-btn
-                padding="10px 20px"
+                padding="10px 40px"
                 color="primary"
                 label="Salvar"
                 @click="onOKClick"
@@ -123,17 +144,14 @@
 import { onMounted, ref } from "vue";
 import { inputRules } from "src/helpers/utils";
 import { useDialogPluginComponent } from "quasar";
+import { getCliente } from "src/api/clientes";
 
 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";
+import TabsGlobal from "src/components/geral/TabsGlobal.vue";
+import EnderecosComponent from "src/pages/clientes/components/EnderecosComponent.vue";
 
-defineEmits([
-  // REQUIRED; need to specify some events that your
-  // component will emit through useDialogPluginComponent()
-  ...useDialogPluginComponent.emits,
-]);
+defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
   cliente: {
@@ -154,7 +172,16 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
 const formRef = ref(null);
-
+const enderecos = ref([]);
+const contatos = ref([]);
+const notas = ref([]);
+const activeTab = ref("principal");
+const tabsItems = [
+  { name: "principal", label: "Principal", disable: false, hide: false },
+  { name: "enderecos", label: "Endereços", disable: false, hide: false },
+  { name: "contatos", label: "Contatos", disable: false, hide: false },
+  { name: "notas", label: "Notas", disable: false, hide: false },
+];
 const form = ref({
   nome_fantasia: null,
   razao_social: null,
@@ -166,10 +193,9 @@ const form = ref({
   status: "ativo",
 });
 
-const onOKClick = () => {
-  if (!formRef.value.validate()) {
-    return;
-  }
+const onOKClick = async () => {
+  const validate = await formRef.value.validate();
+  if (!validate) return;
 
   onDialogOK(form.value);
 };
@@ -177,6 +203,9 @@ const onOKClick = () => {
 const fetchClientes = async () => {
   const response = await getCliente(props.clienteId);
   form.value = response;
+  enderecos.value = response.enderecos;
+  contatos.value = response.contatos;
+  notas.value = response.notas;
 };
 
 onMounted(async () => {

+ 213 - 0
src/pages/clientes/components/AddEditEnderecosDialog.vue

@@ -0,0 +1,213 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card
+      class="q-dialog-plugin bg-background-2 column"
+      style="min-width: 600px; min-height: 300px"
+    >
+      <DefaultDialogHeader :title="props.title" @close="onDialogCancel" />
+      <q-card-section class="bg-background-2">
+        <q-form ref="formRef" class="row q-col-gutter-sm bg-background-2">
+          <!-- CEP-->
+          <q-input
+            v-model="form.cep"
+            dense
+            class="col-4 q-input-border"
+            outlined
+            label="CEP"
+            hint="Obrigatório"
+            :mask="masks.Brasil.cep"
+            hide-bottom-space
+            bg-color="white"
+            debounce="500"
+            @update:model-value="procuraCep"
+          />
+
+          <!-- RUA-->
+          <q-input
+            v-model="form.rua"
+            dense
+            class="col-8 q-input-border"
+            outlined
+            label="Rua"
+            hint="Obrigatório"
+            lazy-rules
+            :rules="[inputRules.required]"
+            hide-bottom-space
+            bg-color="white"
+          />
+
+          <!-- BAIRRO-->
+          <q-input
+            v-model="form.bairro"
+            dense
+            class="col-8 q-input-border"
+            outlined
+            label="Bairro"
+            hint="Obrigatório"
+            lazy-rules
+            :rules="[inputRules.required]"
+            hide-bottom-space
+            bg-color="white"
+          />
+
+          <!-- NUMERO-->
+          <q-input
+            v-model="form.numero"
+            dense
+            class="col-4 q-input-border"
+            outlined
+            label="Número"
+            hint="Obrigatório"
+            lazy-rules
+            bg-color="white"
+            mask="#####"
+            :rules="[inputRules.required]"
+            hide-bottom-space
+          />
+
+          <!-- Complemento -->
+          <q-input
+            v-model="form.complemento"
+            dense
+            class="col-12 q-input-border"
+            outlined
+            label="Complemento"
+            hide-bottom-space
+            autogrow
+            type="textarea"
+            bg-color="white"
+          />
+
+          <!-- <div class="col-4">
+              <PaisSelect
+                v-model:pais="selectedPais"
+              />
+            </div>
+            <div class="col-4">
+              <EstadoSelect
+                v-model:estado="selectedEstado"
+                :pais="selectedPais"
+              />
+            </div>
+            <div class="col-4">
+              <CidadeSelect
+                v-model:cidade="selectedCidade"
+                :estado="selectedEstado"
+                :pais="selectedPais"
+              />
+            </div> -->
+          <!-- Principal-->
+          <q-checkbox
+            v-if="!add"
+            v-model="form.principal"
+            dense
+            :true-value="true"
+            :false-value="false"
+            label="Endereço Principal"
+            color="primary"
+            class="q-mt-xs"
+          />
+        </q-form>
+      </q-card-section>
+      <q-card-section >
+        <div class="row items-center">
+          <!-- Input com Toggle e textos Ativo/Inativo -->
+          <div v-if="props.enderecoId" 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 40px"
+                label="Cancelar"
+                color="dark"
+                @click="onDialogCancel"
+              />
+              <q-btn
+                padding="10px 40px"
+                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";
+
+defineEmits([
+  ...useDialogPluginComponent.emits,
+]);
+
+const props = defineProps({
+  enderecoId: {
+    type: String,
+    default: null,
+  },
+  endereco: {
+    type: Object,
+    default: null,
+  },
+  title: {
+    type: String,
+    default: "Novo Endereço",
+  },
+  clienteId: {
+    type: Number,
+    default: null,
+  },
+});
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const formRef = ref(null);
+const form = ref({
+  cliente_id: props.clienteId,
+  cep: null,
+  rua: null,
+  numero: null,
+  bairro: null,
+  complemento: null,
+  principal: false,
+  status: "ativo",
+  pais_id: 1,
+  estado_id: 1,
+  cidade_id: 1,
+});
+
+const onOKClick = () => {
+  if (!formRef.value.validate()) {
+    return;
+  }
+  onDialogOK(form.value);
+};
+
+onMounted(() => {
+  if (props.endereco) {
+    form.value = props.endereco;
+  }
+});
+</script>

+ 203 - 0
src/pages/clientes/components/EnderecosComponent.vue

@@ -0,0 +1,203 @@
+<template>
+  <div>
+    <div
+      v-if="
+        !props.enderecos || (props.enderecos && props.enderecos.length == 0)
+      "
+    >
+      <div class="justify-end full-width flex">
+        <q-btn
+          padding="10px 40px"
+          color="primary"
+          label="Novo"
+          @click="onAddItem"
+        />
+      </div>
+
+      <div class="full-width q-mt-lg flex justify-center">
+        <q-img src="src/assets/sem_items.svg" style="width: 200px" />
+      </div>
+    </div>
+
+    <div v-else class="full-width">
+      <DefaultTable
+        :key="tableKey"
+        :rows="enderecos_cliente"
+        :columns="columns"
+        :mostrar-selecao-de-colunas="false"
+        :mostrar-botao-fullscreen="false"
+        :mostrar-toggle-inativos="false"
+        :label="'Novo'"
+        style="padding-right: 16px"
+        open-item
+        add-item
+        no-api-route
+        @on-row-click="onRowClick"
+        @on-add-item="onAddItem"
+        @toggle-principal="togglePrincipal($event)"
+      >
+      </DefaultTable>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, defineAsyncComponent, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import {
+  createEndereco,
+  getAllEnderecosCliente,
+  updateEndereco,
+  updateEnderecoPrincipal,
+} from "src/api/clientes-enderecos";
+import DefaultTable from "src/components/geral/DefaultTable.vue";
+
+const AddEditEnderecosDialog = defineAsyncComponent(
+  () => import("src/pages/clientes/components/AddEditEnderecosDialog.vue"),
+);
+
+const props = defineProps({
+  cliente: {
+    type: Object,
+    default: null,
+  },
+  enderecos: {
+    type: Array,
+    default: () => [],
+  },
+});
+
+const $q = useQuasar();
+const tableKey = ref(0);
+const enderecos_cliente = ref([]);
+
+const columns = [
+  {
+    name: "rua",
+    label: "Endereço",
+    field: "rua",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "cidade",
+    label: "Cidade",
+    field: "cidade",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "estado",
+    label: "Estado",
+    field: "estado",
+    align: "left",
+    style: "width: 20%",
+    required: true,
+  },
+  {
+    name: "numero",
+    label: "Número",
+    field: "numero",
+    align: "left",
+    style: "width: 1%",
+    required: true,
+  },
+  {
+    name: "principal",
+    label: "Principal",
+    field: "principal",
+    align: "left",
+    style: "width: 1%",
+    required: true,
+  },
+];
+
+const fetchEnderecos = async () => {
+  // if (permission_store.getAccess("enderecos", "view") === false) {
+  //   $q.loading.hide();
+  //   $q.notify({
+  //     type: "negative",
+  //     message: "Você não tem permissão para visualizar endereços",
+  //   });
+  //   return;
+  // }
+  const response = await getAllEnderecosCliente(props.cliente.id);
+  enderecos_cliente.value = response;
+  tableKey.value = tableKey.value + 1;
+};
+
+const onRowClick = ({ row }) => {
+  // if (permission_store.getAccess("enderecos", "view") === false) {
+  //   $q.loading.hide();
+  //   $q.notify({
+  //     type: "negative",
+  //     message: "Você não tem permissão para visualizar endereços",
+  //   });
+  //   return;
+  // }
+  $q.dialog({
+    component: AddEditEnderecosDialog,
+    componentProps: {
+      endereco: row,
+      enderecoId: row.id,
+      title: "Editar Endereço",
+    },
+  }).onOk(async (payload) => {
+    await updateEndereco(payload, row.id);
+    tableKey.value = tableKey.value + 1;
+  });
+};
+
+const onAddItem = () => {
+  // if (permission_store.getAccess("enderecos", "add") === false) {
+  //   $q.loading.hide();
+  //   $q.notify({
+  //     type: "negative",
+  //     message: "Você não tem permissão para adicionar endereços",
+  //   });
+  //   return;
+  // }
+  $q.dialog({
+    component: AddEditEnderecosDialog,
+
+    componentProps: {
+      title: "Novo Endereço",
+      clienteId: props.cliente.id,
+    },
+  }).onOk(async (payload) => {
+    await createEndereco(payload);
+    await fetchEnderecos();
+  });
+};
+
+const togglePrincipal = async (row) => {
+  // if (comp_store.getAccess("vuePageContatos", "editar") === false) {
+  //   $q.loading.hide();
+  //   $q.notify({
+  //     type: "negative",
+  //     message: "Você não tem permissão para editar!",
+  //   });
+  //   return;
+  // }
+
+  if (row.principal) {
+    $q.notify({
+      type: "negative",
+      message: "O cliente deve ter um endereço principal!",
+    });
+    return;
+  }
+
+  const response = await updateEnderecoPrincipal(row.id);
+
+  if (response) {
+    await fetchEnderecos();
+  }
+};
+
+onMounted(async () => {
+  await fetchEnderecos();
+});
+</script>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio