Sfoglia il codice sorgente

feat(products): adiciona importacao de produtos

ebagabee 1 mese fa
parent
commit
a534bfbd92

+ 9 - 0
src/api/product.js

@@ -29,3 +29,12 @@ export const adjustProductStock = async (id, payload) => {
   const { data } = await api.patch(`/product/${id}/stock`, payload);
   return data.payload;
 };
+
+export const importProducts = async (file) => {
+  const formData = new FormData();
+  formData.append("file", file);
+  const { data } = await api.post("/product/import", formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};

+ 22 - 2
src/pages/products/ProductsPage.vue

@@ -39,6 +39,15 @@
           >
             <q-icon name="mdi-plus" color="white" size="20px" />
           </q-btn>
+
+          <q-btn
+            v-if="currentTab === 'orders'"
+            label="Importar"
+            color="primary-2"
+            icon="mdi-file-import-outline"
+            style="height: 40px"
+            @click="handleImport"
+          />
         </div>
       </div>
 
@@ -51,7 +60,7 @@
       </div>
 
       <div v-show="currentTab === 'orders'">
-        <OrdersTab />
+        <OrdersTab ref="ordersTabRef" />
       </div>
     </div>
   </div>
@@ -64,6 +73,7 @@ import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
 import AddEditProductDialog from "./components/AddEditProductDialog.vue";
+import ImportOrderDialog from "./components/ImportOrderDialog.vue";
 
 const ProductsTab = defineAsyncComponent(() => import("./tabs/ProductsTab.vue"));
 const StockTab = defineAsyncComponent(() => import("./tabs/StockTab.vue"));
@@ -75,10 +85,12 @@ const currentPage = ref(1);
 const totalPages = ref(1);
 const productsTabRef = shallowRef(null);
 const stockTabRef = shallowRef(null);
+const ordersTabRef = shallowRef(null);
 
 watch(currentTab, (tab) => {
   if (tab === "products") productsTabRef.value?.fetchProducts();
-  if (tab === "stock") stockTabRef.value?.refresh();
+  if (tab === "stock")    stockTabRef.value?.refresh();
+  if (tab === "orders")   ordersTabRef.value?.refresh();
 });
 
 const tabs = [
@@ -94,4 +106,12 @@ const handleAdd = () => {
     productsTabRef.value?.fetchProducts();
   });
 };
+
+const handleImport = () => {
+  $q.dialog({ component: ImportOrderDialog }).onOk(() => {
+    ordersTabRef.value?.refresh();
+    productsTabRef.value?.fetchProducts();
+    stockTabRef.value?.refresh();
+  });
+};
 </script>

+ 120 - 0
src/pages/products/components/ImportOrderDialog.vue

@@ -0,0 +1,120 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <div style="width: 100%; max-width: 680px">
+      <q-card class="overflow-hidden" style="width: 100%">
+        <DefaultDialogHeader title="Importar Pedido" @close="onDialogCancel" />
+
+        <q-card-section class="q-pt-sm">
+          <div class="row items-center q-col-gutter-sm">
+            <div class="col">
+              <q-file
+                v-model="selectedFile"
+                outlined
+                dense
+                accept=".xlsx,.xls"
+                label="Pesquisar arquivo Excel..."
+                @update:model-value="importedRows = []"
+              >
+                <template #prepend>
+                  <q-icon name="mdi-file-excel-outline" color="secondary" />
+                </template>
+              </q-file>
+            </div>
+
+            <div class="col-auto">
+              <q-btn
+                label="Importar"
+                color="primary-2"
+                :loading="loading"
+                :disable="!selectedFile"
+                style="height: 40px"
+                @click="handleImport"
+              />
+            </div>
+          </div>
+
+          <template v-if="importedRows.length > 0">
+            <q-separator class="q-my-md" />
+
+            <q-table
+              flat
+              dense
+              :rows="importedRows"
+              :columns="columns"
+              row-key="name"
+              hide-bottom
+              :pagination="{ rowsPerPage: 0 }"
+            >
+              <template #body-cell-price_sale="{ row }">
+                <q-td>{{ formatToBRLCurrency(row.price_sale) }}</q-td>
+              </template>
+              <template #body-cell-subtotal="{ row }">
+                <q-td>{{ formatToBRLCurrency(row.price_sale * row.quantity) }}</q-td>
+              </template>
+            </q-table>
+
+            <div class="row justify-end q-mt-sm q-pr-sm">
+              <span class="text-body2">
+                Total:
+                <strong>{{ formatToBRLCurrency(total) }}</strong>
+              </span>
+            </div>
+          </template>
+        </q-card-section>
+
+        <q-card-actions align="right" class="q-px-md q-pb-md">
+          <q-btn
+            label="Fechar"
+            outline
+            color="primary-2"
+            @click="importedRows.length > 0 ? onDialogOK() : onDialogCancel()"
+          />
+        </q-card-actions>
+      </q-card>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { importProducts } from "src/api/product";
+import { formatToBRLCurrency } from "src/helpers/utils";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const $q = useQuasar();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const selectedFile = ref(null);
+const loading = ref(false);
+const importedRows = ref([]);
+
+const columns = [
+  { name: "name",      label: "Descrição", field: "name",      align: "left" },
+  { name: "quantity",  label: "Qtde",      field: "quantity",  align: "center" },
+  { name: "price_sale", label: "Preço",   field: "price_sale", align: "right" },
+  { name: "subtotal",  label: "Subtotal",  field: "subtotal",  align: "right" },
+];
+
+const total = computed(() =>
+  importedRows.value.reduce((sum, r) => sum + r.price_sale * r.quantity, 0)
+);
+
+const handleImport = async () => {
+  if (!selectedFile.value) return;
+
+  loading.value = true;
+  try {
+    importedRows.value = await importProducts(selectedFile.value);
+  } catch (err) {
+    const msg = err?.response?.data?.message ?? "Erro ao importar arquivo.";
+    $q.notify({ type: "negative", message: msg });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 42 - 1
src/pages/products/tabs/OrdersTab.vue

@@ -1,3 +1,44 @@
 <template>
-  <div>OrdersTab</div>
+  <DefaultTable
+    ref="tableRef"
+    :columns="columns"
+    :api-call="getProducts"
+    :show-search-field="false"
+    description="produtos"
+  />
 </template>
+
+<script setup>
+import { ref } from "vue";
+import { formatToBRLCurrency } from "src/helpers/utils";
+import { getProducts } from "src/api/product";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+
+const tableRef = ref(null);
+
+const columns = [
+  {
+    name:     "name",
+    label:    "Produto",
+    field:    "name",
+    align:    "left",
+    sortable: true,
+  },
+  {
+    name:    "price_sale",
+    label:   "Preço",
+    field:   "price_sale",
+    align:   "left",
+    format:  (val) => formatToBRLCurrency(val),
+  },
+  {
+    name:     "quantity",
+    label:    "Estoque Atual",
+    field:    "quantity",
+    align:    "center",
+    sortable: true,
+  },
+];
+
+defineExpose({ refresh: () => tableRef.value?.refresh() });
+</script>