AddEditPackageDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <div style="width: 100%; max-width: 700px">
  4. <q-card
  5. class="dialog-form-card"
  6. style="width: 100%"
  7. >
  8. <DefaultDialogHeader
  9. :title="props.package ? 'Editar Pacote' : 'Novo Pacote'"
  10. @close="onDialogCancel"
  11. />
  12. <DefaultForm
  13. ref="formRef"
  14. @submit="onOKClick"
  15. >
  16. <q-scroll-area
  17. ref="scrollAreaRef"
  18. class="dialog-form-scroll"
  19. >
  20. <q-card-section class="q-pt-sm">
  21. <div class="row q-col-gutter-sm">
  22. <DefaultInput
  23. v-model="form.name"
  24. v-model:error="validationErrors.name"
  25. class="col-12"
  26. label="Nome do Pacote"
  27. :rules="[inputRules.required]"
  28. />
  29. <DefaultInput
  30. v-model="form.quantity_classes"
  31. v-model:error="validationErrors.quantity_classes"
  32. class="col-12"
  33. label="Quantidade de Aulas"
  34. type="number"
  35. :rules="[inputRules.required, inputRules.minValue(1)]"
  36. />
  37. <DefaultInput
  38. v-model="form.class_duration_hours"
  39. v-model:error="validationErrors.class_duration_minutes"
  40. class="col-12"
  41. label="Duração da Aula (horas)"
  42. max="24"
  43. min="0.5"
  44. step="0.5"
  45. type="number"
  46. :rules="[
  47. inputRules.required,
  48. inputRules.minValue(0.5),
  49. inputRules.maxValue(24),
  50. ]"
  51. />
  52. <div class="col-12 package-duration-hint">
  53. A duração padrão das aulas é de 2 horas e pode ser alterada se
  54. necessário. É possível configurar no máximo dois dias.
  55. </div>
  56. <DefaultSelect
  57. v-model="form.weekday"
  58. class="col-4"
  59. clearable
  60. emit-value
  61. label="1º Dia da Semana"
  62. map-options
  63. option-label="label"
  64. option-value="value"
  65. :options="weekdays"
  66. :rules="form.start_time ? [inputRules.required] : []"
  67. />
  68. <DefaultInput
  69. v-model="form.start_time"
  70. class="col-4"
  71. label="Horário Inicial"
  72. mask="##:##"
  73. :rules="
  74. form.weekday !== null
  75. ? [inputRules.required]
  76. : []
  77. "
  78. />
  79. <DefaultInput
  80. :model-value="firstEndTime"
  81. class="col-4"
  82. disable
  83. label="Horário Final"
  84. />
  85. <DefaultSelect
  86. v-model="form.second_weekday"
  87. class="col-4"
  88. clearable
  89. emit-value
  90. label="2º Dia da Semana"
  91. map-options
  92. option-label="label"
  93. option-value="value"
  94. :options="secondWeekdayOptions"
  95. :rules="
  96. form.second_start_time
  97. ? [inputRules.required]
  98. : []
  99. "
  100. />
  101. <DefaultInput
  102. v-model="form.second_start_time"
  103. class="col-4"
  104. label="Horário Inicial do 2º dia"
  105. mask="##:##"
  106. :rules="
  107. form.second_weekday !== null
  108. ? [inputRules.required]
  109. : []
  110. "
  111. />
  112. <DefaultInput
  113. :model-value="secondEndTime"
  114. class="col-4"
  115. disable
  116. label="Horário Final do 2º dia"
  117. />
  118. <DefaultCurrencyInput
  119. v-model="form.contract_register_value"
  120. v-model:error="validationErrors.contract_register_value"
  121. class="col-4"
  122. label="R$ Matrícula"
  123. :rules="[inputRules.required]"
  124. />
  125. <DefaultCurrencyInput
  126. v-model="form.contract_value"
  127. v-model:error="validationErrors.contract_value"
  128. class="col-4"
  129. label="R$ Total do Contrato"
  130. :rules="[inputRules.required]"
  131. />
  132. <DefaultInput
  133. v-model="form.contrat_discount_value"
  134. class="col-4"
  135. label="Desconto em %"
  136. max="100"
  137. min="0"
  138. type="number"
  139. />
  140. <div
  141. v-for="(material, index) in form.materials"
  142. :key="index"
  143. class="col-12"
  144. >
  145. <div class="row q-col-gutter-sm items-center">
  146. <DefaultSelect
  147. v-model="material.product_id"
  148. class="col-4"
  149. emit-value
  150. label="Material"
  151. map-options
  152. :options="productOptions"
  153. :rules="
  154. material.product_id
  155. ? [inputRules.required]
  156. : []
  157. "
  158. @update:model-value="onProductSelected(material)"
  159. />
  160. <DefaultInput
  161. v-model="material.quantity"
  162. class="col-4"
  163. label="Qtd"
  164. min="1"
  165. type="number"
  166. :rules="
  167. material.product_id
  168. ? [
  169. inputRules.required,
  170. inputRules.minValue(1),
  171. ]
  172. : []
  173. "
  174. />
  175. <DefaultCurrencyInput
  176. v-model="material.price"
  177. class="col"
  178. label="R$ Unitário"
  179. :rules="
  180. material.product_id
  181. ? [inputRules.required]
  182. : []
  183. "
  184. />
  185. <div class="col-auto">
  186. <q-btn
  187. v-if="index === form.materials.length - 1"
  188. color="primary"
  189. icon="mdi-plus"
  190. style="
  191. border-radius: 8px;
  192. height: 40px;
  193. width: 40px;
  194. "
  195. unelevated
  196. @click="addMaterial"
  197. />
  198. <q-btn
  199. v-else
  200. color="negative"
  201. dense
  202. flat
  203. icon="mdi-delete-outline"
  204. round
  205. @click="removeMaterial(index)"
  206. />
  207. </div>
  208. </div>
  209. </div>
  210. </div>
  211. </q-card-section>
  212. </q-scroll-area>
  213. <q-card-actions
  214. align="right"
  215. class="q-px-md q-pb-md"
  216. >
  217. <q-btn
  218. color="primary"
  219. label="Cancelar"
  220. no-caps
  221. outline
  222. @click="onDialogCancel"
  223. />
  224. <q-btn
  225. color="primary"
  226. label="Salvar"
  227. no-caps
  228. type="submit"
  229. :loading="loading"
  230. />
  231. </q-card-actions>
  232. </DefaultForm>
  233. </q-card>
  234. </div>
  235. </q-dialog>
  236. </template>
  237. <script setup>
  238. import {
  239. computed,
  240. onMounted,
  241. ref,
  242. useTemplateRef,
  243. watch,
  244. } from "vue";
  245. import {
  246. createUnitPackage,
  247. getUnitPackage,
  248. updateUnitPackage,
  249. } from "src/api/package";
  250. import { getProductsForSelect } from "src/api/product";
  251. import { useDialogPluginComponent } from "quasar";
  252. import { useForm } from "src/composables/useForm";
  253. import { useInputRules } from "src/composables/useInputRules";
  254. import { useScroll } from "src/composables/useScroll";
  255. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  256. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  257. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  258. import DefaultForm from "src/components/defaults/DefaultForm.vue";
  259. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  260. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  261. defineEmits([...useDialogPluginComponent.emits]);
  262. const props = defineProps({
  263. package: {
  264. type: Object,
  265. default: null,
  266. },
  267. });
  268. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  269. useDialogPluginComponent();
  270. const { inputRules } = useInputRules();
  271. const { scrollToComponent } = useScroll();
  272. const formRef = useTemplateRef("formRef");
  273. const scrollAreaRef = useTemplateRef("scrollAreaRef");
  274. const products = ref([]);
  275. const firstEndTime = computed(() =>
  276. calculateEndTime(
  277. form.start_time,
  278. form.class_duration_hours,
  279. ),
  280. );
  281. const productOptions = computed(() =>
  282. products.value.map((product) => ({
  283. label: product.name,
  284. price_sale: product.price_sale,
  285. value: product.id,
  286. })),
  287. );
  288. const secondEndTime = computed(() =>
  289. calculateEndTime(
  290. form.second_start_time,
  291. form.class_duration_hours,
  292. ),
  293. );
  294. const secondWeekdayOptions = computed(() =>
  295. weekdays.filter(
  296. (weekday) => weekday.value !== form.weekday,
  297. ),
  298. );
  299. const weekdays = [
  300. {
  301. label: "Segunda",
  302. value: 1,
  303. },
  304. {
  305. label: "Terça",
  306. value: 2,
  307. },
  308. {
  309. label: "Quarta",
  310. value: 3,
  311. },
  312. {
  313. label: "Quinta",
  314. value: 4,
  315. },
  316. {
  317. label: "Sexta",
  318. value: 5,
  319. },
  320. {
  321. label: "Sábado",
  322. value: 6,
  323. },
  324. {
  325. label: "Domingo",
  326. value: 0,
  327. },
  328. ];
  329. const trimTime = (time) =>
  330. time ? time.slice(0, 5) : null;
  331. const { form, getUpdatedFields } = useForm({
  332. class_duration_hours:
  333. (props.package?.class_duration_minutes ?? 120) / 60,
  334. contract_register_value:
  335. props.package?.contract_register_value ?? null,
  336. contract_value: props.package?.contract_value ?? null,
  337. contrat_discount_value:
  338. props.package?.contrat_discount_value ?? null,
  339. materials: [
  340. {
  341. price: null,
  342. product_id: null,
  343. quantity: 1,
  344. },
  345. ],
  346. name: props.package?.name ?? null,
  347. quantity_classes: props.package?.quantity_classes ?? null,
  348. second_start_time: trimTime(
  349. props.package?.second_start_time,
  350. ),
  351. second_weekday: props.package?.second_weekday ?? null,
  352. start_time: trimTime(props.package?.start_time),
  353. weekday: props.package?.weekday ?? null,
  354. });
  355. const {
  356. loading,
  357. validationErrors,
  358. execute,
  359. } = useSubmitHandler({
  360. containerRef: scrollAreaRef,
  361. formRef,
  362. onSuccess: (result) => {
  363. onDialogOK(result);
  364. },
  365. scrollFn: scrollToComponent,
  366. });
  367. const addMaterial = () => {
  368. form.materials.push({
  369. price: null,
  370. product_id: null,
  371. quantity: 1,
  372. });
  373. };
  374. const calculateEndTime = (startTime, durationHours) => {
  375. if (!/^\d{2}:\d{2}$/.test(startTime ?? "")) {
  376. return null;
  377. }
  378. const [hours, minutes] = startTime
  379. .split(":")
  380. .map(Number);
  381. if (hours > 23 || minutes > 59) {
  382. return null;
  383. }
  384. const durationMinutes = Math.round(
  385. Number(durationHours) * 60,
  386. );
  387. const endMinutes =
  388. (hours * 60 + minutes + durationMinutes) %
  389. (24 * 60);
  390. return `${String(
  391. Math.floor(endMinutes / 60),
  392. ).padStart(2, "0")}:${String(
  393. endMinutes % 60,
  394. ).padStart(2, "0")}`;
  395. };
  396. const onOKClick = async () => {
  397. const validMaterials = form.materials.filter(
  398. (material) => material.product_id,
  399. );
  400. const payload = {
  401. class_duration_minutes: Math.round(
  402. Number(form.class_duration_hours) * 60,
  403. ),
  404. contract_register_value:
  405. form.contract_register_value,
  406. contract_value: form.contract_value,
  407. contrat_discount_value:
  408. form.contrat_discount_value,
  409. materials: validMaterials.map((material) => ({
  410. price: Number(material.price),
  411. product_id: material.product_id,
  412. quantity: Number(material.quantity),
  413. })),
  414. name: form.name,
  415. quantity_classes: form.quantity_classes,
  416. second_start_time:
  417. form.second_start_time || null,
  418. second_weekday: form.second_weekday,
  419. start_time: form.start_time || null,
  420. weekday: form.weekday,
  421. };
  422. await execute(() => {
  423. if (!props.package?.id) {
  424. return createUnitPackage(payload);
  425. }
  426. const changedFields = getUpdatedFields.value;
  427. const updatePayload = {};
  428. const directKeys = [
  429. "name",
  430. "quantity_classes",
  431. "contract_value",
  432. "contract_register_value",
  433. "contrat_discount_value",
  434. "weekday",
  435. "start_time",
  436. "second_weekday",
  437. "second_start_time",
  438. ];
  439. for (const key of directKeys) {
  440. if (key in changedFields) {
  441. updatePayload[key] = payload[key];
  442. }
  443. }
  444. if ("class_duration_hours" in changedFields) {
  445. updatePayload.class_duration_minutes =
  446. payload.class_duration_minutes;
  447. }
  448. if ("materials" in changedFields) {
  449. updatePayload.materials = payload.materials;
  450. }
  451. return updateUnitPackage(
  452. props.package.id,
  453. updatePayload,
  454. );
  455. });
  456. };
  457. const onProductSelected = (material) => {
  458. const option = productOptions.value.find(
  459. (product) => product.value === material.product_id,
  460. );
  461. if (option) {
  462. material.price = option.price_sale;
  463. }
  464. };
  465. const removeMaterial = (index) => {
  466. form.materials.splice(index, 1);
  467. };
  468. watch(
  469. () => form.weekday,
  470. (weekday) => {
  471. if (form.second_weekday === weekday) {
  472. form.second_weekday = null;
  473. }
  474. },
  475. );
  476. onMounted(async () => {
  477. const productsResponse = await getProductsForSelect();
  478. products.value = productsResponse;
  479. if (!props.package?.id) return;
  480. const packageData = await getUnitPackage(
  481. props.package.id,
  482. );
  483. form.class_duration_hours =
  484. (packageData.class_duration_minutes ?? 120) / 60;
  485. form.second_start_time = trimTime(
  486. packageData.second_start_time,
  487. );
  488. form.second_weekday =
  489. packageData.second_weekday ?? null;
  490. form.start_time = trimTime(packageData.start_time);
  491. form.weekday = packageData.weekday ?? null;
  492. if (packageData.materials?.length) {
  493. form.materials = packageData.materials.map(
  494. (material) => ({
  495. price: material.price,
  496. product_id: material.product_id,
  497. quantity: material.quantity,
  498. }),
  499. );
  500. }
  501. });
  502. </script>
  503. <style scoped>
  504. .package-duration-hint {
  505. padding-top: 4px;
  506. padding-bottom: 2px;
  507. color: #757575;
  508. font-size: 12px;
  509. line-height: 1.35;
  510. }
  511. </style>