|
@@ -0,0 +1,366 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ class="q-dialog-plugin overflow-hidden"
|
|
|
|
|
+ style="width: 900px; max-width: 95vw"
|
|
|
|
|
+ >
|
|
|
|
|
+ <DefaultDialogHeader title="Editar dados do Aluno" @close="onDialogCancel" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Tab navigation -->
|
|
|
|
|
+ <div class="q-px-md q-pt-sm q-pb-xs">
|
|
|
|
|
+ <CustomTabComponent v-model:active-tab="activeTab" :tabs="tabs" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ─── TAB: PERFIL DO ALUNO ─── -->
|
|
|
|
|
+ <q-form v-if="activeTab === 'perfil'" 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>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.name"
|
|
|
|
|
+ label="Nome do aluno"
|
|
|
|
|
+ class="col-md-5 col-12"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInputDatePicker
|
|
|
|
|
+ v-model="form.birthdate"
|
|
|
|
|
+ label="Data de Nascimento"
|
|
|
|
|
+ class="col-md-5 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <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"
|
|
|
|
|
+ :mask="masks.Brasil.cpf"
|
|
|
|
|
+ :rules="[inputRules.cpf]"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultSelect
|
|
|
|
|
+ v-model="form.gender"
|
|
|
|
|
+ label="Gênero"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ emit-value
|
|
|
|
|
+ map-options
|
|
|
|
|
+ :options="genderOptions"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.email"
|
|
|
|
|
+ label="E-mail"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ :rules="[inputRules.email]"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.phone"
|
|
|
|
|
+ label="Celular com DDD"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ :mask="masks.Brasil.celular"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.cep"
|
|
|
|
|
+ label="CEP"
|
|
|
|
|
+ class="col-md-3 col-12"
|
|
|
|
|
+ :mask="masks.Brasil.cep"
|
|
|
|
|
+ :rules="[inputRules.cep]"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.address"
|
|
|
|
|
+ label="Endereço"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.address_number"
|
|
|
|
|
+ label="Número"
|
|
|
|
|
+ class="col-md-3 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.neighborhood"
|
|
|
|
|
+ label="Bairro"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <StateSelect
|
|
|
|
|
+ ref="stateSelectRef"
|
|
|
|
|
+ v-model="form.state"
|
|
|
|
|
+ label="Cidade / Estado"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ outlined
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.complement"
|
|
|
|
|
+ label="Complemento"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.payer"
|
|
|
|
|
+ label="Pagador"
|
|
|
|
|
+ class="col-md-6 col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultSelect
|
|
|
|
|
+ v-model="form.how_found"
|
|
|
|
|
+ label="Como nos conheceu?"
|
|
|
|
|
+ class="col-12"
|
|
|
|
|
+ emit-value
|
|
|
|
|
+ map-options
|
|
|
|
|
+ :options="howFoundOptions"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.notes"
|
|
|
|
|
+ label="Observações"
|
|
|
|
|
+ class="col-12"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ autogrow
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-separator />
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-actions align="right">
|
|
|
|
|
+ <q-btn outline color="primary" label="CANCELAR" no-caps @click="onDialogCancel" />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ label="SALVAR"
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </q-form>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ─── TAB: RESPONSÁVEL ─── -->
|
|
|
|
|
+ <template v-else-if="activeTab === 'responsavel'">
|
|
|
|
|
+ <q-card-section style="min-height: 300px" class="flex flex-center column q-gutter-sm">
|
|
|
|
|
+ <q-icon name="mdi-account-supervisor-outline" size="64px" color="grey-4" />
|
|
|
|
|
+ <div class="text-subtitle1 text-grey-6">Responsável</div>
|
|
|
|
|
+ <div class="text-caption text-grey-5 text-center">
|
|
|
|
|
+ Funcionalidade em desenvolvimento.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ <q-separator />
|
|
|
|
|
+ <q-card-actions align="right">
|
|
|
|
|
+ <q-btn outline color="primary" label="FECHAR" no-caps @click="onDialogCancel" />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ─── TAB: CONTATOS ─── -->
|
|
|
|
|
+ <template v-else-if="activeTab === 'contatos'">
|
|
|
|
|
+ <q-card-section style="min-height: 300px" class="flex flex-center column q-gutter-sm">
|
|
|
|
|
+ <q-icon name="mdi-contacts-outline" size="64px" color="grey-4" />
|
|
|
|
|
+ <div class="text-subtitle1 text-grey-6">Contatos</div>
|
|
|
|
|
+ <div class="text-caption text-grey-5 text-center">
|
|
|
|
|
+ Funcionalidade em desenvolvimento.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ <q-separator />
|
|
|
|
|
+ <q-card-actions align="right">
|
|
|
|
|
+ <q-btn outline color="primary" label="FECHAR" no-caps @click="onDialogCancel" />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ─── TAB: HISTÓRICO ─── -->
|
|
|
|
|
+ <template v-else-if="activeTab === 'historico'">
|
|
|
|
|
+ <q-card-section style="min-height: 300px" class="flex flex-center column q-gutter-sm">
|
|
|
|
|
+ <q-icon name="mdi-history" size="64px" color="grey-4" />
|
|
|
|
|
+ <div class="text-subtitle1 text-grey-6">Histórico</div>
|
|
|
|
|
+ <div class="text-caption text-grey-5 text-center">
|
|
|
|
|
+ Funcionalidade em desenvolvimento.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ <q-separator />
|
|
|
|
|
+ <q-card-actions align="right">
|
|
|
|
|
+ <q-btn outline color="primary" label="FECHAR" no-caps @click="onDialogCancel" />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ─── TAB: MÍDIAS ─── -->
|
|
|
|
|
+ <template v-else-if="activeTab === 'midias'">
|
|
|
|
|
+ <q-card-section style="min-height: 300px" class="flex flex-center column q-gutter-sm">
|
|
|
|
|
+ <q-icon name="mdi-image-multiple-outline" size="64px" color="grey-4" />
|
|
|
|
|
+ <div class="text-subtitle1 text-grey-6">Mídias</div>
|
|
|
|
|
+ <div class="text-caption text-grey-5 text-center">
|
|
|
|
|
+ Funcionalidade em desenvolvimento.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ <q-separator />
|
|
|
|
|
+ <q-card-actions align="right">
|
|
|
|
|
+ <q-btn outline color="primary" label="FECHAR" no-caps @click="onDialogCancel" />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </q-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, useTemplateRef, onMounted } 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 StateSelect from "src/components/selects/StateSelect.vue";
|
|
|
|
|
+import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
|
|
|
|
|
+import { useInputRules } from "src/composables/useInputRules";
|
|
|
|
|
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
|
|
+import { updateStudent } from "src/api/student";
|
|
|
|
|
+import masks from "src/helpers/masks";
|
|
|
|
|
+import { formatDateDMYtoYMD, formatDateYMDtoDMY } from "src/helpers/utils";
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ student: {
|
|
|
|
|
+ type: Object,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
|
|
+
|
|
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|
|
|
|
+ useDialogPluginComponent();
|
|
|
|
|
+
|
|
|
|
|
+const { inputRules } = useInputRules();
|
|
|
|
|
+
|
|
|
|
|
+const formRef = useTemplateRef("formRef");
|
|
|
|
|
+const fileInputRef = useTemplateRef("fileInputRef");
|
|
|
|
|
+const stateSelectRef = useTemplateRef("stateSelectRef");
|
|
|
|
|
+const avatarPreview = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const activeTab = ref("perfil");
|
|
|
|
|
+
|
|
|
|
|
+const tabs = [
|
|
|
|
|
+ { name: "perfil", label: "Perfil do Aluno" },
|
|
|
|
|
+ { name: "responsavel", label: "Responsável" },
|
|
|
|
|
+ { name: "contatos", label: "Contatos" },
|
|
|
|
|
+ { name: "historico", label: "Histórico" },
|
|
|
|
|
+ { name: "midias", label: "Mídias" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const genderOptions = [
|
|
|
|
|
+ { label: "Prefiro não informar", value: "no_preference" },
|
|
|
|
|
+ { label: "Masculino", value: "male" },
|
|
|
|
|
+ { label: "Feminino", value: "female" },
|
|
|
|
|
+ { label: "Outro", value: "other" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const howFoundOptions = [
|
|
|
|
|
+ { label: "Indicação", value: "referral" },
|
|
|
|
|
+ { label: "Redes Sociais", value: "social_media" },
|
|
|
|
|
+ { label: "Google", value: "google" },
|
|
|
|
|
+ { label: "Outro", value: "other" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const form = ref({
|
|
|
|
|
+ name: props.student.name ?? 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,
|
|
|
|
|
+ phone: props.student.phone ?? null,
|
|
|
|
|
+ cep: props.student.postal_code ?? null,
|
|
|
|
|
+ address: props.student.street ?? null,
|
|
|
|
|
+ address_number: props.student.address_number ?? null,
|
|
|
|
|
+ neighborhood: props.student.neighborhood ?? null,
|
|
|
|
|
+ state: 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,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const { loading, execute } = useSubmitHandler({
|
|
|
|
|
+ formRef,
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ onDialogOK(true);
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function buildPayload() {
|
|
|
|
|
+ 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,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function triggerFileInput() {
|
|
|
|
|
+ fileInputRef.value?.click();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onAvatarChange(event) {
|
|
|
|
|
+ const file = event.target.files[0];
|
|
|
|
|
+ if (file) {
|
|
|
|
|
+ avatarPreview.value = URL.createObjectURL(file);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onSave() {
|
|
|
|
|
+ await execute(() => updateStudent(buildPayload(), props.student.id));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ if (props.student.state_id) {
|
|
|
|
|
+ stateSelectRef.value?.selectStateById(props.student.state_id);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|