Sfoglia il codice sorgente

feat: adiciona pacotes para franquia, atualiza pacote por unidade

ebagabee 1 mese fa
parent
commit
1df631d9e8

+ 30 - 1
src/api/package.js

@@ -1,6 +1,35 @@
 import api from "src/api";
 
 export const getUnitPackages = async () => {
-  const { data } = await api.get("/class-package/by-unit");
+  const { data } = await api.get("/class-package-unit/visible");
   return data.payload;
 };
+
+export const getAllUnitPackages = async () => {
+  const { data } = await api.get("/class-package-unit");
+  return data.payload;
+};
+
+export const getUnitPackage = async (id) => {
+  const { data } = await api.get(`/class-package-unit/${id}`);
+  return data.payload;
+};
+
+export const createUnitPackage = async (payload) => {
+  const { data } = await api.post("/class-package-unit", payload);
+  return data.payload;
+};
+
+export const updateUnitPackage = async (id, payload) => {
+  const { data } = await api.put(`/class-package-unit/${id}`, payload);
+  return data.payload;
+};
+
+export const toggleUnitPackageVisibility = async (id) => {
+  const { data } = await api.patch(`/class-package-unit/${id}/toggle-visibility`);
+  return data.payload;
+};
+
+export const deleteUnitPackage = async (id) => {
+  await api.delete(`/class-package-unit/${id}`);
+};

+ 6 - 0
src/api/product.js

@@ -0,0 +1,6 @@
+import api from "src/api";
+
+export const getProductsForSelect = async () => {
+  const { data } = await api.get("/product/all/select");
+  return data.payload;
+};

+ 7 - 16
src/components/defaults/DefaultCurrencyInput.vue

@@ -10,9 +10,8 @@
 </template>
 
 <script setup>
-import { watch, onBeforeMount, ref } from "vue";
+import { computed, watch } from "vue";
 import { useCurrencyInput } from "vue-currency-input";
-import { useI18n } from "vue-i18n";
 import { useInputRules } from "src/composables/useInputRules";
 
 import DefaultInput from "./DefaultInput.vue";
@@ -21,9 +20,6 @@ const { inputRules } = useInputRules();
 
 const model = defineModel({ type: Number });
 
