AddEditPackageDialog.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <div style="width: 100%; max-width: 700px">
  4. <q-card class="overflow-hidden" style="width: 100%">
  5. <DefaultDialogHeader
  6. :title="props.package ? 'Editar Pacote' : 'Novo Pacote'"
  7. @close="onDialogCancel"
  8. />
  9. <DefaultForm ref="formRef" @submit="onOKClick">
  10. <q-card-section class="q-pt-sm">
  11. <div class="row q-col-gutter-sm">
  12. <DefaultInput
  13. v-model="form.name"
  14. label="Nome do Pacote"
  15. class="col-12"
  16. />
  17. <DefaultInput
  18. v-model="form.quantity_classes"
  19. label="Quantidade de Aulas"
  20. class="col-12"
  21. type="number"
  22. />
  23. <DefaultInput
  24. v-model="form.class_duration_hours"
  25. label="Duração da Aula (horas)"
  26. class="col-12"
  27. type="number"
  28. min="0.5"
  29. max="24"
  30. step="0.5"
  31. />
  32. <div class="col-12 package-duration-hint">
  33. A duração padrão das aulas é de 2 horas e pode ser alterada se
  34. necessário. É possível configurar no máximo dois dias.
  35. </div>
  36. <DefaultSelect
  37. v-model="form.weekday"
  38. label="1º Dia da Semana"
  39. class="col-4"
  40. :options="weekdays"
  41. option-value="value"
  42. option-label="label"
  43. emit-value
  44. map-options
  45. clearable
  46. />
  47. <DefaultInput
  48. v-model="form.start_time"
  49. label="Horário Inicial"
  50. class="col-4"
  51. mask="##:##"
  52. />
  53. <DefaultInput
  54. :model-value="firstEndTime"
  55. label="Horário Final"
  56. class="col-4"
  57. disable
  58. />
  59. <DefaultSelect
  60. v-model="form.second_weekday"
  61. label="2º Dia da Semana"
  62. class="col-4"
  63. :options="secondWeekdayOptions"
  64. option-value="value"
  65. option-label="label"
  66. emit-value
  67. map-options
  68. clearable
  69. />
  70. <DefaultInput
  71. v-model="form.second_start_time"
  72. label="Horário Inicial do 2º dia"
  73. class="col-4"
  74. mask="##:##"
  75. />
  76. <DefaultInput
  77. :model-value="secondEndTime"
  78. label="Horário Final do 2º dia"
  79. class="col-4"
  80. disable
  81. />
  82. <DefaultCurrencyInput
  83. v-model="form.contract_register_value"
  84. label="R$ Matrícula"
  85. class="col-4"
  86. />
  87. <DefaultCurrencyInput
  88. v-model="form.contract_value"
  89. label="R$ Total do Contrato"
  90. class="col-4"
  91. />
  92. <DefaultInput
  93. v-model="form.contrat_discount_value"
  94. label="Desconto em %"
  95. class="col-4"
  96. type="number"
  97. min="0"
  98. max="100"
  99. />
  100. <div
  101. v-for="(material, index) in form.materials"
  102. :key="index"
  103. class="col-12"
  104. >
  105. <div class="row q-col-gutter-sm items-center">
  106. <DefaultSelect
  107. v-model="material.product_id"
  108. label="Material"
  109. class="col"
  110. :options="productOptions"
  111. emit-value
  112. map-options
  113. @update:model-value="onProductSelected(material)"
  114. />
  115. <DefaultInput
  116. v-model="material.quantity"
  117. label="Qtd"
  118. class="col-2"
  119. type="number"
  120. min="1"
  121. />
  122. <DefaultCurrencyInput
  123. v-model="material.price"
  124. label="R$ Unitário"
  125. class="col-3"
  126. />
  127. <div class="col-auto">
  128. <q-btn
  129. v-if="index === form.materials.length - 1"
  130. color="primary"
  131. icon="mdi-plus"
  132. unelevated
  133. style="border-radius: 8px; height: 40px; width: 40px"
  134. @click="addMaterial"
  135. />
  136. <q-btn
  137. v-else
  138. flat
  139. round
  140. dense
  141. icon="mdi-delete-outline"
  142. color="negative"
  143. @click="removeMaterial(index)"
  144. />
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. </q-card-section>
  150. <q-card-actions align="right" class="q-px-md q-pb-md">
  151. <q-btn
  152. outline
  153. color="primary"
  154. label="Cancelar"
  155. @click="onDialogCancel"
  156. />
  157. <q-btn
  158. color="primary"
  159. label="Salvar"
  160. type="submit"
  161. :loading="loading"
  162. />
  163. </q-card-actions>
  164. </DefaultForm>
  165. </q-card>
  166. </div>
  167. </q-dialog>
  168. </template>
  169. <script setup>
  170. import { ref, computed, onMounted, watch } from "vue";
  171. import { useDialogPluginComponent } from "quasar";
  172. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  173. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  174. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  175. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  176. import {
  177. getUnitPackage,
  178. createUnitPackage,
  179. updateUnitPackage,
  180. } from "src/api/package";
  181. import { getProductsForSelect } from "src/api/product";
  182. defineEmits([...useDialogPluginComponent.emits]);
  183. const props = defineProps({
  184. package: {
  185. type: Object,
  186. default: null,
  187. },
  188. });
  189. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  190. useDialogPluginComponent();
  191. const formRef = ref(null);
  192. const loading = ref(false);
  193. const products = ref([]);
  194. const weekdays = [
  195. { value: 1, label: "Segunda" },
  196. { value: 2, label: "Terça" },
  197. { value: 3, label: "Quarta" },
  198. { value: 4, label: "Quinta" },
  199. { value: 5, label: "Sexta" },
  200. { value: 6, label: "Sábado" },
  201. { value: 0, label: "Domingo" },
  202. ];
  203. const trimTime = (time) => (time ? time.slice(0, 5) : null);
  204. const calculateEndTime = (startTime, durationHours) => {
  205. if (!/^\d{2}:\d{2}$/.test(startTime ?? "")) return null;
  206. const [hours, minutes] = startTime.split(":").map(Number);
  207. if (hours > 23 || minutes > 59) return null;
  208. const end =
  209. (hours * 60 + minutes + Math.round(Number(durationHours) * 60)) %
  210. (24 * 60);
  211. return `${String(Math.floor(end / 60)).padStart(2, "0")}:${String(
  212. end % 60,
  213. ).padStart(2, "0")}`;
  214. };
  215. const productOptions = computed(() =>
  216. products.value.map((p) => ({
  217. label: p.name,
  218. value: p.id,
  219. price_sale: p.price_sale,
  220. })),
  221. );
  222. const form = ref({
  223. name: props.package?.name ?? null,
  224. quantity_classes: props.package?.quantity_classes ?? null,
  225. contract_register_value: props.package?.contract_register_value ?? null,
  226. contract_value: props.package?.contract_value ?? null,
  227. contrat_discount_value: props.package?.contrat_discount_value ?? null,
  228. class_duration_hours:
  229. (props.package?.class_duration_minutes ?? 120) / 60,
  230. weekday: props.package?.weekday ?? null,
  231. start_time: trimTime(props.package?.start_time),
  232. second_weekday: props.package?.second_weekday ?? null,
  233. second_start_time: trimTime(props.package?.second_start_time),
  234. materials: [{ product_id: null, quantity: 1, price: null }],
  235. });
  236. const secondWeekdayOptions = computed(() =>
  237. weekdays.filter((day) => day.value !== form.value.weekday),
  238. );
  239. const firstEndTime = computed(() =>
  240. calculateEndTime(form.value.start_time, form.value.class_duration_hours),
  241. );
  242. const secondEndTime = computed(() =>
  243. calculateEndTime(
  244. form.value.second_start_time,
  245. form.value.class_duration_hours,
  246. ),
  247. );
  248. watch(
  249. () => form.value.weekday,
  250. (weekday) => {
  251. if (form.value.second_weekday === weekday) {
  252. form.value.second_weekday = null;
  253. }
  254. },
  255. );
  256. const onProductSelected = (material) => {
  257. const option = productOptions.value.find(
  258. (o) => o.value === material.product_id,
  259. );
  260. if (option) material.price = option.price_sale;
  261. };
  262. const addMaterial = () => {
  263. form.value.materials.push({ product_id: null, quantity: 1, price: null });
  264. };
  265. const removeMaterial = (index) => {
  266. form.value.materials.splice(index, 1);
  267. };
  268. onMounted(async () => {
  269. const [allProducts] = await Promise.all([getProductsForSelect()]);
  270. products.value = allProducts;
  271. if (props.package?.id) {
  272. const pkg = await getUnitPackage(props.package.id);
  273. form.value.class_duration_hours =
  274. (pkg.class_duration_minutes ?? 120) / 60;
  275. form.value.weekday = pkg.weekday ?? null;
  276. form.value.start_time = trimTime(pkg.start_time);
  277. form.value.second_weekday = pkg.second_weekday ?? null;
  278. form.value.second_start_time = trimTime(pkg.second_start_time);
  279. if (pkg.materials?.length) {
  280. form.value.materials = pkg.materials.map((m) => ({
  281. product_id: m.product_id,
  282. quantity: m.quantity,
  283. price: m.price,
  284. }));
  285. }
  286. }
  287. });
  288. const onOKClick = async () => {
  289. loading.value = true;
  290. try {
  291. const validMaterials = form.value.materials.filter((m) => m.product_id);
  292. const payload = {
  293. name: form.value.name,
  294. quantity_classes: form.value.quantity_classes,
  295. contract_value: form.value.contract_value,
  296. contract_register_value: form.value.contract_register_value,
  297. contrat_discount_value: form.value.contrat_discount_value,
  298. class_duration_minutes: Math.round(
  299. Number(form.value.class_duration_hours) * 60,
  300. ),
  301. weekday: form.value.weekday,
  302. start_time: form.value.start_time || null,
  303. second_weekday: form.value.second_weekday,
  304. second_start_time: form.value.second_start_time || null,
  305. materials: validMaterials.map((m) => ({
  306. product_id: m.product_id,
  307. quantity: Number(m.quantity),
  308. price: Number(m.price),
  309. })),
  310. };
  311. const result = props.package?.id
  312. ? await updateUnitPackage(props.package.id, payload)
  313. : await createUnitPackage(payload);
  314. onDialogOK(result);
  315. } finally {
  316. loading.value = false;
  317. }
  318. };
  319. </script>
  320. <style scoped>
  321. .package-duration-hint {
  322. color: #757575;
  323. font-size: 12px;
  324. line-height: 1.35;
  325. padding-top: 4px;
  326. padding-bottom: 2px;
  327. }
  328. </style>