Procházet zdrojové kódy

feat: adiciona listagem de responsaveis

ebagabee před 1 měsícem
rodič
revize
c7dec7c4ad

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

@@ -128,19 +128,22 @@ watch(treatedDate, (value) => {
   date.value = unformatDate(value);
 });
 
-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);
-  },
-  { 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);
+});
+
+if (untreatedDate.value) {
+  date.value = untreatedDate.value;
+  treatedDate.value = formatDate(untreatedDate.value);
+} else if (treatedDate.value) {
+  date.value = unformatDate(treatedDate.value);
+}
 </script>

+ 4 - 31
src/pages/students/components/EditStudentDialog.vue

@@ -18,10 +18,7 @@
         </div>
 
         <div v-show="currentTab === 'responsible'">
-          <ResponsibleTab
-            ref="responsibleTabRef"
-            :student-id="props.student.id"
-          />
+          <ResponsibleTab :student-id="props.student.id" />
         </div>
 
         <div v-show="currentTab === 'contracts'">
@@ -94,7 +91,6 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
 const studentDataTabRef = useTemplateRef("studentDataTabRef");
-const responsibleTabRef = useTemplateRef("responsibleTabRef");
 
 const currentTab = ref("profile");
 
@@ -110,34 +106,11 @@ const tabs = [
   { name: "media", label: "Mídias" },
 ];
 
-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 valid = await (studentDataTabRef.value?.validate() ?? true);
+  if (!valid) return;
 
   const studentPayload = studentDataTabRef.value.buildPayload();
-  const responsiblePayload = responsibleTabRef.value?.buildPayload();
-
-  await execute(() =>
-    updateStudent(
-      responsiblePayload
-        ? mergePayloads(studentPayload, responsiblePayload)
-        : studentPayload,
-      props.student.id,
-    ),
-  );
+  await execute(() => updateStudent(studentPayload, props.student.id));
 }
 </script>

+ 286 - 0
src/pages/students/components/ResponsibleDialog.vue

