AddEditContractDialog.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <q-card class="q-dialog-plugin dialog-form-card" :style="dialogCardStyle">
  4. <DefaultDialogHeader
  5. :title="props.contract ? 'Editar Contrato' : 'Novo Contrato'"
  6. @close="onDialogCancel"
  7. />
  8. <DefaultForm ref="formRef" @submit="handleSave">
  9. <q-scroll-area ref="scrollAreaRef" class="dialog-form-scroll">
  10. <q-card-section class="q-pt-sm">
  11. <div>
  12. <template v-if="!props.contract">
  13. <DefaultInput
  14. :model-value="store.user?.name"
  15. class="q-mb-md"
  16. disable
  17. label="Franqueado operador"
  18. />
  19. <div class="text-subtitle1 q-mb-md">Dados da Unidade</div>
  20. <div class="row q-col-gutter-sm">
  21. <DefaultInput
  22. v-model:error="validationErrors.unit_id"
  23. :model-value="unitDetails.name"
  24. class="col-12 col-md-6"
  25. disable
  26. label="Nome da Unidade"
  27. />
  28. <DefaultInput
  29. v-model:error="validationErrors.unit_id"
  30. :model-value="unitDetails.cnpj"
  31. :mask="masks.Brasil.cnpj"
  32. class="col-12 col-md-6"
  33. disable
  34. label="CNPJ"
  35. />
  36. </div>
  37. <div class="text-subtitle1 q-mt-lg q-mb-md">Dados do Aluno</div>
  38. <div class="row q-col-gutter-sm">
  39. <div v-if="props.selectStudent" class="col-12">
  40. <div
  41. class="row items-start no-wrap q-col-gutter-sm"
  42. @keydown.enter.prevent
  43. >
  44. <DefaultSelect
  45. v-model="selectedStudent"
  46. v-model:error="validationErrors.student_id"
  47. class="col"
  48. dropdown-icon="mdi-magnify"
  49. fill-input
  50. hide-selected
  51. input-debounce="0"
  52. label="Aluno"
  53. option-label="name"
  54. use-input
  55. :options="filteredStudents"
  56. :rules="[inputRules.required]"
  57. @filter="filterStudents"
  58. >
  59. <template #no-option>
  60. <q-item>
  61. <q-item-section class="text-grey">
  62. {{ $t("http.errors.no_records_found") }}
  63. </q-item-section>
  64. </q-item>
  65. </template>
  66. </DefaultSelect>
  67. <div v-if="canAddStudents" class="col-auto">
  68. <q-btn
  69. aria-label="Cadastrar novo aluno"
  70. color="primary"
  71. icon="mdi-plus"
  72. round
  73. unelevated
  74. @click="openCreateStudent"
  75. >
  76. <q-tooltip> Cadastrar novo aluno </q-tooltip>
  77. </q-btn>
  78. </div>
  79. </div>
  80. </div>
  81. <div v-if="!props.selectStudent" class="col-12">
  82. <DefaultInput
  83. v-model:error="validationErrors.student_id"
  84. :model-value="currentStudent?.name"
  85. :rules="[inputRules.required]"
  86. disable
  87. label="Aluno"
  88. />
  89. </div>
  90. <div class="col-12">
  91. <DefaultInput
  92. v-model:error="validationErrors.birth_date"
  93. :model-value="formattedBirthDate"
  94. disable
  95. label="Data de Nascimento"
  96. />
  97. </div>
  98. </div>
  99. </template>
  100. <div class="text-subtitle1 q-mt-lg q-mb-md">
  101. Dados do Contrato
  102. </div>
  103. <div class="row q-col-gutter-sm">
  104. <div class="col-4">
  105. <DefaultInput
  106. v-model="form.protocol"
  107. v-model:error="validationErrors.protocol"
  108. label="Protocolo"
  109. :rules="[inputRules.max(255)]"
  110. />
  111. </div>
  112. <div class="col-4">
  113. <DefaultInputDatePicker
  114. v-model="form.signature_date"
  115. v-model:error="validationErrors.signature_date"
  116. :disable="!!props.contract"
  117. :rules="[inputRules.date]"
  118. label="Data de Início"
  119. />
  120. </div>
  121. <div class="col-4">
  122. <DefaultInputDatePicker
  123. v-model="form.end_date"
  124. v-model:error="validationErrors.end_date"
  125. label="Data Encerramento"
  126. reactive-rules
  127. :lazy-rules="false"
  128. :rules="[
  129. inputRules.date,
  130. props.contract
  131. ? inputRules.renewalEndDate(
  132. () => props.contract?.end_date,
  133. )
  134. : inputRules.dateMaxYearsFrom(
  135. () => form.signature_date,
  136. ),
  137. ]"
  138. />
  139. </div>
  140. <div class="col-4">
  141. <DefaultSelect
  142. v-model="form.package_id"
  143. v-model:error="validationErrors.class_package_unit_id"
  144. emit-value
  145. label="Pacote de Aulas"
  146. map-options
  147. option-label="name"
  148. option-value="id"
  149. :options="packages"
  150. :rules="props.contract ? [] : [inputRules.required]"
  151. />
  152. </div>
  153. <div class="col-3">
  154. <DefaultSelect
  155. v-model="form.pricing_mode"
  156. v-model:error="validationErrors.pricing_mode"
  157. emit-value
  158. label="Modalidade"
  159. map-options
  160. option-label="label"
  161. option-value="value"
  162. :options="modalityOptions"
  163. :rules="
  164. modalityOptions.length
  165. ? [inputRules.required]
  166. : []
  167. "
  168. />
  169. </div>
  170. <div class="col-5">
  171. <DefaultInput
  172. v-model="form.class_quantity"
  173. v-model:error="validationErrors.class_quantity"
  174. disable
  175. label="Qtd. Aulas"
  176. type="number"
  177. :rules="[inputRules.integerRange(1)]"
  178. />
  179. </div>
  180. <div class="col-6">
  181. <DefaultCurrencyInput
  182. :model-value="form.total_value"
  183. disable
  184. label="Total do Contrato"
  185. />
  186. </div>
  187. <div class="col-6">
  188. <DefaultInput
  189. :model-value="contractWeeks"
  190. disable
  191. label="Total de Semanas"
  192. />
  193. </div>
  194. <div class="col-4">
  195. <DefaultSelect
  196. v-model="form.weekday"
  197. v-model:error="validationErrors.weekday"
  198. emit-value
  199. label="Dia da Semana"
  200. map-options
  201. option-label="label"
  202. option-value="value"
  203. :options="weekdays"
  204. :rules="[inputRules.integerRange(0, 6)]"
  205. />
  206. </div>
  207. <div class="col-4">
  208. <DefaultInput
  209. v-model="form.start_time"
  210. v-model:error="validationErrors.start_time"
  211. label="Hora de Início"
  212. mask="##:##"
  213. :rules="[inputRules.time]"
  214. >
  215. <template #append>
  216. <q-icon name="mdi-clock-outline" />
  217. </template>
  218. </DefaultInput>
  219. </div>
  220. <div class="col-4">
  221. <DefaultInput
  222. v-model="form.end_time"
  223. v-model:error="validationErrors.end_time"
  224. label="Hora de Término"
  225. mask="##:##"
  226. :rules="[inputRules.time]"
  227. >
  228. <template #append>
  229. <q-icon name="mdi-clock-outline" />
  230. </template>
  231. </DefaultInput>
  232. </div>
  233. <div class="col-4">
  234. <DefaultSelect
  235. v-model="form.second_weekday"
  236. v-model:error="validationErrors.second_weekday"
  237. emit-value
  238. label="2° Dia da Semana"
  239. map-options
  240. option-label="label"
  241. option-value="value"
  242. :options="weekdays"
  243. :rules="[inputRules.integerRange(0, 6)]"
  244. />
  245. </div>
  246. <div class="col-4">
  247. <DefaultInput
  248. v-model="form.second_start_time"
  249. v-model:error="validationErrors.second_start_time"
  250. label="Hora de Início"
  251. mask="##:##"
  252. :rules="[inputRules.time]"
  253. >
  254. <template #append>
  255. <q-icon name="mdi-clock-outline" />
  256. </template>
  257. </DefaultInput>
  258. </div>
  259. <div class="col-4">
  260. <DefaultInput
  261. v-model="form.second_end_time"
  262. v-model:error="validationErrors.second_end_time"
  263. label="Hora de Término"
  264. mask="##:##"
  265. :rules="[inputRules.time]"
  266. >
  267. <template #append>
  268. <q-icon name="mdi-clock-outline" />
  269. </template>
  270. </DefaultInput>
  271. </div>
  272. </div>
  273. <div class="text-subtitle1 q-mt-lg q-mb-md">
  274. Dados Financeiros
  275. </div>
  276. <div class="row q-col-gutter-sm">
  277. <div class="col-4">
  278. <DefaultInput
  279. v-model="form.due_day"
  280. v-model:error="validationErrors.due_day"
  281. label="Dia de Vencimento"
  282. type="number"
  283. :rules="[inputRules.integerRange(1, 31)]"
  284. />
  285. </div>
  286. </div>
  287. <div class="text-subtitle2 q-mt-md q-mb-sm text-grey-8">
  288. Total do Curso
  289. </div>
  290. <div class="row q-col-gutter-sm">
  291. <div class="col-3">
  292. <DefaultCurrencyInput
  293. v-model="form.total_value"
  294. v-model:error="validationErrors.total_value"
  295. label="Valor Total do Curso"
  296. :disable="!props.contract"
  297. />
  298. </div>
  299. <div class="col-3">
  300. <DefaultSelect
  301. v-model="form.total_installments"
  302. v-model:error="validationErrors.total_installments"
  303. emit-value
  304. label="Qtde Parcelas"
  305. map-options
  306. option-label="label"
  307. option-value="value"
  308. :options="packageInstallmentOptions"
  309. :rules="[inputRules.integerRange(1, 13)]"
  310. />
  311. </div>
  312. <div class="col-3">
  313. <DefaultCurrencyInput
  314. v-model:error="validationErrors.total_installments"
  315. :model-value="totalInstallmentValue"
  316. disable
  317. label="Valor da Parcela"
  318. />
  319. </div>
  320. <div class="col-3">
  321. <DefaultInputDatePicker
  322. v-model="form.total_due_date"
  323. v-model:error="validationErrors.total_due_date"
  324. label="Data 1ª Parcela"
  325. :rules="[inputRules.date]"
  326. />
  327. </div>
  328. </div>
  329. <div class="row q-col-gutter-sm q-mt-xs">
  330. <div class="col-6">
  331. <DefaultSelect
  332. v-model="form.payment_method"
  333. v-model:error="validationErrors.payment_method"
  334. emit-value
  335. label="Forma de Pagamento"
  336. map-options
  337. option-label="label"
  338. option-value="value"
  339. :options="paymentMethods"
  340. :rules="[
  341. inputRules.oneOf(['pix', 'credit_card', 'debit_card']),
  342. ]"
  343. />
  344. </div>
  345. <div class="col-6">
  346. <DefaultInput
  347. v-model="form.late_fee"
  348. v-model:error="validationErrors.fine_cancelled"
  349. label="Multa (%)"
  350. type="number"
  351. :rules="[inputRules.minValue(0), inputRules.maxValue(100)]"
  352. />
  353. </div>
  354. </div>
  355. </div>
  356. </q-card-section>
  357. </q-scroll-area>
  358. <q-card-actions align="right" class="q-px-md q-pb-md">
  359. <q-btn
  360. color="primary"
  361. label="Cancelar"
  362. no-caps
  363. outline
  364. @click="onDialogCancel"
  365. />
  366. <q-btn
  367. color="primary"
  368. label="Salvar"
  369. no-caps
  370. type="submit"
  371. :loading="saving"
  372. />
  373. </q-card-actions>
  374. </DefaultForm>
  375. </q-card>
  376. </q-dialog>
  377. </template>
  378. <script setup>
  379. import {
  380. addYears,
  381. differenceInCalendarDays,
  382. format,
  383. isValid,
  384. parse,
  385. } from "date-fns";
  386. import {
  387. createStudentContract,
  388. updateStudentContract,
  389. } from "src/api/studentContract";
  390. import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from "vue";
  391. import { formatDateDMYtoYMD, formatDateYMDtoDMY } from "src/helpers/utils";
  392. import { getStudentsForSelect } from "src/api/student";
  393. import { getUnitMe } from "src/api/unit";
  394. import { getUnitPackagesForSelect } from "src/api/package";
  395. import { permissionStore } from "src/stores/permission";
  396. import { useDialogPluginComponent, useQuasar } from "quasar";
  397. import { useForm } from "src/composables/useForm";
  398. import { useInputRules } from "src/composables/useInputRules";
  399. import { useScroll } from "src/composables/useScroll";
  400. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  401. import { userStore } from "src/stores/user";
  402. import masks from "src/helpers/masks";
  403. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  404. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  405. import DefaultForm from "src/components/defaults/DefaultForm.vue";
  406. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  407. import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
  408. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  409. import AddEditStudentDialog from "src/pages/students/components/AddEditStudentDialog.vue";
  410. defineEmits([...useDialogPluginComponent.emits]);
  411. const props = defineProps({
  412. contract: {
  413. type: Object,
  414. default: null,
  415. },
  416. selectStudent: {
  417. type: Boolean,
  418. default: false,
  419. },
  420. student: {
  421. type: Object,
  422. default: null,
  423. },
  424. });
  425. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  426. useDialogPluginComponent();
  427. const $q = useQuasar();
  428. const { inputRules } = useInputRules();
  429. const { scrollToComponent } = useScroll();
  430. const permissions = permissionStore();
  431. const store = userStore();
  432. const formRef = useTemplateRef("formRef");
  433. const scrollAreaRef = useTemplateRef("scrollAreaRef");
  434. const dateConfirmationOpen = ref(false);
  435. const dateOverrideConfirmed = ref(false);
  436. const filteredStudents = ref([]);
  437. const packages = ref([]);
  438. const restoringDefaultDates = ref(false);
  439. const selectedStudent = ref(props.student);
  440. const students = ref([]);
  441. const studentSearchTerm = ref("");
  442. const syncingFromDate = ref(false);
  443. const syncingFromDay = ref(false);
  444. const unitDetails = ref(store.selectedUnit ?? {});
  445. const canAddStudents = computed(() =>
  446. permissions.getAccess("franchisee_students", "add"),
  447. );
  448. const currentStudent = computed(() => selectedStudent.value ?? props.student);
  449. const contractWeeks = computed(() => {
  450. if (!form.signature_date || !form.end_date) return null;
  451. const startDate = parse(form.signature_date, "dd/MM/yyyy", new Date());
  452. const endDate = parse(form.end_date, "dd/MM/yyyy", new Date());
  453. if (!isValid(startDate) || !isValid(endDate)) {
  454. return null;
  455. }
  456. const days = differenceInCalendarDays(endDate, startDate);
  457. if (days < 0) return null;
  458. return Math.max(1, Math.round(days / 7));
  459. });
  460. const dialogCardStyle = computed(() => ({
  461. height: props.selectStudent ? "95vh" : "auto",
  462. maxHeight: props.selectStudent ? "95vh" : "none",
  463. maxWidth: props.selectStudent ? "1400px" : "1350px",
  464. width: "100%",
  465. }));
  466. const formattedBirthDate = computed(() =>
  467. currentStudent.value?.birth_date
  468. ? formatDateYMDtoDMY(currentStudent.value.birth_date)
  469. : "",
  470. );
  471. const totalInstallmentValue = computed(() => {
  472. if (!form.total_value || !form.total_installments) {
  473. return null;
  474. }
  475. return Number((form.total_value / form.total_installments).toFixed(2));
  476. });
  477. const modalityOptions = computed(() => {
  478. const selectedPackage = packages.value.find(
  479. (item) => item.id === form.package_id,
  480. );
  481. if (!selectedPackage) return [];
  482. const options = [];
  483. if (selectedPackage.pavao) options.push({ label: "Pavão", value: "pavao" });
  484. if (selectedPackage.irrecusavel) {
  485. options.push({ label: "Irrecusável", value: "irrecusavel" });
  486. }
  487. return options;
  488. });
  489. const completeDatePattern = /^\d{2}\/\d{2}\/\d{4}$/;
  490. const defaultContractDates = (() => {
  491. if (props.contract) return null;
  492. const startDate = new Date();
  493. return {
  494. end_date: format(addYears(startDate, 1), "dd/MM/yyyy"),
  495. signature_date: format(startDate, "dd/MM/yyyy"),
  496. };
  497. })();
  498. const packageInstallmentOptions = Array.from({ length: 13 }, (_, index) => ({
  499. label: `${index + 1}x`,
  500. value: index + 1,
  501. }));
  502. const paymentMethods = [
  503. {
  504. label: "Pix",
  505. value: "pix",
  506. },
  507. {
  508. label: "Cartão de Crédito",
  509. value: "credit_card",
  510. },
  511. {
  512. label: "Cartão de Débito",
  513. value: "debit_card",
  514. },
  515. ];
  516. const weekdays = [
  517. {
  518. label: "Segunda",
  519. value: 1,
  520. },
  521. {
  522. label: "Terça",
  523. value: 2,
  524. },
  525. {
  526. label: "Quarta",
  527. value: 3,
  528. },
  529. {
  530. label: "Quinta",
  531. value: 4,
  532. },
  533. {
  534. label: "Sexta",
  535. value: 5,
  536. },
  537. {
  538. label: "Sábado",
  539. value: 6,
  540. },
  541. {
  542. label: "Domingo",
  543. value: 0,
  544. },
  545. ];
  546. const trimTime = (time) => (time ? time.slice(0, 5) : null);
  547. const { form, getUpdatedFields } = useForm({
  548. class_quantity: props.contract?.class_quantity ?? null,
  549. due_day: props.contract?.due_day ?? null,
  550. end_date: props.contract?.end_date ?? defaultContractDates?.end_date ?? null,
  551. end_time: trimTime(props.contract?.end_time),
  552. late_fee: props.contract?.fine_cancelled ?? null,
  553. package_id: props.contract?.class_package_unit_id ?? null,
  554. payment_method: props.contract?.payment_method ?? null,
  555. pricing_mode: props.contract?.pricing_mode ?? null,
  556. protocol: props.contract?.protocol ?? null,
  557. total_due_date: props.contract?.total_due_date ?? null,
  558. total_installments: props.contract?.total_installments ?? null,
  559. total_value: props.contract?.total_value ?? null,
  560. second_end_time: trimTime(props.contract?.second_end_time),
  561. second_start_time: trimTime(props.contract?.second_start_time),
  562. second_weekday: props.contract?.second_weekday ?? null,
  563. signature_date:
  564. props.contract?.signature_date ??
  565. defaultContractDates?.signature_date ??
  566. null,
  567. start_time: trimTime(props.contract?.start_time),
  568. total_classes: props.contract?.class_quantity ?? null,
  569. weekday: props.contract?.weekday ?? null,
  570. });
  571. const {
  572. loading: saving,
  573. validationErrors,
  574. execute,
  575. } = useSubmitHandler({
  576. containerRef: scrollAreaRef,
  577. formRef,
  578. onSuccess: () => {
  579. onDialogOK(true);
  580. },
  581. scrollFn: scrollToComponent,
  582. });
  583. const buildDateFromDay = (day) => {
  584. const parsedDay = Number.parseInt(day);
  585. if (!parsedDay || parsedDay < 1 || parsedDay > 31) {
  586. return null;
  587. }
  588. const now = new Date();
  589. const nextMonthIndex = (now.getMonth() + 1) % 12;
  590. const nextYear =
  591. now.getMonth() === 11 ? now.getFullYear() + 1 : now.getFullYear();
  592. const lastDayOfNextMonth = new Date(
  593. nextYear,
  594. nextMonthIndex + 1,
  595. 0,
  596. ).getDate();
  597. const safeDay = Math.min(parsedDay, lastDayOfNextMonth);
  598. return `${String(safeDay).padStart(2, "0")}/${String(
  599. nextMonthIndex + 1,
  600. ).padStart(2, "0")}/${nextYear}`;
  601. };
  602. const buildPayload = () => ({
  603. class_package_unit_id: form.package_id,
  604. class_quantity: form.class_quantity,
  605. due_day: form.due_day ? Number.parseInt(form.due_day) : null,
  606. end_date: form.end_date ? formatDateDMYtoYMD(form.end_date) : null,
  607. end_time: form.end_time,
  608. fine_cancelled: form.late_fee,
  609. payment_method: form.payment_method,
  610. pricing_mode: form.pricing_mode,
  611. protocol: form.protocol,
  612. second_end_time: form.second_end_time,
  613. second_start_time: form.second_start_time,
  614. second_weekday: form.second_weekday,
  615. signature_date: form.signature_date
  616. ? formatDateDMYtoYMD(form.signature_date)
  617. : null,
  618. start_time: form.start_time,
  619. student_id: currentStudent.value?.id ?? null,
  620. total_due_date: form.total_due_date
  621. ? formatDateDMYtoYMD(form.total_due_date)
  622. : null,
  623. total_installments: form.total_installments,
  624. total_value: form.total_value,
  625. weekday: form.weekday,
  626. });
  627. const calculateEndTime = (startTime, durationMinutes = 120) => {
  628. if (!/^\d{2}:\d{2}$/.test(startTime ?? "")) {
  629. return null;
  630. }
  631. const [hours, minutes] = startTime.split(":").map(Number);
  632. if (hours > 23 || minutes > 59) {
  633. return null;
  634. }
  635. const endMinutes = (hours * 60 + minutes + durationMinutes) % (24 * 60);
  636. return `${String(Math.floor(endMinutes / 60)).padStart(2, "0")}:${String(
  637. endMinutes % 60,
  638. ).padStart(2, "0")}`;
  639. };
  640. const filterStudents = (value, update) => {
  641. studentSearchTerm.value = value.trim();
  642. update(() => {
  643. const search = value.trim().toLocaleLowerCase("pt-BR");
  644. filteredStudents.value = search
  645. ? students.value.filter((student) =>
  646. student.name.toLocaleLowerCase("pt-BR").includes(search),
  647. )
  648. : students.value;
  649. });
  650. };
  651. const handleSave = async () => {
  652. const payload = buildPayload();
  653. await execute(() => {
  654. if (!props.contract) {
  655. return createStudentContract(payload);
  656. }
  657. const changedFields = getUpdatedFields.value;
  658. const updatePayload = {};
  659. const fieldMap = {
  660. late_fee: "fine_cancelled",
  661. package_id: "class_package_unit_id",
  662. total_classes: "class_quantity",
  663. };
  664. for (const key of [
  665. "protocol",
  666. "class_quantity",
  667. "weekday",
  668. "start_time",
  669. "end_time",
  670. "second_weekday",
  671. "second_start_time",
  672. "second_end_time",
  673. "payment_method",
  674. "pricing_mode",
  675. "total_value",
  676. "total_installments",
  677. ]) {
  678. if (key in changedFields) {
  679. updatePayload[key] = payload[key];
  680. }
  681. }
  682. for (const [source, target] of Object.entries(fieldMap)) {
  683. if (source in changedFields) {
  684. updatePayload[target] = payload[target];
  685. }
  686. }
  687. for (const key of [
  688. "signature_date",
  689. "end_date",
  690. "total_due_date",
  691. ]) {
  692. if (key in changedFields) {
  693. updatePayload[key] = payload[key];
  694. }
  695. }
  696. if ("due_day" in changedFields) {
  697. updatePayload.due_day = payload.due_day;
  698. }
  699. return updateStudentContract(props.contract.id, updatePayload);
  700. });
  701. };
  702. const openCreateStudent = () => {
  703. $q.dialog({
  704. component: AddEditStudentDialog,
  705. componentProps: {
  706. initialName: studentSearchTerm.value || null,
  707. },
  708. }).onOk(async (createdStudent) => {
  709. const unitStudents = await getStudentsForSelect();
  710. students.value = unitStudents;
  711. filteredStudents.value = unitStudents;
  712. selectedStudent.value =
  713. unitStudents.find((student) => student.id === createdStudent?.id) ??
  714. createdStudent;
  715. });
  716. };
  717. const restoreDefaultContractDates = () => {
  718. restoringDefaultDates.value = true;
  719. form.signature_date = defaultContractDates.signature_date;
  720. form.end_date = defaultContractDates.end_date;
  721. nextTick(() => {
  722. dateConfirmationOpen.value = false;
  723. restoringDefaultDates.value = false;
  724. });
  725. };
  726. watch(
  727. [() => form.signature_date, () => form.end_date],
  728. ([signatureDate, endDate]) => {
  729. if (
  730. props.contract ||
  731. dateOverrideConfirmed.value ||
  732. dateConfirmationOpen.value ||
  733. restoringDefaultDates.value
  734. ) {
  735. return;
  736. }
  737. const datesAreStillDefault =
  738. signatureDate === defaultContractDates.signature_date &&
  739. endDate === defaultContractDates.end_date;
  740. if (datesAreStillDefault) return;
  741. const datesAreComplete =
  742. (!signatureDate || completeDatePattern.test(signatureDate)) &&
  743. (!endDate || completeDatePattern.test(endDate));
  744. if (!datesAreComplete) return;
  745. dateConfirmationOpen.value = true;
  746. $q.dialog({
  747. cancel: {
  748. color: "primary",
  749. label: "Não",
  750. outline: true,
  751. },
  752. message:
  753. "As datas foram preenchidas automaticamente para a duração de um ano. Tem certeza de que deseja alterá-las?",
  754. ok: {
  755. color: "primary",
  756. label: "Sim",
  757. },
  758. persistent: true,
  759. title: "Alterar duração do contrato?",
  760. })
  761. .onOk(() => {
  762. dateConfirmationOpen.value = false;
  763. dateOverrideConfirmed.value = true;
  764. })
  765. .onCancel(restoreDefaultContractDates);
  766. },
  767. );
  768. watch(
  769. () => form.due_day,
  770. (day) => {
  771. if (syncingFromDate.value) return;
  772. const dateString = buildDateFromDay(day);
  773. if (!dateString) return;
  774. syncingFromDay.value = true;
  775. form.package_due_date = dateString;
  776. nextTick(() => {
  777. syncingFromDay.value = false;
  778. });
  779. },
  780. );
  781. watch(
  782. () => form.package_due_date,
  783. (dateString) => {
  784. if (syncingFromDay.value) return;
  785. if (!dateString || dateString.length < 8) return;
  786. const day = Number.parseInt(dateString.split("/")[0]);
  787. if (!Number.isNaN(day) && day !== Number.parseInt(form.due_day)) {
  788. syncingFromDate.value = true;
  789. form.due_day = day;
  790. nextTick(() => {
  791. syncingFromDate.value = false;
  792. });
  793. }
  794. },
  795. );
  796. watch(
  797. () => form.package_id,
  798. (id) => {
  799. const selectedPackage = packages.value.find((item) => item.id === id);
  800. if (!selectedPackage) return;
  801. form.class_quantity = selectedPackage.quantity_classes;
  802. form.total_classes = selectedPackage.quantity_classes;
  803. form.weekday = selectedPackage.weekday ?? null;
  804. form.start_time = trimTime(selectedPackage.start_time);
  805. form.end_time = calculateEndTime(
  806. form.start_time,
  807. selectedPackage.class_duration_minutes ?? 120,
  808. );
  809. form.second_weekday = selectedPackage.second_weekday ?? null;
  810. form.second_start_time = trimTime(selectedPackage.second_start_time);
  811. form.second_end_time = calculateEndTime(
  812. form.second_start_time,
  813. selectedPackage.class_duration_minutes ?? 120,
  814. );
  815. // Escolhe automaticamente a modalidade quando só há uma disponível;
  816. // se a proposta ainda não tem Pavão nem Irrecusável, limpa a escolha.
  817. if (selectedPackage.pavao) {
  818. form.pricing_mode = "pavao";
  819. } else if (selectedPackage.irrecusavel) {
  820. form.pricing_mode = "irrecusavel";
  821. } else {
  822. form.pricing_mode = null;
  823. }
  824. },
  825. );
  826. // Os valores do contrato vêm da modalidade escolhida na proposta comercial:
  827. // o que estiver marcado "incluso no valor do curso" não é cobrado à parte
  828. // (só entra no Total do Curso); o que "permite parcelar" vira cobrança
  829. // separada com o próprio valor/parcelas daquele item.
  830. watch(
  831. [() => form.package_id, () => form.pricing_mode],
  832. ([id, mode]) => {
  833. const selectedPackage = packages.value.find((item) => item.id === id);
  834. const modality =
  835. mode === "irrecusavel"
  836. ? selectedPackage?.irrecusavel
  837. : selectedPackage?.pavao;
  838. if (!modality) {
  839. form.total_value = null;
  840. form.total_installments = null;
  841. return;
  842. }
  843. form.total_value = modality.total_value;
  844. form.total_installments = modality.total_max_installments;
  845. },
  846. );
  847. watch(
  848. () => form.start_time,
  849. (startTime) => {
  850. const selectedPackage = packages.value.find(
  851. (item) => item.id === form.package_id,
  852. );
  853. const calculatedEndTime = calculateEndTime(
  854. startTime,
  855. selectedPackage?.class_duration_minutes ?? 120,
  856. );
  857. if (calculatedEndTime) {
  858. form.end_time = calculatedEndTime;
  859. }
  860. },
  861. );
  862. watch(
  863. () => form.second_start_time,
  864. (startTime) => {
  865. const selectedPackage = packages.value.find(
  866. (item) => item.id === form.package_id,
  867. );
  868. const calculatedEndTime = calculateEndTime(
  869. startTime,
  870. selectedPackage?.class_duration_minutes ?? 120,
  871. );
  872. if (calculatedEndTime) {
  873. form.second_end_time = calculatedEndTime;
  874. }
  875. },
  876. );
  877. onMounted(async () => {
  878. const unitRequest = getUnitMe().catch((error) => {
  879. console.error("Falha ao carregar os dados da unidade:", error);
  880. return store.selectedUnit;
  881. });
  882. const requests = [getUnitPackagesForSelect(), unitRequest];
  883. if (props.selectStudent) {
  884. requests.push(getStudentsForSelect());
  885. }
  886. const [unitPackages, unitData, unitStudents = []] =
  887. await Promise.all(requests);
  888. packages.value = unitPackages;
  889. unitDetails.value = unitData ?? store.selectedUnit ?? {};
  890. students.value = unitStudents;
  891. filteredStudents.value = unitStudents;
  892. });
  893. </script>