Browse Source

feat: add tela para acompanhar pagamento

Gustavo Mantovani 2 weeks ago
parent
commit
9ab06afa0c

+ 6 - 2
package.json

@@ -10,8 +10,12 @@
     "lint": "eslint -c ./eslint.config.js \"./src*/**/*.{js,cjs,mjs,vue}\"",
     "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
     "test": "echo \"No test specified\" && exit 0",
-    "dev": "quasar dev",
-    "build": "quasar build"
+    "dev": "APP_ENV=dev quasar dev",
+    "dev:staging": "APP_ENV=staging quasar dev",
+    "build": "APP_ENV=prod quasar build",
+    "build:dev": "APP_ENV=dev quasar build",
+    "build:staging": "APP_ENV=staging quasar build",
+    "build:prod": "APP_ENV=prod quasar build"
   },
   "dependencies": {
     "@bufbuild/protobuf": "^2.5.1",

+ 68 - 11
quasar.config.js

@@ -4,9 +4,76 @@
 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
 
 import { defineConfig } from "#q-app/wrappers";
+import { existsSync, readFileSync } from "node:fs";
+import { resolve } from "node:path";
 import { fileURLToPath } from "node:url";
 
+const envFiles = {
+  dev: ".env.app.dev",
+  staging: ".env.app.staging",
+  prod: ".env.app.prod",
+};
+
+const parseEnvFile = (filePath) => {
+  if (!existsSync(filePath)) {
+    return {};
+  }
+
+  return readFileSync(filePath, "utf8")
+    .split(/\r?\n/)
+    .reduce((env, line) => {
+      const trimmedLine = line.trim();
+
+      if (!trimmedLine || trimmedLine.startsWith("#")) {
+        return env;
+      }
+
+      const separatorIndex = trimmedLine.indexOf("=");
+
+      if (separatorIndex === -1) {
+        return env;
+      }
+
+      const key = trimmedLine.slice(0, separatorIndex).trim();
+      const value = trimmedLine.slice(separatorIndex + 1).trim();
+
+      env[key] = value.replace(/^["']|["']$/g, "");
+
+      return env;
+    }, {});
+};
+
+const loadAppEnv = (ctx) => {
+  const appEnv = process.env.APP_ENV || (ctx.dev ? "dev" : "prod");
+  const envFile = envFiles[appEnv];
+
+  if (!envFile) {
+    throw new Error(`APP_ENV invalido: "${appEnv}". Use dev, staging ou prod.`);
+  }
+
+  const fileEnv = parseEnvFile(resolve(process.cwd(), envFile));
+
+  return {
+    APP_ENV: appEnv,
+    API_URL: process.env.API_URL || fileEnv.API_URL || "http://localhost:3000",
+    PASSWORD: process.env.PASSWORD || fileEnv.PASSWORD || "",
+    SENHA: process.env.SENHA || fileEnv.SENHA || "",
+    WEBSOCKET_API:
+      process.env.WEBSOCKET_API ||
+      fileEnv.WEBSOCKET_API ||
+      "http://localhost:4321/",
+    WEBSOCKET_PATH:
+      process.env.WEBSOCKET_PATH || fileEnv.WEBSOCKET_PATH || "/socket.io",
+    WEBSOCKET_ROOM:
+      process.env.WEBSOCKET_ROOM || fileEnv.WEBSOCKET_ROOM || "LARAVEL",
+    WEBSOCKET_API_KEY:
+      process.env.WEBSOCKET_API_KEY || fileEnv.WEBSOCKET_API_KEY || "",
+  };
+};
+
 export default defineConfig((ctx) => {
+  const appEnv = loadAppEnv(ctx);
+
   return {
     // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
     // preFetch: true,
@@ -54,17 +121,7 @@ export default defineConfig((ctx) => {
 
       // publicPath: '/',
       // analyze: true,
-      env: {
-        API_URL: ctx.dev ? "http://localhost:3000" : "http://localhost:3000",
-        PASSWORD: ctx.dev ? "S@ft2080." : "",
-        WEBSOCKET_API: ctx.dev
-          ? "http://localhost:4321/"
-          : "http://localhost:4321/",
-        WEBSOCKET_PATH: ctx.dev ? "/socket.io" : "/socket.io",
-        WEBSOCKET_ROOM: ctx.dev ? "LARAVEL" : "LARAVEL",
-        WEBSOCKET_API_KEY:
-          "7wArC/kl0nTbt4zBu0agw.NXLyjA96I6x1XmBcuokwPqfo3/CIxzqYw.PTthh5eqa08Uf4ubFlOqatpShoz1CRRID9pZReEFvBk3il6E9u",
-      },
+      env: appEnv,
       // rawDefine: {}
       // ignorePublicFolder: true,
       // minify: false,

+ 21 - 0
src/api/payment.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const getPayments = async () => {
+  const { data } = await api.get("/payment");
+  return data.payload;
+};
+
+export const getPayment = async (id) => {
+  const { data } = await api.get(`/payment/${id}`);
+  return data.payload;
+};
+
+export const getPaymentSplits = async () => {
+  const { data } = await api.get("/payment-split");
+  return data.payload;
+};
+
+export const getProviderWithdrawals = async () => {
+  const { data } = await api.get("/provider-withdrawals");
+  return data.payload;
+};

+ 46 - 2
src/i18n/locales/en.json

@@ -712,7 +712,8 @@
       "improvement_type": "Improvement Type",
       "service_type": "Service Type",
       "speciality": "Speciality",
-      "reviews": "Reviews"
+      "reviews": "Reviews",
+      "payments": "Payments"
     }
   },
   "charts": {
@@ -730,6 +731,49 @@
       "detractors": "Detractors"
     }
   },
+  "payments": {
+    "header": "Payments",
+    "view_title": "Payment details",
+    "schedule_id": "Schedule",
+    "client": "Client",
+    "provider": "Provider",
+    "payment_method": "Method",
+    "gross_amount": "Gross amount",
+    "gateway_fee_amount": "Gateway fee",
+    "platform_fee_amount": "Platform fee",
+    "net_amount": "Net amount",
+    "splits": "Payment splits",
+    "withdrawals": "Withdrawals",
+    "withdrawal_id": "Withdrawal",
+    "no_splits": "No splits found for this payment",
+    "no_withdrawals": "No withdrawals found for this payment",
+    "payment_methods": {
+      "credit_card": "Credit card",
+      "pix": "Pix"
+    },
+    "statuses": {
+      "pending": "Pending",
+      "processing": "Processing",
+      "authorized": "Authorized",
+      "paid": "Paid",
+      "failed": "Failed",
+      "cancelled": "Cancelled"
+    },
+    "split_statuses": {
+      "pending": "Pending",
+      "processing": "Processing",
+      "transferred": "Transferred",
+      "failed": "Failed",
+      "cancelled": "Cancelled"
+    },
+    "withdrawal_statuses": {
+      "pending_transfer": "Pending transfer",
+      "processing": "Processing",
+      "transferred": "Transferred",
+      "failed": "Failed",
+      "canceled": "Canceled"
+    }
+  },
   "dashboard": {
     "currency_format": "$ {value}",
     "cards": {
@@ -766,4 +810,4 @@
       }
     }
   }
-}
+}

+ 46 - 2
src/i18n/locales/es.json

@@ -712,7 +712,8 @@
       "improvement_type": "Tipo de Mejora",
       "service_type": "Tipo de Servicio",
       "speciality": "Especialidad",
-      "reviews": "Evaluaciones"
+      "reviews": "Evaluaciones",
+      "payments": "Pagos"
     }
   },
   "charts": {
@@ -730,6 +731,49 @@
       "detractors": "Detractores"
     }
   },
