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

863 lines
38 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { adminResultsApi, AdminResultItem, apiClient } from '../../services/api';
import { Database, AlertTriangle, RefreshCw, Search, X } from 'lucide-react';
import * as XLSX from 'xlsx';
interface Tenant {
_id: string;
name: string;
}
interface User {
_id: string;
name: string;
email: string;
role: string;
tenant: {
_id: string;
name: string;
} | null;
isActive: boolean;
}
const AdminResultsPage: React.FC = () => {
const { t } = useTranslation();
const [items, setItems] = useState<AdminResultItem[]>([]);
const [page, setPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(20);
const [totalPages, setTotalPages] = useState<number>(0);
const [total, setTotal] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [rut, setRut] = useState<string>('');
const [tenantId, setTenantId] = useState<string>('');
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [tenants, setTenants] = useState<Tenant[]>([]);
const [selectedTenants, setSelectedTenants] = useState<string[]>([]);
const [selectedPepFilter, setSelectedPepFilter] = useState<string>('');
const [showTenantDropdown, setShowTenantDropdown] = useState(false);
const [appliedRut, setAppliedRut] = useState<string>('');
const [appliedTenants, setAppliedTenants] = useState<string[]>([]);
const [appliedPepFilter, setAppliedPepFilter] = useState<string>('');
// Query type filter
const [selectedQueryType, setSelectedQueryType] = useState<string>('');
const [appliedQueryType, setAppliedQueryType] = useState<string>('');
// User states
const [allUsers, setAllUsers] = useState<User[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [appliedUserId, setAppliedUserId] = useState<string>('');
// Export states
const [isExporting, setIsExporting] = useState<boolean>(false);
const [exportProgress, setExportProgress] = useState<number>(0);
// NUEVOS ESTADOS PARA "APLICAR A TODAS LAS PÁGINAS"
const [isLoadingAllPages, setIsLoadingAllPages] = useState(false);
const [allPagesProgress, setAllPagesProgress] = useState(0);
const [allPagesLoaded, setAllPagesLoaded] = useState(false);
const [fullDataset, setFullDataset] = useState<AdminResultItem[]>([]);
const loadData = async (resetPage = false) => {
setLoading(true);
setError(null);
try {
const params: any = {
page: resetPage ? 1 : page,
limit,
};
if (rut.trim()) params.rut = rut.trim();
if (tenantId.trim()) params.tenantId = tenantId.trim();
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
if (appliedUserId) params.userEmail = appliedUserId;
if (appliedQueryType) params.queryType = appliedQueryType;
const res = await adminResultsApi.list(params);
setItems(res.items);
setPage(res.page);
setTotalPages(res.totalPages);
setTotal(res.total);
} catch (err) {
setError(err instanceof Error ? err.message : t('common.error', 'Error loading data'));
} finally {
setLoading(false);
}
};
// NUEVA FUNCIÓN: Cargar todas las páginas
const loadAllData = async () => {
try {
setIsLoadingAllPages(true);
setAllPagesProgress(0);
// NO enviamos el RUT al servidor, lo filtramos localmente después con checkRowVisibility
const params: any = {
page: 1,
limit: limit,
};
// NO incluir rut en params
if (tenantId.trim()) params.tenantId = tenantId.trim();
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
if (appliedUserId) params.userEmail = appliedUserId;
if (appliedQueryType) params.queryType = appliedQueryType;
// Obtener primera página para saber total de páginas
const firstRes = await adminResultsApi.list(params);
const totalPagesToLoad = firstRes.totalPages;
let allData: AdminResultItem[] = [...firstRes.items];
setAllPagesProgress(Math.round((1 / totalPagesToLoad) * 100));
// Cargar el resto de páginas
for (let currentPage = 2; currentPage <= totalPagesToLoad; currentPage++) {
const pageParams = { ...params, page: currentPage };
const res = await adminResultsApi.list(pageParams);
allData = [...allData, ...res.items];
setAllPagesProgress(Math.round((currentPage / totalPagesToLoad) * 100));
}
// NO filtramos por RUT aquí, se filtra con checkRowVisibility
setFullDataset(allData);
setAllPagesLoaded(true);
setTotal(allData.length);
setPage(1);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar todas las páginas');
} finally {
setIsLoadingAllPages(false);
setAllPagesProgress(0);
}
};
useEffect(() => {
if (!allPagesLoaded) {
void loadData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, limit, allPagesLoaded]);
// Cerrar dropdowns al hacer clic fuera
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.dropdown-container')) {
setShowTenantDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const toggleTenant = (tenantId: string) => {
setSelectedTenants(prev =>
prev.includes(tenantId) ? prev.filter(t => t !== tenantId) : [...prev, tenantId]
);
};
const onApplyFilters = () => {
setAllPagesLoaded(false);
setFullDataset([]);
setAppliedRut(rut.trim());
setAppliedTenants([...selectedTenants]);
setAppliedPepFilter(selectedPepFilter);
setAppliedUserId(selectedUserId);
setAppliedQueryType(selectedQueryType);
// NO llamamos a loadData, solo aplicamos filtros localmente
};
const onApplyFiltersAllPages = async () => {
setAppliedRut(rut.trim());
setAppliedTenants([...selectedTenants]);
setAppliedPepFilter(selectedPepFilter);
setAppliedUserId(selectedUserId);
setAppliedQueryType(selectedQueryType);
await loadAllData();
};
const onClearFilters = () => {
setRut('');
setTenantId('');
setStartDate('');
setEndDate('');
setSelectedTenants([]);
setSelectedPepFilter('');
setAppliedRut('');
setAppliedTenants([]);
setAppliedPepFilter('');
setSelectedUserId('');
setAppliedUserId('');
setSelectedQueryType('');
setAppliedQueryType('');
setAllPagesLoaded(false);
setFullDataset([]);
setLimit(20);
setPage(1);
void loadData(true);
};
const fetchTenants = async () => {
try {
const response = await apiClient.get('/tenant/all');
if (response.data && Array.isArray(response.data)) {
setTenants(response.data);
}
} catch (err) {
console.error('Error fetching tenants:', err);
}
};
useEffect(() => { fetchTenants() }, [])
const fetchUsers = async () => {
try {
const response = await apiClient.get('/users/superadmin/all');
if (response.data && Array.isArray(response.data)) {
setAllUsers(response.data);
}
} catch (err) {
console.error('Error fetching users:', err);
}
};
useEffect(() => { fetchUsers() }, [])
const checkRowVisibility = (item: AdminResultItem) => {
// Filtro de RUT (solo cuando está aplicado)
if (appliedRut.trim()) {
if (!item.rut || !item.rut.toLowerCase().includes(appliedRut.trim().toLowerCase())) {
return false;
}
}
// Filtro de Tenants
if (appliedTenants.length > 0) {
const itemTenantId = typeof item.tenantId === 'string' ? item.tenantId : (item.tenantId as any)?._id;
if (!appliedTenants.includes(itemTenantId)) return false;
}
// Filtro de PEP
if (appliedPepFilter) {
const isPepValue = item.isPep === true ? 'pep' : item.isPep === false ? 'no-pep' : 'unknown';
if (isPepValue !== appliedPepFilter) return false;
}
// Filtro de Usuario
if (appliedUserId) {
const usuarioData = allUsers.find(u => u.email === item.userEmail);
const usuarioNombre = usuarioData ? usuarioData.name : item.userEmail;
const selectedUserData = allUsers.find(u => u.email === appliedUserId);
const selectedUserName = selectedUserData ? selectedUserData.name : appliedUserId;
if (usuarioNombre !== selectedUserName) return false;
}
// Filtro de Tipo de Consulta
if (appliedQueryType) {
const itemType = (item.type || '').toLowerCase();
const filterType = appliedQueryType.toLowerCase();
if (itemType !== filterType) return false;
}
return true;
};
const removeFilter = (filterType: 'tenant' | 'pep' | 'rut' | 'tenantId' | 'startDate' | 'endDate' | 'userId' | 'queryType', value?: string) => {
switch (filterType) {
case 'tenant':
if (value) {
const newTenants = appliedTenants.filter(t => t !== value);
setAppliedTenants(newTenants);
setSelectedTenants(newTenants);
}
break;
case 'pep':
setAppliedPepFilter('');
setSelectedPepFilter('');
break;
case 'rut':
setRut('');
setAppliedRut('');
break;
case 'tenantId':
setTenantId('');
break;
case 'startDate':
setStartDate('');
break;
case 'endDate':
setEndDate('');
break;
case 'userId':
setSelectedUserId('');
setAppliedUserId('');
break;
case 'queryType':
setSelectedQueryType('');
setAppliedQueryType('');
break;
}
// NO llamamos a loadData, solo limpiamos el filtro localmente
};
const exportToExcel = async () => {
try {
setIsExporting(true);
setExportProgress(0);
let allExportResults: AdminResultItem[] = [];
let currentExportPage = 1;
// Recolectar todos los datos de todas las páginas
while (currentExportPage <= totalPages) {
const params: any = {
page: currentExportPage,
limit: limit,
};
if (rut.trim()) params.rut = rut.trim();
if (tenantId.trim()) params.tenantId = tenantId.trim();
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
if (appliedUserId) params.userEmail = appliedUserId;
if (appliedQueryType) params.queryType = appliedQueryType;
const response = await adminResultsApi.list(params);
if (response?.items && Array.isArray(response.items)) {
allExportResults = [...allExportResults, ...response.items];
}
// Calcular progreso
setExportProgress(Math.round((currentExportPage / totalPages) * 100));
currentExportPage++;
}
// Aplicar filtros de visibilidad
const filteredResults = allExportResults.filter(r => checkRowVisibility(r));
// Preparar datos para exportar
const exportData = filteredResults.map(item => {
const fecha = item.createdAt ? new Date(item.createdAt) : null;
const fechaStr = fecha ? fecha.toLocaleDateString() : '—';
const horaStr = fecha ? fecha.toLocaleTimeString() : '—';
const nombreProveedor = item.sheriffLogData?.summaryData?.data?.sii?.razonSocial ||
item.sheriffLogData?.summaryData?.data?.compliance?.name ||
'—';
const tenantName = item.tenantName || (typeof item.tenantId === 'string' ? item.tenantId : item.tenantId && (item.tenantId as any).name) || '—';
const usuarioData = allUsers.find(u => u.email === item.userEmail);
const usuario = usuarioData ? usuarioData.name : (item.userEmail || '—');
const tipo = item.type || '—';
const estado = item.status || '—';
const tiempo = item.processingTime ? `${item.processingTime}ms` : '—';
const credito = item.creditsConsumed !== undefined && item.creditsConsumed !== null ? item.creditsConsumed : '—';
const clasificacion = item?.isPep === true ? 'PEP' : item?.isPep === false ? 'No PEP' : '—';
return {
"Tenant": tenantName,
"Usuario": usuario,
"Fecha": fechaStr,
"Hora": horaStr,
"RUT Proveedor": item.rut || '—',
"Nombre Proveedor": nombreProveedor,
"Clasificación Consulta": clasificacion,
"Tipo": tipo,
"Estado": estado,
"Tiempo": tiempo,
"Créditos": credito
};
});
// Crear archivo Excel
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Resultados Globales");
// Ajustar ancho de columnas
const columnWidths = [
{ wch: 20 }, // Tenant
{ wch: 20 }, // Usuario
{ wch: 12 }, // Fecha
{ wch: 12 }, // Hora
{ wch: 15 }, // RUT Proveedor
{ wch: 30 }, // Nombre Proveedor
{ wch: 20 }, // Clasificación Consulta
{ wch: 15 }, // Tipo
{ wch: 12 }, // Estado
{ wch: 12 }, // Tiempo
{ wch: 10 } // Créditos
];
ws['!cols'] = columnWidths;
// Descargar archivo
XLSX.writeFile(wb, `resultados_globales_${new Date().toISOString().slice(0, 10)}.xlsx`);
} catch (err) {
console.error('Error exporting to Excel:', err);
setError(err instanceof Error ? err.message : 'Error al exportar a Excel');
} finally {
setIsExporting(false);
setExportProgress(0);
}
};
// Usar el dataset apropiado dependiendo del modo
const activeDataset = allPagesLoaded ? fullDataset : items;
const visibleItems = activeDataset.filter(checkRowVisibility);
const hiddenItemsCount = activeDataset.length - visibleItems.length;
const hasActiveFilters = appliedRut || tenantId || startDate || endDate || appliedTenants.length > 0 || appliedPepFilter || appliedUserId || appliedQueryType;
// Filtrar usuarios basado en tenants seleccionados
const filteredUsers = selectedTenants.length > 0
? allUsers.filter(user => user.tenant && selectedTenants.includes(user.tenant._id))
: allUsers;
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('adminResults.title', 'Resultados Globales (Todos los Tenants)')}
</h1>
<button
className="px-4 py-2 rounded bg-green-600 text-white hover:bg-primary-700 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={exportToExcel}
disabled={isExporting}
>
{isExporting ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
Exportando... {exportProgress}%
</>
) : (
'Exportar Excel'
)}
</button>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="space-y-4">
{/* Primera fila: RUT, Tenant, Clasificación, Usuario */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">RUT</label>
<input
type="text"
value={rut}
onChange={(e) => setRut(e.target.value)}
placeholder="12.345.678-9"
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<div className="relative dropdown-container">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tenant</label>
<div className="relative mt-1">
<button
type="button"
onClick={() => setShowTenantDropdown(!showTenantDropdown)}
className="block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 px-3 py-2 text-left flex items-center justify-between"
>
<span className="truncate">
{selectedTenants.length === 0
? 'Seleccionar...'
: `${selectedTenants.length} seleccionado${selectedTenants.length > 1 ? 's' : ''}`
}
</span>
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
{showTenantDropdown && (
<div className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto">
{tenants.length > 0 ? (
tenants.map((tenant) => (
<label key={tenant._id} className="flex items-center space-x-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer">
<input
type="checkbox"
checked={selectedTenants.includes(tenant._id)}
onChange={() => toggleTenant(tenant._id)}
className="w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{tenant.name}</span>
</label>
))
) : (
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">No hay tenants disponibles</div>
)}
</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Clasificación</label>
<select
value={selectedPepFilter}
onChange={(e) => setSelectedPepFilter(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
>
<option value="">Todos</option>
<option value="pep">PEP</option>
<option value="no-pep">No PEP</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Usuario</label>
<select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
disabled={selectedTenants.length === 0}
>
<option value="">Todos los usuarios</option>
{filteredUsers.map((user) => (
<option key={user._id} value={user.email}>
{user.name}
</option>
))}
</select>
</div>
</div>
{/* Segunda fila: Tenant ID, Fecha Inicio, Fecha Fin, Tipo Consulta */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tenant ID</label>
<input
type="text"
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
placeholder="tenant ObjectId"
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('consultas.filters.startDate', 'Fecha Inicio')}</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('consultas.filters.endDate', 'Fecha Fin')}</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tipo de Consulta</label>
<select
value={selectedQueryType}
onChange={(e) => setSelectedQueryType(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
>
<option value="">Todos</option>
<option value="individual">Individual</option>
<option value="masiva">Masiva</option>
</select>
</div>
</div>
{/* Tercera fila: Botones */}
<div className="flex items-end gap-2 flex-wrap">
<button
className="px-4 py-2 rounded bg-primary-600 text-white hover:bg-primary-700 flex items-center gap-2"
onClick={onApplyFilters}
>
<Search className="h-4 w-4" /> {t('consultas.filters.apply', 'Aplicar Filtros')}
</button>
<button
className={`px-4 py-2 rounded text-white flex items-center gap-2 transition-colors ${isLoadingAllPages ? 'bg-purple-800 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700'}`}
onClick={onApplyFiltersAllPages}
disabled={isLoadingAllPages}
>
<Search className="h-4 w-4" />
{isLoadingAllPages ? `Cargando ${allPagesProgress}%` : 'Aplicar Todas las Páginas'}
</button>
<button
className="px-4 py-2 rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={onClearFilters}
>
{t('consultas.filters.clear', 'Limpiar Filtros')}
</button>
</div>
</div>
</div>
{/* Indicador cuando se han cargado todas las páginas */}
{allPagesLoaded && (
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
<div className="flex items-center gap-2">
<svg className="h-5 w-5 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-purple-800 dark:text-purple-300">
Modo: Todas las páginas cargadas ({fullDataset.length} resultados totales)
</span>
</div>
</div>
)}
{hasActiveFilters && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="flex flex-wrap gap-2 items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Filtros activos:</span>
{appliedRut && (
<button
onClick={() => removeFilter('rut')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>RUT: {appliedRut}</span>
<X className="h-3 w-3" />
</button>
)}
{appliedTenants.map(tenantId => {
const tenant = tenants.find(t => t._id === tenantId);
return (
<button
key={tenantId}
onClick={() => removeFilter('tenant', tenantId)}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Tenant: {tenant?.name || tenantId}</span>
<X className="h-3 w-3" />
</button>
);
})}
{appliedPepFilter && (
<button
onClick={() => removeFilter('pep')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Clasificación: {appliedPepFilter === 'pep' ? 'PEP' : 'No PEP'}</span>
<X className="h-3 w-3" />
</button>
)}
{tenantId && (
<button
onClick={() => removeFilter('tenantId')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Tenant ID: {tenantId}</span>
<X className="h-3 w-3" />
</button>
)}
{startDate && (
<button
onClick={() => removeFilter('startDate')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Desde: {new Date(startDate).toLocaleDateString()}</span>
<X className="h-3 w-3" />
</button>
)}
{endDate && (
<button
onClick={() => removeFilter('endDate')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Hasta: {new Date(endDate).toLocaleDateString()}</span>
<X className="h-3 w-3" />
</button>
)}
{appliedUserId && (
<button
onClick={() => removeFilter('userId')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Usuario: {allUsers.find(u => u.email === appliedUserId)?.name || appliedUserId}</span>
<X className="h-3 w-3" />
</button>
)}
{appliedQueryType && (
<button
onClick={() => removeFilter('queryType')}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors"
>
<span>Tipo: {appliedQueryType === 'individual' ? 'Individual' : 'Masiva'}</span>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
)}
{error && (
<div className="mb-4 p-3 rounded bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 flex items-center gap-2">
<AlertTriangle className="h-4 w-4" /> {error}
</div>
)}
<div className="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow">
<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">Tenant</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Usuario</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Fecha</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Hora</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">RUT Proveedor</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Nombre Proveedor</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Clasificación Consulta</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Tipo</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Estado</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Tiempo</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Crédito</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{loading ? (
<tr>
<td colSpan={12} className="px-6 py-4 text-center text-gray-600 dark:text-gray-300">
{t('common.loading', 'Cargando...')}
</td>
</tr>
) : visibleItems.length === 0 ? (
<tr>
<td colSpan={12} className="px-6 py-4 text-center text-gray-600 dark:text-gray-300">
{activeDataset.length > 0 ? 'Hay resultados en esta página pero están ocultos por los filtros.' : t('resultsTable.noResults', 'No se encontraron resultados')}
</td>
</tr>
) : (
visibleItems.map(item => {
const fecha = item.createdAt ? new Date(item.createdAt) : null;
const fechaStr = fecha ? fecha.toLocaleDateString() : '—';
const horaStr = fecha ? fecha.toLocaleTimeString() : '—';
const nombreProveedor = item.sheriffLogData?.summaryData?.data?.sii?.razonSocial ||
item.sheriffLogData?.summaryData?.data?.compliance?.name ||
'—';
const tenantName = item.tenantName || (typeof item.tenantId === 'string' ? item.tenantId : item.tenantId && (item.tenantId as any).name) || '—';
const usuarioData = allUsers.find(u => u.email === item.userEmail);
const usuario = usuarioData ? usuarioData.name : (item.userEmail || '—');
const tipo = item.type || '—';
const estado = item.status || '—';
const tiempo = item.processingTime ? `${item.processingTime}ms` : '—';
const credito = item.creditsConsumed !== undefined && item.creditsConsumed !== null ? item.creditsConsumed : '—';
return (
<tr key={item._id}>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300 flex items-center gap-2">
<Database className="h-4 w-4" /> {tenantName}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{usuario}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{fechaStr}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{horaStr}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{item.rut || '—'}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{nombreProveedor}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{item?.isPep === true ? 'PEP' : item?.isPep === false ? 'No PEP' : '—'}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{tipo}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{estado}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{tiempo}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
{credito}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-300">
<a
className="text-primary-600 hover:text-primary-700"
href={`/consultas?rut=${encodeURIComponent(item.rut || '')}`}
>
{t('common.view', 'Ver')}
</a>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-300">
{allPagesLoaded ? (
<>
Mostrando todos los resultados | Total: {fullDataset.length}
{(appliedTenants.length > 0 || appliedPepFilter || appliedUserId || appliedQueryType) && (
<span className="ml-2 font-semibold text-primary-600 dark:text-primary-400">
({visibleItems.length} visibles después de filtros)
</span>
)}
</>
) : (
<>
Mostrando página {page} de {totalPages} | Total: {total}
{(appliedTenants.length > 0 || appliedPepFilter) && (
<span className="ml-2 font-semibold text-primary-600 dark:text-primary-400">
(En esta página: {visibleItems.length} visibles, {hiddenItemsCount} ocultos)
</span>
)}
</>
)}
</div>
{!allPagesLoaded && (
<div className="flex items-center gap-3">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
{t('common.previous', 'Anterior')}
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
>
{t('common.next', 'Siguiente')}
</button>
<select
value={limit}
onChange={(e) => setLimit(parseInt(e.target.value, 10))}
className="rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500"
>
{[10, 20, 50, 100].map(n => (
<option key={n} value={n}>{n} / {t('common.page', 'Página')}</option>
))}
</select>
</div>
)}
</div>
</div>
);
};
export default AdminResultsPage;