new funcionality
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import StatTile from '../../components/charts/StatTile.vue';
|
||||
@@ -26,10 +26,28 @@ onMounted(load);
|
||||
|
||||
const appItems = computed(() => (summary.value?.logins_by_app ?? []).map((a) => ({ label: a.client_name, value: a.count })));
|
||||
const userItems = computed(() => (summary.value?.top_users ?? []).map((u) => ({ label: u.name, value: u.count })));
|
||||
const activeTokenItems = computed(() => (summary.value?.active_tokens_by_client ?? []).map((c) => ({ label: c.client_name, value: c.count })));
|
||||
const scopeItems = computed(() => (summary.value?.scope_usage ?? []).map((s) => ({ label: s.description, value: s.count })));
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString(locale.value, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
async function exportPdf(): Promise<void> {
|
||||
const tableWasHidden = !showTable.value;
|
||||
if (tableWasHidden) showTable.value = true;
|
||||
await nextTick();
|
||||
|
||||
// Fuerza tema claro para la impresión (ahorra tinta, evita fondos oscuros
|
||||
// recortados) y lo restaura después de que el usuario cierre el diálogo.
|
||||
const wasDark = document.documentElement.classList.contains('dark');
|
||||
document.documentElement.classList.remove('dark');
|
||||
|
||||
window.print();
|
||||
|
||||
if (wasDark) document.documentElement.classList.add('dark');
|
||||
if (tableWasHidden) showTable.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,12 +58,18 @@ function formatDateTime(iso: string): string {
|
||||
<p class="page-subtitle">{{ t('analytics.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- filtro de rango de fechas: una fila, arriba de todo, escopa todo lo de abajo -->
|
||||
<select v-model.number="days" class="pill-select">
|
||||
<option :value="7">{{ t('analytics.last7') }}</option>
|
||||
<option :value="30">{{ t('analytics.last30') }}</option>
|
||||
<option :value="90">{{ t('analytics.last90') }}</option>
|
||||
</select>
|
||||
<div class="analytics__header-controls no-print">
|
||||
<!-- filtro de rango de fechas: una fila, arriba de todo, escopa todo lo de abajo -->
|
||||
<select v-model.number="days" class="pill-select">
|
||||
<option :value="7">{{ t('analytics.last7') }}</option>
|
||||
<option :value="30">{{ t('analytics.last30') }}</option>
|
||||
<option :value="90">{{ t('analytics.last90') }}</option>
|
||||
</select>
|
||||
|
||||
<button v-if="summary" @click="exportPdf" type="button" class="btn btn--secondary">
|
||||
{{ t('analytics.exportPdf') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="loading && !summary" class="analytics__loading">{{ t('common.loading') }}</p>
|
||||
@@ -56,6 +80,16 @@ function formatDateTime(iso: string): string {
|
||||
<StatTile :label="t('analytics.activeSessions')" :value="summary.kpis.active_sessions" :hint="t('analytics.activeSessionsHint')" />
|
||||
<StatTile :label="t('analytics.loginsToday')" :value="summary.kpis.logins_today" />
|
||||
<StatTile :label="t('analytics.logins7d')" :value="summary.kpis.logins_7d" />
|
||||
<StatTile
|
||||
:label="t('analytics.loginFailureRate')"
|
||||
:value="`${summary.kpis.login_failure_rate_7d}%`"
|
||||
:hint="t('analytics.loginFailureRateHint', { count: summary.kpis.login_failures_7d })"
|
||||
/>
|
||||
<StatTile
|
||||
:label="t('analytics.oauthApprovalRate')"
|
||||
:value="`${summary.oauth_funnel.approval_rate}%`"
|
||||
:hint="t('analytics.oauthApprovalRateHint', { approved: summary.oauth_funnel.approved, attempted: summary.oauth_funnel.attempted })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="analytics__chart" :class="{ 'analytics__section--loading': loading }">
|
||||
@@ -67,10 +101,43 @@ function formatDateTime(iso: string): string {
|
||||
<BarList :title="t('analytics.topUsers')" :items="userItems" :empty-text="t('analytics.noUserActivity')" />
|
||||
</div>
|
||||
|
||||
<div class="card analytics__funnel" :class="{ 'analytics__section--loading': loading }">
|
||||
<h3 class="card__title">{{ t('analytics.funnelTitle') }}</h3>
|
||||
|
||||
<p v-if="!summary.oauth_funnel_by_client.length" class="analytics__recent-count">{{ t('analytics.funnelEmpty') }}</p>
|
||||
<div v-else class="analytics__table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table__head-cell">{{ t('analytics.colClient') }}</th>
|
||||
<th class="table__head-cell">{{ t('analytics.colAttempted') }}</th>
|
||||
<th class="table__head-cell">{{ t('analytics.colApproved') }}</th>
|
||||
<th class="table__head-cell">{{ t('analytics.colDenied') }}</th>
|
||||
<th class="table__head-cell">{{ t('analytics.colApprovalRate') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table__body">
|
||||
<tr v-for="row in summary.oauth_funnel_by_client" :key="row.client_name">
|
||||
<td class="table__cell table__cell--primary">{{ row.client_name }}</td>
|
||||
<td class="table__cell">{{ row.attempted }}</td>
|
||||
<td class="table__cell">{{ row.approved }}</td>
|
||||
<td class="table__cell">{{ row.denied }}</td>
|
||||
<td class="table__cell">{{ row.attempted ? Math.round((row.approved / row.attempted) * 100) : 0 }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analytics__bar-lists" :class="{ 'analytics__section--loading': loading }">
|
||||
<BarList :title="t('analytics.activeTokensByClient')" :items="activeTokenItems" :empty-text="t('analytics.noActiveTokens')" />
|
||||
<BarList :title="t('analytics.scopeUsageTitle')" :items="scopeItems" :empty-text="t('analytics.noScopeUsage')" />
|
||||
</div>
|
||||
|
||||
<div class="card analytics__recent" :class="{ 'analytics__section--loading': loading }">
|
||||
<div class="analytics__recent-header">
|
||||
<h3 class="card__title">{{ t('analytics.recentActivity') }}</h3>
|
||||
<button @click="showTable = !showTable" class="link analytics__toggle">
|
||||
<button @click="showTable = !showTable" class="link analytics__toggle no-print">
|
||||
{{ showTable ? t('analytics.hide') : t('analytics.viewAll') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -109,6 +176,12 @@ function formatDateTime(iso: string): string {
|
||||
--series-2: #eb6834;
|
||||
}
|
||||
|
||||
.analytics__header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.analytics__loading {
|
||||
margin-top: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
@@ -132,6 +205,10 @@ function formatDateTime(iso: string): string {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.analytics__funnel {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.analytics__recent {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user