2 Angajamente 113a852ece ... 54ec328027

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  Gustavo Mantovani 54ec328027 feat: add btn para upload do contrato assinado 1 lună în urmă
  Gustavo Mantovani b00a0d2a5e feat: add btn para gerar e visualizar pdf dos templates de contrato para estudante 1 lună în urmă

+ 32 - 13
src/api/studentContract.js

@@ -5,11 +5,6 @@ export const getStudentContractById = async (id) => {
   return data.payload;
 };
 
-export const getStudentContracts = async (studentId) => {
-  const { data } = await api.get("/franchisee/contracts", { params: { student_id: studentId } });
-  return data.payload;
-};
-
 export const getAllContracts = async () => {
   const { data } = await api.get("/franchisee/contracts");
   return data.payload;
@@ -25,23 +20,51 @@ export const updateStudentContract = async (id, payload) => {
   return data.payload;
 };
 
-export const attachContractFile = async (id, formData) => {
-  const { data } = await api.post(`/franchisee/contracts/${id}/file`, formData, {
-    headers: { "Content-Type": "multipart/form-data" },
+export const deleteStudentContract = async (id) => {
+  await api.delete(`/franchisee/contracts/${id}`);
+};
+
+//
+
+export const getStudentContracts = async (studentId) => {
+  const { data } = await api.get("/franchisee/contracts", {
+    params: { student_id: studentId },
   });
   return data.payload;
 };
 
+export const generateStudentContractFile = async (id) => {
+  const { data } = await api.post(`/franchisee/contracts/${id}/generate-file`);
+  return data.payload;
+};
+
+export const attachSignedContractFile = async (id, formData) => {
+  const { data } = await api.post(
+    `/franchisee/contracts/${id}/signed-file`,
+    formData,
+    {
+      headers: { "Content-Type": "multipart/form-data" },
+    },
+  );
+  return data.payload;
+};
+
+//
+
 export const getContractInstallments = async (id) => {
   const { data } = await api.get(`/franchisee/contracts/${id}/installments`);
   return data.payload;
 };
 
 export const freezeContract = async (id, months) => {
-  const { data } = await api.post(`/franchisee/contracts/${id}/freeze`, { months });
+  const { data } = await api.post(`/franchisee/contracts/${id}/freeze`, {
+    months,
+  });
   return data.payload;
 };
 
+//
+
 export const cancelContract = async (id) => {
   const { data } = await api.post(`/franchisee/contracts/${id}/cancel`);
   return data.payload;
@@ -51,7 +74,3 @@ export const reactivateContract = async (id) => {
   const { data } = await api.post(`/franchisee/contracts/${id}/reactivate`);
   return data.payload;
 };
-
-export const deleteStudentContract = async (id) => {
-  await api.delete(`/franchisee/contracts/${id}`);
-};

+ 223 - 37
src/pages/contracts/ContractsPage.vue

@@ -1,5 +1,13 @@
 <template>
   <div>
+    <input
+      ref="signedFileInputRef"
+      accept="image/jpeg,image/png,.pdf"
+      style="display: none"
+      type="file"
+      @change="onSignedFileSelected"
+    />
+
     <DefaultHeaderPage title="Contratos" />
 
     <div class="q-px-sm">
@@ -77,9 +85,7 @@
                 style="width: 36px"
                 @click.prevent.stop="handleView(row)"
               >
-                <q-tooltip>
-                  Visualizar contrato
-                </q-tooltip>
+                <q-tooltip> Visualizar contrato </q-tooltip>
               </q-btn>
 
               <q-btn
@@ -89,9 +95,7 @@
                 style="width: 36px"
                 @click.prevent.stop="handleEdit(row)"
               >
-                <q-tooltip>
-                  Editar contrato
-                </q-tooltip>
+                <q-tooltip> Editar contrato </q-tooltip>
               </q-btn>
 
               <q-btn
@@ -100,57 +104,131 @@
                 style="width: 36px"
                 @click.prevent.stop="handleHistory(row)"
               >
-                <q-tooltip>
-                  Ver histórico
-                </q-tooltip>
+                <q-tooltip> Ver histórico </q-tooltip>
               </q-btn>
 
               <q-btn
-                v-if="canEdit"
+                v-if="canEdit || row.file_url || row.signed_file_url"
                 icon="mdi-dots-vertical"
                 outline
                 style="width: 36px"
                 @click.prevent.stop
               >
                 <q-menu>
-                  <q-list style="min-width: 170px">
-                    <template
-                      v-if="
-                        row.status === 'frozen' ||
-                        row.status === 'cancelled'
-                      "
-                    >
+                  <q-list style="min-width: 280px">
                       <q-item
+                        v-if="row.file_url || canEdit"
                         v-close-popup
+                        class="q-pa-md"
                         clickable
-                        @click="handleReactivate(row)"
+                        @click="handleContractFile(row)"
                       >
-                        <q-item-section>
-                          Reativar Contrato
+                        <q-item-section avatar>
+                          <q-spinner
+                            v-if="generatingId === row.id"
+                            color="primary"
+                            size="20px"
+                          />
+
+                          <q-icon
+                            v-else
+                            :name="
+                              row.file_url
+                                ? 'mdi-file-eye-outline'
+                                : 'mdi-file-plus-outline'
+                            "
+                          />
                         </q-item-section>
-                      </q-item>
-                    </template>
 
-                    <template v-else>
-                      <q-item
-                        v-close-popup
-                        clickable
-                        @click="handleFreeze(row)"
-                      >
                         <q-item-section>
-                          Trancar Contrato
+                          {{
+                            row.file_url
+                              ? "Visualizar arquivo do contrato"
+                              : "Gerar arquivo do contrato"
+                          }}
                         </q-item-section>
                       </q-item>
 
                       <q-item
+                        v-if="row.signed_file_url || canEdit"
                         v-close-popup
+                        class="q-pa-md"
                         clickable
-                        @click="handleCancel(row)"
+                        @click="handleSignedContractFile(row)"
                       >
+                        <q-item-section avatar>
+                          <q-spinner
+                            v-if="uploadingSignedId === row.id"
+                            color="primary"
+                            size="20px"
+                          />
+
+                          <q-icon
+                            v-else
+                            :name="
+                              row.signed_file_url
+                                ? 'mdi-file-check-outline'
+                                : 'mdi-file-upload-outline'
+                            "
+                          />
+                        </q-item-section>
+
                         <q-item-section>
-                          Cancelar Contrato
+                          {{
+                            row.signed_file_url
+                              ? "Visualizar contrato assinado"
+                              : "Enviar contrato assinado"
+                          }}
                         </q-item-section>
                       </q-item>
+
+                      <template
+                        v-if="
+                          canEdit &&
+                          (row.status === 'frozen' ||
+                            row.status === 'cancelled')
+                        "
+                      >
+                        <q-item
+                          v-close-popup
+                          class="q-pa-md"
+                          clickable
+                          @click="handleReactivate(row)"
+                        >
+                          <q-item-section avatar>
+                            <q-icon name="mdi-refresh" />
+                          </q-item-section>
+
+                          <q-item-section> Reativar Contrato </q-item-section>
+                        </q-item>
+                      </template>
+
+                      <template v-else-if="canEdit">
+                        <q-item
+                          v-close-popup
+                          class="q-pa-md"
+                          clickable
+                          @click="handleFreeze(row)"
+                        >
+                          <q-item-section avatar>
+                            <q-icon name="mdi-file-cancel-outline" />
+                          </q-item-section>
+
+                          <q-item-section> Trancar Contrato </q-item-section>
+                        </q-item>
+
+                        <q-item
+                          v-close-popup
+                          class="q-pa-md"
+                          clickable
+                          @click="handleCancel(row)"
+                        >
+                          <q-item-section avatar>
+                            <q-icon name="mdi-file-cancel-outline" />
+                          </q-item-section>
+
+                          <q-item-section> Cancelar Contrato </q-item-section>
+                        </q-item>
                     </template>
                   </q-list>
                 </q-menu>
@@ -165,7 +243,9 @@
 
 <script setup>
 import {
+  attachSignedContractFile,
   cancelContract,
+  generateStudentContractFile,
   getAllContracts,
   reactivateContract,
 } from "src/api/studentContract";
@@ -192,6 +272,9 @@ const store = userStore();
 const permissions = permissionStore();
 
 const isLoading = ref(false);
+const generatingId = ref(null);
+const signedFileInputRef = ref(null);
+const uploadingSignedId = ref(null);
 const rows = ref([]);
 
 const canEdit = computed(() =>
@@ -237,6 +320,8 @@ const columns = [
   },
 ];
 
+let pendingSignedContractId = null;
+
 const confirmAction = (title, message, apiFunction, contract) => {
   $q.dialog({
     component: ContractActionConfirmDialog,
@@ -248,9 +333,7 @@ const confirmAction = (title, message, apiFunction, contract) => {
     try {
       const updated = await apiFunction(contract.id);
 
-      const index = rows.value.findIndex(
-        (row) => row.id === contract.id,
-      );
+      const index = rows.value.findIndex((row) => row.id === contract.id);
 
       if (index !== -1) {
         rows.value[index] = {
@@ -294,9 +377,7 @@ const handleFreeze = (contract) => {
       contract,
     },
   }).onOk((updated) => {
-    const index = rows.value.findIndex(
-      (row) => row.id === contract.id,
-    );
+    const index = rows.value.findIndex((row) => row.id === contract.id);
 
     if (index !== -1) {
       rows.value[index] = {
@@ -317,6 +398,111 @@ const handleHistory = (contract) => {
   });
 };
 
+//
+
+const handleContractFile = async (contract) => {
+  if (contract.file_url) {
+    window.open(contract.file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  const fileWindow = window.open("", "_blank");
+
+  generatingId.value = contract.id;
+
+  try {
+    const updated = await generateStudentContractFile(contract.id);
+
+    const index = rows.value.findIndex((row) => row.id === contract.id);
+
+    if (index !== -1) {
+      rows.value[index] = {
+        ...rows.value[index],
+        ...updated,
+      };
+    }
+
+    if (updated.file_url && fileWindow) {
+      fileWindow.location.href = updated.file_url;
+    } else {
+      fileWindow?.close();
+
+      throw new Error("A API não retornou o arquivo gerado.");
+    }
+  } catch (error) {
+    fileWindow?.close();
+
+    console.error(error);
+
+    $q.notify({
+      message: "Erro ao gerar o arquivo do contrato.",
+      type: "negative",
+    });
+  } finally {
+    generatingId.value = null;
+  }
+};
+
+//
+
+const handleSignedContractFile = (contract) => {
+  if (contract.signed_file_url) {
+    window.open(contract.signed_file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  pendingSignedContractId = contract.id;
+
+  signedFileInputRef.value.value = "";
+  signedFileInputRef.value.click();
+};
+
+const onSignedFileSelected = async (event) => {
+  const file = event.target.files?.[0];
+
+  const contractId = pendingSignedContractId;
+
+  if (!file || !contractId) return;
+
+  uploadingSignedId.value = contractId;
+
+  try {
+    const formData = new FormData();
+
+    formData.append("file", file);
+
+    const updated = await attachSignedContractFile(contractId, formData);
+
+    const index = rows.value.findIndex((row) => row.id === contractId);
+
+    if (index !== -1) {
+      rows.value[index] = {
+        ...rows.value[index],
+        ...updated,
+      };
+    }
+
+    $q.notify({
+      message: "Contrato assinado enviado com sucesso.",
+      type: "positive",
+    });
+  } catch (error) {
+    console.error(error);
+
+    $q.notify({
+      message: "Erro ao enviar o contrato assinado.",
+      type: "negative",
+    });
+  } finally {
+    pendingSignedContractId = null;
+    uploadingSignedId.value = null;
+  }
+};
+
+//
+
 const handleReactivate = (contract) => {
   confirmAction(
     "Reativar Contrato",

+ 188 - 7
src/pages/students/components/ContractHistoryDialog.vue

@@ -4,6 +4,14 @@
       class="q-dialog-plugin overflow-hidden"
       style="width: 100%; max-width: 1100px"
     >
+      <input
+        ref="signedFileInputRef"
+        accept="image/jpeg,image/png,.pdf"
+        style="display: none"
+        type="file"
+        @change="onSignedFileSelected"
+      />
+
       <DefaultDialogHeader
         title="Histórico do Contrato"
         @close="onDialogCancel"
@@ -69,6 +77,53 @@
               {{ formatDateTime(row.updated_at) }}
             </q-td>
           </template>
+
+          <template #body-cell-actions="{ row }">
+            <q-td align="center">
+              <q-btn
+                v-if="row.file_url || canEdit"
+                :icon="
+                  row.file_url
+                    ? 'mdi-file-eye-outline'
+                    : 'mdi-file-plus-outline'
+                "
+                :loading="generatingId === row.id"
+                outline
+                style="width: 36px"
+                @click="handleContractFile(row)"
+              >
+                <q-tooltip>
+                  {{
+                    row.file_url
+                      ? "Visualizar arquivo desta versão"
+                      : "Gerar arquivo desta versão"
+                  }}
+                </q-tooltip>
+              </q-btn>
+
+              <q-btn
+                v-if="row.signed_file_url || canEdit"
+                class="q-ml-xs"
+                outline
+                style="width: 36px"
+                :icon="
+                  row.signed_file_url
+                    ? 'mdi-file-check-outline'
+                    : 'mdi-file-upload-outline'
+                "
+                :loading="uploadingSignedId === row.id"
+                @click="handleSignedContractFile(row)"
+              >
+                <q-tooltip>
+                  {{
+                    row.signed_file_url
+                      ? "Visualizar contrato assinado desta versão"
+                      : "Enviar contrato assinado desta versão"
+                  }}
+                </q-tooltip>
+              </q-btn>
+            </q-td>
+          </template>
         </q-table>
       </q-card-section>
 
@@ -86,9 +141,15 @@
 </template>
 
 <script setup>
+import {
+  attachSignedContractFile,
+  generateStudentContractFile,
+  getStudentContractById,
+} from "src/api/studentContract";
+
 import { computed, onMounted, ref } from "vue";
 import { getUnitPackagesForSelect } from "src/api/package";
-import { getStudentContractById } from "src/api/studentContract";
+import { permissionStore } from "src/stores/permission";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -110,11 +171,21 @@ const props = defineProps({
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 
 const $q = useQuasar();
+const permissions = permissionStore();
 
 const contract = ref(null);
 const loading = ref(false);
+const generatingId = ref(null);
+const signedFileInputRef = ref(null);
+const uploadingSignedId = ref(null);
 const packages = ref([]);
 
+let pendingSignedContractId = null;
+
+const canEdit = computed(() =>
+  permissions.getAccess("franchisee_contracts", "edit"),
+);
+
 const columns = [
   {
     align: "center",
@@ -164,15 +235,19 @@ const columns = [
     label: "Alterado em",
     name: "updated_at",
   },
+  {
+    align: "center",
+    field: null,
+    label: "Arquivo",
+    name: "actions",
+  },
 ];
 
 const normalizeId = (value) => (value == null ? null : String(value));
 
 const packageNames = computed(
   () =>
-    new Map(
-      packages.value.map((item) => [normalizeId(item.id), item.name]),
-    ),
+    new Map(packages.value.map((item) => [normalizeId(item.id), item.name])),
 );
 
 const historyRows = computed(() =>
@@ -187,11 +262,11 @@ const historyRows = computed(() =>
           ? props.contractData?.package_name
           : null),
     }))
-    .sort(
-      (first, second) => Number(second.version) - Number(first.version),
-    ),
+    .sort((first, second) => Number(second.version) - Number(first.version)),
 );
 
+//
+
 const formatCurrency = (value) =>
   new Intl.NumberFormat("pt-BR", {
     currency: "BRL",
@@ -209,6 +284,110 @@ const formatDateTime = (value) => {
   return `${day}/${month}/${year}${time ? ` ${time.slice(0, 5)}` : ""}`;
 };
 
+const handleContractFile = async (row) => {
+  if (row.file_url) {
+    window.open(row.file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  const fileWindow = window.open("", "_blank");
+
+  generatingId.value = row.id;
+
+  try {
+    const updated = await generateStudentContractFile(row.id);
+
+    const version = contract.value?.version_history?.find(
+      (item) => item.id === row.id,
+    );
+
+    if (version) {
+      version.file_url = updated.file_url;
+      version.file_type = updated.file_type;
+    }
+
+    if (updated.file_url && fileWindow) {
+      fileWindow.location.href = updated.file_url;
+    } else {
+      fileWindow?.close();
+
+      throw new Error("A API não retornou o arquivo gerado.");
+    }
+  } catch (error) {
+    fileWindow?.close();
+
+    console.error(error);
+
+    $q.notify({
+      message: "Erro ao gerar o arquivo desta versão do contrato.",
+      type: "negative",
+    });
+  } finally {
+    generatingId.value = null;
+  }
+};
+
+//
+
+const handleSignedContractFile = (row) => {
+  if (row.signed_file_url) {
+    window.open(row.signed_file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  pendingSignedContractId = row.id;
+
+  signedFileInputRef.value.value = "";
+  signedFileInputRef.value.click();
+};
+
+const onSignedFileSelected = async (event) => {
+  const file = event.target.files?.[0];
+
+  const contractId = pendingSignedContractId;
+
+  if (!file || !contractId) return;
+
+  uploadingSignedId.value = contractId;
+
+  try {
+    const formData = new FormData();
+
+    formData.append("file", file);
+
+    const updated = await attachSignedContractFile(contractId, formData);
+
+    const version = contract.value?.version_history?.find(
+      (item) => item.id === contractId,
+    );
+
+    if (version) {
+      version.signed_file_url = updated.signed_file_url;
+      version.signed_file_type = updated.signed_file_type;
+    }
+
+    $q.notify({
+      message: "Contrato assinado enviado com sucesso.",
+      type: "positive",
+    });
+  } catch (error) {
+    console.error(error);
+
+    $q.notify({
+      message: "Erro ao enviar o contrato assinado.",
+      type: "negative",
+    });
+  } finally {
+    pendingSignedContractId = null;
+
+    uploadingSignedId.value = null;
+  }
+};
+
+//
+
 const statusColor = (status) => {
   if (status === "active") return "positive";
   if (status === "frozen") return "info";
@@ -225,6 +404,8 @@ const statusLabel = (status) => {
   return "Inativo";
 };
 
+//
+
 onMounted(async () => {
   loading.value = true;
 

+ 117 - 57
src/pages/students/tabs/ContractTab.vue

@@ -2,7 +2,7 @@
   <div>
     <input
       ref="fileInputRef"
-      accept="image/*,video/*,.pdf"
+      accept="image/jpeg,image/png,.pdf"
       style="display: none"
       type="file"
       @change="onFileSelected"
@@ -20,9 +20,7 @@
       @on-add-item="handleAdd"
     >
       <template #body-cell-period="{ row }">
-        <q-td>
-          {{ row.signature_date }} — {{ row.end_date }}
-        </q-td>
+        <q-td> {{ row.signature_date }} — {{ row.end_date }} </q-td>
       </template>
 
       <template #body-cell-status="{ row }">
@@ -36,10 +34,7 @@
 
       <template #body-cell-actions="{ row }">
         <q-td align="center">
-          <q-item-section
-            class="no-wrap"
-            style="flex-direction: row; gap: 4px"
-          >
+          <q-item-section class="no-wrap" style="flex-direction: row; gap: 4px">
             <q-btn
               v-if="canEditContract"
               icon="mdi-pencil-outline"
@@ -47,9 +42,7 @@
               style="width: 36px"
               @click.prevent.stop="handleEdit(row)"
             >
-              <q-tooltip>
-                Editar contrato
-              </q-tooltip>
+              <q-tooltip> Editar contrato </q-tooltip>
             </q-btn>
 
             <q-btn
@@ -57,20 +50,49 @@
               outline
               style="width: 36px"
               @click.prevent.stop="handleHistory(row)"
+            >
+              <q-tooltip> Ver histórico </q-tooltip>
+            </q-btn>
+
+            <q-btn
+              v-if="row.file_url || canEdit"
+              :icon="
+                row.file_url ? 'mdi-file-eye-outline' : 'mdi-file-plus-outline'
+              "
+              :loading="generatingId === row.id"
+              outline
+              style="width: 36px"
+              @click.prevent.stop="handleContractFile(row)"
             >
               <q-tooltip>
-                Ver histórico
+                {{
+                  row.file_url
+                    ? "Visualizar arquivo do contrato"
+                    : "Gerar arquivo do contrato"
+                }}
               </q-tooltip>
             </q-btn>
 
             <q-btn
-              v-if="canEdit"
-              icon="mdi-paperclip"
+              v-if="row.signed_file_url || canEdit"
+              :icon="
+                row.signed_file_url
+                  ? 'mdi-file-check-outline'
+                  : 'mdi-file-upload-outline'
+              "
+              :loading="uploadingId === row.id"
               outline
               style="width: 36px"
-              :loading="uploadingId === row.id"
-              @click.prevent.stop="triggerFileInput(row)"
-            />
+              @click.prevent.stop="handleSignedContractFile(row)"
+            >
+              <q-tooltip>
+                {{
+                  row.signed_file_url
+                    ? "Visualizar contrato assinado"
+                    : "Enviar contrato assinado"
+                }}
+              </q-tooltip>
+            </q-btn>
 
             <q-btn
               v-if="canEdit"
@@ -82,41 +104,24 @@
               <q-menu>
                 <q-list style="min-width: 170px">
                   <template
-                    v-if="
-                      row.status === 'frozen' ||
-                      row.status === 'cancelled'
-                    "
+                    v-if="row.status === 'frozen' || row.status === 'cancelled'"
                   >
                     <q-item
                       v-close-popup
                       clickable
                       @click="handleReactivate(row)"
                     >
-                      <q-item-section>
-                        Reativar Contrato
-                      </q-item-section>
+                      <q-item-section> Reativar Contrato </q-item-section>
                     </q-item>
                   </template>
 
                   <template v-else>
-                    <q-item
-                      v-close-popup
-                      clickable
-                      @click="handleFreeze(row)"
-                    >
-                      <q-item-section>
-                        Trancar Contrato
-                      </q-item-section>
+                    <q-item v-close-popup clickable @click="handleFreeze(row)">
+                      <q-item-section> Trancar Contrato </q-item-section>
                     </q-item>
 
-                    <q-item
-                      v-close-popup
-                      clickable
-                      @click="handleCancel(row)"
-                    >
-                      <q-item-section>
-                        Cancelar Contrato
-                      </q-item-section>
+                    <q-item v-close-popup clickable @click="handleCancel(row)">
+                      <q-item-section> Cancelar Contrato </q-item-section>
                     </q-item>
                   </template>
                 </q-list>
@@ -131,8 +136,9 @@
 
 <script setup>
 import {
-  attachContractFile,
+  attachSignedContractFile,
   cancelContract,
+  generateStudentContractFile,
   getStudentContracts,
   reactivateContract,
 } from "src/api/studentContract";
@@ -159,6 +165,7 @@ const permissions = permissionStore();
 
 const fileInputRef = ref(null);
 const rows = ref([]);
+const generatingId = ref(null);
 const uploadingId = ref(null);
 
 const canAdd = computed(() =>
@@ -211,9 +218,7 @@ const confirmAction = (title, message, apiFunction, contract) => {
     try {
       const updated = await apiFunction(contract.id);
 
-      const index = rows.value.findIndex(
-        (row) => row.id === contract.id,
-      );
+      const index = rows.value.findIndex((row) => row.id === contract.id);
 
       if (index !== -1) {
         rows.value[index] = updated;
@@ -253,9 +258,7 @@ const handleFreeze = (contract) => {
       contract,
     },
   }).onOk((updated) => {
-    const index = rows.value.findIndex(
-      (row) => row.id === contract.id,
-    );
+    const index = rows.value.findIndex((row) => row.id === contract.id);
 
     if (index !== -1) {
       rows.value[index] = {
@@ -276,6 +279,60 @@ const handleHistory = (contract) => {
   });
 };
 
+const handleContractFile = async (contract) => {
+  if (contract.file_url) {
+    window.open(contract.file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  const fileWindow = window.open("", "_blank");
+
+  generatingId.value = contract.id;
+
+  try {
+    const updated = await generateStudentContractFile(contract.id);
+
+    const index = rows.value.findIndex((row) => row.id === contract.id);
+
+    if (index !== -1) {
+      rows.value[index] = {
+        ...rows.value[index],
+        ...updated,
+      };
+    }
+
+    if (updated.file_url && fileWindow) {
+      fileWindow.location.href = updated.file_url;
+    } else {
+      fileWindow?.close();
+
+      throw new Error("A API não retornou o arquivo gerado.");
+    }
+  } catch (error) {
+    fileWindow?.close();
+
+    console.error(error);
+
+    $q.notify({
+      message: "Erro ao gerar o arquivo do contrato.",
+      type: "negative",
+    });
+  } finally {
+    generatingId.value = null;
+  }
+};
+
+const handleSignedContractFile = (contract) => {
+  if (contract.signed_file_url) {
+    window.open(contract.signed_file_url, "_blank", "noopener,noreferrer");
+
+    return;
+  }
+
+  triggerFileInput(contract);
+};
+
 const handleReactivate = (contract) => {
   confirmAction(
     "Reativar Contrato",
@@ -301,23 +358,26 @@ const onFileSelected = async (event) => {
 
     formData.append("file", file);
 
-    const updated = await attachContractFile(
-      pendingContractId,
-      formData,
-    );
+    const updated = await attachSignedContractFile(pendingContractId, formData);
 
-    const index = rows.value.findIndex(
-      (row) => row.id === pendingContractId,
-    );
+    const index = rows.value.findIndex((row) => row.id === pendingContractId);
 
     if (index !== -1) {
-      rows.value[index] = updated;
+      rows.value[index] = {
+        ...rows.value[index],
+        ...updated,
+      };
     }
+
+    $q.notify({
+      message: "Contrato assinado enviado com sucesso.",
+      type: "positive",
+    });
   } catch (error) {
     console.error(error);
 
     $q.notify({
-      message: "Erro ao anexar arquivo.",
+      message: "Erro ao anexar o contrato assinado.",
       type: "negative",
     });
   } finally {
@@ -360,4 +420,4 @@ const triggerFileInput = (row) => {
 };
 
 onMounted(loadContracts);
-</script>
+</script>