Pārlūkot izejas kodu

feat: ajusta layout em dados de alunos

ebagabee 1 mēnesi atpakaļ
vecāks
revīzija
b58055057a

+ 2 - 2
src/components/defaults/DefaultCepInput.vue

@@ -3,7 +3,7 @@
     v-model="model"
     v-bind="$attrs"
     debounce="500"
-    :class="disable ?? 'no-pointer-events'"
+    :class="disable ? 'no-pointer-events' : ''"
     :disable="disable"
     :label
     :mask="masks.Brasil.cep"
@@ -45,7 +45,7 @@ const emit = defineEmits([
   "cepData",
 ]);
 
-const loading = ref(true);
+const loading = ref(false);
 
 watch(
   () => model.value,

+ 15 - 15
src/components/defaults/DefaultInputDatePicker.vue

@@ -117,19 +117,8 @@ watch(date, (value) => {
   treatedDate.value = formatDate(value);
 });
 
-watch(treatedDate, (value) => {
-  if (!value) {
-    date.value = null;
-    untreatedDate.value = null;
-    treatedDate.value = null;
-    activePanel.value = "date";
-    return;
-  }
-  date.value = unformatDate(value);
-});
-
 watch(
-  untreatedDate,
+  treatedDate,
   (value) => {
     if (!value) {
       date.value = null;
@@ -138,9 +127,20 @@ watch(
       activePanel.value = "date";
       return;
     }
-    date.value = value;
-    treatedDate.value = formatDate(value);
+    date.value = unformatDate(value);
   },
-  { immediate: true }
+  { immediate: true },
 );
+
+watch(untreatedDate, (value) => {
+  if (!value) {
+    date.value = null;
+    untreatedDate.value = null;
+    treatedDate.value = null;
+    activePanel.value = "date";
+    return;
+  }
+  date.value = value;
+  treatedDate.value = formatDate(value);
+});
 </script>

+ 136 - 0
src/components/shared/AvatarImageComponent.vue

@@ -0,0 +1,136 @@
+<template>
+  <div
+    class="avatar-wrapper relative-position"
+    :class="{ 'drag-over': isDragOver }"
+    @dragover.prevent="isDragOver = true"
+    @dragleave="isDragOver = false"
+    @drop.prevent="onDrop"
+  >
+    <div
+      v-if="!imageUrl"
+      class="full-width full-height flex flex-center column gap-xs no-image-state"
+      @click="openChangeDialog"
+    >
+      <q-icon name="add_photo_alternate" size="32px" color="grey-5" />
+      <span class="text-grey-6" style="font-size: 12px"
+        >adicione uma imagem</span
+      >
+    </div>
+
+    <template v-else>
+      <img :src="imageUrl" class="avatar-image" />
+
+      <div
+        class="actions-overlay absolute row no-wrap"
+        style="top: 6px; right: 6px; gap: 4px"
+      >
+        <q-btn
+          round
+          unelevated
+          size="xs"
+          icon="edit"
+          color="grey-8"
+          text-color="white"
+          @click.stop="openChangeDialog"
+        >
+          <q-tooltip>Trocar imagem</q-tooltip>
+        </q-btn>
+        <q-btn
+          round
+          unelevated
+          size="xs"
+          icon="delete"
+          color="negative"
+          text-color="white"
+          @click.stop="removeImage"
+        >
+          <q-tooltip>Remover imagem</q-tooltip>
+        </q-btn>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useQuasar } from "quasar";
+import ChangeImageDialog from "src/components/shared/ChangeImageDialog.vue";
+
+const emit = defineEmits(["update:file"]);
+
+const $q = useQuasar();
+const imageUrl = ref(null);
+const isDragOver = ref(false);
+
+function openChangeDialog() {
+  $q.dialog({ component: ChangeImageDialog }).onOk(({ file, previewUrl }) => {
+    imageUrl.value = previewUrl;
+    emit("update:file", file);
+  });
+}
+
+function removeImage() {
+  imageUrl.value = null;
+  emit("update:file", null);
+}
+
+function onDrop(event) {
+  isDragOver.value = false;
+  const file = event.dataTransfer.files[0];
+  if (file && file.type.startsWith("image/")) {
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      imageUrl.value = e.target.result;
+    };
+    reader.readAsDataURL(file);
+    emit("update:file", file);
+  }
+}
+
+defineExpose({
+  setImageUrl(url) {
+    imageUrl.value = url;
+  },
+});
+</script>
+
+<style scoped>
+.avatar-wrapper {
+  width: 174px;
+  height: 149px;
+  border-radius: 8px;
+  border: 2px dashed #ccc;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  cursor: pointer;
+  transition:
+    border-color 0.2s,
+    background-color 0.2s;
+}
+
+.avatar-wrapper:hover,
+.avatar-wrapper.drag-over {
+  border-color: #ff8340;
+  background-color: #fff5ef;
+}
+
+.no-image-state {
+  height: 100%;
+}
+
+.avatar-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+}
+
+.actions-overlay {
+  opacity: 0;
+  transition: opacity 0.2s;
+}
+
+.avatar-wrapper:hover .actions-overlay {
+  opacity: 1;
+}
+</style>

