223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
/**
|
||
* 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;
|