Explorar el Código

Merge branch 'feature/GINC-GAB-CB003-descongelar-contrato' of Softpar/sfp_vue_franchisee_ginastica_cerebro into development

Gabriel Alves hace 1 mes
padre
commit
02db949736

+ 17 - 2
src/api/studentContract.js

@@ -1,5 +1,10 @@
 import api from "src/api";
 
+export const getStudentContractById = async (id) => {
+  const { data } = await api.get(`/student-contract/${id}`);
+  return data.payload;
+};
+
 export const getStudentContracts = async (studentId) => {
   const { data } = await api.get("/student-contract", { params: { student_id: studentId } });
   return data.payload;
@@ -27,8 +32,18 @@ export const attachContractFile = async (id, formData) => {
   return data.payload;
 };
 
-export const updateContractStatus = async (id, status) => {
-  const { data } = await api.patch(`/student-contract/${id}/status`, { status });
+export const freezeContract = async (id) => {
+  const { data } = await api.post(`/student-contract/${id}/freeze`);
+  return data.payload;
+};
+
+export const cancelContract = async (id) => {
+  const { data } = await api.post(`/student-contract/${id}/cancel`);
+  return data.payload;
+};
+
+export const reactivateContract = async (id) => {
+  const { data } = await api.post(`/student-contract/${id}/reactivate`);
   return data.payload;
 };
 

+ 9 - 6
src/api/student_media.js

@@ -1,14 +1,17 @@
 import api from "src/api";
 
 export const getStudentMedias = async (studentId) => {
-  const { data } = await api.get("/media", {
-    params: { origem: "student", origem_id: studentId },
-  });
+  const { data } = await api.get("/student-media", { params: { student_id: studentId } });
+  return data.payload;
+};
+
+export const getContractMedias = async (contractId) => {
+  const { data } = await api.get("/student-media", { params: { student_contract_id: contractId } });
   return data.payload;
 };
 
 export const createStudentMedia = async (formData) => {
-  const { data } = await api.post("/media", formData, {
+  const { data } = await api.post("/student-media", formData, {
     headers: { "Content-Type": "multipart/form-data" },
   });
   return data.payload;
@@ -16,13 +19,13 @@ export const createStudentMedia = async (formData) => {
 
 export const updateStudentMedia = async (id, formData) => {
   formData.append("_method", "PUT");
-  const { data } = await api.post(`/media/${id}`, formData, {
+  const { data } = await api.post(`/student-media/${id}`, formData, {
     headers: { "Content-Type": "multipart/form-data" },
   });
   return data.payload;
 };
 
 export const deleteStudentMedia = async (id) => {
-  const { data } = await api.delete(`/media/${id}`);
+  const { data } = await api.delete(`/student-media/${id}`);
   return data;
 };

+ 110 - 1
src/pages/contracts/ContractsPage.vue

@@ -52,6 +52,55 @@
             />
           </q-td>
         </template>
+
+        <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-eye-outline"
+              style="width: 36px"
+              @click.prevent.stop="handleView(row)"
+            />
+            <q-btn
+              outline
+              icon="mdi-dots-vertical"
+              style="width: 36px"
+              @click.prevent.stop
+            >
+              <q-menu>
+                <q-list style="min-width: 170px">
+                  <template 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>
+                  </template>
+                  <template v-else>
+                    <q-item
+                      v-close-popup
+                      clickable
+                      @click="handleFreeze(row)"
+                    >
+                      <q-item-section>Congelar 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>
+                  </template>
+                </q-list>
+              </q-menu>
+            </q-btn>
+            </q-item-section>
+          </q-td>
+        </template>
       </DefaultTable>
     </div>
   </div>
@@ -59,11 +108,20 @@
 
 <script setup>
 import { ref, computed, onMounted } from "vue";
+import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
-import { getAllContracts } from "src/api/studentContract";
+import ContractActionConfirmDialog from "src/pages/students/components/ContractActionConfirmDialog.vue";
+import ViewContractDialog from "src/pages/students/components/ViewContractDialog.vue";
+import {
+  getAllContracts,
+  freezeContract,
+  cancelContract,
+  reactivateContract,
+} from "src/api/studentContract";
 
+const $q = useQuasar();
 const rows = ref([]);
 const isLoading = ref(false);
 
@@ -78,6 +136,7 @@ const columns = ref([
   { name: "contato", label: "Contato/Endereço", field: null, align: "left" },
   { name: "package_name", label: "Pacote", field: "package_name", align: "left" },
   { name: "status", label: "Status Contrato", field: "status", align: "center" },
+  { name: "actions", label: "Ações", field: null, align: "center" },
 ]);
 
 async function loadContracts() {
@@ -89,6 +148,56 @@ async function loadContracts() {
   }
 }
 
+function handleView(contract) {
+  $q.dialog({
+    component: ViewContractDialog,
+    componentProps: { id: contract.id },
+  });
+}
+
+function confirmAction(title, message, apiFn, contract) {
+  $q.dialog({
+    component: ContractActionConfirmDialog,
+    componentProps: { title, message },
+  }).onOk(async () => {
+    try {
+      const updated = await apiFn(contract.id);
+      const idx = rows.value.findIndex((r) => r.id === contract.id);
+      if (idx !== -1) rows.value[idx] = { ...rows.value[idx], status: updated.status };
+    } catch (e) {
+      console.error(e);
+      $q.notify({ type: "negative", message: "Erro ao atualizar status do contrato." });
+    }
+  });
+}
+
+function handleFreeze(contract) {
+  confirmAction(
+    "Congelar Contrato",
+    "Você tem certeza que deseja CONGELAR este contrato? Isso irá gerar multas e cancelamento da cobrança recorrente.",
+    freezeContract,
+    contract,
+  );
+}
+
+function handleCancel(contract) {
+  confirmAction(
+    "Cancelar Contrato",
+    "Você tem certeza que deseja CANCELAR este contrato? Isso irá gerar multas e cancelamento da cobrança recorrente.",
+    cancelContract,
+    contract,
+  );
+}
+
+function handleReactivate(contract) {
+  confirmAction(
+    "Reativar Contrato",
+    "Você tem certeza que deseja REATIVAR este contrato?",
+    reactivateContract,
+    contract,
+  );
+}
+
 function statusColor(status) {
   if (status === "active") return "positive";
   if (status === "frozen") return "info";

+ 99 - 1
src/pages/students/components/CreateContractDialog.vue → src/pages/students/components/AddEditContractDialog.vue

@@ -9,7 +9,23 @@
         @close="onDialogCancel"
       />
 
+      <template v-if="props.contract">
+        <q-tabs
+          v-model="activeTab"
+          dense
+          align="left"
+          class="q-px-md text-grey-7"
+          active-color="primary"
+          indicator-color="primary"
+        >
+          <q-tab name="dados" label="Dados do Contrato" />
+          <q-tab name="midias" label="Mídias do Contrato" />
+        </q-tabs>
+        <q-separator />
+      </template>
+
       <q-card-section class="q-pt-sm" style="height: 65vh; overflow-y: auto">
+        <div v-show="activeTab === 'dados'">
         <div class="text-subtitle1 q-mb-md">Dados do Aluno</div>
 
         <div class="row q-col-gutter-sm">
@@ -252,6 +268,40 @@
             />
           </div>
         </div>
+        </div>
+
+        <div v-if="props.contract" v-show="activeTab === 'midias'">
+          <DefaultTable
+            v-model:rows="medias"
+            title="Mídias"
+            :columns="mediaColumns"
+            descricao="mídias"
+            :feminino="true"
+            no-api-call
+            :show-search-field="false"
+            :loading="loadingMedias"
+          >
+            <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-eye-outline"
+                    style="width: 36px"
+                    @click.prevent.stop="openFile(row.file_url)"
+                  />
+                  <q-btn
+                    outline
+                    icon="mdi-trash-can-outline"
+                    style="width: 36px"
+                    color="negative"
+                    @click.prevent.stop="handleDeleteMedia(row)"
+                  />
+                </q-item-section>
+              </q-td>
+            </template>
+          </DefaultTable>
+        </div>
       </q-card-section>
 
       <q-separator />
@@ -264,6 +314,7 @@
           @click="onDialogCancel"
         />
         <q-btn
+          v-if="activeTab === 'dados'"
           color="primary"
           label="SALVAR"
           :loading="saving"
@@ -276,10 +327,11 @@
 
 <script setup>
 import { computed, ref, watch, onMounted } from "vue";
-import { useDialogPluginComponent } from "quasar";
+import { useDialogPluginComponent, useQuasar } 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 DefaultTable from "src/components/defaults/DefaultTable.vue";
 import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
@@ -290,6 +342,7 @@ import {
   createStudentContract,
   updateStudentContract,
 } from "src/api/studentContract";
+import { getContractMedias, deleteStudentMedia } from "src/api/student_media";
 
 const props = defineProps({
   student: {
@@ -307,6 +360,11 @@ defineEmits([...useDialogPluginComponent.emits]);
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
+const $q = useQuasar();
+const activeTab = ref("dados");
+const medias = ref([]);
+const loadingMedias = ref(false);
+
 const trimTime = (t) => (t ? t.slice(0, 5) : null);
 
 const { form } = useFormUpdateTracker({
@@ -357,10 +415,50 @@ const weekdays = [
 
 const packages = ref([]);
 
+const mediaColumns = [
+  { name: "created_at", label: "Data de Anexo", field: "created_at", align: "left" },
+  { name: "actions", label: "Ações", field: null, align: "center" },
+];
+
+async function fetchMedias() {
+  if (!props.contract) return;
+  loadingMedias.value = true;
+  try {
+    medias.value = await getContractMedias(props.contract.id);
+  } finally {
+    loadingMedias.value = false;
+  }
+}
+
+watch(activeTab, (tab) => {
+  if (tab === "midias") fetchMedias();
+});
+
 onMounted(async () => {
   packages.value = await getUnitPackages();
 });
 
+function openFile(url) {
+  window.open(url, "_blank");
+}
+
+function handleDeleteMedia(media) {
+  $q.dialog({
+    title: "Excluir mídia",
+    message: "Deseja excluir esta mídia permanentemente?",
+    ok: { color: "negative", label: "Excluir" },
+    cancel: { color: "primary", outline: true, label: "Cancelar" },
+  }).onOk(async () => {
+    try {
+      await deleteStudentMedia(media.id);
+      medias.value = medias.value.filter((m) => m.id !== media.id);
+    } catch (e) {
+      console.error(e);
+      $q.notify({ type: "negative", message: "Erro ao excluir mídia." });
+    }
+  });
+}
+
 watch(
   () => form.package_id,
   (id) => {

+ 1 - 2
src/pages/students/components/AddStudentMediaDialog.vue

@@ -66,8 +66,7 @@ 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("student_id", studentId);
     formData.append("file", selectedFile.value);
     return createStudentMedia(formData);
   });

+ 7 - 2
src/pages/students/components/EditStudentDialog.vue

@@ -30,7 +30,7 @@
         </div>
 
         <div v-show="currentTab === 'media'">
-          <MediaTab :student-id="props.student.id" />
+          <MediaTab ref="mediaTabRef" :student-id="props.student.id" />
         </div>
       </q-card-section>
 
@@ -55,7 +55,7 @@
 </template>
 
 <script setup>
-import { ref, useTemplateRef, defineAsyncComponent } from "vue";
+import { ref, watch, useTemplateRef, defineAsyncComponent } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
@@ -91,9 +91,14 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
 const studentDataTabRef = useTemplateRef("studentDataTabRef");
+const mediaTabRef = useTemplateRef("mediaTabRef");
 
 const currentTab = ref("profile");
 
+watch(currentTab, (tab) => {
+  if (tab === "media") mediaTabRef.value?.refresh();
+});
+
 const { loading: saving, execute } = useSubmitHandler({
   onSuccess: () => onDialogOK(true),
 });

+ 1 - 2
src/pages/students/components/EditStudentMediaDialog.vue

@@ -66,8 +66,7 @@ async function onSubmit() {
   await execute(() => {
     const formData = new FormData();
     formData.append("name", form.value.name);
-    formData.append("origem", media.origem);
-    formData.append("origem_id", media.origem_id);
+    formData.append("student_id", media.student_id);
     if (selectedFile.value) {
       formData.append("file", selectedFile.value);
     }

+ 268 - 0
src/pages/students/components/ViewContractDialog.vue

@@ -0,0 +1,268 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card
+      class="q-dialog-plugin overflow-hidden"
+      style="width: 100%; max-width: 1350px"
+    >
+      <DefaultDialogHeader title="Visualizar Contrato" @close="onDialogCancel" />
+
+      <template v-if="loading">
+        <q-card-section style="height: 65vh" class="flex flex-center">
+          <q-spinner size="40px" color="primary" />
+        </q-card-section>
+      </template>
+
+      <template v-else-if="contract">
+        <q-tabs
+          v-model="activeTab"
+          dense
+          align="left"
+          class="q-px-md text-grey-7"
+          active-color="primary"
+          indicator-color="primary"
+        >
+          <q-tab name="dados" label="Dados do Contrato" />
+          <q-tab name="midias" label="Mídias do Contrato" />
+        </q-tabs>
+        <q-separator />
+
+        <q-card-section class="q-pt-sm" style="height: 65vh; overflow-y: auto">
+          <div v-show="activeTab === 'dados'">
+            <div class="text-subtitle1 q-mb-md">Dados do Aluno</div>
+
+            <div class="row q-col-gutter-sm">
+              <div class="col-12">
+                <DefaultInput :model-value="contract.student_name" label="Aluno" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="contract.student_document" label="CPF" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="contract.student_birth_date" label="Data de Nascimento" disable />
+              </div>
+            </div>
+
+            <div class="text-subtitle1 q-mt-lg q-mb-md">Dados do Contrato</div>
+
+            <div class="row q-col-gutter-sm">
+              <div class="col-4">
+                <DefaultInput :model-value="contract.protocol" label="Protocolo" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.signature_date" label="Data Assinatura" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.end_date" label="Data Encerramento" disable />
+              </div>
+
+              <div class="col-5">
+                <DefaultInput :model-value="contract.package_name" label="Pacote de Aulas" disable />
+              </div>
+
+              <div class="col-7">
+                <DefaultInput :model-value="contract.class_quantity" label="Qtd. Aulas" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="weekdayLabel(contract.weekday)" label="Dia da Semana" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.start_time" label="Hora de Início" disable>
+                  <template #append><q-icon name="mdi-clock-outline" /></template>
+                </DefaultInput>
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.end_time" label="Hora de Término" disable>
+                  <template #append><q-icon name="mdi-clock-outline" /></template>
+                </DefaultInput>
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="weekdayLabel(contract.second_weekday)" label="2° Dia da Semana" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.second_start_time" label="Hora de Início" disable>
+                  <template #append><q-icon name="mdi-clock-outline" /></template>
+                </DefaultInput>
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.second_end_time" label="Hora de Término" disable>
+                  <template #append><q-icon name="mdi-clock-outline" /></template>
+                </DefaultInput>
+              </div>
+            </div>
+
+            <div class="text-subtitle1 q-mt-lg q-mb-md">Dados Financeiros</div>
+
+            <div class="row q-col-gutter-sm">
+              <div class="col-4">
+                <DefaultInput :model-value="contract.due_day" label="Dia de Vencimento" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultCurrencyInput :model-value="contract.tax_register" label="Taxa de Matrícula" disable />
+              </div>
+
+              <div class="col-4">
+                <DefaultInput :model-value="contract.class_quantity" label="Total de Aulas" disable />
+              </div>
+
+              <div class="col-3">
+                <DefaultCurrencyInput :model-value="contract.down_payment" label="Entrada" disable />
+              </div>
+
+              <div class="col-3">
+                <DefaultInput :model-value="contract.installments ? `${contract.installments}x` : null" label="Parcelas" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="contract.early_payment_discount" label="Desconto até o vencimento (%)" disable />
+              </div>
+
+              <div class="col-3">
+                <DefaultCurrencyInput :model-value="contract.material_value" label="Valor dos Materiais" disable />
+              </div>
+
+              <div class="col-3">
+                <DefaultInput :model-value="contract.material_installments ? `${contract.material_installments}x` : null" label="Parcelas" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="contract.interest_rate" label="Juros (%) a.m" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="paymentMethodLabel(contract.payment_method)" label="Forma de Pagamento" disable />
+              </div>
+
+              <div class="col-6">
+                <DefaultInput :model-value="contract.fine_cancelled" label="Multa (%)" disable />
+              </div>
+            </div>
+          </div>
+
+          <div v-show="activeTab === 'midias'">
+            <DefaultTable
+              v-model:rows="medias"
+              title="Mídias"
+              :columns="mediaColumns"
+              descricao="mídias"
+              :feminino="true"
+              no-api-call
+              :show-search-field="false"
+              :loading="loadingMedias"
+            >
+              <template #body-cell-actions="{ row }">
+                <q-td align="center">
+                  <q-btn
+                    outline
+                    icon="mdi-eye-outline"
+                    style="width: 36px"
+                    @click.prevent.stop="openFile(row.file_url)"
+                  />
+                </q-td>
+              </template>
+            </DefaultTable>
+          </div>
+        </q-card-section>
+      </template>
+
+      <q-separator />
+
+      <q-card-actions align="right">
+        <q-btn outline color="primary" label="FECHAR" @click="onDialogCancel" />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, watch, onMounted } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import { getStudentContractById } from "src/api/studentContract";
+import { getContractMedias } from "src/api/student_media";
+
+const props = defineProps({
+  id: {
+    type: Number,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+
+const contract = ref(null);
+const loading = ref(false);
+const activeTab = ref("dados");
+const medias = ref([]);
+const loadingMedias = ref(false);
+
+onMounted(async () => {
+  loading.value = true;
+  try {
+    contract.value = await getStudentContractById(props.id);
+  } finally {
+    loading.value = false;
+  }
+});
+
+async function fetchMedias() {
+  loadingMedias.value = true;
+  try {
+    medias.value = await getContractMedias(props.id);
+  } finally {
+    loadingMedias.value = false;
+  }
+}
+
+watch(activeTab, (tab) => {
+  if (tab === "midias") fetchMedias();
+});
+
+function openFile(url) {
+  window.open(url, "_blank");
+}
+
+const mediaColumns = [
+  { name: "created_at", label: "Data de Anexo", field: "created_at", align: "left" },
+  { name: "actions", label: "Ações", field: null, align: "center" },
+];
+
+const weekdays = {
+  0: "Domingo",
+  1: "Segunda",
+  2: "Terça",
+  3: "Quarta",
+  4: "Quinta",
+  5: "Sexta",
+  6: "Sábado",
+};
+
+const paymentMethods = {
+  pix: "Pix",
+  credit_card: "Cartão de Crédito",
+  debit_card: "Cartão de Débito",
+};
+
+function weekdayLabel(value) {
+  return value != null ? weekdays[value] ?? null : null;
+}
+
+function paymentMethodLabel(value) {
+  return value ? paymentMethods[value] ?? value : null;
+}
+</script>

+ 69 - 44
src/pages/students/tabs/ContractTab.vue

@@ -64,13 +64,31 @@
                   >
                     <q-item-section>Visualizar arquivo</q-item-section>
                   </q-item>
-                  <q-item
-                    v-close-popup
-                    clickable
-                    @click="handleContractActions(row)"
-                  >
-                    <q-item-section>Ações do Contrato</q-item-section>
-                  </q-item>
+                  <template 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>
+                  </template>
+                  <template v-else>
+                    <q-item
+                      v-close-popup
+                      clickable
+                      @click="handleFreeze(row)"
+                    >
+                      <q-item-section>Congelar 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>
+                  </template>
                 </q-list>
               </q-menu>
             </q-btn>
@@ -85,13 +103,14 @@
 import { ref, onMounted } from "vue";
 import { useQuasar } from "quasar";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
-import CreateContractDialog from "src/pages/students/components/CreateContractDialog.vue";
-import ContractActionsDialog from "src/pages/students/components/ContractActionsDialog.vue";
+import AddEditContractDialog from "src/pages/students/components/AddEditContractDialog.vue";
 import ContractActionConfirmDialog from "src/pages/students/components/ContractActionConfirmDialog.vue";
 import {
   getStudentContracts,
   attachContractFile,
-  updateContractStatus,
+  freezeContract,
+  cancelContract,
+  reactivateContract,
 } from "src/api/studentContract";
 
 const props = defineProps({
@@ -115,7 +134,7 @@ onMounted(loadContracts);
 
 function openDialog(contract = null) {
   $q.dialog({
-    component: CreateContractDialog,
+    component: AddEditContractDialog,
     componentProps: { student: props.student, contract },
   }).onOk(loadContracts);
 }
@@ -158,43 +177,49 @@ function openFile(url) {
   window.open(url, "_blank");
 }
 
-const actionLabels = {
-  frozen: {
-    title: "Congelar Contrato",
-    message:
-      "Você tem certeza que deseja CONGELAR este contrato? isso irá gerar multas e cancelamento da cobrança recorrente.",
-  },
-  cancelled: {
-    title: "Cancelar Contrato",
-    message:
-      "Você tem certeza que deseja CANCELAR este contrato? isso irá gerar multas e cancelamento da cobrança recorrente.",
-  },
-};
-
-function handleContractActions(contract) {
+function confirmAction(title, message, apiFn, contract) {
   $q.dialog({
-    component: ContractActionsDialog,
-  }).onOk((status) => {
-    const { title, message } = actionLabels[status];
-    $q.dialog({
-      component: ContractActionConfirmDialog,
-      componentProps: { title, message },
-    }).onOk(async () => {
-      try {
-        const updated = await updateContractStatus(contract.id, status);
-        const idx = rows.value.findIndex((r) => r.id === contract.id);
-        if (idx !== -1) rows.value[idx] = updated;
-      } catch (e) {
-        console.error(e);
-        $q.notify({
-          type: "negative",
-          message: "Erro ao atualizar status do contrato.",
-        });
-      }
-    });
+    component: ContractActionConfirmDialog,
+    componentProps: { title, message },
+  }).onOk(async () => {
+    try {
+      const updated = await apiFn(contract.id);
+      const idx = rows.value.findIndex((r) => r.id === contract.id);
+      if (idx !== -1) rows.value[idx] = updated;
+    } catch (e) {
+      console.error(e);
+      $q.notify({ type: "negative", message: "Erro ao atualizar status do contrato." });
+    }
   });
 }
 
+function handleFreeze(contract) {
+  confirmAction(
+    "Congelar Contrato",
+    "Você tem certeza que deseja CONGELAR este contrato? Isso irá gerar multas e cancelamento da cobrança recorrente.",
+    freezeContract,
+    contract,
+  );
+}
+
+function handleCancel(contract) {
+  confirmAction(
+    "Cancelar Contrato",
+    "Você tem certeza que deseja CANCELAR este contrato? Isso irá gerar multas e cancelamento da cobrança recorrente.",
+    cancelContract,
+    contract,
+  );
+}
+
+function handleReactivate(contract) {
+  confirmAction(
+    "Reativar Contrato",
+    "Você tem certeza que deseja REATIVAR este contrato?",
+    reactivateContract,
+    contract,
+  );
+}
+
 function statusColor(status) {
   if (status === "active") return "positive";
   if (status === "frozen") return "info";

+ 17 - 0
src/pages/students/tabs/MediaTab.vue

@@ -13,6 +13,19 @@
         <q-td>{{ rowIndex + 1 }}</q-td>
       </template>
 
+      <template #body-cell-type="{ row }">
+        <q-td>
+          <q-badge
+            :color="row.type === 'contract' ? 'info' : 'primary'"
+            :label="row.type === 'contract' ? 'Contrato' : 'Mídia'"
+          />
+        </q-td>
+      </template>
+
+      <template #body-cell-name="{ row }">
+        <q-td>{{ row.name ?? '—' }}</q-td>
+      </template>
+
       <template #body-cell-actions="{ row }">
         <q-btn
           flat
@@ -24,6 +37,7 @@
           @click.stop="openFile(row.file_url)"
         />
         <q-btn
+          v-if="row.type !== 'contract'"
           flat
           round
           dense
@@ -61,6 +75,7 @@ const rows = ref([]);
 const columns = [
   { name: "item", label: "Item", field: "id", align: "left" },
   { name: "date", label: "Data", field: "created_at", align: "left" },
+  { name: "type", label: "Tipo", field: "type", align: "left" },
   { name: "name", label: "Nome", field: "name", align: "left" },
   { name: "actions", label: "Ações", field: null, align: "right" },
 ];
@@ -113,4 +128,6 @@ function confirmDelete(row) {
 }
 
 onMounted(fetchMedias);
+
+defineExpose({ refresh: fetchMedias });
 </script>