Browse Source

feat: :sparkles: feat (dependentes do associado) adicionada aprovacao de dependentes + correcao no status

foi adicionada a aprovacao de dependentes cadastrados pelos associados. O adm precisa aprovar na dashboard. Alem disso, foi corrigido para que o associado nao consiga preencher campo status do dependente

fase:dev | origin:escopo
Gustavo Zanatta 1 day ago
parent
commit
77c7f06c93

+ 18 - 0
src/api/profile.js

@@ -24,3 +24,21 @@ export const deleteDependent = async (id) => {
   const { data } = await api.delete(`/user-dependent/${id}`);
   return data.payload;
 };
+
+export const getDependentsPaginated = async ({ page = 1, perPage = 10, filter, status } = {}) => {
+  const params = { page, per_page: perPage };
+  if (status) params.status = status;
+  if (filter) params.search = filter;
+  const { data } = await api.get("/user-dependent/paginated", { params });
+  return { data: { result: data.payload } };
+};
+
+export const approveDependent = async (id) => {
+  const { data } = await api.put(`/user-dependent/${id}/approve`);
+  return data.payload;
+};
+
+export const refuseDependent = async (id) => {
+  const { data } = await api.put(`/user-dependent/${id}/refuse`);
+  return data.payload;
+};

+ 9 - 0
src/css/app.scss

@@ -150,6 +150,15 @@ input[type="number"]::-webkit-outer-spin-button {
   background: #fff !important;
 }
 
+:where(.q-field--standout.q-field--highlighted) {
+  .q-field__native,
+  .q-field__input,
+  .q-field__prefix,
+  .q-field__suffix {
+    color: $color-text !important;
+  }
+}
+
 .q-field--dark .q-field__native {
   color: black;
 }

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

@@ -488,6 +488,10 @@
     "approve_title": "Approve registration",
     "approve_message": "Confirm approval of {name}'s registration?",
     "approve_confirm": "Confirm approval",
+    "dependent_registered_at": "Registration Date/Time",
+    "dependent_approval_dialog_title": "Dependent Approval",
+    "dependent_approve_confirm": "Approve",
+    "dependent_refuse_confirm": "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.",
@@ -590,6 +594,7 @@
       "contratos_a_vencer": "Expiring Contracts",
       "novos_mes": "New (month)",
       "associados_pendentes": "Pending Members",
+      "dependentes_pendentes": "Pending Dependents",
       "ultimos_acessos": "Last Accesses",
       "total_acessos_app": "Serprati App Accesses",
       "association_date": "Association Date",

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

@@ -489,6 +489,10 @@
     "approve_title": "Aprobar registro",
     "approve_message": "¿Confirma la aprobación del registro de {name}?",
     "approve_confirm": "Confirmar aprobación",
+    "dependent_registered_at": "Fecha/Hora de Registro",
+    "dependent_approval_dialog_title": "Aprobación de Dependiente",
+    "dependent_approve_confirm": "Aprobar",
+    "dependent_refuse_confirm": "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.",
@@ -591,6 +595,7 @@
       "contratos_a_vencer": "Contratos por Vencer",
       "novos_mes": "Nuevos (mes)",
       "associados_pendentes": "Asociados Pendientes",
+      "dependentes_pendentes": "Dependientes Pendientes",
       "ultimos_acessos": "Últimos Accesos",
       "total_acessos_app": "Accesos al App Serprati",
       "association_date": "Fecha de Asociación",

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

@@ -489,6 +489,10 @@
     "approve_title": "Aprovar cadastro",
     "approve_message": "Confirma a aprovação do cadastro de {name}?",
     "approve_confirm": "Confirmar aprovação",
+    "dependent_registered_at": "Data/Hora de Cadastro",
+    "dependent_approval_dialog_title": "Aprovação de Dependente",
+    "dependent_approve_confirm": "Aprovar",
+    "dependent_refuse_confirm": "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.",
@@ -591,6 +595,7 @@
       "contratos_a_vencer": "Contratos a Vencer",
       "novos_mes": "Novos (mês)",
       "associados_pendentes": "Associados Pendentes",
+      "dependentes_pendentes": "Dependentes Pendentes",
       "ultimos_acessos": "Últimos Acessos",
       "total_acessos_app": "Acessos ao App Serprati",
       "association_date": "Data Associação",

+ 2 - 1
src/pages/associado/profile/components/AddEditDependentDialog.vue