@@ -0,0 +1,286 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 100%; max-width: 900px"
+    >
+      <DefaultDialogHeader
+        :title="props.responsible ? 'Editar Responsável' : 'Novo Responsável'"
+        @close="onDialogCancel"
+      />
+
+      <q-card-section class="q-pt-sm" style="height: 65vh; overflow-y: auto">
+        <q-form ref="formRef">
+          <div class="row q-col-gutter-sm">
+            <DefaultInput
+              v-model="form.name"
+              label="Nome"
+              class="col-6"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              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-4"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultInput
+              v-model="form.cpf"
+              label="CPF"
+              class="col-4"
+              :mask="masks.Brasil.cpf"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultSelect
+              v-model="form.gender"
+              label="Gênero"
+              class="col-4"
+              emit-value
+              map-options
+              :options="genderOptions"
+            />
+
+            <DefaultInput
+              v-model="form.email"
+              label="E-mail"
+              class="col-6"
+              type="email"
+              :rules="[inputRules.email]"
+            />
+
+            <DefaultInput
+              v-model="form.phone"
+              label="Telefone"
+              class="col-6"
+              :mask="masks.Brasil.celular"
+              :rules="[inputRules.required]"
+            />
+
+            <DefaultCepInput
+              v-model="form.postal_code"
+              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-5"
+            />
+
+            <DefaultInput
+              v-model="form.address_number"
+              label="Número"
+              class="col-3"
+            />
+
+            <DefaultInput
+              v-model="form.neighborhood"
+              label="Bairro"
+              class="col-4"
+            />
+
+            <CitySelect
+              ref="citySelectRef"
+              v-model="selectedCity"
+              label="Cidade"
+              class="col-4"
+              :state="selectedState"
+              :initial-id="props.responsible?.city_id ?? null"
+            />
+
+            <StateSelect
+              ref="stateSelectRef"
+              v-model="selectedState"
+              label="Estado"
+              class="col-4"
+              :initial-id="props.responsible?.state_id ?? null"
+            />
+
+            <DefaultInput
+              v-model="form.complement"
+              label="Complemento"
+              class="col-12"
+            />
+
+            <DefaultInput
+              v-model="form.notes"
+              label="Observações"
+              class="col-12"
+              type="textarea"
+              :input-style="{ minHeight: '120px' }"
+              autogrow
+            />
+          </div>
+        </q-form>
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-actions align="right">
+        <q-btn
+          outline
+          color="primary"
+          label="CANCELAR"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          color="primary"
+          label="SALVAR"
+          :loading="saving"
+          @click="handleSave"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, watch, 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 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 {
+  createStudentResponsible,
+  updateStudentResponsible,
+} from "src/api/studentResponsible";
+import masks from "src/helpers/masks";
+import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
+
+const props = defineProps({
+  studentId: {
+    type: Number,
+    required: true,
+  },
+  responsible: {
+    type: Object,
+    default: null,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const { inputRules } = useInputRules();
+const formRef = useTemplateRef("formRef");
+const stateSelectRef = useTemplateRef("stateSelectRef");
+const citySelectRef = useTemplateRef("citySelectRef");
+
+const selectedState = ref(null);
+const selectedCity = ref(null);
+
+const genderOptions = [
+  { label: "Masculino", value: "male" },
+  { label: "Feminino", value: "female" },
+  { label: "Outro", value: "other" },
+  { label: "Prefiro não informar", value: "no_preference" },
+];
+
+const form = ref(
+  props.responsible
+    ? {
+        name: props.responsible.name ?? null,
+        birth_date: props.responsible.birth_date
+          ? formatDateYMDtoDMY(props.responsible.birth_date)
+          : null,
+        cpf: props.responsible.cpf ?? null,
+        gender: props.responsible.gender ?? "no_preference",
+        degree: props.responsible.degree ?? null,
+        email: props.responsible.email ?? null,
+        phone: props.responsible.phone ?? null,
+        postal_code: props.responsible.postal_code ?? null,
+        street: props.responsible.street ?? null,
+        address_number: props.responsible.address_number ?? null,
+        neighborhood: props.responsible.neighborhood ?? null,
+        state_id: props.responsible.state_id ?? null,
+        city_id: props.responsible.city_id ?? null,
+        complement: props.responsible.complement ?? null,
+        notes: props.responsible.notes ?? null,
+      }
+    : {
+        name: null,
+        birth_date: null,
+        cpf: null,
+        gender: "no_preference",
+        degree: null,
+        email: null,
+        phone: null,
+        postal_code: null,
+        street: null,
+        address_number: null,
+        neighborhood: null,
+        state_id: null,
+        city_id: null,
+        complement: null,
+        notes: null,
+      },
+);
+
+watch(selectedState, (state) => {
+  form.value.state_id = state?.value ?? null;
+});
+
+watch(selectedCity, (city) => {
+  form.value.city_id = city?.value ?? null;
+});
+
+const { loading: saving, execute } = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+});
+
+async function handleSave() {
+  const valid = await formRef.value?.validate();
+  if (!valid) return;
+
+  const payload = {
+    student_id: props.studentId,
+    name: form.value.name,
+    birth_date: form.value.birth_date
+      ? formatDateDMYtoYMD(form.value.birth_date)
+      : null,
+    cpf: form.value.cpf,
+    gender: form.value.gender,
+    degree: form.value.degree,
+    email: form.value.email,
+    phone: form.value.phone,
+    postal_code: form.value.postal_code,
+    street: form.value.street,
+    address_number: form.value.address_number,
+    neighborhood: form.value.neighborhood,
+    city_id: form.value.city_id,
+    state_id: form.value.state_id,
+    complement: form.value.complement,
+    notes: form.value.notes,
+  };
+
+  if (props.responsible) {
+    await execute(() =>
+      updateStudentResponsible(props.responsible.id, payload),
+    );
+  } else {
+    await execute(() => createStudentResponsible(payload));
+  }
+}
+</script>

+ 82 - 222
src/pages/students/tabs/ResponsibleTab.vue

@@ -1,135 +1,47 @@
 <template>
   <div>
-    <q-form ref="formRef" @submit="onSave">
-      <div class="row q-col-gutter-sm">
-        <DefaultInput
-          v-model="form.name"
-          label="Nome"
-          class="col-6"
-          :rules="[inputRules.required]"
-        />
-
-        <DefaultInput
-          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-4"
-          :rules="[inputRules.required]"
-        />
-
-        <DefaultInput
-          v-model="form.cpf"
-          label="CPF"
-          class="col-4"
-          :mask="masks.Brasil.cpf"
-          :rules="[inputRules.required]"
-        />
-
-        <DefaultSelect
-          v-model="form.gender"
-          label="Gênero"
-          class="col-4"
-          emit-value
-          map-options
-          :options="genderOptions"
-        />
-
-        <DefaultInput
-          v-model="form.email"
-          label="E-mail"
-          class="col-6"
-          type="email"
-          :rules="[inputRules.email]"
-        />
-
-        <DefaultInput
-          v-model="form.phone"
-          label="Telefone"
-          class="col-6"
-          :mask="masks.Brasil.celular"
-          :rules="[inputRules.required]"
-        />
-
-        <DefaultCepInput
-          v-model="form.postal_code"
-          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-5"
-        />
-
-        <DefaultInput
-          v-model="form.address_number"
-          label="Número"
-          class="col-3"
-        />
-
-        <DefaultInput
-          v-model="form.neighborhood"
-          label="Bairro"
-          class="col-4"
-        />
-
-        <CitySelect
-          ref="citySelectRef"
-          v-model="selectedCity"
-          label="Cidade"
-          class="col-4"
-          :state="selectedState"
-        />
-
-        <StateSelect
-          ref="stateSelectRef"
-          v-model="selectedState"
-          label="Estado"
-          class="col-4"
-        />
-
-        <DefaultInput
-          v-model="form.complement"
-          label="Complemento"
-          class="col-12"
-        />
-
-        <DefaultInput
-          v-model="form.notes"
-          label="Observações"
-          class="col-12"
-          type="textarea"
-          :input-style="{ minHeight: '120px' }"
-          autogrow
-        />
-      </div>
-
-    </q-form>
+    <DefaultTable
+      v-model:rows="rows"
+      title="Responsáveis"
+      :columns
+      descricao="responsáveis"
+      :feminino="true"
+      no-api-call
+      add-item
+      :show-search-field="false"
+      @on-add-item="handleAdd"
+    >
+      <template #body-cell-actions="{ row }">
+        <q-td align="center">
+          <q-item-section class="no-wrap" style="flex-direction: row; gap: 4px">
+            <q-btn
+              outline
+              icon="mdi-pencil-outline"
+              style="width: 36px"
+              @click.prevent.stop="handleEdit(row)"
+            />
+            <q-btn
+              outline
+              icon="mdi-trash-can-outline"
+              style="width: 36px"
+              @click.prevent.stop="handleDelete(row)"
+            />
+          </q-item-section>
+        </q-td>
+      </template>
+    </DefaultTable>
   </div>
 </template>
 
 <script setup>
-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 CitySelect from "src/components/selects/CitySelect.vue";
-import StateSelect from "src/components/selects/StateSelect.vue";
-import { useInputRules } from "src/composables/useInputRules";
-import { getStudentResponsible } from "src/api/studentResponsible";
-import masks from "src/helpers/masks";
-import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import ResponsibleDialog from "src/pages/students/components/ResponsibleDialog.vue";
+import {
+  getStudentResponsible,
+  deleteStudentResponsible,
+} from "src/api/studentResponsible";
 
 const props = defineProps({
   studentId: {
@@ -138,109 +50,57 @@ const props = defineProps({
   },
 });
 
-const { inputRules } = useInputRules();
-const formRef = useTemplateRef("formRef");
-const stateSelectRef = useTemplateRef("stateSelectRef");
-const citySelectRef = useTemplateRef("citySelectRef");
+const $q = useQuasar();
+const rows = ref([]);
 
-const selectedState = ref(null);
-const selectedCity = ref(null);
-
-const genderOptions = [
-  { label: "Masculino", value: "male" },
-  { label: "Feminino", value: "female" },
-  { label: "Outro", value: "other" },
-  { label: "Prefiro não informar", value: "no_preference" },
-];
-
-const emptyForm = () => ({
-  name: null,
-  birth_date: null,
-  cpf: null,
-  gender: "no_preference",
-  degree: null,
-  email: null,
-  phone: null,
-  postal_code: null,
-  street: null,
-  address_number: null,
-  neighborhood: null,
-  state_id: null,
-  city_id: null,
-  complement: null,
-  notes: null,
-});
-
-const form = ref(emptyForm());
-
-watch(selectedState, (state) => {
-  form.value.state_id = state?.value ?? null;
-});
+const columns = ref([
+  { name: "name", label: "Nome", field: "name", align: "left" },
+  {
+    name: "degree",
+    label: "Grau de Parentesco",
+    field: "degree",
+    align: "left",
+  },
+  { name: "phone", label: "Telefone", field: "phone", align: "left" },
+  { name: "email", label: "E-mail", field: "email", align: "left" },
+  { name: "actions", label: "Ações", field: null, align: "center" },
+]);
 
-watch(selectedCity, (city) => {
-  form.value.city_id = city?.value ?? null;
-});
+async function loadResponsibles() {
+  rows.value = await getStudentResponsible(props.studentId);
+}
 
-async function loadResponsible() {
-  try {
-    const data = await getStudentResponsible(props.studentId);
-    if (data) {
-      form.value = {
-        name: data.name ?? null,
-        birth_date: data.birth_date ? formatDateYMDtoDMY(data.birth_date) : null,
-        cpf: data.cpf ?? null,
-        gender: data.gender ?? "no_preference",
-        degree: data.degree ?? null,
-        email: data.email ?? null,
-        phone: data.phone ?? null,
-        postal_code: data.postal_code ?? null,
-        street: data.street ?? null,
-        address_number: data.address_number ?? null,
-        neighborhood: data.neighborhood ?? null,
-        state_id: data.state_id ?? null,
-        city_id: data.city_id ?? null,
-        complement: data.complement ?? null,
-        notes: data.notes ?? null,
-      };
+onMounted(loadResponsibles);
 
-      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);
-  }
+function openDialog(responsible = null) {
+  $q.dialog({
+    component: ResponsibleDialog,
+    componentProps: { studentId: props.studentId, responsible },
+  }).onOk(loadResponsibles);
 }
 
-function buildPayload() {
-  return {
-    name: form.value.name,
-    birth_date: form.value.birth_date
-      ? formatDateDMYtoYMD(form.value.birth_date)
-      : null,
-    cpf: form.value.cpf,
-    gender: form.value.gender,
-    degree: form.value.degree,
-    email: form.value.email,
-    phone: form.value.phone,
-    postal_code: form.value.postal_code,
-    street: form.value.street,
-    address_number: form.value.address_number,
-    neighborhood: form.value.neighborhood,
-    city_id: form.value.city_id,
-    state_id: form.value.state_id,
-    complement: form.value.complement,
-    notes: form.value.notes,
-  };
+function handleAdd() {
+  openDialog();
 }
 
-onMounted(loadResponsible);
+function handleEdit(responsible) {
+  openDialog(responsible);
+}
 
-defineExpose({
-  validate: () => formRef.value?.validate(),
-  buildPayload,
-});
+function handleDelete(responsible) {
+  $q.dialog({
+    title: "Excluir Responsável",
+    message: `Deseja excluir o responsável "${responsible.name}"?`,
+    cancel: true,
+    persistent: true,
+  }).onOk(async () => {
+    try {
+      await deleteStudentResponsible(responsible.id);
+      rows.value = rows.value.filter((r) => r.id !== responsible.id);
+    } catch (e) {
+      console.error(e);
+      $q.notify({ type: "negative", message: "Erro ao excluir responsável." });
+    }
+  });
+}
 </script>