int #6

Merged
p.motsch merged 6 commits from int into main 2026-06-12 12:08:34 +00:00
89 changed files with 603 additions and 342 deletions
Showing only changes of commit 0ad9006b94 - Show all commits

View file

@ -41,7 +41,7 @@ import { GDPRPage } from './pages/GDPR';
import StorePage from './pages/Store'; import StorePage from './pages/Store';
import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage'; import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage';
import { FeatureViewPage } from './pages/FeatureView'; import { FeatureViewPage } from './pages/FeatureView';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin'; import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage, AdminSessionsPage } from './pages/admin';
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards'; import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing'; import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
@ -220,6 +220,7 @@ function App() {
</Route> </Route>
<Route path="subscriptions" element={<AdminSubscriptionsPage />} /> <Route path="subscriptions" element={<AdminSubscriptionsPage />} />
<Route path="logs" element={<AdminLogsPage />} /> <Route path="logs" element={<AdminLogsPage />} />
<Route path="sessions" element={<AdminSessionsPage />} />
<Route path="languages" element={null} /> <Route path="languages" element={null} />
<Route path="database-health" element={null} /> <Route path="database-health" element={null} />
<Route path="demo-config" element={<AdminDemoConfigPage />} /> <Route path="demo-config" element={<AdminDemoConfigPage />} />

View file

@ -39,6 +39,9 @@
padding: 8px 14px; padding: 8px 14px;
} }
/* Toolbars are chrome (filter/action bars), not collapsible content regions.
title/id are still required for identification + a11y, but no visible header
bar is rendered to avoid duplicate headers above existing toolbar content. */
.panel[data-variant="toolbar"] .header { .panel[data-variant="toolbar"] .header {
display: none; display: none;
} }
@ -131,19 +134,17 @@
gap: 4px; gap: 4px;
} }
/* Stable chevron position: fixed at the right edge of the header, same spot
whether collapsed or expanded. Icon swaps (down = open, right = collapsed). */
.chevron { .chevron {
flex-shrink: 0; flex-shrink: 0;
width: 0; display: inline-flex;
height: 0; align-items: center;
border-left: 5px solid transparent; justify-content: center;
border-right: 5px solid transparent; width: 16px;
border-top: 6px solid var(--text-tertiary, #888); height: 16px;
transition: transform 0.2s ease; font-size: 12px;
transform-origin: 50% 40%; color: var(--text-tertiary, #888);
}
.panelCollapsed .chevron {
transform: rotate(-90deg);
} }
.body { .body {

View file

@ -1,11 +1,12 @@
// Copyright (c) 2026 PowerOn AG // Copyright (c) 2026 PowerOn AG
// All rights reserved. // All rights reserved.
import { type FC, useState, useEffect, useCallback } from 'react'; import { type FC, useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
import type { PanelProps } from './types'; import type { PanelProps } from './types';
import styles from './Panel.module.css'; import styles from './Panel.module.css';
function _loadCollapsed(key: string | undefined, fallback: boolean): boolean { function _loadCollapsed(key: string, fallback: boolean): boolean {
if (!key) return fallback;
try { try {
const stored = localStorage.getItem(`panel-collapse:${key}`); const stored = localStorage.getItem(`panel-collapse:${key}`);
if (stored !== null) return stored === '1'; if (stored !== null) return stored === '1';
@ -13,8 +14,7 @@ function _loadCollapsed(key: string | undefined, fallback: boolean): boolean {
return fallback; return fallback;
} }
function _saveCollapsed(key: string | undefined, value: boolean): void { function _saveCollapsed(key: string, value: boolean): void {
if (!key) return;
try { try {
localStorage.setItem(`panel-collapse:${key}`, value ? '1' : '0'); localStorage.setItem(`panel-collapse:${key}`, value ? '1' : '0');
} catch { /* noop */ } } catch { /* noop */ }
@ -23,59 +23,65 @@ function _saveCollapsed(key: string | undefined, value: boolean): void {
export const Panel: FC<PanelProps> = ({ export const Panel: FC<PanelProps> = ({
variant = 'card', variant = 'card',
title, title,
id,
subtitle, subtitle,
actions, actions,
collapsible = false, collapsible = true,
defaultCollapsed = false, defaultCollapsed = false,
collapseKey, collapseKey,
className = '', className = '',
style,
fill = false, fill = false,
children, children,
}) => { }) => {
const [collapsed, setCollapsed] = useState(() => _loadCollapsed(collapseKey, defaultCollapsed)); const { pathname } = useLocation();
const persistKey = collapseKey ?? `${pathname}:${id}`;
const [collapsed, setCollapsed] = useState(() => _loadCollapsed(persistKey, defaultCollapsed));
useEffect(() => { useEffect(() => {
_saveCollapsed(collapseKey, collapsed); _saveCollapsed(persistKey, collapsed);
}, [collapseKey, collapsed]); }, [persistKey, collapsed]);
const _toggleCollapsed = useCallback(() => { const _toggleCollapsed = useCallback(() => {
if (collapsible) setCollapsed((prev) => !prev); if (collapsible) setCollapsed((prev) => !prev);
}, [collapsible]); }, [collapsible]);
const hasHeader = title != null;
return ( return (
<div <div
className={`${styles.panel} ${collapsed ? styles.panelCollapsed : ''} ${className}`} className={`${styles.panel} ${collapsed ? styles.panelCollapsed : ''} ${className}`}
data-variant={variant} data-variant={variant}
data-fill={fill ? 'true' : undefined} data-fill={fill ? 'true' : undefined}
data-panel-id={id}
style={style}
> >
{hasHeader && ( <div
<div className={`${styles.header} ${collapsible ? styles.headerCollapsible : ''}`}
className={`${styles.header} ${collapsible ? styles.headerCollapsible : ''}`} role={collapsible ? 'button' : undefined}
role={collapsible ? 'button' : undefined} tabIndex={collapsible ? 0 : undefined}
tabIndex={collapsible ? 0 : undefined} aria-expanded={collapsible ? !collapsed : undefined}
aria-expanded={collapsible ? !collapsed : undefined} onClick={_toggleCollapsed}
onClick={_toggleCollapsed} onKeyDown={
onKeyDown={ collapsible
collapsible ? (e) => {
? (e) => { if (e.key === 'Enter' || e.key === ' ') {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault();
e.preventDefault(); _toggleCollapsed();
_toggleCollapsed();
}
} }
: undefined }
} : undefined
> }
<div className={styles.titles}> >
<span className={styles.title}>{title}</span> <div className={styles.titles}>
{subtitle && <span className={styles.subtitle}>{subtitle}</span>} <span className={styles.title}>{title}</span>
</div> {subtitle && <span className={styles.subtitle}>{subtitle}</span>}
{actions && <div className={styles.actions}>{actions}</div>}
{collapsible && <span className={styles.chevron} aria-hidden />}
</div> </div>
)} {actions && <div className={styles.actions} onClick={(e) => e.stopPropagation()}>{actions}</div>}
{collapsible && (
<span className={styles.chevron} aria-hidden>
{collapsed ? <FaChevronRight /> : <FaChevronDown />}
</span>
)}
</div>
<div className={`${styles.body} ${collapsed ? styles.bodyHidden : ''}`}> <div className={`${styles.body} ${collapsed ? styles.bodyHidden : ''}`}>
{children} {children}
</div> </div>

View file

@ -4,7 +4,7 @@
* Shared types for the Layout component system. * Shared types for the Layout component system.
*/ */
import type { ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ScrollMode (from useScrollMode hook) // ScrollMode (from useScrollMode hook)
@ -81,13 +81,19 @@ export type PanelVariant = 'card' | 'table' | 'dashboard' | 'toolbar' | 'editor'
export interface PanelProps { export interface PanelProps {
variant?: PanelVariant; variant?: PanelVariant;
title?: string | ReactNode; /** Region title (required). Rendered in the header; use t() for i18n. */
title: string | ReactNode;
/** Stable, non-i18n region id (required). Used for collapse persistence. */
id: string;
subtitle?: string | ReactNode; subtitle?: string | ReactNode;
actions?: ReactNode; actions?: ReactNode;
/** Collapse/expand toggle. Default true (opt-out for chat/editor regions). */
collapsible?: boolean; collapsible?: boolean;
defaultCollapsed?: boolean; defaultCollapsed?: boolean;
/** Explicit persistence key. Defaults to `{pathname}:{id}`. */
collapseKey?: string; collapseKey?: string;
className?: string; className?: string;
style?: CSSProperties;
/** /**
* Fill the available height of the parent flex container and let the body * Fill the available height of the parent flex container and let the body
* own its scroll. Use when a `card` (or any non-table/editor) Panel is placed * own its scroll. Use when a `card` (or any non-table/editor) Panel is placed

View file

@ -475,7 +475,7 @@ export const ComplianceAuditPage: React.FC = () => {
}, [detail?.neutralizationMappings]); }, [detail?.neutralizationMappings]);
return ( return (
<Panel variant="editor" title={t('AI-Audit Inhalt')}> <Panel variant="editor" title={t('AI-Audit Inhalt')} id="ai-audit-content">
{detail?.neutralizationMappings && detail.neutralizationMappings.length > 0 && ( {detail?.neutralizationMappings && detail.neutralizationMappings.length > 0 && (
<div className={styles.modalMappingBar}> <div className={styles.modalMappingBar}>
<span className={styles.modalMappingLabel}> <span className={styles.modalMappingLabel}>
@ -713,7 +713,7 @@ export const ComplianceAuditPage: React.FC = () => {
if (!selectedMandateId) return []; if (!selectedMandateId) return [];
const statsPanel = ( const statsPanel = (
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Statistiken')} id="audit-stats">
<div className={styles.statsControls}> <div className={styles.statsControls}>
<PeriodPicker <PeriodPicker
value={statsPeriod} value={statsPeriod}
@ -861,7 +861,7 @@ export const ComplianceAuditPage: React.FC = () => {
id: 'audit-log', id: 'audit-log',
label: _tabLabel('audit-log', t), label: _tabLabel('audit-log', t),
render: () => ( render: () => (
<Panel variant="table"> <Panel variant="table" title={t('Audit-Einträge')} id="audit-log-table">
<FormGeneratorTable <FormGeneratorTable
key={`audit-log-${selectedMandateId}`} key={`audit-log-${selectedMandateId}`}
data={auditEntries} data={auditEntries}
@ -887,7 +887,7 @@ export const ComplianceAuditPage: React.FC = () => {
render: () => ( render: () => (
<ViewStack entityParam="entryId"> <ViewStack entityParam="entryId">
<ViewStack.View id="list"> <ViewStack.View id="list">
<Panel variant="table"> <Panel variant="table" title={t('AI-Audit-Einträge')} id="ai-log-table">
<FormGeneratorTable <FormGeneratorTable
key={`ai-log-${selectedMandateId}`} key={`ai-log-${selectedMandateId}`}
data={aiEntries} data={aiEntries}
@ -934,7 +934,7 @@ export const ComplianceAuditPage: React.FC = () => {
id: 'neutralization', id: 'neutralization',
label: _tabLabel('neutralization', t), label: _tabLabel('neutralization', t),
render: () => ( render: () => (
<Panel variant="table"> <Panel variant="table" title={t('Neutralisierungs-Zuordnungen')} id="neutralization-table">
<FormGeneratorTable <FormGeneratorTable
key={`neut-${selectedMandateId}`} key={`neut-${selectedMandateId}`}
data={neutEntries} data={neutEntries}
@ -1010,7 +1010,7 @@ export const ComplianceAuditPage: React.FC = () => {
</p> </p>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="audit-toolbar">
<div className={styles.mandateSelector}> <div className={styles.mandateSelector}>
<label className={styles.mandateLabel}>{t('Mandant auswählen')}</label> <label className={styles.mandateLabel}>{t('Mandant auswählen')}</label>
<select <select
@ -1028,7 +1028,7 @@ export const ComplianceAuditPage: React.FC = () => {
</Panel> </Panel>
{!selectedMandateId ? ( {!selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Mandant auswählen')} id="audit-mandate-empty">
<p className={styles.emptyText}>{t('Bitte wählen Sie einen Mandanten aus.')}</p> <p className={styles.emptyText}>{t('Bitte wählen Sie einen Mandanten aus.')}</p>
</Panel> </Panel>
) : ( ) : (

View file

@ -96,7 +96,7 @@ export const DashboardPage: React.FC = () => {
)} )}
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Erste Schritte')} id="dashboard-onboarding">
<OnboardingAssistant /> <OnboardingAssistant />
</Panel> </Panel>
@ -104,6 +104,7 @@ export const DashboardPage: React.FC = () => {
<Panel <Panel
key={mandate.id} key={mandate.id}
variant="dashboard" variant="dashboard"
id={`mandate-dashboard-${mandate.id}`}
title={( title={(
<span className={styles.sectionTitle}> <span className={styles.sectionTitle}>
<FaBuilding /> <FaBuilding />

View file

@ -165,7 +165,7 @@ export const GDPRPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card" title={t('Ihre Datenrechte')}> <Panel variant="card" title={t('Ihre Datenrechte')} id="gdpr-data-rights">
<div className={styles.actions}> <div className={styles.actions}>
<div className={styles.actionCard}> <div className={styles.actionCard}>
<h3>{t('Zugriff (Artikel 15)')}</h3> <h3>{t('Zugriff (Artikel 15)')}</h3>
@ -283,7 +283,7 @@ export const GDPRPage: React.FC = () => {
)} )}
</Panel> </Panel>
<Panel variant="card" title={t('Verarbeitungsinformationen')}> <Panel variant="card" title={t('Verarbeitungsinformationen')} id="gdpr-processing-info">
{isLoadingConsent && <p className={styles.mutedText}>{t('Lade Einwilligungsinformationen')}</p>} {isLoadingConsent && <p className={styles.mutedText}>{t('Lade Einwilligungsinformationen')}</p>}
{consentError && <p className={styles.errorText}>{consentError}</p>} {consentError && <p className={styles.errorText}>{consentError}</p>}
{!isLoadingConsent && !consentError && consentInfo && ( {!isLoadingConsent && !consentError && consentInfo && (

View file

@ -218,13 +218,13 @@ export const IntegrationsOverviewPage: React.FC = () => {
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Integrationen')}</h1> <h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Integrationen')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Einleitung')} id="integrations-lead">
<p className={styles.pageLead} style={{ margin: 0 }}> <p className={styles.pageLead} style={{ margin: 0 }}>
{t('PORTA Architektur — Daten, Verarbeitung und Mandanten auf einen Blick.')} {t('PORTA Architektur — Daten, Verarbeitung und Mandanten auf einen Blick.')}
</p> </p>
</Panel> </Panel>
<Panel variant="card"> <Panel variant="card" title={t('Architekturdiagramm')} id="integrations-diagram">
<h2 className={styles.srOnly}> <h2 className={styles.srOnly}>
{t('PORTA Architektur v3: Drei separate Boxen in Schicht 2 — Infrastruktur, PORTA, Nutzen')} {t('PORTA Architektur v3: Drei separate Boxen in Schicht 2 — Infrastruktur, PORTA, Nutzen')}
</h2> </h2>

View file

@ -254,7 +254,7 @@ export const RagInventoryPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="rag-inventory-toolbar">
<div className={styles.headerRight} style={{ marginLeft: 0 }}> <div className={styles.headerRight} style={{ marginLeft: 0 }}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('Kontext:')}</label> <label className={styles.filterLabel}>{t('Kontext:')}</label>
@ -281,19 +281,19 @@ export const RagInventoryPage: React.FC = () => {
</Panel> </Panel>
{loading && !inventory && ( {loading && !inventory && (
<Panel variant="card"> <Panel variant="card" title={t('Laden...')} id="rag-inventory-loading">
<div className={styles.loading}>{t('Laden...')}</div> <div className={styles.loading}>{t('Laden...')}</div>
</Panel> </Panel>
)} )}
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="rag-inventory-error">
<div className={styles.error}>{error}</div> <div className={styles.error}>{error}</div>
</Panel> </Panel>
)} )}
{inventory && ( {inventory && (
<> <>
<Panel variant="card"> <Panel variant="card" title={t('Übersicht')} id="rag-inventory-totals">
<div className={styles.totals}> <div className={styles.totals}>
<span className={styles.totalLabel}>{t('Total Dateien')}:</span> <span className={styles.totalLabel}>{t('Total Dateien')}:</span>
<strong className={styles.totalValue}>{inventory.totals?.files ?? 0}</strong> <strong className={styles.totalValue}>{inventory.totals?.files ?? 0}</strong>
@ -306,7 +306,7 @@ export const RagInventoryPage: React.FC = () => {
</Panel> </Panel>
{(inventory.connections || []).map((conn: RagConnectionDto) => ( {(inventory.connections || []).map((conn: RagConnectionDto) => (
<Panel key={conn.id} variant="card"> <Panel key={conn.id} variant="card" title={t('Datenverbindung')} id={`rag-connection-${conn.id}`}>
<div className={styles.connectionCard} style={{ border: 'none', padding: 0, background: 'transparent' }}> <div className={styles.connectionCard} style={{ border: 'none', padding: 0, background: 'transparent' }}>
<div className={styles.connectionHeader}> <div className={styles.connectionHeader}>
<span className={styles.authority}>{conn.authority}</span> <span className={styles.authority}>{conn.authority}</span>
@ -467,7 +467,7 @@ export const RagInventoryPage: React.FC = () => {
<FaCubes style={{ marginRight: 8 }} /> <FaCubes style={{ marginRight: 8 }} />
{t('Feature-Daten')} {t('Feature-Daten')}
</span> </span>
)}> )} id="rag-feature-data">
{(inventory.featureInstances || []).map((fi: RagFeatureInstanceDto) => { {(inventory.featureInstances || []).map((fi: RagFeatureInstanceDto) => {
const runningJobs = fi.runningJobs || []; const runningJobs = fi.runningJobs || [];
const lastSuccess = fi.lastSuccess; const lastSuccess = fi.lastSuccess;
@ -574,7 +574,7 @@ export const RagInventoryPage: React.FC = () => {
)} )}
{(inventory.connections || []).length === 0 && (inventory.featureInstances || []).length === 0 && ( {(inventory.connections || []).length === 0 && (inventory.featureInstances || []).length === 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Keine Daten')} id="rag-inventory-empty">
<div className={styles.emptyState}>{t('Keine Daten für diese Sicht vorhanden.')}</div> <div className={styles.emptyState}>{t('Keine Daten für diese Sicht vorhanden.')}</div>
</Panel> </Panel>
)} )}

View file

@ -64,7 +64,7 @@ const _ProfileTab: React.FC<_ProfileTabProps> = ({ currentUser, refetchUser, onS
return ( return (
<> <>
<Panel variant="card" title={t('Konto')}> <Panel variant="card" title={t('Konto')} id="settings-account">
{!isProfileEditing ? ( {!isProfileEditing ? (
<> <>
<div className={styles.settingRow}> <div className={styles.settingRow}>
@ -104,7 +104,7 @@ const _ProfileTab: React.FC<_ProfileTabProps> = ({ currentUser, refetchUser, onS
</> </>
)} )}
</Panel> </Panel>
<Panel variant="card" title={t('Applikation')}> <Panel variant="card" title={t('Applikation')} id="settings-application">
<div className={styles.infoCard}> <div className={styles.infoCard}>
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Version')}</span><span className={styles.infoValue}>2.0.0</span></div> <div className={styles.infoRow}><span className={styles.infoLabel}>{t('Version')}</span><span className={styles.infoValue}>2.0.0</span></div>
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Build')}</span><span className={styles.infoValue}>2026.03.23</span></div> <div className={styles.infoRow}><span className={styles.infoLabel}>{t('Build')}</span><span className={styles.infoValue}>2026.03.23</span></div>
@ -232,10 +232,10 @@ const VoiceSettingsTab: React.FC = () => {
return entry ? `${entry.flag ? entry.flag + ' ' : ''}${entry.label}` : code; return entry ? `${entry.flag ? entry.flag + ' ' : ''}${entry.label}` : code;
}, [voiceCatalog]); }, [voiceCatalog]);
if (loading) return <Panel variant="card"><p style={{ padding: '1rem', color: 'var(--text-secondary, #888)' }}>{t('Einstellungen werden geladen')}</p></Panel>; if (loading) return <Panel variant="card" title={t('Spracheingabe')} id="settings-voice-loading"><p style={{ padding: '1rem', color: 'var(--text-secondary, #888)' }}>{t('Einstellungen werden geladen')}</p></Panel>;
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Spracheingabe')} id="settings-voice">
{error && <div className={styles.errorMessage}>{error}</div>} {error && <div className={styles.errorMessage}>{error}</div>}
{success && <div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', color: '#16a34a', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.875rem' }}>{success}</div>} {success && <div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', color: '#16a34a', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.875rem' }}>{success}</div>}
@ -379,10 +379,10 @@ const NeutralizationMappingsTab: React.FC = () => {
return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2); return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2);
}; };
if (loading) return <Panel variant="card"><p style={{ padding: '1rem', color: 'var(--text-secondary, #888)' }}>{t('Mappings werden geladen')}</p></Panel>; if (loading) return <Panel variant="card" title={t('Platzhaltermappings')} id="settings-mappings-loading"><p style={{ padding: '1rem', color: 'var(--text-secondary, #888)' }}>{t('Mappings werden geladen')}</p></Panel>;
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Platzhaltermappings lokal')} id="settings-mappings">
{error && <div className={styles.errorMessage}>{error}</div>} {error && <div className={styles.errorMessage}>{error}</div>}
<section className={styles.section}> <section className={styles.section}>
@ -533,11 +533,11 @@ const MfaSettingsTab: React.FC = () => {
}; };
if (loading) { if (loading) {
return <Panel variant="card"><p>{t('wird geladen…')}</p></Panel>; return <Panel variant="card" title={t('Zwei-Faktor-Authentifizierung (MFA)')} id="settings-mfa-loading"><p>{t('wird geladen…')}</p></Panel>;
} }
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Zwei-Faktor-Authentifizierung (MFA)')} id="settings-mfa">
<h2 className={styles.sectionTitle}>{t('Zwei-Faktor-Authentifizierung (MFA)')}</h2> <h2 className={styles.sectionTitle}>{t('Zwei-Faktor-Authentifizierung (MFA)')}</h2>
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}> <p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
{t('Schützen Sie Ihr Konto mit einem zusätzlichen Bestätigungscode bei der Anmeldung.')} {t('Schützen Sie Ihr Konto mit einem zusätzlichen Bestätigungscode bei der Anmeldung.')}
@ -661,7 +661,7 @@ const _AppearanceTab: React.FC<_AppearanceTabProps> = ({
const { t } = useLanguage(); const { t } = useLanguage();
return ( return (
<Panel variant="card" title={t('Darstellung')}> <Panel variant="card" title={t('Darstellung')} id="settings-appearance">
<div className={styles.settingRow}> <div className={styles.settingRow}>
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('Theme')}</label><p className={styles.settingDescription}>{t('Wählen zwischen Hell- und Dunkelmodus')}</p></div> <div className={styles.settingInfo}><label className={styles.settingLabel}>{t('Theme')}</label><p className={styles.settingDescription}>{t('Wählen zwischen Hell- und Dunkelmodus')}</p></div>
<div className={styles.settingControl}> <div className={styles.settingControl}>
@ -693,7 +693,7 @@ const _PrivacyTab: React.FC = () => {
return ( return (
<> <>
<Panel variant="card" title={t('Datenschutz')}> <Panel variant="card" title={t('Datenschutz')} id="settings-privacy">
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}> <p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
{t('Datenschutzbeschreibung')} {t('Datenschutzbeschreibung')}
</p> </p>

View file

@ -154,7 +154,7 @@ export const StorePage: React.FC = () => {
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
{subscriptionInfo && subscriptionInfo.plan && ( {subscriptionInfo && subscriptionInfo.plan && (
<Panel variant="card"> <Panel variant="card" title={t('Abonnement')} id="store-subscription">
<div className={styles.subscriptionBanner}> <div className={styles.subscriptionBanner}>
<span>{t('Plan:')} <strong>{subscriptionInfo.plan}</strong></span> <span>{t('Plan:')} <strong>{subscriptionInfo.plan}</strong></span>
<span className={styles.bannerSeparator}> <span className={styles.bannerSeparator}>
@ -188,25 +188,25 @@ export const StorePage: React.FC = () => {
)} )}
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="store-error">
<div className={styles.error}>{error}</div> <div className={styles.error}>{error}</div>
</Panel> </Panel>
)} )}
{loading ? ( {loading ? (
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="store-loading">
<div className={styles.loading}> <div className={styles.loading}>
{t('Lade Features…')} {t('Lade Features…')}
</div> </div>
</Panel> </Panel>
) : features.length === 0 ? ( ) : features.length === 0 ? (
<Panel variant="card"> <Panel variant="card" title={t('Keine Features')} id="store-empty">
<div className={styles.empty}> <div className={styles.empty}>
{t('Keine Features im Store verfügbar.')} {t('Keine Features im Store verfügbar.')}
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Features')} id="store-features">
<div className={styles.grid}> <div className={styles.grid}>
{features.map((feature) => ( {features.map((feature) => (
<FeatureCard <FeatureCard

View file

@ -310,7 +310,7 @@ export const AccessManagementHub: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="access-hub-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -338,7 +338,7 @@ export const AccessManagementHub: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="access-hub-toolbar">
<div className={hubStyles.filters}> <div className={hubStyles.filters}>
{/* Filter dropdowns only shown in list view - hierarchy shows everything */} {/* Filter dropdowns only shown in list view - hierarchy shows everything */}
{viewMode === 'list' && ( {viewMode === 'list' && (
@ -441,7 +441,7 @@ export const AccessManagementHub: React.FC = () => {
onOpenDetail={handleOpenDetail} onOpenDetail={handleOpenDetail}
/> />
) : !selectedMandateId ? ( ) : !selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Mandant ausgewählt')} id="access-hub-no-mandate">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
@ -452,7 +452,7 @@ export const AccessManagementHub: React.FC = () => {
</Panel> </Panel>
) : ( ) : (
<> <>
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Übersicht')} id="access-hub-overview">
<div className={hubStyles.overviewRow}> <div className={hubStyles.overviewRow}>
<div className={hubStyles.statsCard}> <div className={hubStyles.statsCard}>
<FaChartBar className={hubStyles.statsIcon} /> <FaChartBar className={hubStyles.statsIcon} />
@ -507,7 +507,7 @@ export const AccessManagementHub: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Feature-Instanzen')} id="access-hub-instances">
<section className={hubStyles.section}> <section className={hubStyles.section}>
<h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2> <h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2>
{loading && filteredInstances.length === 0 ? ( {loading && filteredInstances.length === 0 ? (

View file

@ -315,7 +315,7 @@ const StatsTab: React.FC = () => {
return ( return (
<> <>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="db-stats-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('Datenbank')}</label> <label className={styles.filterLabel}>{t('Datenbank')}</label>
@ -334,7 +334,7 @@ const StatsTab: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="card"> <Panel variant="card" title={t('Zusammenfassung')} id="db-stats-summary">
<div className={styles.filterSection} style={{ gap: '0.75rem', flexWrap: 'wrap' }}> <div className={styles.filterSection} style={{ gap: '0.75rem', flexWrap: 'wrap' }}>
<span className={styles.filterLabel}>{t('{dbs} Datenbanken', { dbs: totals.dbs })}</span> <span className={styles.filterLabel}>{t('{dbs} Datenbanken', { dbs: totals.dbs })}</span>
<span className={styles.filterLabel}>{t('{tables} Tabellen', { tables: totals.tables })}</span> <span className={styles.filterLabel}>{t('{tables} Tabellen', { tables: totals.tables })}</span>
@ -344,7 +344,7 @@ const StatsTab: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Tabellen')} id="db-stats-table">
<FormGeneratorTable <FormGeneratorTable
data={visibleData} data={visibleData}
columns={columns} columns={columns}
@ -634,7 +634,7 @@ const OrphansTab: React.FC = () => {
<> <>
<ConfirmDialog /> <ConfirmDialog />
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="orphans-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('Datenbank')}</label> <label className={styles.filterLabel}>{t('Datenbank')}</label>
@ -680,7 +680,7 @@ const OrphansTab: React.FC = () => {
</Panel> </Panel>
{totalOrphans > 0 && ( {totalOrphans > 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Warnung')} id="orphans-warning">
<div className={styles.infoBox} style={{ background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}> <div className={styles.infoBox} style={{ background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}>
<FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} /> <FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} />
{t('{count} verwaiste Einträge in {relations} Beziehungen gefunden', { {t('{count} verwaiste Einträge in {relations} Beziehungen gefunden', {
@ -691,7 +691,7 @@ const OrphansTab: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="table"> <Panel variant="table" title={t('Verwaiste Einträge')} id="orphans-table">
<FormGeneratorTable <FormGeneratorTable
data={visibleData} data={visibleData}
columns={columns} columns={columns}
@ -1170,7 +1170,7 @@ const MigrationTab: React.FC = () => {
<> <>
<ConfirmDialog /> <ConfirmDialog />
<Panel variant="card" title={t('Backup')}> <Panel variant="card" title={t('Backup')} id="db-backup">
<section> <section>
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: 'var(--text-primary)', margin: '0 0 1rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: 'var(--text-primary)', margin: '0 0 1rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FaDownload /> {t('Backup')} <FaDownload /> {t('Backup')}
@ -1252,7 +1252,7 @@ const MigrationTab: React.FC = () => {
</section> </section>
</Panel> </Panel>
<Panel variant="card" title={t('Restore')}> <Panel variant="card" title={t('Restore')} id="db-restore">
<section> <section>
{!uploadedFile ? ( {!uploadedFile ? (
<div <div
@ -1668,7 +1668,7 @@ const LegacyCleanupTab: React.FC = () => {
<> <>
<ConfirmDialog /> <ConfirmDialog />
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="legacy-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button className={styles.secondaryButton} onClick={_fetchLegacy} disabled={loading}> <button className={styles.secondaryButton} onClick={_fetchLegacy} disabled={loading}>
@ -1689,7 +1689,7 @@ const LegacyCleanupTab: React.FC = () => {
</Panel> </Panel>
{allLegacy.length > 0 && ( {allLegacy.length > 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Warnung')} id="legacy-warning">
<div className={styles.infoBox} style={{ background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}> <div className={styles.infoBox} style={{ background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}>
<FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} /> <FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} />
{t('{count} Legacy-Tabellen in {dbs} Datenbanken ({rows} Zeilen, {size})', { {t('{count} Legacy-Tabellen in {dbs} Datenbanken ({rows} Zeilen, {size})', {
@ -1699,7 +1699,7 @@ const LegacyCleanupTab: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="table"> <Panel variant="table" title={t('Legacy-Tabellen')} id="legacy-table">
<FormGeneratorTable <FormGeneratorTable
data={visibleData} data={visibleData}
columns={columns} columns={columns}

View file

@ -121,7 +121,7 @@ export const AdminDemoConfigPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="demo-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button className={styles.secondaryButton} onClick={_fetchConfigs} disabled={loading}> <button className={styles.secondaryButton} onClick={_fetchConfigs} disabled={loading}>
<FaSync /> {t('Aktualisieren')} <FaSync /> {t('Aktualisieren')}
@ -130,13 +130,13 @@ export const AdminDemoConfigPage: React.FC = () => {
</Panel> </Panel>
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="demo-error">
<div className={demoStyles.errorBanner}>{error}</div> <div className={demoStyles.errorBanner}>{error}</div>
</Panel> </Panel>
)} )}
{lastResult && ( {lastResult && (
<Panel variant="card"> <Panel variant="card" title={t('Ergebnis')} id="demo-result">
<div className={lastResult.status === 'ok' ? demoStyles.successBanner : demoStyles.errorBanner}> <div className={lastResult.status === 'ok' ? demoStyles.successBanner : demoStyles.errorBanner}>
<strong>{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:</strong>{' '} <strong>{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:</strong>{' '}
{lastResult.status === 'ok' ? ( {lastResult.status === 'ok' ? (
@ -151,7 +151,7 @@ export const AdminDemoConfigPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Demo-Konfigurationen')} id="demo-configs">
{loading && configs.length === 0 ? ( {loading && configs.length === 0 ? (
<div className={demoStyles.loadingState}>{t('Lade…')}</div> <div className={demoStyles.loadingState}>{t('Lade…')}</div>
) : configs.length === 0 ? ( ) : configs.length === 0 ? (

View file

@ -276,7 +276,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="feature-access-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{t('Fehler')}: {error}</p> <p className={styles.errorMessage}>{t('Fehler')}: {error}</p>
@ -300,7 +300,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="feature-access-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -348,7 +348,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
</Panel> </Panel>
{features.length > 0 ? ( {features.length > 0 ? (
<Panel variant="card"> <Panel variant="card" title={t('Verfügbare Features')} id="feature-access-features">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span>{t('Verfügbare Features')} </span> <span>{t('Verfügbare Features')} </span>
@ -361,7 +361,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : selectedMandateId && !loading ? ( ) : selectedMandateId && !loading ? (
<Panel variant="card"> <Panel variant="card" title={t('Keine Features geladen')} id="feature-access-no-features">
<div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}> <div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span> <span>
@ -382,7 +382,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
) : null} ) : null}
{!selectedMandateId ? ( {!selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Mandant ausgewählt')} id="feature-access-no-mandate">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
@ -392,7 +392,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Feature-Instanzen')} id="feature-access-table">
<FormGeneratorTable <FormGeneratorTable
data={instances} data={instances}
columns={columns} columns={columns}

View file

@ -36,7 +36,6 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
loading, loading,
error, error,
fetchFeatures, fetchFeatures,
fetchInstanceUsers,
addUserToInstance, addUserToInstance,
removeUserFromInstance, removeUserFromInstance,
updateInstanceUserRoles, updateInstanceUserRoles,
@ -403,7 +402,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="fi-users-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -429,7 +428,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="fi-users-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}> <div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -488,7 +487,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
</Panel> </Panel>
{selectedOption && ( {selectedOption && (
<Panel variant="card"> <Panel variant="card" title={t('Ausgewählte Instanz')} id="fi-users-selection">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaBuilding style={{ marginRight: 8 }} /> <FaBuilding style={{ marginRight: 8 }} />
<span> <span>
@ -504,7 +503,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
)} )}
{selectedInstance && instanceRoles.length > 0 && ( {selectedInstance && instanceRoles.length > 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Verfügbare Rollen')} id="fi-users-roles">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<span>{t('Verfügbare Rollen')} </span> <span>{t('Verfügbare Rollen')} </span>
{instanceRoles.map((r, i) => ( {instanceRoles.map((r, i) => (
@ -518,7 +517,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
)} )}
{selectedInstance && instanceRoles.length === 0 && !usersLoading && ( {selectedInstance && instanceRoles.length === 0 && !usersLoading && (
<Panel variant="card"> <Panel variant="card" title={t('Keine Rollen')} id="fi-users-no-roles">
<div className={styles.infoBox} style={{ borderColor: 'var(--warning-color, #d69e2e)', backgroundColor: 'var(--warning-bg, rgba(214, 158, 46, 0.12))' }}> <div className={styles.infoBox} style={{ borderColor: 'var(--warning-color, #d69e2e)', backgroundColor: 'var(--warning-bg, rgba(214, 158, 46, 0.12))' }}>
<span> </span> <span> </span>
<span>{t('Diese Instanz hat noch keine')}</span> <span>{t('Diese Instanz hat noch keine')}</span>
@ -527,7 +526,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
)} )}
{!selectedCombinedKey ? ( {!selectedCombinedKey ? (
<Panel variant="card"> <Panel variant="card" title={t('Keine Feature-Instanz ausgewählt')} id="fi-users-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} /> <FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanz ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Keine Feature-Instanz ausgewählt')}</h3>
@ -539,7 +538,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Benutzer')} id="fi-users-table">
<FormGeneratorTable <FormGeneratorTable
data={instanceUsers} data={instanceUsers}
columns={columns} columns={columns}

View file

@ -290,7 +290,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="feature-roles-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{error}</p> <p className={styles.errorMessage}>{error}</p>
@ -314,7 +314,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="feature-roles-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -359,7 +359,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
</Panel> </Panel>
{selectedFeatureCode && ( {selectedFeatureCode && (
<Panel variant="card"> <Panel variant="card" title={t('Feature-Template-Rollen')} id="feature-roles-info">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaUserShield style={{ marginRight: 8 }} /> <FaUserShield style={{ marginRight: 8 }} />
<span> <span>
@ -371,7 +371,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
)} )}
{!selectedFeatureCode ? ( {!selectedFeatureCode ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Feature ausgewählt')} id="feature-roles-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} /> <FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
@ -381,7 +381,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Feature-Rollen')} id="feature-roles-table">
<FormGeneratorTable <FormGeneratorTable
data={roles} data={roles}
columns={columns} columns={columns}

View file

@ -218,7 +218,7 @@ export const AdminInvitationsPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="invitations-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -244,7 +244,7 @@ export const AdminInvitationsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="invitations-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -305,7 +305,7 @@ export const AdminInvitationsPage: React.FC = () => {
</Panel> </Panel>
{!selectedMandateId ? ( {!selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Mandant ausgewählt')} id="invitations-no-mandate">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
@ -315,7 +315,7 @@ export const AdminInvitationsPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Einladungen')} id="invitations-table">
<FormGeneratorTable <FormGeneratorTable
data={invitations} data={invitations}
columns={columns} columns={columns}

View file

@ -871,7 +871,7 @@ export const AdminLanguagesPage: React.FC = () => {
const isBusy = progress !== null; const isBusy = progress !== null;
return ( return (
<StackLayout variant="table" className="" style={{ position: 'relative' }}> <StackLayout variant="table">
<StackLayout.Header> <StackLayout.Header>
<div> <div>
<h1 className={styles.pageTitle}>{t('UI-Sprachen')}</h1> <h1 className={styles.pageTitle}>{t('UI-Sprachen')}</h1>
@ -884,7 +884,7 @@ export const AdminLanguagesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="languages-toolbar">
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', alignItems: 'center' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', alignItems: 'center' }}>
<button type="button" className={styles.secondaryButton} onClick={_load} disabled={isBusy || loading} title={t('Daten neu laden')}> <button type="button" className={styles.secondaryButton} onClick={_load} disabled={isBusy || loading} title={t('Daten neu laden')}>
<FaSync style={loading ? { animation: 'spin 1s linear infinite' } : undefined} /> <FaSync style={loading ? { animation: 'spin 1s linear infinite' } : undefined} />
@ -925,7 +925,7 @@ export const AdminLanguagesPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table" className="" style={{ position: 'relative' }}> <Panel variant="table" title={t('Sprachen')} id="languages-table" className="" style={{ position: 'relative' }}>
<FormGeneratorTable <FormGeneratorTable
data={displayRows} data={displayRows}
columns={columns} columns={columns}

View file

@ -130,7 +130,7 @@ export const AdminLogsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="logs-toolbar">
<div className={logStyles.controls}> <div className={logStyles.controls}>
<div className={logStyles.loadGroup}> <div className={logStyles.loadGroup}>
<label className={logStyles.controlLabel}>{t('Letzte')}</label> <label className={logStyles.controlLabel}>{t('Letzte')}</label>
@ -167,7 +167,7 @@ export const AdminLogsPage: React.FC = () => {
</Panel> </Panel>
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="logs-error">
<div <div
className={styles.infoBox} className={styles.infoBox}
style={{ style={{
@ -181,7 +181,7 @@ export const AdminLogsPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="editor"> <Panel variant="editor" title={t('Logs')} id="logs-editor">
<div <div
ref={logContainerRef} ref={logContainerRef}
className={logStyles.logContainer} className={logStyles.logContainer}

View file

@ -229,7 +229,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler beim Laden')} id="role-permissions-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -277,7 +277,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="role-permissions-toolbar">
<div className={styles.filterBar}> <div className={styles.filterBar}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('Mandant')}</label> <label className={styles.filterLabel}>{t('Mandant')}</label>
@ -311,7 +311,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="role-permissions-info">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaShieldAlt style={{ marginRight: '0.5rem' }} /> <FaShieldAlt style={{ marginRight: '0.5rem' }} />
<span> <span>
@ -324,7 +324,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
</Panel> </Panel>
{loading && ( {loading && (
<Panel variant="card"> <Panel variant="card" title={t('Lade Rollen')} id="role-permissions-loading">
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <div className={styles.spinner} />
<span>{t('Lade Rollen')}</span> <span>{t('Lade Rollen')}</span>
@ -333,7 +333,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
)} )}
{!loading && roles.length === 0 && ( {!loading && roles.length === 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Keine Rollen gefunden')} id="role-permissions-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} /> <FaUserShield className={styles.emptyIcon} />
<p>{t('Keine Rollen gefunden')}</p> <p>{t('Keine Rollen gefunden')}</p>
@ -349,7 +349,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
)} )}
{!loading && roles.length > 0 && ( {!loading && roles.length > 0 && (
<Panel variant="card"> <Panel variant="card" title={t('Rollen')} id="role-permissions-list">
<div className={styles.rolesList}> <div className={styles.rolesList}>
{roles.map(role => ( {roles.map(role => (
<div key={role.id} className={styles.roleCard}> <div key={role.id} className={styles.roleCard}>

View file

@ -250,7 +250,7 @@ export const AdminMandateRolesPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="mandate-roles-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -292,7 +292,7 @@ export const AdminMandateRolesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="mandate-roles-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -348,7 +348,7 @@ export const AdminMandateRolesPage: React.FC = () => {
</Panel> </Panel>
{selectedMandateId && ( {selectedMandateId && (
<Panel variant="card"> <Panel variant="card" title={t('System-Templates')} id="mandate-roles-info">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaUserShield style={{ marginRight: 8 }} /> <FaUserShield style={{ marginRight: 8 }} />
<span> <span>
@ -362,7 +362,7 @@ export const AdminMandateRolesPage: React.FC = () => {
)} )}
{!selectedMandateId ? ( {!selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Mandant ausgewählt')} id="mandate-roles-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
@ -372,7 +372,7 @@ export const AdminMandateRolesPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Rollen')} id="mandate-roles-table">
<FormGeneratorTable <FormGeneratorTable
data={roles} data={roles}
columns={columns} columns={columns}

View file

@ -192,7 +192,7 @@ export const AdminMandatesPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="mandates-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -224,7 +224,7 @@ export const AdminMandatesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="mandates-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
type="button" type="button"
@ -250,7 +250,7 @@ export const AdminMandatesPage: React.FC = () => {
)} )}
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Mandanten')} id="mandates-table">
<FormGeneratorTable <FormGeneratorTable
data={mandates} data={mandates}
columns={columns} columns={columns}

View file

@ -0,0 +1,204 @@
// Copyright (c) 2026 PowerOn AG
// All rights reserved.
/**
* AdminSessionsPage
*
* Admin page for viewing and managing active sessions and trusted devices per user.
*/
import React, { useState, useCallback } from 'react';
import api from '../../api';
import { StackLayout } from '../../components/Layout/StackLayout';
import { Panel } from '../../components/Layout/Panel';
import { FaSearch, FaTrash, FaShieldAlt, FaDesktop } from 'react-icons/fa';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
interface SessionEntry {
sessionId: string;
tokenId: string;
authority: string;
createdAt: number;
expiresAt: number;
}
interface TrustedDeviceEntry {
id: string;
trustedUntil: number;
isExpired: boolean;
userAgent: string | null;
ipAddress: string | null;
createdAt: number;
}
export const AdminSessionsPage: React.FC = () => {
const { t } = useLanguage();
const [userId, setUserId] = useState('');
const [sessions, setSessions] = useState<SessionEntry[]>([]);
const [trustedDevices, setTrustedDevices] = useState<TrustedDeviceEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadData = useCallback(async () => {
if (!userId.trim()) return;
setLoading(true);
setError(null);
try {
const [sessRes, devRes] = await Promise.all([
api.get('/api/admin/sessions', { params: { userId } }),
api.get('/api/admin/trusted-devices', { params: { userId } }),
]);
setSessions(sessRes.data || []);
setTrustedDevices(devRes.data || []);
} catch (e: any) {
setError(e.response?.data?.detail || 'Failed to load session data');
} finally {
setLoading(false);
}
}, [userId]);
const revokeSession = useCallback(async (sessionId: string) => {
try {
await api.delete(`/api/admin/sessions/${sessionId}`);
setSessions(prev => prev.filter(s => s.sessionId !== sessionId));
} catch (e: any) {
setError(e.response?.data?.detail || 'Failed to revoke session');
}
}, []);
const revokeAllSessions = useCallback(async () => {
if (!userId.trim()) return;
try {
await api.delete('/api/admin/sessions', { params: { userId } });
setSessions([]);
} catch (e: any) {
setError(e.response?.data?.detail || 'Failed to revoke sessions');
}
}, [userId]);
const revokeAllTrustedDevices = useCallback(async () => {
if (!userId.trim()) return;
try {
await api.delete('/api/admin/trusted-devices', { params: { userId } });
setTrustedDevices([]);
} catch (e: any) {
setError(e.response?.data?.detail || 'Failed to revoke trusted devices');
}
}, [userId]);
const formatTimestamp = (ts: number) => {
if (!ts) return '-';
return new Date(ts * 1000).toLocaleString();
};
return (
<StackLayout variant="scroll">
<StackLayout.Header>
<div>
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Session-Verwaltung')}</h1>
</div>
</StackLayout.Header>
<StackLayout.Body>
<Panel variant="card" title={t('Sitzungen und Geräte')} id="sessions-panel">
<div className={styles.searchBar} style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '1rem' }}>
<input
type="text"
placeholder="User ID"
value={userId}
onChange={e => setUserId(e.target.value)}
onKeyDown={e => e.key === 'Enter' && loadData()}
style={{ flex: 1, padding: '0.5rem', borderRadius: '4px', border: '1px solid var(--border-color)' }}
/>
<button onClick={loadData} disabled={loading || !userId.trim()} className={styles.actionButton}>
<FaSearch /> {t('Suchen')}
</button>
</div>
{error && <div style={{ color: 'var(--color-error)', marginBottom: '1rem' }}>{error}</div>}
{/* Active Sessions */}
<h3 style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FaDesktop /> {t('Aktive Sitzungen')} ({sessions.length})
{sessions.length > 0 && (
<button onClick={revokeAllSessions} className={styles.dangerButton} style={{ marginLeft: 'auto', fontSize: '0.8rem' }}>
<FaTrash /> {t('Alle widerrufen')}
</button>
)}
</h3>
{sessions.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '2rem' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Session</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Authority</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Created</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Expires</th>
<th style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}></th>
</tr>
</thead>
<tbody>
{sessions.map(s => (
<tr key={s.tokenId}>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>
<code>{s.sessionId?.slice(0, 8)}...</code>
</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{s.authority}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{formatTimestamp(s.createdAt)}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{formatTimestamp(s.expiresAt)}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)', textAlign: 'right' }}>
<button onClick={() => revokeSession(s.sessionId)} className={styles.dangerButton} style={{ fontSize: '0.75rem' }}>
<FaTrash />
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
!loading && userId && <p style={{ color: 'var(--text-muted)' }}>{t('Keine aktiven Sitzungen')}</p>
)}
{/* Trusted Devices */}
<h3 style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FaShieldAlt /> {t('Vertrauenswürdige Geräte')} ({trustedDevices.length})
{trustedDevices.length > 0 && (
<button onClick={revokeAllTrustedDevices} className={styles.dangerButton} style={{ marginLeft: 'auto', fontSize: '0.8rem' }}>
<FaTrash /> {t('Alle widerrufen')}
</button>
)}
</h3>
{trustedDevices.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Device</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>IP</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Trusted Until</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Status</th>
</tr>
</thead>
<tbody>
{trustedDevices.map((d, i) => (
<tr key={i}>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>
<span title={d.userAgent || ''}>{d.id}</span>
</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{d.ipAddress || '-'}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{formatTimestamp(d.trustedUntil)}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>
<span style={{ color: d.isExpired ? 'var(--color-error)' : 'var(--color-success)' }}>
{d.isExpired ? 'Expired' : 'Active'}
</span>
</td>
</tr>
))}
</tbody>
</table>
) : (
!loading && userId && <p style={{ color: 'var(--text-muted)' }}>{t('Keine vertrauenswürdigen Geräte')}</p>
)}
</Panel>
</StackLayout.Body>
</StackLayout>
);
};

View file

@ -84,8 +84,6 @@ interface UserAccessOverview {
resourceAccess: AccessEntry[]; resourceAccess: AccessEntry[];
} }
type TabId = 'overview' | 'ui' | 'data' | 'resources';
export const AdminUserAccessOverviewPage: React.FC = () => { export const AdminUserAccessOverviewPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -588,22 +586,22 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{ {
id: 'overview', id: 'overview',
label: t('Übersicht'), label: t('Übersicht'),
render: () => <Panel variant="card">{renderOverviewTab()}</Panel>, render: () => <Panel variant="card" title={t('Übersicht')} id="access-tab-overview">{renderOverviewTab()}</Panel>,
}, },
{ {
id: 'ui', id: 'ui',
label: `${t('UI-Zugriff')} (${overview.uiAccess.length})`, label: `${t('UI-Zugriff')} (${overview.uiAccess.length})`,
render: () => <Panel variant="card">{renderUiAccessTab()}</Panel>, render: () => <Panel variant="card" title={t('UI-Zugriff')} id="access-tab-ui">{renderUiAccessTab()}</Panel>,
}, },
{ {
id: 'data', id: 'data',
label: `${t('Daten-Zugriff')} (${overview.dataAccess.length})`, label: `${t('Daten-Zugriff')} (${overview.dataAccess.length})`,
render: () => <Panel variant="card">{renderDataAccessTab()}</Panel>, render: () => <Panel variant="card" title={t('Daten-Zugriff')} id="access-tab-data">{renderDataAccessTab()}</Panel>,
}, },
{ {
id: 'resources', id: 'resources',
label: `${t('Ressourcen')} (${overview.resourceAccess.length})`, label: `${t('Ressourcen')} (${overview.resourceAccess.length})`,
render: () => <Panel variant="card">{renderResourceAccessTab()}</Panel>, render: () => <Panel variant="card" title={t('Ressourcen')} id="access-tab-resources">{renderResourceAccessTab()}</Panel>,
}, },
]; ];
}, [overview, t, expandedRoles, expandedMandates]); }, [overview, t, expandedRoles, expandedMandates]);
@ -612,7 +610,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="access-overview-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -640,7 +638,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="access-overview-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -678,7 +676,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
</Panel> </Panel>
{!selectedUserId ? ( {!selectedUserId ? (
<Panel variant="card"> <Panel variant="card" title={t('Benutzer auswählen')} id="access-overview-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} /> <FaUserShield className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Benutzer auswählen')}</h3> <h3 className={styles.emptyTitle}>{t('Benutzer auswählen')}</h3>
@ -688,7 +686,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : loading ? ( ) : loading ? (
<Panel variant="card"> <Panel variant="card" title={t('Lade Zugriffsübersicht')} id="access-overview-loading">
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <div className={styles.spinner} />
<span>{t('Lade Zugriffsübersicht')}</span> <span>{t('Lade Zugriffsübersicht')}</span>
@ -696,7 +694,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
</Panel> </Panel>
) : overview ? ( ) : overview ? (
<> <>
<Panel variant="card"> <Panel variant="card" title={t('Benutzerinfo')} id="access-overview-user">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<strong>{overview.user.fullName || overview.user.username}</strong> <strong>{overview.user.fullName || overview.user.username}</strong>
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span> <span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>

View file

@ -259,7 +259,7 @@ export const AdminUserMandatesPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="user-mandates-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -285,7 +285,7 @@ export const AdminUserMandatesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="user-mandates-toolbar">
<div className={styles.filterSection}> <div className={styles.filterSection}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
@ -328,7 +328,7 @@ export const AdminUserMandatesPage: React.FC = () => {
</Panel> </Panel>
{!selectedMandateId ? ( {!selectedMandateId ? (
<Panel variant="card"> <Panel variant="card" title={t('Kein Mandant ausgewählt')} id="user-mandates-empty">
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
@ -338,7 +338,7 @@ export const AdminUserMandatesPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="table"> <Panel variant="table" title={t('Mandanten-Mitglieder')} id="user-mandates-table">
<FormGeneratorTable <FormGeneratorTable
data={users} data={users}
columns={columns} columns={columns}

View file

@ -169,7 +169,7 @@ export const AdminUsersPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="users-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -195,7 +195,7 @@ export const AdminUsersPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="users-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
type="button" type="button"
@ -228,7 +228,7 @@ export const AdminUsersPage: React.FC = () => {
)} )}
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Benutzer')} id="users-table">
<FormGeneratorTable <FormGeneratorTable
data={users} data={users}
columns={columns} columns={columns}

View file

@ -190,7 +190,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
if (loading) { if (loading) {
return ( return (
<Panel variant="card" title={t('Hierarchie')}> <Panel variant="card" title={t('Hierarchie')} id="hierarchy-loading">
<div className={hierarchyStyles.hierarchyLoading}> <div className={hierarchyStyles.hierarchyLoading}>
<span className={hierarchyStyles.spinner} /> <span className={hierarchyStyles.spinner} />
<span>{t('Lade Hierarchie und Benutzer')}</span> <span>{t('Lade Hierarchie und Benutzer')}</span>
@ -201,7 +201,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
if (mandates.length === 0) { if (mandates.length === 0) {
return ( return (
<Panel variant="card" title={t('Hierarchie')}> <Panel variant="card" title={t('Hierarchie')} id="hierarchy-empty">
<div className={hierarchyStyles.emptyHierarchy}> <div className={hierarchyStyles.emptyHierarchy}>
{t('Keine Mandanten vorhanden. Legen Sie unter „Mandanten verwalten“ einen Mandanten an.')} {t('Keine Mandanten vorhanden. Legen Sie unter „Mandanten verwalten“ einen Mandanten an.')}
</div> </div>
@ -210,7 +210,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
} }
return ( return (
<Panel variant="card" title={t('Hierarchie')}> <Panel variant="card" title={t('Hierarchie')} id="hierarchy-tree">
<div className={hierarchyStyles.hierarchyRoot}> <div className={hierarchyStyles.hierarchyRoot}>
{mandates.map((mandate) => { {mandates.map((mandate) => {
const mandateId = mandate.id; const mandateId = mandate.id;

View file

@ -52,7 +52,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
if (roles.length === 0) { if (roles.length === 0) {
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Keine Rollen in dieser Instanz')} id="matrix-empty">
<div className={matrixStyles.empty}> <div className={matrixStyles.empty}>
<p>{t('Keine Rollen in dieser Instanz')}</p> <p>{t('Keine Rollen in dieser Instanz')}</p>
</div> </div>
@ -62,7 +62,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
return ( return (
<> <>
<Panel variant="table"> <Panel variant="table" title={t('Berechtigungsmatrix')} id="matrix-table">
<div className={matrixStyles.tableWrap}> <div className={matrixStyles.tableWrap}>
<table className={matrixStyles.table}> <table className={matrixStyles.table}>
<thead> <thead>
@ -140,7 +140,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
</table> </table>
</div> </div>
</Panel> </Panel>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="matrix-toolbar">
<div className={matrixStyles.footer}> <div className={matrixStyles.footer}>
<button <button
type="button" type="button"

View file

@ -21,3 +21,4 @@ export { AdminLogsPage } from './AdminLogsPage';
export { AdminLanguagesPage } from './AdminLanguagesPage'; export { AdminLanguagesPage } from './AdminLanguagesPage';
export { AdminDemoConfigPage } from './AdminDemoConfigPage'; export { AdminDemoConfigPage } from './AdminDemoConfigPage';
export { AdminDatabaseHealthPage } from './AdminDatabaseHealthPage'; export { AdminDatabaseHealthPage } from './AdminDatabaseHealthPage';
export { AdminSessionsPage } from './AdminSessionsPage';

View file

@ -331,7 +331,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="invitation-error">
<div className={styles.errorContainer} style={{ <div className={styles.errorContainer} style={{
flexDirection: 'row', padding: '12px 16px', marginBottom: '16px', flexDirection: 'row', padding: '12px 16px', marginBottom: '16px',
background: '#fef2f2', borderRadius: '8px', fontSize: '13px', justifyContent: 'flex-start', background: '#fef2f2', borderRadius: '8px', fontSize: '13px', justifyContent: 'flex-start',
@ -343,7 +343,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
)} )}
{!dispatchResults && ( {!dispatchResults && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Schritte')} id="invitation-steps-toolbar">
<div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
{stepLabels.map((label, idx) => { {stepLabels.map((label, idx) => {
const s = idx + 1; const s = idx + 1;
@ -369,7 +369,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="wizard"> <Panel variant="wizard" title={t('Einladungs-Wizard')} id="invitation-wizard">
{/* ── STEP 1: Invite type ── */} {/* ── STEP 1: Invite type ── */}
{step === 1 && ( {step === 1 && (
<div style={_cardStyle}> <div style={_cardStyle}>

View file

@ -685,7 +685,7 @@ export const AdminMandateWizardPage: React.FC = () => {
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
{error && ( {error && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="mandate-error">
<div style={{ <div style={{
padding: '12px 16px', background: 'var(--error-bg, #fef2f2)', color: 'var(--danger-color, #dc2626)', padding: '12px 16px', background: 'var(--error-bg, #fef2f2)', color: 'var(--danger-color, #dc2626)',
borderRadius: '8px', fontSize: '13px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderRadius: '8px', fontSize: '13px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
@ -697,11 +697,11 @@ export const AdminMandateWizardPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Schritte')} id="mandate-steps-toolbar">
{renderStepIndicator()} {renderStepIndicator()}
</Panel> </Panel>
<Panel variant="wizard"> <Panel variant="wizard" title={t('Mandanten-Verwaltung')} id="mandate-wizard">
{step === 1 && ( {step === 1 && (
<div style={cardStyle}> <div style={cardStyle}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('Mandant auswählen oder erstellen')}</h3> <h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('Mandant auswählen oder erstellen')}</h3>

View file

@ -159,7 +159,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
</button> </button>
</div> </div>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Schritte')} id="feature-instance-steps-toolbar">
<div className={wizardStyles.steps}> <div className={wizardStyles.steps}>
{steps.map((s, i) => ( {steps.map((s, i) => (
<div <div
@ -174,7 +174,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
</Panel> </Panel>
<div className={styles.modalContent}> <div className={styles.modalContent}>
<Panel variant="wizard"> <Panel variant="wizard" title={t('Neue Feature-Instanz')} id="feature-instance-wizard">
{currentStepId === 'create' && ( {currentStepId === 'create' && (
<div className={wizardStyles.stepContent}> <div className={wizardStyles.stepContent}>
<div className={wizardStyles.fieldGroup}> <div className={wizardStyles.fieldGroup}>

View file

@ -298,7 +298,7 @@ export const ConnectionsPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="connections-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}</p> <p className={styles.errorMessage}>{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}</p>
@ -324,7 +324,7 @@ export const ConnectionsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="connections-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
className={styles.secondaryButton} className={styles.secondaryButton}
@ -349,6 +349,7 @@ export const ConnectionsPage: React.FC = () => {
{syncBanner && ( {syncBanner && (
<Panel <Panel
variant="card" variant="card"
id="connections-sync-banner"
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
collapseKey="connections-sync-banner" collapseKey="connections-sync-banner"
@ -376,7 +377,7 @@ export const ConnectionsPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="table"> <Panel variant="table" title={t('Verbindungen')} id="connections-table">
<FormGeneratorTable <FormGeneratorTable
data={connections} data={connections}
columns={columns} columns={columns}

View file

@ -456,7 +456,7 @@ export const FilesPage: React.FC = () => {
); );
const _treePanelContent = ( const _treePanelContent = (
<Panel variant="card" fill> <Panel variant="card" title={t('Ordnerstruktur')} id="files-tree" fill>
<FormGeneratorTree <FormGeneratorTree
key={`own-${treeKey}`} key={`own-${treeKey}`}
provider={provider} provider={provider}
@ -481,7 +481,7 @@ export const FilesPage: React.FC = () => {
); );
const _tablePanelContent = ( const _tablePanelContent = (
<Panel variant="table" actions={_tablePanelActions}> <Panel variant="table" title={t('Dateien')} id="files-table" actions={_tablePanelActions}>
<FormGeneratorTable <FormGeneratorTable
data={tableFiles || []} data={tableFiles || []}
columns={columns} columns={columns}
@ -586,7 +586,7 @@ export const FilesPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="files-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}>&#9888;&#65039;</span> <span className={styles.errorIcon}>&#9888;&#65039;</span>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}</p> <p className={styles.errorMessage}>{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}</p>
@ -617,7 +617,7 @@ export const FilesPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="files-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<div className={fileStyles.toggleGroup}> <div className={fileStyles.toggleGroup}>
<button <button

View file

@ -170,7 +170,7 @@ export const PromptsPage: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="prompts-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Prompts: {detail}', { detail: String(error) })}</p> <p className={styles.errorMessage}>{t('Fehler beim Laden der Prompts: {detail}', { detail: String(error) })}</p>
@ -194,7 +194,7 @@ export const PromptsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="prompts-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
className={styles.secondaryButton} className={styles.secondaryButton}
@ -214,7 +214,7 @@ export const PromptsPage: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Prompts')} id="prompts-table">
<FormGeneratorTable <FormGeneratorTable
data={prompts} data={prompts}
columns={columns} columns={columns}

View file

@ -201,7 +201,7 @@ const AdminSubscriptionsPage: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="admin-subscriptions-toolbar">
<button <button
type="button" type="button"
className={`${styles.button} ${styles.buttonPrimary}`} className={`${styles.button} ${styles.buttonPrimary}`}
@ -211,7 +211,7 @@ const AdminSubscriptionsPage: React.FC = () => {
</button> </button>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Abonnemente')} id="admin-subscriptions-table">
<FormGeneratorTable <FormGeneratorTable
data={subscriptions} data={subscriptions}
columns={columns} columns={columns}

View file

@ -643,7 +643,7 @@ export const BillingAdmin: React.FC = () => {
id: 'settings', id: 'settings',
label: t('Einstellungen'), label: t('Einstellungen'),
render: () => ( render: () => (
<Panel variant="card"> <Panel variant="card" title={t('Einstellungen')} id="billing-settings">
<SettingsEditor <SettingsEditor
settings={settings} settings={settings}
onSave={handleSaveSettings} onSave={handleSaveSettings}
@ -658,16 +658,16 @@ export const BillingAdmin: React.FC = () => {
render: () => ( render: () => (
<> <>
{isSysAdmin && ( {isSysAdmin && (
<Panel variant="card"> <Panel variant="card" title={t('Guthaben aufladen')} id="billing-credit-adder">
<CreditAdder onAddCredit={_handleAddCredit} /> <CreditAdder onAddCredit={_handleAddCredit} />
</Panel> </Panel>
)} )}
{showStripeForMandateAdmin && ( {showStripeForMandateAdmin && (
<Panel variant="card"> <Panel variant="card" title={t('Stripe-Aufladung')} id="billing-stripe-topup">
<MandateStripeTopUp mandateId={selectedMandateId} createCheckout={createCheckout} /> <MandateStripeTopUp mandateId={selectedMandateId} createCheckout={createCheckout} />
</Panel> </Panel>
)} )}
<Panel variant="card"> <Panel variant="card" title={t('Kontenübersicht')} id="billing-accounts-overview">
<AccountsOverview accounts={accounts} users={users} loading={loading} /> <AccountsOverview accounts={accounts} users={users} loading={loading} />
</Panel> </Panel>
</> </>
@ -701,7 +701,7 @@ export const BillingAdmin: React.FC = () => {
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
{stripeReturnMessage && ( {stripeReturnMessage && (
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="billing-stripe-message">
<div <div
className={ className={
stripeReturnMessage.type === 'success' ? styles.successMessage : styles.errorMessage stripeReturnMessage.type === 'success' ? styles.successMessage : styles.errorMessage
@ -717,7 +717,7 @@ export const BillingAdmin: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Mandant')} id="billing-mandate-toolbar">
<MandateSelector <MandateSelector
mandates={mandateList} mandates={mandateList}
loading={mandatesLoading} loading={mandatesLoading}
@ -738,7 +738,7 @@ export const BillingAdmin: React.FC = () => {
}} }}
/> />
) : ( ) : (
<Panel variant="card"> <Panel variant="card" title={t('Mandant')} id="billing-mandate-empty">
<div className={styles.noData}>{t('Bitte wählen Sie einen Mandanten aus.')}</div> <div className={styles.noData}>{t('Bitte wählen Sie einen Mandanten aus.')}</div>
</Panel> </Panel>
)} )}

View file

@ -672,7 +672,7 @@ export const BillingDataView: React.FC = () => {
id: 'overview', id: 'overview',
label: t('Übersicht'), label: t('Übersicht'),
render: () => ( render: () => (
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Übersicht')} id="billing-stats-overview">
<FormGeneratorReport <FormGeneratorReport
loading={statsLoading || dashboardLoading} loading={statsLoading || dashboardLoading}
sections={overviewSections} sections={overviewSections}
@ -686,7 +686,7 @@ export const BillingDataView: React.FC = () => {
id: 'diagrams', id: 'diagrams',
label: t('Diagramme'), label: t('Diagramme'),
render: () => ( render: () => (
<Panel variant="card"> <Panel variant="card" title={t('Diagramme')} id="billing-stats-diagrams">
<div className={styles.diagramControlsEnd}> <div className={styles.diagramControlsEnd}>
<div className={styles.chartModeToggle}> <div className={styles.chartModeToggle}>
<button <button
@ -736,7 +736,7 @@ export const BillingDataView: React.FC = () => {
id: 'transactions', id: 'transactions',
label: t('Transaktionen'), label: t('Transaktionen'),
render: () => ( render: () => (
<Panel variant="table"> <Panel variant="table" title={t('Transaktionen')} id="billing-stats-transactions-table">
{transactionsError && ( {transactionsError && (
<div className={styles.errorMessage}> <div className={styles.errorMessage}>
{transactionsError} {transactionsError}
@ -793,7 +793,7 @@ export const BillingDataView: React.FC = () => {
</div> </div>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="billing-stats-toolbar">
<div className={styles.contextToolbar}> <div className={styles.contextToolbar}>
<label className={styles.contextToolbarLabel}>{t('Kontext:')}</label> <label className={styles.contextToolbarLabel}>{t('Kontext:')}</label>
<select <select
@ -817,7 +817,7 @@ export const BillingDataView: React.FC = () => {
</Panel> </Panel>
{checkoutMessage && ( {checkoutMessage && (
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="billing-stats-checkout-message">
<div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}> <div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}>
{checkoutMessage.text} {checkoutMessage.text}
{(successParam || canceledParam) && ( {(successParam || canceledParam) && (

View file

@ -228,6 +228,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
<> <>
<Panel <Panel
variant="card" variant="card"
id="billing-mandate-balances"
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
collapseKey="billing-mandate-balances" collapseKey="billing-mandate-balances"
@ -248,6 +249,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
<Panel <Panel
variant="card" variant="card"
id="billing-mandate-transactions"
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
collapseKey="billing-mandate-transactions" collapseKey="billing-mandate-transactions"

View file

@ -16,7 +16,7 @@ export const BillingNav: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return ( return (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Navigation')} id="billing-nav-toolbar">
<nav className={styles.billingNav} aria-label={t('Billing-Navigation')}> <nav className={styles.billingNav} aria-label={t('Billing-Navigation')}>
<NavLink <NavLink
to="/billing" to="/billing"

View file

@ -590,7 +590,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{checkoutMessage && ( {checkoutMessage && (
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="subscription-checkout-message">
<div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}> <div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}>
{checkoutMessage.text} {checkoutMessage.text}
</div> </div>
@ -598,14 +598,14 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
)} )}
{(error || actionError) && ( {(error || actionError) && (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="subscription-error">
<div className={styles.errorMessage}> <div className={styles.errorMessage}>
{actionError || error} {actionError || error}
</div> </div>
</Panel> </Panel>
)} )}
<Panel variant="card" title={t('Aktuelles Abonnement')}> <Panel variant="card" title={t('Aktuelles Abonnement')} id="subscription-current">
{subscription ? ( {subscription ? (
<_SubInfoCard <_SubInfoCard
sub={subscription} sub={subscription}
@ -628,7 +628,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
</Panel> </Panel>
{scheduled && ( {scheduled && (
<Panel variant="card" title={t('Geplanter Nachfolgeplan')}> <Panel variant="card" title={t('Geplanter Nachfolgeplan')} id="subscription-scheduled">
<_SubInfoCard <_SubInfoCard
sub={scheduled} sub={scheduled}
plan={null} plan={null}
@ -642,7 +642,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
)} )}
{!_isEnterpriseSub(subscription) && ( {!_isEnterpriseSub(subscription) && (
<Panel variant="dashboard" title={t('Verfügbare Pläne')}> <Panel variant="dashboard" title={t('Verfügbare Pläne')} id="subscription-plans">
{plans.length === 0 ? ( {plans.length === 0 ? (
<div className={styles.noData}>{t('Keine Pläne verfügbar')}</div> <div className={styles.noData}>{t('Keine Pläne verfügbar')}</div>
) : ( ) : (

View file

@ -78,7 +78,7 @@ export const CommcoachAssistantView: React.FC = () => {
return ( return (
<StackLayout variant="form"> <StackLayout variant="form">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="commcoach-assistant-toolbar">
<div className={styles.wizardHeader}> <div className={styles.wizardHeader}>
<h2>{t('Neues Modul erstellen')}</h2> <h2>{t('Neues Modul erstellen')}</h2>
<div className={styles.wizardHeaderRight}> <div className={styles.wizardHeaderRight}>
@ -105,7 +105,7 @@ export const CommcoachAssistantView: React.FC = () => {
{error && <div className={styles.errorBanner}>{error}</div>} {error && <div className={styles.errorBanner}>{error}</div>}
<Panel variant="wizard"> <Panel variant="wizard" title={t('Neues Modul erstellen')} id="commcoach-assistant-wizard">
<div className={styles.wizardContent}> <div className={styles.wizardContent}>
{step === 'type' && ( {step === 'type' && (
<div className={styles.wizardStep}> <div className={styles.wizardStep}>

View file

@ -56,7 +56,7 @@ export const CommcoachDashboardView: React.FC = () => {
return ( return (
<StackLayout variant="dashboard"> <StackLayout variant="dashboard">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="commcoach-dashboard-loading">
<div className={styles.loading}>{t('Dashboard wird geladen…')}</div> <div className={styles.loading}>{t('Dashboard wird geladen…')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -68,7 +68,7 @@ export const CommcoachDashboardView: React.FC = () => {
return ( return (
<StackLayout variant="dashboard"> <StackLayout variant="dashboard">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="commcoach-dashboard-error">
<div className={styles.error}>{error}</div> <div className={styles.error}>{error}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -80,7 +80,7 @@ export const CommcoachDashboardView: React.FC = () => {
return ( return (
<StackLayout variant="dashboard"> <StackLayout variant="dashboard">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Keine Daten')} id="commcoach-dashboard-empty">
<div className={styles.empty}>{t('Keine Daten verfügbar')}</div> <div className={styles.empty}>{t('Keine Daten verfügbar')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -91,7 +91,7 @@ export const CommcoachDashboardView: React.FC = () => {
return ( return (
<StackLayout variant="dashboard"> <StackLayout variant="dashboard">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Übersicht')} id="commcoach-dashboard-kpis">
<div className={styles.kpiGrid}> <div className={styles.kpiGrid}>
<div className={styles.kpiCard}> <div className={styles.kpiCard}>
<div className={styles.kpiValue}>{dashboard.streakDays}</div> <div className={styles.kpiValue}>{dashboard.streakDays}</div>
@ -125,6 +125,7 @@ export const CommcoachDashboardView: React.FC = () => {
<Panel <Panel
variant="card" variant="card"
title={t('Aktive Coaching-Themen')} title={t('Aktive Coaching-Themen')}
id="commcoach-dashboard-active-topics"
collapsible collapsible
collapseKey="commcoach-dashboard-active-topics" collapseKey="commcoach-dashboard-active-topics"
actions={ actions={
@ -181,6 +182,7 @@ export const CommcoachDashboardView: React.FC = () => {
}) })
: t('Auszeichnungen') : t('Auszeichnungen')
} }
id="commcoach-dashboard-badges"
collapsible collapsible
collapseKey="commcoach-dashboard-badges" collapseKey="commcoach-dashboard-badges"
> >
@ -200,6 +202,7 @@ export const CommcoachDashboardView: React.FC = () => {
<Panel <Panel
variant="card" variant="card"
title={t('Tipp des Tages')} title={t('Tipp des Tages')}
id="commcoach-dashboard-tip"
collapsible collapsible
collapseKey="commcoach-dashboard-tip" collapseKey="commcoach-dashboard-tip"
> >

View file

@ -154,7 +154,7 @@ export const CommcoachModulesView: React.FC = () => {
<> <>
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="commcoach-modules-toolbar">
<div className={styles.modulesHeader}> <div className={styles.modulesHeader}>
<h2>{t('Module')}</h2> <h2>{t('Module')}</h2>
<div className={styles.modulesFilters}> <div className={styles.modulesFilters}>
@ -181,12 +181,12 @@ export const CommcoachModulesView: React.FC = () => {
</Panel> </Panel>
{loading && ( {loading && (
<Panel variant="card"> <Panel variant="card" title={t('Laden...')} id="commcoach-modules-loading">
<div className={styles.loading}>{t('Laden...')}</div> <div className={styles.loading}>{t('Laden...')}</div>
</Panel> </Panel>
)} )}
<Panel variant="card"> <Panel variant="card" title={t('Module')} id="commcoach-modules-list">
<div className={styles.modulesList}> <div className={styles.modulesList}>
{filteredModules.map(mod => ( {filteredModules.map(mod => (
<div <div

View file

@ -281,7 +281,7 @@ export const CommcoachSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden...')} id="commcoach-session-loading">
<div className={sessionStyles.loading}>{t('Laden...')}</div> <div className={sessionStyles.loading}>{t('Laden...')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -294,7 +294,7 @@ export const CommcoachSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Keine aktive Session')} id="commcoach-session-empty">
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '2rem' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '2rem' }}>
<h3>{t('Keine aktive Session')}</h3> <h3>{t('Keine aktive Session')}</h3>
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}> <p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}>
@ -320,7 +320,7 @@ export const CommcoachSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Session starten')} id="commcoach-session-start">
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
<h3>{coach.selectedContext?.title || t('Modul')}</h3> <h3>{coach.selectedContext?.title || t('Modul')}</h3>
{coach.selectedContext?.description && ( {coach.selectedContext?.description && (
@ -394,7 +394,7 @@ export const CommcoachSessionView: React.FC = () => {
collapsible: true, collapsible: true,
collapseKey: 'commcoach-session-udb', collapseKey: 'commcoach-session-udb',
content: ( content: (
<Panel variant="card" title={t('Datenquellen')}> <Panel variant="card" title={t('Datenquellen')} id="commcoach-session-data-sources">
<UnifiedDataBar <UnifiedDataBar
context={_udbContext} context={_udbContext}
activeTab={udbTab} activeTab={udbTab}
@ -411,7 +411,7 @@ export const CommcoachSessionView: React.FC = () => {
defaultSize: 78, defaultSize: 78,
minSize: 40, minSize: 40,
content: ( content: (
<Panel variant="editor"> <Panel variant="editor" title={t('Coaching-Session')} id="commcoach-session-editor">
<div className={styles.sessionRoot}> <div className={styles.sessionRoot}>
{/* Session Header */} {/* Session Header */}
<div className={styles.sessionHeader}> <div className={styles.sessionHeader}>

View file

@ -227,7 +227,7 @@ export const CommcoachSettingsView: React.FC = () => {
render: () => ( render: () => (
<> <>
{loading && ( {loading && (
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="commcoach-settings-loading">
<div className={styles.loading}>{t('Einstellungen werden geladen')}</div> <div className={styles.loading}>{t('Einstellungen werden geladen')}</div>
</Panel> </Panel>
)} )}
@ -236,7 +236,7 @@ export const CommcoachSettingsView: React.FC = () => {
{error && <div className={styles.error}>{error}</div>} {error && <div className={styles.error}>{error}</div>}
{success && <div className={styles.success}>{success}</div>} {success && <div className={styles.success}>{success}</div>}
<Panel variant="card" title={t('Stimme/Sprache')}> <Panel variant="card" title={t('Stimme/Sprache')} id="commcoach-settings-voice">
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', margin: '0 0 0.5rem' }}> <p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', margin: '0 0 0.5rem' }}>
{t('Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.')} {t('Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.')}
</p> </p>
@ -245,7 +245,7 @@ export const CommcoachSettingsView: React.FC = () => {
</Link> </Link>
</Panel> </Panel>
<Panel variant="card" title={t('Erinnerungen')}> <Panel variant="card" title={t('Erinnerungen')} id="commcoach-settings-reminders">
<div className={styles.field}> <div className={styles.field}>
<label className={styles.checkboxLabel}> <label className={styles.checkboxLabel}>
<input type="checkbox" checked={reminderEnabled} onChange={e => setReminderEnabled(e.target.checked)} /> <input type="checkbox" checked={reminderEnabled} onChange={e => setReminderEnabled(e.target.checked)} />
@ -280,7 +280,7 @@ export const CommcoachSettingsView: React.FC = () => {
<> <>
{personaError && <div className={styles.error}>{personaError}</div>} {personaError && <div className={styles.error}>{personaError}</div>}
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="commcoach-settings-personas-toolbar">
<div className={styles.personasHeader}> <div className={styles.personasHeader}>
<div> <div>
<h3 className={styles.sectionTitle} style={{ margin: 0 }}>{t('Gespraechspartner verwalten')}</h3> <h3 className={styles.sectionTitle} style={{ margin: 0 }}>{t('Gespraechspartner verwalten')}</h3>
@ -297,7 +297,7 @@ export const CommcoachSettingsView: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Gespraechspartner')} id="commcoach-settings-personas-table">
<FormGeneratorTable <FormGeneratorTable
data={personas} data={personas}
columns={personaColumns} columns={personaColumns}

View file

@ -129,14 +129,14 @@ const ConfigTab: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="neutralization-config-loading">
<div className={styles.loading}>{t('Konfiguration wird geladen')}</div> <div className={styles.loading}>{t('Konfiguration wird geladen')}</div>
</Panel> </Panel>
); );
} }
return ( return (
<Panel variant="card" title={t('Neutralisierungskonfiguration')} subtitle={t('Datenneutralisierung für diese Instanz konfigurieren.')}> <Panel variant="card" title={t('Neutralisierungskonfiguration')} id="neutralization-config" subtitle={t('Datenneutralisierung für diese Instanz konfigurieren.')}>
{error && ( {error && (
<div className={styles.errorMessage}> <div className={styles.errorMessage}>
<span>{error}</span> <span>{error}</span>
@ -485,7 +485,7 @@ const PlaygroundTab: React.FC = () => {
return ( return (
<> <>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="card" title={t('Datei-Upload')}> <Panel variant="card" title={t('Datei-Upload')} id="neutralization-playground-file-upload">
<p className={styles.sectionDescription} style={{ marginTop: 0 }}> <p className={styles.sectionDescription} style={{ marginTop: 0 }}>
{t('Laden Sie eine Datei hoch, um sie zu neutralisieren.')} {t('Laden Sie eine Datei hoch, um sie zu neutralisieren.')}
</p> </p>
@ -525,7 +525,7 @@ const PlaygroundTab: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="card" title={t('Text neutralisieren / auflösen')}> <Panel variant="card" title={t('Text neutralisieren / auflösen')} id="neutralization-playground-text">
<div className={styles.formRow}> <div className={styles.formRow}>
<label htmlFor="input-text">{t('2. Oder Text einfügen')}</label> <label htmlFor="input-text">{t('2. Oder Text einfügen')}</label>
<textarea <textarea
@ -581,7 +581,7 @@ const PlaygroundTab: React.FC = () => {
)} )}
</Panel> </Panel>
<Panel variant="card" title={t('SharePoint-Dateien')}> <Panel variant="card" title={t('SharePoint-Dateien')} id="neutralization-playground-sharepoint">
<div className={styles.formRow}> <div className={styles.formRow}>
<label>{t('3. Oder SharePoint-Dateien neutralisieren')}</label> <label>{t('3. Oder SharePoint-Dateien neutralisieren')}</label>
<div className={styles.formRow} style={{ gap: '0.75rem', marginTop: 0 }}> <div className={styles.formRow} style={{ gap: '0.75rem', marginTop: 0 }}>
@ -753,8 +753,8 @@ export const NeutralizationView: React.FC = () => {
const tabs = useMemo( const tabs = useMemo(
() => [ () => [
{ id: 'config', label: t('Konfiguration'), content: <ConfigTab /> }, { id: 'config', label: t('Konfiguration'), render: () => <ConfigTab /> },
{ id: 'playground', label: t('Spielwiese'), content: <PlaygroundTab /> }, { id: 'playground', label: t('Spielwiese'), render: () => <PlaygroundTab /> },
], ],
[t], [t],
); );

View file

@ -24,7 +24,7 @@ export const RealEstateInstanceRolesPlaceholder: React.FC = () => {
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Rollen & Rechte')}</h1> <h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Rollen & Rechte')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Rollen & Rechte')} id="realestate-roles-placeholder">
<p style={{ margin: '0 0 1rem', color: 'var(--text-secondary, #666)', fontSize: '0.9375rem', lineHeight: 1.5 }}> <p style={{ margin: '0 0 1rem', color: 'var(--text-secondary, #666)', fontSize: '0.9375rem', lineHeight: 1.5 }}>
{t('Die Verwaltung von Rollen und Benutzern für diese Instanz erfolgt in der Administration.')} {t('Die Verwaltung von Rollen und Benutzern für diese Instanz erfolgt in der Administration.')}
</p> </p>

View file

@ -12,18 +12,21 @@ import { PekProvider } from '../../../contexts/PekContext';
import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { StackLayout } from '../../../components/Layout/StackLayout'; import { StackLayout } from '../../../components/Layout/StackLayout';
import { Panel } from '../../../components/Layout/Panel'; import { Panel } from '../../../components/Layout/Panel';
import { useLanguage } from '../../../providers/language/LanguageContext';
import PekLocationInput from './pek/PekLocationInput'; import PekLocationInput from './pek/PekLocationInput';
import PekMapView from './pek/PekMapView'; import PekMapView from './pek/PekMapView';
function RealEstatePekViewContent() { function RealEstatePekViewContent() {
const { t } = useLanguage();
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', flex: 1, minHeight: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', flex: 1, minHeight: 0 }}>
<Panel variant="card"> <Panel variant="card" title={t('Standortsuche')} id="realestate-pek-location">
<PekLocationInput /> <PekLocationInput />
</Panel> </Panel>
<Panel variant="editor"> <Panel variant="editor" title={t('Karte')} id="realestate-pek-map">
<PekMapView /> <PekMapView />
</Panel> </Panel>
</div> </div>

View file

@ -34,7 +34,7 @@ const PekLocationInput: React.FC = () => {
}; };
return ( return (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Standortsuche')} id="pek-location-toolbar">
<div className={styles.locationInputContainer}> <div className={styles.locationInputContainer}>
<div className={styles.fieldsRow}> <div className={styles.fieldsRow}>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>

View file

@ -28,7 +28,7 @@ const PekMapView: React.FC = () => {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, height: '100%' }}>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Kartenoptionen')} id="pek-map-toolbar">
<div className={styles.checkboxRow}> <div className={styles.checkboxRow}>
<label className={styles.checkboxLabel}> <label className={styles.checkboxLabel}>
<input <input
@ -49,7 +49,7 @@ const PekMapView: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="editor"> <Panel variant="editor" title={t('Karte')} id="pek-map-editor">
<div className={styles.mapContainer}> <div className={styles.mapContainer}>
<MapView <MapView
parcels={parcelGeometries} parcels={parcelGeometries}

View file

@ -330,7 +330,7 @@ export const RedmineBrowserView: React.FC = () => {
}, []); }, []);
const _treePane = ( const _treePane = (
<Panel variant="table"> <Panel variant="table" title={t('Tickets')} id="redmine-browser-tickets-table">
<div className={styles.browserToolbar}> <div className={styles.browserToolbar}>
<span> <span>
{t('{rows} Zeilen sichtbar -- {orphans} Orphan-Tickets', { {t('{rows} Zeilen sichtbar -- {orphans} Orphan-Tickets', {
@ -384,11 +384,11 @@ export const RedmineBrowserView: React.FC = () => {
); );
const _editorPane = selectedId == null ? ( const _editorPane = selectedId == null ? (
<Panel variant="editor"> <Panel variant="editor" title={t('Ticket-Editor')} id="redmine-browser-editor-empty">
<div className={styles.browserToolbar}>{t('Ticket links auswaehlen')}</div> <div className={styles.browserToolbar}>{t('Ticket links auswaehlen')}</div>
</Panel> </Panel>
) : selectedId === ORPHAN_ROOT_ID ? ( ) : selectedId === ORPHAN_ROOT_ID ? (
<Panel variant="editor"> <Panel variant="editor" title={t('Orphan User Story')} id="redmine-browser-editor-orphan">
<div className={styles.browserToolbar}> <div className={styles.browserToolbar}>
{t('Virtueller "Orphan User Story"-Knoten -- enthaelt {count} Tickets ohne Verbindung.', { {t('Virtueller "Orphan User Story"-Knoten -- enthaelt {count} Tickets ohne Verbindung.', {
count: forest.orphanCount, count: forest.orphanCount,
@ -396,7 +396,7 @@ export const RedmineBrowserView: React.FC = () => {
</div> </div>
</Panel> </Panel>
) : ( ) : (
<Panel variant="editor"> <Panel variant="editor" title={t('Ticket-Editor')} id="redmine-browser-editor">
<RedmineTicketEditor <RedmineTicketEditor
instanceId={instanceId!} instanceId={instanceId!}
ticketId={selectedId} ticketId={selectedId}
@ -416,7 +416,7 @@ export const RedmineBrowserView: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="redmine-browser-no-instance">
<div className={styles.placeholder}>{t('Keine Feature-Instanz ausgewaehlt')}</div> <div className={styles.placeholder}>{t('Keine Feature-Instanz ausgewaehlt')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -428,7 +428,7 @@ export const RedmineBrowserView: React.FC = () => {
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, gap: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, gap: 0 }}>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Ticket-Browser')} id="redmine-browser-header-toolbar">
<div className={styles.browserHeader}> <div className={styles.browserHeader}>
<div> <div>
<h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Ticket-Browser')}</h2> <h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Ticket-Browser')}</h2>
@ -448,12 +448,12 @@ export const RedmineBrowserView: React.FC = () => {
</Panel> </Panel>
{error && ( {error && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Fehler')} id="redmine-browser-error-toolbar">
<div className={styles.alertErr} style={{ marginBottom: 0, width: '100%' }}>{error}</div> <div className={styles.alertErr} style={{ marginBottom: 0, width: '100%' }}>{error}</div>
</Panel> </Panel>
)} )}
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="redmine-browser-filters-toolbar">
<div className={styles.browserFilters} style={{ padding: 0, border: 'none', background: 'transparent' }}> <div className={styles.browserFilters} style={{ padding: 0, border: 'none', background: 'transparent' }}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label>{t('Zeitraum (letzte Aenderung)')}</label> <label>{t('Zeitraum (letzte Aenderung)')}</label>

View file

@ -198,7 +198,7 @@ export const RedmineSettingsView: React.FC = () => {
<h1 style={{ fontSize: '1.35rem', fontWeight: 600, margin: 0 }}>{t('Redmine -- Einstellungen')}</h1> <h1 style={{ fontSize: '1.35rem', fontWeight: 600, margin: 0 }}>{t('Redmine -- Einstellungen')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="redmine-settings-loading">
<div className={styles.loading}>{t('Einstellungen werden geladen ...')}</div> <div className={styles.loading}>{t('Einstellungen werden geladen ...')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -222,6 +222,7 @@ export const RedmineSettingsView: React.FC = () => {
<Panel <Panel
variant="card" variant="card"
title={t('Verbindung')} title={t('Verbindung')}
id="redmine-settings-connection"
collapsible collapsible
collapseKey={instanceId ? `redmine-settings:connection:${instanceId}` : undefined} collapseKey={instanceId ? `redmine-settings:connection:${instanceId}` : undefined}
> >
@ -318,6 +319,7 @@ export const RedmineSettingsView: React.FC = () => {
<Panel <Panel
variant="card" variant="card"
title={t('Mirror-Sync')} title={t('Mirror-Sync')}
id="redmine-settings-sync"
collapsible collapsible
collapseKey={instanceId ? `redmine-settings:sync:${instanceId}` : undefined} collapseKey={instanceId ? `redmine-settings:sync:${instanceId}` : undefined}
> >

View file

@ -375,7 +375,7 @@ export const RedmineStatsView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="redmine-stats-no-instance">
<div className={styles.placeholder}>{t('Keine Feature-Instanz ausgewaehlt')}</div> <div className={styles.placeholder}>{t('Keine Feature-Instanz ausgewaehlt')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -387,7 +387,7 @@ export const RedmineStatsView: React.FC = () => {
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Statistik')} id="redmine-stats-header-toolbar">
<div> <div>
<h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Statistik')}</h2> <h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Statistik')}</h2>
<p className={styles.subheading} style={{ margin: '0.35rem 0 0' }}> <p className={styles.subheading} style={{ margin: '0.35rem 0 0' }}>
@ -399,7 +399,7 @@ export const RedmineStatsView: React.FC = () => {
{schemaError && <div className={styles.alertErr}>{schemaError}</div>} {schemaError && <div className={styles.alertErr}>{schemaError}</div>}
{error && <div className={styles.alertErr}>{error}</div>} {error && <div className={styles.alertErr}>{error}</div>}
<Panel variant="card"> <Panel variant="card" title={t('Statistik')} id="redmine-stats-report">
<FormGeneratorReport <FormGeneratorReport
title={stats title={stats
? t('{total} Tickets ({open} offen, {closed} geschlossen)', { ? t('{total} Tickets ({open} offen, {closed} geschlossen)', {

View file

@ -162,7 +162,7 @@ export const RedmineTicketEditor: React.FC<Props> = ({
return ( return (
<div className={styles.editorScroll} style={{ display: 'flex', flexDirection: 'column', gap: 0, padding: 0 }}> <div className={styles.editorScroll} style={{ display: 'flex', flexDirection: 'column', gap: 0, padding: 0 }}>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Ticket')} id="redmine-ticket-editor-header-toolbar">
<div className={styles.editorHeader} style={{ marginBottom: 0, paddingBottom: 0, borderBottom: 'none' }}> <div className={styles.editorHeader} style={{ marginBottom: 0, paddingBottom: 0, borderBottom: 'none' }}>
{tracker && (() => { {tracker && (() => {
const sty = getTrackerStyle(tracker.name); const sty = getTrackerStyle(tracker.name);
@ -192,13 +192,13 @@ export const RedmineTicketEditor: React.FC<Props> = ({
</Panel> </Panel>
{(error || successMsg) && ( {(error || successMsg) && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Meldungen')} id="redmine-ticket-editor-messages-toolbar">
{error && <div className={styles.alertErr} style={{ marginBottom: successMsg ? '0.5rem' : 0 }}>{error}</div>} {error && <div className={styles.alertErr} style={{ marginBottom: successMsg ? '0.5rem' : 0 }}>{error}</div>}
{successMsg && <div className={styles.alertOk} style={{ marginBottom: 0 }}>{successMsg}</div>} {successMsg && <div className={styles.alertOk} style={{ marginBottom: 0 }}>{successMsg}</div>}
</Panel> </Panel>
)} )}
<Panel variant="card"> <Panel variant="card" title={t('Ticket bearbeiten')} id="redmine-ticket-editor-form">
<div className={styles.editorGrid}> <div className={styles.editorGrid}>
<label>{t('Titel')}</label> <label>{t('Titel')}</label>
<input className={styles.input} value={subject} onChange={e => setSubject(e.target.value)} /> <input className={styles.input} value={subject} onChange={e => setSubject(e.target.value)} />
@ -294,7 +294,7 @@ export const RedmineTicketEditor: React.FC<Props> = ({
</div> </div>
</Panel> </Panel>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="redmine-ticket-editor-actions-toolbar">
<div className={styles.buttonRow} style={{ marginTop: 0, paddingTop: 0, borderTop: 'none', justifyContent: 'flex-end', width: '100%' }}> <div className={styles.buttonRow} style={{ marginTop: 0, paddingTop: 0, borderTop: 'none', justifyContent: 'flex-end', width: '100%' }}>
<button className={styles.btnSecondary} onClick={_load} disabled={saving}> <button className={styles.btnSecondary} onClick={_load} disabled={saving}>
{t('Zuruecksetzen')} {t('Zuruecksetzen')}

View file

@ -152,7 +152,7 @@ export const TeamsbotAssistantView: React.FC = () => {
return ( return (
<StackLayout variant="form"> <StackLayout variant="form">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="teamsbot-assistant-toolbar">
<div className={styles.wizardHeader}> <div className={styles.wizardHeader}>
<h2>{t('Neues Meeting starten')}</h2> <h2>{t('Neues Meeting starten')}</h2>
<div className={styles.wizardHeaderRight}> <div className={styles.wizardHeaderRight}>
@ -187,7 +187,7 @@ export const TeamsbotAssistantView: React.FC = () => {
{error && <div className={styles.errorBanner}>{error}</div>} {error && <div className={styles.errorBanner}>{error}</div>}
<Panel variant="wizard"> <Panel variant="wizard" title={t('Meeting-Assistent')} id="teamsbot-assistant-wizard">
<div className={styles.wizardContent}> <div className={styles.wizardContent}>
{step === 'module' && ( {step === 'module' && (
<div className={styles.wizardStep}> <div className={styles.wizardStep}>

View file

@ -166,7 +166,7 @@ export const TeamsbotDashboardView: React.FC = () => {
<StackLayout.Body> <StackLayout.Body>
{error && <div className={styles.errorBanner}>{error}</div>} {error && <div className={styles.errorBanner}>{error}</div>}
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Teams Bot')} id="teamsbot-dashboard-hero">
<header className={styles.tbDashHero}> <header className={styles.tbDashHero}>
<div className={styles.tbDashHeroText}> <div className={styles.tbDashHeroText}>
<h1 className={styles.tbDashTitle}>{t('Teams Bot')}</h1> <h1 className={styles.tbDashTitle}>{t('Teams Bot')}</h1>
@ -200,7 +200,7 @@ export const TeamsbotDashboardView: React.FC = () => {
</header> </header>
</Panel> </Panel>
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Kennzahlen')} id="teamsbot-dashboard-kpis">
<section className={styles.tbDashKpiGrid} aria-label={t('Kennzahlen')}> <section className={styles.tbDashKpiGrid} aria-label={t('Kennzahlen')}>
<div className={styles.tbDashKpiCard}> <div className={styles.tbDashKpiCard}>
<div className={styles.tbDashKpiValue}>{modules.length}</div> <div className={styles.tbDashKpiValue}>{modules.length}</div>
@ -222,7 +222,7 @@ export const TeamsbotDashboardView: React.FC = () => {
</section> </section>
</Panel> </Panel>
<Panel variant="card" title={t('Module nach Aktivität')} collapsible collapseKey="teamsbot-dashboard-modules"> <Panel variant="card" title={t('Module nach Aktivität')} id="teamsbot-dashboard-modules" collapsible collapseKey="teamsbot-dashboard-modules">
{topModules.length === 0 ? ( {topModules.length === 0 ? (
<p className={styles.emptyState}>{t('Noch keine Sitzungen — starte ein Meeting im Assistenten.')}</p> <p className={styles.emptyState}>{t('Noch keine Sitzungen — starte ein Meeting im Assistenten.')}</p>
) : ( ) : (
@ -247,7 +247,7 @@ export const TeamsbotDashboardView: React.FC = () => {
</Panel> </Panel>
{activeSessions.length > 0 && ( {activeSessions.length > 0 && (
<Panel variant="card" title={t('Aktive Sitzungen')} collapsible collapseKey="teamsbot-dashboard-active-sessions"> <Panel variant="card" title={t('Aktive Sitzungen')} id="teamsbot-dashboard-active-sessions" collapsible collapseKey="teamsbot-dashboard-active-sessions">
<div className={styles.tbDashSessionList}> <div className={styles.tbDashSessionList}>
{activeSessions.map((session) => ( {activeSessions.map((session) => (
<div key={session.id} className={styles.tbDashSessionRow}> <div key={session.id} className={styles.tbDashSessionRow}>

View file

@ -224,7 +224,7 @@ export const TeamsbotModulesView: React.FC = () => {
<> <>
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="teamsbot-modules-toolbar">
<div className={styles.modulesHeader}> <div className={styles.modulesHeader}>
<h2>{t('Meeting-Module')}</h2> <h2>{t('Meeting-Module')}</h2>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
@ -247,12 +247,12 @@ export const TeamsbotModulesView: React.FC = () => {
</Panel> </Panel>
{loading && ( {loading && (
<Panel variant="card"> <Panel variant="card" title={t('Laden...')} id="teamsbot-modules-loading">
<div className={styles.loading}>{t('Laden...')}</div> <div className={styles.loading}>{t('Laden...')}</div>
</Panel> </Panel>
)} )}
<Panel variant="card"> <Panel variant="card" title={t('Meeting-Module')} id="teamsbot-modules-list">
<div className={styles.modulesList}> <div className={styles.modulesList}>
{modules.map(mod => ( {modules.map(mod => (
<div <div

View file

@ -813,7 +813,7 @@ export const TeamsbotSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Sitzung laden')} id="teamsbot-session-loading">
<div className={styles.loading}>{t('Sitzung laden')}</div> <div className={styles.loading}>{t('Sitzung laden')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -824,7 +824,7 @@ export const TeamsbotSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Keine aktive Sitzung')} id="teamsbot-session-empty">
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '1rem', padding: '3rem' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '1rem', padding: '3rem' }}>
<h3>{t('Keine aktive Sitzung')}</h3> <h3>{t('Keine aktive Sitzung')}</h3>
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}> <p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}>
@ -851,7 +851,7 @@ export const TeamsbotSessionView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Sitzung nicht gefunden')} id="teamsbot-session-not-found">
<div className={styles.errorBanner}>{t('Sitzung nicht gefunden')}</div> <div className={styles.errorBanner}>{t('Sitzung nicht gefunden')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -906,7 +906,7 @@ export const TeamsbotSessionView: React.FC = () => {
</div> </div>
)} )}
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Sitzung')} id="teamsbot-session-toolbar">
{/* Session Switcher */} {/* Session Switcher */}
{allSessions.length > 1 && ( {allSessions.length > 1 && (
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
@ -1006,7 +1006,7 @@ export const TeamsbotSessionView: React.FC = () => {
collapsible: true, collapsible: true,
collapseKey: 'teamsbot-session-udb', collapseKey: 'teamsbot-session-udb',
content: ( content: (
<Panel variant="card" title={t('Datenquellen')}> <Panel variant="card" title={t('Datenquellen')} id="teamsbot-session-datasources">
<UnifiedDataBar <UnifiedDataBar
context={_udbContext} context={_udbContext}
activeTab={udbTab} activeTab={udbTab}
@ -1025,7 +1025,7 @@ export const TeamsbotSessionView: React.FC = () => {
collapsible: true, collapsible: true,
collapseKey: 'teamsbot-session-director', collapseKey: 'teamsbot-session-director',
content: ( content: (
<Panel variant="card" title={t('Regieanweisungen')}> <Panel variant="card" title={t('Regieanweisungen')} id="teamsbot-session-director">
{!['ended', 'error', 'leaving'].includes(session.status) ? ( {!['ended', 'error', 'leaving'].includes(session.status) ? (
<div <div
className={`${styles.directorPanel} ${directorDragOver ? styles.directorPanelDragOver : ''}`} className={`${styles.directorPanel} ${directorDragOver ? styles.directorPanelDragOver : ''}`}
@ -1229,7 +1229,7 @@ export const TeamsbotSessionView: React.FC = () => {
defaultSize: 54, defaultSize: 54,
minSize: 30, minSize: 30,
content: ( content: (
<Panel variant="editor"> <Panel variant="editor" title={t('Transkript & Antworten')} id="teamsbot-session-editor">
<PanelLayout <PanelLayout
persistenceKey={`teamsbot-session-editor-${instanceId}`} persistenceKey={`teamsbot-session-editor-${instanceId}`}
direction="horizontal" direction="horizontal"

View file

@ -202,7 +202,7 @@ export const TeamsbotSettingsView: React.FC = () => {
id: 'general', id: 'general',
label: t('Bot-Einstellungen'), label: t('Bot-Einstellungen'),
render: () => ( render: () => (
<Panel variant="card" title={t('Bot-Einstellungen')}> <Panel variant="card" title={t('Bot-Einstellungen')} id="teamsbot-settings-general">
{error && <div className={styles.errorBanner}>{error}</div>} {error && <div className={styles.errorBanner}>{error}</div>}
{successMsg && <div className={styles.successBanner}>{successMsg}</div>} {successMsg && <div className={styles.successBanner}>{successMsg}</div>}
@ -455,7 +455,7 @@ export const TeamsbotSettingsView: React.FC = () => {
id: 'systemBots', id: 'systemBots',
label: t('System-Bots'), label: t('System-Bots'),
render: () => ( render: () => (
<Panel variant="table"> <Panel variant="table" title={t('System-Bots')} id="teamsbot-system-bots-table">
<_SystemBotsPanel instanceId={instanceId} /> <_SystemBotsPanel instanceId={instanceId} />
</Panel> </Panel>
), ),
@ -472,7 +472,7 @@ export const TeamsbotSettingsView: React.FC = () => {
return ( return (
<StackLayout variant="table"> <StackLayout variant="table">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Konfiguration laden')} id="teamsbot-settings-loading">
<div className={styles.loading}>{t('Konfiguration laden')}</div> <div className={styles.loading}>{t('Konfiguration laden')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>

View file

@ -190,7 +190,7 @@ const _AbschlussTabBody: React.FC<_AbschlussTabBodyProps> = ({
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="card" title={_tabLabel(tabDef.id, t)}> <Panel variant="card" id="trustee-abschluss-tab-desc" title={_tabLabel(tabDef.id, t)}>
<p className={styles.sectionDescription} style={{ marginTop: 0 }}> <p className={styles.sectionDescription} style={{ marginTop: 0 }}>
{_tabDescription(tabDef.id, t)} {_tabDescription(tabDef.id, t)}
</p> </p>
@ -205,7 +205,7 @@ const _AbschlussTabBody: React.FC<_AbschlussTabBodyProps> = ({
</Panel> </Panel>
{!isComingSoon && ( {!isComingSoon && (
<Panel variant="card" title={currentWorkflow?.label || t('Workflow')}> <Panel variant="card" id="trustee-abschluss-workflow" title={currentWorkflow?.label || t('Workflow')}>
{workflowsLoading ? ( {workflowsLoading ? (
<p className={styles.loadingText}>{t('Workflows werden geladen…')}</p> <p className={styles.loadingText}>{t('Workflows werden geladen…')}</p>
) : !currentWorkflow ? ( ) : !currentWorkflow ? (

View file

@ -221,6 +221,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel <Panel
variant="card" variant="card"
id="trustee-accounting-connection-status"
title={t('Verbindungsstatus')} title={t('Verbindungsstatus')}
collapsible collapsible
collapseKey="trustee-accounting-connection-status" collapseKey="trustee-accounting-connection-status"
@ -254,7 +255,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
)} )}
</Panel> </Panel>
<Panel variant="wizard" title={t('Verbindung einrichten')}> <Panel variant="wizard" id="trustee-accounting-connection-setup" title={t('Verbindung einrichten')}>
{/* Step 1: Select system */} {/* Step 1: Select system */}
<div className={styles.setupStep}> <div className={styles.setupStep}>
<div className={styles.stepNumber}>1</div> <div className={styles.stepNumber}>1</div>
@ -444,7 +445,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
const _importTabContent = ( const _importTabContent = (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{!existingConfig?.configured ? ( {!existingConfig?.configured ? (
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="trustee-accounting-import-hint">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<p> <p>
{t('Bevor Sie Daten importieren können, richten Sie zuerst die Verbindung zum Buchhaltungssystem im Tab «Verbindungseinstellungen» ein.')} {t('Bevor Sie Daten importieren können, richten Sie zuerst die Verbindung zum Buchhaltungssystem im Tab «Verbindungseinstellungen» ein.')}
@ -453,11 +454,11 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
</Panel> </Panel>
) : ( ) : (
<> <>
<Panel variant="card" title={t('Letzter Import')}> <Panel variant="card" id="trustee-accounting-last-import" title={t('Letzter Import')}>
{_importLastSyncPanel} {_importLastSyncPanel}
</Panel> </Panel>
<Panel variant="card" title={t('Import starten')}> <Panel variant="card" id="trustee-accounting-import-start" title={t('Import starten')}>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: 0, marginBottom: '0.75rem' }}> <p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: 0, marginBottom: '0.75rem' }}>
{t('Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.')} {t('Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.')}
</p> </p>
@ -610,7 +611,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
)} )}
</Panel> </Panel>
<Panel variant="card" title={t('Aktueller Datenbestand')}> <Panel variant="card" id="trustee-accounting-data-status" title={t('Aktueller Datenbestand')}>
{importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) ? ( {importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) ? (
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}> <div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
{t('{konten} Konten, {buchungen} Buchungen, {zeilen} Zeilen, {kontakte} Kontakte, {salden} Salden', { {t('{konten} Konten, {buchungen} Buchungen, {zeilen} Zeilen, {kontakte} Kontakte, {salden} Salden', {
@ -657,7 +658,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Buchhaltungssystem-Anbindung')}</h1> <h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Buchhaltungssystem-Anbindung')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="trustee-accounting-loading">
<div className={styles.loading}>{t('Buchhaltungseinstellungen werden geladen…')}</div> <div className={styles.loading}>{t('Buchhaltungseinstellungen werden geladen…')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>

View file

@ -256,13 +256,13 @@ const _AnalyseTabBody: React.FC<_AnalyseTabBodyProps> = ({
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="card" title={_tabLabel(tabDef.id, t)}> <Panel variant="card" id="trustee-analyse-tab-desc" title={_tabLabel(tabDef.id, t)}>
<p className={styles.sectionDescription} style={{ marginTop: 0 }}> <p className={styles.sectionDescription} style={{ marginTop: 0 }}>
{_tabDescription(tabDef.id, t)} {_tabDescription(tabDef.id, t)}
</p> </p>
</Panel> </Panel>
<Panel variant="card" title={currentWorkflow?.label || t('Workflow')}> <Panel variant="card" id="trustee-analyse-workflow" title={currentWorkflow?.label || t('Workflow')}>
{workflowsLoading ? ( {workflowsLoading ? (
<p className={styles.loadingText}>{t('Workflows werden geladen…')}</p> <p className={styles.loadingText}>{t('Workflows werden geladen…')}</p>
) : !currentWorkflow ? ( ) : !currentWorkflow ? (

View file

@ -98,7 +98,7 @@ export const TrusteeDashboardView: React.FC = () => {
return ( return (
<StackLayout variant="dashboard"> <StackLayout variant="dashboard">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="dashboard"> <Panel variant="dashboard" title={t('Kennzahlen')} id="trustee-dashboard-kpis">
<div className={styles.statsGrid}> <div className={styles.statsGrid}>
<div className={styles.statCard}> <div className={styles.statCard}>
<div className={styles.statIcon}>📊</div> <div className={styles.statIcon}>📊</div>
@ -163,7 +163,7 @@ export const TrusteeDashboardView: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="card" title={t('Schnellzugriff')}> <Panel variant="card" id="trustee-dashboard-quick-actions" title={t('Schnellzugriff')}>
<QuickActionBoard <QuickActionBoard
actions={quickActions} actions={quickActions}
categories={quickActionCategories} categories={quickActionCategories}
@ -174,6 +174,7 @@ export const TrusteeDashboardView: React.FC = () => {
<Panel <Panel
variant="card" variant="card"
id="trustee-dashboard-instance-details"
title={t('Instanz-Details')} title={t('Instanz-Details')}
collapsible collapsible
collapseKey="trustee-dashboard-instance-details" collapseKey="trustee-dashboard-instance-details"

View file

@ -191,7 +191,7 @@ export const TrusteeDocumentsView: React.FC = () => {
if (error) { if (error) {
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="trustee-documents-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -207,7 +207,7 @@ export const TrusteeDocumentsView: React.FC = () => {
return ( return (
<> <>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="trustee-documents-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
type="button" type="button"
@ -229,7 +229,7 @@ export const TrusteeDocumentsView: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Dokumente')} id="documents-table">
<FormGeneratorTable <FormGeneratorTable
data={documents} data={documents}
columns={columns} columns={columns}

View file

@ -476,7 +476,7 @@ export const TrusteeExpenseImportView: React.FC<TrusteeExpenseImportViewProps> =
const _panelStack = ( const _panelStack = (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="card" title={t('Aktueller Status')}> <Panel variant="card" id="trustee-expense-import-status" title={t('Aktueller Status')}>
<p className={styles.sectionDescription} style={{ marginTop: 0 }}> <p className={styles.sectionDescription} style={{ marginTop: 0 }}>
{t('Verbinden Sie Ihr Microsoft-Konto und wählen Sie einen SharePoint-Ordner mit Ausgaben-PDFs. Das System extrahiert automatisch täglich die Ausgabendaten und speichert sie als Positionen.')} {t('Verbinden Sie Ihr Microsoft-Konto und wählen Sie einen SharePoint-Ordner mit Ausgaben-PDFs. Das System extrahiert automatisch täglich die Ausgabendaten und speichert sie als Positionen.')}
<span <span
@ -523,7 +523,7 @@ export const TrusteeExpenseImportView: React.FC<TrusteeExpenseImportViewProps> =
)} )}
</Panel> </Panel>
<Panel variant="wizard" title={t('Einrichtung')}> <Panel variant="wizard" id="trustee-expense-import-setup" title={t('Einrichtung')}>
{/* Step 1: Microsoft Connection */} {/* Step 1: Microsoft Connection */}
<div className={styles.setupStep}> <div className={styles.setupStep}>
<div className={styles.stepNumber}>1</div> <div className={styles.stepNumber}>1</div>

View file

@ -61,7 +61,7 @@ export const TrusteeImportProcessView: React.FC = () => {
label: t('Daten einlesen'), label: t('Daten einlesen'),
icon: <span aria-hidden="true">{'\uD83D\uDD04'}</span>, icon: <span aria-hidden="true">{'\uD83D\uDD04'}</span>,
render: () => ( render: () => (
<Panel variant="card"> <Panel variant="card" title={t('Weiterleitung')} id="trustee-import-sync-redirect">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<p>{t('Weiterleitung zu den Buchhaltungs-Einstellungen…')}</p> <p>{t('Weiterleitung zu den Buchhaltungs-Einstellungen…')}</p>
</div> </div>

View file

@ -77,7 +77,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="trustee-instance-roles-no-instance">
<div className={styles.error}>{t('Keine Feature-Instanz ausgewählt')}</div> <div className={styles.error}>{t('Keine Feature-Instanz ausgewählt')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -92,7 +92,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1> <h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden')} id="trustee-instance-roles-loading">
<div className={styles.loading}>{t('Lade Instanzrollen')}</div> <div className={styles.loading}>{t('Lade Instanzrollen')}</div>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -107,7 +107,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1> <h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1>
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="trustee-instance-roles-error">
<div className={styles.error}> <div className={styles.error}>
<p>{error}</p> <p>{error}</p>
<button type="button" onClick={fetchRoles} className={styles.retryButton}> <button type="button" onClick={fetchRoles} className={styles.retryButton}>
@ -127,13 +127,13 @@ export const TrusteeInstanceRolesView: React.FC = () => {
</StackLayout.Header> </StackLayout.Header>
<StackLayout.Body> <StackLayout.Body>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="trustee-instance-roles-toolbar">
<button type="button" onClick={fetchRoles} className={styles.secondaryButton}> <button type="button" onClick={fetchRoles} className={styles.secondaryButton}>
<FaSync /> {t('Aktualisieren')} <FaSync /> {t('Aktualisieren')}
</button> </button>
</Panel> </Panel>
<Panel variant="card"> <Panel variant="card" title={t('Hinweis')} id="trustee-instance-roles-info">
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaShieldAlt style={{ marginRight: '0.5rem' }} /> <FaShieldAlt style={{ marginRight: '0.5rem' }} />
<span> <span>
@ -146,7 +146,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
</p> </p>
</Panel> </Panel>
<Panel variant="card" title={t('Rollen')}> <Panel variant="card" id="trustee-instance-roles-list" title={t('Rollen')}>
{roles.length === 0 ? ( {roles.length === 0 ? (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} /> <FaUserShield className={styles.emptyIcon} />

View file

@ -341,7 +341,7 @@ export const TrusteePositionsView: React.FC = () => {
if (error) { if (error) {
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="trustee-positions-error">
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}> <p className={styles.errorMessage}>
@ -357,7 +357,7 @@ export const TrusteePositionsView: React.FC = () => {
return ( return (
<> <>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="trustee-positions-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<button <button
type="button" type="button"
@ -379,7 +379,7 @@ export const TrusteePositionsView: React.FC = () => {
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Positionen')} id="positions-table">
<FormGeneratorTable <FormGeneratorTable
data={positions} data={positions}
columns={columns} columns={columns}

View file

@ -268,7 +268,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
if (!fileContext) { if (!fileContext) {
const unavailable = ( const unavailable = (
<Panel variant="card"> <Panel variant="card" title={t('Nicht verfügbar')} id="trustee-scan-upload-unavailable">
<p>{t('Datei-Upload ist nicht verfügbar')}</p> <p>{t('Datei-Upload ist nicht verfügbar')}</p>
</Panel> </Panel>
); );
@ -282,7 +282,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
const _panelStack = ( const _panelStack = (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Panel variant="card" title={t('Pipeline')}> <Panel variant="card" id="trustee-scan-upload-pipeline" title={t('Pipeline')}>
<p className={styles.sectionDescription} style={{ marginTop: 0 }}> <p className={styles.sectionDescription} style={{ marginTop: 0 }}>
{t('Laden Sie PDF- oder JPG-Dokumente (Belege, Rechnungen) hoch. Starten Sie dann die Pipeline: Daten extrahieren → Positionen erstellen → in Buchhaltung synchronisieren.')} {t('Laden Sie PDF- oder JPG-Dokumente (Belege, Rechnungen) hoch. Starten Sie dann die Pipeline: Daten extrahieren → Positionen erstellen → in Buchhaltung synchronisieren.')}
</p> </p>
@ -301,7 +301,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
)} )}
</Panel> </Panel>
<Panel variant="card" title={t('Dateien hochladen')}> <Panel variant="card" id="trustee-scan-upload-files" title={t('Dateien hochladen')}>
<div <div
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}

View file

@ -178,7 +178,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
if (error) { if (error) {
return ( return (
<Panel variant="card"> <Panel variant="card" title={t('Fehler')} id="trustee-data-error">
<div className={adminStyles.errorContainer}> <div className={adminStyles.errorContainer}>
<span className={adminStyles.errorIcon}></span> <span className={adminStyles.errorIcon}></span>
<p className={adminStyles.errorMessage}> <p className={adminStyles.errorMessage}>
@ -210,7 +210,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
return ( return (
<> <>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="trustee-data-toolbar">
<div className={adminStyles.headerActions}> <div className={adminStyles.headerActions}>
<button <button
type="button" type="button"
@ -223,7 +223,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={entityLabel ? t(entityLabel) : t('Daten')} id={`${apiEndpoint.split('/').slice(4).join('-')}-table`}>
<FormGeneratorTable <FormGeneratorTable
data={items} data={items}
columns={columns} columns={columns}

View file

@ -52,7 +52,7 @@ export const WorkflowEditorPage: React.FC<WorkflowEditorPageProps> = ({
} }
}, [workflowIdFromUrl]); }, [workflowIdFromUrl]);
const { currentLanguage } = useLanguage(); const { currentLanguage, t } = useLanguage();
const language = (currentLanguage?.slice(0, 2) || 'de') as string; const language = (currentLanguage?.slice(0, 2) || 'de') as string;
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]); const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
@ -115,7 +115,7 @@ export const WorkflowEditorPage: React.FC<WorkflowEditorPageProps> = ({
}, [instanceId]); }, [instanceId]);
const _editorPanel = ( const _editorPanel = (
<Panel variant="editor"> <Panel variant="editor" title={t('Workflow-Editor')} id="workflow-editor">
<FlowEditor <FlowEditor
instanceId={instanceId || ''} instanceId={instanceId || ''}
mandateId={mandateId || undefined} mandateId={mandateId || undefined}

View file

@ -222,7 +222,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
if (!instanceId) { if (!instanceId) {
const _noInstance = ( const _noInstance = (
<Panel variant="card"> <Panel variant="card" title={t('Keine Feature-Instanz gefunden')} id="workflow-templates-no-instance">
<p>{t('Keine Feature-Instanz gefunden')}</p> <p>{t('Keine Feature-Instanz gefunden')}</p>
</Panel> </Panel>
); );
@ -236,7 +236,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
const _panelStack = ( const _panelStack = (
<> <>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Filter')} id="workflow-templates-toolbar">
<div className={styles.headerActions} style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}> <div className={styles.headerActions} style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{(['all', 'user', 'instance', 'mandate', 'system'] as const).map((s) => ( {(['all', 'user', 'instance', 'mandate', 'system'] as const).map((s) => (
@ -262,7 +262,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Workflow-Vorlagen')} id="workflow-templates-table">
<FormGeneratorTable<AutoWorkflowTemplate> <FormGeneratorTable<AutoWorkflowTemplate>
data={templates} data={templates}
columns={columns} columns={columns}

View file

@ -85,7 +85,7 @@ export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
}, [messages, audioQueue]); }, [messages, audioQueue]);
return ( return (
<Panel variant="editor"> <Panel variant="editor" title={t('Chat')} id="chat-stream" collapsible={false}>
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}
onScroll={_handleScroll} onScroll={_handleScroll}

View file

@ -63,7 +63,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ instanceId, fileId, fi
if (!file) { if (!file) {
return ( return (
<Panel variant="card" title={t('Vorschau')}> <Panel variant="card" title={t('Vorschau')} id="file-preview-empty">
<div style={{ padding: 24, textAlign: 'center', color: '#999', fontSize: 13 }}> <div style={{ padding: 24, textAlign: 'center', color: '#999', fontSize: 13 }}>
{t('Datei für Vorschau auswählen')} {t('Datei für Vorschau auswählen')}
</div> </div>
@ -82,7 +82,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ instanceId, fileId, fi
); );
return ( return (
<Panel variant="card" title={file.fileName} subtitle={_metaSubtitle}> <Panel variant="card" title={file.fileName} subtitle={_metaSubtitle} id="file-preview">
{file.description && ( {file.description && (
<div style={{ fontSize: 12, color: '#555', marginBottom: 8 }}>{file.description}</div> <div style={{ fontSize: 12, color: '#555', marginBottom: 8 }}>{file.description}</div>
)} )}

View file

@ -309,6 +309,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
{snapshots.length > 0 && ( {snapshots.length > 0 && (
<Panel <Panel
variant="card" variant="card"
id="neutralization-snapshots"
title={`${t('Neutralisierter Text')} (${snapshots.length})`} title={`${t('Neutralisierter Text')} (${snapshots.length})`}
collapsible collapsible
collapseKey={`neutralization-snapshots-${instanceId}`} collapseKey={`neutralization-snapshots-${instanceId}`}
@ -361,6 +362,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
{sources.length > 0 && ( {sources.length > 0 && (
<Panel <Panel
variant="card" variant="card"
id="neutralization-sources"
title={t('Datenquellen')} title={t('Datenquellen')}
collapsible collapsible
collapseKey={`neutralization-sources-${instanceId}`} collapseKey={`neutralization-sources-${instanceId}`}
@ -420,6 +422,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
{selectedSource && mappings.length > 0 && ( {selectedSource && mappings.length > 0 && (
<Panel <Panel
variant="table" variant="table"
id="neutralization-mappings"
title={`${t('Platzhalter-Mappings')} (${mappings.length})`} title={`${t('Platzhalter-Mappings')} (${mappings.length})`}
> >
<div style={{ maxHeight: 300, overflowY: 'auto' }}> <div style={{ maxHeight: 300, overflowY: 'auto' }}>

View file

@ -192,7 +192,7 @@ export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities })
if (!activities.length) { if (!activities.length) {
return ( return (
<Panel variant="card" title={t('Aktivität')}> <Panel variant="card" title={t('Aktivität')} id="tool-activity-empty">
<div style={{ padding: 16, textAlign: 'center', color: 'var(--color-text-secondary, #999)', fontSize: 12 }}> <div style={{ padding: 16, textAlign: 'center', color: 'var(--color-text-secondary, #999)', fontSize: 12 }}>
{t('Noch keine Aktivität')} {t('Noch keine Aktivität')}
</div> </div>
@ -201,7 +201,7 @@ export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities })
} }
return ( return (
<Panel variant="card" title={t('Aktivität')}> <Panel variant="card" title={t('Aktivität')} id="tool-activity">
<div style={{ padding: 8 }}> <div style={{ padding: 8 }}>
{activities.map(activity => { {activities.map(activity => {
const isError = activity.status === 'error'; const isError = activity.status === 'error';

View file

@ -37,6 +37,8 @@ export interface WorkspaceContextSidebarProps {
files: WorkspaceFile[]; files: WorkspaceFile[];
/** When false, sidebar stays expanded (e.g. mobile bottom sheet). */ /** When false, sidebar stays expanded (e.g. mobile bottom sheet). */
allowCollapse?: boolean; allowCollapse?: boolean;
/** Fill the host container width (used inside a resizable PanelLayout pane). */
fillWidth?: boolean;
} }
export const WorkspaceContextSidebar: React.FC<WorkspaceContextSidebarProps> = ({ export const WorkspaceContextSidebar: React.FC<WorkspaceContextSidebarProps> = ({
@ -55,12 +57,13 @@ export const WorkspaceContextSidebar: React.FC<WorkspaceContextSidebarProps> = (
selectedFileId, selectedFileId,
files, files,
allowCollapse = true, allowCollapse = true,
fillWidth = false,
}) => { }) => {
const isCollapsed = allowCollapse && collapsed; const isCollapsed = allowCollapse && collapsed;
return ( return (
<aside <aside
className={`${styles.contextSidebar} ${isCollapsed ? styles.contextSidebarCollapsed : ''}`} className={`${styles.contextSidebar} ${isCollapsed ? styles.contextSidebarCollapsed : ''} ${fillWidth && !isCollapsed ? styles.contextSidebarFill : ''}`}
aria-label="Kontext" aria-label="Kontext"
> >
<div className={styles.contextToolbar}> <div className={styles.contextToolbar}>

View file

@ -67,7 +67,7 @@ export const WorkspaceEditorPage: React.FC = () => {
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="editor-header-toolbar">
<div className={pageStyles.headerRow}> <div className={pageStyles.headerRow}>
<div className={pageStyles.headerLeft}> <div className={pageStyles.headerLeft}>
<button onClick={_goBack} style={_btnStyle} title={t('Zurück zum Dashboard')}> <button onClick={_goBack} style={_btnStyle} title={t('Zurück zum Dashboard')}>
@ -103,7 +103,7 @@ export const WorkspaceEditorPage: React.FC = () => {
</Panel> </Panel>
{pendingEdits.length > 0 && ( {pendingEdits.length > 0 && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Dateien')} id="editor-tabs-toolbar">
<div className={pageStyles.tabBar}> <div className={pageStyles.tabBar}>
{pendingEdits.map(edit => ( {pendingEdits.map(edit => (
<_EditorTab <_EditorTab
@ -117,7 +117,7 @@ export const WorkspaceEditorPage: React.FC = () => {
</Panel> </Panel>
)} )}
<Panel variant="editor"> <Panel variant="editor" title={t('Vergleich')} id="editor-diff" collapsible={false}>
<div className={pageStyles.editorFill}> <div className={pageStyles.editorFill}>
{editor.isLoading ? ( {editor.isLoading ? (
<div className={pageStyles.loadingState}> <div className={pageStyles.loadingState}>
@ -143,7 +143,7 @@ export const WorkspaceEditorPage: React.FC = () => {
</Panel> </Panel>
{activeEdit && activeEdit.status === 'pending' && ( {activeEdit && activeEdit.status === 'pending' && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="editor-footer-toolbar">
<div className={pageStyles.footerRow}> <div className={pageStyles.footerRow}>
<div className={pageStyles.footerMeta}> <div className={pageStyles.footerMeta}>
<span>{activeEdit.fileName}</span> <span>{activeEdit.fileName}</span>

View file

@ -194,7 +194,7 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
{error && <div className={styles.error}>{error}</div>} {error && <div className={styles.error}>{error}</div>}
{success && <div className={styles.success}>{success}</div>} {success && <div className={styles.success}>{success}</div>}
<Panel variant="card" title={t('Agenten-Konfiguration')}> <Panel variant="card" title={t('Agenten-Konfiguration')} id="agent-config">
<div className={styles.field}> <div className={styles.field}>
<label className={styles.label}> <label className={styles.label}>
{t('Max. Agenten-Runden')} {t('Max. Agenten-Runden')}
@ -239,7 +239,7 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
</button> </button>
</Panel> </Panel>
<Panel variant="card" title={t('KI-Einstellungen')}> <Panel variant="card" title={t('KI-Einstellungen')} id="ai-settings">
<div className={styles.field}> <div className={styles.field}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}> <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
<input <input

View file

@ -506,7 +506,6 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
: []; : [];
const hasAttachments = hasFileOrSourceAttachments; const hasAttachments = hasFileOrSourceAttachments;
const _horizontalPadding = isMobile ? 12 : 24;
const _controlSize = isMobile ? 38 : 40; const _controlSize = isMobile ? 38 : 40;
const _handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => { const _handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
@ -624,7 +623,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
onDrop={e => void _handlePromptDrop(e)} onDrop={e => void _handlePromptDrop(e)}
> >
{hasAttachments && ( {hasAttachments && (
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Anhänge')} id="input-attachments-toolbar">
<div style={{ <div style={{
display: 'flex', display: 'flex',
gap: 6, gap: 6,
@ -757,7 +756,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
</div> </div>
</FloatingPortal> </FloatingPortal>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Eingabe')} id="input-toolbar">
<div style={{ <div style={{
display: 'flex', display: 'flex',
gap: 8, gap: 8,

View file

@ -162,6 +162,14 @@
width: 44px; width: 44px;
} }
/* When hosted inside a PanelLayout pane, the sidebar fills the pane width so
the divider controls its size (instead of the fixed 320px). */
.contextSidebarFill {
flex: 1 1 auto;
width: auto;
border-right: none;
}
.contextToolbar { .contextToolbar {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;

View file

@ -27,6 +27,7 @@ import type { ProviderSelection } from '../../../components/ProviderSelector';
import { useBilling } from '../../../hooks/useBilling'; import { useBilling } from '../../../hooks/useBilling';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { StackLayout } from '../../../components/Layout/StackLayout'; import { StackLayout } from '../../../components/Layout/StackLayout';
import { PanelLayout } from '../../../components/Layout/PanelLayout';
import { FloatingPortal } from '../../../components/UiComponents/FloatingPortal'; import { FloatingPortal } from '../../../components/UiComponents/FloatingPortal';
import { WorkspaceContextSidebar, type WorkspaceCtxTab } from './WorkspaceContextSidebar'; import { WorkspaceContextSidebar, type WorkspaceCtxTab } from './WorkspaceContextSidebar';
import styles from './WorkspacePage.module.css'; import styles from './WorkspacePage.module.css';
@ -376,7 +377,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
} }
}, [instanceId, workspace]); }, [instanceId, workspace]);
const _contextSidebar = (allowCollapse = true) => ( const _contextSidebar = (allowCollapse = true, fillWidth = false) => (
<WorkspaceContextSidebar <WorkspaceContextSidebar
context={_udbContext} context={_udbContext}
activeTab={udbTab} activeTab={udbTab}
@ -384,6 +385,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
collapsed={ctxSidebarCollapsed} collapsed={ctxSidebarCollapsed}
onToggleCollapsed={() => setCtxSidebarCollapsed(v => !v)} onToggleCollapsed={() => setCtxSidebarCollapsed(v => !v)}
allowCollapse={allowCollapse} allowCollapse={allowCollapse}
fillWidth={fillWidth}
onFileSelect={_handleFileSelect} onFileSelect={_handleFileSelect}
onSourcesChanged={_handleSourcesChanged} onSourcesChanged={_handleSourcesChanged}
onSendToChat_Files={_handleSendToChat_Files} onSendToChat_Files={_handleSendToChat_Files}
@ -615,8 +617,21 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
<div className={styles.workspaceShell}> <div className={styles.workspaceShell}>
{_desktopTopBar} {_desktopTopBar}
<div className={styles.mainStage}> <div className={styles.mainStage}>
{ctxPanelOpen && _contextSidebar()} {ctxPanelOpen && !ctxSidebarCollapsed ? (
{_centerColumn} <PanelLayout
persistenceKey="workspace-main-split"
direction="horizontal"
panes={[
{ id: 'context', defaultSize: 26, minSize: 16, maxSize: 50, content: _contextSidebar(true, true) },
{ id: 'chat', defaultSize: 74, content: _centerColumn },
]}
/>
) : (
<>
{ctxPanelOpen && _contextSidebar()}
{_centerColumn}
</>
)}
</div> </div>
</div> </div>
)} )}

View file

@ -29,7 +29,7 @@ export const WorkspaceSettingsPage: React.FC = () => {
return ( return (
<StackLayout variant="form"> <StackLayout variant="form">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Einstellungen')} id="workspace-settings">
<WorkspaceGeneralSettings instanceId={instanceId} /> <WorkspaceGeneralSettings instanceId={instanceId} />
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>

View file

@ -203,7 +203,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Lauf auswählen')} id="run-detail-empty">
<p style={{ margin: 0, color: 'var(--text-secondary)' }}> <p style={{ margin: 0, color: 'var(--text-secondary)' }}>
{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')} {t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}
</p> </p>
@ -217,7 +217,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="card"> <Panel variant="card" title={t('Laden…')} id="run-detail-loading">
<p style={{ margin: 0 }}>{t('Laden…')}</p> <p style={{ margin: 0 }}>{t('Laden…')}</p>
</Panel> </Panel>
</StackLayout.Body> </StackLayout.Body>
@ -234,13 +234,13 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="run-detail-toolbar">
<button type="button" className={styles.secondaryButton} onClick={onBack}> <button type="button" className={styles.secondaryButton} onClick={onBack}>
{t('Zurück zu Läufe')} {t('Zurück zu Läufe')}
</button> </button>
</Panel> </Panel>
<Panel variant="card"> <Panel variant="card" title={t('Laufübersicht')} id="run-detail-overview">
<h3 style={{ margin: '0 0 0.5rem' }}>{run.workflowLabel || run.workflowId}</h3> <h3 style={{ margin: '0 0 0.5rem' }}>{run.workflowLabel || run.workflowId}</h3>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)' }}> <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
<span><strong>{t('Status')}:</strong> {run.status}</span> <span><strong>{t('Status')}:</strong> {run.status}</span>
@ -259,7 +259,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
</Panel> </Panel>
{_producedFiles.length > 0 && ( {_producedFiles.length > 0 && (
<Panel variant="card" title={`${t('Ergebnisse')} (${_producedFiles.length})`} collapsible> <Panel variant="card" title={`${t('Ergebnisse')} (${_producedFiles.length})`} id="run-detail-results" collapsible>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{_producedFiles.map(f => ( {_producedFiles.map(f => (
<a <a
@ -276,7 +276,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
</Panel> </Panel>
)} )}
<Panel variant="card" title={t('Schritte')}> <Panel variant="card" title={t('Schritte')} id="run-detail-steps">
{steps.length === 0 ? ( {steps.length === 0 ? (
<p style={{ margin: 0, color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p> <p style={{ margin: 0, color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
) : ( ) : (
@ -349,7 +349,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
</Panel> </Panel>
{_visibleUnassigned.length > 0 && ( {_visibleUnassigned.length > 0 && (
<Panel variant="card" title={t('Sonstige Dokumente')} collapsible> <Panel variant="card" title={t('Sonstige Dokumente')} id="run-detail-other-docs" collapsible>
<_FileLinkList files={_visibleUnassigned} /> <_FileLinkList files={_visibleUnassigned} />
</Panel> </Panel>
)} )}

View file

@ -475,6 +475,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
<Panel <Panel
variant="dashboard" variant="dashboard"
title={t('Übersicht')} title={t('Übersicht')}
id="runs-overview"
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
actions={ actions={
@ -529,7 +530,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
</div> </div>
)} )}
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Läufe')} id="runs-table">
<FormGeneratorTable<WorkflowRun> <FormGeneratorTable<WorkflowRun>
data={runs} data={runs}
columns={_runColumns} columns={_runColumns}

View file

@ -113,7 +113,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
return ( return (
<StackLayout variant="scroll"> <StackLayout variant="scroll">
<StackLayout.Body> <StackLayout.Body>
<Panel variant="toolbar"> <Panel variant="toolbar" title={t('Aktionen')} id="tasks-toolbar">
<div className={styles.headerActions}> <div className={styles.headerActions}>
<span style={{ fontSize: '0.875rem', color: 'var(--text-secondary, #666)', marginRight: 'auto' }}> <span style={{ fontSize: '0.875rem', color: 'var(--text-secondary, #666)', marginRight: 'auto' }}>
{t('Offene Aufgaben')}: {_pendingTasks.length} {t('Offene Aufgaben')}: {_pendingTasks.length}
@ -124,7 +124,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
</div> </div>
</Panel> </Panel>
<Panel variant="card" title={t('Offene Aufgaben')}> <Panel variant="card" title={t('Offene Aufgaben')} id="tasks-pending">
{_pendingTasks.length === 0 && !loading && ( {_pendingTasks.length === 0 && !loading && (
<p style={{ margin: 0, textAlign: 'center', color: 'var(--text-secondary)' }}> <p style={{ margin: 0, textAlign: 'center', color: 'var(--text-secondary)' }}>
{t('Keine offenen Aufgaben vorhanden.')} {t('Keine offenen Aufgaben vorhanden.')}
@ -183,6 +183,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
<Panel <Panel
variant="card" variant="card"
title={`${t('Abgeschlossene Aufgaben')} (${_completedTasks.length})`} title={`${t('Abgeschlossene Aufgaben')} (${_completedTasks.length})`}
id="tasks-completed"
collapsible collapsible
defaultCollapsed defaultCollapsed
collapseKey="workflow-tasks-completed" collapseKey="workflow-tasks-completed"

View file

@ -296,6 +296,7 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
<Panel <Panel
variant="dashboard" variant="dashboard"
title={t('Übersicht')} title={t('Übersicht')}
id="workflows-overview"
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
> >
@ -306,7 +307,7 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
</div> </div>
</Panel> </Panel>
<Panel variant="table"> <Panel variant="table" title={t('Workflows')} id="workflows-table">
<FormGeneratorTable<SystemWorkflow> <FormGeneratorTable<SystemWorkflow>
data={workflows} data={workflows}
columns={_columns} columns={_columns}