frontend_nyla/src/pages/admin/AdminAutomationLogsPage.tsx
2026-03-22 22:19:47 +01:00

223 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* AdminAutomationLogsPage
*
* SysAdmin-only page for viewing consolidated automation execution logs
* across all mandates and feature instances.
* Uses FormGeneratorTable with backend-driven pagination.
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { FaSync, FaCheck, FaExclamationCircle, FaTimes } from 'react-icons/fa';
import api from '../../api';
import styles from './Admin.module.css';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
interface AutomationLogEntry {
id: string;
timestamp: number;
automationId: string;
automationLabel: string;
mandateName: string;
featureInstanceName: string;
executedBy: string;
status: string;
workflowId: string;
messages: string;
}
const _formatTimestamp = (ts: unknown): React.ReactNode => {
if (!ts || typeof ts !== 'number') return <span style={{ color: 'var(--text-tertiary, #999)' }}></span>;
return new Date(ts * 1000).toLocaleString('de-CH', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
});
};
const _formatStatus = (value: unknown): React.ReactNode => {
const status = String(value || '');
const map: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
completed: { icon: <FaCheck style={{ marginRight: 4 }} />, color: 'var(--success-color, #16a34a)', label: 'Abgeschlossen' },
error: { icon: <FaExclamationCircle style={{ marginRight: 4 }} />, color: 'var(--error-color, #dc2626)', label: 'Fehler' },
failed: { icon: <FaExclamationCircle style={{ marginRight: 4 }} />, color: 'var(--error-color, #dc2626)', label: 'Fehlgeschlagen' },
stopped: { icon: <FaTimes style={{ marginRight: 4 }} />, color: 'var(--warning-color, #d97706)', label: 'Gestoppt' },
};
const entry = map[status];
if (!entry) return status || '';
return (
<span style={{ display: 'inline-flex', alignItems: 'center', color: entry.color, fontWeight: 500 }}>
{entry.icon}{entry.label}
</span>
);
};
export const AdminAutomationLogsPage: React.FC = () => {
const [logs, setLogs] = useState<AutomationLogEntry[]>([]);
const [pagination, setPagination] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const _fetchLogs = useCallback(async (params?: any) => {
try {
setLoading(true);
setError(null);
const requestParams: Record<string, string> = {};
if (params && typeof params === 'object') {
const paginationObj: any = {};
if (params.page !== undefined) paginationObj.page = params.page;
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
}
const response = await api.get('/api/admin/automation-logs', { params: requestParams });
const data = response.data;
if (data && typeof data === 'object' && 'items' in data) {
setLogs(data.items || []);
if (data.pagination) setPagination(data.pagination);
} else {
setLogs(Array.isArray(data) ? data : []);
setPagination(null);
}
} catch (err: any) {
setError(err.response?.data?.detail || 'Fehler beim Laden der Ausführungsprotokolle');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { _fetchLogs(); }, [_fetchLogs]);
const columns: ColumnConfig[] = useMemo(() => [
{
key: 'timestamp',
label: 'Zeitpunkt',
type: 'number' as const,
sortable: true,
filterable: false,
width: 170,
minWidth: 140,
formatter: _formatTimestamp,
},
{
key: 'automationLabel',
label: 'Automatisierung',
type: 'string' as const,
sortable: true,
filterable: true,
searchable: true,
width: 200,
minWidth: 130,
},
{
key: 'mandateName',
label: 'Mandant',
type: 'string' as const,
sortable: true,
filterable: true,
width: 150,
minWidth: 100,
},
{
key: 'featureInstanceName',
label: 'Feature-Instanz',
type: 'string' as const,
sortable: true,
filterable: true,
width: 150,
minWidth: 100,
},
{
key: 'executedBy',
label: 'Ausgeführt von',
type: 'string' as const,
sortable: true,
filterable: true,
width: 140,
minWidth: 100,
},
{
key: 'status',
label: 'Status',
type: 'string' as const,
sortable: true,
filterable: true,
width: 140,
minWidth: 100,
formatter: _formatStatus,
},
{
key: 'workflowId',
label: 'Workflow-ID',
type: 'string' as const,
sortable: false,
filterable: false,
width: 120,
minWidth: 80,
formatter: (v: unknown) =>
v ? <code style={{ fontSize: '0.8em', color: 'var(--text-secondary)' }}>{String(v).slice(0, 8)}</code> : '',
},
{
key: 'messages',
label: 'Meldungen',
type: 'string' as const,
sortable: false,
filterable: false,
searchable: true,
width: 300,
minWidth: 150,
maxWidth: 500,
},
], []);
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Ausführungsprotokolle</h1>
<p className={styles.pageSubtitle}>
Konsolidierte Automation-Logs über alle Mandanten
</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => _fetchLogs()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
</div>
</div>
{error && (
<div className={styles.infoBox} style={{ background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}>
<span style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }}>!</span>
{error}
</div>
)}
<FormGeneratorTable
data={logs}
columns={columns}
apiEndpoint="/api/admin/automation-logs"
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
hookData={{
refetch: _fetchLogs,
pagination,
}}
emptyMessage="Keine Ausführungsprotokolle vorhanden"
/>
</div>
);
};
export default AdminAutomationLogsPage;