fixed tables and forms

This commit is contained in:
ValueOn AG 2026-03-22 22:19:47 +01:00
parent e6d28c436b
commit 05317e64ca
9 changed files with 233 additions and 34 deletions

View file

@ -38,7 +38,7 @@ import { SettingsPage } from './pages/Settings';
import { GDPRPage } from './pages/GDPR';
import StorePage from './pages/Store';
import { FeatureViewPage } from './pages/FeatureView';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage } from './pages/admin';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminAutomationLogsPage, AdminLogsPage } from './pages/admin';
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
@ -188,9 +188,10 @@ function App() {
<Route path="billing">
<Route index element={<BillingAdmin />} />
<Route path="mandates" element={<BillingMandateView />} />
<Route path="subscriptions" element={<AdminSubscriptionsPage />} />
</Route>
<Route path="subscriptions" element={<AdminSubscriptionsPage />} />
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
<Route path="automation-logs" element={<AdminAutomationLogsPage />} />
<Route path="logs" element={<AdminLogsPage />} />
<Route path="mandate-wizard" element={<AdminMandateWizardPage />} />
<Route path="invitation-wizard" element={<AdminInvitationWizardPage />} />

View file

@ -70,6 +70,8 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
'page.admin.subscriptions': <FaFileContract />,
'page.admin.automationEvents': <FaClock />,
'page.admin.automation-events': <FaClock />,
'page.admin.automationLogs': <FaClipboardList />,
'page.admin.automation-logs': <FaClipboardList />,
'page.admin.logs': <FaFileAlt />,
'page.admin.mandate-wizard': <FaHatWizard />,
'page.admin.mandateWizard': <FaHatWizard />,

View file

@ -28,7 +28,7 @@ import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsVi
import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/realestate';
// Automation Views
import { AutomationDefinitionsView, AutomationTemplatesView, AutomationLogsView } from './views/automation';
import { AutomationDefinitionsView, AutomationTemplatesView } from './views/automation';
// Workspace Views
import { WorkspacePage } from './views/workspace/WorkspacePage';
@ -126,7 +126,6 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
automation: {
definitions: AutomationDefinitionsView,
templates: AutomationTemplatesView,
logs: AutomationLogsView,
},
workspace: {
dashboard: WorkspacePage,

View file

@ -0,0 +1,223 @@
/**
* 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;

View file

@ -16,4 +16,5 @@ export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
export { AdminAutomationLogsPage } from './AdminAutomationLogsPage';
export { AdminLogsPage } from './AdminLogsPage';

View file

@ -721,7 +721,7 @@ export const BillingAdmin: React.FC = () => {
</div>
{isSysAdmin && (
<Link
to="/admin/billing/subscriptions"
to="/admin/subscriptions"
className={`${styles.button} ${styles.buttonPrimary}`}
style={{ whiteSpace: 'nowrap', marginTop: 4 }}
>

View file

@ -1,14 +0,0 @@
/**
* AutomationLogsView
*
* Placeholder view for automation execution logs.
*/
import React from 'react';
import styles from '../../FeatureView.module.css';
export const AutomationLogsView: React.FC = () => (
<div className={styles.placeholder}>
<h2>Execution Logs</h2>
<p>Automatisierungs-Ausführungsprotokolle</p>
</div>
);

View file

@ -4,4 +4,3 @@
export { AutomationDefinitionsView } from './AutomationDefinitionsView';
export { AutomationTemplatesView } from './AutomationTemplatesView';
export { AutomationLogsView } from './AutomationLogsView';

View file

@ -11,7 +11,7 @@ import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaReceipt, FaDownload } from 'react-icons/fa';
import { FaSync, FaDownload } from 'react-icons/fa';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import { fetchSyncStatus, syncPositionsToAccounting, type AccountingSyncStatus } from '../../../api/trusteeApi';
@ -293,19 +293,7 @@ export const TrusteePositionsView: React.FC = () => {
return col;
});
const createdAtCol = {
key: '_createdAt',
label: 'Erstellt am',
type: 'timestamp' as any,
sortable: true,
filterable: false,
searchable: false,
width: 150,
minWidth: 120,
maxWidth: 200,
};
const allColumns = [...attrColumns, belegeColumn, syncStatusColumn, createdAtCol];
const allColumns = [...attrColumns, belegeColumn, syncStatusColumn];
const byKey = new Map(allColumns.map(c => [c.key, c]));
const ordered: typeof allColumns = [];