Sfoglia il codice sorgente

feat(products): adiciona estoque

ebagabee 1 mese fa
parent
commit
0ddf2249cf

+ 5 - 0
src/api/product.js

@@ -24,3 +24,8 @@ export const deleteProduct = async (id) => {
   const { data } = await api.delete(`/product/${id}`);
   return data;
 };
+
+export const adjustProductStock = async (id, payload) => {
+  const { data } = await api.patch(`/product/${id}/stock`, payload);
+  return data.payload;
+};

+ 168 - 0
src/pages/products/components/AddEditStockProductDialog.vue

@@ -0,0 +1,168 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <div style="width: 100%; max-width: 560px">
+      <q-card class="overflow-hidden" style="width: 100%">
+        <DefaultDialogHeader title="Ajustar Estoque" @close="onDialogCancel" />
+
+        <q-form @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 q-mb-md">
+              <DefaultInput
+                :model-value="product.name"
+                label="Nome do Produto"
+                class="col-12"
+                disable
+                readonly
+              />
+
+              <DefaultInput
+                :model-value="product.description"
+                label="Descrição"
+                class="col-12"
+                type="textarea"
+                autogrow
+                disable
+                readonly
+              />
+
+              <DefaultInput
+                :model-value="formatToBRLCurrency(product.price_sale)"
+                label="Valor Unitário"
+                class="col-6"
+                disable
+                readonly
+              />
+
+              <DefaultInput
+                :model-value="String(product.quantity)"
+                label="Estoque Atual"
+                class="col-6"
+                disable
+                readonly
+              />
+            </div>
+
+            <q-separator class="q-mb-md" />
+
+            <div class="text-h6 q-mb-sm">Movimentação</div>
+
+            <div class="row q-col-gutter-sm">
+              <DefaultSelect
+                v-model="form.type"
+                label="Tipo"
+                class="col-6"
+                :options="typeOptions"
+                emit-value
+                map-options
+              />
+
+              <DefaultInput
+                v-model="form.quantity"
+                label="Quantidade"
+                class="col-6"
+                type="number"
+                min="1"
+                :rules="[quantityRule]"
+              />
+            </div>
+
+            <div v-if="stockError" class="text-negative text-caption q-mt-sm">
+              {{ stockError }}
+            </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="Confirmar"
+              color="primary-2"
+              type="submit"
+              :loading="loading"
+            />
+          </q-card-actions>
+        </q-form>
+      </q-card>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, watch } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { adjustProductStock } 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 DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+const props = defineProps({
+  product: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const $q = useQuasar();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const loading = ref(false);
+const stockError = ref(null);
+
+const typeOptions = [
+  { label: "Entrada", value: "entrada" },
+  { label: "Saída", value: "saida" },
+];
+
+const form = ref({
+  type:     "entrada",
+  quantity: 1,
+});
+
+watch(() => [form.value.type, form.value.quantity], () => {
+  stockError.value = null;
+});
+
+const quantityRule = (val) => {
+  const qty = Number(val);
+  if (!qty || qty < 1) return "Informe uma quantidade válida.";
+  if (form.value.type === "saida" && qty > props.product.quantity) {
+    return `Estoque insuficiente. Disponível: ${props.product.quantity}`;
+  }
+  return true;
+};
+
+const onOKClick = async () => {
+  const qty = Number(form.value.quantity);
+
+  if (form.value.type === "saida" && qty > props.product.quantity) {
+    stockError.value = `Estoque insuficiente. Disponível: ${props.product.quantity}`;
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const updated = await adjustProductStock(props.product.id, {
+      type:     form.value.type,
+      quantity: qty,
+    });
+    onDialogOK(updated);
+  } catch (err) {
+    const msg = err?.response?.data?.message ?? "Erro ao ajustar estoque.";
+    stockError.value = msg;
+    $q.notify({ type: "negative", message: msg });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 71 - 1
src/pages/products/tabs/StockTab.vue

@@ -1,3 +1,73 @@
 <template>
-  <div>StockTab</div>
+  <DefaultTable
+    ref="tableRef"
+    :columns="columns"
+    :api-call="getProducts"
+    :show-search-field="false"
+    description="produtos"
+  >
+    <template #body-cell-actions="{ row }">
+      <q-btn
+        flat
+        dense
+        round
+        icon="mdi-file-edit-outline"
+        color="primary"
+        size="sm"
+        @click.stop="handleEdit(row)"
+      />
+    </template>
+  </DefaultTable>
 </template>
+
+<script setup>
+import { ref } from "vue";
+import { useQuasar } from "quasar";
+import { formatToBRLCurrency } from "src/helpers/utils";
+import { getProducts } from "src/api/product";
+
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import AddEditStockProductDialog from "../components/AddEditStockProductDialog.vue";
+
+const $q = useQuasar();
+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,
+  },
+  {
+    name:  "actions",
+    label: "Ações",
+    field: "actions",
+    align: "center",
+  },
+];
+
+const handleEdit = (product) => {
+  $q.dialog({
+    component: AddEditStockProductDialog,
+    componentProps: { product },
+  }).onOk(() => {
+    tableRef.value?.refresh();
+  });
+};
+</script>