Przeglądaj źródła

feat: enhance package dialogs and pricing sections with new readonly component and toggle options

alvesantos 9 godzin temu
rodzic
commit
85a28c556f

+ 15 - 180
src/pages/packages/components/AddEditPackageDialog.vue

@@ -1,6 +1,6 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <div style="width: 100%; max-width: 700px">
+    <div style="width: 100%; max-width: 960px">
       <q-card
         class="dialog-form-card"
         style="width: 100%"
@@ -68,104 +68,6 @@
                       A duração padrão das aulas é de 2 horas e pode ser alterada se
                       necessário. É possível configurar no máximo dois dias.
                     </div>
-
-                    <DefaultCurrencyInput
-                      v-model="form.contract_register_value"
-                      v-model:error="validationErrors.contract_register_value"
-                      class="col-4"
-                      label="R$ Matrícula"
-                    />
-
-                    <DefaultCurrencyInput
-                      v-model="form.contract_value"
-                      v-model:error="validationErrors.contract_value"
-                      class="col-4"
-                      label="R$ Total do Contrato"
-                    />
-
-                    <DefaultInput
-                      v-model="form.contrat_discount_value"
-                      class="col-4"
-                      label="Desconto em %"
-                      max="100"
-                      min="0"
-                      type="number"
-                    />
-
-                    <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"
-                          class="col-4"
-                          emit-value
-                          label="Material"
-                          map-options
-                          :options="productOptions"
-                          :rules="
-                            material.product_id
-                              ? [inputRules.required]
-                              : []
-                          "
-                          @update:model-value="onProductSelected(material)"
-                        />
-
-                        <DefaultInput
-                          v-model="material.quantity"
-                          class="col-4"
-                          label="Qtd"
-                          min="1"
-                          type="number"
-                          :rules="
-                            material.product_id
-                              ? [
-                                  inputRules.required,
-                                  inputRules.minValue(1),
-                                ]
-                              : []
-                          "
-                        />
-
-                        <DefaultCurrencyInput
-                          v-model="material.price"
-                          class="col"
-                          label="R$ Unitário"
-                          :rules="
-                            material.product_id
-                              ? [inputRules.required]
-                              : []
-                          "
-                        />
-
-                        <div class="col-auto">
-                          <q-btn
-                            v-if="index === form.materials.length - 1"
-                            color="primary"
-                            icon="mdi-plus"
-                            style="
-                              border-radius: 8px;
-                              height: 40px;
-                              width: 40px;
-                            "
-                            unelevated
-                            @click="addMaterial"
-                          />
-
-                          <q-btn
-                            v-else
-                            color="negative"
-                            dense
-                            flat
-                            icon="mdi-delete-outline"
-                            round
-                            @click="removeMaterial(index)"
-                          />
-                        </div>
-                      </div>
-                    </div>
                   </div>
                 </div>
 
@@ -175,7 +77,9 @@
                 >
                   <ProposalPricingTab
                     v-model="form.pavao"
+                    v-model:enabled="form.pavao_enabled"
                     :product-options="productOptions"
+                    section-label="Pavão"
                   />
                 </div>
 
@@ -185,7 +89,9 @@
                 >
                   <ProposalPricingTab
                     v-model="form.irrecusavel"
+                    v-model:enabled="form.irrecusavel_enabled"
                     :product-options="productOptions"
+                    section-label="Irrecusável"
                     show-condition
                   />
                 </div>
@@ -241,11 +147,9 @@ import { useScroll } from "src/composables/useScroll";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
 
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
-import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import DefaultForm from "src/components/defaults/DefaultForm.vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import ProposalPricingTab from "./ProposalPricingTab.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
@@ -311,26 +215,12 @@ const { form, getUpdatedFields } = useForm({
   class_duration_hours:
     (props.package?.class_duration_minutes ?? 120) / 60,
 
-  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,
-
   irrecusavel: defaultPricingBlock(true),
-
-  materials: [
-    {
-      price: null,
-      product_id: null,
-      quantity: 1,
-    },
-  ],
+  irrecusavel_enabled: false,
 
   name: props.package?.name ?? null,
   pavao: defaultPricingBlock(),
+  pavao_enabled: false,
   quantity_classes: props.package?.quantity_classes ?? null,
 });
 
@@ -347,14 +237,6 @@ const {
   scrollFn: scrollToComponent,
 });
 