+  "payments": {
+    "header": "Pagos",
+    "view_title": "Detalles del pago",
+    "schedule_id": "Agenda",
+    "client": "Cliente",
+    "provider": "Proveedor",
+    "payment_method": "Método",
+    "gross_amount": "Importe bruto",
+    "gateway_fee_amount": "Tarifa del gateway",
+    "platform_fee_amount": "Tarifa de plataforma",
+    "net_amount": "Importe neto",
+    "splits": "Splits del pago",
+    "withdrawals": "Retiros",
+    "withdrawal_id": "Retiro",
+    "no_splits": "No se encontraron splits para este pago",
+    "no_withdrawals": "No se encontraron retiros para este pago",
+    "payment_methods": {
+      "credit_card": "Tarjeta de crédito",
+      "pix": "Pix"
+    },
+    "statuses": {
+      "pending": "Pendiente",
+      "processing": "Procesando",
+      "authorized": "Autorizado",
+      "paid": "Pagado",
+      "failed": "Falló",
+      "cancelled": "Cancelado"
+    },
+    "split_statuses": {
+      "pending": "Pendiente",
+      "processing": "Procesando",
+      "transferred": "Transferido",
+      "failed": "Falló",
+      "cancelled": "Cancelado"
+    },
+    "withdrawal_statuses": {
+      "pending_transfer": "Transferencia pendiente",
+      "processing": "Procesando",
+      "transferred": "Transferido",
+      "failed": "Falló",
+      "canceled": "Cancelado"
+    }
+  },
   "dashboard": {
     "currency_format": "R$ {value}",
     "cards": {
@@ -766,4 +810,4 @@
       }
     }
   }