+ 92 - 0
src/components/shared/ChangeImageDialog.vue

@@ -0,0 +1,92 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="min-width: 400px">
+      <DefaultDialogHeader title="Trocar Imagem" @close="onDialogCancel" />
+
+      <q-card-section class="q-pt-none q-pb-sm">
+        <div class="text-caption text-grey-6 q-mb-xs">Personalizar</div>
+        <q-file
+          v-model="selectedFile"
+          accept="image/*"
+          outlined
+          dense
+          placeholder="Buscar no Desktop"
+          @update:model-value="onFileSelected"
+        >
+          <template #append>
+            <q-icon name="search" />
+          </template>
+        </q-file>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none">
+        <div class="text-caption text-grey-6 q-mb-xs">Pré - Visualização</div>
+        <div class="preview-area flex flex-center">
+          <img v-if="previewUrl" :src="previewUrl" class="preview-image" />
+        </div>
+      </q-card-section>
+
+      <q-card-actions align="right">
+        <q-btn
+          outline
+          color="primary"
+          label="Cancelar"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          color="primary"
+          label="Salvar"
+          :disable="!selectedFile"
+          @click="onSave"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const selectedFile = ref(null);
+const previewUrl = ref(null);
+
+function onFileSelected(file) {
+  if (!file) {
+    previewUrl.value = null;
+    return;
+  }
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    previewUrl.value = e.target.result;
+  };
+  reader.readAsDataURL(file);
+}
+
+function onSave() {
+  onDialogOK({ file: selectedFile.value, previewUrl: previewUrl.value });
+}
+</script>
+
+<style scoped>
+.preview-area {
+  width: 100%;
+  height: 200px;
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  background-color: #fafafa;
+  overflow: hidden;
+}
+
+.preview-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+</style>

+ 65 - 77
src/pages/students/components/AddEditStudentDialog.vue

@@ -2,62 +2,37 @@
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card
       class="q-dialog-plugin overflow-hidden"
-      style="width: 900px; max-width: 95vw"
+      style="width: 100%; max-width: 1100px"
     >
-      <DefaultDialogHeader title="Cadastro de Aluno" @close="onDialogCancel" />
+      <DefaultDialogHeader title="Cadastrar Aluno" @close="onDialogCancel" />
 
       <q-form ref="formRef" @submit="onOKClick">
         <q-card-section class="q-pt-none">
-          <div class="text-subtitle2 q-mb-sm">Dados do Aluno</div>
+          <div class="flex justify-center q-mb-md">
+            <AvatarImageComponent
+              ref="avatarRef"
+              @update:file="onAvatarChange"
+            />
+          </div>
 
           <div class="row q-col-gutter-sm">
             <DefaultInput
               v-model="form.name"
-              label="Nome do aluno"
-              class="col-md-5 col-12"
+              label="Nome do Aluno"
+              class="col-6"
               :rules="[inputRules.required]"
             />
 
             <DefaultInputDatePicker
               v-model="form.birthdate"
               label="Data de Nascimento"
-              class="col-md-5 col-12"
+              class="col-6"
             />
 
-            <div class="col-md-2 col-12 flex justify-center items-start">
-              <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
               v-model="form.cpf"
-              label="CPF / CNH"
-              class="col-md-6 col-12"
+              label="CPF"
+              class="col-6"
               :mask="masks.Brasil.cpf"
               :rules="[inputRules.cpf]"
             />
@@ -65,7 +40,7 @@
             <DefaultSelect
               v-model="form.gender"
               label="Gênero"
