panel fixes 3
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m26s
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m26s
This commit is contained in:
parent
766a767d59
commit
0ad9006b94
89 changed files with 603 additions and 342 deletions
|
|
@ -41,7 +41,7 @@ import { GDPRPage } from './pages/GDPR';
|
|||
import StorePage from './pages/Store';
|
||||
import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage';
|
||||
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 { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
|
||||
|
|
@ -220,6 +220,7 @@ function App() {
|
|||
</Route>
|
||||
<Route path="subscriptions" element={<AdminSubscriptionsPage />} />
|
||||
<Route path="logs" element={<AdminLogsPage />} />
|
||||
<Route path="sessions" element={<AdminSessionsPage />} />
|
||||
<Route path="languages" element={null} />
|
||||
<Route path="database-health" element={null} />
|
||||
<Route path="demo-config" element={<AdminDemoConfigPage />} />
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@
|
|||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -131,19 +134,17 @@
|
|||
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 {
|
||||
flex-shrink: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid var(--text-tertiary, #888);
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: 50% 40%;
|
||||
}
|
||||
|
||||
.panelCollapsed .chevron {
|
||||
transform: rotate(-90deg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #888);
|
||||
}
|
||||
|
||||
.body {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright (c) 2026 PowerOn AG
|
||||
// All rights reserved.
|
||||
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 styles from './Panel.module.css';
|
||||
|
||||
function _loadCollapsed(key: string | undefined, fallback: boolean): boolean {
|
||||
if (!key) return fallback;
|
||||
function _loadCollapsed(key: string, fallback: boolean): boolean {
|
||||
try {
|
||||
const stored = localStorage.getItem(`panel-collapse:${key}`);
|
||||
if (stored !== null) return stored === '1';
|
||||
|
|
@ -13,8 +14,7 @@ function _loadCollapsed(key: string | undefined, fallback: boolean): boolean {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
function _saveCollapsed(key: string | undefined, value: boolean): void {
|
||||
if (!key) return;
|
||||
function _saveCollapsed(key: string, value: boolean): void {
|
||||
try {
|
||||
localStorage.setItem(`panel-collapse:${key}`, value ? '1' : '0');
|
||||
} catch { /* noop */ }
|
||||
|
|
@ -23,34 +23,37 @@ function _saveCollapsed(key: string | undefined, value: boolean): void {
|
|||
export const Panel: FC<PanelProps> = ({
|
||||
variant = 'card',
|
||||
title,
|
||||
id,
|
||||
subtitle,
|
||||
actions,
|
||||
collapsible = false,
|
||||
collapsible = true,
|
||||
defaultCollapsed = false,
|
||||
collapseKey,
|
||||
className = '',
|
||||
style,
|
||||
fill = false,
|
||||
children,
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(() => _loadCollapsed(collapseKey, defaultCollapsed));
|
||||
const { pathname } = useLocation();
|
||||
const persistKey = collapseKey ?? `${pathname}:${id}`;
|
||||
const [collapsed, setCollapsed] = useState(() => _loadCollapsed(persistKey, defaultCollapsed));
|
||||
|
||||
useEffect(() => {
|
||||
_saveCollapsed(collapseKey, collapsed);
|
||||
}, [collapseKey, collapsed]);
|
||||
_saveCollapsed(persistKey, collapsed);
|
||||
}, [persistKey, collapsed]);
|
||||
|
||||
const _toggleCollapsed = useCallback(() => {
|
||||
if (collapsible) setCollapsed((prev) => !prev);
|
||||
}, [collapsible]);
|
||||
|
||||
const hasHeader = title != null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.panel} ${collapsed ? styles.panelCollapsed : ''} ${className}`}
|
||||
data-variant={variant}
|
||||
data-fill={fill ? 'true' : undefined}
|
||||
data-panel-id={id}
|
||||
style={style}
|
||||
>
|
||||
{hasHeader && (
|
||||
<div
|
||||
className={`${styles.header} ${collapsible ? styles.headerCollapsible : ''}`}
|
||||
role={collapsible ? 'button' : undefined}
|
||||
|
|
@ -72,10 +75,13 @@ export const Panel: FC<PanelProps> = ({
|
|||
<span className={styles.title}>{title}</span>
|
||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||
</div>
|
||||
{actions && <div className={styles.actions}>{actions}</div>}
|
||||
{collapsible && <span className={styles.chevron} aria-hidden />}
|
||||
</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 : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Shared types for the Layout component system.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ScrollMode (from useScrollMode hook)
|
||||
|
|
@ -81,13 +81,19 @@ export type PanelVariant = 'card' | 'table' | 'dashboard' | 'toolbar' | 'editor'
|
|||
|
||||
export interface PanelProps {
|
||||
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;
|
||||
actions?: ReactNode;
|
||||
/** Collapse/expand toggle. Default true (opt-out for chat/editor regions). */
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
/** Explicit persistence key. Defaults to `{pathname}:{id}`. */
|
||||
collapseKey?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
}, [detail?.neutralizationMappings]);
|
||||
|
||||
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 && (
|
||||
<div className={styles.modalMappingBar}>
|
||||
<span className={styles.modalMappingLabel}>
|
||||
|
|
@ -713,7 +713,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
if (!selectedMandateId) return [];
|
||||
|
||||
const statsPanel = (
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Statistiken')} id="audit-stats">
|
||||
<div className={styles.statsControls}>
|
||||
<PeriodPicker
|
||||
value={statsPeriod}
|
||||
|
|
@ -861,7 +861,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
id: 'audit-log',
|
||||
label: _tabLabel('audit-log', t),
|
||||
render: () => (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Audit-Einträge')} id="audit-log-table">
|
||||
<FormGeneratorTable
|
||||
key={`audit-log-${selectedMandateId}`}
|
||||
data={auditEntries}
|
||||
|
|
@ -887,7 +887,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
render: () => (
|
||||
<ViewStack entityParam="entryId">
|
||||
<ViewStack.View id="list">
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('AI-Audit-Einträge')} id="ai-log-table">
|
||||
<FormGeneratorTable
|
||||
key={`ai-log-${selectedMandateId}`}
|
||||
data={aiEntries}
|
||||
|
|
@ -934,7 +934,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
id: 'neutralization',
|
||||
label: _tabLabel('neutralization', t),
|
||||
render: () => (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Neutralisierungs-Zuordnungen')} id="neutralization-table">
|
||||
<FormGeneratorTable
|
||||
key={`neut-${selectedMandateId}`}
|
||||
data={neutEntries}
|
||||
|
|
@ -1010,7 +1010,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
</p>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="audit-toolbar">
|
||||
<div className={styles.mandateSelector}>
|
||||
<label className={styles.mandateLabel}>{t('Mandant auswählen')}</label>
|
||||
<select
|
||||
|
|
@ -1028,7 +1028,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{!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>
|
||||
</Panel>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const DashboardPage: React.FC = () => {
|
|||
)}
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Erste Schritte')} id="dashboard-onboarding">
|
||||
<OnboardingAssistant />
|
||||
</Panel>
|
||||
|
||||
|
|
@ -104,6 +104,7 @@ export const DashboardPage: React.FC = () => {
|
|||
<Panel
|
||||
key={mandate.id}
|
||||
variant="dashboard"
|
||||
id={`mandate-dashboard-${mandate.id}`}
|
||||
title={(
|
||||
<span className={styles.sectionTitle}>
|
||||
<FaBuilding />
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export const GDPRPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<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.actionCard}>
|
||||
<h3>{t('Zugriff (Artikel 15)')}</h3>
|
||||
|
|
@ -283,7 +283,7 @@ export const GDPRPage: React.FC = () => {
|
|||
)}
|
||||
</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>}
|
||||
{consentError && <p className={styles.errorText}>{consentError}</p>}
|
||||
{!isLoadingConsent && !consentError && consentInfo && (
|
||||
|
|
|
|||
|
|
@ -218,13 +218,13 @@ export const IntegrationsOverviewPage: React.FC = () => {
|
|||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Integrationen')}</h1>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Einleitung')} id="integrations-lead">
|
||||
<p className={styles.pageLead} style={{ margin: 0 }}>
|
||||
{t('PORTA Architektur — Daten, Verarbeitung und Mandanten auf einen Blick.')}
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Architekturdiagramm')} id="integrations-diagram">
|
||||
<h2 className={styles.srOnly}>
|
||||
{t('PORTA Architektur v3: Drei separate Boxen in Schicht 2 — Infrastruktur, PORTA, Nutzen')}
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ export const RagInventoryPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<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.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Kontext:')}</label>
|
||||
|
|
@ -281,19 +281,19 @@ export const RagInventoryPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{loading && !inventory && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden...')} id="rag-inventory-loading">
|
||||
<div className={styles.loading}>{t('Laden...')}</div>
|
||||
</Panel>
|
||||
)}
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="rag-inventory-error">
|
||||
<div className={styles.error}>{error}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{inventory && (
|
||||
<>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Übersicht')} id="rag-inventory-totals">
|
||||
<div className={styles.totals}>
|
||||
<span className={styles.totalLabel}>{t('Total Dateien')}:</span>
|
||||
<strong className={styles.totalValue}>{inventory.totals?.files ?? 0}</strong>
|
||||
|
|
@ -306,7 +306,7 @@ export const RagInventoryPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{(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.connectionHeader}>
|
||||
<span className={styles.authority}>{conn.authority}</span>
|
||||
|
|
@ -467,7 +467,7 @@ export const RagInventoryPage: React.FC = () => {
|
|||
<FaCubes style={{ marginRight: 8 }} />
|
||||
{t('Feature-Daten')}
|
||||
</span>
|
||||
)}>
|
||||
)} id="rag-feature-data">
|
||||
{(inventory.featureInstances || []).map((fi: RagFeatureInstanceDto) => {
|
||||
const runningJobs = fi.runningJobs || [];
|
||||
const lastSuccess = fi.lastSuccess;
|
||||
|
|
@ -574,7 +574,7 @@ export const RagInventoryPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{(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>
|
||||
</Panel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const _ProfileTab: React.FC<_ProfileTabProps> = ({ currentUser, refetchUser, onS
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="card" title={t('Konto')}>
|
||||
<Panel variant="card" title={t('Konto')} id="settings-account">
|
||||
{!isProfileEditing ? (
|
||||
<>
|
||||
<div className={styles.settingRow}>
|
||||
|
|
@ -104,7 +104,7 @@ const _ProfileTab: React.FC<_ProfileTabProps> = ({ currentUser, refetchUser, onS
|
|||
</>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel variant="card" title={t('Applikation')}>
|
||||
<Panel variant="card" title={t('Applikation')} id="settings-application">
|
||||
<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('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;
|
||||
}, [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 (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Spracheingabe')} id="settings-voice">
|
||||
{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>}
|
||||
|
||||
|
|
@ -379,10 +379,10 @@ const NeutralizationMappingsTab: React.FC = () => {
|
|||
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 (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Platzhaltermappings lokal')} id="settings-mappings">
|
||||
{error && <div className={styles.errorMessage}>{error}</div>}
|
||||
|
||||
<section className={styles.section}>
|
||||
|
|
@ -533,11 +533,11 @@ const MfaSettingsTab: React.FC = () => {
|
|||
};
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
|
||||
{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();
|
||||
|
||||
return (
|
||||
<Panel variant="card" title={t('Darstellung')}>
|
||||
<Panel variant="card" title={t('Darstellung')} id="settings-appearance">
|
||||
<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.settingControl}>
|
||||
|
|
@ -693,7 +693,7 @@ const _PrivacyTab: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="card" title={t('Datenschutz')}>
|
||||
<Panel variant="card" title={t('Datenschutz')} id="settings-privacy">
|
||||
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
|
||||
{t('Datenschutzbeschreibung')}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export const StorePage: React.FC = () => {
|
|||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
{subscriptionInfo && subscriptionInfo.plan && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Abonnement')} id="store-subscription">
|
||||
<div className={styles.subscriptionBanner}>
|
||||
<span>{t('Plan:')} <strong>{subscriptionInfo.plan}</strong></span>
|
||||
<span className={styles.bannerSeparator}>
|
||||
|
|
@ -188,25 +188,25 @@ export const StorePage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="store-error">
|
||||
<div className={styles.error}>{error}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="store-loading">
|
||||
<div className={styles.loading}>
|
||||
{t('Lade Features…')}
|
||||
</div>
|
||||
</Panel>
|
||||
) : features.length === 0 ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Keine Features')} id="store-empty">
|
||||
<div className={styles.empty}>
|
||||
{t('Keine Features im Store verfügbar.')}
|
||||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Features')} id="store-features">
|
||||
<div className={styles.grid}>
|
||||
{features.map((feature) => (
|
||||
<FeatureCard
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="access-hub-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -338,7 +338,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="access-hub-toolbar">
|
||||
<div className={hubStyles.filters}>
|
||||
{/* Filter dropdowns only shown in list view - hierarchy shows everything */}
|
||||
{viewMode === 'list' && (
|
||||
|
|
@ -441,7 +441,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
onOpenDetail={handleOpenDetail}
|
||||
/>
|
||||
) : !selectedMandateId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Mandant ausgewählt')} id="access-hub-no-mandate">
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
|
|
@ -452,7 +452,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
</Panel>
|
||||
) : (
|
||||
<>
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Übersicht')} id="access-hub-overview">
|
||||
<div className={hubStyles.overviewRow}>
|
||||
<div className={hubStyles.statsCard}>
|
||||
<FaChartBar className={hubStyles.statsIcon} />
|
||||
|
|
@ -507,7 +507,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Feature-Instanzen')} id="access-hub-instances">
|
||||
<section className={hubStyles.section}>
|
||||
<h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2>
|
||||
{loading && filteredInstances.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ const StatsTab: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="db-stats-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Datenbank')}</label>
|
||||
|
|
@ -334,7 +334,7 @@ const StatsTab: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Zusammenfassung')} id="db-stats-summary">
|
||||
<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('{tables} Tabellen', { tables: totals.tables })}</span>
|
||||
|
|
@ -344,7 +344,7 @@ const StatsTab: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Tabellen')} id="db-stats-table">
|
||||
<FormGeneratorTable
|
||||
data={visibleData}
|
||||
columns={columns}
|
||||
|
|
@ -634,7 +634,7 @@ const OrphansTab: React.FC = () => {
|
|||
<>
|
||||
<ConfirmDialog />
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="orphans-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Datenbank')}</label>
|
||||
|
|
@ -680,7 +680,7 @@ const OrphansTab: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{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)' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} />
|
||||
{t('{count} verwaiste Einträge in {relations} Beziehungen gefunden', {
|
||||
|
|
@ -691,7 +691,7 @@ const OrphansTab: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Verwaiste Einträge')} id="orphans-table">
|
||||
<FormGeneratorTable
|
||||
data={visibleData}
|
||||
columns={columns}
|
||||
|
|
@ -1170,7 +1170,7 @@ const MigrationTab: React.FC = () => {
|
|||
<>
|
||||
<ConfirmDialog />
|
||||
|
||||
<Panel variant="card" title={t('Backup')}>
|
||||
<Panel variant="card" title={t('Backup')} id="db-backup">
|
||||
<section>
|
||||
<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')}
|
||||
|
|
@ -1252,7 +1252,7 @@ const MigrationTab: React.FC = () => {
|
|||
</section>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Restore')}>
|
||||
<Panel variant="card" title={t('Restore')} id="db-restore">
|
||||
<section>
|
||||
{!uploadedFile ? (
|
||||
<div
|
||||
|
|
@ -1668,7 +1668,7 @@ const LegacyCleanupTab: React.FC = () => {
|
|||
<>
|
||||
<ConfirmDialog />
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="legacy-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.headerActions}>
|
||||
<button className={styles.secondaryButton} onClick={_fetchLegacy} disabled={loading}>
|
||||
|
|
@ -1689,7 +1689,7 @@ const LegacyCleanupTab: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{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)' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }} />
|
||||
{t('{count} Legacy-Tabellen in {dbs} Datenbanken ({rows} Zeilen, {size})', {
|
||||
|
|
@ -1699,7 +1699,7 @@ const LegacyCleanupTab: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Legacy-Tabellen')} id="legacy-table">
|
||||
<FormGeneratorTable
|
||||
data={visibleData}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export const AdminDemoConfigPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="demo-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button className={styles.secondaryButton} onClick={_fetchConfigs} disabled={loading}>
|
||||
<FaSync /> {t('Aktualisieren')}
|
||||
|
|
@ -130,13 +130,13 @@ export const AdminDemoConfigPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="demo-error">
|
||||
<div className={demoStyles.errorBanner}>{error}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{lastResult && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Ergebnis')} id="demo-result">
|
||||
<div className={lastResult.status === 'ok' ? demoStyles.successBanner : demoStyles.errorBanner}>
|
||||
<strong>{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:</strong>{' '}
|
||||
{lastResult.status === 'ok' ? (
|
||||
|
|
@ -151,7 +151,7 @@ export const AdminDemoConfigPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Demo-Konfigurationen')} id="demo-configs">
|
||||
{loading && configs.length === 0 ? (
|
||||
<div className={demoStyles.loadingState}>{t('Lade…')}</div>
|
||||
) : configs.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="feature-access-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>{t('Fehler')}: {error}</p>
|
||||
|
|
@ -300,7 +300,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="feature-access-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -348,7 +348,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{features.length > 0 ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Verfügbare Features')} id="feature-access-features">
|
||||
<div className={styles.infoBox}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>{t('Verfügbare Features')} </span>
|
||||
|
|
@ -361,7 +361,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : 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))' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
|
|
@ -382,7 +382,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
) : null}
|
||||
|
||||
{!selectedMandateId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Mandant ausgewählt')} id="feature-access-no-mandate">
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
|
|
@ -392,7 +392,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Feature-Instanzen')} id="feature-access-table">
|
||||
<FormGeneratorTable
|
||||
data={instances}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
loading,
|
||||
error,
|
||||
fetchFeatures,
|
||||
fetchInstanceUsers,
|
||||
addUserToInstance,
|
||||
removeUserFromInstance,
|
||||
updateInstanceUserRoles,
|
||||
|
|
@ -403,7 +402,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="fi-users-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -429,7 +428,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="fi-users-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -488,7 +487,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{selectedOption && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Ausgewählte Instanz')} id="fi-users-selection">
|
||||
<div className={styles.infoBox}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
|
|
@ -504,7 +503,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{selectedInstance && instanceRoles.length > 0 && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Verfügbare Rollen')} id="fi-users-roles">
|
||||
<div className={styles.infoBox}>
|
||||
<span>{t('Verfügbare Rollen')} </span>
|
||||
{instanceRoles.map((r, i) => (
|
||||
|
|
@ -518,7 +517,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{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))' }}>
|
||||
<span>⚠️ </span>
|
||||
<span>{t('Diese Instanz hat noch keine')}</span>
|
||||
|
|
@ -527,7 +526,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!selectedCombinedKey ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Keine Feature-Instanz ausgewählt')} id="fi-users-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanz ausgewählt')}</h3>
|
||||
|
|
@ -539,7 +538,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Benutzer')} id="fi-users-table">
|
||||
<FormGeneratorTable
|
||||
data={instanceUsers}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="feature-roles-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>{error}</p>
|
||||
|
|
@ -314,7 +314,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="feature-roles-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -359,7 +359,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{selectedFeatureCode && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Feature-Template-Rollen')} id="feature-roles-info">
|
||||
<div className={styles.infoBox}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
|
|
@ -371,7 +371,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!selectedFeatureCode ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Feature ausgewählt')} id="feature-roles-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
|
||||
|
|
@ -381,7 +381,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Feature-Rollen')} id="feature-roles-table">
|
||||
<FormGeneratorTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="invitations-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -244,7 +244,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="invitations-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -305,7 +305,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{!selectedMandateId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Mandant ausgewählt')} id="invitations-no-mandate">
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
|
|
@ -315,7 +315,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Einladungen')} id="invitations-table">
|
||||
<FormGeneratorTable
|
||||
data={invitations}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -871,7 +871,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
const isBusy = progress !== null;
|
||||
|
||||
return (
|
||||
<StackLayout variant="table" className="" style={{ position: 'relative' }}>
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Header>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>{t('UI-Sprachen')}</h1>
|
||||
|
|
@ -884,7 +884,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<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' }}>
|
||||
<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} />
|
||||
|
|
@ -925,7 +925,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table" className="" style={{ position: 'relative' }}>
|
||||
<Panel variant="table" title={t('Sprachen')} id="languages-table" className="" style={{ position: 'relative' }}>
|
||||
<FormGeneratorTable
|
||||
data={displayRows}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="logs-toolbar">
|
||||
<div className={logStyles.controls}>
|
||||
<div className={logStyles.loadGroup}>
|
||||
<label className={logStyles.controlLabel}>{t('Letzte')}</label>
|
||||
|
|
@ -167,7 +167,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="logs-error">
|
||||
<div
|
||||
className={styles.infoBox}
|
||||
style={{
|
||||
|
|
@ -181,7 +181,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Logs')} id="logs-editor">
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
className={logStyles.logContainer}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler beim Laden')} id="role-permissions-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -277,7 +277,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="role-permissions-toolbar">
|
||||
<div className={styles.filterBar}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Mandant')}</label>
|
||||
|
|
@ -311,7 +311,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="role-permissions-info">
|
||||
<div className={styles.infoBox}>
|
||||
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
|
||||
<span>
|
||||
|
|
@ -324,7 +324,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{loading && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Lade Rollen')} id="role-permissions-loading">
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Rollen')}</span>
|
||||
|
|
@ -333,7 +333,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!loading && roles.length === 0 && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Keine Rollen gefunden')} id="role-permissions-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
<p>{t('Keine Rollen gefunden')}</p>
|
||||
|
|
@ -349,7 +349,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!loading && roles.length > 0 && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Rollen')} id="role-permissions-list">
|
||||
<div className={styles.rolesList}>
|
||||
{roles.map(role => (
|
||||
<div key={role.id} className={styles.roleCard}>
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="mandate-roles-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -292,7 +292,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="mandate-roles-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -348,7 +348,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{selectedMandateId && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('System-Templates')} id="mandate-roles-info">
|
||||
<div className={styles.infoBox}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
|
|
@ -362,7 +362,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!selectedMandateId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Mandant ausgewählt')} id="mandate-roles-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
|
|
@ -372,7 +372,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Rollen')} id="mandate-roles-table">
|
||||
<FormGeneratorTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="mandates-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -224,7 +224,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="mandates-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -250,7 +250,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Mandanten')} id="mandates-table">
|
||||
<FormGeneratorTable
|
||||
data={mandates}
|
||||
columns={columns}
|
||||
|
|
|
|||
204
src/pages/admin/AdminSessionsPage.tsx
Normal file
204
src/pages/admin/AdminSessionsPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -84,8 +84,6 @@ interface UserAccessOverview {
|
|||
resourceAccess: AccessEntry[];
|
||||
}
|
||||
|
||||
type TabId = 'overview' | 'ui' | 'data' | 'resources';
|
||||
|
||||
export const AdminUserAccessOverviewPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
|
|
@ -588,22 +586,22 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
{
|
||||
id: 'overview',
|
||||
label: t('Übersicht'),
|
||||
render: () => <Panel variant="card">{renderOverviewTab()}</Panel>,
|
||||
render: () => <Panel variant="card" title={t('Übersicht')} id="access-tab-overview">{renderOverviewTab()}</Panel>,
|
||||
},
|
||||
{
|
||||
id: 'ui',
|
||||
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',
|
||||
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',
|
||||
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]);
|
||||
|
|
@ -612,7 +610,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="access-overview-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -640,7 +638,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="access-overview-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -678,7 +676,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{!selectedUserId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Benutzer auswählen')} id="access-overview-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Benutzer auswählen')}</h3>
|
||||
|
|
@ -688,7 +686,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : loading ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Lade Zugriffsübersicht')} id="access-overview-loading">
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Zugriffsübersicht')}</span>
|
||||
|
|
@ -696,7 +694,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
</Panel>
|
||||
) : overview ? (
|
||||
<>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Benutzerinfo')} id="access-overview-user">
|
||||
<div className={styles.infoBox}>
|
||||
<strong>{overview.user.fullName || overview.user.username}</strong>
|
||||
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="user-mandates-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -285,7 +285,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="user-mandates-toolbar">
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
|
|
@ -328,7 +328,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{!selectedMandateId ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kein Mandant ausgewählt')} id="user-mandates-empty">
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
|
|
@ -338,7 +338,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Mandanten-Mitglieder')} id="user-mandates-table">
|
||||
<FormGeneratorTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="users-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -195,7 +195,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="users-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -228,7 +228,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Benutzer')} id="users-table">
|
||||
<FormGeneratorTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<Panel variant="card" title={t('Hierarchie')}>
|
||||
<Panel variant="card" title={t('Hierarchie')} id="hierarchy-loading">
|
||||
<div className={hierarchyStyles.hierarchyLoading}>
|
||||
<span className={hierarchyStyles.spinner} />
|
||||
<span>{t('Lade Hierarchie und Benutzer')}</span>
|
||||
|
|
@ -201,7 +201,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
|
||||
if (mandates.length === 0) {
|
||||
return (
|
||||
<Panel variant="card" title={t('Hierarchie')}>
|
||||
<Panel variant="card" title={t('Hierarchie')} id="hierarchy-empty">
|
||||
<div className={hierarchyStyles.emptyHierarchy}>
|
||||
{t('Keine Mandanten vorhanden. Legen Sie unter „Mandanten verwalten“ einen Mandanten an.')}
|
||||
</div>
|
||||
|
|
@ -210,7 +210,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Panel variant="card" title={t('Hierarchie')}>
|
||||
<Panel variant="card" title={t('Hierarchie')} id="hierarchy-tree">
|
||||
<div className={hierarchyStyles.hierarchyRoot}>
|
||||
{mandates.map((mandate) => {
|
||||
const mandateId = mandate.id;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
|
|||
|
||||
if (roles.length === 0) {
|
||||
return (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Keine Rollen in dieser Instanz')} id="matrix-empty">
|
||||
<div className={matrixStyles.empty}>
|
||||
<p>{t('Keine Rollen in dieser Instanz')}</p>
|
||||
</div>
|
||||
|
|
@ -62,7 +62,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Berechtigungsmatrix')} id="matrix-table">
|
||||
<div className={matrixStyles.tableWrap}>
|
||||
<table className={matrixStyles.table}>
|
||||
<thead>
|
||||
|
|
@ -140,7 +140,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
|
|||
</table>
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="matrix-toolbar">
|
||||
<div className={matrixStyles.footer}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ export { AdminLogsPage } from './AdminLogsPage';
|
|||
export { AdminLanguagesPage } from './AdminLanguagesPage';
|
||||
export { AdminDemoConfigPage } from './AdminDemoConfigPage';
|
||||
export { AdminDatabaseHealthPage } from './AdminDatabaseHealthPage';
|
||||
export { AdminSessionsPage } from './AdminSessionsPage';
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="invitation-error">
|
||||
<div className={styles.errorContainer} style={{
|
||||
flexDirection: 'row', padding: '12px 16px', marginBottom: '16px',
|
||||
background: '#fef2f2', borderRadius: '8px', fontSize: '13px', justifyContent: 'flex-start',
|
||||
|
|
@ -343,7 +343,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{!dispatchResults && (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Schritte')} id="invitation-steps-toolbar">
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
|
||||
{stepLabels.map((label, idx) => {
|
||||
const s = idx + 1;
|
||||
|
|
@ -369,7 +369,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="wizard">
|
||||
<Panel variant="wizard" title={t('Einladungs-Wizard')} id="invitation-wizard">
|
||||
{/* ── STEP 1: Invite type ── */}
|
||||
{step === 1 && (
|
||||
<div style={_cardStyle}>
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
{error && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="mandate-error">
|
||||
<div style={{
|
||||
padding: '12px 16px', background: 'var(--error-bg, #fef2f2)', color: 'var(--danger-color, #dc2626)',
|
||||
borderRadius: '8px', fontSize: '13px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
|
|
@ -697,11 +697,11 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Schritte')} id="mandate-steps-toolbar">
|
||||
{renderStepIndicator()}
|
||||
</Panel>
|
||||
|
||||
<Panel variant="wizard">
|
||||
<Panel variant="wizard" title={t('Mandanten-Verwaltung')} id="mandate-wizard">
|
||||
{step === 1 && (
|
||||
<div style={cardStyle}>
|
||||
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('Mandant auswählen oder erstellen')}</h3>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Schritte')} id="feature-instance-steps-toolbar">
|
||||
<div className={wizardStyles.steps}>
|
||||
{steps.map((s, i) => (
|
||||
<div
|
||||
|
|
@ -174,7 +174,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
|||
</Panel>
|
||||
|
||||
<div className={styles.modalContent}>
|
||||
<Panel variant="wizard">
|
||||
<Panel variant="wizard" title={t('Neue Feature-Instanz')} id="feature-instance-wizard">
|
||||
{currentStepId === 'create' && (
|
||||
<div className={wizardStyles.stepContent}>
|
||||
<div className={wizardStyles.fieldGroup}>
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="connections-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<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>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="connections-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
|
|
@ -349,6 +349,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
{syncBanner && (
|
||||
<Panel
|
||||
variant="card"
|
||||
id="connections-sync-banner"
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
collapseKey="connections-sync-banner"
|
||||
|
|
@ -376,7 +377,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Verbindungen')} id="connections-table">
|
||||
<FormGeneratorTable
|
||||
data={connections}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ export const FilesPage: React.FC = () => {
|
|||
);
|
||||
|
||||
const _treePanelContent = (
|
||||
<Panel variant="card" fill>
|
||||
<Panel variant="card" title={t('Ordnerstruktur')} id="files-tree" fill>
|
||||
<FormGeneratorTree
|
||||
key={`own-${treeKey}`}
|
||||
provider={provider}
|
||||
|
|
@ -481,7 +481,7 @@ export const FilesPage: React.FC = () => {
|
|||
);
|
||||
|
||||
const _tablePanelContent = (
|
||||
<Panel variant="table" actions={_tablePanelActions}>
|
||||
<Panel variant="table" title={t('Dateien')} id="files-table" actions={_tablePanelActions}>
|
||||
<FormGeneratorTable
|
||||
data={tableFiles || []}
|
||||
columns={columns}
|
||||
|
|
@ -586,7 +586,7 @@ export const FilesPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="files-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<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>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="files-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<div className={fileStyles.toggleGroup}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export const PromptsPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="prompts-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<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>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="prompts-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
|
|
@ -214,7 +214,7 @@ export const PromptsPage: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Prompts')} id="prompts-table">
|
||||
<FormGeneratorTable
|
||||
data={prompts}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="admin-subscriptions-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.button} ${styles.buttonPrimary}`}
|
||||
|
|
@ -211,7 +211,7 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
</button>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Abonnemente')} id="admin-subscriptions-table">
|
||||
<FormGeneratorTable
|
||||
data={subscriptions}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -643,7 +643,7 @@ export const BillingAdmin: React.FC = () => {
|
|||
id: 'settings',
|
||||
label: t('Einstellungen'),
|
||||
render: () => (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Einstellungen')} id="billing-settings">
|
||||
<SettingsEditor
|
||||
settings={settings}
|
||||
onSave={handleSaveSettings}
|
||||
|
|
@ -658,16 +658,16 @@ export const BillingAdmin: React.FC = () => {
|
|||
render: () => (
|
||||
<>
|
||||
{isSysAdmin && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Guthaben aufladen')} id="billing-credit-adder">
|
||||
<CreditAdder onAddCredit={_handleAddCredit} />
|
||||
</Panel>
|
||||
)}
|
||||
{showStripeForMandateAdmin && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Stripe-Aufladung')} id="billing-stripe-topup">
|
||||
<MandateStripeTopUp mandateId={selectedMandateId} createCheckout={createCheckout} />
|
||||
</Panel>
|
||||
)}
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Kontenübersicht')} id="billing-accounts-overview">
|
||||
<AccountsOverview accounts={accounts} users={users} loading={loading} />
|
||||
</Panel>
|
||||
</>
|
||||
|
|
@ -701,7 +701,7 @@ export const BillingAdmin: React.FC = () => {
|
|||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
{stripeReturnMessage && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="billing-stripe-message">
|
||||
<div
|
||||
className={
|
||||
stripeReturnMessage.type === 'success' ? styles.successMessage : styles.errorMessage
|
||||
|
|
@ -717,7 +717,7 @@ export const BillingAdmin: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Mandant')} id="billing-mandate-toolbar">
|
||||
<MandateSelector
|
||||
mandates={mandateList}
|
||||
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>
|
||||
</Panel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ export const BillingDataView: React.FC = () => {
|
|||
id: 'overview',
|
||||
label: t('Übersicht'),
|
||||
render: () => (
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Übersicht')} id="billing-stats-overview">
|
||||
<FormGeneratorReport
|
||||
loading={statsLoading || dashboardLoading}
|
||||
sections={overviewSections}
|
||||
|
|
@ -686,7 +686,7 @@ export const BillingDataView: React.FC = () => {
|
|||
id: 'diagrams',
|
||||
label: t('Diagramme'),
|
||||
render: () => (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Diagramme')} id="billing-stats-diagrams">
|
||||
<div className={styles.diagramControlsEnd}>
|
||||
<div className={styles.chartModeToggle}>
|
||||
<button
|
||||
|
|
@ -736,7 +736,7 @@ export const BillingDataView: React.FC = () => {
|
|||
id: 'transactions',
|
||||
label: t('Transaktionen'),
|
||||
render: () => (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Transaktionen')} id="billing-stats-transactions-table">
|
||||
{transactionsError && (
|
||||
<div className={styles.errorMessage}>
|
||||
{transactionsError}
|
||||
|
|
@ -793,7 +793,7 @@ export const BillingDataView: React.FC = () => {
|
|||
</div>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="billing-stats-toolbar">
|
||||
<div className={styles.contextToolbar}>
|
||||
<label className={styles.contextToolbarLabel}>{t('Kontext:')}</label>
|
||||
<select
|
||||
|
|
@ -817,7 +817,7 @@ export const BillingDataView: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{checkoutMessage && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="billing-stats-checkout-message">
|
||||
<div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}>
|
||||
{checkoutMessage.text}
|
||||
{(successParam || canceledParam) && (
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
|
|||
<>
|
||||
<Panel
|
||||
variant="card"
|
||||
id="billing-mandate-balances"
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
collapseKey="billing-mandate-balances"
|
||||
|
|
@ -248,6 +249,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
|
|||
|
||||
<Panel
|
||||
variant="card"
|
||||
id="billing-mandate-transactions"
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
collapseKey="billing-mandate-transactions"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const BillingNav: React.FC = () => {
|
|||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Navigation')} id="billing-nav-toolbar">
|
||||
<nav className={styles.billingNav} aria-label={t('Billing-Navigation')}>
|
||||
<NavLink
|
||||
to="/billing"
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
|
|||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{checkoutMessage && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="subscription-checkout-message">
|
||||
<div className={checkoutMessage.type === 'success' ? styles.successMessage : styles.errorMessage}>
|
||||
{checkoutMessage.text}
|
||||
</div>
|
||||
|
|
@ -598,14 +598,14 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
|
|||
)}
|
||||
|
||||
{(error || actionError) && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="subscription-error">
|
||||
<div className={styles.errorMessage}>
|
||||
{actionError || error}
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="card" title={t('Aktuelles Abonnement')}>
|
||||
<Panel variant="card" title={t('Aktuelles Abonnement')} id="subscription-current">
|
||||
{subscription ? (
|
||||
<_SubInfoCard
|
||||
sub={subscription}
|
||||
|
|
@ -628,7 +628,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
|
|||
</Panel>
|
||||
|
||||
{scheduled && (
|
||||
<Panel variant="card" title={t('Geplanter Nachfolgeplan')}>
|
||||
<Panel variant="card" title={t('Geplanter Nachfolgeplan')} id="subscription-scheduled">
|
||||
<_SubInfoCard
|
||||
sub={scheduled}
|
||||
plan={null}
|
||||
|
|
@ -642,7 +642,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
|
|||
)}
|
||||
|
||||
{!_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 ? (
|
||||
<div className={styles.noData}>{t('Keine Pläne verfügbar')}</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export const CommcoachAssistantView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="form">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="commcoach-assistant-toolbar">
|
||||
<div className={styles.wizardHeader}>
|
||||
<h2>{t('Neues Modul erstellen')}</h2>
|
||||
<div className={styles.wizardHeaderRight}>
|
||||
|
|
@ -105,7 +105,7 @@ export const CommcoachAssistantView: React.FC = () => {
|
|||
|
||||
{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}>
|
||||
{step === 'type' && (
|
||||
<div className={styles.wizardStep}>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="dashboard">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="commcoach-dashboard-loading">
|
||||
<div className={styles.loading}>{t('Dashboard wird geladen…')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -68,7 +68,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="dashboard">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="commcoach-dashboard-error">
|
||||
<div className={styles.error}>{error}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -80,7 +80,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="dashboard">
|
||||
<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>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -91,7 +91,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="dashboard">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Übersicht')} id="commcoach-dashboard-kpis">
|
||||
<div className={styles.kpiGrid}>
|
||||
<div className={styles.kpiCard}>
|
||||
<div className={styles.kpiValue}>{dashboard.streakDays}</div>
|
||||
|
|
@ -125,6 +125,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
<Panel
|
||||
variant="card"
|
||||
title={t('Aktive Coaching-Themen')}
|
||||
id="commcoach-dashboard-active-topics"
|
||||
collapsible
|
||||
collapseKey="commcoach-dashboard-active-topics"
|
||||
actions={
|
||||
|
|
@ -181,6 +182,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
})
|
||||
: t('Auszeichnungen')
|
||||
}
|
||||
id="commcoach-dashboard-badges"
|
||||
collapsible
|
||||
collapseKey="commcoach-dashboard-badges"
|
||||
>
|
||||
|
|
@ -200,6 +202,7 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
<Panel
|
||||
variant="card"
|
||||
title={t('Tipp des Tages')}
|
||||
id="commcoach-dashboard-tip"
|
||||
collapsible
|
||||
collapseKey="commcoach-dashboard-tip"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export const CommcoachModulesView: React.FC = () => {
|
|||
<>
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Filter')} id="commcoach-modules-toolbar">
|
||||
<div className={styles.modulesHeader}>
|
||||
<h2>{t('Module')}</h2>
|
||||
<div className={styles.modulesFilters}>
|
||||
|
|
@ -181,12 +181,12 @@ export const CommcoachModulesView: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{loading && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden...')} id="commcoach-modules-loading">
|
||||
<div className={styles.loading}>{t('Laden...')}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Module')} id="commcoach-modules-list">
|
||||
<div className={styles.modulesList}>
|
||||
{filteredModules.map(mod => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ export const CommcoachSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden...')} id="commcoach-session-loading">
|
||||
<div className={sessionStyles.loading}>{t('Laden...')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -294,7 +294,7 @@ export const CommcoachSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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' }}>
|
||||
<h3>{t('Keine aktive Session')}</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}>
|
||||
|
|
@ -320,7 +320,7 @@ export const CommcoachSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Session starten')} id="commcoach-session-start">
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<h3>{coach.selectedContext?.title || t('Modul')}</h3>
|
||||
{coach.selectedContext?.description && (
|
||||
|
|
@ -394,7 +394,7 @@ export const CommcoachSessionView: React.FC = () => {
|
|||
collapsible: true,
|
||||
collapseKey: 'commcoach-session-udb',
|
||||
content: (
|
||||
<Panel variant="card" title={t('Datenquellen')}>
|
||||
<Panel variant="card" title={t('Datenquellen')} id="commcoach-session-data-sources">
|
||||
<UnifiedDataBar
|
||||
context={_udbContext}
|
||||
activeTab={udbTab}
|
||||
|
|
@ -411,7 +411,7 @@ export const CommcoachSessionView: React.FC = () => {
|
|||
defaultSize: 78,
|
||||
minSize: 40,
|
||||
content: (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Coaching-Session')} id="commcoach-session-editor">
|
||||
<div className={styles.sessionRoot}>
|
||||
{/* Session Header */}
|
||||
<div className={styles.sessionHeader}>
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
render: () => (
|
||||
<>
|
||||
{loading && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="commcoach-settings-loading">
|
||||
<div className={styles.loading}>{t('Einstellungen werden geladen')}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
|
@ -236,7 +236,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
{error && <div className={styles.error}>{error}</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' }}>
|
||||
{t('Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.')}
|
||||
</p>
|
||||
|
|
@ -245,7 +245,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
</Link>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Erinnerungen')}>
|
||||
<Panel variant="card" title={t('Erinnerungen')} id="commcoach-settings-reminders">
|
||||
<div className={styles.field}>
|
||||
<label className={styles.checkboxLabel}>
|
||||
<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>}
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="commcoach-settings-personas-toolbar">
|
||||
<div className={styles.personasHeader}>
|
||||
<div>
|
||||
<h3 className={styles.sectionTitle} style={{ margin: 0 }}>{t('Gespraechspartner verwalten')}</h3>
|
||||
|
|
@ -297,7 +297,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Gespraechspartner')} id="commcoach-settings-personas-table">
|
||||
<FormGeneratorTable
|
||||
data={personas}
|
||||
columns={personaColumns}
|
||||
|
|
|
|||
|
|
@ -129,14 +129,14 @@ const ConfigTab: React.FC = () => {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="neutralization-config-loading">
|
||||
<div className={styles.loading}>{t('Konfiguration wird geladen')}</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
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 && (
|
||||
<div className={styles.errorMessage}>
|
||||
<span>{error}</span>
|
||||
|
|
@ -485,7 +485,7 @@ const PlaygroundTab: React.FC = () => {
|
|||
return (
|
||||
<>
|
||||
<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 }}>
|
||||
{t('Laden Sie eine Datei hoch, um sie zu neutralisieren.')}
|
||||
</p>
|
||||
|
|
@ -525,7 +525,7 @@ const PlaygroundTab: React.FC = () => {
|
|||
</div>
|
||||
</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}>
|
||||
<label htmlFor="input-text">{t('2. Oder Text einfügen')}</label>
|
||||
<textarea
|
||||
|
|
@ -581,7 +581,7 @@ const PlaygroundTab: React.FC = () => {
|
|||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('SharePoint-Dateien')}>
|
||||
<Panel variant="card" title={t('SharePoint-Dateien')} id="neutralization-playground-sharepoint">
|
||||
<div className={styles.formRow}>
|
||||
<label>{t('3. Oder SharePoint-Dateien neutralisieren')}</label>
|
||||
<div className={styles.formRow} style={{ gap: '0.75rem', marginTop: 0 }}>
|
||||
|
|
@ -753,8 +753,8 @@ export const NeutralizationView: React.FC = () => {
|
|||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{ id: 'config', label: t('Konfiguration'), content: <ConfigTab /> },
|
||||
{ id: 'playground', label: t('Spielwiese'), content: <PlaygroundTab /> },
|
||||
{ id: 'config', label: t('Konfiguration'), render: () => <ConfigTab /> },
|
||||
{ id: 'playground', label: t('Spielwiese'), render: () => <PlaygroundTab /> },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const RealEstateInstanceRolesPlaceholder: React.FC = () => {
|
|||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Rollen & Rechte')}</h1>
|
||||
</StackLayout.Header>
|
||||
<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 }}>
|
||||
{t('Die Verwaltung von Rollen und Benutzern für diese Instanz erfolgt in der Administration.')}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -12,18 +12,21 @@ import { PekProvider } from '../../../contexts/PekContext';
|
|||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
import { StackLayout } from '../../../components/Layout/StackLayout';
|
||||
import { Panel } from '../../../components/Layout/Panel';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import PekLocationInput from './pek/PekLocationInput';
|
||||
import PekMapView from './pek/PekMapView';
|
||||
|
||||
function RealEstatePekViewContent() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<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 />
|
||||
</Panel>
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Karte')} id="realestate-pek-map">
|
||||
<PekMapView />
|
||||
</Panel>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const PekLocationInput: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Standortsuche')} id="pek-location-toolbar">
|
||||
<div className={styles.locationInputContainer}>
|
||||
<div className={styles.fieldsRow}>
|
||||
<div className={styles.fieldWrapper}>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const PekMapView: React.FC = () => {
|
|||
|
||||
return (
|
||||
<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}>
|
||||
<label className={styles.checkboxLabel}>
|
||||
<input
|
||||
|
|
@ -49,7 +49,7 @@ const PekMapView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Karte')} id="pek-map-editor">
|
||||
<div className={styles.mapContainer}>
|
||||
<MapView
|
||||
parcels={parcelGeometries}
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
const _treePane = (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Tickets')} id="redmine-browser-tickets-table">
|
||||
<div className={styles.browserToolbar}>
|
||||
<span>
|
||||
{t('{rows} Zeilen sichtbar -- {orphans} Orphan-Tickets', {
|
||||
|
|
@ -384,11 +384,11 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
);
|
||||
|
||||
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>
|
||||
</Panel>
|
||||
) : selectedId === ORPHAN_ROOT_ID ? (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Orphan User Story')} id="redmine-browser-editor-orphan">
|
||||
<div className={styles.browserToolbar}>
|
||||
{t('Virtueller "Orphan User Story"-Knoten -- enthaelt {count} Tickets ohne Verbindung.', {
|
||||
count: forest.orphanCount,
|
||||
|
|
@ -396,7 +396,7 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Ticket-Editor')} id="redmine-browser-editor">
|
||||
<RedmineTicketEditor
|
||||
instanceId={instanceId!}
|
||||
ticketId={selectedId}
|
||||
|
|
@ -416,7 +416,7 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<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>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -428,7 +428,7 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<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>
|
||||
<h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Ticket-Browser')}</h2>
|
||||
|
|
@ -448,12 +448,12 @@ export const RedmineBrowserView: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{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>
|
||||
</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.filterGroup}>
|
||||
<label>{t('Zeitraum (letzte Aenderung)')}</label>
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export const RedmineSettingsView: React.FC = () => {
|
|||
<h1 style={{ fontSize: '1.35rem', fontWeight: 600, margin: 0 }}>{t('Redmine -- Einstellungen')}</h1>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="redmine-settings-loading">
|
||||
<div className={styles.loading}>{t('Einstellungen werden geladen ...')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -222,6 +222,7 @@ export const RedmineSettingsView: React.FC = () => {
|
|||
<Panel
|
||||
variant="card"
|
||||
title={t('Verbindung')}
|
||||
id="redmine-settings-connection"
|
||||
collapsible
|
||||
collapseKey={instanceId ? `redmine-settings:connection:${instanceId}` : undefined}
|
||||
>
|
||||
|
|
@ -318,6 +319,7 @@ export const RedmineSettingsView: React.FC = () => {
|
|||
<Panel
|
||||
variant="card"
|
||||
title={t('Mirror-Sync')}
|
||||
id="redmine-settings-sync"
|
||||
collapsible
|
||||
collapseKey={instanceId ? `redmine-settings:sync:${instanceId}` : undefined}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ export const RedmineStatsView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -387,7 +387,7 @@ export const RedmineStatsView: React.FC = () => {
|
|||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Statistik')} id="redmine-stats-header-toolbar">
|
||||
<div>
|
||||
<h2 className={styles.heading} style={{ margin: 0 }}>{t('Redmine -- Statistik')}</h2>
|
||||
<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>}
|
||||
{error && <div className={styles.alertErr}>{error}</div>}
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Statistik')} id="redmine-stats-report">
|
||||
<FormGeneratorReport
|
||||
title={stats
|
||||
? t('{total} Tickets ({open} offen, {closed} geschlossen)', {
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export const RedmineTicketEditor: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<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' }}>
|
||||
{tracker && (() => {
|
||||
const sty = getTrackerStyle(tracker.name);
|
||||
|
|
@ -192,13 +192,13 @@ export const RedmineTicketEditor: React.FC<Props> = ({
|
|||
</Panel>
|
||||
|
||||
{(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>}
|
||||
{successMsg && <div className={styles.alertOk} style={{ marginBottom: 0 }}>{successMsg}</div>}
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Ticket bearbeiten')} id="redmine-ticket-editor-form">
|
||||
<div className={styles.editorGrid}>
|
||||
<label>{t('Titel')}</label>
|
||||
<input className={styles.input} value={subject} onChange={e => setSubject(e.target.value)} />
|
||||
|
|
@ -294,7 +294,7 @@ export const RedmineTicketEditor: React.FC<Props> = ({
|
|||
</div>
|
||||
</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%' }}>
|
||||
<button className={styles.btnSecondary} onClick={_load} disabled={saving}>
|
||||
{t('Zuruecksetzen')}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export const TeamsbotAssistantView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="form">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="teamsbot-assistant-toolbar">
|
||||
<div className={styles.wizardHeader}>
|
||||
<h2>{t('Neues Meeting starten')}</h2>
|
||||
<div className={styles.wizardHeaderRight}>
|
||||
|
|
@ -187,7 +187,7 @@ export const TeamsbotAssistantView: React.FC = () => {
|
|||
|
||||
{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}>
|
||||
{step === 'module' && (
|
||||
<div className={styles.wizardStep}>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export const TeamsbotDashboardView: React.FC = () => {
|
|||
<StackLayout.Body>
|
||||
{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}>
|
||||
<div className={styles.tbDashHeroText}>
|
||||
<h1 className={styles.tbDashTitle}>{t('Teams Bot')}</h1>
|
||||
|
|
@ -200,7 +200,7 @@ export const TeamsbotDashboardView: React.FC = () => {
|
|||
</header>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Kennzahlen')} id="teamsbot-dashboard-kpis">
|
||||
<section className={styles.tbDashKpiGrid} aria-label={t('Kennzahlen')}>
|
||||
<div className={styles.tbDashKpiCard}>
|
||||
<div className={styles.tbDashKpiValue}>{modules.length}</div>
|
||||
|
|
@ -222,7 +222,7 @@ export const TeamsbotDashboardView: React.FC = () => {
|
|||
</section>
|
||||
</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 ? (
|
||||
<p className={styles.emptyState}>{t('Noch keine Sitzungen — starte ein Meeting im Assistenten.')}</p>
|
||||
) : (
|
||||
|
|
@ -247,7 +247,7 @@ export const TeamsbotDashboardView: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{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}>
|
||||
{activeSessions.map((session) => (
|
||||
<div key={session.id} className={styles.tbDashSessionRow}>
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
<>
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="teamsbot-modules-toolbar">
|
||||
<div className={styles.modulesHeader}>
|
||||
<h2>{t('Meeting-Module')}</h2>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
|
|
@ -247,12 +247,12 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{loading && (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden...')} id="teamsbot-modules-loading">
|
||||
<div className={styles.loading}>{t('Laden...')}</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Meeting-Module')} id="teamsbot-modules-list">
|
||||
<div className={styles.modulesList}>
|
||||
{modules.map(mod => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -813,7 +813,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Sitzung laden')} id="teamsbot-session-loading">
|
||||
<div className={styles.loading}>{t('Sitzung laden')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -824,7 +824,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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' }}>
|
||||
<h3>{t('Keine aktive Sitzung')}</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', textAlign: 'center', maxWidth: 400 }}>
|
||||
|
|
@ -851,7 +851,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -906,7 +906,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Sitzung')} id="teamsbot-session-toolbar">
|
||||
{/* Session Switcher */}
|
||||
{allSessions.length > 1 && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
|
|
@ -1006,7 +1006,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
collapsible: true,
|
||||
collapseKey: 'teamsbot-session-udb',
|
||||
content: (
|
||||
<Panel variant="card" title={t('Datenquellen')}>
|
||||
<Panel variant="card" title={t('Datenquellen')} id="teamsbot-session-datasources">
|
||||
<UnifiedDataBar
|
||||
context={_udbContext}
|
||||
activeTab={udbTab}
|
||||
|
|
@ -1025,7 +1025,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
collapsible: true,
|
||||
collapseKey: 'teamsbot-session-director',
|
||||
content: (
|
||||
<Panel variant="card" title={t('Regieanweisungen')}>
|
||||
<Panel variant="card" title={t('Regieanweisungen')} id="teamsbot-session-director">
|
||||
{!['ended', 'error', 'leaving'].includes(session.status) ? (
|
||||
<div
|
||||
className={`${styles.directorPanel} ${directorDragOver ? styles.directorPanelDragOver : ''}`}
|
||||
|
|
@ -1229,7 +1229,7 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
defaultSize: 54,
|
||||
minSize: 30,
|
||||
content: (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Transkript & Antworten')} id="teamsbot-session-editor">
|
||||
<PanelLayout
|
||||
persistenceKey={`teamsbot-session-editor-${instanceId}`}
|
||||
direction="horizontal"
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
id: 'general',
|
||||
label: t('Bot-Einstellungen'),
|
||||
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>}
|
||||
{successMsg && <div className={styles.successBanner}>{successMsg}</div>}
|
||||
|
||||
|
|
@ -455,7 +455,7 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
id: 'systemBots',
|
||||
label: t('System-Bots'),
|
||||
render: () => (
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('System-Bots')} id="teamsbot-system-bots-table">
|
||||
<_SystemBotsPanel instanceId={instanceId} />
|
||||
</Panel>
|
||||
),
|
||||
|
|
@ -472,7 +472,7 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="table">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Konfiguration laden')} id="teamsbot-settings-loading">
|
||||
<div className={styles.loading}>{t('Konfiguration laden')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ const _AbschlussTabBody: React.FC<_AbschlussTabBodyProps> = ({
|
|||
|
||||
return (
|
||||
<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 }}>
|
||||
{_tabDescription(tabDef.id, t)}
|
||||
</p>
|
||||
|
|
@ -205,7 +205,7 @@ const _AbschlussTabBody: React.FC<_AbschlussTabBodyProps> = ({
|
|||
</Panel>
|
||||
|
||||
{!isComingSoon && (
|
||||
<Panel variant="card" title={currentWorkflow?.label || t('Workflow')}>
|
||||
<Panel variant="card" id="trustee-abschluss-workflow" title={currentWorkflow?.label || t('Workflow')}>
|
||||
{workflowsLoading ? (
|
||||
<p className={styles.loadingText}>{t('Workflows werden geladen…')}</p>
|
||||
) : !currentWorkflow ? (
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
|
|||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<Panel
|
||||
variant="card"
|
||||
id="trustee-accounting-connection-status"
|
||||
title={t('Verbindungsstatus')}
|
||||
collapsible
|
||||
collapseKey="trustee-accounting-connection-status"
|
||||
|
|
@ -254,7 +255,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
|
|||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel variant="wizard" title={t('Verbindung einrichten')}>
|
||||
<Panel variant="wizard" id="trustee-accounting-connection-setup" title={t('Verbindung einrichten')}>
|
||||
{/* Step 1: Select system */}
|
||||
<div className={styles.setupStep}>
|
||||
<div className={styles.stepNumber}>1</div>
|
||||
|
|
@ -444,7 +445,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
|
|||
const _importTabContent = (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{!existingConfig?.configured ? (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="trustee-accounting-import-hint">
|
||||
<div className={styles.infoBox}>
|
||||
<p>
|
||||
{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 variant="card" title={t('Letzter Import')}>
|
||||
<Panel variant="card" id="trustee-accounting-last-import" title={t('Letzter Import')}>
|
||||
{_importLastSyncPanel}
|
||||
</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' }}>
|
||||
{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>
|
||||
|
|
@ -610,7 +611,7 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
|
|||
)}
|
||||
</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) ? (
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
||||
{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>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="trustee-accounting-loading">
|
||||
<div className={styles.loading}>{t('Buchhaltungseinstellungen werden geladen…')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
|
|||
|
|
@ -256,13 +256,13 @@ const _AnalyseTabBody: React.FC<_AnalyseTabBodyProps> = ({
|
|||
|
||||
return (
|
||||
<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 }}>
|
||||
{_tabDescription(tabDef.id, t)}
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={currentWorkflow?.label || t('Workflow')}>
|
||||
<Panel variant="card" id="trustee-analyse-workflow" title={currentWorkflow?.label || t('Workflow')}>
|
||||
{workflowsLoading ? (
|
||||
<p className={styles.loadingText}>{t('Workflows werden geladen…')}</p>
|
||||
) : !currentWorkflow ? (
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="dashboard">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="dashboard">
|
||||
<Panel variant="dashboard" title={t('Kennzahlen')} id="trustee-dashboard-kpis">
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statIcon}>📊</div>
|
||||
|
|
@ -163,7 +163,7 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Schnellzugriff')}>
|
||||
<Panel variant="card" id="trustee-dashboard-quick-actions" title={t('Schnellzugriff')}>
|
||||
<QuickActionBoard
|
||||
actions={quickActions}
|
||||
categories={quickActionCategories}
|
||||
|
|
@ -174,6 +174,7 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
|
||||
<Panel
|
||||
variant="card"
|
||||
id="trustee-dashboard-instance-details"
|
||||
title={t('Instanz-Details')}
|
||||
collapsible
|
||||
collapseKey="trustee-dashboard-instance-details"
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="trustee-documents-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -207,7 +207,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="trustee-documents-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -229,7 +229,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Dokumente')} id="documents-table">
|
||||
<FormGeneratorTable
|
||||
data={documents}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ export const TrusteeExpenseImportView: React.FC<TrusteeExpenseImportViewProps> =
|
|||
|
||||
const _panelStack = (
|
||||
<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 }}>
|
||||
{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
|
||||
|
|
@ -523,7 +523,7 @@ export const TrusteeExpenseImportView: React.FC<TrusteeExpenseImportViewProps> =
|
|||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel variant="wizard" title={t('Einrichtung')}>
|
||||
<Panel variant="wizard" id="trustee-expense-import-setup" title={t('Einrichtung')}>
|
||||
{/* Step 1: Microsoft Connection */}
|
||||
<div className={styles.setupStep}>
|
||||
<div className={styles.stepNumber}>1</div>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const TrusteeImportProcessView: React.FC = () => {
|
|||
label: t('Daten einlesen'),
|
||||
icon: <span aria-hidden="true">{'\uD83D\uDD04'}</span>,
|
||||
render: () => (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Weiterleitung')} id="trustee-import-sync-redirect">
|
||||
<div className={styles.infoBox}>
|
||||
<p>{t('Weiterleitung zu den Buchhaltungs-Einstellungen…')}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -92,7 +92,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
|
|||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden')} id="trustee-instance-roles-loading">
|
||||
<div className={styles.loading}>{t('Lade Instanzrollen')}</div>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -107,7 +107,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
|
|||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, margin: 0 }}>{t('Instanzrollen')}</h1>
|
||||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="trustee-instance-roles-error">
|
||||
<div className={styles.error}>
|
||||
<p>{error}</p>
|
||||
<button type="button" onClick={fetchRoles} className={styles.retryButton}>
|
||||
|
|
@ -127,13 +127,13 @@ export const TrusteeInstanceRolesView: React.FC = () => {
|
|||
</StackLayout.Header>
|
||||
<StackLayout.Body>
|
||||
<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}>
|
||||
<FaSync /> {t('Aktualisieren')}
|
||||
</button>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Hinweis')} id="trustee-instance-roles-info">
|
||||
<div className={styles.infoBox}>
|
||||
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
|
||||
<span>
|
||||
|
|
@ -146,7 +146,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
|
|||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Rollen')}>
|
||||
<Panel variant="card" id="trustee-instance-roles-list" title={t('Rollen')}>
|
||||
{roles.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="trustee-positions-error">
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
|
|
@ -357,7 +357,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="trustee-positions-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -379,7 +379,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Positionen')} id="positions-table">
|
||||
<FormGeneratorTable
|
||||
data={positions}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
|
|||
|
||||
if (!fileContext) {
|
||||
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>
|
||||
</Panel>
|
||||
);
|
||||
|
|
@ -282,7 +282,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
|
|||
|
||||
const _panelStack = (
|
||||
<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 }}>
|
||||
{t('Laden Sie PDF- oder JPG-Dokumente (Belege, Rechnungen) hoch. Starten Sie dann die Pipeline: Daten extrahieren → Positionen erstellen → in Buchhaltung synchronisieren.')}
|
||||
</p>
|
||||
|
|
@ -301,7 +301,7 @@ export const TrusteeScanUploadView: React.FC<TrusteeScanUploadViewProps> = ({ em
|
|||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Dateien hochladen')}>
|
||||
<Panel variant="card" id="trustee-scan-upload-files" title={t('Dateien hochladen')}>
|
||||
<div
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Fehler')} id="trustee-data-error">
|
||||
<div className={adminStyles.errorContainer}>
|
||||
<span className={adminStyles.errorIcon}>⚠️</span>
|
||||
<p className={adminStyles.errorMessage}>
|
||||
|
|
@ -210,7 +210,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="trustee-data-toolbar">
|
||||
<div className={adminStyles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -223,7 +223,7 @@ export const TrusteeDataTab: React.FC<TrusteeDataTabProps> = ({
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={entityLabel ? t(entityLabel) : t('Daten')} id={`${apiEndpoint.split('/').slice(4).join('-')}-table`}>
|
||||
<FormGeneratorTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const WorkflowEditorPage: React.FC<WorkflowEditorPageProps> = ({
|
|||
}
|
||||
}, [workflowIdFromUrl]);
|
||||
|
||||
const { currentLanguage } = useLanguage();
|
||||
const { currentLanguage, t } = useLanguage();
|
||||
const language = (currentLanguage?.slice(0, 2) || 'de') as string;
|
||||
|
||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||
|
|
@ -115,7 +115,7 @@ export const WorkflowEditorPage: React.FC<WorkflowEditorPageProps> = ({
|
|||
}, [instanceId]);
|
||||
|
||||
const _editorPanel = (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Workflow-Editor')} id="workflow-editor">
|
||||
<FlowEditor
|
||||
instanceId={instanceId || ''}
|
||||
mandateId={mandateId || undefined}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
|||
|
||||
if (!instanceId) {
|
||||
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>
|
||||
</Panel>
|
||||
);
|
||||
|
|
@ -236,7 +236,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
|||
|
||||
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 style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{(['all', 'user', 'instance', 'mandate', 'system'] as const).map((s) => (
|
||||
|
|
@ -262,7 +262,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Workflow-Vorlagen')} id="workflow-templates-table">
|
||||
<FormGeneratorTable<AutoWorkflowTemplate>
|
||||
data={templates}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const ChatStream: React.FC<ChatStreamProps> = ({ messages,
|
|||
}, [messages, audioQueue]);
|
||||
|
||||
return (
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Chat')} id="chat-stream" collapsible={false}>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={_handleScroll}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ instanceId, fileId, fi
|
|||
|
||||
if (!file) {
|
||||
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 }}>
|
||||
{t('Datei für Vorschau auswählen')}
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ instanceId, fileId, fi
|
|||
);
|
||||
|
||||
return (
|
||||
<Panel variant="card" title={file.fileName} subtitle={_metaSubtitle}>
|
||||
<Panel variant="card" title={file.fileName} subtitle={_metaSubtitle} id="file-preview">
|
||||
{file.description && (
|
||||
<div style={{ fontSize: 12, color: '#555', marginBottom: 8 }}>{file.description}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
|
|||
{snapshots.length > 0 && (
|
||||
<Panel
|
||||
variant="card"
|
||||
id="neutralization-snapshots"
|
||||
title={`${t('Neutralisierter Text')} (${snapshots.length})`}
|
||||
collapsible
|
||||
collapseKey={`neutralization-snapshots-${instanceId}`}
|
||||
|
|
@ -361,6 +362,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
|
|||
{sources.length > 0 && (
|
||||
<Panel
|
||||
variant="card"
|
||||
id="neutralization-sources"
|
||||
title={t('Datenquellen')}
|
||||
collapsible
|
||||
collapseKey={`neutralization-sources-${instanceId}`}
|
||||
|
|
@ -420,6 +422,7 @@ const NeutralizationPanel: React.FC<NeutralizationPanelProps> = ({ instanceId })
|
|||
{selectedSource && mappings.length > 0 && (
|
||||
<Panel
|
||||
variant="table"
|
||||
id="neutralization-mappings"
|
||||
title={`${t('Platzhalter-Mappings')} (${mappings.length})`}
|
||||
>
|
||||
<div style={{ maxHeight: 300, overflowY: 'auto' }}>
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities })
|
|||
|
||||
if (!activities.length) {
|
||||
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 }}>
|
||||
{t('Noch keine Aktivität')}
|
||||
</div>
|
||||
|
|
@ -201,7 +201,7 @@ export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities })
|
|||
}
|
||||
|
||||
return (
|
||||
<Panel variant="card" title={t('Aktivität')}>
|
||||
<Panel variant="card" title={t('Aktivität')} id="tool-activity">
|
||||
<div style={{ padding: 8 }}>
|
||||
{activities.map(activity => {
|
||||
const isError = activity.status === 'error';
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export interface WorkspaceContextSidebarProps {
|
|||
files: WorkspaceFile[];
|
||||
/** When false, sidebar stays expanded (e.g. mobile bottom sheet). */
|
||||
allowCollapse?: boolean;
|
||||
/** Fill the host container width (used inside a resizable PanelLayout pane). */
|
||||
fillWidth?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceContextSidebar: React.FC<WorkspaceContextSidebarProps> = ({
|
||||
|
|
@ -55,12 +57,13 @@ export const WorkspaceContextSidebar: React.FC<WorkspaceContextSidebarProps> = (
|
|||
selectedFileId,
|
||||
files,
|
||||
allowCollapse = true,
|
||||
fillWidth = false,
|
||||
}) => {
|
||||
const isCollapsed = allowCollapse && collapsed;
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`${styles.contextSidebar} ${isCollapsed ? styles.contextSidebarCollapsed : ''}`}
|
||||
className={`${styles.contextSidebar} ${isCollapsed ? styles.contextSidebarCollapsed : ''} ${fillWidth && !isCollapsed ? styles.contextSidebarFill : ''}`}
|
||||
aria-label="Kontext"
|
||||
>
|
||||
<div className={styles.contextToolbar}>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const WorkspaceEditorPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="editor-header-toolbar">
|
||||
<div className={pageStyles.headerRow}>
|
||||
<div className={pageStyles.headerLeft}>
|
||||
<button onClick={_goBack} style={_btnStyle} title={t('Zurück zum Dashboard')}>
|
||||
|
|
@ -103,7 +103,7 @@ export const WorkspaceEditorPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{pendingEdits.length > 0 && (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Dateien')} id="editor-tabs-toolbar">
|
||||
<div className={pageStyles.tabBar}>
|
||||
{pendingEdits.map(edit => (
|
||||
<_EditorTab
|
||||
|
|
@ -117,7 +117,7 @@ export const WorkspaceEditorPage: React.FC = () => {
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="editor">
|
||||
<Panel variant="editor" title={t('Vergleich')} id="editor-diff" collapsible={false}>
|
||||
<div className={pageStyles.editorFill}>
|
||||
{editor.isLoading ? (
|
||||
<div className={pageStyles.loadingState}>
|
||||
|
|
@ -143,7 +143,7 @@ export const WorkspaceEditorPage: React.FC = () => {
|
|||
</Panel>
|
||||
|
||||
{activeEdit && activeEdit.status === 'pending' && (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="editor-footer-toolbar">
|
||||
<div className={pageStyles.footerRow}>
|
||||
<div className={pageStyles.footerMeta}>
|
||||
<span>{activeEdit.fileName}</span>
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
{error && <div className={styles.error}>{error}</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}>
|
||||
<label className={styles.label}>
|
||||
{t('Max. Agenten-Runden')}
|
||||
|
|
@ -239,7 +239,7 @@ export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ insta
|
|||
</button>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('KI-Einstellungen')}>
|
||||
<Panel variant="card" title={t('KI-Einstellungen')} id="ai-settings">
|
||||
<div className={styles.field}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -506,7 +506,6 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
: [];
|
||||
|
||||
const hasAttachments = hasFileOrSourceAttachments;
|
||||
const _horizontalPadding = isMobile ? 12 : 24;
|
||||
const _controlSize = isMobile ? 38 : 40;
|
||||
|
||||
const _handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
|
|
@ -624,7 +623,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
onDrop={e => void _handlePromptDrop(e)}
|
||||
>
|
||||
{hasAttachments && (
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Anhänge')} id="input-attachments-toolbar">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
|
|
@ -757,7 +756,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
</div>
|
||||
</FloatingPortal>
|
||||
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Eingabe')} id="input-toolbar">
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,14 @@
|
|||
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 {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import type { ProviderSelection } from '../../../components/ProviderSelector';
|
|||
import { useBilling } from '../../../hooks/useBilling';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import { StackLayout } from '../../../components/Layout/StackLayout';
|
||||
import { PanelLayout } from '../../../components/Layout/PanelLayout';
|
||||
import { FloatingPortal } from '../../../components/UiComponents/FloatingPortal';
|
||||
import { WorkspaceContextSidebar, type WorkspaceCtxTab } from './WorkspaceContextSidebar';
|
||||
import styles from './WorkspacePage.module.css';
|
||||
|
|
@ -376,7 +377,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
|||
}
|
||||
}, [instanceId, workspace]);
|
||||
|
||||
const _contextSidebar = (allowCollapse = true) => (
|
||||
const _contextSidebar = (allowCollapse = true, fillWidth = false) => (
|
||||
<WorkspaceContextSidebar
|
||||
context={_udbContext}
|
||||
activeTab={udbTab}
|
||||
|
|
@ -384,6 +385,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
|||
collapsed={ctxSidebarCollapsed}
|
||||
onToggleCollapsed={() => setCtxSidebarCollapsed(v => !v)}
|
||||
allowCollapse={allowCollapse}
|
||||
fillWidth={fillWidth}
|
||||
onFileSelect={_handleFileSelect}
|
||||
onSourcesChanged={_handleSourcesChanged}
|
||||
onSendToChat_Files={_handleSendToChat_Files}
|
||||
|
|
@ -615,8 +617,21 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
|||
<div className={styles.workspaceShell}>
|
||||
{_desktopTopBar}
|
||||
<div className={styles.mainStage}>
|
||||
{ctxPanelOpen && !ctxSidebarCollapsed ? (
|
||||
<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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const WorkspaceSettingsPage: React.FC = () => {
|
|||
return (
|
||||
<StackLayout variant="form">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Einstellungen')} id="workspace-settings">
|
||||
<WorkspaceGeneralSettings instanceId={instanceId} />
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<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)' }}>
|
||||
{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}
|
||||
</p>
|
||||
|
|
@ -217,7 +217,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="card">
|
||||
<Panel variant="card" title={t('Laden…')} id="run-detail-loading">
|
||||
<p style={{ margin: 0 }}>{t('Laden…')}</p>
|
||||
</Panel>
|
||||
</StackLayout.Body>
|
||||
|
|
@ -234,13 +234,13 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="run-detail-toolbar">
|
||||
<button type="button" className={styles.secondaryButton} onClick={onBack}>
|
||||
← {t('Zurück zu Läufe')}
|
||||
</button>
|
||||
</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>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
||||
<span><strong>{t('Status')}:</strong> {run.status}</span>
|
||||
|
|
@ -259,7 +259,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
|
|||
</Panel>
|
||||
|
||||
{_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' }}>
|
||||
{_producedFiles.map(f => (
|
||||
<a
|
||||
|
|
@ -276,7 +276,7 @@ export const _RunDetailTab: React.FC<RunDetailTabProps> = ({ runId, onBack }) =>
|
|||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel variant="card" title={t('Schritte')}>
|
||||
<Panel variant="card" title={t('Schritte')} id="run-detail-steps">
|
||||
{steps.length === 0 ? (
|
||||
<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>
|
||||
|
||||
{_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} />
|
||||
</Panel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -475,6 +475,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
|||
<Panel
|
||||
variant="dashboard"
|
||||
title={t('Übersicht')}
|
||||
id="runs-overview"
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
actions={
|
||||
|
|
@ -529,7 +530,7 @@ export const _RunsTab: React.FC<RunsTabProps> = ({ workflowFilter, onRunClick, s
|
|||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Läufe')} id="runs-table">
|
||||
<FormGeneratorTable<WorkflowRun>
|
||||
data={runs}
|
||||
columns={_runColumns}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
|
|||
return (
|
||||
<StackLayout variant="scroll">
|
||||
<StackLayout.Body>
|
||||
<Panel variant="toolbar">
|
||||
<Panel variant="toolbar" title={t('Aktionen')} id="tasks-toolbar">
|
||||
<div className={styles.headerActions}>
|
||||
<span style={{ fontSize: '0.875rem', color: 'var(--text-secondary, #666)', marginRight: 'auto' }}>
|
||||
{t('Offene Aufgaben')}: {_pendingTasks.length}
|
||||
|
|
@ -124,7 +124,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="card" title={t('Offene Aufgaben')}>
|
||||
<Panel variant="card" title={t('Offene Aufgaben')} id="tasks-pending">
|
||||
{_pendingTasks.length === 0 && !loading && (
|
||||
<p style={{ margin: 0, textAlign: 'center', color: 'var(--text-secondary)' }}>
|
||||
{t('Keine offenen Aufgaben vorhanden.')}
|
||||
|
|
@ -183,6 +183,7 @@ export const _TasksTab: React.FC<TasksTabProps> = ({ selectedMandateId = 'all' }
|
|||
<Panel
|
||||
variant="card"
|
||||
title={`${t('Abgeschlossene Aufgaben')} (${_completedTasks.length})`}
|
||||
id="tasks-completed"
|
||||
collapsible
|
||||
defaultCollapsed
|
||||
collapseKey="workflow-tasks-completed"
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
|
|||
<Panel
|
||||
variant="dashboard"
|
||||
title={t('Übersicht')}
|
||||
id="workflows-overview"
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
|
|
@ -306,7 +307,7 @@ export const _WorkflowsTab: React.FC<WorkflowsTabProps> = ({ onWorkflowClick, se
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel variant="table">
|
||||
<Panel variant="table" title={t('Workflows')} id="workflows-table">
|
||||
<FormGeneratorTable<SystemWorkflow>
|
||||
data={workflows}
|
||||
columns={_columns}
|
||||
|
|
|
|||
Loading…
Reference in a new issue