Explorar el Código

feat: ✨ feat (parceiro / convenio opcao de excluir) adicionada opcao de excluir parceiro / convenio

foi adicionada opcao para excluir parceiro e convenio, sempre mantendo historico, e com opcao para restaurar registros excluidos

fase:dev | origin:escopo
Gustavo Zanatta hace 4 días
padre
commit
56197ae558

+ 12 - 0
src/api/partnerAgreement.js

@@ -61,6 +61,18 @@ export const deletePartnerAgreement = async (id) => {
   await api.delete(`/partner-agreement/${id}`);
 };
 
+export const getTrashedPartnerAgreements = async ({ type } = {}) => {
+  const { data } = await api.get("/partner-agreement/trashed", {
+    params: type ? { type } : {},
+  });
+  return data.payload;
+};
+
+export const restorePartnerAgreement = async (id) => {
+  const { data } = await api.put(`/partner-agreement/${id}/restore`);
+  return data.payload;
+};
+
 export const uploadPartnerLogo = async (id, file) => {
   const form = new FormData();
   form.append("logo", file);

+ 9 - 0
src/i18n/locales/en.json

@@ -735,6 +735,15 @@
     "cadastro_title": "Partner Registration",
     "cadastro_parceiro": "Partner Registration",
     "dados_parceiro": "Partner Data",
+    "delete_title": "Delete record",
+    "delete_message": "Delete {name}? Linked services and the partner's access will be deactivated. The data stays preserved in the history.",
+    "delete_confirm": "Confirm deletion",
+    "delete_success": "Record deleted successfully",
+    "deleted_parceiros_title": "Deleted partners",
+    "deleted_convenios_title": "Deleted agreements",
+    "deleted_at": "Deleted at",
+    "restore": "Restore",
+    "restore_success": "Record restored successfully",
     "tab_dados": "Company Data",
     "tab_contato": "Contact",
     "tab_endereco": "Address",

+ 9 - 0
src/i18n/locales/es.json

@@ -735,6 +735,15 @@
     "cadastro_title": "Registro de Socio",
     "cadastro_parceiro": "Registro de Socio",
     "dados_parceiro": "Datos del Socio",
+    "delete_title": "Eliminar registro",
+    "delete_message": "¿Confirma la eliminación de {name}? Los servicios vinculados y el acceso del socio serán desactivados. Los datos se conservan en el historial.",
+    "delete_confirm": "Confirmar eliminación",
+    "delete_success": "Registro eliminado con éxito",
+    "deleted_parceiros_title": "Socios eliminados",
+    "deleted_convenios_title": "Convenios eliminados",
+    "deleted_at": "Eliminado el",
+    "restore": "Restaurar",
+    "restore_success": "Registro restaurado con éxito",
     "tab_dados": "Datos de la Empresa",
     "tab_contato": "Contacto",
     "tab_endereco": "Dirección",

+ 9 - 0
src/i18n/locales/pt.json

@@ -736,6 +736,15 @@
     "cadastro_title": "Cadastro de Parceiro",
     "cadastro_parceiro": "Cadastro de Parceiro",
     "dados_parceiro": "Dados de Parceiro",
+    "delete_title": "Excluir cadastro",
+    "delete_message": "Confirma a exclusão de {name}? Os serviços vinculados e o acesso do parceiro serão desativados. Os dados ficam preservados no histórico.",
+    "delete_confirm": "Confirmar exclusão",
+    "delete_success": "Cadastro excluído com sucesso",
+    "deleted_parceiros_title": "Parceiros excluídos",
+    "deleted_convenios_title": "Convênios excluídos",
+    "deleted_at": "Excluído em",
+    "restore": "Restaurar",
+    "restore_success": "Registro restaurado com sucesso",
     "tab_dados": "Dados da Empresa",
     "tab_contato": "Contato",
     "tab_endereco": "Endereço",

+ 1 - 0
src/pages/parceiros-convenios/ConvenioCadastroPage.vue

@@ -2,6 +2,7 @@
   <CadastroFormPanel
     entity-type="agreement"
     cadastro-route-name="ConvenioCadastroPage"
+    list-route-name="ConveniosMedicosPage"
     :show-scheduling="true"
   />
 </template>

+ 23 - 0
src/pages/parceiros-convenios/ConveniosMedicosPage.vue

@@ -11,6 +11,15 @@
     <DefaultHeaderPage>
       <template #after>
         <div v-if="isAdministrador" class="flex gap-sm q-mt-md">
+          <q-btn
+            unelevated
+            dense
+            class="btn-gradient"
+            icon="mdi-trash-can-outline"
+            padding="6px 10px"
+            :title="$t('parceiro.deleted_convenios_title')"
+            @click="onOpenDeleted"
+          />
           <q-btn
             unelevated
             dense
@@ -116,6 +125,9 @@ import PartnerAgreementCard from "./components/PartnerAgreementCard.vue";
 const ImportHistoryDialog = defineAsyncComponent(
   () => import("src/components/ImportHistoryDialog.vue"),
 );
+const DeletedPartnersDialog = defineAsyncComponent(
+  () => import("./components/DeletedPartnersDialog.vue"),
+);
 
 const router = useRouter();
 const $q    = useQuasar();
@@ -175,6 +187,17 @@ const onOpenImportHistory = () => {
   $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'convenio' } });
 };
 
