فهرست منبع

Merge branch 'feature/gc-gab-proposals' of Softpar/sfp_vue_franchisee_ginastica_cerebro into development

Gabriel Alves 15 ساعت پیش
والد
کامیت
412162de08

+ 2 - 2
src/pages/packages/PackagesPage.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <DefaultHeaderPage title="Pacotes" />
+    <DefaultHeaderPage title="Proposta Comercial" />
 
     <div class="q-px-sm">
       <!-- Add button -->
@@ -49,7 +49,7 @@
           v-if="packages.length === 0"
           class="col-12 text-center text-grey-5 q-py-xl"
         >
-          Nenhum pacote disponível para sua unidade
+          Nenhuma proposta comercial disponível para sua unidade
         </div>
       </div>
     </div>

+ 270 - 117
src/pages/packages/components/AddEditPackageDialog.vue

@@ -6,7 +6,7 @@
         style="width: 100%"
       >
         <DefaultDialogHeader
-          :title="props.package ? 'Editar Pacote' : 'Novo Pacote'"
+          :title="props.package ? 'Editar Proposta Comercial' : 'Nova Proposta Comercial'"
           @close="onDialogCancel"
         />
 
@@ -20,141 +20,175 @@
           >
             <q-card-section class="q-pt-sm">
               <div class="row q-col-gutter-sm">
-                <DefaultInput
-                  v-model="form.name"
-                  v-model:error="validationErrors.name"
-                  class="col-12"
-                  label="Nome do Pacote"
-                  :rules="[inputRules.required]"
+                <CustomTabComponent
+                  v-model:active-tab="currentTab"
+                  class="col-12 q-mb-md"
+                  :tabs="tabs"
                 />
 
-                <DefaultInput
-                  v-model="form.quantity_classes"
-                  v-model:error="validationErrors.quantity_classes"
-                  class="col-12"
-                  label="Quantidade de Aulas"
-                  type="number"
-                  :rules="[inputRules.required, inputRules.minValue(1)]"
-                />
-
-                <DefaultInput
-                  v-model="form.class_duration_hours"
-                  v-model:error="validationErrors.class_duration_minutes"
+                <div
+                  v-show="currentTab === 'dados'"
                   class="col-12"
-                  label="Duração da Aula (horas)"
-                  max="24"
-                  min="0.5"
-                  step="0.5"
-                  type="number"
-                  :rules="[
-                    inputRules.required,
-                    inputRules.minValue(0.5),
-                    inputRules.maxValue(24),
-                  ]"
-                />
+                >
+                  <div class="row q-col-gutter-sm">
+                    <DefaultInput
+                      v-model="form.name"
+                      v-model:error="validationErrors.name"
+                      class="col-12"
+                      label="Nome do Pacote"
+                      :rules="[inputRules.required]"
+                    />
 
-                <div class="col-12 package-duration-hint">
-                  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>
+                    <DefaultInput
+                      v-model="form.quantity_classes"
+                      v-model:error="validationErrors.quantity_classes"
+                      class="col-12"
+                      label="Quantidade de Aulas"
+                      type="number"
+                      :rules="[inputRules.required, inputRules.minValue(1)]"
+                    />
 
-                <DefaultCurrencyInput
-                  v-model="form.contract_register_value"
-                  v-model:error="validationErrors.contract_register_value"
-                  class="col-4"
-                  label="R$ Matrícula"
-                />
+                    <DefaultInput
+                      v-model="form.class_duration_hours"
+                      v-model:error="validationErrors.class_duration_minutes"
+                      class="col-12"
+                      label="Duração da Aula (horas)"
+                      max="24"
+                      min="0.5"
+                      step="0.5"
+                      type="number"
+                      :rules="[
+                        inputRules.required,
+                        inputRules.minValue(0.5),
+                        inputRules.maxValue(24),
+                      ]"
+                    />
 
-                <DefaultCurrencyInput
-                  v-model="form.contract_value"
-                  v-model:error="validationErrors.contract_value"
-                  class="col-4"
-                  label="R$ Total do Contrato"
-                />
+                    <div class="col-12 package-duration-hint">
+                      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>
 
-                <DefaultInput
-                  v-model="form.contrat_discount_value"
-                  class="col-4"
-                  label="Desconto em %"
-                  max="100"
-                  min="0"
-                  type="number"
-                />
+                    <DefaultCurrencyInput
+                      v-model="form.contract_register_value"
+                      v-model:error="validationErrors.contract_register_value"
+                      class="col-4"
+                      label="R$ Matrícula"
+                    />
 
