AddEditContractDialog.vue 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <q-card
  4. class="q-dialog-plugin dialog-form-card"
  5. :style="dialogCardStyle"
  6. >
  7. <DefaultDialogHeader
  8. :title="props.contract ? 'Editar Contrato' : 'Novo Contrato'"
  9. @close="onDialogCancel"
  10. />
  11. <template v-if="props.contract">
  12. <q-tabs
  13. v-model="activeTab"
  14. active-color="primary"
  15. align="left"
  16. class="q-px-md text-grey-7"
  17. dense
  18. indicator-color="primary"
  19. >
  20. <q-tab
  21. label="Dados do Contrato"
  22. name="dados"
  23. />
  24. <q-tab
  25. label="Mídias do Contrato"
  26. name="midias"
  27. />
  28. </q-tabs>
  29. <q-separator />
  30. </template>
  31. <DefaultForm
  32. ref="formRef"
  33. @submit="handleSave"
  34. >
  35. <q-scroll-area
  36. ref="scrollAreaRef"
  37. class="dialog-form-scroll"
  38. >
  39. <q-card-section class="q-pt-sm">
  40. <div v-show="activeTab === 'dados'">
  41. <DefaultInput
  42. :model-value="store.user?.name"
  43. class="q-mb-md"
  44. label="Franqueado operador"
  45. readonly
  46. />
  47. <div class="text-subtitle1 q-mb-md">
  48. Dados da Unidade
  49. </div>
  50. <div class="row q-col-gutter-sm">
  51. <DefaultInput
  52. v-model:error="validationErrors.unit_id"
  53. :model-value="unitDetails.name"
  54. class="col-12 col-md-6"
  55. label="Nome da Unidade"
  56. readonly
  57. />
  58. <DefaultInput
  59. v-model:error="validationErrors.unit_id"
  60. :model-value="unitDetails.cnpj"
  61. :mask="masks.Brasil.cnpj"
  62. class="col-12 col-md-6"
  63. label="CNPJ"
  64. readonly
  65. />
  66. </div>
  67. <div class="text-subtitle1 q-mt-lg q-mb-md">
  68. Dados do Aluno
  69. </div>
  70. <div class="row q-col-gutter-sm">
  71. <div
  72. v-if="props.selectStudent"
  73. class="col-12"
  74. >
  75. <div class="row items-start no-wrap q-col-gutter-sm" @keydown.enter.prevent>
  76. <DefaultSelect
  77. v-model="selectedStudent"
  78. v-model:error="validationErrors.student_id"
  79. class="col"
  80. dropdown-icon="mdi-magnify"
  81. fill-input
  82. hide-selected
  83. input-debounce="0"
  84. label="Aluno"
  85. option-label="name"
  86. use-input
  87. :options="filteredStudents"
  88. :rules="[inputRules.required]"
  89. @filter="filterStudents"
  90. />
  91. <div
  92. v-if="canAddStudents"
  93. class="col-auto"
  94. >
  95. <q-btn
  96. aria-label="Cadastrar novo aluno"
  97. color="primary"
  98. icon="mdi-plus"
  99. round
  100. unelevated
  101. @click="openCreateStudent"
  102. >
  103. <q-tooltip>
  104. Cadastrar novo aluno
  105. </q-tooltip>
  106. </q-btn>
  107. </div>
  108. </div>
  109. </div>
  110. <div
  111. v-if="!props.selectStudent"
  112. class="col-12"
  113. >
  114. <DefaultInput
  115. v-model:error="validationErrors.student_id"
  116. :model-value="currentStudent?.name"
  117. :rules="[inputRules.required]"
  118. disable
  119. label="Aluno"
  120. />
  121. </div>
  122. <div class="col-12">
  123. <DefaultInput
  124. v-model:error="validationErrors.birth_date"
  125. :model-value="formattedBirthDate"
  126. disable
  127. label="Data de Nascimento"
  128. />
  129. </div>
  130. </div>
  131. <div class="text-subtitle1 q-mt-lg q-mb-md">
  132. Dados do Contrato
  133. </div>
  134. <div class="row q-col-gutter-sm">
  135. <div class="col-4">
  136. <DefaultInput
  137. v-model="form.protocol"
  138. v-model:error="validationErrors.protocol"
  139. label="Protocolo"
  140. :rules="[inputRules.max(255)]"
  141. />
  142. </div>
  143. <div class="col-4">
  144. <DefaultInputDatePicker
  145. v-model="form.signature_date"
  146. v-model:error="validationErrors.signature_date"
  147. label="Data Assinatura"
  148. :rules="[inputRules.date]"
  149. />
  150. </div>
  151. <div class="col-4">
  152. <DefaultInputDatePicker
  153. v-model="form.end_date"
  154. v-model:error="validationErrors.end_date"
  155. label="Data Encerramento"
  156. :rules="[inputRules.date]"
  157. />
  158. </div>
  159. <div class="col-5">
  160. <DefaultSelect
  161. v-model="form.package_id"
  162. v-model:error="validationErrors.class_package_unit_id"
  163. emit-value
  164. label="Pacote de Aulas"
  165. map-options
  166. option-label="name"
  167. option-value="id"
  168. :options="packages"
  169. :rules="[inputRules.required]"
  170. />
  171. </div>
  172. <div class="col-7">
  173. <DefaultInput
  174. v-model="form.class_quantity"
  175. v-model:error="validationErrors.class_quantity"
  176. disable
  177. label="Qtd. Aulas"
  178. type="number"
  179. :rules="[inputRules.integerRange(1)]"
  180. />
  181. </div>
  182. <div class="col-4">
  183. <DefaultSelect
  184. v-model="form.weekday"
  185. v-model:error="validationErrors.weekday"
  186. emit-value
  187. label="Dia da Semana"
  188. map-options
  189. option-label="label"
  190. option-value="value"
  191. :options="weekdays"
  192. :rules="[inputRules.integerRange(0, 6)]"
  193. />
  194. </div>
  195. <div class="col-4">
  196. <DefaultInput
  197. v-model="form.start_time"
  198. v-model:error="validationErrors.start_time"
  199. label="Hora de Início"
  200. mask="##:##"
  201. :rules="[inputRules.time]"
  202. >
  203. <template #append>
  204. <q-icon name="mdi-clock-outline" />
  205. </template>
  206. </DefaultInput>
  207. </div>
  208. <div class="col-4">
  209. <DefaultInput
  210. v-model="form.end_time"
  211. v-model:error="validationErrors.end_time"
  212. label="Hora de Término"
  213. mask="##:##"
  214. :rules="[inputRules.time]"
  215. >
  216. <template #append>
  217. <q-icon name="mdi-clock-outline" />
  218. </template>
  219. </DefaultInput>
  220. </div>
  221. <div class="col-4">
  222. <DefaultSelect
  223. v-model="form.second_weekday"
  224. v-model:error="validationErrors.second_weekday"
  225. emit-value
  226. label="2° Dia da Semana"
  227. map-options
  228. option-label="label"
  229. option-value="value"
  230. :options="weekdays"
  231. :rules="[inputRules.integerRange(0, 6)]"
  232. />
  233. </div>
  234. <div class="col-4">
  235. <DefaultInput
  236. v-model="form.second_start_time"
  237. v-model:error="validationErrors.second_start_time"
  238. label="Hora de Início"
  239. mask="##:##"
  240. :rules="[inputRules.time]"
  241. >
  242. <template #append>
  243. <q-icon name="mdi-clock-outline" />
  244. </template>
  245. </DefaultInput>
  246. </div>
  247. <div class="col-4">
  248. <DefaultInput
  249. v-model="form.second_end_time"
  250. v-model:error="validationErrors.second_end_time"
  251. label="Hora de Término"
  252. mask="##:##"
  253. :rules="[inputRules.time]"
  254. >
  255. <template #append>
  256. <q-icon name="mdi-clock-outline" />
  257. </template>
  258. </DefaultInput>
  259. </div>
  260. </div>
  261. <div class="text-subtitle1 q-mt-lg q-mb-md">
  262. Dados Financeiros
  263. </div>
  264. <div class="row q-col-gutter-sm">
  265. <div class="col-4">
  266. <DefaultInput
  267. v-model="form.due_day"
  268. v-model:error="validationErrors.due_day"
  269. label="Dia de Vencimento"
  270. type="number"
  271. :rules="[inputRules.integerRange(1, 31)]"
  272. />
  273. </div>
  274. </div>
  275. <div class="text-subtitle2 q-mt-md q-mb-sm text-grey-8">
  276. Matrícula
  277. </div>
  278. <div class="row q-col-gutter-sm">
  279. <div class="col-3">
  280. <DefaultCurrencyInput
  281. v-model="form.enrollment_fee"
  282. v-model:error="validationErrors.tax_register"
  283. disable
  284. label="Valor da Matrícula"
  285. />
  286. </div>
  287. <div class="col-3">
  288. <DefaultSelect
  289. v-model="form.installments"
  290. v-model:error="validationErrors.installments"
  291. emit-value
  292. label="Qtde Parcelas"
  293. map-options
  294. option-label="label"
  295. option-value="value"
  296. :options="enrollmentInstallmentOptions"
  297. :rules="[inputRules.integerRange(1, 12)]"
  298. />
  299. </div>
  300. <div class="col-3">
  301. <DefaultCurrencyInput
  302. v-model:error="validationErrors.installments"
  303. :model-value="enrollmentInstallmentValue"
  304. disable
  305. label="Valor da Parcela"
  306. />
  307. </div>
  308. <div class="col-3">
  309. <DefaultInputDatePicker
  310. v-model="form.enrollment_due_date"
  311. v-model:error="validationErrors.enrollment_due_date"
  312. label="Data Vencimento"
  313. :rules="[inputRules.date]"
  314. />
  315. </div>
  316. </div>
  317. <div class="text-subtitle2 q-mt-md q-mb-sm text-grey-8">
  318. Pacote
  319. </div>
  320. <div class="row q-col-gutter-sm">
  321. <div class="col-3">
  322. <DefaultCurrencyInput
  323. v-model="form.package_value"
  324. v-model:error="validationErrors.package_value"
  325. disable
  326. label="Valor do Pacote"
  327. />
  328. </div>
  329. <div class="col-3">
  330. <DefaultSelect
  331. v-model="form.package_installments"
  332. v-model:error="validationErrors.package_installments"
  333. emit-value
  334. label="Qtde Parcelas"
  335. map-options
  336. option-label="label"
  337. option-value="value"
  338. :options="packageInstallmentOptions"
  339. :rules="[inputRules.integerRange(1, 13)]"
  340. />
  341. </div>
  342. <div class="col-3">
  343. <DefaultCurrencyInput
  344. v-model:error="validationErrors.package_installments"
  345. :model-value="packageInstallmentValue"
  346. disable
  347. label="Valor da Parcela"
  348. />
  349. </div>
  350. <div class="col-3">
  351. <DefaultInputDatePicker
  352. v-model="form.package_due_date"
  353. v-model:error="validationErrors.package_due_date"
  354. label="Data 1ª Parcela"
  355. :rules="[inputRules.date]"
  356. />
  357. </div>
  358. </div>
  359. <div class="row q-col-gutter-sm q-mt-xs">
  360. <div class="col-6">
  361. <DefaultInput
  362. v-model="form.early_payment_discount"
  363. v-model:error="validationErrors.early_payment_discount"
  364. label="Desconto até o vencimento (%)"
  365. type="number"
  366. :rules="[
  367. inputRules.minValue(0),
  368. inputRules.maxValue(100),
  369. ]"
  370. />
  371. </div>
  372. <div class="col-6">
  373. <DefaultSelect
  374. v-model="form.payment_method"
  375. v-model:error="validationErrors.payment_method"
  376. emit-value
  377. label="Forma de Pagamento"
  378. map-options
  379. option-label="label"
  380. option-value="value"
  381. :options="paymentMethods"
  382. :rules="[
  383. inputRules.oneOf([
  384. 'pix',
  385. 'credit_card',
  386. 'debit_card',
  387. ]),
  388. ]"
  389. />
  390. </div>
  391. <div class="col-6">
  392. <DefaultInput
  393. v-model="form.interest_rate"
  394. v-model:error="validationErrors.interest_rate"
  395. label="Juros (%) a.m"
  396. type="number"
  397. :rules="[inputRules.minValue(0)]"
  398. />
  399. </div>
  400. <div class="col-6">
  401. <DefaultInput
  402. v-model="form.late_fee"
  403. v-model:error="validationErrors.fine_cancelled"
  404. label="Multa (%)"
  405. type="number"
  406. :rules="[
  407. inputRules.minValue(0),
  408. inputRules.maxValue(100),
  409. ]"
  410. />
  411. </div>
  412. </div>
  413. </div>
  414. <div
  415. v-if="props.contract"
  416. v-show="activeTab === 'midias'"
  417. >
  418. <DefaultTable
  419. v-model:rows="medias"
  420. :columns="mediaColumns"
  421. :feminino="true"
  422. :loading="loadingMedias"
  423. :show-search-field="false"
  424. descricao="mídias"
  425. no-api-call
  426. title="Mídias"
  427. >
  428. <template #body-cell-actions="{ row }">
  429. <q-td align="center">
  430. <q-item-section
  431. class="no-wrap"
  432. style="flex-direction: row; gap: 4px"
  433. >
  434. <q-btn
  435. icon="mdi-eye-outline"
  436. outline
  437. style="width: 36px"
  438. @click.prevent.stop="openFile(row.file_url)"
  439. />
  440. <q-btn
  441. color="negative"
  442. icon="mdi-trash-can-outline"
  443. outline
  444. style="width: 36px"
  445. @click.prevent.stop="handleDeleteMedia(row)"
  446. />
  447. </q-item-section>
  448. </q-td>
  449. </template>
  450. </DefaultTable>
  451. </div>
  452. </q-card-section>
  453. </q-scroll-area>
  454. <q-card-actions
  455. align="right"
  456. class="q-px-md q-pb-md"
  457. >
  458. <q-btn
  459. color="primary"
  460. label="Cancelar"
  461. no-caps
  462. outline
  463. @click="onDialogCancel"
  464. />
  465. <q-btn
  466. v-if="activeTab === 'dados'"
  467. color="primary"
  468. label="Salvar"
  469. no-caps
  470. type="submit"
  471. :loading="saving"
  472. />
  473. </q-card-actions>
  474. </DefaultForm>
  475. </q-card>
  476. </q-dialog>
  477. </template>
  478. <script setup>
  479. import { addYears, format } from "date-fns";
  480. import {
  481. createStudentContract,
  482. updateStudentContract,
  483. } from "src/api/studentContract";
  484. import {
  485. computed,
  486. nextTick,
  487. onMounted,
  488. ref,
  489. useTemplateRef,
  490. watch,
  491. } from "vue";
  492. import {
  493. deleteContractMedia,
  494. getContractMedias,
  495. } from "src/api/student_media";
  496. import {
  497. formatDateDMYtoYMD,
  498. formatDateYMDtoDMY,
  499. } from "src/helpers/utils";
  500. import { getFinancialMe } from "src/api/unit_financial";
  501. import { getStudentsForSelect } from "src/api/student";
  502. import { getUnitMe } from "src/api/unit";
  503. import { getUnitPackagesForSelect } from "src/api/package";
  504. import { permissionStore } from "src/stores/permission";
  505. import { useDialogPluginComponent, useQuasar } from "quasar";
  506. import { useForm } from "src/composables/useForm";
  507. import { useInputRules } from "src/composables/useInputRules";
  508. import { useScroll } from "src/composables/useScroll";
  509. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  510. import { userStore } from "src/stores/user";
  511. import masks from "src/helpers/masks";
  512. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  513. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  514. import DefaultForm from "src/components/defaults/DefaultForm.vue";
  515. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  516. import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
  517. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  518. import DefaultTable from "src/components/defaults/DefaultTable.vue";
  519. import AddEditStudentDialog from "src/pages/students/components/AddEditStudentDialog.vue";
  520. defineEmits([...useDialogPluginComponent.emits]);
  521. const props = defineProps({
  522. contract: {
  523. type: Object,
  524. default: null,
  525. },
  526. selectStudent: {
  527. type: Boolean,
  528. default: false,
  529. },
  530. student: {
  531. type: Object,
  532. default: null,
  533. },
  534. });
  535. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  536. useDialogPluginComponent();
  537. const $q = useQuasar();
  538. const { inputRules } = useInputRules();
  539. const { scrollToComponent } = useScroll();
  540. const permissions = permissionStore();
  541. const store = userStore();
  542. const formRef = useTemplateRef("formRef");
  543. const scrollAreaRef = useTemplateRef("scrollAreaRef");
  544. const activeTab = ref("dados");
  545. const dateConfirmationOpen = ref(false);
  546. const dateOverrideConfirmed = ref(false);
  547. const filteredStudents = ref([]);
  548. const loadingMedias = ref(false);
  549. const medias = ref([]);
  550. const packages = ref([]);
  551. const restoringDefaultDates = ref(false);
  552. const selectedStudent = ref(props.student);
  553. const students = ref([]);
  554. const syncingFromDate = ref(false);
  555. const syncingFromDay = ref(false);
  556. const unitDetails = ref(store.selectedUnit ?? {});
  557. const canAddStudents = computed(() =>
  558. permissions.getAccess("franchisee_students", "add"),
  559. );
  560. const canViewFinancial = computed(() =>
  561. permissions.getAccess("franchisee_financial", "view"),
  562. );
  563. const currentStudent = computed(
  564. () => selectedStudent.value ?? props.student,
  565. );
  566. const dialogCardStyle = computed(() => ({
  567. height: props.selectStudent ? "95vh" : "auto",
  568. maxHeight: props.selectStudent ? "95vh" : "none",
  569. maxWidth: props.selectStudent ? "1400px" : "1350px",
  570. width: "100%",
  571. }));
  572. const enrollmentInstallmentValue = computed(() => {
  573. if (!form.enrollment_fee || !form.installments) {
  574. return null;
  575. }
  576. return Number(
  577. (form.enrollment_fee / form.installments).toFixed(2),
  578. );
  579. });
  580. const formattedBirthDate = computed(() =>
  581. currentStudent.value?.birth_date
  582. ? formatDateYMDtoDMY(currentStudent.value.birth_date)
  583. : "",
  584. );
  585. const packageInstallmentValue = computed(() => {
  586. if (!form.package_value || !form.package_installments) {
  587. return null;
  588. }
  589. return Number(
  590. (
  591. form.package_value /
  592. form.package_installments
  593. ).toFixed(2),
  594. );
  595. });
  596. const completeDatePattern = /^\d{2}\/\d{2}\/\d{4}$/;
  597. const defaultContractDates = (() => {
  598. if (props.contract) return null;
  599. const startDate = new Date();
  600. return {
  601. end_date: format(addYears(startDate, 1), "dd/MM/yyyy"),
  602. signature_date: format(startDate, "dd/MM/yyyy"),
  603. };
  604. })();
  605. const enrollmentInstallmentOptions = Array.from(
  606. { length: 12 },
  607. (_, index) => ({
  608. label: `${index + 1}x`,
  609. value: index + 1,
  610. }),
  611. );
  612. const mediaColumns = [
  613. {
  614. align: "left",
  615. field: "created_at",
  616. label: "Data de Anexo",
  617. name: "created_at",
  618. },
  619. {
  620. align: "center",
  621. field: null,
  622. label: "Ações",
  623. name: "actions",
  624. },
  625. ];
  626. const packageInstallmentOptions = Array.from(
  627. { length: 13 },
  628. (_, index) => ({
  629. label: `${index + 1}x`,
  630. value: index + 1,
  631. }),
  632. );
  633. const paymentMethods = [
  634. {
  635. label: "Pix",
  636. value: "pix",
  637. },
  638. {
  639. label: "Cartão de Crédito",
  640. value: "credit_card",
  641. },
  642. {
  643. label: "Cartão de Débito",
  644. value: "debit_card",
  645. },
  646. ];
  647. const weekdays = [
  648. {
  649. label: "Segunda",
  650. value: 1,
  651. },
  652. {
  653. label: "Terça",
  654. value: 2,
  655. },
  656. {
  657. label: "Quarta",
  658. value: 3,
  659. },
  660. {
  661. label: "Quinta",
  662. value: 4,
  663. },
  664. {
  665. label: "Sexta",
  666. value: 5,
  667. },
  668. {
  669. label: "Sábado",
  670. value: 6,
  671. },
  672. {
  673. label: "Domingo",
  674. value: 0,
  675. },
  676. ];
  677. const trimTime = (time) =>
  678. time ? time.slice(0, 5) : null;
  679. const { form, getUpdatedFields } = useForm({
  680. class_quantity: props.contract?.class_quantity ?? null,
  681. due_day: props.contract?.recurring_day ?? null,
  682. early_payment_discount:
  683. props.contract?.early_payment_discount ?? null,
  684. end_date:
  685. props.contract?.end_date ??
  686. defaultContractDates?.end_date ??
  687. null,
  688. end_time: trimTime(props.contract?.end_time),
  689. enrollment_due_date:
  690. props.contract?.enrollment_due_date ?? null,
  691. enrollment_fee: props.contract?.tax_register ?? null,
  692. installments: props.contract?.installments ?? null,
  693. interest_rate: props.contract?.interest_rate ?? null,
  694. late_fee: props.contract?.fine_cancelled ?? null,
  695. package_due_date: props.contract?.package_due_date ?? null,
  696. package_id:
  697. props.contract?.class_package_unit_id ?? null,
  698. package_installments:
  699. props.contract?.package_installments ?? null,
  700. package_value: props.contract?.package_value ?? null,
  701. payment_method: props.contract?.payment_method ?? null,
  702. protocol: props.contract?.protocol ?? null,
  703. second_end_time: trimTime(
  704. props.contract?.second_end_time,
  705. ),
  706. second_start_time: trimTime(
  707. props.contract?.second_start_time,
  708. ),
  709. second_weekday:
  710. props.contract?.second_weekday ?? null,
  711. signature_date:
  712. props.contract?.signature_date ??
  713. defaultContractDates?.signature_date ??
  714. null,
  715. start_time: trimTime(props.contract?.start_time),
  716. total_classes: props.contract?.class_quantity ?? null,
  717. weekday: props.contract?.weekday ?? null,
  718. });
  719. const {
  720. loading: saving,
  721. validationErrors,
  722. execute,
  723. } = useSubmitHandler({
  724. containerRef: scrollAreaRef,
  725. formRef,
  726. onSuccess: () => {
  727. onDialogOK(true);
  728. },
  729. scrollFn: scrollToComponent,
  730. });
  731. const buildDateFromDay = (day) => {
  732. const parsedDay = Number.parseInt(day);
  733. if (!parsedDay || parsedDay < 1 || parsedDay > 31) {
  734. return null;
  735. }
  736. const now = new Date();
  737. const nextMonthIndex = (now.getMonth() + 1) % 12;
  738. const nextYear =
  739. now.getMonth() === 11
  740. ? now.getFullYear() + 1
  741. : now.getFullYear();
  742. const lastDayOfNextMonth = new Date(
  743. nextYear,
  744. nextMonthIndex + 1,
  745. 0,
  746. ).getDate();
  747. const safeDay = Math.min(
  748. parsedDay,
  749. lastDayOfNextMonth,
  750. );
  751. return `${String(safeDay).padStart(2, "0")}/${String(
  752. nextMonthIndex + 1,
  753. ).padStart(2, "0")}/${nextYear}`;
  754. };
  755. const buildPayload = () => ({
  756. class_package_unit_id: form.package_id,
  757. class_quantity: form.class_quantity,
  758. due_day: form.due_day
  759. ? Number.parseInt(form.due_day)
  760. : null,
  761. early_payment_discount: form.early_payment_discount,
  762. end_date: form.end_date
  763. ? formatDateDMYtoYMD(form.end_date)
  764. : null,
  765. end_time: form.end_time,
  766. enrollment_due_date: form.enrollment_due_date
  767. ? formatDateDMYtoYMD(form.enrollment_due_date)
  768. : null,
  769. fine_cancelled: form.late_fee,
  770. installments: form.installments,
  771. interest_rate: form.interest_rate,
  772. package_due_date: form.package_due_date
  773. ? formatDateDMYtoYMD(form.package_due_date)
  774. : null,
  775. package_installments: form.package_installments,
  776. package_value: form.package_value,
  777. payment_method: form.payment_method,
  778. protocol: form.protocol,
  779. second_end_time: form.second_end_time,
  780. second_start_time: form.second_start_time,
  781. second_weekday: form.second_weekday,
  782. signature_date: form.signature_date
  783. ? formatDateDMYtoYMD(form.signature_date)
  784. : null,
  785. start_time: form.start_time,
  786. student_id: currentStudent.value?.id ?? null,
  787. tax_register: form.enrollment_fee,
  788. weekday: form.weekday,
  789. });
  790. const calculateEndTime = (
  791. startTime,
  792. durationMinutes = 120,
  793. ) => {
  794. if (!/^\d{2}:\d{2}$/.test(startTime ?? "")) {
  795. return null;
  796. }
  797. const [hours, minutes] = startTime
  798. .split(":")
  799. .map(Number);
  800. if (hours > 23 || minutes > 59) {
  801. return null;
  802. }
  803. const endMinutes =
  804. (hours * 60 + minutes + durationMinutes) %
  805. (24 * 60);
  806. return `${String(
  807. Math.floor(endMinutes / 60),
  808. ).padStart(2, "0")}:${String(
  809. endMinutes % 60,
  810. ).padStart(2, "0")}`;
  811. };
  812. const fetchMedias = async () => {
  813. if (!props.contract) return;
  814. loadingMedias.value = true;
  815. try {
  816. medias.value = await getContractMedias(
  817. props.contract.id,
  818. );
  819. } finally {
  820. loadingMedias.value = false;
  821. }
  822. };
  823. const filterStudents = (value, update) => {
  824. update(() => {
  825. const search = value
  826. .trim()
  827. .toLocaleLowerCase("pt-BR");
  828. filteredStudents.value = search
  829. ? students.value.filter((student) =>
  830. student.name
  831. .toLocaleLowerCase("pt-BR")
  832. .includes(search),
  833. )
  834. : students.value;
  835. });
  836. };
  837. const handleDeleteMedia = (media) => {
  838. $q.dialog({
  839. cancel: {
  840. color: "primary",
  841. label: "Cancelar",
  842. outline: true,
  843. },
  844. message: "Deseja excluir esta mídia permanentemente?",
  845. ok: {
  846. color: "negative",
  847. label: "Excluir",
  848. },
  849. title: "Excluir mídia",
  850. }).onOk(async () => {
  851. try {
  852. await deleteContractMedia(media.id);
  853. medias.value = medias.value.filter(
  854. (item) => item.id !== media.id,
  855. );
  856. } catch (error) {
  857. console.error(error);
  858. $q.notify({
  859. message: "Erro ao excluir mídia.",
  860. type: "negative",
  861. });
  862. }
  863. });
  864. };
  865. const handleSave = async () => {
  866. const payload = buildPayload();
  867. await execute(() => {
  868. if (!props.contract) {
  869. return createStudentContract(payload);
  870. }
  871. const changedFields = getUpdatedFields.value;
  872. const updatePayload = {
  873. student_id: currentStudent.value?.id ?? null,
  874. };
  875. const fieldMap = {
  876. enrollment_fee: "tax_register",
  877. late_fee: "fine_cancelled",
  878. package_id: "class_package_unit_id",
  879. total_classes: "class_quantity",
  880. };
  881. for (const key of [
  882. "protocol",
  883. "class_quantity",
  884. "weekday",
  885. "start_time",
  886. "end_time",
  887. "second_weekday",
  888. "second_start_time",
  889. "second_end_time",
  890. "installments",
  891. "package_value",
  892. "package_installments",
  893. "early_payment_discount",
  894. "interest_rate",
  895. "payment_method",
  896. ]) {
  897. if (key in changedFields) {
  898. updatePayload[key] = payload[key];
  899. }
  900. }
  901. for (const [source, target] of Object.entries(
  902. fieldMap,
  903. )) {
  904. if (source in changedFields) {
  905. updatePayload[target] = payload[target];
  906. }
  907. }
  908. for (const key of [
  909. "signature_date",
  910. "end_date",
  911. "enrollment_due_date",
  912. "package_due_date",
  913. ]) {
  914. if (key in changedFields) {
  915. updatePayload[key] = payload[key];
  916. }
  917. }
  918. if ("due_day" in changedFields) {
  919. updatePayload.due_day = payload.due_day;
  920. }
  921. return updateStudentContract(
  922. props.contract.id,
  923. updatePayload,
  924. );
  925. });
  926. };
  927. const openCreateStudent = () => {
  928. $q.dialog({
  929. component: AddEditStudentDialog,
  930. }).onOk(async (createdStudent) => {
  931. const unitStudents = await getStudentsForSelect();
  932. students.value = unitStudents;
  933. filteredStudents.value = unitStudents;
  934. selectedStudent.value =
  935. unitStudents.find(
  936. (student) => student.id === createdStudent?.id,
  937. ) ?? createdStudent;
  938. });
  939. };
  940. const openFile = (url) => {
  941. window.open(url, "_blank");
  942. };
  943. const restoreDefaultContractDates = () => {
  944. restoringDefaultDates.value = true;
  945. form.signature_date = defaultContractDates.signature_date;
  946. form.end_date = defaultContractDates.end_date;
  947. nextTick(() => {
  948. dateConfirmationOpen.value = false;
  949. restoringDefaultDates.value = false;
  950. });
  951. };
  952. watch(
  953. [() => form.signature_date, () => form.end_date],
  954. ([signatureDate, endDate]) => {
  955. if (
  956. props.contract ||
  957. dateOverrideConfirmed.value ||
  958. dateConfirmationOpen.value ||
  959. restoringDefaultDates.value
  960. ) {
  961. return;
  962. }
  963. const datesAreStillDefault =
  964. signatureDate ===
  965. defaultContractDates.signature_date &&
  966. endDate === defaultContractDates.end_date;
  967. if (datesAreStillDefault) return;
  968. const datesAreComplete =
  969. (!signatureDate ||
  970. completeDatePattern.test(signatureDate)) &&
  971. (!endDate ||
  972. completeDatePattern.test(endDate));
  973. if (!datesAreComplete) return;
  974. dateConfirmationOpen.value = true;
  975. $q.dialog({
  976. cancel: {
  977. color: "primary",
  978. label: "Não",
  979. outline: true,
  980. },
  981. message:
  982. "As datas foram preenchidas automaticamente para a duração de um ano. Tem certeza de que deseja alterá-las?",
  983. ok: {
  984. color: "primary",
  985. label: "Sim",
  986. },
  987. persistent: true,
  988. title: "Alterar duração do contrato?",
  989. })
  990. .onOk(() => {
  991. dateConfirmationOpen.value = false;
  992. dateOverrideConfirmed.value = true;
  993. })
  994. .onCancel(restoreDefaultContractDates);
  995. },
  996. );
  997. watch(
  998. () => form.due_day,
  999. (day) => {
  1000. if (syncingFromDate.value) return;
  1001. const dateString = buildDateFromDay(day);
  1002. if (!dateString) return;
  1003. syncingFromDay.value = true;
  1004. form.package_due_date = dateString;
  1005. nextTick(() => {
  1006. syncingFromDay.value = false;
  1007. });
  1008. },
  1009. );
  1010. watch(
  1011. () => form.package_due_date,
  1012. (dateString) => {
  1013. if (syncingFromDay.value) return;
  1014. if (!dateString || dateString.length < 8) return;
  1015. const day = Number.parseInt(
  1016. dateString.split("/")[0],
  1017. );
  1018. if (
  1019. !Number.isNaN(day) &&
  1020. day !== Number.parseInt(form.due_day)
  1021. ) {
  1022. syncingFromDate.value = true;
  1023. form.due_day = day;
  1024. nextTick(() => {
  1025. syncingFromDate.value = false;
  1026. });
  1027. }
  1028. },
  1029. );
  1030. watch(activeTab, (tab) => {
  1031. if (tab === "midias") {
  1032. fetchMedias();
  1033. }
  1034. });
  1035. watch(
  1036. () => form.package_id,
  1037. (id) => {
  1038. const selectedPackage = packages.value.find(
  1039. (item) => item.id === id,
  1040. );
  1041. if (!selectedPackage) return;
  1042. form.class_quantity =
  1043. selectedPackage.quantity_classes;
  1044. form.total_classes =
  1045. selectedPackage.quantity_classes;
  1046. form.enrollment_fee =
  1047. selectedPackage.contract_register_value;
  1048. form.package_value =
  1049. selectedPackage.contract_value;
  1050. form.weekday = selectedPackage.weekday ?? null;
  1051. form.start_time = trimTime(
  1052. selectedPackage.start_time,
  1053. );
  1054. form.end_time = calculateEndTime(
  1055. form.start_time,
  1056. selectedPackage.class_duration_minutes ?? 120,
  1057. );
  1058. form.second_weekday =
  1059. selectedPackage.second_weekday ?? null;
  1060. form.second_start_time = trimTime(
  1061. selectedPackage.second_start_time,
  1062. );
  1063. form.second_end_time = calculateEndTime(
  1064. form.second_start_time,
  1065. selectedPackage.class_duration_minutes ?? 120,
  1066. );
  1067. },
  1068. );
  1069. watch(
  1070. () => form.start_time,
  1071. (startTime) => {
  1072. const selectedPackage = packages.value.find(
  1073. (item) => item.id === form.package_id,
  1074. );
  1075. const calculatedEndTime = calculateEndTime(
  1076. startTime,
  1077. selectedPackage?.class_duration_minutes ?? 120,
  1078. );
  1079. if (calculatedEndTime) {
  1080. form.end_time = calculatedEndTime;
  1081. }
  1082. },
  1083. );
  1084. watch(
  1085. () => form.second_start_time,
  1086. (startTime) => {
  1087. const selectedPackage = packages.value.find(
  1088. (item) => item.id === form.package_id,
  1089. );
  1090. const calculatedEndTime = calculateEndTime(
  1091. startTime,
  1092. selectedPackage?.class_duration_minutes ?? 120,
  1093. );
  1094. if (calculatedEndTime) {
  1095. form.second_end_time = calculatedEndTime;
  1096. }
  1097. },
  1098. );
  1099. onMounted(async () => {
  1100. const unitRequest = getUnitMe().catch((error) => {
  1101. console.error(
  1102. "Falha ao carregar os dados da unidade:",
  1103. error,
  1104. );
  1105. return store.selectedUnit;
  1106. });
  1107. const requests = [
  1108. getUnitPackagesForSelect(),
  1109. unitRequest,
  1110. ];
  1111. if (props.selectStudent) {
  1112. requests.push(getStudentsForSelect());
  1113. }
  1114. const [
  1115. unitPackages,
  1116. unitData,
  1117. unitStudents = [],
  1118. ] = await Promise.all(requests);
  1119. packages.value = unitPackages;
  1120. unitDetails.value =
  1121. unitData ?? store.selectedUnit ?? {};
  1122. students.value = unitStudents;
  1123. filteredStudents.value = unitStudents;
  1124. if (!props.contract && canViewFinancial.value) {
  1125. try {
  1126. const financial = await getFinancialMe();
  1127. if (!financial) return;
  1128. if (financial.default_discount != null) {
  1129. form.early_payment_discount =
  1130. financial.default_discount;
  1131. }
  1132. if (financial.default_interest != null) {
  1133. form.interest_rate =
  1134. financial.default_interest;
  1135. }
  1136. if (financial.default_fine != null) {
  1137. form.late_fee = financial.default_fine;
  1138. }
  1139. } catch (error) {
  1140. console.error(error);
  1141. }
  1142. }
  1143. });
  1144. </script>