-}
+}

+ 46 - 2
src/i18n/locales/pt.json

@@ -712,7 +712,8 @@
       "improvement_type": "Tipo de Melhoria",
       "service_type": "Tipo de Serviço",
       "speciality": "Especialidade",
-      "reviews": "Avaliações"
+      "reviews": "Avaliações",
+      "payments": "Pagamentos"
     }
   },
   "charts": {
@@ -730,6 +731,49 @@
       "detractors": "Detratores"
     }
   },
+  "payments": {
+    "header": "Pagamentos",
+    "view_title": "Detalhes do pagamento",
+    "schedule_id": "Agendamento",
+    "client": "Cliente",
+    "provider": "Prestador",
+    "payment_method": "Método",
+    "gross_amount": "Valor bruto",
+    "gateway_fee_amount": "Taxa do gateway",
+    "platform_fee_amount": "Taxa da plataforma",
+    "net_amount": "Valor líquido",
+    "splits": "Splits do pagamento",
+    "withdrawals": "Saques",
+    "withdrawal_id": "Saque",
+    "no_splits": "Nenhum split encontrado para este pagamento",
+    "no_withdrawals": "Nenhum saque encontrado para este pagamento",
+    "payment_methods": {
+      "credit_card": "Cartão de crédito",
+      "pix": "Pix"
+    },
+    "statuses": {
+      "pending": "Pendente",
+      "processing": "Processando",
+      "authorized": "Autorizado",
+      "paid": "Pago",
+      "failed": "Falhou",
+      "cancelled": "Cancelado"
+    },
+    "split_statuses": {
+      "pending": "Pendente",
+      "processing": "Processando",
+      "transferred": "Transferido",
+      "failed": "Falhou",
+      "cancelled": "Cancelado"
+    },
+    "withdrawal_statuses": {
+      "pending_transfer": "Transferência pendente",
+      "processing": "Processando",
+      "transferred": "Transferido",
+      "failed": "Falhou",
+      "canceled": "Cancelado"
+    }
+  },
   "dashboard": {
     "currency_format": "R$ {value}",
     "cards": {
@@ -766,4 +810,4 @@
       }
     }
   }
-}
+}

+ 160 - 0
src/pages/payment/PaymentsPage.vue

