Quellcode durchsuchen

feat: adiciona tab de responsavel

ebagabee vor 1 Monat
Ursprung
Commit
3e4452df25

+ 55 - 6
src/pages/students/components/EditStudentDialog.vue

@@ -14,11 +14,11 @@
         />
 
         <div v-show="currentTab === 'profile'">
-          <StudentDataTab :student="props.student" @saved="onStudentSaved" />
+          <StudentDataTab ref="studentDataTabRef" :student="props.student" />
         </div>
 
         <div v-show="currentTab === 'responsible'">
-          <ResponsibleTab :student-id="props.student.id" />
+          <ResponsibleTab ref="responsibleTabRef" :student-id="props.student.id" />
         </div>
 
         <div v-show="currentTab === 'contracts'">
@@ -37,17 +37,31 @@
       <q-separator />
 
       <q-card-actions align="right">
-        <q-btn outline color="primary" label="FECHAR" no-caps @click="onDialogCancel" />
+        <q-btn
+          outline
+          color="primary"
+          label="CANCELAR"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          v-if="saveableTabs.includes(currentTab)"
+          color="primary"
+          label="SALVAR"
+          :loading="saving"
+          @click="handleSave"
+        />
       </q-card-actions>
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, defineAsyncComponent } from "vue";
+import { ref, useTemplateRef, defineAsyncComponent } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { updateStudent } from "src/api/student";
 
 const StudentDataTab = defineAsyncComponent(
   () => import("src/pages/students/tabs/StudentDataTab.vue"),
@@ -77,7 +91,15 @@ defineEmits([...useDialogPluginComponent.emits]);
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
+const studentDataTabRef = useTemplateRef("studentDataTabRef");
+const responsibleTabRef = useTemplateRef("responsibleTabRef");
+
 const currentTab = ref("profile");
+const saveableTabs = ["profile", "responsible"];
+
+const { loading: saving, execute } = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+});
 
 const tabs = [
   { name: "profile", label: "Perfil do Aluno" },
@@ -87,7 +109,34 @@ const tabs = [
   { name: "media", label: "Mídias" },
 ];
 
-function onStudentSaved() {
-  onDialogOK(true);
+function mergePayloads(studentPayload, responsiblePayload) {
+  if (studentPayload instanceof FormData) {
+    Object.entries(responsiblePayload).forEach(([key, value]) => {
+      studentPayload.append(`responsible[${key}]`, value ?? "");
+    });
+    return studentPayload;
+  }
+  return { ...studentPayload, responsible: responsiblePayload };
+}
+
+async function handleSave() {
+  const [studentValid, responsibleValid] = await Promise.all([
+    studentDataTabRef.value?.validate() ?? true,
+    responsibleTabRef.value?.validate() ?? true,
+  ]);
+
+  if (!studentValid || !responsibleValid) return;
+
+  const studentPayload = studentDataTabRef.value.buildPayload();
+  const responsiblePayload = responsibleTabRef.value?.buildPayload();
+
+  await execute(() =>
+    updateStudent(
+      responsiblePayload
+        ? mergePayloads(studentPayload, responsiblePayload)
+        : studentPayload,
+      props.student.id,
+    ),
+  );
 }
 </script>

+ 53 - 90
src/pages/students/tabs/ResponsibleTab.vue

@@ -1,34 +1,32 @@
 <template>
   <div>
-    <div class="text-h6 q-mb-md">Dados do Responsável</div>
-
     <q-form ref="formRef" @submit="onSave">
       <div class="row q-col-gutter-sm">
         <DefaultInput
-          :model-value="String(props.studentId)"
-          label="ID do Aluno"
-          class="col-md-3 col-12"
-          readonly
+          v-model="form.name"
+          label="Nome"
+          class="col-6"
+          :rules="[inputRules.required]"
         />
 
         <DefaultInput
-          v-model="form.name"
-          label="Nome"
-          class="col-md-9 col-12"
+          v-model="form.degree"
+          label="Grau de Parentesco"
+          class="col-6"
           :rules="[inputRules.required]"
         />
 
         <DefaultInputDatePicker
           v-model="form.birth_date"
           label="Data de Nascimento"
-          class="col-md-4 col-12"
+          class="col-4"
           :rules="[inputRules.required]"
         />
 
         <DefaultInput
           v-model="form.cpf"
           label="CPF"
-          class="col-md-4 col-12"
+          class="col-4"
           :mask="masks.Brasil.cpf"
           :rules="[inputRules.required]"
         />
@@ -36,23 +34,16 @@
         <DefaultSelect
           v-model="form.gender"
           label="Gênero"
-          class="col-md-4 col-12"
+          class="col-4"
           emit-value
           map-options
           :options="genderOptions"
         />
 
-        <DefaultInput
-          v-model="form.degree"
-          label="Grau de Parentesco"
-          class="col-md-6 col-12"
-          :rules="[inputRules.required]"
-        />
-
         <DefaultInput
           v-model="form.email"
           label="E-mail"
-          class="col-md-6 col-12"
+          class="col-6"
           type="email"
           :rules="[inputRules.email]"
         />
@@ -60,93 +51,75 @@
         <DefaultInput
           v-model="form.phone"
           label="Telefone"
-          class="col-md-6 col-12"
+          class="col-6"
           :mask="masks.Brasil.celular"
           :rules="[inputRules.required]"
         />
 
         <DefaultCepInput
           v-model="form.postal_code"
-          class="col-md-6 col-12"
+          class="col-4"
           @rua="(v) => (form.street = v)"
           @bairro="(v) => (form.neighborhood = v)"
           @uf="(v) => stateSelectRef?.selectStateByCode(v)"
+          @cidade="(v) => citySelectRef?.selectCityByName(v)"
         />
 
         <DefaultInput
           v-model="form.street"
           label="Endereço"
-          class="col-md-6 col-12"
+          class="col-5"
         />
 
         <DefaultInput
           v-model="form.address_number"
           label="Número"
-          class="col-md-6 col-12"
+          class="col-3"
         />
 
         <DefaultInput
           v-model="form.neighborhood"
           label="Bairro"
-          class="col-md-6 col-12"
+          class="col-4"
         />
 
         <CitySelect
           ref="citySelectRef"
-          v-model="form.city"
+          v-model="selectedCity"
           label="Cidade"
-          class="col-md-6 col-12"
-          outlined
-          :state="form.state"
-          :initial-id="form.city_id ?? null"
+          class="col-4"
+          :state="selectedState"
         />
 
         <StateSelect
           ref="stateSelectRef"
-          v-model="form.state"
+          v-model="selectedState"
           label="Estado"
-          class="col-md-6 col-12"
-          outlined
-          :initial-id="form.state_id ?? null"
+          class="col-4"
         />
 
         <DefaultInput
           v-model="form.complement"
           label="Complemento"
-          class="col-md-6 col-12"
+          class="col-12"
         />
 
         <DefaultInput
           v-model="form.notes"
-          label="Observação"
+          label="Observações"
           class="col-12"
           type="textarea"
+          :input-style="{ minHeight: '120px' }"
           autogrow
         />
       </div>
 
-      <div class="row justify-end q-mt-md" style="gap: 8px">
-        <q-btn
-          outline
-          color="primary"
-          label="Cancelar"
-          no-caps
-          @click="onCancel"
-        />
-        <q-btn
-          color="primary"
-          label="Salvar"
-          no-caps
-          type="submit"
-          :loading="loading"
-        />
-      </div>
     </q-form>
   </div>
 </template>
 
 <script setup>
-import { ref, useTemplateRef, onMounted } from "vue";
+import { ref, watch, onMounted, useTemplateRef } from "vue";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
@@ -154,12 +127,7 @@ import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
 import CitySelect from "src/components/selects/CitySelect.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
 import { useInputRules } from "src/composables/useInputRules";
-import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import {
-  getStudentResponsible,
-  createStudentResponsible,
-  updateStudentResponsible,
-} from "src/api/studentResponsible";
+import { getStudentResponsible } from "src/api/studentResponsible";
 import masks from "src/helpers/masks";
 import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
 
@@ -175,7 +143,8 @@ const formRef = useTemplateRef("formRef");
 const stateSelectRef = useTemplateRef("stateSelectRef");
 const citySelectRef = useTemplateRef("citySelectRef");
 
-const responsibleId = ref(null);
+const selectedState = ref(null);
+const selectedCity = ref(null);
 
 const genderOptions = [
   { label: "Masculino", value: "male" },
@@ -196,28 +165,29 @@ const emptyForm = () => ({
   street: null,
   address_number: null,
   neighborhood: null,
-  city: null,
-  city_id: null,
-  state: null,
   state_id: null,
+  city_id: null,
   complement: null,
   notes: null,
 });
 
 const form = ref(emptyForm());
 
-const { loading, execute } = useSubmitHandler({ formRef });
+watch(selectedState, (state) => {
+  form.value.state_id = state?.value ?? null;
+});
+
+watch(selectedCity, (city) => {
+  form.value.city_id = city?.value ?? null;
+});
 
 async function loadResponsible() {
   try {
     const data = await getStudentResponsible(props.studentId);
     if (data) {
-      responsibleId.value = data.id;
       form.value = {
         name: data.name ?? null,
-        birth_date: data.birth_date
-          ? formatDateYMDtoDMY(data.birth_date)
-          : null,
+        birth_date: data.birth_date ? formatDateYMDtoDMY(data.birth_date) : null,
         cpf: data.cpf ?? null,
         gender: data.gender ?? "no_preference",
         degree: data.degree ?? null,
@@ -227,13 +197,18 @@ async function loadResponsible() {
         street: data.street ?? null,
         address_number: data.address_number ?? null,
         neighborhood: data.neighborhood ?? null,
-        city: null,
-        city_id: data.city_id ?? null,
-        state: null,
         state_id: data.state_id ?? null,
+        city_id: data.city_id ?? null,
         complement: data.complement ?? null,
         notes: data.notes ?? null,
       };
+
+      if (data.state_id) {
+        stateSelectRef.value?.selectStateById(data.state_id);
+      }
+      if (data.city_id) {
+        citySelectRef.value?.selectCityById(data.city_id);
+      }
     }
   } catch (error) {
     console.error("Failed to load responsible:", error);
@@ -242,7 +217,6 @@ async function loadResponsible() {
 
 function buildPayload() {
   return {
-    student_id: props.studentId,
     name: form.value.name,
     birth_date: form.value.birth_date
       ? formatDateDMYtoYMD(form.value.birth_date)
@@ -256,28 +230,17 @@ function buildPayload() {
     street: form.value.street,
     address_number: form.value.address_number,
     neighborhood: form.value.neighborhood,
-    city_id: form.value.city?.value ?? form.value.city_id ?? null,
-    state_id: form.value.state?.value ?? form.value.state_id ?? null,
+    city_id: form.value.city_id,
+    state_id: form.value.state_id,
     complement: form.value.complement,
     notes: form.value.notes,
   };
 }
 
-async function onSave() {
-  await execute(async () => {
-    const payload = buildPayload();
-    if (responsibleId.value) {
-      await updateStudentResponsible(responsibleId.value, payload);
-    } else {
-      const created = await createStudentResponsible(payload);
-      responsibleId.value = created.id;
-    }
-  });
-}
-
-function onCancel() {
-  loadResponsible();
-}
-
 onMounted(loadResponsible);
+
+defineExpose({
+  validate: () => formRef.value?.validate(),
+  buildPayload,
+});
 </script>

+ 4 - 21
src/pages/students/tabs/StudentDataTab.vue

@@ -121,15 +121,6 @@
       />
     </div>
 
-    <div class="row justify-end q-mt-md" style="gap: 8px">
-      <q-btn
-        color="primary"
-        label="Salvar"
-        no-caps
-        type="submit"
-        :loading="loading"
-      />
-    </div>
   </q-form>
 </template>
 
@@ -142,9 +133,7 @@ import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
 import AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
 import CitySelect from "src/components/selects/CitySelect.vue";
-import { useSubmitHandler } from "src/composables/useSubmitHandler";
 import { useInputRules } from "src/composables/useInputRules";
-import { updateStudent } from "src/api/student";
 import masks from "src/helpers/masks";
 import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
 
@@ -155,8 +144,6 @@ const props = defineProps({
   },
 });
 
-const emit = defineEmits(["saved"]);
-
 const { inputRules } = useInputRules();
 
 const formRef = useTemplateRef("formRef");
@@ -223,11 +210,6 @@ onMounted(() => {
   }
 });
 
-const { loading, execute } = useSubmitHandler({
-  formRef,
-  onSuccess: () => emit("saved"),
-});
-
 function onAvatarChange(file) {
   avatarFile.value = file;
 }
@@ -283,7 +265,8 @@ function buildPayload() {
   };
 }
 
-async function onSave() {
-  await execute(() => updateStudent(buildPayload(), props.student.id));
-}
+defineExpose({
+  validate: () => formRef.value?.validate(),
+  buildPayload,
+});
 </script>