+const onOpenDeleted = () => {
+  if (!permission_store.getAccess("parceiro.convenio", "delete")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.delete") });
+    return;
+  }
+  $q.dialog({
+    component: DeletedPartnersDialog,
+    componentProps: { entityType: "agreement" },
+  }).onOk(() => loadItems());
+};
+
 const onImportConveniosClick = () => {
   if (!permission_store.getAccess("parceiro.convenio", "add")) {
     $q.notify({ type: "negative", message: t("validation.permissions.add") });

+ 1 - 0
src/pages/parceiros-convenios/ParceiroCadastroPage.vue

@@ -2,6 +2,7 @@
   <CadastroFormPanel
     entity-type="partner"
     cadastro-route-name="ParceiroCadastroPage"
+    list-route-name="ParceirosPage"
     :show-scheduling="false"
   />
 </template>

+ 23 - 0
src/pages/parceiros-convenios/ParceirosConveniosPage.vue

@@ -11,6 +11,15 @@
     <DefaultHeaderPage>
       <template #after>
         <div v-if="isAdministrador" class="flex gap-sm q-mt-md">
+          <q-btn
+            unelevated
+            dense
+            class="btn-gradient"
+            icon="mdi-trash-can-outline"
+            padding="6px 10px"
+            :title="$t('parceiro.deleted_parceiros_title')"
+            @click="onOpenDeleted"
+          />
           <q-btn
             unelevated
             dense
@@ -114,6 +123,9 @@ import { useImportPoller } from "src/composables/useImportPoller";
 const ImportHistoryDialog = defineAsyncComponent(
   () => import("src/components/ImportHistoryDialog.vue"),
 );
+const DeletedPartnersDialog = defineAsyncComponent(
+  () => import("./components/DeletedPartnersDialog.vue"),
+);
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import PartnerAgreementCard from "./components/PartnerAgreementCard.vue";
 
@@ -175,6 +187,17 @@ const onOpenImportHistory = () => {
   $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'parceiro' } });
 };
 
+const onOpenDeleted = () => {
+  if (!permission_store.getAccess("parceiro.convenio", "delete")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.delete") });
+    return;
+  }
+  $q.dialog({
+    component: DeletedPartnersDialog,
+    componentProps: { entityType: "partner" },
+  }).onOk(() => loadPartners());
+};
+
 const onImportParceirosClick = () => {
   if (!permission_store.getAccess("parceiro.convenio", "add")) {
     $q.notify({ type: "negative", message: t("validation.permissions.add") });

+ 44 - 2
src/pages/parceiros-convenios/components/CadastroFormPanel.vue

@@ -110,7 +110,17 @@
             </div>
           </q-card>
 
-          <div class="row justify-end q-py-md bg-violet-light">
+          <div class="row justify-end items-center q-py-md bg-violet-light" style="gap: 8px">
+            <q-btn
+              v-if="entityId"
+              outline
+              color="negative"
+              :label="$t('common.actions.delete')"
+              icon="mdi-trash-can-outline"
+              :loading="loadingDelete"
+              :disable="loadingDados"
+              @click="onDelete"
+            />
             <q-btn
               unelevated
               class="btn-gradient"
@@ -118,7 +128,7 @@
               icon="mdi-check"
               type="submit"
               :loading="loadingDados"
-              :disable="!hasUpdatedDados"
+              :disable="!hasUpdatedDados || loadingDelete"
             />
           </div>
         </q-form>
@@ -372,10 +382,12 @@ import {
   getPartnerAgreement,
   createPartnerAgreement,
   updatePartnerAgreement,
+  deletePartnerAgreement,
   uploadPartnerLogo,
   uploadPartnerMedia,
   deletePartnerMedia,
 } from "src/api/partnerAgreement";
+import { permissionStore } from "src/stores/permission";
 import { getServicesByPartner } from "src/api/partnerAgreementService";
 import axios from "axios";
 
@@ -392,6 +404,7 @@ import ServicoFormPanel from "./ServicoFormPanel.vue";
 const props = defineProps({
   entityType:        { type: String, required: true },
   cadastroRouteName: { type: String, required: true },
+  listRouteName:     { type: String, required: true },
   showScheduling:    { type: Boolean, default: false },
 });
 
@@ -400,6 +413,7 @@ const router = useRouter();
 const $q     = useQuasar();
 const { t }  = useI18n();
 const { inputRules } = useInputRules();
+const permission_store = permissionStore();
 
 const entityId = computed(() => (route.params.id ? Number(route.params.id) : null));
 
@@ -470,6 +484,34 @@ const saveDados = async () => {
   });
 };
 