-                <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"
+                    <DefaultCurrencyInput
+                      v-model="form.contract_value"
+                      v-model:error="validationErrors.contract_value"
                       class="col-4"
-                      emit-value
-                      label="Material"
-                      map-options
-                      :options="productOptions"
-                      :rules="
-                        material.product_id
-                          ? [inputRules.required]
-                          : []
-                      "
-                      @update:model-value="onProductSelected(material)"
+                      label="R$ Total do Contrato"
                     />
 
                     <DefaultInput
-                      v-model="material.quantity"
+                      v-model="form.contrat_discount_value"
                       class="col-4"
-                      label="Qtd"
-                      min="1"
+                      label="Desconto em %"
+                      max="100"
+                      min="0"
                       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
+                      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>
+
+                <div
+                  v-show="currentTab === 'pavao'"
+                  class="col-12"
+                >
+                  <ProposalPricingTab
+                    v-model="form.pavao"
+                    :product-options="productOptions"
+                  />
+                </div>
+
+                <div
+                  v-show="currentTab === 'irrecusavel'"
+                  class="col-12"
+                >
+                  <ProposalPricingTab
+                    v-model="form.irrecusavel"
+                    :product-options="productOptions"
+                    show-condition
+                  />
+                </div>
               </div>
             </q-card-section>
           </q-scroll-area>
@@ -206,11 +240,13 @@ import { useInputRules } from "src/composables/useInputRules";
 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]);
 
@@ -240,6 +276,37 @@ const productOptions = computed(() =>
   })),
 );
 