-const defaultRules = [inputRules.minValue(0)];
-const finalRules = ref([]);
-
 const { options, label, rules } = defineProps({
   options: {
     type: Object,
@@ -41,7 +37,7 @@ const { options, label, rules } = defineProps({
   },
   label: {
     type: String,
-    default: useI18n().t("common.terms.currency"),
+    default: "Valor",
   },
   errorMessage: {
     type: String,
@@ -74,14 +70,9 @@ watch(
   },
 );
 
-watch(
-  () => rules,
-  (value) => {
-    finalRules.value = [...value, ...defaultRules];
-  },
-);
-
-onBeforeMount(() => {
-  finalRules.value = [...rules, ...defaultRules];
-});
+const minRule = inputRules.minValue(0);
+const finalRules = computed(() => [
+  ...rules,
+  () => minRule(numberValue.value),
+]);
 </script>

+ 25 - 6
src/pages/packages/PackagesPage.vue

@@ -3,6 +3,17 @@
     <DefaultHeaderPage title="Pacotes" />
 
     <div class="q-px-sm">
+      <!-- Add button -->
+      <div class="row justify-end q-mb-md">
+        <q-btn
+          color="primary"
+          icon="mdi-plus"
+          unelevated
+          style="border-radius: 8px; height: 40px; width: 40px"
+          @click="openAddDialog"
+        />
+      </div>
+
       <!-- Loading skeleton -->
       <div v-if="loading" class="row q-col-gutter-sm">
         <div v-for="n in 6" :key="n" class="col-12 col-sm-6 col-md-4">
@@ -25,7 +36,7 @@
           :key="pkg.id"
           class="col-12 col-sm-6 col-md-4"
         >
-          <PackageCard :pkg="pkg" @click="openDetail(pkg)" />
+          <PackageCard :pkg="pkg" @click="openEditDialog(pkg)" />
         </div>
 
         <div v-if="packages.length === 0" class="col-12 text-center text-grey-5 q-py-xl">
@@ -42,9 +53,9 @@ import { useQuasar } from "quasar";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import PackageCard from "./components/PackageCard.vue";
-import PackageDetailDialog from "./components/PackageDetailDialog.vue";
+import AddEditPackageDialog from "./components/AddEditPackageDialog.vue";
 
-import { getUnitPackages } from "src/api/package";
+import { getAllUnitPackages } from "src/api/package";
 
 const $q = useQuasar();
 const loading = ref(false);
@@ -53,14 +64,22 @@ const packages = ref([]);
 const fetchPackages = async () => {
   loading.value = true;
   try {
-    packages.value = await getUnitPackages();
+    packages.value = await getAllUnitPackages();
   } finally {
     loading.value = false;
   }
 };
 
-const openDetail = (pkg) => {
-  $q.dialog({ component: PackageDetailDialog, componentProps: { pkg } });
+const openAddDialog = () => {
+  $q.dialog({ component: AddEditPackageDialog }).onOk(() => {
+    fetchPackages();
+  });
+};
+
+const openEditDialog = (pkg) => {
+  $q.dialog({ component: AddEditPackageDialog, componentProps: { package: pkg } }).onOk(() => {
+    fetchPackages();
+  });
 };
 
 onMounted(fetchPackages);

+ 233 - 0
src/pages/packages/components/AddEditPackageDialog.vue

@@ -0,0 +1,233 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <div style="width: 100%; max-width: 700px">
+      <q-card class="overflow-hidden" style="width: 100%">
+        <q-btn
+          flat
+          round
+          dense
+          icon="mdi-close"
+          class="absolute-top-right q-ma-sm"
+          style="z-index: 1"
+          @click="onDialogCancel"
+        />
+
+        <q-form ref="formRef" @submit="onOKClick">
+          <q-card-section class="q-pt-lg">
+            <div class="row q-col-gutter-sm">
+              <DefaultInput
+                v-model="form.name"
+                label="Nome do Pacote"
+                class="col-12"
+              />
+
+              <DefaultInput
+                v-model="form.quantity_classes"
+                label="Quantidade de Aulas"
+                class="col-12"
+                type="number"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.contract_register_value"
+                label="R$ Matrícula"
+                class="col-4"
+              />
+
+              <DefaultCurrencyInput
+                v-model="form.contract_value"
+                label="R$ Total do Contrato"
+                class="col-4"
+              />
+
+              <DefaultInput
+                v-model="form.contrat_discount_value"
+                label="Desconto em %"
+                class="col-4"
+                type="number"
+                min="0"
+                max="100"
+              />
+
+              <div
+                v-for="(material, index) in form.materials"
+                :key="index"
+                class="col-12"
+              >
+                <div class="row q-col-gutter-sm items-center">
+                  <DefaultSelect
+                    v-model="material.product_id"
+                    label="Material"
+                    class="col"
+                    :options="productOptions"
+                    emit-value
+                    map-options
+                    @update:model-value="onProductSelected(material)"
+                  />
+
+                  <DefaultInput
+                    v-model="material.quantity"
+                    label="Qtd"
+                    class="col-2"
+                    type="number"
+                    min="1"
+                  />
+
+                  <DefaultCurrencyInput
+                    v-model="material.price"
+                    label="R$ Unitário"
+                    class="col-3"
+                  />
+
+                  <div class="col-auto">
+                    <q-btn
+                      v-if="index === form.materials.length - 1"
+                      color="primary"
+                      icon="mdi-plus"
+                      unelevated
+                      style="border-radius: 8px; height: 40px; width: 40px"
+                      @click="addMaterial"
+                    />
+                    <q-btn
+                      v-else
+                      flat
+                      round
+                      dense
+                      icon="mdi-delete-outline"
+                      color="negative"
+                      @click="removeMaterial(index)"
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          </q-card-section>
+
+          <q-card-actions align="right" class="q-px-md q-pb-md">
+            <q-btn
+              outline
+              color="primary"
+              label="Cancelar"
+              @click="onDialogCancel"
+            />
+            <q-btn
+              color="primary"
+              label="Salvar"
+              type="submit"
+              :loading="loading"
+            />
+          </q-card-actions>
+        </q-form>
+      </q-card>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+
+import {
+  getUnitPackage,
+  createUnitPackage,
+  updateUnitPackage,
+} from "src/api/package";
+import { getProductsForSelect } from "src/api/product";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const props = defineProps({
+  package: {
+    type: Object,
+    default: null,
+  },
+});
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const formRef = ref(null);
+const loading = ref(false);
+
+const products = ref([]);
+
+const productOptions = computed(() =>
+  products.value.map((p) => ({
+    label: p.name,
+    value: p.id,
+    price_sale: p.price_sale,
+  })),
+);
+
+const form = ref({
+  name: props.package?.name ?? null,
+  quantity_classes: props.package?.quantity_classes ?? null,
+  contract_register_value: props.package?.contract_register_value ?? null,
+  contract_value: props.package?.contract_value ?? null,
+  contrat_discount_value: props.package?.contrat_discount_value ?? null,
+  materials: [{ product_id: null, quantity: 1, price: null }],
+});
+
+const onProductSelected = (material) => {
+  const option = productOptions.value.find(
+    (o) => o.value === material.product_id,
+  );
+  if (option) material.price = option.price_sale;
+};
+
+const addMaterial = () => {
+  form.value.materials.push({ product_id: null, quantity: 1, price: null });
+};
+
+const removeMaterial = (index) => {
+  form.value.materials.splice(index, 1);
+};
+
+onMounted(async () => {
+  const [allProducts] = await Promise.all([getProductsForSelect()]);
+  products.value = allProducts;
+
+  if (props.package?.id) {
+    const pkg = await getUnitPackage(props.package.id);
+    if (pkg.materials?.length) {
+      form.value.materials = pkg.materials.map((m) => ({
+        product_id: m.product_id,
+        quantity: m.quantity,
+        price: m.price,
+      }));
+    }
+  }
+});
+
+const onOKClick = async () => {
+  loading.value = true;
+  try {
+    const validMaterials = form.value.materials.filter((m) => m.product_id);
+
+    const payload = {
+      name: form.value.name,
+      quantity_classes: form.value.quantity_classes,
+      contract_value: form.value.contract_value,
+      contract_register_value: form.value.contract_register_value,
+      contrat_discount_value: form.value.contrat_discount_value,
+      materials: validMaterials.map((m) => ({
+        product_id: m.product_id,
+        quantity: Number(m.quantity),
+        price: Number(m.price),
+      })),
+    };
+
+    const result = props.package?.id
+      ? await updateUnitPackage(props.package.id, payload)
+      : await createUnitPackage(payload);
+
+    onDialogOK(result);
+  } finally {
+    loading.value = false;
+  }
+};
+</script>