Project-Image-Uploader/frontend/src/Components/Pages/ModerationGroupsPage.js
matthias.lotz 6effded8bf feat(frontend): Add comprehensive error handling for admin API
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
2025-11-16 18:56:21 +01:00

375 lines
14 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, 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;