+const currentTab = ref("dados");
+
+const tabs = [
+  { label: "Dados", name: "dados" },
+  { label: "Pavão", name: "pavao" },
+  { label: "Irrecusável", name: "irrecusavel" },
+];
+
+const defaultPricingBlock = (withCondition = false) => ({
+  classes_discount_percentage: null,
+  classes_included_in_course: false,
+  classes_installments_allowed: false,
+  classes_max_installments: 13,
+  classes_value: null,
+
+  materials: [{ price: null, product_id: null, quantity: 1 }],
+  materials_included_in_course: false,
+  materials_installments_allowed: false,
+  materials_max_installments: 13,
+
+  registration_included_in_course: false,
+  registration_installments_allowed: false,
+  registration_max_installments: 13,
+  registration_value: null,
+
+  total_max_installments: 13,
+  total_value: null,
+
+  ...(withCondition ? { condition: null } : {}),
+});
+
 const { form, getUpdatedFields } = useForm({
   class_duration_hours:
     (props.package?.class_duration_minutes ?? 120) / 60,
@@ -252,6 +319,8 @@ const { form, getUpdatedFields } = useForm({
   contrat_discount_value:
     props.package?.contrat_discount_value ?? null,
 
+  irrecusavel: defaultPricingBlock(true),
+
   materials: [
     {
       price: null,
@@ -261,6 +330,7 @@ const { form, getUpdatedFields } = useForm({
   ],
 
   name: props.package?.name ?? null,
+  pavao: defaultPricingBlock(),
   quantity_classes: props.package?.quantity_classes ?? null,
 });
 
@@ -285,6 +355,73 @@ const addMaterial = () => {
   });
 };
 
+const mapPricingBlockFromApi = (data, existingMaterials) => ({
+  classes_discount_percentage: data.classes_discount_percentage,
+  classes_included_in_course: data.classes_included_in_course,
+  classes_installments_allowed: data.classes_installments_allowed,
+  classes_max_installments: data.classes_max_installments,
+  classes_value: data.classes_value,
+
+  materials: data.materials?.length
+    ? data.materials.map((material) => ({
+        price: material.price,
+        product_id: material.product_id,
+        quantity: material.quantity,
+      }))
+    : existingMaterials,
+  materials_included_in_course: data.materials_included_in_course,
+  materials_installments_allowed: data.materials_installments_allowed,
+  materials_max_installments: data.materials_max_installments,
+
+  registration_included_in_course: data.registration_included_in_course,
+  registration_installments_allowed: data.registration_installments_allowed,
+  registration_max_installments: data.registration_max_installments,
+  registration_value: data.registration_value,
+
+  total_max_installments: data.total_max_installments,
+  total_value: data.total_value,
+
+  ...(data.condition !== undefined ? { condition: data.condition } : {}),
+});
+
+const numOrNull = (value) =>
+  value === null || value === "" || value === undefined ? null : Number(value);
+
+const toPricingPayload = (block, { withCondition = false } = {}) => {
+  const payload = {
+    classes_discount_percentage: numOrNull(block.classes_discount_percentage),
+    classes_included_in_course: block.classes_included_in_course,
+    classes_installments_allowed: block.classes_installments_allowed,
+    classes_max_installments: Number(block.classes_max_installments) || 13,
+    classes_value: numOrNull(block.classes_value),
+
+    materials: block.materials
+      .filter((material) => material.product_id)
+      .map((material) => ({
+        price: Number(material.price),
+        product_id: material.product_id,
+        quantity: Number(material.quantity),
+      })),
+    materials_included_in_course: block.materials_included_in_course,
+    materials_installments_allowed: block.materials_installments_allowed,
+    materials_max_installments: Number(block.materials_max_installments) || 13,
+
+    registration_included_in_course: block.registration_included_in_course,
+    registration_installments_allowed: block.registration_installments_allowed,
+    registration_max_installments: Number(block.registration_max_installments) || 13,
+    registration_value: numOrNull(block.registration_value),
+
+    total_max_installments: Number(block.total_max_installments) || 13,
+    total_value: numOrNull(block.total_value),
+  };
+
+  if (withCondition) {
+    payload.condition = block.condition ?? null;
+  }
+
+  return payload;
+};
+
 const onOKClick = async () => {
   const validMaterials = form.materials.filter(
     (material) => material.product_id,
@@ -303,6 +440,8 @@ const onOKClick = async () => {
     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,
@@ -310,6 +449,7 @@ const onOKClick = async () => {
     })),
 
     name: form.name,
+    pavao: toPricingPayload(form.pavao),
     quantity_classes: form.quantity_classes,
   };
 
@@ -344,6 +484,11 @@ const onOKClick = async () => {
       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.
+    updatePayload.pavao = payload.pavao;
+    updatePayload.irrecusavel = payload.irrecusavel;
+
     return updateUnitPackage(
       props.package.id,
       updatePayload,
@@ -388,6 +533,14 @@ onMounted(async () => {
       }),
     );
   }
+
+  if (packageData.pavao) {
+    form.pavao = mapPricingBlockFromApi(packageData.pavao, form.pavao.materials);
+  }
+
+  if (packageData.irrecusavel) {
+    form.irrecusavel = mapPricingBlockFromApi(packageData.irrecusavel, form.irrecusavel.materials);
+  }
 });
 </script>
 

+ 44 - 0
src/pages/packages/components/ExclusiveCheckboxPair.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="row q-col-gutter-md">
+    <div class="col-6">
+      <q-checkbox
+        color="primary"
+        label="Incluso no valor do curso"
+        :model-value="included"
+        @update:model-value="onToggle('included', $event)"
+      />
+    </div>
+
+    <div class="col-6">
+      <q-checkbox
+        color="primary"
+        label="Permite Parcelar"
+        :model-value="installments"
+        @update:model-value="onToggle('installments', $event)"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  included: {
+    default: false,
+    type: Boolean,
+  },
+  installments: {
+    default: false,
+    type: Boolean,
+  },
+});
+
+const emit = defineEmits(["update:included", "update:installments"]);
+
+// Comportam-se como um par exclusivo (um dos dois é sempre obrigatório):
+// marcar um desmarca o outro; desmarcar direto é ignorado.
+const onToggle = (which, value) => {
+  if (!value) return;
+
+  emit(which === "included" ? "update:included" : "update:installments", true);
+};
+</script>

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