-              class="col-md-6 col-12"
+              class="col-6"
               emit-value
               map-options
               :options="genderOptions"
@@ -74,7 +49,7 @@
             <DefaultInput
               v-model="form.email"
               label="E-mail"
-              class="col-md-6 col-12"
+              class="col-6"
               type="email"
               :rules="[inputRules.email]"
             />
@@ -82,59 +57,60 @@
             <DefaultInput
               v-model="form.phone"
               label="Celular com DDD"
-              class="col-md-6 col-12"
+              class="col-6"
               :mask="masks.Brasil.celular"
             />
 
             <DefaultCepInput
               v-model="form.cep"
-              class="col-md-3 col-12"
-              :disable="false"
+              class="col-4"
               :rules="[inputRules.cep]"
               @rua="(v) => (form.address = v)"
               @bairro="(v) => (form.neighborhood = v)"
               @uf="(v) => stateSelectRef?.selectStateByCode(v)"
-              @numero="() => {}"
-              @complemento="() => {}"
+              @cidade="(v) => citySelectRef?.selectCityByName(v)"
             />
 
             <DefaultInput
               v-model="form.address"
               label="Endereço"
-              class="col-md-6 col-12"
+              class="col-5"
             />
 
             <DefaultInput
               v-model="form.address_number"
               label="Número"
-              class="col-md-3 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="selectedCity"
+              label="Cidade"
+              class="col-4"
+              :state="selectedState"
             />
 
             <StateSelect
               ref="stateSelectRef"
-              v-model="form.state"
+              v-model="selectedState"
               label="Estado"
-              class="col-md-6 col-12"
-              outlined
+              class="col-4"
             />
 
             <DefaultInput
               v-model="form.complement"
               label="Complemento"
-              class="col-md-6 col-12"
+              class="col-6"
             />
 
-            <DefaultInput
-              v-model="form.payer"
-              label="Pagador"
-              class="col-md-6 col-12"
-            />
+            <DefaultInput v-model="form.payer" label="Pagador" class="col-6" />
 
             <DefaultSelect
               v-model="form.how_found"
@@ -150,6 +126,7 @@
               label="Observações"
               class="col-12"
               type="textarea"
+              :input-style="{ minHeight: '120px' }"
               autogrow
             />
           </div>
@@ -175,7 +152,7 @@
 </template>
 
 <script setup>
-import { ref, useTemplateRef } from "vue";
+import { ref, watch, useTemplateRef } from "vue";
 import { useDialogPluginComponent } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -183,7 +160,9 @@ 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 AvatarImageComponent from "src/components/shared/AvatarImageComponent.vue";
 import StateSelect from "src/components/selects/StateSelect.vue";
+import CitySelect from "src/components/selects/CitySelect.vue";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
 import { createStudent } from "src/api/student";
@@ -198,10 +177,12 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
 const { inputRules } = useInputRules();
 
 const formRef = useTemplateRef("formRef");
-const fileInputRef = useTemplateRef("fileInputRef");
 const stateSelectRef = useTemplateRef("stateSelectRef");
-const avatarPreview = ref(null);
+const citySelectRef = useTemplateRef("citySelectRef");
+
 const avatarFile = ref(null);
