소스 검색

feat: ✨ feat (agendamento) permite cancelar agendamento autorizado

alterada regra para permitir cancelar um agendamento autorizado caso seja para uma data/hora futura

fase:dev | origin: escopo
Gustavo Zanatta 1 일 전
부모
커밋
b3a614eeb9

+ 5 - 0
src/api/user.js

@@ -86,6 +86,11 @@ export const approveUser = async (id) => {
   return data.payload;
 };
 
+export const refuseUser = async (id) => {
+  const { data } = await api.put(`/user/${id}/refuse`);
+  return data.payload;
+};
+
 export const updateMyProfile = async (payload) => {
   const { data } = await api.put("/user/my/profile", payload);
   return data.payload;

+ 2 - 0
src/helpers/utils.js

@@ -110,6 +110,7 @@ const getStatusColor = (status) => {
   if (s === "pending")  return "warning-light";
   if (s === "on_leave") return "orange";
   if (s === "inactive") return "inactive";
+  if (s === "refused")  return "negative";
   return "grey-6";
 };
 
@@ -119,6 +120,7 @@ const getStatusI18nKey = (status) => {
   if (s === "inactive") return "common.status.inactive";
   if (s === "pending")  return "common.status.pending";
   if (s === "on_leave") return "common.status.on_leave";
+  if (s === "refused")  return "common.status.refused";
   return "common.status.inactive";
 };
 

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

@@ -161,6 +161,7 @@
       "inactive": "Inactive",
       "pending": "Pending",
       "on_leave": "On Leave",
+      "refused": "Refused",
       "canceled": "Canceled",
       "loading": "Please wait...",
       "yes": "Yes",
@@ -492,6 +493,9 @@
     "dependent_approval_dialog_title": "Dependent Approval",
     "dependent_approve_confirm": "Approve",
     "dependent_refuse_confirm": "Refuse",
+    "pending_approval_dialog_title": "Associate Approval",
+    "pending_approve": "Approve",
+    "pending_refuse": "Refuse",
     "import_associados": "Associates",
     "import_sync_result": "{created} created · {updated} updated · {inactivated} inactivated",
     "import_processing": "Import is still processing. Please refresh the page in a moment.",

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

@@ -162,6 +162,7 @@
       "inactive": "Inactivo",
       "pending": "Pendiente",
       "on_leave": "De Baja",
+      "refused": "Rechazado",
       "canceled": "Cancelado",
       "loading": "Por favor espere...",
       "yes": "Sí",
@@ -493,6 +494,9 @@
     "dependent_approval_dialog_title": "Aprobación de Dependiente",
     "dependent_approve_confirm": "Aprobar",
     "dependent_refuse_confirm": "Rechazar",
+    "pending_approval_dialog_title": "Aprobación de Asociado",
+    "pending_approve": "Aprobar",
+    "pending_refuse": "Rechazar",
     "import_associados": "Asociados",
     "import_sync_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
     "import_processing": "La importación está en progreso. Actualice la página en unos instantes.",

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

@@ -162,6 +162,7 @@
       "inactive": "Inativo",
       "pending": "Pendente",
       "on_leave": "Afastado",
+      "refused": "Recusado",
       "canceled": "Cancelado",
       "loading": "Por favor, aguarde...",
       "yes": "Sim",
@@ -493,6 +494,9 @@
     "dependent_approval_dialog_title": "Aprovação de Dependente",
     "dependent_approve_confirm": "Aprovar",
     "dependent_refuse_confirm": "Recusar",
+    "pending_approval_dialog_title": "Aprovação de Associado",
+    "pending_approve": "Aprovar",
+    "pending_refuse": "Recusar",
     "import_associados": "Associados",
     "import_sync_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
     "import_processing": "Importação em andamento. Atualize a página em alguns instantes.",

+ 26 - 3
src/pages/dashboard/DashboardPage.vue

@@ -41,9 +41,9 @@
             :api-call="activeApiCall"
             :add-item="false"
             sem-table-top
-            :open-item="activeCard === 'dependentes_pendentes'"
+            :open-item="['dependentes_pendentes', 'associados_pendentes'].includes(activeCard)"
             :show-search-field="!['ultimos_acessos', 'associados_com_acesso_app'].includes(activeCard)"
-            @on-row-click="onDependentRowClick"
+            @on-row-click="onRowClick"
           >
             <template #body-cell-status="{ row }">
               <q-td>
@@ -92,6 +92,7 @@ import { getAccessLogsPaginated } from "src/api/userAccessLog";
 import { formatDateYMDtoDMY, getStatusColor, getStatusI18nKey } from "src/helpers/utils";
 import { permissionStore } from "src/stores/permission";
 import DependentApprovalDialog from "./components/DependentApprovalDialog.vue";
+import AssociadoApprovalDialog from "./components/AssociadoApprovalDialog.vue";
 
 const { t } = useI18n();
 const $q = useQuasar();
@@ -197,7 +198,15 @@ const onCardClick = (card) => {
   activeCard.value = activeCard.value === card.key ? null : card.key;
 };
 
-const onDependentRowClick = ({ row }) => {
+const onRowClick = ({ row }) => {
+  if (activeCard.value === "dependentes_pendentes") {
+    onDependentRowClick(row);
+  } else if (activeCard.value === "associados_pendentes") {
+    onAssociadoRowClick(row);
+  }
+};
+
+const onDependentRowClick = (row) => {
   if (!permission_store.getAccess("associado.dependente_aprovacao", "edit")) {
     $q.notify({ type: "negative", message: t("validation.permissions.edit") });
     return;
@@ -211,6 +220,20 @@ const onDependentRowClick = ({ row }) => {
   });
 };
 
+const onAssociadoRowClick = (row) => {
+  if (!permission_store.getAccess("associado", "edit")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.edit") });
+    return;
+  }
+  $q.dialog({
+    component: AssociadoApprovalDialog,
+    componentProps: { associado: row },
+  }).onOk(async () => {
+    tableRef.value?.refresh();
+    stats.value = await getDashboardStats();
+  });
+};
+
 onMounted(async () => {
   try {
     stats.value = await getDashboardStats();

+ 132 - 0
src/pages/dashboard/components/AssociadoApprovalDialog.vue

@@ -0,0 +1,132 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 560px; max-width: 95vw">
+      <DefaultDialogHeader :title="() => $t('associado.pending_approval_dialog_title')" @close="onDialogCancel" />
+      <q-card-section class="q-gutter-y-sm q-pt-none">
+        <div class="row q-col-gutter-sm">
+          <div class="col-8">
+            <div class="text-caption text-grey-7">{{ $t("common.terms.name") }}</div>
+            <div class="text-body1">{{ associado.name ?? "—" }}</div>
+          </div>
+          <div class="col-4">
+            <div class="text-caption text-grey-7">{{ $t("common.terms.status") }}</div>
+            <q-badge
+              :color="getStatusColor(associado.status)"
+              :label="$t(getStatusI18nKey(associado.status))"
+              class="text-capitalize q-mt-xs"
+              outline
+            />
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-8">
+            <div class="text-caption text-grey-7">{{ $t("common.terms.email") }}</div>
+            <div class="text-body1">{{ associado.email ?? "—" }}</div>
+          </div>
+          <div class="col-4">
+            <div class="text-caption text-grey-7">{{ $t("common.terms.cpf") }}</div>
+            <div class="text-body1">{{ associado.cpf ?? "—" }}</div>
+          </div>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.registration") }}</div>
+            <div class="text-body1">{{ associado.registration ?? "—" }}</div>
+          </div>
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.position") }}</div>
+            <div class="text-body1">{{ associado.position?.name ?? "—" }}</div>
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.sector") }}</div>
+            <div class="text-body1">{{ associado.sector?.name ?? "—" }}</div>
+          </div>
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.admission_date") }}</div>
+            <div class="text-body1">{{ associado.admission_date ?? "—" }}</div>
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.validity") }}</div>
+            <div class="text-body1">{{ associado.expiry_date ?? "—" }}</div>
+          </div>
+        </div>
+      </q-card-section>
+
+      <q-card-actions>
+        <q-space />
+        <q-btn
+          outline
+          color="negative"
+          :label="$t('associado.pending_refuse')"
+          :loading="loading === 'refuse'"
+          :disable="!!loading"
+          @click="onRefuse"
+        />
+        <q-btn
+          unelevated
+          color="violet-normal"
+          :label="$t('associado.pending_approve')"
+          :loading="loading === 'approve'"
+          :disable="!!loading"
+          @click="onApprove"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { approveUser, refuseUser } from "src/api/user";
+import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { associado } = defineProps({
+  associado: { type: Object, required: true },
+});
+
+const { t } = useI18n();
+const $q = useQuasar();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const loading = ref(null);
+
+const onApprove = async () => {
+  loading.value = "approve";
+  try {
+    await approveUser(associado.id);
+    onDialogOK();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loading.value = null;
+  }
+};
+
+const onRefuse = async () => {
+  loading.value = "refuse";
+  try {
+    await refuseUser(associado.id);
+    onDialogOK();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loading.value = null;
+  }
+};
+</script>

+ 1 - 0
src/pages/gestao-associados/components/AddEditAssociadoDialog.vue

@@ -154,6 +154,7 @@ const statusOptions = computed(() => [
   { label: t("common.status.inactive"), value: "inactive" },
   { label: t("common.status.pending"),  value: "pending" },
   { label: t("common.status.on_leave"), value: "on_leave" },
+  { label: t("common.status.refused"),  value: "refused" },
 ]);
 
 const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({