@@ -0,0 +1,338 @@
+<template>
+  <div class="row q-col-gutter-md">
+    <!-- Matrícula -->
+    <DefaultCurrencyInput
+      v-model="modelValue.registration_value"
+      class="col-12"
+      label="Valor da Matrícula"
+    />
+
+    <ExclusiveCheckboxPair
+      class="col-12"
+      :included="modelValue.registration_included_in_course"
+      :installments="modelValue.registration_installments_allowed"
+      @update:included="setExclusive('registration', 'included')"
+      @update:installments="setExclusive('registration', 'installments')"
+    />
+
+    <template v-if="modelValue.registration_installments_allowed">
+      <DefaultInput
+        v-model="modelValue.registration_max_installments"
+        class="col-6"
+        label="Quantidade de Parcelas Máximas"
+        min="1"
+        type="number"
+      />
+
+      <div class="col-6 installment-hint">
+        Valor da Parcela: {{ formatInstallment(registrationInstallmentValue) }}
+      </div>
+    </template>
+
+    <q-separator class="col-12" />
+
+    <!-- Aulas -->
+    <DefaultCurrencyInput
+      v-model="modelValue.classes_value"
+      class="col-12"
+      label="Valor de Aulas"
+    />
+
+    <ExclusiveCheckboxPair
+      class="col-12"
+      :included="modelValue.classes_included_in_course"
+      :installments="modelValue.classes_installments_allowed"
+      @update:included="setExclusive('classes', 'included')"
+      @update:installments="setExclusive('classes', 'installments')"
+    />
+
+    <template v-if="modelValue.classes_installments_allowed">
+      <DefaultInput
+        v-model="modelValue.classes_max_installments"
+        class="col-4"
+        label="Quantidade de Parcelas Máximas"
+        min="1"
+        type="number"
+      />
+
+      <div class="col-4 installment-hint">
+        Valor da Parcela: {{ formatInstallment(classesInstallmentValue) }}
+      </div>
+
+      <DefaultInput
+        v-model="modelValue.classes_discount_percentage"
+        class="col-4"
+        label="Desconto (%)"
+        max="100"
+        min="0"
+        type="number"
+      />
+    </template>
+
+    <q-separator class="col-12" />
+
+    <!-- Materiais -->
+    <div class="col-12 text-weight-medium">Materiais</div>
+
+    <div
+      v-for="(material, index) in modelValue.materials"
+      :key="index"
+      class="col-12"
+    >
+      <div class="row q-col-gutter-sm items-center">
+        <DefaultSelect
+          v-model="material.product_id"
+          class="col"
+          emit-value
+          label="Material"
+          map-options
+          :options="productOptions"
+          @update:model-value="onProductSelected(material)"
+        />
+
+        <DefaultInput
+          v-model="material.quantity"
+          class="col-2"
+          label="Qtd"
+          min="1"
+          type="number"
+        />
+
+        <DefaultCurrencyInput
+          v-model="material.price"
+          class="col-3"
+          label="R$ Unitário"
+        />
+
+        <div class="col-auto">
+          <q-btn
+            v-if="index === modelValue.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>
+
+    <ExclusiveCheckboxPair
+      class="col-12"
+      :included="modelValue.materials_included_in_course"
+      :installments="modelValue.materials_installments_allowed"
+      @update:included="setExclusive('materials', 'included')"
+      @update:installments="setExclusive('materials', 'installments')"
+    />
+
+    <template v-if="modelValue.materials_installments_allowed">
+      <DefaultInput
+        v-model="modelValue.materials_max_installments"
+        class="col-6"
+        label="Quantidade de Parcelas Máximas"
+        min="1"
+        type="number"
+      />
+
+      <div class="col-6 installment-hint">
+        Valor da Parcela: {{ formatInstallment(materialsInstallmentValue) }}
+      </div>
+    </template>
+
+    <q-separator class="col-12" />
+
+    <!-- Valor Total do Curso -->
+    <div class="col-12 text-weight-medium">Valor Total do Curso</div>
+
+    <DefaultCurrencyInput
+      v-model="modelValue.total_value"
+      class="col-12"
+      label="Valor Total do Curso"
+    />
+
+    <DefaultInput
+      v-model="modelValue.total_max_installments"
+      class="col-6"
+      label="Quantidade de Parcelas Máximas"
+      min="1"
+      type="number"
+      :rules="[inputRules.required, inputRules.minValue(1)]"
+    />
+
+    <div class="col-6 installment-hint">
+      Valor da Parcela: {{ formatInstallment(totalInstallmentValue) }}
+    </div>
+
+    <!-- Condição (só Irrecusável) -->
+    <template v-if="showCondition">
+      <q-separator class="col-12" />
+
+      <DefaultInput
+        v-model="modelValue.condition"
+        autogrow
+        class="col-12"
+        label="Condição"
+        type="textarea"
+      />
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { computed, watch } from "vue";
+
+import { formatToBRLCurrency } from "src/helpers/utils";
+import { useInputRules } from "src/composables/useInputRules";
+
+import ExclusiveCheckboxPair from "./ExclusiveCheckboxPair.vue";
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+const { inputRules } = useInputRules();
+
+const modelValue = defineModel({ type: Object, required: true });
+
+const props = defineProps({
+  productOptions: {
+    default: () => [],
+    type: Array,
+  },
+  showCondition: {
+    default: false,
+    type: Boolean,
+  },
+});
+
+const materialsTotalValue = computed(() =>
+  modelValue.value.materials.reduce(
+    (sum, material) =>
+      sum + (Number(material.quantity) || 0) * (Number(material.price) || 0),
+    0,
+  ),
+);
+
+const registrationInstallmentValue = computed(() =>
+  installmentValue(
+    modelValue.value.registration_value,
+    modelValue.value.registration_max_installments,
+  ),
+);
+
+const classesInstallmentValue = computed(() =>
+  installmentValue(
+    modelValue.value.classes_value,
+    modelValue.value.classes_max_installments,
+  ),
+);
+
+const materialsInstallmentValue = computed(() =>
+  installmentValue(
+    materialsTotalValue.value,
+    modelValue.value.materials_max_installments,
+  ),
+);
+
+const totalInstallmentValue = computed(() =>
+  installmentValue(
+    modelValue.value.total_value,
+    modelValue.value.total_max_installments,
+  ),
+);
+
+// Soma sugerida do Valor Total, considerando só as seções marcadas como
+// "incluso no valor do curso". O campo continua editável: só reaplicamos a
+// sugestão enquanto o usuário não tiver digitado um valor diferente dela.
+let lastSuggestedTotal = null;
+
+const suggestedTotal = computed(() => {
+  let total = 0;
+
+  if (modelValue.value.registration_included_in_course) {
+    total += Number(modelValue.value.registration_value) || 0;
+  }
+
+  if (modelValue.value.classes_included_in_course) {
+    total += Number(modelValue.value.classes_value) || 0;
+  }
+
+  if (modelValue.value.materials_included_in_course) {
+    total += materialsTotalValue.value;
+  }
+
+  return total;
+});
+
+watch(
+  suggestedTotal,
+  (newTotal) => {
+    if (
+      modelValue.value.total_value === null ||
+      modelValue.value.total_value === lastSuggestedTotal
+    ) {
+      modelValue.value.total_value = newTotal;
+    }
+
+    lastSuggestedTotal = newTotal;
+  },
+  { immediate: true },
+);
+
+const setExclusive = (section, option) => {
+  const includedKey = `${section}_included_in_course`;
+  const installmentsKey = `${section}_installments_allowed`;
+
+  modelValue.value[includedKey] = option === "included";
+  modelValue.value[installmentsKey] = option === "installments";
+};
+
+const addMaterial = () => {
+  modelValue.value.materials.push({
+    price: null,
+    product_id: null,
+    quantity: 1,
+  });
+};
+
+const removeMaterial = (index) => {
+  modelValue.value.materials.splice(index, 1);
+};
+
+const onProductSelected = (material) => {
+  const option = props.productOptions.find(
+    (item) => item.value === material.product_id,
+  );
+
+  if (option) {
+    material.price = option.price_sale;
+  }
+};
+
+const formatInstallment = (value) =>
+  value === null ? "—" : formatToBRLCurrency(value);
+
+function installmentValue(value, maxInstallments) {
+  if (!value || !maxInstallments || maxInstallments <= 0) return null;
+
+  return Number(value) / Number(maxInstallments);
+}
+</script>
+
+<style scoped>
+.installment-hint {
+  display: flex;
+  align-items: center;
+  color: #757575;
+  font-size: 13px;
+}
+</style>