@@ -0,0 +1,160 @@
+<template>
+  <q-page class="q-pa-md">
+    <DefaultHeaderPage />
+    <DefaultTable
+      ref="tableRef"
+      :columns="columns"
+      :api-call="getPayments"
+      :add-item="false"
+      :open-item="true"
+      @on-row-click="onRowClick"
+    >
+      <template #body-cell-status="templateProps">
+        <q-td :props="templateProps">
+          <q-chip
+            :label="statusLabel(templateProps.row.status)"
+            :color="paymentStatusColor(templateProps.row.status)"
+            text-color="white"
+            size="sm"
+          />
+        </q-td>
+      </template>
+
+      <template #body-cell-payment_method="templateProps">
+        <q-td :props="templateProps">
+          {{ paymentMethodLabel(templateProps.row.payment_method) }}
+        </q-td>
+      </template>
+
+      <template #body-cell-gross_amount="templateProps">
+        <q-td :props="templateProps">
+          {{ formatToBRLCurrency(templateProps.row.gross_amount) }}
+        </q-td>
+      </template>
+
+      <template #body-cell-created_at="templateProps">
+        <q-td :props="templateProps">
+          {{ formatDateTime(templateProps.row.created_at) }}
+        </q-td>
+      </template>
+    </DefaultTable>
+  </q-page>
+</template>
+
+<script setup>
+import { computed, defineAsyncComponent, ref } from "vue";
+import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+import { getPayments } from "src/api/payment";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import { formatToBRLCurrency } from "src/helpers/utils";
+import { format, parseISO } from "date-fns";
+
+const ViewPaymentDialog = defineAsyncComponent(
+  () => import("src/pages/payment/components/ViewPaymentDialog.vue"),
+);
+
+const { t } = useI18n();
+const $q = useQuasar();
+const tableRef = ref(null);
+const idLabel = "ID";
+
+const columns = computed(() => [
+  {
+    name: "id",
+    label: idLabel,
+    align: "left",
+    field: "id",
+    required: true,
+    sortable: true,
+  },
+  {
+    name: "schedule_id",
+    label: t("payments.schedule_id"),
+    align: "left",
+    field: "schedule_id",
+    sortable: true,
+  },
+  {
+    name: "client_name",
+    label: t("payments.client"),
+    align: "left",
+    field: (row) => row.client_name || row.client_id || "-",
+    sortable: true,
+  },
+  {
+    name: "provider_name",
+    label: t("payments.provider"),
+    align: "left",
+    field: (row) => row.provider_name || row.provider_id || "-",
+    sortable: true,
+  },
+  {
+    name: "payment_method",
+    label: t("payments.payment_method"),
+    align: "left",
+    field: "payment_method",
+    format: paymentMethodLabel,
+    sortable: true,
+  },
+  {
+    name: "status",
+    label: t("common.terms.status"),
+    align: "center",
+    field: "status",
+    format: statusLabel,
+    sortable: true,
+  },
+  {
+    name: "gross_amount",
+    label: t("payments.gross_amount"),
+    align: "right",
+    field: "gross_amount",
+    format: formatToBRLCurrency,
+    sortable: true,
+  },
+  {
+    name: "created_at",
+    label: t("common.terms.created_at"),
+    align: "left",
+    field: "created_at",
+    format: formatDateTime,
+    sortable: true,
+  },
+]);
+
+function paymentMethodLabel(method) {
+  return method ? t(`payments.payment_methods.${method}`) : "-";
+}
+
+function statusLabel(status) {
+  return status ? t(`payments.statuses.${status}`) : "-";
+}
+
+function paymentStatusColor(status) {
+  const colors = {
+    pending: "orange",
+    processing: "blue",
+    authorized: "indigo",
+    paid: "green",
+    failed: "negative",
+    cancelled: "grey",
+  };
+  return colors[status] || "grey";
+}
+
+function formatDateTime(value) {
+  return value ? format(parseISO(value), "dd/MM/yyyy HH:mm") : "-";
+}
+
+const onRowClick = ({ row }) => {
+  $q.dialog({
+    component: ViewPaymentDialog,
+    componentProps: {
+      payment: row,
+      title: () => t("payments.view_title"),
+    },
+  });
+};
+</script>

+ 259 - 0
src/pages/payment/components/ViewPaymentDialog.vue

