fastcheck/src/pages/admin/TenantMonitoringStatusPage.tsx
2026-04-08 13:58:46 -04:00

398 lines
17 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { Activity, Clock, CheckCircle, XCircle, AlertTriangle, Users, BarChart3, TrendingUp } from 'lucide-react';
import PageLoader from '../../components/common/PageLoader';
import { logger } from '../../utils/logger';
import { useTranslation } from 'react-i18next';
interface TenantMonitoringStats {
tenantId: string;
tenantName: string;
totalSchedules: number;
activeSchedules: number;
inactiveSchedules: number;
totalExecutions: number;
successfulExecutions: number;
failedExecutions: number;
lastExecution?: string;
nextExecution?: string;
averageExecutionsPerSchedule: number;
schedulesByFrequency: {
minute: number;
daily: number;
weekly: number;
monthly: number;
};
}
interface GlobalStats {
totalTenants: number;
totalSchedules: number;
totalActiveSchedules: number;
totalExecutions: number;
averageSchedulesPerTenant: number;
}
interface MonitoringStatistics {
tenantStats: TenantMonitoringStats[];
globalStats: GlobalStats;
}
const TenantMonitoringStatusPage: React.FC = () => {
const { token, user } = useAuth();
const { t, i18n } = useTranslation();
const locale = i18n.language || 'es-ES';
const [statistics, setStatistics] = useState<MonitoringStatistics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStatistics = async () => {
try {
setLoading(true);
setError(null);
if (!token) {
throw new Error(t('superAdminMonitoring.errors.authMissing'));
}
if (!user || user.role !== 'superuser') {
throw new Error(t('superAdminMonitoring.errors.accessDenied'));
}
logger.log('Fetching monitoring statistics with token:', token?.substring(0, 10) + '...');
const response = await fetch('/api/monitoring/statistics', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
logger.log('Response status:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
logger.error('Error response:', errorText);
throw new Error(t('superAdminMonitoring.errors.fetchFailed', { status: response.status, statusText: response.statusText }));
}
const data = await response.json();
logger.log('Received data:', data);
setStatistics(data);
} catch (err) {
logger.error('Error fetching monitoring statistics:', err);
setError(err instanceof Error ? err.message : t('superAdminMonitoring.errors.loadStats'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatistics();
}, [token]);
const getStatusColor = (activeSchedules: number, totalSchedules: number) => {
if (totalSchedules === 0) return 'text-gray-500';
const percentage = (activeSchedules / totalSchedules) * 100;
if (percentage >= 80) return 'text-green-500';
if (percentage >= 50) return 'text-yellow-500';
return 'text-red-500';
};
const getStatusIcon = (activeSchedules: number, totalSchedules: number) => {
if (totalSchedules === 0) return <AlertTriangle className="h-5 w-5 text-gray-500" />;
const percentage = (activeSchedules / totalSchedules) * 100;
if (percentage >= 80) return <CheckCircle className="h-5 w-5 text-green-500" />;
if (percentage >= 50) return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
return <XCircle className="h-5 w-5 text-red-500" />;
};
const formatDate = (dateString?: string) => {
if (!dateString) return t('superAdminMonitoring.date.never');
return new Date(dateString).toLocaleDateString(locale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getFrequencyBadge = (frequency: string, count: number) => {
if (count === 0) return null;
const colors = {
minute: 'bg-red-100 text-red-800',
daily: 'bg-blue-100 text-blue-800',
weekly: 'bg-green-100 text-green-800',
monthly: 'bg-purple-100 text-purple-800'
};
const labels = {
minute: t('superAdminMonitoring.frequency.min'),
daily: t('superAdminMonitoring.frequency.daily'),
weekly: t('superAdminMonitoring.frequency.weekly'),
monthly: t('superAdminMonitoring.frequency.monthly')
};
return (
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${colors[frequency as keyof typeof colors]}`}>
{labels[frequency as keyof typeof labels]}: {count}
</span>
);
};
if (loading) {
return <PageLoader />;
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl mx-auto">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center">
<XCircle className="h-5 w-5 text-red-500 mr-2" />
<span className="text-red-700 dark:text-red-400">{error}</span>
</div>
<button
onClick={fetchStatistics}
className="mt-3 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
>
{t('superAdminMonitoring.buttons.retry')}
</button>
</div>
</div>
</div>
);
}
if (!statistics) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl mx-auto">
<div className="text-center text-gray-500 dark:text-gray-400">
{t('superAdminMonitoring.messages.noData')}
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
{t('superAdminMonitoring.title')}
</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('superAdminMonitoring.subtitle')}
</p>
</div>
{/* Global Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<Users className="h-8 w-8 text-blue-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{t('superAdminMonitoring.stats.totalTenants')}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{statistics.globalStats.totalTenants}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<Activity className="h-8 w-8 text-green-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{t('superAdminMonitoring.stats.totalSchedules')}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{statistics.globalStats.totalSchedules}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<CheckCircle className="h-8 w-8 text-green-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{t('superAdminMonitoring.stats.activeSchedules')}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{statistics.globalStats.totalActiveSchedules}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<BarChart3 className="h-8 w-8 text-purple-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{t('superAdminMonitoring.stats.totalExecutions')}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{statistics.globalStats.totalExecutions}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<TrendingUp className="h-8 w-8 text-orange-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{t('superAdminMonitoring.stats.averagePerTenant')}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{statistics.globalStats.averageSchedulesPerTenant}
</p>
</div>
</div>
</div>
</div>
{/* Tenant Statistics Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('superAdminMonitoring.sections.tenantDetail')}
</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.tenant')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.status')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.schedules')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.executions')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.frequencies')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.lastExecution')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
{t('superAdminMonitoring.table.columns.nextExecution')}
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{statistics.tenantStats.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center">
<div className="text-gray-500 dark:text-gray-400">
<Activity className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{t('superAdminMonitoring.messages.noMonitoringConfigured')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('superAdminMonitoring.messages.noMonitoringFound')}
</p>
</div>
</td>
</tr>
) : (
statistics.tenantStats.map((tenant) => (
<tr key={tenant.tenantId} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{tenant.tenantName}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('superAdminMonitoring.labels.id')}: {tenant.tenantId.substring(0, 8)}...
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getStatusIcon(tenant.activeSchedules, tenant.totalSchedules)}
<span className={`ml-2 text-sm font-medium ${getStatusColor(tenant.activeSchedules, tenant.totalSchedules)}`}>
{tenant.totalSchedules === 0 ? t('superAdminMonitoring.status.noSchedules') :
tenant.activeSchedules === tenant.totalSchedules ? t('superAdminMonitoring.status.allActive') :
tenant.activeSchedules === 0 ? t('superAdminMonitoring.status.allInactive') :
t('superAdminMonitoring.status.activeCount', { active: tenant.activeSchedules, total: tenant.totalSchedules })}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
<div>{t('superAdminMonitoring.labels.totals')}: <span className="font-medium">{tenant.totalSchedules}</span></div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{t('superAdminMonitoring.labels.active')}: {tenant.activeSchedules} | {t('superAdminMonitoring.labels.inactive')}: {tenant.inactiveSchedules}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
<div>{t('superAdminMonitoring.labels.totals')}: <span className="font-medium">{tenant.totalExecutions}</span></div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{tenant.successfulExecutions} | {tenant.failedExecutions}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{t('superAdminMonitoring.labels.average')}: {tenant.averageExecutionsPerSchedule}{t('superAdminMonitoring.labels.perSchedule')}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-wrap gap-1">
{getFrequencyBadge('minute', tenant.schedulesByFrequency.minute)}
{getFrequencyBadge('daily', tenant.schedulesByFrequency.daily)}
{getFrequencyBadge('weekly', tenant.schedulesByFrequency.weekly)}
{getFrequencyBadge('monthly', tenant.schedulesByFrequency.monthly)}
{tenant.totalSchedules === 0 && (
<span className="text-xs text-gray-500 dark:text-gray-400">-</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{formatDate(tenant.lastExecution)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{formatDate(tenant.nextExecution)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Refresh Button */}
<div className="mt-6 flex justify-end">
<button
onClick={fetchStatistics}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center"
>
<Activity className="h-4 w-4 mr-2" />
{loading ? t('superAdminMonitoring.buttons.refreshing') : t('superAdminMonitoring.buttons.refresh')}
</button>
</div>
</div>
</div>
);
};
export default TenantMonitoringStatusPage;