Преглед на файлове

feat: :sparkles: feat (tabelas) paginacao das tabelas

criada paginacao nas tabelas do sistema

fase:dev | origin:escopo
Gustavo Zanatta преди 1 месец
родител
ревизия
523fd0df15

+ 1 - 19
src/App.vue

@@ -14,26 +14,8 @@ defineOptions({
 const { locale } = useI18n();
 
 const $q = useQuasar();
-// const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
-//   ? "dark"
-//   : "light";
 
-const systemTheme = "light";
-
-const theme = Cookies.get("theme") || systemTheme;
-
-$q.dark.set(theme == "dark");
-
-watch(
-  () => $q.dark.isActive,
-  (value) => {
-    Cookies.set("theme", value ? "dark" : "light", {
-      expires: 365,
-      sameSite: "Lax",
-      path: "/",
-    });
-  },
-);
+$q.dark.set(false);
 
 watch(
   () => locale.value,

+ 9 - 0
src/api/partnerAgreement.js

@@ -5,6 +5,15 @@ export const getPartnerAgreements = async () => {
   return data.payload;
 };
 
+export const getPartnerAgreementsPaginated = async ({ page = 1, perPage = 10, filter, expiresInDays, createdMonth } = {}) => {
+  const params = { page, per_page: perPage };
+  if (filter)        params.search          = filter;
+  if (expiresInDays) params.expires_in_days = expiresInDays;
+  if (createdMonth)  params.created_month   = createdMonth;
+  const { data } = await api.get("/partner-agreement/paginated", { params });
+  return { data: { result: data.payload } };
+};
+
 export const getPartnerAgreement = async (id) => {
   const { data } = await api.get(`/partner-agreement/${id}`);
   return data.payload;

+ 12 - 0
src/api/user.js

@@ -34,3 +34,15 @@ export const getAssociados = async () => {
   const users = await getUsers();
   return users.filter((u) => u.type === "associado");
 };
+
+export const getUsersPaginated = async ({ page = 1, perPage = 10, filter, type, status } = {}) => {
+  const params = { page, per_page: perPage };
+  if (type)   params.type   = type;
+  if (status) params.status = status;
+  if (filter) params.search = filter;
+  const { data } = await api.get("/user/paginated", { params });
+  return { data: { result: data.payload } };
+};
+
+export const getAssociadosPaginated = async (params = {}) =>
+  getUsersPaginated({ ...params, type: "associado" });

+ 2 - 2
src/boot/defaultPropsComponents.js

@@ -21,14 +21,14 @@ export default defineBoot(() => {
   });
   SetComponentDefaults(QInput, {
     rounded: true,
-    dark: true,
+    dark: false,
     standout: true,
     dense: true,
   });
   SetComponentDefaults(QSelect, {
     rounded: true,
     standout: true,
-    dark: true,
+    dark: false,
     dense: true,
   });
   SetComponentDefaults(QCard, {

+ 9 - 2
src/components/defaults/DefaultTable.vue

@@ -12,11 +12,13 @@
     :columns
     :loading
     :rows
-    class="softpar-table q-pa-sm q-mt-md"
+    :hide-top="semTableTop"
+    class="softpar-table q-pa-sm"
     @row-click="onRowClick"
   >
-    <template #top>
+    <template v-if="!semTableTop" #top>
       <div
+        v-if="title || showSearchField || showColumnsSelect || addItem || $slots.top"
         class="flex full-width align-center q-mb-md q-pl-sm"
         style="gap: 1rem"
       >
@@ -160,6 +162,7 @@ const {
   noApiCall,
   hideNoDataLabel,
   deleteFunction,
+  semTableTop,
 } = defineProps({
   title: {
     type: String,
@@ -217,6 +220,10 @@ const {
     type: Function,
     default: null,
   },
+  semTableTop: {
+    type: Boolean,
+    default: true,
+  },
 });
 
 const router = useRouter();

+ 41 - 52
src/components/defaults/DefaultTableServerSide.vue

@@ -14,10 +14,11 @@
     :filter="pagination.filter"
     :grid="$q.screen.lt.sm"
     :loading="loading"
+    :hide-top="semTableTop"
     v-bind="$attrs"
     @row-click="onRowClick"
   >
-    <template #top>
+    <template v-if="!semTableTop" #top>
       <div
         class="flex full-width justify-between items-center q-mb-md q-pl-sm"
         style="gap: 1rem"
@@ -67,9 +68,11 @@
     </template>
 
     <template #body-cell-actions="{ row }">
-      <q-td v-if="deleteFunction">
-        <q-item-section>
+      <q-td auto-width>
+        <q-item-section class="no-wrap" style="flex-direction: row">
+          <slot name="body-cell-actions" :row="row" />
           <q-btn
+            v-if="deleteFunction"
             color="negative"
             flat
             dense
@@ -92,59 +95,36 @@
     </template>
 
     <template #bottom="scope">
-      <div class="flex full-width justify-end">
-        <div class="flex items-center">
-          {{ $t("common.ui.table.rows_per_page") }}
-          <DefaultSelect
-            v-model="pagination.rowsPerPage"
-            class="q-mx-sm"
-            dense
-            borderless
-            :options="rowsPerPageOptions"
-          >
-            <template #option="selectData">
-              <q-item v-bind="selectData.itemProps">
-                <q-item-section>
-                  <q-item-label>{{
-                    selectData.opt == 0
-                      ? $t("common.ui.misc.all")
-                      : selectData.opt
-                  }}</q-item-label>
-                </q-item-section>
-              </q-item>
-            </template>
-          </DefaultSelect>
-        </div>
-        <div class="flex items-center">
-          {{ pagination.from + "-" + pagination.to }}
+      <div class="flex full-width justify-end items-center" style="gap: 8px">
+        <span class="text-caption text-grey-7">
+          {{ $t("common.ui.table.rows_per_page") }} 10 &nbsp;|&nbsp;
+          {{ pagination.from }}-{{ pagination.to }}
           {{ $t("common.ui.table.of") }}
           {{ pagination.rowsNumber }}
-        </div>
-        <div class="flex items-center">
-          <q-btn
-            icon="mdi-chevron-left"
-            color="grey-8"
-            round
-            dense
-            flat
-            :disable="scope.isFirstPage"
-            @click="prevPage"
-          />
-          <q-btn
-            icon="mdi-chevron-right"
-            color="grey-8"
-            round
-            dense
-            flat
-            :disable="scope.isLastPage"
-            @click="nextPage"
-          />
-        </div>
+        </span>
+        <q-btn
+          icon="mdi-chevron-left"
+          color="grey-8"
+          round
+          dense
+          flat
+          :disable="scope.isFirstPage"
+          @click="prevPage"
+        />
+        <q-btn
+          icon="mdi-chevron-right"
+          color="grey-8"
+          round
+          dense
+          flat
+          :disable="scope.isLastPage"
+          @click="nextPage"
+        />
       </div>
     </template>
 
-    <template v-for="name in $slots" #[name]="data">
-      <slot :name="name" v-bind="data"></slot>
+    <template v-for="slotName in usableSlots($slots)" #[slotName]="data">
+      <slot :name="slotName" v-bind="data" />
     </template>
   </q-table>
 </template>
@@ -176,6 +156,7 @@ const {
   showSearchField,
   hideNoDataLabel,
   deleteFunction,
+  semTableTop,
 } = defineProps({
   columns: {
     type: Array,
@@ -233,6 +214,10 @@ const {
     type: Function,
     default: null,
   },
+  semTableTop: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 const { t } = useI18n();
@@ -240,7 +225,6 @@ const router = useRouter();
 const rows = ref([]);
 const loading = ref(true);
 const fullscreen = ref(false);
-const rowsPerPageOptions = [10, 15, 25, 50];
 
 const pagination = ref({
   filter: undefined,
@@ -369,6 +353,11 @@ onMounted(async () => {
   await onRequest();
 });
 
+const usableSlots = (slots) => {
+  const handled = ["top", "body-cell-actions", "no-data", "bottom", "loading"];
+  return Object.keys(slots).filter((s) => !handled.includes(s));
+};
+
 defineExpose({
   refresh: onRequest,
 });

+ 12 - 3
src/css/quasar.variables.scss

@@ -31,6 +31,9 @@ $neutral-dark-hover:   #919191;
 $neutral-dark-active:  #6d6d6d;
 $neutral-darker:       #555555;
 
+// Surface
+$surface: #FEFEFE;
+
 // Text
 $color-text:  #161616;
 $color-text-2:#505050;
@@ -44,7 +47,7 @@ $dark: #1d1d1d;
 $positive: #2e7d32; // Material Green 800
 $negative: #d32f2f; // Material Red 700
 $info: #0288d1; // Material Light Blue 700
-$warning: #ed6c02; // Material Orange 800
+$inactive: #FFE100; // Material Orange 800
 
 // Extended Color System with Light/Dark Variants
 $colors: (
@@ -102,7 +105,7 @@ $colors: (
   "error-dark": #c62828,
 
   // Red 800
-  "warning": #ed6c02,
+  "warning": #FFE100,
   // Orange 800
   "warning-light": #ff9800,
   // Orange 500
@@ -113,7 +116,13 @@ $colors: (
   // Light Blue 700
   "info-light": #03a9f4,
   // Light Blue 500
-  "info-dark": #01579b
+  "info-dark": #01579b,
+
+  // Surface
+  "surface": #FEFEFE,
+
+  // Inactive
+  "inactive": #ed6c02
 );
 
 // Dark Theme Color Overrides

+ 101 - 156
src/css/table.scss

@@ -1,196 +1,141 @@
 @use "sass:map";
 @use "src/css/quasar.variables.scss";
-.softpar-table {
-  // .body--dark & {
-  //   --table-bg-color: #{map.get($colors-dark, "surface")}; // Using our dark background
-  //   --table-border-color: #{map.get($colors-dark, "surface-light")}; // Darker border
-  //   --table-header-color: #{map.get($colors-dark, "text")}; // Light text for dark mode
-  //   --table-ring-color: #505050;
-  // }
-
-  .body--light & {
-    --table-bg-color: #{map.get($colors, "surface")}; // Light background
-    --table-border-color: #{map.get($colors, "surface-light")}; // Border color
-    --table-header-color: #{map.get($colors, "text")}; // Dark text for light mode
-    --table-ring-color: #c0c0c0c0;
-  }
 
+// ─── Tabela principal ─────────────────────────────────────────────────────────
+.softpar-table {
+  background: $surface !important;
+  border-radius: 8px !important;
+  box-shadow: none !important;
   padding-left: 16px !important;
   padding-right: 16px !important;
-  border-radius: 8px !important;
-  box-shadow: 0 0 0 1px var(--table-ring-color);
-
-  :deep(.q-table) {
-    thead tr:first-child th {
-      background-color: $primary !important;
-    }
-
-    thead tr th {
-      text-transform: uppercase;
-      position: sticky;
-      z-index: 1;
-    }
-
-    thead tr:first-child th {
-      top: 0;
-    }
-
-    &.q-table--loading thead tr:last-child th {
-      top: 48px;
-    }
-  }
 
-  tr {
-    background-color: var(--table-bg-color) !important;
+  // Sem overflow hidden para não cortar bordas arredondadas das linhas
+  .q-table__middle {
+    overflow: visible;
+    background: $surface;
   }
 
-  .q-table__top {
-    padding-top: 16px;
-    padding-left: 0px;
-    padding-right: 0px;
-    padding-bottom: 16px;
+  // Espaçamento vertical entre linhas (necessário para o efeito de "linhas separadas")
+  .q-table {
+    border-collapse: separate !important;
+    border-spacing: 0 5px !important;
+    background: $surface;
   }
 
-  .q-table th {
-    font-weight: normal;
-    font-family: "Roboto";
-    font-style: normal;
-    font-weight: 500;
-    font-size: 14px;
+  // ── Cabeçalho ───────────────────────────────────────────────────────────────
+  .q-table thead tr th {
+    background-color: $surface !important;
+    color: $violet-normal;
+    text-transform: uppercase;
+    font-family: "Roboto", sans-serif;
+    font-weight: 600;
+    font-size: 13px;
     line-height: 16px;
     letter-spacing: 0.1px;
-    color: var(--table-header-color);
+    position: sticky;
+    z-index: 1;
+    top: 0;
+    border-top: 1.5px solid $violet-normal !important;
+    border-bottom: 1.5px solid $violet-normal !important;
+    border-left: none !important;
+    border-right: none !important;
   }
-}
 
-.ativo {
-  justify-content: center;
-  padding: 5px 12px;
-  gap: 10px;
-  border-radius: 24px;
-  width: fit-content;
-  margin-right: auto;
-  margin-left: auto;
+  .q-table thead tr th:first-child {
+    border-left: 1.5px solid $violet-normal !important;
+    border-radius: 8px 0 0 8px;
+  }
 
-  // .body--dark & {
-  //   background: none;
-  //   border: 1px solid #{map.get($colors, "positive-1")};
-  // }
+  .q-table thead tr th:last-child {
+    border-right: 1.5px solid $violet-normal !important;
+    border-radius: 0 8px 8px 0;
+  }
 
-  .body--light & {
-    background: #{map.get($colors, "positive-1")};
-    border: none;
+  // Ajuste de sticky quando a barra de loading aparece
+  .q-table.q-table--loading thead tr:last-child th {
+    top: 48px;
   }
-}
 
-.inativo {
-  justify-content: center;
-  padding: 5px 12px;
-  gap: 10px;
-  border-radius: 24px;
-  width: fit-content;
-  margin-right: auto;
-  margin-left: auto;
+  // ── Linhas do body ───────────────────────────────────────────────────────────
+  .q-table tbody tr td {
+    background-color: $surface !important;
+    border-top: 1.5px solid $violet-normal !important;
+    border-bottom: 1.5px solid $violet-normal !important;
+    border-left: none !important;
+    border-right: none !important;
+  }
 
-  // .body--dark & {
-  //   background: none;
-  //   border: 1px solid #{map.get($colors, "negative-1")};
-  // }
+  .q-table tbody tr td:first-child {
+    border-left: 1.5px solid $violet-normal !important;
+    border-radius: 8px 0 0 8px;
+  }
 
-  .body--light & {
-    background: #{map.get($colors, "negative-1")};
-    border: none;
+  .q-table tbody tr td:last-child {
+    border-right: 1.5px solid $violet-normal !important;
+    border-radius: 0 8px 8px 0;
   }
-}
 
-.rejeitado {
-  justify-content: center;
-  padding: 5px 12px;
-  gap: 10px;
-  border-radius: 24px;
-  width: fit-content;
-  margin-right: auto;
-  margin-left: auto;
+  .q-table tbody tr:hover td {
+    background-color: $violet-light !important;
+  }
 
-  // .body--dark & {
-  //   background: none;
-  //   border: 1px solid #{map.get($colors, "negative-1")};
-  // }
+  // ── Topo e rodapé (search + paginação) ──────────────────────────────────────
+  .q-table__top {
+    padding: 16px 0;
+    background: $surface;
+  }
 
-  .body--light & {
-    background: #{map.get($colors, "negative-1")};
-    border: none;
+  .q-table__bottom {
+    border-top: none;
+    background: $surface;
   }
 }
 
-.gerado {
-  justify-content: center;
-  padding: 5px 12px;
-  gap: 10px;
-  border-radius: 24px;
-  width: fit-content;
-  margin-right: auto;
-  margin-left: auto;
-
-  // .body--dark & {
-  //   background: none;
-  //   border: 1px solid #{map.get($colors, "primary-1")};
-  // }
-
-  .body--light & {
-    background: #{map.get($colors, "primary-1")};
-    border: none;
-  }
+// ─── Cards (modo grid / mobile) ───────────────────────────────────────────────
+.q-table__grid-item-card {
+  background: $surface;
 }
 
-.pendente {
+// ─── Status badges (outlined) ─────────────────────────────────────────────────
+%badge-base {
+  display: flex;
   justify-content: center;
-  padding: 5px 12px;
-  gap: 10px;
+  padding: 3px 12px;
   border-radius: 24px;
+  border: 1.5px solid;
   width: fit-content;
-  margin-right: auto;
-  margin-left: auto;
-
-  // .body--dark & {
-  //   background: none;
-  //   border: 1px solid #{map.get($colors, "warning")};
-  // }
-
-  .body--light & {
-    background: #{map.get($colors, "warning")};
-    border: none;
-  }
+  margin: 0 auto;
+  font-size: 0.75rem;
+  font-weight: 600;
+  background: transparent;
 }
 
-.table-bottom {
-  .q-table__top {
-    .body--light & {
-      background: #{map.get($colors, "page")};
-    }
-
-    // .body--dark & {
-    //   background: #{map.get($colors, "background-3")};
-    // }
-  }
+.ativo {
+  @extend %badge-base;
+  color: #{map.get($colors, "success-light")};
+  border-color: #{map.get($colors, "success-light")};
+}
 
-  .q-table__bottom {
-    .body--light & {
-      background: #{map.get($colors, "page")};
-    }
+.inativo {
+  @extend %badge-base;
+  color: #{map.get($colors, "error-light")};
+  border-color: #{map.get($colors, "error-light")};
+}
 
-    // .body--dark & {
-    //   background: #{map.get($colors, "background-3")};
-    // }
-  }
+.rejeitado {
+  @extend %badge-base;
+  color: #{map.get($colors, "error-light")};
+  border-color: #{map.get($colors, "error-light")};
 }
 
-.q-table__grid-item-card {
-  .body--light & {
-    background: #{map.get($colors, "dark")};
-  }
+.gerado {
+  @extend %badge-base;
+  color: #{map.get($colors, "info-light")};
+  border-color: #{map.get($colors, "info-light")};
+}
 
-  // .body--dark & {
-  //   background: #{map.get($colors, "background-3")};
-  // }
+.pendente {
+  @extend %badge-base;
+  color: #ca8a04;
+  border-color: #ca8a04;
 }

+ 1 - 0
src/helpers/utils.js

@@ -106,6 +106,7 @@ const getStatusColor = (status) => {
   const s = typeof status === "object" ? status?.value : status;
   if (s === "active")  return "positive";
   if (s === "pending") return "warning";
+  if (s === "inactive") return "inactive";
   return "grey-6";
 };
 

+ 60 - 116
src/pages/dashboard/DashboardPage.vue

@@ -33,32 +33,26 @@
           </div>
         </div>
 
-        <div v-if="activeCard">
-          <q-card flat class="bg-white q-pa-md" style="border-radius: 12px">
-            <div v-if="tableLoading" class="flex flex-center q-pa-xl">
-              <q-spinner color="violet-normal" size="40px" />
-            </div>
-
-            <DefaultTable
-              v-else
-              v-model:rows="tableRows"
-              :columns="tableColumns"
-              no-api-call
-              :rows-per-page="10"
-              :show-search-field="false"
-            >
-              <template #body-cell-status="{ row }">
-                <q-td>
-                  <q-badge
-                    :color="getStatusColor(row.status)"
-                    :label="$t(getStatusI18nKey(row.status))"
-                    class="text-capitalize"
-                  />
-                </q-td>
-              </template>
-            </DefaultTable>
-          </q-card>
-        </div>
+        <q-card v-if="activeCard" flat class="bg-white" style="border-radius: 12px">
+          <DefaultTableServerSide
+            :key="activeCard"
+            :columns="tableColumns"
+            :api-call="activeApiCall"
+            :add-item="false"
+            sem-table-top
+          >
+            <template #body-cell-status="{ row }">
+              <q-td>
+                <q-badge
+                  :color="getStatusColor(row.status)"
+                  :label="$t(getStatusI18nKey(row.status))"
+                  class="text-capitalize"
+                  outline
+                />
+              </q-td>
+            </template>
+          </DefaultTableServerSide>
+        </q-card>
       </template>
     </div>
   </div>
@@ -68,128 +62,78 @@
 import { ref, computed, onMounted } from "vue";
 import { useI18n } from "vue-i18n";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
 import { getDashboardStats } from "src/api/dashboard";
-import { getUsers } from "src/api/user";
-import { getPartnerAgreements } from "src/api/partnerAgreement";
+import { getUsersPaginated } from "src/api/user";
+import { getPartnerAgreementsPaginated } from "src/api/partnerAgreement";
 import { formatDateYMDtoDMY, getStatusColor, getStatusI18nKey } from "src/helpers/utils";
 
 const { t } = useI18n();
 
 const statsLoading = ref(true);
-const tableLoading = ref(false);
 const stats = ref({});
 const activeCard = ref(null);
-const tableRows = ref([]);
-
-const usersCache = ref(null);
-const partnersCache = ref(null);
 
 const statCards = [
-  { key: "total_associados",    icon: "mdi-account-group",   labelKey: "dashboard.stats.total_associados" },
-  { key: "associados_ativos",   icon: "mdi-trending-up",     labelKey: "dashboard.stats.associados_ativos" },
-  { key: "parceiros",           icon: "mdi-handshake",       labelKey: "dashboard.stats.parceiros" },
-  { 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: "total_associados",     icon: "mdi-account-group",  labelKey: "dashboard.stats.total_associados" },
+  { key: "associados_ativos",    icon: "mdi-trending-up",    labelKey: "dashboard.stats.associados_ativos" },
+  { key: "parceiros",            icon: "mdi-handshake",      labelKey: "dashboard.stats.parceiros" },
+  { 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" },
 ];
 
+// Colunas por tipo de card
 const columnsAssociados = computed(() => [
-  { name: "name",       label: t("common.terms.name"),                   field: "name",       align: "left", sortable: true },
-  { name: "email",      label: t("common.terms.email"),                   field: "email",      align: "left", sortable: true },
-  { name: "created_at", label: t("dashboard.stats.association_date"),    field: (row) => formatDateYMDtoDMY(row.created_at), align: "left", sortable: true },
-  { name: "status",     label: t("common.terms.status"),                  field: "status",     align: "left" },
+  { name: "name",       label: t("common.terms.name"),                field: "name",       align: "left", sortable: true },
+  { name: "email",      label: t("common.terms.email"),               field: "email",      align: "left", sortable: true },
+  { name: "created_at", label: t("dashboard.stats.association_date"), field: (row) => formatDateYMDtoDMY(row.created_at), align: "left", sortable: true },
+  { name: "status",     label: t("common.terms.status"),              field: "status",     align: "left" },
 ]);
 
 const columnsParceiros = computed(() => [
-  { name: "company_name", label: t("common.terms.name"),               field: "company_name", align: "left", sortable: true },
+  { name: "company_name", label: t("common.terms.name"),                field: "company_name", align: "left", sortable: true },
   { name: "responsible",  label: t("parceiro.responsible"),             field: "responsible",  align: "left", sortable: true },
   { name: "created_at",   label: t("dashboard.stats.registration_date"), field: (row) => formatDateYMDtoDMY(row.created_at), align: "left", sortable: true },
   { name: "status",       label: t("common.terms.status"),              field: "status",       align: "left" },
 ]);
 
 const columnsContratosAVencer = computed(() => [
-  { name: "company_name",  label: t("common.terms.name"),          field: "company_name",  align: "left", sortable: true },
-  { name: "responsible",   label: t("parceiro.responsible"),        field: "responsible",   align: "left", sortable: true },
-  { name: "contract_end",  label: t("parceiro.contract_end"),       field: (row) => formatDateYMDtoDMY(row.contract_end), align: "left", sortable: true },
-  { name: "status",        label: t("common.terms.status"),         field: "status",        align: "left" },
+  { name: "company_name", label: t("common.terms.name"),       field: "company_name",  align: "left", sortable: true },
+  { name: "responsible",  label: t("parceiro.responsible"),     field: "responsible",   align: "left", sortable: true },
+  { name: "contract_end", label: t("parceiro.contract_end"),    field: (row) => formatDateYMDtoDMY(row.contract_end), align: "left", sortable: true },
+  { name: "status",       label: t("common.terms.status"),     field: "status",        align: "left" },
 ]);
 
 const tableColumns = computed(() => {
   if (!activeCard.value) return [];
   if (activeCard.value === "contratos_a_vencer") return columnsContratosAVencer.value;
-  if (activeCard.value === "parceiros" || activeCard.value === "novos_mes") {
-    return columnsParceiros.value;
-  }
+  if (activeCard.value === "parceiros" || activeCard.value === "novos_mes") return columnsParceiros.value;
   return columnsAssociados.value;
 });
 
-
-
-const loadUsers = async () => {
-  if (usersCache.value) return usersCache.value;
-  const users = await getUsers();
-  usersCache.value = users;
-  return users;
-};
-
-const loadPartners = async () => {
-  if (partnersCache.value) return partnersCache.value;
-  const partners = await getPartnerAgreements();
-  partnersCache.value = partners;
-  return partners;
-};
-
-const isCurrentMonth = (dateStr) => {
-  if (!dateStr) return false;
-  const now = new Date();
-  const [datePart] = dateStr.split(" ");
-  const [year, month] = datePart.split("-");
-  return parseInt(year) === now.getFullYear() && parseInt(month) === now.getMonth() + 1;
-};
-
-const getStatusValue = (status) => typeof status === "object" ? status?.value : status;
-
-const onCardClick = async (card) => {
-  if (activeCard.value === card.key) return;
-
-  activeCard.value = card.key;
-  tableLoading.value = true;
-  tableRows.value = [];
-
-  try {
-    if (card.key === "contratos_a_vencer") {
-      const partners = await loadPartners();
-      const today = new Date();
-      today.setHours(0, 0, 0, 0);
-      const in30Days = new Date(today);
-      in30Days.setDate(in30Days.getDate() + 30);
-      tableRows.value = partners.filter((p) => {
-        if (!p.contract_end) return false;
-        const end = new Date(p.contract_end + "T00:00:00");
-        return end >= today && end <= in30Days;
-      });
-    } else if (card.key === "parceiros") {
-      const partners = await loadPartners();
-      tableRows.value = partners;
-    } else if (card.key === "novos_mes") {
-      const partners = await loadPartners();
-      tableRows.value = partners.filter((p) => isCurrentMonth(p.created_at));
-    } else {
-      const users = await loadUsers();
-      const associados = users.filter((u) => getStatusValue(u.type) === "associado");
-
-      if (card.key === "total_associados") {
-        tableRows.value = associados;
-      } else if (card.key === "associados_ativos") {
-        tableRows.value = associados.filter((u) => getStatusValue(u.status) === "active");
-      } else if (card.key === "associados_pendentes") {
-        tableRows.value = associados.filter((u) => getStatusValue(u.status) === "pending");
-      }
-    }
-  } finally {
-    tableLoading.value = false;
+// Função de API correspondente a cada card — nova referência a cada computed
+const activeApiCall = computed(() => {
+  switch (activeCard.value) {
+    case "total_associados":
+      return (p) => getUsersPaginated({ ...p, type: "associado" });
+    case "associados_ativos":
+      return (p) => getUsersPaginated({ ...p, type: "associado", status: "active" });
+    case "associados_pendentes":
+      return (p) => getUsersPaginated({ ...p, type: "associado", status: "pending" });
+    case "parceiros":
+      return (p) => getPartnerAgreementsPaginated({ ...p });
+    case "contratos_a_vencer":
+      return (p) => getPartnerAgreementsPaginated({ ...p, expiresInDays: 30 });
+    case "novos_mes":
+      return (p) => getPartnerAgreementsPaginated({ ...p, createdMonth: "current" });
+    default:
+      return null;
   }
+});
+
+const onCardClick = (card) => {
+  activeCard.value = activeCard.value === card.key ? null : card.key;
 };
 
 onMounted(async () => {

+ 19 - 84
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -29,45 +29,21 @@
       </template>
     </DefaultHeaderPage>
 
-    <div class="q-px-none q-pb-sm">
-      <q-input
-        v-model="searchQuery"
-        outlined
-        dense
-        :placeholder="$t('associado.search_placeholder')"
-        clearable
-        class="search-input"
-      >
-        <template #prepend>
-          <q-icon name="mdi-magnify" color="violet-normal" />
-        </template>
-      </q-input>
-    </div>
-
-    <div v-if="loading" class="flex flex-center q-pa-xl">
-      <q-spinner color="violet-normal" size="50px" />
-    </div>
-
-    <DefaultTable
-      v-else
+    <DefaultTableServerSide
       ref="tableRef"
-      v-model:rows="filteredRows"
       :columns="columns"
-      no-api-call
-      :show-search-field="false"
-      :rows-per-page="10"
+      :api-call="getAssociadosPaginated"
+      :add-item="false"
       open-item
       @on-row-click="onRowClick"
     >
       <template #body-cell-actions>
-        <q-td width="15%" class="text-center">
-          <div class="row no-wrap items-center" style="gap: 4px">
-            <q-btn flat round dense color="violet-normal" icon="mdi-pencil" />
-            <q-btn flat round dense color="violet-normal" icon="mdi-account-minus" />
-            <q-btn flat round dense color="violet-normal" icon="mdi-calendar" />
-            <q-btn flat round dense color="violet-normal" icon="mdi-sofa" />
-          </div>
-        </q-td>
+        <div class="row no-wrap items-center" style="gap: 4px">
+          <q-btn flat round dense color="violet-normal" icon="mdi-pencil" />
+          <q-btn flat round dense color="violet-normal" icon="mdi-account-minus" />
+          <q-btn flat round dense color="violet-normal" icon="mdi-calendar" />
+          <q-btn flat round dense color="violet-normal" icon="mdi-sofa" />
+        </div>
       </template>
 
       <template #body-cell-status="{ row }">
@@ -79,19 +55,19 @@
           />
         </q-td>
       </template>
-    </DefaultTable>
+    </DefaultTableServerSide>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, defineAsyncComponent } from "vue";
+import { ref, defineAsyncComponent } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
-import { getAssociados } from "src/api/user";
-import { normalizeString, getStatusColor, getStatusI18nKey } from "src/helpers/utils";
+import { getAssociadosPaginated } from "src/api/user";
+import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
 
 const AddEditAssociadoDialog = defineAsyncComponent(
   () => import("./components/AddEditAssociadoDialog.vue"),
@@ -101,9 +77,6 @@ const permission_store = permissionStore();
 const $q = useQuasar();
 const { t } = useI18n();
 const tableRef = ref(null);
-const loading = ref(true);
-const allRows = ref([]);
-const searchQuery = ref("");
 
 const columns = [
   {
@@ -113,7 +86,6 @@ const columns = [
     align: "left",
     sortable: true,
     width: "10%",
-
   },
   {
     name: "name",
@@ -164,38 +136,14 @@ const columns = [
   },
 ];
 
-const filteredRows = computed(() => {
-  if (!searchQuery.value) return allRows.value;
-
-  const needle = normalizeString(searchQuery.value);
-
-  return allRows.value.filter((row) => {
-    const fields = [
-      row.registration,
-      row.name,
-      row.email,
-      row.cpf,
-      row.position?.name,
-      row.sector?.name,
-      row.admission_date,
-      row.expiry_date,
-      row.status,
-    ];
-    return fields.some((f) => f && normalizeString(String(f)).includes(needle));
-  });
-});
-
-
 const onAddItem = async () => {
   if (!permission_store.getAccess("associado", "add")) {
     $q.notify({ type: "negative", message: t("validation.permissions.add") });
     return;
   }
-  $q.dialog({ component: AddEditAssociadoDialog })
-    .onOk(async () => {
-      const updated = await getAssociados();
-      allRows.value = updated;
-    });
+  $q.dialog({ component: AddEditAssociadoDialog }).onOk(() => {
+    tableRef.value?.refresh();
+  });
 };
 
 const onRowClick = async ({ row }) => {
@@ -209,26 +157,13 @@ const onRowClick = async ({ row }) => {
       associado: row,
       title: () => t("common.actions.edit") + " " + t("ui.navigation.associados"),
     },
-  }).onOk(async () => {
-    const updated = await getAssociados();
-    allRows.value = updated;
+  }).onOk(() => {
+    tableRef.value?.refresh();
   });
 };
-
-onMounted(async () => {
-  try {
-    allRows.value = await getAssociados();
-  } finally {
-    loading.value = false;
-  }
-});
 </script>
 
 <style scoped>
-.search-input {
-  border-radius: 8px;
-}
-
 .gap-sm {
   gap: 8px;
 }