ProposalPricingTab.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <template>
  2. <div class="row q-col-gutter-md">
  3. <!-- Matrícula -->
  4. <DefaultCurrencyInput
  5. v-model="modelValue.registration_value"
  6. class="col-12"
  7. label="Valor da Matrícula"
  8. />
  9. <ExclusiveCheckboxPair
  10. class="col-12"
  11. :included="modelValue.registration_included_in_course"
  12. :installments="modelValue.registration_installments_allowed"
  13. @update:included="setExclusive('registration', 'included')"
  14. @update:installments="setExclusive('registration', 'installments')"
  15. />
  16. <template v-if="modelValue.registration_installments_allowed">
  17. <DefaultInput
  18. v-model="modelValue.registration_max_installments"
  19. class="col-6"
  20. label="Quantidade de Parcelas Máximas"
  21. min="1"
  22. type="number"
  23. />
  24. <div class="col-6 installment-hint">
  25. Valor da Parcela: {{ formatInstallment(registrationInstallmentValue) }}
  26. </div>
  27. </template>
  28. <q-separator class="col-12" />
  29. <!-- Aulas -->
  30. <DefaultCurrencyInput
  31. v-model="modelValue.classes_value"
  32. class="col-12"
  33. label="Valor de Aulas"
  34. />
  35. <ExclusiveCheckboxPair
  36. class="col-12"
  37. :included="modelValue.classes_included_in_course"
  38. :installments="modelValue.classes_installments_allowed"
  39. @update:included="setExclusive('classes', 'included')"
  40. @update:installments="setExclusive('classes', 'installments')"
  41. />
  42. <template v-if="modelValue.classes_installments_allowed">
  43. <DefaultInput
  44. v-model="modelValue.classes_max_installments"
  45. class="col-4"
  46. label="Quantidade de Parcelas Máximas"
  47. min="1"
  48. type="number"
  49. />
  50. <div class="col-4 installment-hint">
  51. Valor da Parcela: {{ formatInstallment(classesInstallmentValue) }}
  52. </div>
  53. <DefaultInput
  54. v-model="modelValue.classes_discount_percentage"
  55. class="col-4"
  56. label="Desconto (%)"
  57. max="100"
  58. min="0"
  59. type="number"
  60. />
  61. </template>
  62. <q-separator class="col-12" />
  63. <!-- Materiais -->
  64. <div class="col-12 text-weight-medium">Materiais</div>
  65. <div
  66. v-for="(material, index) in modelValue.materials"
  67. :key="index"
  68. class="col-12"
  69. >
  70. <div class="row q-col-gutter-sm items-center">
  71. <DefaultSelect
  72. v-model="material.product_id"
  73. class="col"
  74. emit-value
  75. label="Material"
  76. map-options
  77. :options="productOptions"
  78. @update:model-value="onProductSelected(material)"
  79. />
  80. <DefaultInput
  81. v-model="material.quantity"
  82. class="col-2"
  83. label="Qtd"
  84. min="1"
  85. type="number"
  86. />
  87. <DefaultCurrencyInput
  88. v-model="material.price"
  89. class="col-3"
  90. label="R$ Unitário"
  91. />
  92. <div class="col-auto">
  93. <q-btn
  94. v-if="index === modelValue.materials.length - 1"
  95. color="primary"
  96. icon="mdi-plus"
  97. style="border-radius: 8px; height: 40px; width: 40px"
  98. unelevated
  99. @click="addMaterial"
  100. />
  101. <q-btn
  102. v-else
  103. color="negative"
  104. dense
  105. flat
  106. icon="mdi-delete-outline"
  107. round
  108. @click="removeMaterial(index)"
  109. />
  110. </div>
  111. </div>
  112. </div>
  113. <ExclusiveCheckboxPair
  114. class="col-12"
  115. :included="modelValue.materials_included_in_course"
  116. :installments="modelValue.materials_installments_allowed"
  117. @update:included="setExclusive('materials', 'included')"
  118. @update:installments="setExclusive('materials', 'installments')"
  119. />
  120. <template v-if="modelValue.materials_installments_allowed">
  121. <DefaultInput
  122. v-model="modelValue.materials_max_installments"
  123. class="col-6"
  124. label="Quantidade de Parcelas Máximas"
  125. min="1"
  126. type="number"
  127. />
  128. <div class="col-6 installment-hint">
  129. Valor da Parcela: {{ formatInstallment(materialsInstallmentValue) }}
  130. </div>
  131. </template>
  132. <q-separator class="col-12" />
  133. <!-- Valor Total do Curso -->
  134. <div class="col-12 text-weight-medium">Valor Total do Curso</div>
  135. <DefaultCurrencyInput
  136. v-model="modelValue.total_value"
  137. class="col-12"
  138. label="Valor Total do Curso"
  139. />
  140. <DefaultInput
  141. v-model="modelValue.total_max_installments"
  142. class="col-6"
  143. label="Quantidade de Parcelas Máximas"
  144. min="1"
  145. type="number"
  146. :rules="[inputRules.required, inputRules.minValue(1)]"
  147. />
  148. <div class="col-6 installment-hint">
  149. Valor da Parcela: {{ formatInstallment(totalInstallmentValue) }}
  150. </div>
  151. <!-- Condição (só Irrecusável) -->
  152. <template v-if="showCondition">
  153. <q-separator class="col-12" />
  154. <DefaultInput
  155. v-model="modelValue.condition"
  156. autogrow
  157. class="col-12"
  158. label="Condição"
  159. type="textarea"
  160. />
  161. </template>
  162. </div>
  163. </template>
  164. <script setup>
  165. import { computed, watch } from "vue";
  166. import { formatToBRLCurrency } from "src/helpers/utils";
  167. import { useInputRules } from "src/composables/useInputRules";
  168. import ExclusiveCheckboxPair from "./ExclusiveCheckboxPair.vue";
  169. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  170. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  171. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  172. const { inputRules } = useInputRules();
  173. const modelValue = defineModel({ type: Object, required: true });
  174. const props = defineProps({
  175. productOptions: {
  176. default: () => [],
  177. type: Array,
  178. },
  179. showCondition: {
  180. default: false,
  181. type: Boolean,
  182. },
  183. });
  184. const materialsTotalValue = computed(() =>
  185. modelValue.value.materials.reduce(
  186. (sum, material) =>
  187. sum + (Number(material.quantity) || 0) * (Number(material.price) || 0),
  188. 0,
  189. ),
  190. );
  191. const registrationInstallmentValue = computed(() =>
  192. installmentValue(
  193. modelValue.value.registration_value,
  194. modelValue.value.registration_max_installments,
  195. ),
  196. );
  197. const classesInstallmentValue = computed(() =>
  198. installmentValue(
  199. modelValue.value.classes_value,
  200. modelValue.value.classes_max_installments,
  201. ),
  202. );
  203. const materialsInstallmentValue = computed(() =>
  204. installmentValue(
  205. materialsTotalValue.value,
  206. modelValue.value.materials_max_installments,
  207. ),
  208. );
  209. const totalInstallmentValue = computed(() =>
  210. installmentValue(
  211. modelValue.value.total_value,
  212. modelValue.value.total_max_installments,
  213. ),
  214. );
  215. // Soma sugerida do Valor Total, considerando só as seções marcadas como
  216. // "incluso no valor do curso". O campo continua editável: só reaplicamos a
  217. // sugestão enquanto o usuário não tiver digitado um valor diferente dela.
  218. let lastSuggestedTotal = null;
  219. const suggestedTotal = computed(() => {
  220. let total = 0;
  221. if (modelValue.value.registration_included_in_course) {
  222. total += Number(modelValue.value.registration_value) || 0;
  223. }
  224. if (modelValue.value.classes_included_in_course) {
  225. total += Number(modelValue.value.classes_value) || 0;
  226. }
  227. if (modelValue.value.materials_included_in_course) {
  228. total += materialsTotalValue.value;
  229. }
  230. return total;
  231. });
  232. watch(
  233. suggestedTotal,
  234. (newTotal) => {
  235. if (
  236. modelValue.value.total_value === null ||
  237. modelValue.value.total_value === lastSuggestedTotal
  238. ) {
  239. modelValue.value.total_value = newTotal;
  240. }
  241. lastSuggestedTotal = newTotal;
  242. },
  243. { immediate: true },
  244. );
  245. const setExclusive = (section, option) => {
  246. const includedKey = `${section}_included_in_course`;
  247. const installmentsKey = `${section}_installments_allowed`;
  248. modelValue.value[includedKey] = option === "included";
  249. modelValue.value[installmentsKey] = option === "installments";
  250. };
  251. const addMaterial = () => {
  252. modelValue.value.materials.push({
  253. price: null,
  254. product_id: null,
  255. quantity: 1,
  256. });
  257. };
  258. const removeMaterial = (index) => {
  259. modelValue.value.materials.splice(index, 1);
  260. };
  261. const onProductSelected = (material) => {
  262. const option = props.productOptions.find(
  263. (item) => item.value === material.product_id,
  264. );
  265. if (option) {
  266. material.price = option.price_sale;
  267. }
  268. };
  269. const formatInstallment = (value) =>
  270. value === null ? "—" : formatToBRLCurrency(value);
  271. function installmentValue(value, maxInstallments) {
  272. if (!value || !maxInstallments || maxInstallments <= 0) return null;
  273. return Number(value) / Number(maxInstallments);
  274. }
  275. </script>
  276. <style scoped>
  277. .installment-hint {
  278. display: flex;
  279. align-items: center;
  280. color: #757575;
  281. font-size: 13px;
  282. }
  283. </style>