refactor: Move deletion log into ModerationGroupsPage
- Create DeletionLogSection component - Integrate deletion log at bottom of moderation page - Remove standalone DeletionLogPage and route - Remove admin nav link (log now in moderation) - Keep /api/admin routes for backend API access - Update nginx configs (remove /admin frontend route)
This commit is contained in:
parent
0f430af877
commit
3a2efd97c3
|
|
@ -107,20 +107,6 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Protected routes - Admin (password protected) - React Dev Server
|
|
||||||
location /admin {
|
|
||||||
auth_basic "Restricted Area - Admin";
|
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Protected routes - Moderation (password protected) - React Dev Server
|
# Protected routes - Moderation (password protected) - React Dev Server
|
||||||
location /moderation {
|
location /moderation {
|
||||||
auth_basic "Restricted Area - Moderation";
|
auth_basic "Restricted Area - Moderation";
|
||||||
|
|
|
||||||
|
|
@ -141,20 +141,6 @@ http {
|
||||||
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
|
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Protected routes - Admin (password protected)
|
|
||||||
location /admin {
|
|
||||||
auth_basic "Restricted Area - Admin";
|
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
expires -1;
|
|
||||||
|
|
||||||
# Prevent indexing
|
|
||||||
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Protected routes - Moderation (password protected)
|
# Protected routes - Moderation (password protected)
|
||||||
location /moderation {
|
location /moderation {
|
||||||
auth_basic "Restricted Area - Moderation";
|
auth_basic "Restricted Area - Moderation";
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import GroupsOverviewPage from './Components/Pages/GroupsOverviewPage';
|
||||||
import ModerationGroupsPage from './Components/Pages/ModerationGroupsPage';
|
import ModerationGroupsPage from './Components/Pages/ModerationGroupsPage';
|
||||||
import ModerationGroupImagesPage from './Components/Pages/ModerationGroupImagesPage';
|
import ModerationGroupImagesPage from './Components/Pages/ModerationGroupImagesPage';
|
||||||
import PublicGroupImagesPage from './Components/Pages/PublicGroupImagesPage';
|
import PublicGroupImagesPage from './Components/Pages/PublicGroupImagesPage';
|
||||||
import DeletionLogPage from './Components/Pages/DeletionLogPage';
|
|
||||||
import FZF from './Components/Pages/404Page.js'
|
import FZF from './Components/Pages/404Page.js'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -21,7 +20,6 @@ function App() {
|
||||||
<Route path="/groups" element={<GroupsOverviewPage />} />
|
<Route path="/groups" element={<GroupsOverviewPage />} />
|
||||||
<Route path="/moderation" exact element={<ModerationGroupsPage />} />
|
<Route path="/moderation" exact element={<ModerationGroupsPage />} />
|
||||||
<Route path="/moderation/groups/:groupId" element={<ModerationGroupImagesPage />} />
|
<Route path="/moderation/groups/:groupId" element={<ModerationGroupImagesPage />} />
|
||||||
<Route path="/admin/deletion-log" element={<DeletionLogPage />} />
|
|
||||||
<Route path="*" element={<FZF />} />
|
<Route path="*" element={<FZF />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
264
frontend/src/Components/ComponentUtils/DeletionLogSection.js
Normal file
264
frontend/src/Components/ComponentUtils/DeletionLogSection.js
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
Grid,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
|
||||||
|
const DeletionLogSection = () => {
|
||||||
|
const [deletions, setDeletions] = useState([]);
|
||||||
|
const [statistics, setStatistics] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDeletionLog();
|
||||||
|
loadStatistics();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [showAll]);
|
||||||
|
|
||||||
|
const loadDeletionLog = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const endpoint = showAll
|
||||||
|
? '/api/admin/deletion-log/all'
|
||||||
|
: '/api/admin/deletion-log?limit=10';
|
||||||
|
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setDeletions(data.deletions || []);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden des Lösch-Logs:', error);
|
||||||
|
setError('Fehler beim Laden des Lösch-Logs');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadStatistics = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/deletion-log/stats');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setStatistics(data.statistics || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Statistiken:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (!bytes || bytes === 0) return '0 KB';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReasonIcon = (reason) => {
|
||||||
|
if (reason && reason.includes('unapproved')) {
|
||||||
|
return <WarningIcon fontSize="small" color="warning" />;
|
||||||
|
}
|
||||||
|
return <DeleteIcon fontSize="small" color="action" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<CircularProgress size={40} />
|
||||||
|
<Typography variant="body2" sx={{ mt: 2, color: '#666' }}>
|
||||||
|
Lade Lösch-Historie...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 6, mb: 4 }}>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<DeleteIcon />
|
||||||
|
Lösch-Historie
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Automatisch gelöschte Gruppen (nicht innerhalb von 7 Tagen freigegeben)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Card sx={{ p: 2, mb: 3, backgroundColor: '#ffebee' }}>
|
||||||
|
<Typography color="error">{error}</Typography>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
{statistics && (
|
||||||
|
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
||||||
|
<Typography variant="h4" color="primary" fontWeight="bold">
|
||||||
|
{statistics.totalGroupsDeleted || 0}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Gelöschte Gruppen
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
||||||
|
<Typography variant="h4" color="secondary" fontWeight="bold">
|
||||||
|
{statistics.totalImagesDeleted || 0}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Gelöschte Bilder
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
||||||
|
<Typography variant="h4" color="success.main" fontWeight="bold">
|
||||||
|
{statistics.totalStorageFreed || '0 KB'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Speicher freigegeben
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
{showAll ? 'Alle Einträge' : 'Letzte 10 Einträge'}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowAll(!showAll)}
|
||||||
|
startIcon={<InfoIcon />}
|
||||||
|
>
|
||||||
|
{showAll ? 'Nur letzte 10' : 'Alle anzeigen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deletion Log Table */}
|
||||||
|
{deletions.length === 0 ? (
|
||||||
|
<Card sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<InfoIcon sx={{ fontSize: 48, color: '#bdbdbd', mb: 1 }} />
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Keine Lösch-Einträge gefunden
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Es wurden bisher keine Gruppen automatisch gelöscht.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ backgroundColor: '#f5f5f5' }}>
|
||||||
|
<TableCell><strong>Gruppe ID</strong></TableCell>
|
||||||
|
<TableCell><strong>Jahr</strong></TableCell>
|
||||||
|
<TableCell align="right"><strong>Bilder</strong></TableCell>
|
||||||
|
<TableCell><strong>Upload-Datum</strong></TableCell>
|
||||||
|
<TableCell><strong>Gelöscht am</strong></TableCell>
|
||||||
|
<TableCell><strong>Grund</strong></TableCell>
|
||||||
|
<TableCell align="right"><strong>Größe</strong></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{deletions.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
sx={{ '&:hover': { backgroundColor: '#fafafa' } }}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
label={row.group_id}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.year || '-'}</TableCell>
|
||||||
|
<TableCell align="right">{row.image_count || 0}</TableCell>
|
||||||
|
<TableCell>{formatDate(row.upload_date)}</TableCell>
|
||||||
|
<TableCell>{formatDate(row.deleted_at)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{getReasonIcon(row.deletion_reason)}
|
||||||
|
<Typography variant="body2">
|
||||||
|
{row.deletion_reason || 'Unbekannt'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{formatFileSize(row.total_file_size)}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<Card sx={{ mt: 3, p: 2, backgroundColor: '#e3f2fd' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<InfoIcon color="info" />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" fontWeight="bold" gutterBottom>
|
||||||
|
Automatische Löschung
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Der Cleanup läuft täglich um 10:00 Uhr. Gruppen, die nicht innerhalb von 7 Tagen
|
||||||
|
freigegeben werden, werden automatisch gelöscht. Alle Lösch-Vorgänge werden hier protokolliert.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeletionLogSection;
|
||||||
|
|
@ -4,7 +4,7 @@ import { NavLink } from 'react-router-dom'
|
||||||
import '../Css/Navbar.css'
|
import '../Css/Navbar.css'
|
||||||
|
|
||||||
import logo from '../../../Images/logo.png'
|
import logo from '../../../Images/logo.png'
|
||||||
import { Lock as LockIcon, AdminPanelSettings as AdminIcon } from '@mui/icons-material';
|
import { Lock as LockIcon } from '@mui/icons-material';
|
||||||
|
|
||||||
function Navbar() {
|
function Navbar() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -15,7 +15,6 @@ function Navbar() {
|
||||||
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
|
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
|
||||||
<li><NavLink to="/slideshow" activeClassName="active">Slideshow</NavLink></li>
|
<li><NavLink to="/slideshow" activeClassName="active">Slideshow</NavLink></li>
|
||||||
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
|
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
|
||||||
<li><NavLink to="/admin/deletion-log" activeClassName="active"><AdminIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Lösch-Log</NavLink></li>
|
|
||||||
<li><NavLink className="cta" exact to="/">Upload</NavLink></li>
|
<li><NavLink className="cta" exact to="/">Upload</NavLink></li>
|
||||||
<li><a href="https://www.hobbyhimmel.de/ueber-uns/konzept/">About</a></li>
|
<li><a href="https://www.hobbyhimmel.de/ueber-uns/konzept/">About</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
Card,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
CircularProgress,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Chip,
|
|
||||||
Grid
|
|
||||||
} from '@mui/material';
|
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
|
||||||
import Footer from '../ComponentUtils/Footer';
|
|
||||||
|
|
||||||
const DeletionLogPage = () => {
|
|
||||||
const [deletions, setDeletions] = useState([]);
|
|
||||||
const [statistics, setStatistics] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [showAll, setShowAll] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadDeletionLog();
|
|
||||||
loadStatistics();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [showAll]);
|
|
||||||
|
|
||||||
const loadDeletionLog = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const endpoint = showAll
|
|
||||||
? '/api/admin/deletion-log/all'
|
|
||||||
: '/api/admin/deletion-log?limit=10';
|
|
||||||
|
|
||||||
const response = await fetch(endpoint);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setDeletions(data.deletions || []);
|
|
||||||
setError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden des Lösch-Logs:', error);
|
|
||||||
setError('Fehler beim Laden des Lösch-Logs');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadStatistics = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/deletion-log/stats');
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setStatistics(data.statistics || null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der Statistiken:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return '-';
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString('de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getReasonIcon = (reason) => {
|
|
||||||
if (reason.includes('unapproved')) {
|
|
||||||
return <WarningIcon fontSize="small" color="warning" />;
|
|
||||||
}
|
|
||||||
return <DeleteIcon fontSize="small" color="action" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="allContainer">
|
|
||||||
<Navbar />
|
|
||||||
<Container maxWidth="lg" className="page-container">
|
|
||||||
<div className="loading-container" style={{ textAlign: 'center', padding: '50px' }}>
|
|
||||||
<CircularProgress size={60} color="primary" />
|
|
||||||
<Typography variant="h6" style={{ marginTop: '20px', color: '#666666' }}>
|
|
||||||
Lade Lösch-Historie...
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="allContainer">
|
|
||||||
<Helmet>
|
|
||||||
<title>Lösch-Historie - Image Uploader</title>
|
|
||||||
</Helmet>
|
|
||||||
<Navbar />
|
|
||||||
<Container maxWidth="lg" className="page-container" style={{ marginTop: '30px', marginBottom: '30px' }}>
|
|
||||||
<Box sx={{ mb: 4 }}>
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
<DeleteIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
||||||
Lösch-Historie
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
Übersicht über automatisch gelöschte Gruppen
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Card sx={{ p: 2, mb: 3, backgroundColor: '#ffebee' }}>
|
|
||||||
<Typography color="error">{error}</Typography>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Statistics Cards */}
|
|
||||||
{statistics && (
|
|
||||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
|
||||||
<Typography variant="h3" color="primary" fontWeight="bold">
|
|
||||||
{statistics.totalGroupsDeleted || 0}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
Gelöschte Gruppen
|
|
||||||
</Typography>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
|
||||||
<Typography variant="h3" color="secondary" fontWeight="bold">
|
|
||||||
{statistics.totalImagesDeleted || 0}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
Gelöschte Bilder
|
|
||||||
</Typography>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
|
|
||||||
<Typography variant="h3" color="success.main" fontWeight="bold">
|
|
||||||
{statistics.totalStorageFreed || '0 KB'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
Speicher freigegeben
|
|
||||||
</Typography>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Toggle Button */}
|
|
||||||
<Box sx={{ mb: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<Typography variant="h6">
|
|
||||||
{showAll ? 'Alle Einträge' : 'Letzte 10 Einträge'}
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setShowAll(!showAll)}
|
|
||||||
startIcon={<InfoIcon />}
|
|
||||||
>
|
|
||||||
{showAll ? 'Nur letzte 10 anzeigen' : 'Alle anzeigen'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Deletion Log Table */}
|
|
||||||
{deletions.length === 0 ? (
|
|
||||||
<Card sx={{ p: 4, textAlign: 'center' }}>
|
|
||||||
<InfoIcon sx={{ fontSize: 60, color: '#bdbdbd', mb: 2 }} />
|
|
||||||
<Typography variant="h6" color="text.secondary">
|
|
||||||
Keine Lösch-Einträge gefunden
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Es wurden bisher keine Gruppen automatisch gelöscht.
|
|
||||||
</Typography>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table sx={{ minWidth: 650 }}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow sx={{ backgroundColor: '#f5f5f5' }}>
|
|
||||||
<TableCell><strong>Gruppe ID</strong></TableCell>
|
|
||||||
<TableCell><strong>Jahr</strong></TableCell>
|
|
||||||
<TableCell align="right"><strong>Bilder</strong></TableCell>
|
|
||||||
<TableCell><strong>Upload-Datum</strong></TableCell>
|
|
||||||
<TableCell><strong>Gelöscht am</strong></TableCell>
|
|
||||||
<TableCell><strong>Grund</strong></TableCell>
|
|
||||||
<TableCell align="right"><strong>Größe</strong></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{deletions.map((row) => (
|
|
||||||
<TableRow
|
|
||||||
key={row.id}
|
|
||||||
sx={{
|
|
||||||
'&:hover': { backgroundColor: '#fafafa' },
|
|
||||||
'&:last-child td, &:last-child th': { border: 0 }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TableCell>
|
|
||||||
<Chip
|
|
||||||
label={row.group_id}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{row.year || '-'}</TableCell>
|
|
||||||
<TableCell align="right">{row.image_count || 0}</TableCell>
|
|
||||||
<TableCell>{formatDate(row.upload_date)}</TableCell>
|
|
||||||
<TableCell>{formatDate(row.deleted_at)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
{getReasonIcon(row.deletion_reason)}
|
|
||||||
<Typography variant="body2">
|
|
||||||
{row.deletion_reason || 'Unbekannt'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{formatFileSize(row.total_file_size)}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Info Box */}
|
|
||||||
<Card sx={{ mt: 4, p: 3, backgroundColor: '#e3f2fd' }}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
|
||||||
<InfoIcon color="info" />
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|
||||||
Hinweis zur automatischen Löschung
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Gruppen, die nicht innerhalb von 7 Tagen nach dem Upload freigegeben werden,
|
|
||||||
werden automatisch gelöscht. Der Cleanup läuft täglich um 10:00 Uhr.
|
|
||||||
Alle Lösch-Vorgänge werden hier protokolliert (ohne personenbezogene Daten).
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Container>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function for file size formatting
|
|
||||||
const formatFileSize = (bytes) => {
|
|
||||||
if (!bytes || bytes === 0) return '0 KB';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeletionLogPage;
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||||||
import Footer from '../ComponentUtils/Footer';
|
import Footer from '../ComponentUtils/Footer';
|
||||||
import ImageGallery from '../ComponentUtils/ImageGallery';
|
import ImageGallery from '../ComponentUtils/ImageGallery';
|
||||||
|
import DeletionLogSection from '../ComponentUtils/DeletionLogSection';
|
||||||
import { getImageSrc } from '../../Utils/imageUtils';
|
import { getImageSrc } from '../../Utils/imageUtils';
|
||||||
|
|
||||||
const ModerationGroupsPage = () => {
|
const ModerationGroupsPage = () => {
|
||||||
|
|
@ -221,6 +222,11 @@ const ModerationGroupsPage = () => {
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Lösch-Historie */}
|
||||||
|
<section className="moderation-section">
|
||||||
|
<DeletionLogSection />
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Bilder-Modal */}
|
{/* Bilder-Modal */}
|
||||||
{showImages && selectedGroup && (
|
{showImages && selectedGroup && (
|
||||||
<ImageModal
|
<ImageModal
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user