ソースを参照

feat: adiciona campos para aluno

ebagabee 2 週間 前
コミット
4fecc7a99c

+ 7 - 2
src/api/student.js

@@ -10,8 +10,13 @@ export const createStudent = async (student) => {
   return data.payload;
 };
 
-export const updateStudent = async (student, id) => {
-  const { data } = await api.put(`/student/${id}`, student);
+export const updateStudent = async (payload, id) => {
+  if (payload instanceof FormData) {
+    payload.append('_method', 'PUT');
+    const { data } = await api.post(`/student/${id}`, payload);
+    return data.payload;
+  }
+  const { data } = await api.put(`/student/${id}`, payload);
   return data.payload;
 };
 

+ 6 - 1
src/components/defaults/DefaultCepInput.vue

@@ -4,6 +4,7 @@
     v-bind="$attrs"
     debounce="500"
     :class="disable ?? 'no-pointer-events'"
+    :disable="disable"
     :label
     :mask="masks.Brasil.cep"
     :loading
@@ -22,11 +23,15 @@ const model = defineModel({
   type: [String, Number],
 });
 
-const { label } = defineProps({
+const { label, disable } = defineProps({
   label: {
     type: String,
     default: "CEP",
   },
+  disable: {
+    type: Boolean,
+    default: undefined,
+  },
 });
 
 const emit = defineEmits([

+ 37 - 3
src/pages/students/components/AddEditStudentDialog.vue

@@ -86,12 +86,16 @@
               :mask="masks.Brasil.celular"
             />
 
-            <DefaultInput
+            <DefaultCepInput
               v-model="form.cep"
-              label="CEP"
               class="col-md-3 col-12"
-              :mask="masks.Brasil.cep"
+              :disable="false"
               :rules="[inputRules.cep]"
+              @rua="(v) => (form.address = v)"
+              @bairro="(v) => (form.neighborhood = v)"
+              @uf="(v) => stateSelectRef?.selectStateByCode(v)"
+              @numero="() => {}"
+              @complemento="() => {}"
             />
 
             <DefaultInput
@@ -113,6 +117,7 @@
             />
 
             <StateSelect
+              ref="stateSelectRef"
               v-model="form.state"
               label="Estado"
               class="col-md-6 col-12"
@@ -177,6 +182,7 @@ import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.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";
+import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
@@ -193,7 +199,9 @@ const { inputRules } = useInputRules();
 
 const formRef = useTemplateRef("formRef");
 const fileInputRef = useTemplateRef("fileInputRef");
+const stateSelectRef = useTemplateRef("stateSelectRef");
 const avatarPreview = ref(null);
+const avatarFile = ref(null);
 
 const form = ref({
   name: null,
@@ -235,6 +243,31 @@ const { loading, execute } = useSubmitHandler({
 });
 
 function buildPayload() {
+  if (avatarFile.value) {
+    const formData = new FormData();
+    formData.append("name", form.value.name ?? "");
+    if (form.value.birthdate) {
+      formData.append("birth_date", formatDateDMYtoYMD(form.value.birthdate));
+    }
+    formData.append("document_number", form.value.cpf ?? "");
+    formData.append("gender", form.value.gender ?? "");
+    formData.append("email", form.value.email ?? "");
+    formData.append("phone", form.value.phone ?? "");
+    formData.append("postal_code", form.value.cep ?? "");
+    formData.append("street", form.value.address ?? "");
+    formData.append("address_number", form.value.address_number ?? "");
+    formData.append("neighborhood", form.value.neighborhood ?? "");
+    if (form.value.state?.value) {
+      formData.append("state_id", form.value.state.value);
+    }
+    formData.append("complement", form.value.complement ?? "");
+    formData.append("payer_name", form.value.payer ?? "");
+    formData.append("how_did_you_know_us", form.value.how_found ?? "");
+    formData.append("notes", form.value.notes ?? "");
+    formData.append("avatar", avatarFile.value);
+    return formData;
+  }
+
   return {
     name: form.value.name,
     birth_date: form.value.birthdate ? formatDateDMYtoYMD(form.value.birthdate) : null,
@@ -261,6 +294,7 @@ function triggerFileInput() {
 function onAvatarChange(event) {
   const file = event.target.files[0];
   if (file) {
+    avatarFile.value = file;
     avatarPreview.value = URL.createObjectURL(file);
   }
 }

+ 112 - 36
src/pages/students/components/EditStudentDialog.vue

@@ -6,8 +6,7 @@
     >
       <DefaultDialogHeader title="Dados do Aluno" @close="onDialogCancel" />
 
-      <!-- ─── PERFIL DO ALUNO ─── -->
-      <div>
+      <q-form ref="formRef" @submit="onSave">
         <q-card-section class="q-pt-sm" style="max-height: 65vh; overflow-y: auto">
           <div class="text-subtitle2 q-mb-sm">Dados do Aluno</div>
 
@@ -16,21 +15,37 @@
               v-model="form.name"
               label="Nome do aluno"
               class="col-md-5 col-12"
-              readonly
             />
 
             <DefaultInputDatePicker
               v-model="form.birthdate"
               label="Data de Nascimento"
               class="col-md-5 col-12"
-              readonly
             />
 
             <div class="col-md-2 col-12 flex justify-center items-start">
-              <q-avatar size="72px" color="grey-3">
-                <img v-if="avatarPreview" :src="avatarPreview" />
-                <q-icon v-else name="mdi-account" size="42px" color="grey-6" />
-              </q-avatar>
+              <div style="position: relative; display: inline-block">
+                <q-avatar size="72px" color="grey-3">
+                  <img v-if="avatarPreview" :src="avatarPreview" />
+                  <q-icon v-else name="mdi-account" size="42px" color="grey-6" />
+                </q-avatar>
+                <q-btn
+                  round
+                  dense
+                  color="primary"
+                  icon="mdi-camera"
+                  size="xs"
+                  style="position: absolute; bottom: 0; right: 0"
+                  @click="triggerFileInput"
+                />
+                <input
+                  ref="fileInputRef"
+                  type="file"
+                  accept="image/*"
+                  style="display: none"
+                  @change="onAvatarChange"
+                />
+              </div>
             </div>
 
             <DefaultInput
@@ -38,7 +53,6 @@
               label="CPF / CNH"
               class="col-md-6 col-12"
               :mask="masks.Brasil.cpf"
-              readonly
             />
 
             <DefaultSelect
@@ -48,14 +62,12 @@
               emit-value
               map-options
               :options="genderOptions"
-              disable
             />
 
             <DefaultInput
               v-model="form.email"
               label="E-mail"
               class="col-md-6 col-12"
-              readonly
             />
 
             <DefaultInput
@@ -63,59 +75,56 @@
               label="Celular com DDD"
               class="col-md-6 col-12"
               :mask="masks.Brasil.celular"
-              readonly
             />
 
-            <DefaultInput
+            <DefaultCepInput
               v-model="form.cep"
-              label="CEP"
               class="col-md-3 col-12"
-              :mask="masks.Brasil.cep"
-              readonly
+              :disable="false"
+              @rua="(v) => (form.address = v)"
+              @bairro="(v) => (form.neighborhood = v)"
+              @uf="(v) => stateSelectRef?.selectStateByCode(v)"
+              @numero="() => {}"
+              @complemento="() => {}"
             />
 
             <DefaultInput
               v-model="form.address"
               label="Endereço"
               class="col-md-6 col-12"
-              readonly
             />
 
             <DefaultInput
               v-model="form.address_number"
               label="Número"
               class="col-md-3 col-12"
-              readonly
             />
 
             <DefaultInput
               v-model="form.neighborhood"
               label="Bairro"
               class="col-md-6 col-12"
-              readonly
             />
 
             <StateSelect
               ref="stateSelectRef"
               v-model="form.state"
-              label="Cidade / Estado"
+              label="Estado"
               class="col-md-6 col-12"
               outlined
-              disable
+              :initial-id="props.student.state_id ?? null"
             />
 
             <DefaultInput
               v-model="form.complement"
               label="Complemento"
               class="col-md-6 col-12"
-              readonly
             />
 
             <DefaultInput
               v-model="form.payer"
               label="Pagador"
               class="col-md-6 col-12"
-              readonly
             />
 
             <DefaultSelect
@@ -125,7 +134,6 @@
               emit-value
               map-options
               :options="howFoundOptions"
-              disable
             />
 
             <DefaultInput
@@ -134,7 +142,6 @@
               class="col-12"
               type="textarea"
               autogrow
-              readonly
             />
           </div>
         </q-card-section>
@@ -142,25 +149,28 @@
         <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" no-caps @click="onDialogCancel" />
+          <q-btn color="primary" label="SALVAR" no-caps type="submit" :loading="loading" />
         </q-card-actions>
-      </div>
+      </q-form>
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, useTemplateRef, onMounted } from "vue";
-
+import { ref, useTemplateRef } from "vue";
 import { useDialogPluginComponent } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.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";
+import DefaultCepInput from "src/components/defaults/DefaultCepInput.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { updateStudent } from "src/api/student";
 import masks from "src/helpers/masks";
-import { formatDateYMDtoDMY } from "src/helpers/utils";
+import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
 
 const props = defineProps({
   student: {
@@ -171,11 +181,15 @@ const props = defineProps({
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogCancel } =
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
+const formRef = useTemplateRef("formRef");
+const fileInputRef = useTemplateRef("fileInputRef");
 const stateSelectRef = useTemplateRef("stateSelectRef");
-const avatarPreview = ref(null);
+
+const avatarPreview = ref(props.student.photo_url ?? null);
+const avatarFile = ref(null);
 
 const genderOptions = [
   { label: "Prefiro não informar", value: "no_preference" },
@@ -211,9 +225,71 @@ const form = ref({
   notes: props.student.notes ?? null,
 });
 
-onMounted(() => {
-  if (props.student.state_id) {
-    stateSelectRef.value?.selectStateById(props.student.state_id);
-  }
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: () => {
+    onDialogOK(true);
+  },
 });
+
+function triggerFileInput() {
+  fileInputRef.value?.click();
+}
+
+function onAvatarChange(event) {
+  const file = event.target.files[0];
+  if (file) {
+    avatarFile.value = file;
+    avatarPreview.value = URL.createObjectURL(file);
+  }
+}
+
+function buildPayload() {
+  if (avatarFile.value) {
+    const formData = new FormData();
+    formData.append("name", form.value.name ?? "");
+    if (form.value.birthdate) {
+      formData.append("birth_date", formatDateDMYtoYMD(form.value.birthdate));
+    }
+    formData.append("document_number", form.value.cpf ?? "");
+    formData.append("gender", form.value.gender ?? "");
+    formData.append("email", form.value.email ?? "");
+    formData.append("phone", form.value.phone ?? "");
+    formData.append("postal_code", form.value.cep ?? "");
+    formData.append("street", form.value.address ?? "");
+    formData.append("address_number", form.value.address_number ?? "");
+    formData.append("neighborhood", form.value.neighborhood ?? "");
+    if (form.value.state?.value) {
+      formData.append("state_id", form.value.state.value);
+    }
+    formData.append("complement", form.value.complement ?? "");
+    formData.append("payer_name", form.value.payer ?? "");
+    formData.append("how_did_you_know_us", form.value.how_found ?? "");
+    formData.append("notes", form.value.notes ?? "");
+    formData.append("avatar", avatarFile.value);
+    return formData;
+  }
+
+  return {
+    name: form.value.name,
+    birth_date: form.value.birthdate ? formatDateDMYtoYMD(form.value.birthdate) : null,
+    document_number: form.value.cpf,
+    gender: form.value.gender,
+    email: form.value.email || null,
+    phone: form.value.phone,
+    postal_code: form.value.cep,
+    street: form.value.address,
+    address_number: form.value.address_number,
+    neighborhood: form.value.neighborhood,
+    state_id: form.value.state?.value ?? null,
+    complement: form.value.complement,
+    payer_name: form.value.payer,
+    how_did_you_know_us: form.value.how_found,
+    notes: form.value.notes,
+  };
+}
+
+async function onSave() {
+  await execute(() => updateStudent(buildPayload(), props.student.id));
+}
 </script>