-const addMaterial = () => {
-  form.materials.push({
-    price: null,
-    product_id: null,
-    quantity: 1,
-  });
-};
-
 const mapPricingBlockFromApi = (data, existingMaterials) => ({
   classes_discount_percentage: data.classes_discount_percentage,
   classes_included_in_course: data.classes_included_in_course,
@@ -423,33 +305,17 @@ const toPricingPayload = (block, { withCondition = false } = {}) => {
 };
 
 const onOKClick = async () => {
-  const validMaterials = form.materials.filter(
-    (material) => material.product_id,
-  );
-
   const payload = {
     class_duration_minutes: Math.round(
       Number(form.class_duration_hours) * 60,
     ),
 
-    contract_register_value:
-      form.contract_register_value,
-
-    contract_value: form.contract_value,
-
-    contrat_discount_value:
-      form.contrat_discount_value,
-
-    irrecusavel: toPricingPayload(form.irrecusavel, { withCondition: true }),
-
-    materials: validMaterials.map((material) => ({
-      price: Number(material.price),
-      product_id: material.product_id,
-      quantity: Number(material.quantity),
-    })),
+    irrecusavel: form.irrecusavel_enabled
+      ? toPricingPayload(form.irrecusavel, { withCondition: true })
+      : null,
 
     name: form.name,
-    pavao: toPricingPayload(form.pavao),
+    pavao: form.pavao_enabled ? toPricingPayload(form.pavao) : null,
     quantity_classes: form.quantity_classes,
   };
 
@@ -461,13 +327,7 @@ const onOKClick = async () => {
     const changedFields = getUpdatedFields.value;
     const updatePayload = {};
 
-    const directKeys = [
-      "name",
-      "quantity_classes",
-      "contract_value",
-      "contract_register_value",
-      "contrat_discount_value",
-    ];
+    const directKeys = ["name", "quantity_classes"];
 
     for (const key of directKeys) {
       if (key in changedFields) {
@@ -480,12 +340,8 @@ const onOKClick = async () => {
         payload.class_duration_minutes;
     }
 
-    if ("materials" in changedFields) {
-      updatePayload.materials = payload.materials;
-    }
-
     // Pavão/Irrecusável têm validação cruzada entre campos (par exclusivo de
-    // checkbox) que não é seguro fatiar num diff parcial — vão inteiros.
+    // checkbox) e um sinal explícito de "removido" (null) — vão sempre inteiros.
     updatePayload.pavao = payload.pavao;
     updatePayload.irrecusavel = payload.irrecusavel;
 
@@ -496,20 +352,6 @@ const onOKClick = async () => {
   });
 };
 
-const onProductSelected = (material) => {
-  const option = productOptions.value.find(
-    (product) => product.value === material.product_id,
-  );
-
-  if (option) {
-    material.price = option.price_sale;
-  }
-};
-
-const removeMaterial = (index) => {
-  form.materials.splice(index, 1);
-};
-
 onMounted(async () => {
   const productsResponse = await getProductsForSelect();
 
@@ -524,15 +366,8 @@ onMounted(async () => {
   form.class_duration_hours =
     (packageData.class_duration_minutes ?? 120) / 60;
 
-  if (packageData.materials?.length) {
-    form.materials = packageData.materials.map(
-      (material) => ({
-        price: material.price,
-        product_id: material.product_id,
-        quantity: material.quantity,
-      }),
-    );
-  }
+  form.pavao_enabled = !!packageData.pavao;
+  form.irrecusavel_enabled = !!packageData.irrecusavel;
 
   if (packageData.pavao) {
     form.pavao = mapPricingBlockFromApi(packageData.pavao, form.pavao.materials);

+ 11 - 5
src/pages/packages/components/PackageCard.vue

@@ -17,12 +17,18 @@
         Duração padrão: {{ (pkg.class_duration_minutes ?? 120) / 60 }}h
       </div>
 
-      <div class="column" style="gap: 2px">
-        <span class="text-body2">Contrato {{ formatToBRLCurrency(pkg.contract_value) }}</span>
-        <span class="text-body2">
-          Material {{ formatToBRLCurrency(pkg.contract_material_value) }} — Incluso
+      <div class="row q-gutter-xs">
+        <q-badge v-if="pkg.pavao" color="primary" outline>
+          Pavão {{ formatToBRLCurrency(pkg.pavao.total_value) }}
+        </q-badge>
+
+        <q-badge v-if="pkg.irrecusavel" color="secondary" outline>
+          Irrecusável {{ formatToBRLCurrency(pkg.irrecusavel.total_value) }}
+        </q-badge>
+
+        <span v-if="!pkg.pavao && !pkg.irrecusavel" class="text-caption text-grey-5">
+          Sem precificação definida
         </span>
-        <span class="text-body2">Matrícula {{ formatToBRLCurrency(pkg.contract_register_value) }}</span>
       </div>
     </q-card-section>
   </q-card>

+ 18 - 67
src/pages/packages/components/PackageDetailDialog.vue

@@ -1,6 +1,6 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <div style="width: 100%; max-width: 700px">
+    <div style="width: 100%; max-width: 960px">
       <q-card class="overflow-hidden" style="width: 100%">
         <q-btn
           flat
@@ -49,22 +49,9 @@
               />
 
               <q-input
-                :model-value="formatToBRLCurrency(pkg.contract_register_value)"
-                label="R$ Matrícula"
-                class="col-4"
-                outlined
-                dense
-                readonly
-                label-color="secondary"
-                color="secondary"
-                bg-color="white"
-                style="border-radius: 8px"
-              />
-
-              <q-input
-                :model-value="formatToBRLCurrency(pkg.contract_value)"
-                label="R$ Total do Contrato"
-                class="col-4"
+                :model-value="((pkg.class_duration_minutes ?? 120) / 60) + 'h'"
+                label="Duração da Aula"
+                class="col-12"
                 outlined
                 dense
                 readonly
@@ -73,55 +60,17 @@
                 bg-color="white"
                 style="border-radius: 8px"
               />
+            </div>
+          </div>
 
-              <q-input
-                :model-value="pkg.contrat_discount_value ? `D ${pkg.contrat_discount_value}%` : '—'"
-                label="Desconto"
-                class="col-4"
-                outlined
-                dense
-                readonly
-                label-color="secondary"
-                color="secondary"
-                bg-color="white"
-                style="border-radius: 8px"
-              />
+          <!-- Tab: Pavão (read-only) -->
+          <div v-if="pkg.pavao" v-show="currentTab === 'pavao'">
+            <ReadonlyPricingSection :block="pkg.pavao" />
+          </div>
 
-              <template v-if="pkg.materials?.length">
-                <div
-                  v-for="material in pkg.materials"
-                  :key="material.product_id"
-                  class="col-12"
-                >
-                  <div class="row q-col-gutter-sm items-center">
-                    <q-input
-                      :model-value="material.name"
-                      label="Material"
-                      class="col"
-                      outlined
-                      dense
-                      readonly
-                      label-color="secondary"
-                      color="secondary"
-                      bg-color="white"
-                      style="border-radius: 8px"
-                    />
-                    <q-input
-                      :model-value="formatToBRLCurrency(material.price)"
-                      label="R$ Unitário"
-                      class="col-3"
-                      outlined
-                      dense
-                      readonly
-                      label-color="secondary"
-                      color="secondary"
-                      bg-color="white"
-                      style="border-radius: 8px"
-                    />
-                  </div>
-                </div>
-              </template>
-            </div>
+          <!-- Tab: Irrecusável (read-only) -->
+          <div v-if="pkg.irrecusavel" v-show="currentTab === 'irrecusavel'">
+            <ReadonlyPricingSection :block="pkg.irrecusavel" show-condition />
           </div>
 
           <!-- Tab: Neurotrainer/Aula (shell — sem funcionalidade ainda) -->
@@ -235,12 +184,12 @@ import { computed, ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
-import { formatToBRLCurrency } from "src/helpers/utils";
+import ReadonlyPricingSection from "./ReadonlyPricingSection.vue";
 import { permissionStore } from "src/stores/permission";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-defineProps({
+const props = defineProps({
   pkg: {
     type: Object,
     required: true,
@@ -258,7 +207,9 @@ const canViewClassAssignments = computed(
 );
 
 const tabs = computed(() => [
-  { name: "dados", label: "Dados da Unidade" },
+  { name: "dados", label: "Dados" },
+  ...(props.pkg.pavao ? [{ name: "pavao", label: "Pavão" }] : []),
+  ...(props.pkg.irrecusavel ? [{ name: "irrecusavel", label: "Irrecusável" }] : []),
   ...(canViewClassAssignments.value
     ? [{ name: "neurotrainer", label: "Neurotrainer/Aula" }]
     : []),

+ 15 - 0
src/pages/packages/components/ProposalPricingTab.vue

@@ -1,5 +1,14 @@
 <template>
   <div class="row q-col-gutter-md">
+    <div class="col-12">
+      <q-toggle
+        v-model="enabled"
+        color="primary"
+        :label="`Esta proposta possui ${sectionLabel}`"
+      />
+    </div>
+
+    <template v-if="enabled">
     <!-- Matrícula -->
     <DefaultCurrencyInput
       v-model="modelValue.registration_value"
@@ -185,6 +194,7 @@
         type="textarea"
       />
     </template>
+    </template>
   </div>
 </template>
 
@@ -202,12 +212,17 @@ import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 const { inputRules } = useInputRules();
 
 const modelValue = defineModel({ type: Object, required: true });
+const enabled = defineModel("enabled", { type: Boolean, default: false });
 
 const props = defineProps({
   productOptions: {
     default: () => [],
     type: Array,
   },
+  sectionLabel: {
+    default: "esta modalidade",
+    type: String,
+  },
   showCondition: {
     default: false,
     type: Boolean,

+ 96 - 0
src/pages/packages/components/ReadonlyPricingSection.vue

@@ -0,0 +1,96 @@
+<template>
+  <div class="column q-gutter-y-md">
+    <div class="pricing-row">
+      <div class="row items-center justify-between">
+        <span class="text-weight-medium">Matrícula</span>
+        <span>{{ formatToBRLCurrency(block.registration_value) }}</span>
+      </div>
+      <div class="text-caption text-grey-6">
+        {{ statusLabel(block.registration_included_in_course, block.registration_max_installments) }}
+      </div>
+    </div>
+
+    <q-separator />
+
+    <div class="pricing-row">
+      <div class="row items-center justify-between">
+        <span class="text-weight-medium">Aulas</span>
+        <span>{{ formatToBRLCurrency(block.classes_value) }}</span>
+      </div>
+      <div class="text-caption text-grey-6">
+        {{ statusLabel(block.classes_included_in_course, block.classes_max_installments) }}
+        <template v-if="block.classes_discount_percentage">
+          — Desconto {{ block.classes_discount_percentage }}%
+        </template>
+      </div>
+    </div>
+
+    <q-separator />
+
+    <div class="pricing-row">
+      <div class="row items-center justify-between">
+        <span class="text-weight-medium">Materiais</span>
+        <span>{{ formatToBRLCurrency(block.materials_total_value) }}</span>
+      </div>
+      <div class="text-caption text-grey-6">
+        {{ statusLabel(block.materials_included_in_course, block.materials_max_installments) }}
+      </div>
+
+      <div
+        v-for="material in block.materials"
+        :key="material.product_id"
+        class="row items-center justify-between text-body2 text-grey-8 q-pl-sm q-pt-xs"
+      >
+        <span>{{ material.name }} (x{{ material.quantity }})</span>
+        <span>{{ formatToBRLCurrency(material.price) }}</span>
+      </div>
+    </div>
+
+    <q-separator />
+
+    <div class="pricing-row">
+      <div class="row items-center justify-between">
+        <span class="text-weight-bold">Valor Total do Curso</span>
+        <span class="text-weight-bold">{{ formatToBRLCurrency(block.total_value) }}</span>
+      </div>
+      <div class="text-caption text-grey-6">
+        Em até {{ block.total_max_installments }}x
+      </div>
+    </div>
+
+    <template v-if="showCondition">
+      <q-separator />
+
+      <div class="pricing-row">
+        <div class="text-weight-medium q-mb-xs">Condição</div>
+        <div class="text-body2 text-grey-8">{{ block.condition || "—" }}</div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { formatToBRLCurrency } from "src/helpers/utils";
+
+defineProps({
+  block: {
+    type: Object,
+    required: true,
+  },
+  showCondition: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const statusLabel = (includedInCourse, maxInstallments) =>
+  includedInCourse
+    ? "Incluso no valor do curso"
+    : `Parcelado em até ${maxInstallments}x`;
+</script>
+
+<style scoped>
+.pricing-row {
+  padding: 4px 0;
+}
+</style>