172 lines
6 KiB
TypeScript
172 lines
6 KiB
TypeScript
/**
|
|
* AdminDemoConfigPage
|
|
*
|
|
* SysAdmin page for managing demo configurations.
|
|
* Lists available demo configs with Load / Remove actions.
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { FaPlay, FaTrash, FaSync, FaCubes } from 'react-icons/fa';
|
|
import api from '../../api';
|
|
import styles from './Admin.module.css';
|
|
import demoStyles from './AdminDemoConfigPage.module.css';
|
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
|
import { useConfirm } from '../../hooks/useConfirm';
|
|
|
|
interface _DemoConfig {
|
|
code: string;
|
|
label: string;
|
|
description: string;
|
|
}
|
|
|
|
interface _ActionResult {
|
|
code: string;
|
|
action: 'load' | 'remove';
|
|
status: 'ok' | 'error';
|
|
summary?: Record<string, unknown>;
|
|
error?: string;
|
|
}
|
|
|
|
export const AdminDemoConfigPage: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const { confirm, ConfirmDialog } = useConfirm();
|
|
const [configs, setConfigs] = useState<_DemoConfig[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
|
|
const [lastResult, setLastResult] = useState<_ActionResult | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const _fetchConfigs = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const response = await api.get('/api/admin/demo-config');
|
|
setConfigs(response.data.configs || []);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.detail || t('Error loading demo configs'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
_fetchConfigs();
|
|
}, [_fetchConfigs]);
|
|
|
|
const _handleLoad = async (code: string) => {
|
|
if (actionInProgress) return;
|
|
setActionInProgress(code);
|
|
setLastResult(null);
|
|
try {
|
|
const response = await api.post(`/api/admin/demo-config/${code}/load`);
|
|
setLastResult({ code, action: 'load', status: 'ok', summary: response.data.summary });
|
|
} catch (err: any) {
|
|
setLastResult({ code, action: 'load', status: 'error', error: err.response?.data?.detail || String(err) });
|
|
} finally {
|
|
setActionInProgress(null);
|
|
}
|
|
};
|
|
|
|
const _handleRemove = async (code: string) => {
|
|
if (actionInProgress) return;
|
|
const ok = await confirm(
|
|
t('Alle Demo-Daten für diese Konfiguration wirklich entfernen?'),
|
|
{ confirmLabel: t('Entfernen'), cancelLabel: t('Abbrechen'), variant: 'danger' },
|
|
);
|
|
if (!ok) return;
|
|
setActionInProgress(code);
|
|
setLastResult(null);
|
|
try {
|
|
const response = await api.post(`/api/admin/demo-config/${code}/remove`);
|
|
setLastResult({ code, action: 'remove', status: 'ok', summary: response.data.summary });
|
|
} catch (err: any) {
|
|
setLastResult({ code, action: 'remove', status: 'error', error: err.response?.data?.detail || String(err) });
|
|
} finally {
|
|
setActionInProgress(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.adminPage}>
|
|
<div className={styles.pageHeader}>
|
|
<div>
|
|
<h1 className={styles.pageTitle}>{t('Demo-Konfigurationen')}</h1>
|
|
<p className={styles.pageSubtitle}>{t('Demo-Umgebungen für Präsentationen und Tests laden oder entfernen.')}</p>
|
|
</div>
|
|
<div className={styles.headerActions}>
|
|
<button className={styles.secondaryButton} onClick={_fetchConfigs} disabled={loading}>
|
|
<FaSync /> {t('Aktualisieren')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <div className={demoStyles.errorBanner}>{error}</div>}
|
|
|
|
{lastResult && (
|
|
<div className={lastResult.status === 'ok' ? demoStyles.successBanner : demoStyles.errorBanner}>
|
|
<strong>{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:</strong>{' '}
|
|
{lastResult.status === 'ok' ? (
|
|
<_SummaryDisplay summary={lastResult.summary} />
|
|
) : (
|
|
<span>{lastResult.error}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{loading && configs.length === 0 ? (
|
|
<div className={demoStyles.loadingState}>{t('Lade…')}</div>
|
|
) : configs.length === 0 ? (
|
|
<div className={demoStyles.emptyState}>{t('Keine Demo-Konfigurationen gefunden.')}</div>
|
|
) : (
|
|
<div className={demoStyles.configGrid}>
|
|
{configs.map((cfg) => (
|
|
<div key={cfg.code} className={demoStyles.configCard}>
|
|
<div className={demoStyles.cardIcon}><FaCubes /></div>
|
|
<div className={demoStyles.cardContent}>
|
|
<h3 className={demoStyles.cardTitle}>{cfg.label}</h3>
|
|
<p className={demoStyles.cardDescription}>{cfg.description}</p>
|
|
<span className={demoStyles.cardCode}>{cfg.code}</span>
|
|
</div>
|
|
<div className={demoStyles.cardActions}>
|
|
<button
|
|
className={demoStyles.loadButton}
|
|
onClick={() => _handleLoad(cfg.code)}
|
|
disabled={actionInProgress !== null}
|
|
>
|
|
{actionInProgress === cfg.code ? <FaSync className={demoStyles.spin} /> : <FaPlay />}
|
|
{t('Laden')}
|
|
</button>
|
|
<button
|
|
className={demoStyles.removeButton}
|
|
onClick={() => _handleRemove(cfg.code)}
|
|
disabled={actionInProgress !== null}
|
|
>
|
|
<FaTrash />
|
|
{t('Entfernen')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<ConfirmDialog />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const _SummaryDisplay: React.FC<{ summary?: Record<string, unknown> }> = ({ summary }) => {
|
|
const { t } = useLanguage();
|
|
if (!summary) return null;
|
|
const sections = Object.entries(summary).filter(([, v]) => Array.isArray(v) && (v as unknown[]).length > 0);
|
|
if (sections.length === 0) return <span>{t('Abgeschlossen (keine Änderungen)')}</span>;
|
|
return (
|
|
<span>
|
|
{sections.map(([key, items]) => (
|
|
<span key={key} style={{ marginRight: 12 }}>
|
|
<strong>{key}:</strong> {(items as string[]).length}
|
|
</span>
|
|
))}
|
|
</span>
|
|
);
|
|
};
|