u
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
parseNum,
|
||||
} from './manifestConfig';
|
||||
import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber';
|
||||
import { getManifestDraftAutomat } from '../../automats/manifestDraftAutomat';
|
||||
|
||||
class ManifestForm extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -42,17 +43,20 @@ class ManifestForm extends PureComponent {
|
||||
? supplierAutomat.state.status === 'ready'
|
||||
: entityAutomat.state.status === 'ready';
|
||||
|
||||
this.draftAutomat = props.mode === 'create' ? getManifestDraftAutomat(props.kind) : null;
|
||||
const initialDraft = this.draftAutomat?.state;
|
||||
|
||||
this.state = {
|
||||
status: props.mode === 'edit' ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'),
|
||||
error: null,
|
||||
parties: initialParties,
|
||||
partiesLoading: !partiesReady,
|
||||
date: '',
|
||||
partyId: null,
|
||||
number: '',
|
||||
externalNumber: '',
|
||||
lines: [emptyLine()],
|
||||
attachments: [],
|
||||
date: initialDraft?.date || '',
|
||||
partyId: initialDraft?.partyId ?? null,
|
||||
number: initialDraft?.number || '',
|
||||
externalNumber: initialDraft?.externalNumber || '',
|
||||
lines: initialDraft?.lines?.length > 0 ? initialDraft.lines : [emptyLine()],
|
||||
attachments: initialDraft?.attachments || [],
|
||||
saving: false,
|
||||
pdfUploading: false,
|
||||
pdfDialogOpen: false,
|
||||
@@ -131,19 +135,48 @@ class ManifestForm extends PureComponent {
|
||||
} else {
|
||||
void this.loadPartiesOnly();
|
||||
}
|
||||
|
||||
if (this.draftAutomat) {
|
||||
this.unsubDraft = this.draftAutomat.subscribe(this, (state) => ({
|
||||
date: state.date || '',
|
||||
partyId: state.partyId,
|
||||
externalNumber: state.externalNumber || '',
|
||||
lines: state.lines?.length > 0 ? state.lines : [emptyLine()],
|
||||
attachments: state.attachments || [],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
this.props.mode === 'edit' &&
|
||||
prevProps.docId !== this.props.docId
|
||||
) {
|
||||
void this.loadEdit();
|
||||
}
|
||||
|
||||
if (this.props.mode === 'create' && this.draftAutomat) {
|
||||
if (
|
||||
prevState.date !== this.state.date ||
|
||||
prevState.partyId !== this.state.partyId ||
|
||||
prevState.externalNumber !== this.state.externalNumber ||
|
||||
prevState.lines !== this.state.lines ||
|
||||
prevState.attachments !== this.state.attachments
|
||||
) {
|
||||
this.draftAutomat.setState({
|
||||
date: this.state.date,
|
||||
partyId: this.state.partyId,
|
||||
externalNumber: this.state.externalNumber,
|
||||
lines: this.state.lines,
|
||||
attachments: this.state.attachments,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.unsubDraft?.();
|
||||
this.unsubParties?.();
|
||||
this.unsubCompany?.();
|
||||
if (this._parseAbortController) {
|
||||
@@ -687,6 +720,7 @@ class ManifestForm extends PureComponent {
|
||||
requireObject(data, cfg.postPathLabel);
|
||||
requireObject(data[cfg.docField], cfg.docField);
|
||||
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
||||
this.draftAutomat?.actions?.reset();
|
||||
await cfg.listAutomat.actions.reload();
|
||||
if (this._cancelled) return;
|
||||
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||
|
||||
567
src/components/settings/BackupSettingsPanel.jsx
Normal file
567
src/components/settings/BackupSettingsPanel.jsx
Normal file
@@ -0,0 +1,567 @@
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import BackupOutlinedIcon from '@mui/icons-material/BackupOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||
import CloudQueueOutlinedIcon from '@mui/icons-material/CloudQueueOutlined';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import backupAutomat from '../../automats/backupAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
BORDER,
|
||||
CHROMA,
|
||||
DARK_1,
|
||||
DARK_2,
|
||||
DARK_4,
|
||||
LIGHT_3,
|
||||
PRIMARY,
|
||||
SURFACE_BASE,
|
||||
SURFACE_RAISED,
|
||||
WHITE,
|
||||
} from '../../colors';
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes == null || isNaN(bytes) || bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function formatDate(ts) {
|
||||
if (!ts) return '—';
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
if (isNaN(d.getTime())) return String(ts);
|
||||
return d.toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return String(ts);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
if (ms == null || isNaN(ms)) return '—';
|
||||
if (ms < 1000) return `${ms} ms`;
|
||||
return `${(ms / 1000).toFixed(2)} s`;
|
||||
}
|
||||
|
||||
export default class BackupSettingsPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { status, error, runningBackup, backupStatus, backups } = backupAutomat.state;
|
||||
this.state = {
|
||||
status,
|
||||
error,
|
||||
runningBackup,
|
||||
backupStatus,
|
||||
backups,
|
||||
tierTab: 'hourly', // 'hourly' | 'daily' | 'monthly'
|
||||
expandedItemName: null,
|
||||
};
|
||||
|
||||
this.handleRefresh = this.handleRefresh.bind(this);
|
||||
this.handleRunBackup = this.handleRunBackup.bind(this);
|
||||
this.handleTierChange = this.handleTierChange.bind(this);
|
||||
this.handleToggleExpand = this.handleToggleExpand.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = backupAutomat.subscribe(this, (state) => ({
|
||||
status: state.status,
|
||||
error: state.error,
|
||||
runningBackup: state.runningBackup,
|
||||
backupStatus: state.backupStatus,
|
||||
backups: state.backups,
|
||||
}));
|
||||
|
||||
if (backupAutomat.state.status === 'idle') {
|
||||
void this.loadBackups();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
// #region loadBackups
|
||||
// MARK: calls backupAutomat.actions.load from ./api/index.js
|
||||
async loadBackups() {
|
||||
try {
|
||||
await backupAutomat.actions.load();
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region handleRefresh
|
||||
// MARK: calls backupAutomat.actions.reload from ./api/index.js
|
||||
async handleRefresh() {
|
||||
try {
|
||||
await backupAutomat.actions.reload();
|
||||
enqueueSnackbar('Backup-Status aktualisiert', { variant: 'info' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region handleRunBackup
|
||||
// MARK: calls POST /backup/run via backupAutomat.actions.runBackup from ./api/index.js
|
||||
async handleRunBackup() {
|
||||
if (this.state.runningBackup || this.state.backupStatus?.isRunning) return;
|
||||
|
||||
try {
|
||||
const result = await backupAutomat.actions.runBackup();
|
||||
const sizeStr = result?.totalSizeBytes != null ? ` (${formatBytes(result.totalSizeBytes)})` : '';
|
||||
enqueueSnackbar(`Manuelles Backup erfolgreich erstellt${sizeStr}.`, {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
handleTierChange(_event, newValue) {
|
||||
this.setState({ tierTab: newValue, expandedItemName: null });
|
||||
}
|
||||
|
||||
handleToggleExpand(name) {
|
||||
this.setState((prev) => ({
|
||||
expandedItemName: prev.expandedItemName === name ? null : name,
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, runningBackup, backupStatus, backups, tierTab, expandedItemName } = this.state;
|
||||
|
||||
if (status === 'loading' && !backupStatus) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 8, gap: 2 }}>
|
||||
<CircularProgress size={36} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Backup-Status wird geladen...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'failed' && !backupStatus) {
|
||||
return (
|
||||
<Stack spacing={2} sx={{ py: 2 }}>
|
||||
<Alert severity="error">
|
||||
{error || 'Fehler beim Laden des Backup-Status.'}
|
||||
</Alert>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={this.handleRefresh}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const isExecuting = Boolean(runningBackup || backupStatus?.isRunning);
|
||||
const retention = backupStatus?.retention ?? { hourlyMax: 24, dailyMax: 7, monthly: 'Monatserster' };
|
||||
const counts = backups?.counts ?? { hourly: 0, daily: 0, monthly: 0 };
|
||||
const hourlyList = backups?.hourly || [];
|
||||
const dailyList = backups?.daily || [];
|
||||
const monthlyList = backups?.monthly || [];
|
||||
|
||||
const activeList = tierTab === 'hourly'
|
||||
? hourlyList
|
||||
: tierTab === 'daily'
|
||||
? dailyList
|
||||
: monthlyList;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Header Title & Actions */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={700} sx={{ color: DARK_4, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<BackupOutlinedIcon sx={{ color: PRIMARY }} />
|
||||
Datensicherung & Backups
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Automatische Sicherung von Datenbank, virtuellen Dateisystemen (VFS) und Dokumentenspeicher.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={this.handleRefresh}
|
||||
disabled={isExecuting}
|
||||
>
|
||||
Aktualisieren
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={isExecuting ? <CircularProgress size={16} color="inherit" /> : <PlayArrowOutlinedIcon />}
|
||||
onClick={this.handleRunBackup}
|
||||
disabled={isExecuting}
|
||||
sx={{ bgcolor: PRIMARY, '&:hover': { bgcolor: CHROMA.PETROL.dark } }}
|
||||
>
|
||||
{isExecuting ? 'Sicherung läuft...' : 'Jetzt sichern'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Status Badges Bar */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
bgcolor: SURFACE_BASE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
size="small"
|
||||
icon={backupStatus?.enabled ? <CheckCircleOutlineIcon /> : <WarningAmberIcon />}
|
||||
label={backupStatus?.enabled ? 'Dienst: Aktiv' : 'Dienst: Deaktiviert'}
|
||||
color={backupStatus?.enabled ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
{isExecuting && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<CircularProgress size={12} color="inherit" />}
|
||||
label="Backup wird ausgeführt..."
|
||||
color="primary"
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<StorageIcon />}
|
||||
label={`Provider: ${backupStatus?.dbProvider?.toUpperCase() || 'DATENBANK'}`}
|
||||
variant="outlined"
|
||||
/>
|
||||
{backupStatus?.backupDir && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<FolderOutlinedIcon />}
|
||||
label={`Verzeichnis: ${backupStatus.backupDir}`}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: 360 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* MSSQL Warning if present */}
|
||||
{backupStatus?.mssqlWarning && (
|
||||
<Alert severity="warning" icon={<WarningAmberIcon />}>
|
||||
MSSQL-Hinweis: Datenbank-Backups erfordern Schreibberechtigungen des SQL-Server-Dienstes auf das Backup-Verzeichnis oder werden getrennt verwaltet.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Overview KPI Cards */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* Hourly KPI */}
|
||||
<Card variant="outlined" sx={{ bgcolor: WHITE, borderColor: BORDER, borderRadius: 2 }}>
|
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} textTransform="uppercase">
|
||||
Stündliche Sicherungen
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: DARK_4, mt: 0.5 }}>
|
||||
{counts.hourly}{' '}
|
||||
<Typography component="span" variant="body2" color="text.secondary">
|
||||
/ {retention.hourlyMax} max
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Zuletzt: {formatDate(backupStatus?.latestHourly?.timestamp || hourlyList[0]?.timestamp)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Größe: {formatBytes(backupStatus?.latestHourly?.totalSizeBytes || hourlyList[0]?.totalSizeBytes)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Daily KPI */}
|
||||
<Card variant="outlined" sx={{ bgcolor: WHITE, borderColor: BORDER, borderRadius: 2 }}>
|
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} textTransform="uppercase">
|
||||
Tägliche Sicherungen
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: DARK_4, mt: 0.5 }}>
|
||||
{counts.daily}{' '}
|
||||
<Typography component="span" variant="body2" color="text.secondary">
|
||||
/ {retention.dailyMax} max
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Zuletzt: {formatDate(backupStatus?.latestDaily?.timestamp || dailyList[0]?.timestamp)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Größe: {formatBytes(backupStatus?.latestDaily?.totalSizeBytes || dailyList[0]?.totalSizeBytes)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Monthly KPI */}
|
||||
<Card variant="outlined" sx={{ bgcolor: WHITE, borderColor: BORDER, borderRadius: 2 }}>
|
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} textTransform="uppercase">
|
||||
Monatliche Sicherungen
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: DARK_4, mt: 0.5 }}>
|
||||
{counts.monthly}
|
||||
</Typography>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Zuletzt: {formatDate(backupStatus?.latestMonthly?.timestamp || monthlyList[0]?.timestamp)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Größe: {formatBytes(backupStatus?.latestMonthly?.totalSizeBytes || monthlyList[0]?.totalSizeBytes)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Retention Policy Note */}
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Aufbewahrungsrichtlinie: Maximal {retention.hourlyMax} stündliche Snapshots, {retention.dailyMax} tägliche Snapshots; monatliche Archivierung ({retention.monthly || 'am 1. Tag des Monats'}). Ältere Snapshots werden automatisch bereinigt.
|
||||
</Typography>
|
||||
|
||||
{/* Backups Table Section with Tabs */}
|
||||
<Box sx={{ border: `1px solid ${BORDER}`, borderRadius: 2, bgcolor: WHITE, overflow: 'hidden' }}>
|
||||
<Box sx={{ borderBottom: `1px solid ${BORDER}`, bgcolor: SURFACE_RAISED, px: 2 }}>
|
||||
<Tabs
|
||||
value={tierTab}
|
||||
onChange={this.handleTierChange}
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
sx={{ minHeight: 44 }}
|
||||
>
|
||||
<Tab
|
||||
value="hourly"
|
||||
label={`Stündlich (${counts.hourly})`}
|
||||
sx={{ minHeight: 44, textTransform: 'none', fontWeight: 600 }}
|
||||
/>
|
||||
<Tab
|
||||
value="daily"
|
||||
label={`Täglich (${counts.daily})`}
|
||||
sx={{ minHeight: 44, textTransform: 'none', fontWeight: 600 }}
|
||||
/>
|
||||
<Tab
|
||||
value="monthly"
|
||||
label={`Monatlich (${counts.monthly})`}
|
||||
sx={{ minHeight: 44, textTransform: 'none', fontWeight: 600 }}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{activeList.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6, gap: 1 }}>
|
||||
<CloudQueueOutlinedIcon sx={{ fontSize: 40, color: DARK_1 }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Sicherungen in dieser Kategorie vorhanden.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer sx={{ maxHeight: 360 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: 40 }} />
|
||||
<TableCell sx={{ fontWeight: 600 }}>Zeitpunkt</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Archivname</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Größe</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Typ</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{activeList.map((item) => {
|
||||
const isExpanded = expandedItemName === item.name;
|
||||
const manifest = item.manifest;
|
||||
const isManual = Boolean(manifest?.manual);
|
||||
|
||||
return (
|
||||
<BackupTableRow
|
||||
key={item.name}
|
||||
item={item}
|
||||
isExpanded={isExpanded}
|
||||
isManual={isManual}
|
||||
manifest={manifest}
|
||||
onToggleExpand={() => this.handleToggleExpand(item.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function BackupTableRow({ item, isExpanded, isManual, manifest, onToggleExpand }) {
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
hover
|
||||
onClick={onToggleExpand}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
bgcolor: isExpanded ? LIGHT_3 : 'inherit',
|
||||
'& > *': { borderBottom: isExpanded ? 'none !important' : undefined },
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onToggleExpand(); }}>
|
||||
{isExpanded ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 500 }}>
|
||||
{formatDate(item.timestamp)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, color: DARK_2, fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 600 }}>
|
||||
{formatBytes(item.totalSizeBytes)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={isManual ? 'Manuell' : 'Automatisch'}
|
||||
color={isManual ? 'primary' : 'default'}
|
||||
variant="outlined"
|
||||
sx={{ height: 22, fontSize: '0.75rem' }}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ bgcolor: LIGHT_3 }}>
|
||||
<TableCell colSpan={5} sx={{ py: 0, px: 3 }}>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Typography variant="caption" fontWeight={700} color="text.secondary" textTransform="uppercase">
|
||||
Backup-Details & Manifest
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' },
|
||||
gap: 1.5,
|
||||
mt: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: WHITE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Datenbank
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.database
|
||||
? `${formatBytes(manifest.files.database.sizeBytes)} (${manifest.files.database.backedUp ? 'OK' : manifest.files.database.reason || 'Übersprungen'})`
|
||||
: '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
VFS (Dateien & Belege)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.vfs ? formatBytes(manifest.files.vfs.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dokumentenspeicher (Storage)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.storage ? formatBytes(manifest.files.storage.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{manifest?.durationMs != null && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dauer
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{formatDuration(manifest.durationMs)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{manifest?.dbProvider && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Datenbank-Provider
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest.dbProvider.toUpperCase()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ gridColumn: { sm: 'span 3' } }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Speicherpfad
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: DARK_2, wordBreak: 'break-all' }}>
|
||||
{item.path}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,9 @@ import SettingsKeysPanel from './SettingsKeysPanel';
|
||||
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
|
||||
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
|
||||
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
|
||||
import BackupSettingsPanel from './BackupSettingsPanel';
|
||||
import SecurityIcon from '@mui/icons-material/Security';
|
||||
import BackupTableOutlinedIcon from '@mui/icons-material/BackupTableOutlined';
|
||||
import { SURFACE_BASE } from '../../colors';
|
||||
|
||||
const TAB_KEYS = {
|
||||
@@ -29,6 +31,7 @@ const TAB_KEYS = {
|
||||
numbers: 'numbers',
|
||||
printers: 'printers',
|
||||
importExport: 'importExport',
|
||||
backup: 'backup',
|
||||
posterminal: 'posterminal',
|
||||
security: 'security',
|
||||
};
|
||||
@@ -39,6 +42,7 @@ const TABS = [
|
||||
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
|
||||
{ id: TAB_KEYS.printers, label: 'Drucker', icon: <PrintOutlinedIcon />, Panel: PrinterSettingsPanel },
|
||||
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
|
||||
{ id: TAB_KEYS.backup, label: 'Backups', icon: <BackupTableOutlinedIcon />, Panel: BackupSettingsPanel },
|
||||
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },
|
||||
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
||||
];
|
||||
|
||||
@@ -5,3 +5,5 @@ export { default as NumberSettingsPanel } from './NumberSettingsPanel';
|
||||
export { default as PrinterSettingsPanel } from './PrinterSettingsPanel';
|
||||
export { default as PosTerminalSettingsPanel } from './PosTerminalSettingsPanel';
|
||||
export { default as ImportExportSettingsPanel } from './ImportExportSettingsPanel';
|
||||
export { default as McpAuthorizationsPanel } from './McpAuthorizationsPanel';
|
||||
export { default as BackupSettingsPanel } from './BackupSettingsPanel';
|
||||
|
||||
Reference in New Issue
Block a user