semillero-special-hotel/src/app/admin/audit-logs/page.tsx

185 lines
6.7 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Header from '@/components/Header';
import { useLocale } from '@/lib/i18n/LocaleContext';
import { useUser } from '@/lib/auth/UserContext';
import styles from './page.module.css';
interface AuditLog {
id: number;
userId: number;
action: 'CREATE' | 'UPDATE' | 'DELETE' | 'APPROVE' | 'REJECT' | 'LOGIN';
targetTable: string;
targetId: number;
previousValue: any;
newValue: any;
ipAddress: string | null;
createdAt: string;
user: {
username: string;
email: string;
role: string;
};
}
export default function AuditLogsPage() {
const router = useRouter();
const { t, locale } = useLocale();
const { role, isLoading: authLoading } = useUser();
const [logs, setLogs] = useState<AuditLog[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const fetchLogs = async () => {
setIsLoading(true);
try {
const res = await fetch('/api/audit-logs');
if (res.ok) {
const data = await res.json();
setLogs(data.logs || []);
}
} catch (err) {
console.error('Failed to fetch audit logs', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (!authLoading && role !== 'admin') {
router.push('/unauthorized');
}
}, [role, authLoading, router]);
useEffect(() => {
if (role === 'admin') {
fetchLogs();
}
}, [role]);
const handleRowClick = (log: AuditLog) => {
setSelectedLog(log);
};
if (authLoading || (role !== 'admin' && role !== null)) {
return <div className={styles.loading}>Verificando credenciales...</div>;
}
if (role !== 'admin') {
return null; // Redirecting...
}
return (
<div className={styles.container}>
<Header activeTab="audit" />
<main className={styles.main} id="audit-logs-main-view">
<div className={styles.titleSection}>
<h1 className={styles.title}>{t('audit.title')}</h1>
</div>
<div className={styles.layoutGrid}>
{/* Audit Logs Table */}
<div className={styles.tableContainer}>
{isLoading ? (
<div className={styles.loading}>Cargando registros...</div>
) : logs.length === 0 ? (
<div className={styles.emptyState}>{t('audit.empty')}</div>
) : (
<table className={styles.table}>
<thead>
<tr>
<th className={styles.th}>{t('audit.timestamp')}</th>
<th className={styles.th}>{t('audit.actor')}</th>
<th className={styles.th}>{t('audit.action')}</th>
<th className={styles.th}>{t('audit.target_table')}</th>
<th className={styles.th}>ID</th>
<th className={styles.th}>{t('audit.ip_address')}</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr
key={log.id}
onClick={() => handleRowClick(log)}
className={`${styles.tr} ${selectedLog?.id === log.id ? styles.trSelected : ''}`}
data-audit-id={log.id}
>
<td className={styles.td}>
{new Date(log.createdAt).toLocaleString(locale === 'es' ? 'es-CO' : 'en-US')}
</td>
<td className={styles.td}>
<div><strong>{log.user.username}</strong></div>
<div style={{ fontSize: '0.75rem', opacity: 0.7 }}>{log.user.role}</div>
</td>
<td className={styles.td}>
<span className={`${styles.badge} ${styles['badge' + log.action]}`}>
{log.action}
</span>
</td>
<td className={styles.td}>{log.targetTable}</td>
<td className={styles.td}>{log.targetId}</td>
<td className={styles.td}>{log.ipAddress || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Details Side Panel */}
<div className={styles.detailsPanel}>
<h3 className={styles.panelTitle}>{t('audit.details')}</h3>
{selectedLog ? (
<div>
<div className={styles.panelHeaderInfo}>
<div><strong>ID:</strong> {selectedLog.id}</div>
<div><strong>Actor:</strong> {selectedLog.user.username} ({selectedLog.user.email})</div>
<div><strong>Acción:</strong> {selectedLog.action}</div>
<div><strong>Tabla:</strong> {selectedLog.targetTable} (ID: {selectedLog.targetId})</div>
<div><strong>Fecha:</strong> {new Date(selectedLog.createdAt).toLocaleString()}</div>
<div><strong>IP:</strong> {selectedLog.ipAddress || '-'}</div>
</div>
<div className={styles.diffContainer}>
<h4 style={{ margin: '0 0 10px 0', fontSize: '0.875rem' }}>{t('audit.diff_panel')}</h4>
<div className={styles.diffBox}>
<span className={styles.diffTitle}>{t('audit.previous')}</span>
{selectedLog.previousValue ? (
<pre className={styles.jsonPre}>
{JSON.stringify(selectedLog.previousValue, null, 2)}
</pre>
) : (
<div className={styles.jsonPre} style={{ opacity: 0.5 }}>
{t('audit.no_diff')}
</div>
)}
</div>
<div className={styles.diffBox}>
<span className={styles.diffTitle}>{t('audit.new')}</span>
{selectedLog.newValue ? (
<pre className={styles.jsonPre}>
{JSON.stringify(selectedLog.newValue, null, 2)}
</pre>
) : (
<div className={styles.jsonPre} style={{ opacity: 0.5 }}>
{t('audit.no_diff')}
</div>
)}
</div>
</div>
</div>
) : (
<div className={styles.emptyDetails}>
Seleccione una fila para ver el JSON Diff
</div>
)}
</div>
</div>
</main>
</div>
);
}