@@ -19,9 +19,10 @@
             :options="kinshipOptions"
             emit-value
             map-options
-            class="col-md-6 col-12"
+            :class="dependent ? 'col-md-6 col-12' : 'col-12'"
           />
           <DefaultSelect
+            v-if="dependent"
             v-model="form.status"
             v-model:error="validationErrors.status"
             :rules="[inputRules.required]"

+ 37 - 2
src/pages/dashboard/DashboardPage.vue

@@ -35,12 +35,15 @@
 
         <q-card v-if="activeCard" flat class="bg-white" style="border-radius: 12px">
           <DefaultTableServerSide
+            ref="tableRef"
             :key="activeCard"
             :columns="tableColumns"
             :api-call="activeApiCall"
             :add-item="false"
             sem-table-top
+            :open-item="activeCard === 'dependentes_pendentes'"
             :show-search-field="!['ultimos_acessos', 'associados_com_acesso_app'].includes(activeCard)"
+            @on-row-click="onDependentRowClick"
           >
             <template #body-cell-status="{ row }">
               <q-td>
@@ -76,17 +79,24 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from "vue";
+import { ref, computed, onMounted, useTemplateRef } from "vue";
 import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
 import { getDashboardStats } from "src/api/dashboard";
 import { getUsersPaginated } from "src/api/user";
+import { getDependentsPaginated } from "src/api/profile";
 import { getPartnerAgreementsPaginated, getExpiringPartnerAgreementsPaginated } from "src/api/partnerAgreement";
 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";
 
 const { t } = useI18n();
+const $q = useQuasar();
+const permission_store = permissionStore();
+const tableRef = useTemplateRef("tableRef");
 
 const statsLoading = ref(true);
 const stats = ref({});
@@ -99,6 +109,7 @@ const statCards = [
   { key: "contratos_a_vencer",   icon: "mdi-file-clock",     labelKey: "dashboard.stats.contratos_a_vencer" },
   { key: "novos_mes",            icon: "mdi-account-plus",   labelKey: "dashboard.stats.novos_mes" },
   { key: "associados_pendentes", icon: "mdi-account-alert",  labelKey: "dashboard.stats.associados_pendentes" },
+  { key: "dependentes_pendentes", icon: "mdi-account-child-circle", labelKey: "dashboard.stats.dependentes_pendentes" },
   { key: "ultimos_acessos",      icon: "mdi-login-variant",  labelKey: "dashboard.stats.ultimos_acessos" },
   { key: "associados_com_acesso_app", icon: "mdi-cellphone-check", labelKey: "dashboard.stats.total_acessos_app" },
 ];
@@ -125,6 +136,12 @@ const columnsContratosAVencer = computed(() => [
   { name: "status",       label: t("common.terms.status"),     field: "status",        align: "left" },
 ]);
 