+ 1 - 1
src/pages/permissions/permission_menus.js

@@ -18,7 +18,7 @@ export const FRANCHISEE_PERMISSION_MENUS = Object.freeze([
   { scope: "franchisee_contracts", label: "Contratos", icon: "mdi-file-document-outline" },
   { scope: "franchisee_classes", label: "Agenda / Aulas", icon: "mdi-calendar" },
   { scope: "franchisee_financial", label: "Financeiro", icon: "mdi-finance" },
-  { scope: "franchisee_packages", label: "Pacotes", icon: "mdi-package-variant-closed" },
+  { scope: "franchisee_packages", label: "Proposta Comercial", icon: "mdi-package-variant-closed" },
   { scope: "franchisee_users", label: "Usuários", icon: "mdi-account-multiple-outline" },
   { scope: "franchisee_permissions", label: "Permissões", icon: "mdi-shield-account-outline" },
   { scope: "franchisee_unit", label: "Unidade", icon: "mdi-cog-outline" },

+ 2 - 2
src/router/routes/package.route.js

@@ -4,13 +4,13 @@ export default [
     name: "PackagesPage",
     component: () => import("pages/packages/PackagesPage.vue"),
     meta: {
-      title: "Pacotes",
+      title: "Proposta Comercial",
       requireAuth: true,
       requiredPermission: "franchisee_packages",
       breadcrumbs: [
         {
           name: "PackagesPage",
-          title: "Pacotes",
+          title: "Proposta Comercial",
         },
       ],
     },

+ 1 - 1
src/stores/navigation.js

@@ -115,7 +115,7 @@ export const navigationStore = defineStore("navigation", () => {
     },
     {
       type: "single",
-      title: "Pacotes",
+      title: "Proposta Comercial",
       name: "PackagesPage",
       icon: "mdi-package-variant-closed",
       disable: false,