Просмотр исходного кода

feat: add btn para gerar e visualizar pdf dos templates de contrato para estudante

Gustavo Mantovani 23 часов назад
Родитель
Сommit
b00a0d2a5e

+ 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}`);
-};

+ 74 - 24
src/pages/contracts/ContractsPage.vue

@@ -77,9 +77,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 +87,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
@@ -99,9 +95,28 @@
                 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>
 
@@ -116,8 +131,7 @@
                   <q-list style="min-width: 170px">
                     <template
                       v-if="
-                        row.status === 'frozen' ||
-                        row.status === 'cancelled'
+                        row.status === 'frozen' || row.status === 'cancelled'
                       "
                     >
                       <q-item
@@ -125,9 +139,7 @@
                         clickable
                         @click="handleReactivate(row)"
                       >
-                        <q-item-section>
-                          Reativar Contrato
-                        </q-item-section>
+                        <q-item-section> Reativar Contrato </q-item-section>
                       </q-item>
                     </template>
 
@@ -137,9 +149,7 @@
                         clickable
                         @click="handleFreeze(row)"
                       >
-                        <q-item-section>
-                          Trancar Contrato
-                        </q-item-section>
+                        <q-item-section> Trancar Contrato </q-item-section>
                       </q-item>
 
                       <q-item
@@ -147,9 +157,7 @@
                         clickable
                         @click="handleCancel(row)"
                       >
-                        <q-item-section>
-                          Cancelar Contrato
-                        </q-item-section>
+                        <q-item-section> Cancelar Contrato </q-item-section>
                       </q-item>
                     </template>
                   </q-list>
@@ -166,6 +174,7 @@
 <script setup>
 import {
   cancelContract,
+  generateStudentContractFile,
   getAllContracts,
   reactivateContract,
 } from "src/api/studentContract";
@@ -192,6 +201,7 @@ const store = userStore();
 const permissions = permissionStore();
 
 const isLoading = ref(false);
+const generatingId = ref(null);
 const rows = ref([]);
 
 const canEdit = computed(() =>
@@ -248,9 +258,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 +302,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 +323,50 @@ 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 handleReactivate = (contract) => {
   confirmAction(
     "Reativar Contrato",

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

@@ -69,6 +69,31 @@
               {{ 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-td>
+          </template>
         </q-table>
       </q-card-section>
 
@@ -88,7 +113,11 @@
 <script setup>
 import { computed, onMounted, ref } from "vue";
 import { getUnitPackagesForSelect } from "src/api/package";
-import { getStudentContractById } from "src/api/studentContract";
+import {
+  generateStudentContractFile,
+  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 +139,17 @@ 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 packages = ref([]);
 
+const canEdit = computed(() =>
+  permissions.getAccess("franchisee_contracts", "edit"),
+);
+
 const columns = [
   {
     align: "center",
@@ -164,15 +199,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,9 +226,7 @@ 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) =>
@@ -225,6 +262,50 @@ const statusLabel = (status) => {
   return "Inativo";
 };
 
+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;
+  }
+};
+
 onMounted(async () => {
   loading.value = true;
 

+ 95 - 54
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,45 @@
               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"
+              icon="mdi-file-sign"
               outline
               style="width: 36px"
               :loading="uploadingId === row.id"
               @click.prevent.stop="triggerFileInput(row)"
-            />
+            >
+              <q-tooltip>
+                {{
+                  row.signed_file_url
+                    ? "Substituir contrato assinado"
+                    : "Anexar contrato assinado"
+                }}
+              </q-tooltip>
+            </q-btn>
 
             <q-btn
               v-if="canEdit"
@@ -82,41 +100,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 +132,9 @@
 
 <script setup>
 import {
-  attachContractFile,
+  attachSignedContractFile,
   cancelContract,
+  generateStudentContractFile,
   getStudentContracts,
   reactivateContract,
 } from "src/api/studentContract";
@@ -159,6 +161,7 @@ const permissions = permissionStore();
 
 const fileInputRef = ref(null);
 const rows = ref([]);
+const generatingId = ref(null);
 const uploadingId = ref(null);
 
 const canAdd = computed(() =>
@@ -211,9 +214,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 +254,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 +275,50 @@ 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 handleReactivate = (contract) => {
   confirmAction(
     "Reativar Contrato",
@@ -301,23 +344,21 @@ 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,
+      };
     }
   } catch (error) {
     console.error(error);
 
     $q.notify({
-      message: "Erro ao anexar arquivo.",
+      message: "Erro ao anexar o contrato assinado.",
       type: "negative",
     });
   } finally {
@@ -360,4 +401,4 @@ const triggerFileInput = (row) => {
 };
 
 onMounted(loadContracts);
-</script>
+</script>