Explorar el Código

feat: adiciona anexo em contrato e midias

ebagabee hace 1 mes
padre
commit
0cde5de169

+ 7 - 0
src/api/studentContract.js

@@ -15,6 +15,13 @@ export const updateStudentContract = async (id, payload) => {
   return data.payload;
   return data.payload;
 };
 };
 
 
+export const attachContractFile = async (id, formData) => {
+  const { data } = await api.post(`/student-contract/${id}/file`, formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
 export const deleteStudentContract = async (id) => {
 export const deleteStudentContract = async (id) => {
   await api.delete(`/student-contract/${id}`);
   await api.delete(`/student-contract/${id}`);
 };
 };

+ 20 - 0
src/api/student_media.js

@@ -0,0 +1,20 @@
+import api from "src/api";
+
+export const getStudentMedias = async (studentId) => {
+  const { data } = await api.get("/media", {
+    params: { origem: "student", origem_id: studentId },
+  });
+  return data.payload;
+};
+
+export const createStudentMedia = async (formData) => {
+  const { data } = await api.post("/media", formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
+export const deleteStudentMedia = async (id) => {
+  const { data } = await api.delete(`/media/${id}`);
+  return data;
+};

+ 75 - 0
src/pages/students/components/AddStudentMediaDialog.vue

@@ -0,0 +1,75 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="width: 480px; max-width: 95vw">
+      <DefaultDialogHeader title="Adicionar Mídia" @close="onDialogCancel" />
+
+      <q-form ref="formRef" @submit="onSubmit">
+        <q-card-section class="q-pt-none">
+          <div class="column q-gutter-sm">
+            <DefaultInput
+              v-model="form.name"
+              label="Nome do documento"
+              :rules="[inputRules.required]"
+            />
+
+            <q-file
+              v-model="selectedFile"
+              label="Arquivo"
+              outlined
+              accept="image/*,video/*,.pdf"
+              :rules="[inputRules.required]"
+            >
+              <template #prepend>
+                <q-icon name="mdi-paperclip" />
+              </template>
+            </q-file>
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right" class="q-pa-md">
+          <q-btn outline color="primary" label="Cancelar" @click="onDialogCancel" />
+          <q-btn color="primary" label="Adicionar" type="submit" :loading="loading" />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { createStudentMedia } from "src/api/student_media";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { studentId } = defineProps({
+  studentId: { type: Number, required: true },
+});
+
+const { inputRules } = useInputRules();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const formRef = ref(null);
+const selectedFile = ref(null);
+const form = ref({ name: "" });
+
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: (result) => onDialogOK(result),
+});
+
+async function onSubmit() {
+  await execute(() => {
+    const formData = new FormData();
+    formData.append("name", form.value.name);
+    formData.append("origem", "student");
+    formData.append("origem_id", studentId);
+    formData.append("file", selectedFile.value);
+    return createStudentMedia(formData);
+  });
+}
+</script>

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

@@ -33,7 +33,7 @@
         </div>
         </div>
 
 
         <div v-show="currentTab === 'media'">
         <div v-show="currentTab === 'media'">
-          <MediaTab />
+          <MediaTab :student-id="props.student.id" />
         </div>
         </div>
       </q-card-section>
       </q-card-section>
 
 

+ 52 - 8
src/pages/students/tabs/ContractTab.vue

@@ -1,5 +1,13 @@
 <template>
 <template>
   <div>
   <div>
+    <input
+      ref="fileInputRef"
+      type="file"
+      accept="image/*,video/*,.pdf"
+      style="display: none"
+      @change="onFileSelected"
+    />
+
     <DefaultTable
     <DefaultTable
       v-model:rows="rows"
       v-model:rows="rows"
       title="Contratos"
       title="Contratos"
@@ -37,7 +45,8 @@
               outline
               outline
               icon="mdi-paperclip"
               icon="mdi-paperclip"
               style="width: 36px"
               style="width: 36px"
-              @click.prevent.stop="() => {}"
+              :loading="uploadingId === row.id"
+              @click.prevent.stop="triggerFileInput(row)"
             />
             />
             <q-btn
             <q-btn
               outline
               outline
@@ -46,12 +55,14 @@
               @click.prevent.stop
               @click.prevent.stop
             >
             >
               <q-menu>
               <q-menu>
-                <q-list style="min-width: 150px">
-                  <q-item v-close-popup clickable @click="() => {}">
-                    <q-item-section>Editar</q-item-section>
-                  </q-item>
-                  <q-item v-close-popup clickable @click="() => {}">
-                    <q-item-section>Anexos</q-item-section>
+                <q-list style="min-width: 170px">
+                  <q-item
+                    v-close-popup
+                    clickable
+                    :disable="!row.file_url"
+                    @click="openFile(row.file_url)"
+                  >
+                    <q-item-section>Visualizar arquivo</q-item-section>
                   </q-item>
                   </q-item>
                 </q-list>
                 </q-list>
               </q-menu>
               </q-menu>
@@ -68,7 +79,7 @@ import { ref, onMounted } from "vue";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import CreateContractDialog from "src/pages/students/components/CreateContractDialog.vue";
 import CreateContractDialog from "src/pages/students/components/CreateContractDialog.vue";
-import { getStudentContracts } from "src/api/studentContract";
+import { getStudentContracts, attachContractFile } from "src/api/studentContract";
 
 
 const props = defineProps({
 const props = defineProps({
   student: {
   student: {
@@ -79,6 +90,9 @@ const props = defineProps({
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 const rows = ref([]);
 const rows = ref([]);
+const fileInputRef = ref(null);
+const uploadingId = ref(null);
+let pendingContractId = null;
 
 
 async function loadContracts() {
 async function loadContracts() {
   rows.value = await getStudentContracts(props.student.id);
   rows.value = await getStudentContracts(props.student.id);
@@ -101,6 +115,36 @@ function handleEdit(contract) {
   openDialog(contract);
   openDialog(contract);
 }
 }
 
 
+function triggerFileInput(row) {
+  pendingContractId = row.id;
+  fileInputRef.value.value = "";
+  fileInputRef.value.click();
+}
+
+async function onFileSelected(event) {
+  const file = event.target.files?.[0];
+  if (!file || !pendingContractId) return;
+
+  uploadingId.value = pendingContractId;
+  try {
+    const formData = new FormData();
+    formData.append("file", file);
+    const updated = await attachContractFile(pendingContractId, formData);
+    const idx = rows.value.findIndex((r) => r.id === pendingContractId);
+    if (idx !== -1) rows.value[idx] = updated;
+  } catch (e) {
+    console.error(e);
+    $q.notify({ type: "negative", message: "Erro ao anexar arquivo." });
+  } finally {
+    uploadingId.value = null;
+    pendingContractId = null;
+  }
+}
+
+function openFile(url) {
+  window.open(url, "_blank");
+}
+
 const columns = ref([
 const columns = ref([
   { name: "contract", label: "Contrato", field: "protocol", align: "left" },
   { name: "contract", label: "Contrato", field: "protocol", align: "left" },
   { name: "period", label: "Data Assinatura - Encerramento", field: null, align: "left" },
   { name: "period", label: "Data Assinatura - Encerramento", field: null, align: "left" },

+ 49 - 22
src/pages/students/tabs/MediaTab.vue

@@ -7,7 +7,7 @@
       add-item
       add-item
       :show-search-field="false"
       :show-search-field="false"
       hide-no-data-label
       hide-no-data-label
-      @on-add-item="addMedia"
+      @on-add-item="openAddDialog"
     >
     >
       <template #body-cell-item="{ rowIndex }">
       <template #body-cell-item="{ rowIndex }">
         <q-td>{{ rowIndex + 1 }}</q-td>
         <q-td>{{ rowIndex + 1 }}</q-td>
@@ -20,14 +20,15 @@
           dense
           dense
           icon="mdi-eye-outline"
           icon="mdi-eye-outline"
           class="q-mr-xs"
           class="q-mr-xs"
-          @click.stop="viewMedia(row)"
+          :disable="!row.file_url"
+          @click.stop="openFile(row.file_url)"
         />
         />
         <q-btn
         <q-btn
           flat
           flat
           round
           round
           dense
           dense
           icon="mdi-trash-can-outline"
           icon="mdi-trash-can-outline"
-          @click.stop="deleteMedia(row)"
+          @click.stop="confirmDelete(row)"
         />
         />
       </template>
       </template>
     </DefaultTable>
     </DefaultTable>
@@ -35,36 +36,62 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import AddStudentMediaDialog from "src/pages/students/components/AddStudentMediaDialog.vue";
+import { getStudentMedias, deleteStudentMedia } from "src/api/student_media";
 
 
-const rows = ref([
-  { id: 1, date: "23/08/2024", name: "Comprovante de pagamento" },
-  { id: 2, date: "23/08/2024", name: "Certidão de nascimento" },
-  { id: 3, date: "23/08/2024", name: "Comprovante de residência" },
-]);
+const props = defineProps({
+  studentId: { type: Number, required: true },
+});
+
+const $q = useQuasar();
+const rows = ref([]);
 
 
 const columns = [
 const columns = [
   { name: "item", label: "Item", field: "id", align: "left" },
   { name: "item", label: "Item", field: "id", align: "left" },
-  { name: "date", label: "Data", field: "date", align: "left" },
-  {
-    name: "name",
-    label: "Comprovante de pagamento",
-    field: "name",
-    align: "left",
-  },
+  { name: "date", label: "Data", field: "created_at", align: "left" },
+  { name: "name", label: "Nome", field: "name", align: "left" },
   { name: "actions", label: "Ações", field: null, align: "right" },
   { name: "actions", label: "Ações", field: null, align: "right" },
 ];
 ];
 
 
-function addMedia() {
-  // TODO: open upload dialog
+async function fetchMedias() {
+  try {
+    rows.value = await getStudentMedias(props.studentId);
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+function openAddDialog() {
+  $q.dialog({
+    component: AddStudentMediaDialog,
+    componentProps: { studentId: props.studentId },
+  }).onOk((result) => {
+    rows.value.unshift(result);
+  });
 }
 }
 
 
-function viewMedia(row) {
-  console.log(row);
+function openFile(url) {
+  window.open(url, "_blank");
 }
 }
 
 
-function deleteMedia(row) {
-  rows.value = rows.value.filter((r) => r.id !== row.id);
+function confirmDelete(row) {
+  $q.dialog({
+    title: "Remover mídia",
+    message: `Deseja remover "${row.name}"?`,
+    ok: { color: "negative", label: "Remover" },
+    cancel: { color: "primary", outline: true, label: "Cancelar" },
+  }).onOk(async () => {
+    try {
+      await deleteStudentMedia(row.id);
+      rows.value = rows.value.filter((r) => r.id !== row.id);
+    } catch (e) {
+      console.error(e);
+    }
+  });
 }
 }
+
+onMounted(fetchMedias);
 </script>
 </script>