@@ -0,0 +1,259 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card
+      class="q-dialog-plugin"
+      style="width: 980px; max-width: 95vw; max-height: 90vh"
+    >
+      <DefaultDialogHeader :title="title" @close="onDialogCancel" />
+
+      <q-scroll-area style="height: calc(90vh - 50px)">
+        <q-card-section class="q-gutter-md">
+          <q-list bordered separator>
+            <q-item v-for="item in paymentDetails" :key="item.label">
+              <q-item-section>
+                <q-item-label caption>{{ item.label }}</q-item-label>
+                <q-item-label>{{ item.value }}</q-item-label>
+              </q-item-section>
+            </q-item>
+          </q-list>
+
+          <div>
+            <div class="text-subtitle2 q-mb-sm">
+              {{ $t("payments.splits") }}
+            </div>
+            <q-markup-table flat bordered dense>
+              <thead>
+                <tr>
+                  <th class="text-left">{{ idLabel }}</th>
+                  <th class="text-left">{{ $t("payments.provider") }}</th>
+                  <th class="text-left">{{ $t("common.terms.status") }}</th>
+                  <th class="text-right">{{ $t("payments.gross_amount") }}</th>
+                  <th class="text-right">{{ $t("payments.net_amount") }}</th>
+                  <th class="text-left">{{ $t("payments.withdrawal_id") }}</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="split in paymentSplits" :key="split.id">
+                  <td>{{ split.id }}</td>
+                  <td>{{ split.provider_name || split.provider_id || "-" }}</td>
+                  <td>
+                    <q-chip
+                      :label="splitStatusLabel(split.status)"
+                      :color="splitStatusColor(split.status)"
+                      text-color="white"
+                      size="sm"
+                    />
+                  </td>
+                  <td class="text-right">
+                    {{ formatToBRLCurrency(split.gross_amount) }}
+                  </td>
+                  <td class="text-right">
+                    {{ formatToBRLCurrency(split.net_amount) }}
+                  </td>
+                  <td>{{ split.provider_withdrawal_id || "-" }}</td>
+                </tr>
+                <tr v-if="!loading && paymentSplits.length === 0">
+                  <td colspan="6" class="text-center text-grey-7 q-pa-md">
+                    {{ $t("payments.no_splits") }}
+                  </td>
+                </tr>
+              </tbody>
+            </q-markup-table>
+          </div>
+
+          <div>
+            <div class="text-subtitle2 q-mb-sm">
+              {{ $t("payments.withdrawals") }}
+            </div>
+            <q-markup-table flat bordered dense>
+              <thead>
+                <tr>
+                  <th class="text-left">{{ idLabel }}</th>
+                  <th class="text-left">{{ $t("payments.provider") }}</th>
+                  <th class="text-left">{{ $t("common.terms.status") }}</th>
+                  <th class="text-right">{{ $t("payments.gross_amount") }}</th>
+                  <th class="text-right">{{ $t("payments.net_amount") }}</th>
+                  <th class="text-left">{{ $t("common.terms.created_at") }}</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr
+                  v-for="withdrawal in paymentWithdrawals"
+                  :key="withdrawal.id"
+                >
+                  <td>{{ withdrawal.id }}</td>
+                  <td>
+                    {{
+                      withdrawal.provider_name || withdrawal.provider_id || "-"
+                    }}
+                  </td>
+                  <td>
+                    <q-chip
+                      :label="withdrawalStatusLabel(withdrawal.status)"
+                      :color="withdrawalStatusColor(withdrawal.status)"
+                      text-color="white"
+                      size="sm"
+                    />
+                  </td>
+                  <td class="text-right">
+                    {{ formatToBRLCurrency(withdrawal.gross_amount) }}
+                  </td>
+                  <td class="text-right">
+                    {{ formatToBRLCurrency(withdrawal.net_amount) }}
+                  </td>
+                  <td>{{ formatDateTime(withdrawal.created_at) }}</td>
+                </tr>
+                <tr v-if="!loading && paymentWithdrawals.length === 0">
+                  <td colspan="6" class="text-center text-grey-7 q-pa-md">
+                    {{ $t("payments.no_withdrawals") }}
+                  </td>
+                </tr>
+              </tbody>
+            </q-markup-table>
+          </div>
+        </q-card-section>
+      </q-scroll-area>
+
+      <q-inner-loading :showing="loading" color="primary" />
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useI18n } from "vue-i18n";
+import { format, parseISO } from "date-fns";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import { getPaymentSplits, getProviderWithdrawals } from "src/api/payment";
+import { formatToBRLCurrency } from "src/helpers/utils";
+
+const props = defineProps({
+  payment: {
+    type: Object,
+    required: true,
+  },
+  title: {
+    type: Function,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+const { t } = useI18n();
+const loading = ref(false);
+const splits = ref([]);
+const withdrawals = ref([]);
+const idLabel = "ID";
+
+const paymentSplits = computed(() =>
+  splits.value.filter((split) => split.payment_id === props.payment.id),
+);
+
+const paymentWithdrawals = computed(() => {
+  const withdrawalIds = new Set(
+    paymentSplits.value
+      .map((split) => split.provider_withdrawal_id)
+      .filter((id) => id !== null && id !== undefined),
+  );
+  return withdrawals.value.filter((withdrawal) =>
+    withdrawalIds.has(withdrawal.id),
+  );
+});
+
+const paymentDetails = computed(() => [
+  { label: idLabel, value: props.payment.id },
+  { label: t("payments.schedule_id"), value: props.payment.schedule_id || "-" },
+  {
+    label: t("payments.client"),
+    value: props.payment.client_name || props.payment.client_id || "-",
+  },
+  {
+    label: t("payments.provider"),
+    value: props.payment.provider_name || props.payment.provider_id || "-",
+  },
+  {
+    label: t("payments.payment_method"),
+    value: paymentMethodLabel(props.payment.payment_method),
+  },
+  { label: t("common.terms.status"), value: statusLabel(props.payment.status) },
+  {
+    label: t("payments.gross_amount"),
+    value: formatToBRLCurrency(props.payment.gross_amount),
+  },
+  {
+    label: t("payments.gateway_fee_amount"),
+    value: formatToBRLCurrency(props.payment.gateway_fee_amount),
+  },
+  {
+    label: t("payments.platform_fee_amount"),
+    value: formatToBRLCurrency(props.payment.platform_fee_amount),
+  },
+  {
+    label: t("payments.net_amount"),
+    value: formatToBRLCurrency(props.payment.net_amount),
+  },
+  {
+    label: t("common.terms.created_at"),
+    value: formatDateTime(props.payment.created_at),
+  },
+]);
+
+function paymentMethodLabel(method) {
+  return method ? t(`payments.payment_methods.${method}`) : "-";
+}
+
+function statusLabel(status) {
+  return status ? t(`payments.statuses.${status}`) : "-";
+}
+
+function splitStatusLabel(status) {
+  return status ? t(`payments.split_statuses.${status}`) : "-";
+}
+
+function withdrawalStatusLabel(status) {
+  return status ? t(`payments.withdrawal_statuses.${status}`) : "-";
+}
+
+function splitStatusColor(status) {
+  const colors = {
+    pending: "orange",
+    processing: "blue",
+    transferred: "green",
+    failed: "negative",
+    cancelled: "grey",
+  };
+  return colors[status] || "grey";
+}
+
+function withdrawalStatusColor(status) {
+  const colors = {
+    pending_transfer: "orange",
+    processing: "blue",
+    transferred: "green",
+    failed: "negative",
+    canceled: "grey",
+  };
+  return colors[status] || "grey";
+}
+
+function formatDateTime(value) {
+  return value ? format(parseISO(value), "dd/MM/yyyy HH:mm") : "-";
+}
+
+onMounted(async () => {
+  loading.value = true;
+  try {
+    const [splitItems, withdrawalItems] = await Promise.all([
+      getPaymentSplits(),
+      getProviderWithdrawals(),
+    ]);
+    splits.value = splitItems;
+    withdrawals.value = withdrawalItems;
+  } finally {
+    loading.value = false;
+  }
+});
+</script>

+ 22 - 0
src/router/routes/payment.route.js

@@ -0,0 +1,22 @@
+export default [
+  {
+    path: "/payments",
+    name: "PaymentsPage",
+    component: () => import("pages/payment/PaymentsPage.vue"),
+    meta: {
+      title: "payments.header",
+      requireAuth: true,
+      requiredPermission: ["payment", "config.payment"],
+      breadcrumbs: [
+        {
+          name: "DashboardPage",
+          title: "ui.navigation.dashboard",
+        },
+        {
+          name: "PaymentsPage",
+          title: "payments.header",
+        },
+      ],
+    },
+  },
+];

+ 9 - 0
src/stores/navigation.js

@@ -40,6 +40,15 @@ export const navigationStore = defineStore("navigation", () => {
       permission: false,
       permissionScope: "config.review",
     },
+    {
+      type: "single",
+      title: "ui.navigation.payments",
+      name: "PaymentsPage",
+      icon: "mdi-cash-multiple",
+      disable: false,
+      permission: false,
+      permissionScope: ["payment", "config.payment"],
+    },
     {
       type: "expansive",
       title: "ui.navigation.registration",

+ 4 - 0
src/stores/permission.js

@@ -78,6 +78,10 @@ export const permissionStore = defineStore("permission", () => {
       return true;
     }
 
+    if (Array.isArray(scopeName)) {
+      return scopeName.some((scope) => getAccess(scope, permissionType));
+    }
+
     if (permissions.value) {
       let checkPermission = 0;
       const scope = permissions.value.find(