+const loadingDelete = ref(false);
+
+const onDelete = () => {
+  if (!permission_store.getAccess("parceiro.convenio", "delete")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.delete") });
+    return;
+  }
+
+  $q.dialog({
+    title: t("parceiro.delete_title"),
+    message: t("parceiro.delete_message", { name: entity.value?.company_name ?? "" }),
+    cancel: { flat: true, label: t("common.actions.cancel"), color: "grey-7" },
+    ok: { unelevated: true, label: t("parceiro.delete_confirm"), color: "negative" },
+    persistent: true,
+  }).onOk(async () => {
+    loadingDelete.value = true;
+    try {
+      await deletePartnerAgreement(entityId.value);
+      $q.notify({ type: "positive", message: t("parceiro.delete_success") });
+      await router.push({ name: props.listRouteName });
+    } catch {
+      $q.notify({ type: "negative", message: t("http.errors.failed") });
+    } finally {
+      loadingDelete.value = false;
+    }
+  });
+};
+
 const formContatoRef = useTemplateRef("formContatoRef");
 const {
   form: formContato,

+ 140 - 0
src/pages/parceiros-convenios/components/DeletedPartnersDialog.vue

@@ -0,0 +1,140 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card style="min-width: 640px; max-width: 95vw">
+      <DefaultDialogHeader
+        :title="() => title"
+        icon="mdi-trash-can-outline"
+        @close="onClose"
+      />
+
+      <q-separator />
+
+      <q-card-section class="q-pa-none">
+        <div v-if="loading" class="flex flex-center q-pa-xl">
+          <q-spinner color="violet-normal" size="40px" />
+        </div>
+
+        <DefaultTable
+          v-else
+          v-model:rows="rows"
+          :columns="columns"
+          no-api-call
+          :show-search-field="false"
+        >
+          <template #body-cell-actions="{ row }">
+            <q-btn
+              outline
+              dense
+              no-caps
+              color="violet-normal"
+              icon="mdi-restore"
+              :label="$t('parceiro.restore')"
+              :loading="restoringId === row.id"
+              :disable="!!restoringId"
+              @click.prevent.stop="onRestore(row)"
+            />
+          </template>
+        </DefaultTable>
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { getTrashedPartnerAgreements, restorePartnerAgreement } from "src/api/partnerAgreement";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+
+const props = defineProps({
+  entityType: { type: String, required: true },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { t } = useI18n();
+const $q = useQuasar();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const rows = ref([]);
+const loading = ref(true);
+const restoringId = ref(null);
+const restoredAny = ref(false);
+
+const title = computed(() =>
+  props.entityType === "agreement"
+    ? t("parceiro.deleted_convenios_title")
+    : t("parceiro.deleted_parceiros_title"),
+);
+
+const columns = computed(() => [
+  {
+    name: "company_name",
+    label: t("parceiro.company_name"),
+    field: "company_name",
+    align: "left",
+    sortable: false,
+  },
+  {
+    name: "deleted_at",
+    label: t("parceiro.deleted_at"),
+    field: (row) => formatDateTime(row.deleted_at),
+    align: "left",
+    sortable: false,
+  },
+  { name: "actions", label: t("common.terms.actions"), field: "actions", align: "right" },
+]);
+
+const formatDateTime = (value) => {
+  if (!value) return "—";
+  const d = new Date(value.replace(" ", "T"));
+  if (Number.isNaN(d.getTime())) return value;
+  return d.toLocaleString("pt-BR", {
+    day: "2-digit", month: "2-digit", year: "numeric",
+    hour: "2-digit", minute: "2-digit",
+  });
+};
+
+const load = async () => {
+  loading.value = true;
+  try {
+    rows.value = await getTrashedPartnerAgreements({ type: props.entityType });
+  } catch {
+    rows.value = [];
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loading.value = false;
+  }
+};
+
+const onRestore = async (row) => {
+  restoringId.value = row.id;
+  try {
+    await restorePartnerAgreement(row.id);
+    rows.value = rows.value.filter((r) => r.id !== row.id);
+    restoredAny.value = true;
+    $q.notify({ type: "positive", message: t("parceiro.restore_success") });
+
+    if (rows.value.length === 0) {
+      onDialogOK(true);
+    }
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    restoringId.value = null;
+  }
+};
+
+const onClose = () => {
+  if (restoredAny.value) {
+    onDialogOK(true);
+    return;
+  }
+  onDialogCancel();
+};
+
+onMounted(load);
+</script>