Phase 2: User-Friendly Error Handling ✅ Error Handler Service: - Created adminErrorHandler.js with handleAdminError() - User-friendly SweetAlert2 dialogs for all error types: * 403 Unauthorized - Clear admin token instructions * 429 Rate Limit - Wait and retry message * 404 Not Found - Resource not found * 500 Server Error - Internal server error * Generic errors with context ✅ Integrated Error Handling in all Admin Components: - ModerationGroupsPage.js (all 6 admin operations) - ModerationGroupImagesPage.js (group loading) - DeletionLogSection.js (log loading + statistics) - ConsentCheckboxes.js (platform loading) ✅ Error Context Messages: - "Gruppe laden" - "Gruppe freigeben" - "Gruppe löschen" - "Bild löschen" - "Consent-Export" - "Plattformen laden" - "Lösch-Log laden" - "Statistiken laden" ✨ Benefits: - Clear technical details for admins in error dialogs - Context-specific error messages - Consistent error handling across all admin features - Better debugging with detailed 403 instructions
375 lines
14 KiB
JavaScript
375 lines
14 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { Helmet } from 'react-helmet';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Container, Box, FormControl, InputLabel, Select, MenuItem } 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 ConsentBadges from '../ComponentUtils/ConsentBadges';
|
||
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 [consentFilter, setConsentFilter] = useState('all');
|
||
const [platforms, setPlatforms] = useState([]);
|
||
const navigate = useNavigate();
|
||
|
||
useEffect(() => {
|
||
loadModerationGroups();
|
||
loadPlatforms();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadModerationGroups();
|
||
}, [consentFilter]);
|
||
|
||
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();
|
||
|
||
if (consentFilter !== 'all') {
|
||
if (consentFilter === 'workshop-only') {
|
||
params.append('workshopOnly', 'true');
|
||
} else {
|
||
// Platform filter (facebook, instagram, tiktok)
|
||
params.append('platform', consentFilter);
|
||
}
|
||
}
|
||
|
||
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 sx={{ minWidth: 250 }} size="small">
|
||
<InputLabel id="consent-filter-label">
|
||
<FilterListIcon sx={{ mr: 0.5, fontSize: 18, verticalAlign: 'middle' }} />
|
||
Consent-Filter
|
||
</InputLabel>
|
||
<Select
|
||
labelId="consent-filter-label"
|
||
value={consentFilter}
|
||
label="Consent-Filter"
|
||
onChange={(e) => setConsentFilter(e.target.value)}
|
||
>
|
||
<MenuItem value="all">Alle Gruppen</MenuItem>
|
||
<MenuItem value="workshop-only">Nur Werkstatt-Consent</MenuItem>
|
||
{platforms.map(platform => (
|
||
<MenuItem key={platform.id} value={platform.platform_name}>
|
||
{platform.display_name}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</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;
|