AddEditStudentDialog.vue 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <q-card
  4. class="q-dialog-plugin dialog-form-card"
  5. style="width: 100%; max-width: 1100px"
  6. >
  7. <DefaultDialogHeader
  8. title="Cadastrar Aluno"
  9. @close="onDialogCancel"
  10. />
  11. <DefaultForm
  12. ref="formRef"
  13. @submit="onOKClick"
  14. >
  15. <q-scroll-area
  16. ref="scrollAreaRef"
  17. class="dialog-form-scroll"
  18. >
  19. <q-card-section class="q-pt-sm">
  20. <CustomTabComponent
  21. :active-tab="activeTab"
  22. :tabs="tabs"
  23. class="q-mb-md"
  24. @update:active-tab="handleTabChange"
  25. />
  26. <div
  27. v-show="activeTab === 'student'"
  28. ref="studentTabRef"
  29. >
  30. <q-banner
  31. v-if="validationErrors.registration_draft_token"
  32. class="bg-red-1 text-negative q-mb-md"
  33. rounded
  34. >
  35. {{ validationErrors.registration_draft_token }}
  36. </q-banner>
  37. <div class="flex justify-center q-mb-md">
  38. <AvatarImageComponent
  39. ref="avatarRef"
  40. @update:file="onAvatarChange"
  41. />
  42. </div>
  43. <div class="row q-col-gutter-sm">
  44. <DefaultInput
  45. v-model="form.name"
  46. class="col-6"
  47. label="Nome do Aluno"
  48. :error="!!validationErrors.name"
  49. :error-message="validationErrors.name"
  50. :rules="[inputRules.required]"
  51. />
  52. <DefaultInputDatePicker
  53. v-model="form.birthdate"
  54. class="col-6"
  55. label="Data de Nascimento"
  56. :error="!!validationErrors.birth_date"
  57. :error-message="validationErrors.birth_date"
  58. :rules="[inputRules.required]"
  59. />
  60. <DefaultInput
  61. v-model="form.cpf"
  62. class="col-6"
  63. label="CPF"
  64. :error="!!validationErrors.document_number"
  65. :error-message="validationErrors.document_number"
  66. :mask="masks.Brasil.cpf"
  67. :rules="[inputRules.cpf]"
  68. />
  69. <DefaultSelect
  70. v-model="form.gender"
  71. class="col-6"
  72. emit-value
  73. label="Gênero"
  74. map-options
  75. :error="!!validationErrors.gender"
  76. :error-message="validationErrors.gender"
  77. :options="genderOptions"
  78. />
  79. <DefaultInput
  80. v-model="form.email"
  81. class="col-6"
  82. label="E-mail"
  83. type="email"
  84. :error="!!validationErrors.email"
  85. :error-message="validationErrors.email"
  86. :rules="[inputRules.email]"
  87. />
  88. <DefaultInput
  89. v-model="form.phone"
  90. class="col-6"
  91. label="Celular com DDD"
  92. :error="!!validationErrors.phone"
  93. :error-message="validationErrors.phone"
  94. :mask="masks.Brasil.celular"
  95. />
  96. <DefaultCepInput
  97. v-model="form.cep"
  98. class="col-4"
  99. :error="!!validationErrors.postal_code"
  100. :error-message="validationErrors.postal_code"
  101. :rules="[inputRules.cep]"
  102. @bairro="(value) => (form.neighborhood = value)"
  103. @cidade="
  104. (value) =>
  105. citySelectRef?.selectCityByName(value)
  106. "
  107. @rua="(value) => (form.address = value)"
  108. @uf="
  109. (value) =>
  110. stateSelectRef?.selectStateByCode(value)
  111. "
  112. />
  113. <DefaultInput
  114. v-model="form.address"
  115. class="col-5"
  116. label="Endereço"
  117. :error="!!validationErrors.street"
  118. :error-message="validationErrors.street"
  119. />
  120. <DefaultInput
  121. v-model="form.address_number"
  122. class="col-3"
  123. label="Número"
  124. :error="!!validationErrors.address_number"
  125. :error-message="validationErrors.address_number"
  126. />
  127. <DefaultInput
  128. v-model="form.neighborhood"
  129. class="col-4"
  130. label="Bairro"
  131. :error="!!validationErrors.neighborhood"
  132. :error-message="validationErrors.neighborhood"
  133. />
  134. <CitySelect
  135. ref="citySelectRef"
  136. v-model="selectedCity"
  137. class="col-4"
  138. label="Cidade"
  139. :error="!!validationErrors.city_id"
  140. :error-message="validationErrors.city_id"
  141. :state="selectedState"
  142. />
  143. <StateSelect
  144. ref="stateSelectRef"
  145. v-model="selectedState"
  146. class="col-4"
  147. label="Estado"
  148. :error="!!validationErrors.state_id"
  149. :error-message="validationErrors.state_id"
  150. />
  151. <DefaultInput
  152. v-model="form.complement"
  153. class="col-6"
  154. label="Complemento"
  155. :error="!!validationErrors.complement"
  156. :error-message="validationErrors.complement"
  157. />
  158. <DefaultInput
  159. v-model="form.payer"
  160. class="col-6"
  161. label="Pagador"
  162. :error="!!validationErrors.payer_name"
  163. :error-message="validationErrors.payer_name"
  164. />
  165. <DefaultSelect
  166. v-model="form.how_found"
  167. class="col-12"
  168. emit-value
  169. label="Como nos conheceu?"
  170. map-options
  171. :error="!!validationErrors.how_did_you_know_us"
  172. :error-message="
  173. validationErrors.how_did_you_know_us
  174. "
  175. :options="howFoundOptions"
  176. />
  177. <DefaultInput
  178. v-model="form.notes"
  179. autogrow
  180. class="col-12"
  181. label="Observações"
  182. type="textarea"
  183. :error="!!validationErrors.notes"
  184. :error-message="validationErrors.notes"
  185. :input-style="{ minHeight: '120px' }"
  186. />
  187. </div>
  188. </div>
  189. <div
  190. v-if="isStudentUnderage"
  191. v-show="activeTab === 'responsible'"
  192. ref="responsibleTabRef"
  193. >
  194. <q-banner
  195. class="bg-orange-1 text-orange-10 q-mb-md"
  196. rounded
  197. >
  198. O aluno é menor de 18 anos. Cadastre um responsável para
  199. concluir.
  200. </q-banner>
  201. <div class="row q-col-gutter-sm">
  202. <DefaultInput
  203. v-model="responsibleForm.name"
  204. class="col-6"
  205. label="Nome"
  206. :error="!!validationErrors['responsible.name']"
  207. :error-message="
  208. validationErrors['responsible.name']
  209. "
  210. :rules="
  211. activeTab === 'responsible'
  212. ? [inputRules.required]
  213. : []
  214. "
  215. />
  216. <DefaultInput
  217. v-model="responsibleForm.degree"
  218. class="col-6"
  219. label="Grau de Parentesco"
  220. :error="!!validationErrors['responsible.degree']"
  221. :error-message="
  222. validationErrors['responsible.degree']
  223. "
  224. :rules="responsibleRequiredRules"
  225. />
  226. <DefaultInputDatePicker
  227. v-model="responsibleForm.birth_date"
  228. class="col-4"
  229. label="Data de Nascimento"
  230. :error="
  231. !!validationErrors['responsible.birth_date']
  232. "
  233. :error-message="
  234. validationErrors['responsible.birth_date']
  235. "
  236. :rules="responsibleRequiredRules"
  237. />
  238. <DefaultInput
  239. v-model="responsibleForm.cpf"
  240. class="col-4"
  241. label="CPF"
  242. :error="!!validationErrors['responsible.cpf']"
  243. :error-message="
  244. validationErrors['responsible.cpf']
  245. "
  246. :mask="masks.Brasil.cpf"
  247. :rules="
  248. activeTab === 'responsible'
  249. ? [inputRules.required, inputRules.cpf]
  250. : []
  251. "
  252. />
  253. <DefaultSelect
  254. v-model="responsibleForm.gender"
  255. class="col-4"
  256. emit-value
  257. label="Gênero"
  258. map-options
  259. :error="!!validationErrors['responsible.gender']"
  260. :error-message="
  261. validationErrors['responsible.gender']
  262. "
  263. :options="genderOptions"
  264. />
  265. <DefaultInput
  266. v-model="responsibleForm.email"
  267. class="col-6"
  268. label="E-mail"
  269. type="email"
  270. :error="!!validationErrors['responsible.email']"
  271. :error-message="
  272. validationErrors['responsible.email']
  273. "
  274. :rules="responsibleEmailRules"
  275. />
  276. <DefaultInput
  277. v-model="responsibleForm.phone"
  278. class="col-6"
  279. label="Telefone"
  280. :error="!!validationErrors['responsible.phone']"
  281. :error-message="
  282. validationErrors['responsible.phone']
  283. "
  284. :mask="masks.Brasil.celular"
  285. :rules="responsibleRequiredRules"
  286. />
  287. <DefaultCepInput
  288. v-model="responsibleForm.postal_code"
  289. class="col-4"
  290. :error="
  291. !!validationErrors['responsible.postal_code']
  292. "
  293. :error-message="
  294. validationErrors['responsible.postal_code']
  295. "
  296. :rules="responsibleCepRules"
  297. @bairro="
  298. (value) =>
  299. (responsibleForm.neighborhood = value)
  300. "
  301. @cidade="
  302. (value) =>
  303. responsibleCitySelectRef?.selectCityByName(
  304. value,
  305. )
  306. "
  307. @rua="
  308. (value) =>
  309. (responsibleForm.street = value)
  310. "
  311. @uf="
  312. (value) =>
  313. responsibleStateSelectRef?.selectStateByCode(
  314. value,
  315. )
  316. "
  317. />
  318. <DefaultInput
  319. v-model="responsibleForm.street"
  320. class="col-5"
  321. label="Endereço"
  322. :error="!!validationErrors['responsible.street']"
  323. :error-message="
  324. validationErrors['responsible.street']
  325. "
  326. :rules="responsibleRequiredRules"
  327. />
  328. <DefaultInput
  329. v-model="responsibleForm.address_number"
  330. class="col-3"
  331. label="Número"
  332. :error="
  333. !!validationErrors[
  334. 'responsible.address_number'
  335. ]
  336. "
  337. :error-message="
  338. validationErrors[
  339. 'responsible.address_number'
  340. ]
  341. "
  342. />
  343. <DefaultInput
  344. v-model="responsibleForm.neighborhood"
  345. class="col-4"
  346. label="Bairro"
  347. :error="
  348. !!validationErrors[
  349. 'responsible.neighborhood'
  350. ]
  351. "
  352. :error-message="
  353. validationErrors[
  354. 'responsible.neighborhood'
  355. ]
  356. "
  357. :rules="responsibleRequiredRules"
  358. />
  359. <CitySelect
  360. ref="responsibleCitySelectRef"
  361. v-model="responsibleSelectedCity"
  362. class="col-4"
  363. label="Cidade"
  364. :error="
  365. !!validationErrors['responsible.city_id']
  366. "
  367. :error-message="
  368. validationErrors['responsible.city_id']
  369. "
  370. :rules="responsibleRequiredRules"
  371. :state="responsibleSelectedState"
  372. />
  373. <StateSelect
  374. ref="responsibleStateSelectRef"
  375. v-model="responsibleSelectedState"
  376. class="col-4"
  377. label="Estado"
  378. :error="
  379. !!validationErrors['responsible.state_id']
  380. "
  381. :error-message="
  382. validationErrors['responsible.state_id']
  383. "
  384. :rules="responsibleRequiredRules"
  385. />
  386. <DefaultInput
  387. v-model="responsibleForm.complement"
  388. class="col-12"
  389. label="Complemento"
  390. :error="
  391. !!validationErrors['responsible.complement']
  392. "
  393. :error-message="
  394. validationErrors['responsible.complement']
  395. "
  396. />
  397. <DefaultInput
  398. v-model="responsibleForm.notes"
  399. autogrow
  400. class="col-12"
  401. label="Observações"
  402. type="textarea"
  403. :error="!!validationErrors['responsible.notes']"
  404. :error-message="
  405. validationErrors['responsible.notes']
  406. "
  407. :input-style="{ minHeight: '120px' }"
  408. />
  409. </div>
  410. </div>
  411. </q-card-section>
  412. </q-scroll-area>
  413. <q-card-actions
  414. align="right"
  415. class="q-px-md q-pb-md"
  416. >
  417. <q-btn
  418. color="primary"
  419. label="Cancelar"
  420. no-caps
  421. outline
  422. @click="onDialogCancel"
  423. />
  424. <q-btn
  425. color="primary"
  426. no-caps
  427. type="submit"
  428. :label="primaryActionLabel"
  429. :loading="loading"
  430. />
  431. </q-card-actions>
  432. </DefaultForm>
  433. </q-card>
  434. </q-dialog>
  435. </template>
  436. <script setup>
  437. import {
  438. computed,
  439. nextTick,
  440. ref,
  441. useTemplateRef,
  442. watch,
  443. } from "vue";
  444. import {
  445. createStudent,
  446. createStudentRegistrationDraft,
  447. } from "src/api/student";
  448. import {
  449. formatDateDMYtoYMD,
  450. isUnderage,
  451. } from "src/helpers/utils";
  452. import { useDialogPluginComponent } from "quasar";
  453. import { useInputRules } from "src/composables/useInputRules";
  454. import { useScroll } from "src/composables/useScroll";
  455. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  456. import masks from "src/helpers/masks";
  457. import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
  458. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  459. import DefaultForm from "src/components/defaults/DefaultForm.vue";
  460. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  461. import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
  462. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  463. import CitySelect from "src/components/selects/CitySelect.vue";
  464. import StateSelect from "src/components/selects/StateSelect.vue";
  465. import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
  466. import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
  467. defineEmits([...useDialogPluginComponent.emits]);
  468. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  469. useDialogPluginComponent();
  470. const { inputRules } = useInputRules();
  471. const { scrollToComponent } = useScroll();
  472. const citySelectRef = useTemplateRef("citySelectRef");
  473. const formRef = useTemplateRef("formRef");
  474. const responsibleCitySelectRef = useTemplateRef(
  475. "responsibleCitySelectRef",
  476. );
  477. const responsibleStateSelectRef = useTemplateRef(
  478. "responsibleStateSelectRef",
  479. );
  480. const responsibleTabRef = useTemplateRef("responsibleTabRef");
  481. const scrollAreaRef = useTemplateRef("scrollAreaRef");
  482. const stateSelectRef = useTemplateRef("stateSelectRef");
  483. const studentTabRef = useTemplateRef("studentTabRef");
  484. const activeTab = ref("student");
  485. const avatarFile = ref(null);
  486. const registrationDraftToken = ref(null);
  487. const responsibleSelectedCity = ref(null);
  488. const responsibleSelectedState = ref(null);
  489. const selectedCity = ref(null);
  490. const selectedState = ref(null);
  491. const form = ref({
  492. address: null,
  493. address_number: null,
  494. birthdate: null,
  495. cep: null,
  496. city_id: null,
  497. complement: null,
  498. cpf: null,
  499. email: null,
  500. gender: "no_preference",
  501. how_found: null,
  502. name: null,
  503. neighborhood: null,
  504. notes: null,
  505. payer: null,
  506. phone: null,
  507. state_id: null,
  508. });
  509. const responsibleForm = ref({
  510. address_number: null,
  511. birth_date: null,
  512. city_id: null,
  513. complement: null,
  514. cpf: null,
  515. degree: null,
  516. email: null,
  517. gender: "no_preference",
  518. name: null,
  519. neighborhood: null,
  520. notes: null,
  521. phone: null,
  522. postal_code: null,
  523. state_id: null,
  524. street: null,
  525. });
  526. const isStudentUnderage = computed(() =>
  527. isUnderage(form.value.birthdate),
  528. );
  529. const responsibleCepRules = computed(() =>
  530. activeTab.value === "responsible"
  531. ? [inputRules.required, inputRules.cep]
  532. : [inputRules.cep],
  533. );
  534. const responsibleEmailRules = computed(() =>
  535. activeTab.value === "responsible"
  536. ? [inputRules.required, inputRules.email]
  537. : [inputRules.email],
  538. );
  539. const responsibleRequiredRules = computed(() =>
  540. activeTab.value === "responsible"
  541. ? [inputRules.required]
  542. : [],
  543. );
  544. const tabs = computed(() => [
  545. {
  546. label: "Dados do Aluno",
  547. name: "student",
  548. },
  549. ...(isStudentUnderage.value
  550. ? [
  551. {
  552. label: "Responsável",
  553. name: "responsible",
  554. },
  555. ]
  556. : []),
  557. ]);
  558. const primaryActionLabel = computed(() =>
  559. isStudentUnderage.value &&
  560. activeTab.value === "student"
  561. ? "Próximo"
  562. : "Cadastrar",
  563. );
  564. const genderOptions = [
  565. {
  566. label: "Prefiro não informar",
  567. value: "no_preference",
  568. },
  569. {
  570. label: "Masculino",
  571. value: "male",
  572. },
  573. {
  574. label: "Feminino",
  575. value: "female",
  576. },
  577. {
  578. label: "Outro",
  579. value: "other",
  580. },
  581. ];
  582. const howFoundOptions = [
  583. {
  584. label: "Indicação",
  585. value: "referral",
  586. },
  587. {
  588. label: "Redes Sociais",
  589. value: "social_media",
  590. },
  591. {
  592. label: "Google",
  593. value: "google",
  594. },
  595. {
  596. label: "Outro",
  597. value: "other",
  598. },
  599. ];
  600. const REGISTRATION_DRAFT_TOKEN_STORAGE_KEY =
  601. "student_registration_draft_token";
  602. localStorage.removeItem(
  603. REGISTRATION_DRAFT_TOKEN_STORAGE_KEY,
  604. );
  605. const scrollToTabError = async (
  606. component,
  607. container,
  608. options,
  609. ) => {
  610. const element = component?.$el ?? component;
  611. const studentElement =
  612. studentTabRef.value?.$el ?? studentTabRef.value;
  613. const responsibleElement =
  614. responsibleTabRef.value?.$el ??
  615. responsibleTabRef.value;
  616. if (studentElement?.contains(element)) {
  617. await setActiveTab("student");
  618. } else if (responsibleElement?.contains(element)) {
  619. await setActiveTab("responsible");
  620. }
  621. return scrollToComponent(
  622. component,
  623. container,
  624. options,
  625. );
  626. };
  627. const {
  628. loading: saving,
  629. validationErrors: submitValidationErrors,
  630. execute: executeSubmit,
  631. } = useSubmitHandler({
  632. containerRef: scrollAreaRef,
  633. formRef,
  634. onSuccess: (student) => {
  635. clearRegistrationDraftToken();
  636. onDialogOK(student);
  637. },
  638. scrollFn: scrollToTabError,
  639. });
  640. const {
  641. loading: validatingStudent,
  642. validationErrors: draftValidationErrors,
  643. execute: executeDraft,
  644. } = useSubmitHandler({
  645. containerRef: scrollAreaRef,
  646. formRef,
  647. scrollFn: scrollToTabError,
  648. });
  649. const loading = computed(
  650. () => saving.value || validatingStudent.value,
  651. );
  652. const validationErrors = computed(() => ({
  653. ...draftValidationErrors.value,
  654. ...submitValidationErrors.value,
  655. }));
  656. const appendResponsibleToFormData = (formData) => {
  657. const responsible = buildResponsiblePayload();
  658. Object.entries(responsible).forEach(([field, value]) => {
  659. formData.append(
  660. `responsible[${field}]`,
  661. value ?? "",
  662. );
  663. });
  664. };
  665. const appendStudentToFormData = (formData) => {
  666. const student = buildStudentPayload();
  667. Object.entries(student).forEach(([field, value]) => {
  668. formData.append(field, value ?? "");
  669. });
  670. };
  671. const buildPayload = () => {
  672. if (isStudentUnderage.value) {
  673. if (avatarFile.value) {
  674. const formData = new FormData();
  675. appendStudentToFormData(formData);
  676. formData.append(
  677. "registration_draft_token",
  678. registrationDraftToken.value,
  679. );
  680. formData.append("avatar", avatarFile.value);
  681. appendResponsibleToFormData(formData);
  682. return formData;
  683. }
  684. return {
  685. ...buildStudentPayload(),
  686. registration_draft_token:
  687. registrationDraftToken.value,
  688. responsible: buildResponsiblePayload(),
  689. };
  690. }
  691. if (avatarFile.value) {
  692. const formData = new FormData();
  693. appendStudentToFormData(formData);
  694. formData.append("avatar", avatarFile.value);
  695. return formData;
  696. }
  697. return buildStudentPayload();
  698. };
  699. const buildRegistrationDraftPayload = () => ({
  700. birth_date: form.value.birthdate
  701. ? formatDateDMYtoYMD(form.value.birthdate)
  702. : null,
  703. document_number: form.value.cpf,
  704. email: form.value.email || null,
  705. name: form.value.name,
  706. registration_draft_token:
  707. registrationDraftToken.value,
  708. });
  709. const buildResponsiblePayload = () => ({
  710. address_number: responsibleForm.value.address_number,
  711. birth_date: responsibleForm.value.birth_date
  712. ? formatDateDMYtoYMD(
  713. responsibleForm.value.birth_date,
  714. )
  715. : null,
  716. city_id: responsibleForm.value.city_id,
  717. complement: responsibleForm.value.complement,
  718. cpf: responsibleForm.value.cpf,
  719. degree: responsibleForm.value.degree,
  720. email: responsibleForm.value.email || null,
  721. gender: responsibleForm.value.gender,
  722. name: responsibleForm.value.name,
  723. neighborhood: responsibleForm.value.neighborhood,
  724. notes: responsibleForm.value.notes,
  725. phone: responsibleForm.value.phone,
  726. postal_code: responsibleForm.value.postal_code,
  727. state_id: responsibleForm.value.state_id,
  728. street: responsibleForm.value.street,
  729. });
  730. const buildStudentPayload = () => ({
  731. address_number: form.value.address_number,
  732. birth_date: form.value.birthdate
  733. ? formatDateDMYtoYMD(form.value.birthdate)
  734. : null,
  735. city_id: form.value.city_id,
  736. complement: form.value.complement,
  737. document_number: form.value.cpf,
  738. email: form.value.email || null,
  739. gender: form.value.gender,
  740. how_did_you_know_us: form.value.how_found,
  741. name: form.value.name,
  742. neighborhood: form.value.neighborhood,
  743. notes: form.value.notes,
  744. payer_name: form.value.payer,
  745. phone: form.value.phone,
  746. postal_code: form.value.cep,
  747. state_id: form.value.state_id,
  748. street: form.value.address,
  749. });
  750. const clearRegistrationDraftToken = () => {
  751. registrationDraftToken.value = null;
  752. localStorage.removeItem(
  753. REGISTRATION_DRAFT_TOKEN_STORAGE_KEY,
  754. );
  755. };
  756. const goToResponsible = async () => {
  757. if (validatingStudent.value) return;
  758. const createDraft = () =>
  759. createStudentRegistrationDraft(
  760. buildRegistrationDraftPayload(),
  761. );
  762. let draft;
  763. try {
  764. draft = await executeDraft(createDraft);
  765. } catch (error) {
  766. const draftTokenError =
  767. draftValidationErrors.value
  768. .registration_draft_token;
  769. if (
  770. !draftTokenError ||
  771. !registrationDraftToken.value
  772. ) {
  773. throw error;
  774. }
  775. clearRegistrationDraftToken();
  776. draft = await executeDraft(createDraft);
  777. }
  778. const nextToken = draft?.registration_draft_token;
  779. if (
  780. nextToken &&
  781. nextToken !== registrationDraftToken.value
  782. ) {
  783. saveRegistrationDraftToken(nextToken);
  784. }
  785. await setActiveTab("responsible", true);
  786. };
  787. const handleTabChange = async (tab) => {
  788. if (tab === activeTab.value) return;
  789. if (tab === "responsible") {
  790. await goToResponsible();
  791. return;
  792. }
  793. await setActiveTab(tab, true);
  794. };
  795. const onAvatarChange = (file) => {
  796. avatarFile.value = file;
  797. };
  798. const onOKClick = async () => {
  799. if (
  800. isStudentUnderage.value &&
  801. activeTab.value === "student"
  802. ) {
  803. await goToResponsible();
  804. return;
  805. }
  806. await executeSubmit(() =>
  807. createStudent(buildPayload()),
  808. );
  809. };
  810. const saveRegistrationDraftToken = (token) => {
  811. registrationDraftToken.value = token;
  812. localStorage.setItem(
  813. REGISTRATION_DRAFT_TOKEN_STORAGE_KEY,
  814. token,
  815. );
  816. };
  817. const setActiveTab = async (
  818. tab,
  819. resetScroll = false,
  820. ) => {
  821. activeTab.value = tab;
  822. await nextTick();
  823. if (resetScroll) {
  824. scrollAreaRef.value?.setScrollPosition(
  825. "vertical",
  826. 0,
  827. 0,
  828. );
  829. }
  830. };
  831. watch(selectedState, (state) => {
  832. form.value.state_id = state?.value ?? null;
  833. });
  834. watch(selectedCity, (city) => {
  835. form.value.city_id = city?.value ?? null;
  836. });
  837. watch(responsibleSelectedState, (state) => {
  838. responsibleForm.value.state_id =
  839. state?.value ?? null;
  840. });
  841. watch(responsibleSelectedCity, (city) => {
  842. responsibleForm.value.city_id =
  843. city?.value ?? null;
  844. });
  845. watch(isStudentUnderage, (underage) => {
  846. if (
  847. !underage &&
  848. activeTab.value === "responsible"
  849. ) {
  850. activeTab.value = "student";
  851. }
  852. });
  853. watch(validationErrors, (errors) => {
  854. const fields = Object.keys(errors);
  855. if (
  856. fields.some(
  857. (field) => !field.startsWith("responsible"),
  858. )
  859. ) {
  860. activeTab.value = "student";
  861. return;
  862. }
  863. if (
  864. fields.some((field) =>
  865. field.startsWith("responsible"),
  866. )
  867. ) {
  868. activeTab.value = "responsible";
  869. }
  870. });
  871. </script>