Browse Source

feat(products): adiciona quantidade

ebagabee 1 month ago
parent
commit
ae9ba28942

+ 7 - 4
src/pages/products/ProductsPage.vue

@@ -43,7 +43,7 @@
       </div>
 
       <div v-show="currentTab === 'products'">
-        <ProductsTab />
+        <ProductsTab ref="productsTabRef" />
       </div>
 
       <div v-show="currentTab === 'stock'">
@@ -58,12 +58,12 @@
 </template>
 
 <script setup>
-import { defineAsyncComponent, ref } from "vue";
+import { defineAsyncComponent, ref, shallowRef } from "vue";
 import { useQuasar } from "quasar";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
-import AddProductDialog from "./components/AddProductDialog.vue";
+import AddEditProductDialog from "./components/AddEditProductDialog.vue";
 
 const ProductsTab = defineAsyncComponent(() => import("./tabs/ProductsTab.vue"));
 const StockTab = defineAsyncComponent(() => import("./tabs/StockTab.vue"));
@@ -73,6 +73,7 @@ const currentTab = ref("products");
 const search = ref("");
 const currentPage = ref(1);
 const totalPages = ref(1);
+const productsTabRef = shallowRef(null);
 
 const tabs = [
   { name: "products", label: "Produtos" },
@@ -83,6 +84,8 @@ const tabs = [
 const $q = useQuasar();
 
 const handleAdd = () => {
-  $q.dialog({ component: AddProductDialog });
+  $q.dialog({ component: AddEditProductDialog }).onOk(() => {
+    productsTabRef.value?.fetchProducts();
+  });
 };
 </script>

+ 134 - 0
src/pages/products/components/AddEditProductDialog.vue

@@ -0,0 +1,134 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <div style="width: 100%; max-width: 600px">
+      <q-card class="overflow-hidden" style="width: 100%">
+        <DefaultDialogHeader
+          :title="isEdit ? 'Editar Produto' : 'Novo Produto'"
+          @close="onDialogCancel"
+        />
+
+        <q-form ref="formRef" @submit="onOKClick">
+          <q-card-section class="q-pt-sm">
+            <div class="text-h6 q-mb-sm">Produto</div>
+
+            <div class="row q-col-gutter-sm">
+              <DefaultInput
+                v-model="form.name"
+                label="Nome do Produto"
+                class="col-12"
+              />
+
+              <DefaultInput
+                v-model="form.description"
+                label="Descrição"
+                class="col-12"
+                type="textarea"
+                autogrow
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.unitPrice"
+                label="Valor Unitário"
+                class="col-4"
+              />
+
+              <DefaultInput
+                v-model="form.quantity"
+                label="Quantidade"
+                class="col-4"
+                type="number"
+                min="0"
+              />
+
+              <DefaultInput
+                :model-value="totalValueFormatted"
+                label="Valor Total do Produto"
+                class="col-4"
+                disable
+                readonly
+              />
+            </div>
+          </q-card-section>
+
+          <q-card-actions align="right" class="q-px-md q-pb-md">
+            <q-btn
+              label="Cancelar"
+              outline
+              color="primary-2"
+              @click="onDialogCancel"
+            />
+            <q-btn
+              :label="isEdit ? 'Salvar Alterações' : 'Cadastrar Produto'"
+              color="primary-2"
+              type="submit"
+              :loading="loading"
+            />
+          </q-card-actions>
+        </q-form>
+      </q-card>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { createProduct, updateProduct } from "src/api/product";
+import { formatToBRLCurrency } from "src/helpers/utils";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+
+const props = defineProps({
+  product: {
+    type: Object,
+    default: null,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const $q = useQuasar();
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const formRef = ref(null);
+const loading = ref(false);
+
+const isEdit = computed(() => !!props.product);
+
+const form = ref({
+  name:        props.product?.name        ?? "",
+  description: props.product?.description ?? "",
+  unitPrice:   props.product?.price_sale  ?? 0,
+  quantity:    props.product?.quantity    ?? 0,
+});
+
+const totalValue = computed(() => form.value.unitPrice * form.value.quantity);
+const totalValueFormatted = computed(() => formatToBRLCurrency(totalValue.value));
+
+const onOKClick = async () => {
+  loading.value = true;
+  try {
+    const payload = {
+      name:        form.value.name,
+      description: form.value.description,
+      price_sale:  form.value.unitPrice,
+      quantity:    Number(form.value.quantity),
+    };
+    const product = isEdit.value
+      ? await updateProduct(props.product.id, payload)
+      : await createProduct(payload);
+    onDialogOK(product);
+  } catch {
+    $q.notify({
+      type: "negative",
+      message: isEdit.value ? "Erro ao editar produto." : "Erro ao cadastrar produto.",
+    });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 6 - 0
src/pages/products/components/ProductCard.vue

@@ -41,6 +41,10 @@
             </div>
           </div>
           <slot name="details" />
+          <div class="row justify-end" style="gap: 4px; margin-top: 4px">
+            <q-btn flat dense round icon="mdi-pencil" color="primary" size="sm" @click.stop="$emit('edit')" />
+            <q-btn flat dense round icon="mdi-delete" color="negative" size="sm" @click.stop="$emit('delete')" />
+          </div>
         </div>
       </q-card-section>
     </q-expansion-item>
@@ -53,6 +57,8 @@ import { formatToBRLCurrency } from "src/helpers/utils";
 
 const expanded = ref(false);
 
+defineEmits(["edit", "delete"]);
+
 defineProps({
   title: {
     type: String,

+ 74 - 35
src/pages/products/tabs/ProductsTab.vue

@@ -1,44 +1,83 @@
 <template>
   <div class="column" style="gap: 8px">
-    <ProductCard
-      v-for="product in mockProducts"
-      :key="product.id"
-      :title="product.title"
-      :description="product.description"
-      :unit-price="product.unitPrice"
-      :current-stock="product.currentStock"
-      :total-value="product.totalValue"
-    />
+    <div v-if="loading" class="row justify-center q-pa-lg">
+      <q-spinner color="primary" size="32px" />
+    </div>
+
+    <template v-else>
+      <ProductCard
+        v-for="product in products"
+        :key="product.id"
+        :title="product.name"
+        :description="product.description"
+        :unit-price="product.price_sale"
+        :current-stock="product.quantity"
+        :total-value="product.price_sale * product.quantity"
+        @edit="handleEdit(product)"
+        @delete="handleDelete(product)"
+      />
+
+      <div v-if="products.length === 0" class="text-center text-grey q-pa-lg">
+        Nenhum produto cadastrado.
+      </div>
+    </template>
   </div>
 </template>
 
 <script setup>
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
 import ProductCard from "../components/ProductCard.vue";
+import AddEditProductDialog from "../components/AddEditProductDialog.vue";
+import { getProducts, deleteProduct } from "src/api/product";
+
+const $q = useQuasar();
+
+const products = ref([]);
+const loading = ref(false);
+
+const fetchProducts = async () => {
+  loading.value = true;
+  try {
+    products.value = await getProducts();
+  } catch {
+    $q.notify({ type: "negative", message: "Erro ao carregar produtos." });
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(fetchProducts);
+
+defineExpose({ fetchProducts });
+
+const handleEdit = (product) => {
+  $q.dialog({
+    component: AddEditProductDialog,
+    componentProps: { product },
+  }).onOk((updated) => {
+    const index = products.value.findIndex((p) => p.id === product.id);
+    if (index !== -1) {
+      products.value[index] = updated;
+    }
+  });
+};
 
-const mockProducts = [
-  {
-    id: 1,
-    title: "Apostila Ginástica Cerebro Vol. 1",
-    description: "Material didático completo com conteúdo para os primeiros 3 meses de prática. Inclui exercícios cognitivos, fichas de avaliação e guia do professor.",
-    unitPrice: 89.9,
-    currentStock: 142,
-    totalValue: 12761.8,
-  },
-  {
-    id: 2,
-    title: "Kit Iniciante",
-    description: "Kit completo para novos alunos contendo apostila, camiseta e carteirinha de identificação.",
-    unitPrice: 159.9,
-    currentStock: 0,
-    totalValue: 0,
-  },
-  {
-    id: 3,
-    title: "Camiseta Oficial GC",
-    description: "Camiseta oficial da Ginástica Cerebro em malha 100% algodão. Disponível nos tamanhos P, M, G e GG.",
-    unitPrice: 49.9,
-    currentStock: 37,
-    totalValue: 1846.3,
-  },
-];
+const handleDelete = (product) => {
+  $q.dialog({
+    title: "Excluir Produto",
+    message: `Deseja excluir o produto "${product.name}"?`,
+    cancel: { label: "Cancelar", flat: true, color: "primary" },
+    ok: { label: "Excluir", color: "negative" },
+    persistent: true,
+  }).onOk(async () => {
+    try {
+      await deleteProduct(product.id);
+      products.value = products.value.filter((p) => p.id !== product.id);
+      $q.notify({ type: "positive", message: "Produto excluído com sucesso." });
+    } catch {
+      $q.notify({ type: "negative", message: "Erro ao excluir produto." });
+    }
+  });
+};
 </script>