Project-Image-Uploader/frontend/src/Components/Pages/ModerationGroupsPage.js
matthias.lotz 98b3616dc4 Fix: Admin deletion log, CSV export revoked consents, consent filter UI
Backend Fixes:
- Admin deletions now create deletion_log entries (admin_moderation_deletion)
- Static mount for /previews added to serve preview images
- Admin groups endpoint supports consent filter parameter

Frontend Improvements:
- Replaced consent dropdown with checkbox UI (Workshop, Facebook, Instagram, TikTok)
- Checkboxes use OR logic for filtering
- Revoked consents excluded from filter counts
- Updated ModerationGroupsPage to send consents array to backend

Infrastructure:
- Simplified nginx.conf (proxy /api/* to backend, all else to frontend)
- Fixed docker-compose port mapping (5001:5000)

Tests: 11/11 passed 
2025-11-22 11:13:10 +01:00

403 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { Container, Box, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@mui/material';
import FilterListIcon from '@mui/icons-material/FilterList';
import Swal from 'sweetalert2/dist/sweetalert2.js';
// Services
import { adminGet, adminRequest, adminDownload } from '../../services/adminApi';
import { handleAdminError } from '../../services/adminErrorHandler';
// Components
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import ImageGallery from '../ComponentUtils/ImageGallery';
import DeletionLogSection from '../ComponentUtils/DeletionLogSection';
import { getImageSrc } from '../../Utils/imageUtils';
const ModerationGroupsPage = () => {
const [groups, setGroups] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedGroup, setSelectedGroup] = useState(null);
const [showImages, setShowImages] = useState(false);
const [consentFilters, setConsentFilters] = useState({
workshop: false,
facebook: false,
instagram: false,
tiktok: false
});
const [platforms, setPlatforms] = useState([]);
const navigate = useNavigate();
useEffect(() => {
loadModerationGroups();
loadPlatforms();
}, []);
useEffect(() => {
loadModerationGroups();
}, [consentFilters]);
const loadPlatforms = async () => {
try {
const data = await adminGet('/api/admin/social-media/platforms');
setPlatforms(data);
} catch (error) {
await handleAdminError(error, 'Plattformen laden');
}
};
const loadModerationGroups = async () => {
try {
setLoading(true);
// Build URL with filter params
let url = '/api/admin/groups';
const params = new URLSearchParams();
// Sende alle aktivierten Filter
const activeFilters = Object.keys(consentFilters).filter(key => consentFilters[key]);
if (activeFilters.length > 0) {
params.append('consents', activeFilters.join(','));
}
if (params.toString()) {
url += '?' + params.toString();
}
const data = await adminGet(url);
setGroups(data.groups);
} catch (error) {
await handleAdminError(error, 'Moderations-Gruppen laden');
setError('Fehler beim Laden der Gruppen');
} finally {
setLoading(false);
}
};
const approveGroup = async (groupId, approved) => {
try {
await adminRequest(
`/api/admin/groups/${groupId}/approve`,
'PATCH',
{ approved: approved }
);
// Update local state
setGroups(groups.map(group =>
group.groupId === groupId
? { ...group, approved: approved }
: group
));
// Success feedback
await Swal.fire({
icon: 'success',
title: approved ? 'Gruppe freigegeben' : 'Freigabe zurückgezogen',
text: approved
? 'Die Gruppe ist jetzt öffentlich sichtbar.'
: 'Die Gruppe wurde zurück in "Wartend" verschoben.',
timer: 2000,
showConfirmButton: false
});
} catch (error) {
await handleAdminError(error, 'Gruppe freigeben');
await Swal.fire({
icon: 'error',
title: 'Fehler',
text: 'Fehler beim Freigeben der Gruppe: ' + error.message
});
}
};
const deleteImage = async (groupId, imageId) => {
console.log('deleteImage called with:', { groupId, imageId });
console.log('API_URL:', window._env_.API_URL);
try {
// Use admin API endpoint
const url = `/api/admin/groups/${groupId}/images/${imageId}`;
console.log('DELETE request to:', url);
await adminRequest(url, 'DELETE');
// Remove image from selectedGroup
if (selectedGroup && selectedGroup.groupId === groupId) {
const updatedImages = selectedGroup.images.filter(img => img.id !== imageId);
setSelectedGroup({
...selectedGroup,
images: updatedImages,
imageCount: updatedImages.length
});
}
// Update group image count
setGroups(groups.map(group =>
group.groupId === groupId
? { ...group, imageCount: group.imageCount - 1 }
: group
));
} catch (error) {
await handleAdminError(error, 'Bild löschen');
}
};
const deleteGroup = async (groupId) => {
if (!window.confirm('Gruppe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
return;
}
try {
await adminRequest(`/api/admin/groups/${groupId}`, 'DELETE');
setGroups(groups.filter(group => group.groupId !== groupId));
if (selectedGroup && selectedGroup.groupId === groupId) {
setSelectedGroup(null);
setShowImages(false);
}
} catch (error) {
await handleAdminError(error, 'Gruppe löschen');
}
};
// Navigate to the dedicated group images page
const viewGroupImages = (group) => {
navigate(`/moderation/groups/${group.groupId}`);
};
const exportConsentData = async () => {
try {
const blob = await adminDownload('/api/admin/consents/export?format=csv');
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `consent-export-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
await Swal.fire({
icon: 'success',
title: 'Export erfolgreich',
text: 'Consent-Daten wurden als CSV heruntergeladen.',
timer: 2000,
showConfirmButton: false
});
} catch (error) {
await handleAdminError(error, 'Consent-Export');
}
};
if (loading) {
return <div className="moderation-loading">Lade Gruppen...</div>;
}
if (error) {
return <div className="moderation-error">{error}</div>;
}
const pendingGroups = groups.filter(g => !g.approved);
const approvedGroups = groups.filter(g => g.approved);
return (
<div className="allContainer">
<Navbar />
<Helmet>
<title>Moderation - Interne Verwaltung</title>
<meta name="robots" content="noindex, nofollow, nosnippet, noarchive" />
<meta name="googlebot" content="noindex, nofollow" />
<meta name="description" content="Interne Moderationsseite - Nicht öffentlich zugänglich" />
</Helmet>
<Container className="moderation-content" maxWidth="lg" style={{ paddingTop: '20px' }}>
<h1>Moderation</h1>
<div className="moderation-stats">
<div className="stat-item">
<span className="stat-number">{pendingGroups.length}</span>
<span className="stat-label">Wartend</span>
</div>
<div className="stat-item">
<span className="stat-number">{approvedGroups.length}</span>
<span className="stat-label">Freigegeben</span>
</div>
<div className="stat-item">
<span className="stat-number">{groups.length}</span>
<span className="stat-label">Gesamt</span>
</div>
</div>
{/* Filter und Export Controls */}
<Box sx={{
display: 'flex',
gap: 2,
mb: 3,
alignItems: 'center',
flexWrap: 'wrap'
}}>
<FormControl component="fieldset" sx={{ minWidth: 250 }}>
<FormLabel component="legend" sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<FilterListIcon sx={{ mr: 0.5, fontSize: 18 }} />
Consent-Filter
</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.workshop}
onChange={(e) => setConsentFilters({...consentFilters, workshop: e.target.checked})}
size="small"
/>
}
label="Werkstatt"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.facebook}
onChange={(e) => setConsentFilters({...consentFilters, facebook: e.target.checked})}
size="small"
/>
}
label="Facebook"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.instagram}
onChange={(e) => setConsentFilters({...consentFilters, instagram: e.target.checked})}
size="small"
/>
}
label="Instagram"
/>
<FormControlLabel
control={
<Checkbox
checked={consentFilters.tiktok}
onChange={(e) => setConsentFilters({...consentFilters, tiktok: e.target.checked})}
size="small"
/>
}
label="TikTok"
/>
</FormGroup>
</FormControl>
<button
className="btn btn-success"
onClick={exportConsentData}
style={{
fontSize: '14px',
padding: '10px 20px'
}}
>
📥 Consent-Daten exportieren
</button>
</Box>
{/* Wartende Gruppen */}
<section className="moderation-section">
<ImageGallery
items={pendingGroups}
title={`🔍 Wartende Freigabe (${pendingGroups.length})`}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
isPending={true}
mode="moderation"
emptyMessage="Keine wartenden Gruppen"
/>
</section>
{/* Freigegebene Gruppen */}
<section className="moderation-section">
<ImageGallery
items={approvedGroups}
title={`✅ Freigegebene Gruppen (${approvedGroups.length})`}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
isPending={false}
mode="moderation"
emptyMessage="Keine freigegebenen Gruppen"
/>
</section>
{/* Lösch-Historie */}
<section className="moderation-section">
<DeletionLogSection />
</section>
{/* Bilder-Modal */}
{showImages && selectedGroup && (
<ImageModal
group={selectedGroup}
onClose={() => {
setShowImages(false);
setSelectedGroup(null);
}}
onDeleteImage={deleteImage}
/>
)}
</Container>
<div className="footerContainer"><Footer /></div>
</div>
);
};
// `GroupCard` has been extracted to `../ComponentUtils/GroupCard`
const ImageModal = ({ group, onClose, onDeleteImage }) => {
return (
<div className="image-modal-overlay" onClick={onClose}>
<div className="image-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>{group.title}</h2>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
<div className="group-details">
<p><strong>Jahr:</strong> {group.year}</p>
<p><strong>Ersteller:</strong> {group.name}</p>
{group.description && (
<p><strong>Beschreibung:</strong> {group.description}</p>
)}
<p><strong>Bilder:</strong> {group.images.length}</p>
</div>
<div className="images-grid">
{group.images.map(image => (
<div key={image.id} className="image-item">
<img
src={getImageSrc(image, true)}
alt={image.originalName}
className="modal-image"
/>
<div className="image-actions">
<span className="image-name">{image.originalName}</span>
<button
className="btn btn-danger btn-sm"
onClick={() => onDeleteImage(group.groupId, image.id)}
title="Bild löschen"
>
🗑
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default ModerationGroupsPage;