+const selectedState = ref(null);
+const selectedCity = ref(null);
 
 const form = ref({
   name: null,
@@ -214,13 +195,22 @@ const form = ref({
   address: null,
   address_number: null,
   neighborhood: null,
-  state: null,
+  state_id: null,
+  city_id: null,
   complement: null,
   payer: null,
   how_found: null,
   notes: null,
 });
 
+watch(selectedState, (state) => {
+  form.value.state_id = state?.value ?? null;
+});
+
+watch(selectedCity, (city) => {
+  form.value.city_id = city?.value ?? null;
+});
+
 const genderOptions = ref([
   { label: "Prefiro não informar", value: "no_preference" },
   { label: "Masculino", value: "male" },
@@ -242,6 +232,10 @@ const { loading, execute } = useSubmitHandler({
   },
 });
 
+function onAvatarChange(file) {
+  avatarFile.value = file;
+}
+
 function buildPayload() {
   if (avatarFile.value) {
     const formData = new FormData();
@@ -257,8 +251,11 @@ function buildPayload() {
     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);
+    if (form.value.state_id) {
+      formData.append("state_id", form.value.state_id);
+    }
+    if (form.value.city_id) {
+      formData.append("city_id", form.value.city_id);
     }
     formData.append("complement", form.value.complement ?? "");
     formData.append("payer_name", form.value.payer ?? "");
@@ -270,7 +267,9 @@ function buildPayload() {
 
   return {
     name: form.value.name,
-    birth_date: form.value.birthdate ? formatDateDMYtoYMD(form.value.birthdate) : null,
+    birth_date: form.value.birthdate
+      ? formatDateDMYtoYMD(form.value.birthdate)
+      : null,
     document_number: form.value.cpf,
     gender: form.value.gender,
     email: form.value.email || null,
@@ -279,7 +278,8 @@ function buildPayload() {
     street: form.value.address,
     address_number: form.value.address_number,
     neighborhood: form.value.neighborhood,
-    state_id: form.value.state?.value ?? null,
+    state_id: form.value.state_id,
+    city_id: form.value.city_id,
     complement: form.value.complement,
     payer_name: form.value.payer,
     how_did_you_know_us: form.value.how_found,
@@ -287,18 +287,6 @@ function buildPayload() {
   };
 }
 
-function triggerFileInput() {
-  fileInputRef.value?.click();
-}
-
-function onAvatarChange(event) {
-  const file = event.target.files[0];
-  if (file) {
-    avatarFile.value = file;
-    avatarPreview.value = URL.createObjectURL(file);
-  }
-}
-
 async function onOKClick() {
   await execute(() => createStudent(buildPayload()));
 }

+ 1 - 1
src/pages/students/components/EditStudentDialog.vue

@@ -2,7 +2,7 @@
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card
       class="q-dialog-plugin overflow-hidden"
-      style="width: 1000px; max-width: 97vw"
+      style="width: 100%; max-width: 1100px"
     >
       <DefaultDialogHeader title="Dados do Aluno" @close="onDialogCancel" />
 

+ 85 - 62
src/pages/students/tabs/StudentDataTab.vue

@@ -1,54 +1,34 @@
 <template>
   <q-form ref="formRef" @submit="onSave">
+    <div class="flex justify-center q-mb-md">
+      <AvatarImageComponent ref="avatarRef" @update:file="onAvatarChange" />
+    </div>
+
     <div class="row q-col-gutter-sm">
       <DefaultInput
         v-model="form.name"
-        label="Nome do aluno"
-        class="col-md-5 col-12"
+        label="Nome do Aluno"
+        class="col-6"
+        :rules="[inputRules.required]"
       />
 
       <DefaultInputDatePicker
         v-model="form.birthdate"
         label="Data de Nascimento"
-        class="col-md-5 col-12"
+        class="col-6"
       />
 
-      <div class="col-md-2 col-12 flex justify-center items-start">
-        <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
         v-model="form.cpf"
         label="CPF / CNH"
-        class="col-md-6 col-12"
+        class="col-6"
         :mask="masks.Brasil.cpf"
       />
 
       <DefaultSelect
         v-model="form.gender"
         label="Gênero"
-        class="col-md-6 col-12"
+        class="col-6"
         emit-value
         map-options
         :options="genderOptions"
@@ -57,61 +37,69 @@
       <DefaultInput
         v-model="form.email"
         label="E-mail"
-        class="col-md-6 col-12"
+        class="col-6"
+        type="email"
       />
 
       <DefaultInput
         v-model="form.phone"
         label="Celular com DDD"
-        class="col-md-6 col-12"
+        class="col-6"
         :mask="masks.Brasil.celular"
       />
 
       <DefaultCepInput
         v-model="form.cep"
-        class="col-md-3 col-12"
+        class="col-4"
         @rua="(v) => (form.address = v)"
         @bairro="(v) => (form.neighborhood = v)"
         @uf="(v) => stateSelectRef?.selectStateByCode(v)"
+        @cidade="(v) => citySelectRef?.selectCityByName(v)"
       />
 
       <DefaultInput
         v-model="form.address"
         label="Endereço"
-        class="col-md-6 col-12"
+        class="col-5"
       />
 
       <DefaultInput
         v-model="form.address_number"
         label="Número"
-        class="col-md-3 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="selectedCity"
+        label="Cidade"
+        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="props.student.state_id ?? null"
+        class="col-4"
       />
 
       <DefaultInput
         v-model="form.complement"
         label="Complemento"
-        class="col-md-6 col-12"
+        class="col-6"
       />
 
       <DefaultInput
         v-model="form.payer"
         label="Pagador"
-        class="col-md-6 col-12"
+        class="col-6"
       />
 
       <DefaultSelect
@@ -128,24 +116,34 @@
         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 color="primary" label="Salvar" no-caps type="submit" :loading="loading" />
+      <q-btn
+        color="primary"
+        label="Salvar"
+        no-caps
+        type="submit"
+        :loading="loading"
+      />
     </div>
   </q-form>
 </template>
 
 <script setup>
-import { ref, useTemplateRef } 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";
 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";
@@ -159,12 +157,16 @@ const props = defineProps({
 
 const emit = defineEmits(["saved"]);
 
+const { inputRules } = useInputRules();
+
 const formRef = useTemplateRef("formRef");
-const fileInputRef = useTemplateRef("fileInputRef");
+const avatarRef = useTemplateRef("avatarRef");
 const stateSelectRef = useTemplateRef("stateSelectRef");
+const citySelectRef = useTemplateRef("citySelectRef");
 
-const avatarPreview = ref(props.student.photo_url ?? null);
 const avatarFile = ref(null);
+const selectedState = ref(null);
+const selectedCity = ref(null);
 
 const genderOptions = [
   { label: "Prefiro não informar", value: "no_preference" },
@@ -182,7 +184,9 @@ const howFoundOptions = [
 
 const form = ref({
   name: props.student.name ?? null,
-  birthdate: props.student.birth_date ? formatDateYMDtoDMY(props.student.birth_date) : null,
+  birthdate: props.student.birth_date
+    ? formatDateYMDtoDMY(props.student.birth_date)
+    : null,
   cpf: props.student.document_number ?? null,
   gender: props.student.gender ?? "no_preference",
   email: props.student.email ?? null,
@@ -191,28 +195,41 @@ const form = ref({
   address: props.student.street ?? null,
   address_number: props.student.address_number ?? null,
   neighborhood: props.student.neighborhood ?? null,
-  state: null,
+  state_id: props.student.state_id ?? null,
+  city_id: props.student.city_id ?? null,
   complement: props.student.complement ?? null,
   payer: props.student.payer_name ?? null,
   how_found: props.student.how_did_you_know_us ?? null,
   notes: props.student.notes ?? null,
 });
 
+watch(selectedState, (state) => {
+  form.value.state_id = state?.value ?? null;
+});
+
+watch(selectedCity, (city) => {
+  form.value.city_id = city?.value ?? null;
+});
+
+onMounted(() => {
+  if (props.student.photo_url) {
+    avatarRef.value?.setImageUrl(props.student.photo_url);
+  }
+  if (props.student.state_id) {
+    stateSelectRef.value?.selectStateById(props.student.state_id);
+  }
+  if (props.student.city_id) {
+    citySelectRef.value?.selectCityById(props.student.city_id);
+  }
+});
+
 const { loading, execute } = useSubmitHandler({
   formRef,
   onSuccess: () => emit("saved"),
 });
 
-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 onAvatarChange(file) {
+  avatarFile.value = file;
 }
 
 function buildPayload() {
@@ -230,8 +247,11 @@ function buildPayload() {
     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);
+    if (form.value.state_id) {
+      formData.append("state_id", form.value.state_id);
+    }
+    if (form.value.city_id) {
+      formData.append("city_id", form.value.city_id);
     }
     formData.append("complement", form.value.complement ?? "");
     formData.append("payer_name", form.value.payer ?? "");
@@ -243,7 +263,9 @@ function buildPayload() {
 
   return {
     name: form.value.name,
-    birth_date: form.value.birthdate ? formatDateDMYtoYMD(form.value.birthdate) : null,
+    birth_date: form.value.birthdate
+      ? formatDateDMYtoYMD(form.value.birthdate)
+      : null,
     document_number: form.value.cpf,
     gender: form.value.gender,
     email: form.value.email || null,
@@ -252,7 +274,8 @@ function buildPayload() {
     street: form.value.address,
     address_number: form.value.address_number,
     neighborhood: form.value.neighborhood,
-    state_id: form.value.state?.value ?? null,
+    state_id: form.value.state_id,
+    city_id: form.value.city_id,
     complement: form.value.complement,
     payer_name: form.value.payer,
     how_did_you_know_us: form.value.how_found,