+const columnsDependentes = computed(() => [
+  { name: "responsible_user_name", label: t("associado.associado"), field: (row) => row.responsible_user?.name ?? "—", align: "left", sortable: false },
+  { name: "name",       label: t("associado.dependent"),                  field: "name",       align: "left", sortable: false },
+  { name: "created_at", label: t("associado.dependent_registered_at"),    field: (row) => formatDateTime(row.created_at), align: "left", sortable: false },
+]);
+
 const columnsAccessLogs = computed(() => [
   { name: "user_name",   label: t("access_log.user"),        field: "user_name",   align: "left",   sortable: false },
   { name: "user_type",   label: t("access_log.user_type"),   field: "user_type",   align: "center", sortable: false },
@@ -136,6 +153,7 @@ const tableColumns = computed(() => {
   if (activeCard.value === "contratos_a_vencer") return columnsContratosAVencer.value;
   if (activeCard.value === "parceiros" || activeCard.value === "novos_mes") return columnsParceiros.value;
   if (activeCard.value === "ultimos_acessos" || activeCard.value === "associados_com_acesso_app") return columnsAccessLogs.value;
+  if (activeCard.value === "dependentes_pendentes") return columnsDependentes.value;
   return columnsAssociados.value;
 });
 
@@ -148,6 +166,8 @@ const activeApiCall = computed(() => {
       return (p) => getUsersPaginated({ ...p, type: "associado", status: "active" });
     case "associados_pendentes":
       return (p) => getUsersPaginated({ ...p, type: "associado", status: "pending" });
+    case "dependentes_pendentes":
+      return (p) => getDependentsPaginated({ ...p, status: "pending" });
     case "parceiros":
       return (p) => getPartnerAgreementsPaginated({ ...p });
     case "contratos_a_vencer":
@@ -165,7 +185,8 @@ const activeApiCall = computed(() => {
 
 const formatDateTime = (isoString) => {
   if (!isoString) return "—";
-  const d = new Date(isoString);
+  const d = new Date(isoString.includes("T") ? isoString : isoString.replace(" ", "T"));
+  if (Number.isNaN(d.getTime())) return isoString;
   return d.toLocaleString("pt-BR", {
     day: "2-digit", month: "2-digit", year: "numeric",
     hour: "2-digit", minute: "2-digit", second: "2-digit",
@@ -176,6 +197,20 @@ const onCardClick = (card) => {
   activeCard.value = activeCard.value === card.key ? null : card.key;
 };
 
+const onDependentRowClick = ({ row }) => {
+  if (!permission_store.getAccess("associado.dependente_aprovacao", "edit")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.edit") });
+    return;
+  }
+  $q.dialog({
+    component: DependentApprovalDialog,
+    componentProps: { dependent: row },
+  }).onOk(async () => {
+    tableRef.value?.refresh();
+    stats.value = await getDashboardStats();
+  });
+};
+
 onMounted(async () => {
   try {
     stats.value = await getDashboardStats();

+ 125 - 0
src/pages/dashboard/components/DependentApprovalDialog.vue

@@ -0,0 +1,125 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 480px">
+      <DefaultDialogHeader :title="() => $t('associado.dependent_approval_dialog_title')" @close="onDialogCancel" />
+      <q-card-section class="q-gutter-y-sm q-pt-none">
+        <div class="row">
+          <div class="col-12">
+            <div class="text-caption text-grey-7">{{ $t("common.terms.name") }} ({{ $t("associado.associado") }})</div>
+            <div class="text-body1">{{ dependent.responsible_user?.name ?? "—" }}</div>
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.position") }}</div>
+            <div class="text-body1">{{ dependent.responsible_user?.position ?? "—" }}</div>
+          </div>
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.sector") }}</div>
+            <div class="text-body1">{{ dependent.responsible_user?.sector ?? "—" }}</div>
+          </div>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="row">
+          <div class="col-12">
+            <div class="text-caption text-grey-7">{{ $t("associado.dependent") }}</div>
+            <div class="text-body1">{{ dependent.name }}</div>
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.kinship") }}</div>
+            <div class="text-body1">{{ kinshipLabel }}</div>
+          </div>
+          <div class="col-6">
+            <div class="text-caption text-grey-7">{{ $t("associado.dependent_registered_at") }}</div>
+            <div class="text-body1">{{ formattedCreatedAt }}</div>
+          </div>
+        </div>
+      </q-card-section>
+
+      <q-card-actions>
+        <q-space />
+        <q-btn
+          outline
+          color="negative"
+          :label="$t('associado.dependent_refuse_confirm')"
+          :loading="loading === 'refuse'"
+          :disable="!!loading"
+          @click="onRefuse"
+        />
+        <q-btn
+          unelevated
+          color="violet-normal"
+          :label="$t('associado.dependent_approve_confirm')"
+          :loading="loading === 'approve'"
+          :disable="!!loading"
+          @click="onApprove"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { approveDependent, refuseDependent } from "src/api/profile";
+
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dependent } = defineProps({
+  dependent: { type: Object, required: true },
+});
+
+const { t } = useI18n();
+const $q = useQuasar();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const loading = ref(null);
+
+const kinshipLabel = computed(() =>
+  dependent.kinship ? t(`associado.kinship_options.${dependent.kinship}`) : "—",
+);
+
+const formattedCreatedAt = computed(() => {
+  if (!dependent.created_at) return "—";
+  const d = new Date(dependent.created_at.replace(" ", "T"));
+  if (Number.isNaN(d.getTime())) return dependent.created_at;
+  return d.toLocaleString("pt-BR", {
+    day: "2-digit", month: "2-digit", year: "numeric",
+    hour: "2-digit", minute: "2-digit",
+  });
+});
+
+const onApprove = async () => {
+  loading.value = "approve";
+  try {
+    await approveDependent(dependent.id);
+    onDialogOK();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loading.value = null;
+  }
+};
+
+const onRefuse = async () => {
+  loading.value = "refuse";
+  try {
+    await refuseDependent(dependent.id);
+    onDialogOK();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    loading.value = null;
+  }
+};
+</script>