feat: Implement moderation panel consent features

- Add ConsentBadges component with platform icons and tooltips
- Add consent filter dropdown in moderation page (all/workshop-only/platforms)
- Add export button for CSV download of consent data
- Extend /moderation/groups endpoint with filter params and consent data
- Display consent badges in ImageGalleryCard for moderation mode
- Visual distinction: workshop (green), social media (blue outlined)
- Export functionality with date-stamped CSV files

Tasks completed:
- Moderation visual consent indicators
- Moderation consent filter
- Moderation export functionality
This commit is contained in:
Matthias Lotz 2025-11-09 22:20:11 +01:00
parent 6745f89f38
commit a27a66f6ee
5 changed files with 296 additions and 64 deletions

View File

@ -33,12 +33,37 @@ router.get(endpoints.GET_ALL_GROUPS, async (req, res) => {
// Alle Gruppen für Moderation abrufen (mit Freigabestatus) - MUSS VOR den :groupId routen stehen!
router.get('/moderation/groups', async (req, res) => {
try {
const groups = await GroupRepository.getAllGroupsWithModerationInfo();
const { workshopOnly, platform } = req.query;
let groups;
if (workshopOnly === 'true') {
// Filter: Nur Gruppen mit Werkstatt-Consent aber ohne Social Media
groups = await GroupRepository.getGroupsByConsentStatus(true, null);
} else if (platform) {
// Filter: Gruppen mit bestimmter Social Media Platform
groups = await GroupRepository.getGroupsByConsentStatus(true, platform);
} else {
// Alle Gruppen mit Consent-Daten
groups = await GroupRepository.getAllGroupsWithModerationInfo();
}
// Füge Consent-Daten für jede Gruppe hinzu
const groupsWithConsents = await Promise.all(
groups.map(async (group) => {
const consents = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
return {
...group,
socialMediaConsents: consents
};
})
);
res.json({
groups,
totalCount: groups.length,
pendingCount: groups.filter(g => !g.approved).length,
approvedCount: groups.filter(g => g.approved).length
groups: groupsWithConsents,
totalCount: groupsWithConsents.length,
pendingCount: groupsWithConsents.filter(g => !g.approved).length,
approvedCount: groupsWithConsents.filter(g => g.approved).length
});
} catch (error) {
console.error('Error fetching moderation groups:', error);

View File

@ -713,71 +713,70 @@ export const uploadImageBatch = async (files, metadata, descriptions, consents)
#### Backend Tasks
**Task 1.1: Datenbank-Migrationen** ⏱️ 1-2h
- [ ] Migration 005: `display_in_workshop`, `consent_timestamp`, `management_token` zu `groups` hinzufügen
- [ ] Migration 006: `social_media_platforms` und `group_social_media_consents` Tabellen erstellen
- [ ] Standard-Plattformen (Facebook, Instagram, TikTok) einfügen
- [ ] Migrationen testen (up/down)
**Task 1.1: Datenbank-Migrationen** ⏱️ 1-2h ✅ ERLEDIGT (nur manuell)
- [x] Migration 005: `display_in_workshop`, `consent_timestamp`, `management_token` zu `groups` hinzufügen
- [x] Migration 006: `social_media_platforms` und `group_social_media_consents` Tabellen erstellen
- [x] Standard-Plattformen (Facebook, Instagram, TikTok) einfügen
- [ ] ⚠️ Automatisches Migrationssystem funktioniert nicht - nur manuelle Ausführung über sqlite3 möglich
**Task 1.2: Repository-Erweiterungen** ⏱️ 3-4h
- [ ] `GroupRepository`: `createGroupWithConsent()` implementieren
- [ ] `GroupRepository`: `getGroupWithConsents()` implementieren
- [ ] `GroupRepository`: `getGroupsByConsentStatus()` für Moderation-Filter
- [ ] `SocialMediaRepository`: Neue Klasse erstellen
- [ ] `SocialMediaRepository`: Platform-Management-Methoden
- [ ] `SocialMediaRepository`: Consent-Management-Methoden
- [ ] Unit-Tests für neue Repository-Methoden
**Task 1.2: Repository-Erweiterungen** ⏱️ 3-4h ✅ ERLEDIGT
- [x] `GroupRepository`: `createGroupWithConsent()` implementieren
- [x] `GroupRepository`: `getGroupWithConsents()` implementieren
- [x] `GroupRepository`: `getGroupsByConsentStatus()` für Moderation-Filter
- [x] `SocialMediaRepository`: Neue Klasse erstellen
- [x] `SocialMediaRepository`: Platform-Management-Methoden
- [x] `SocialMediaRepository`: Consent-Management-Methoden
- [ ] Unit-Tests für neue Repository-Methoden (TODO: später)
**Task 1.3: API-Routes** ⏱️ 3-4h
- [ ] Route `GET /api/social-media/platforms` erstellen
- [ ] Route `POST /api/groups/:groupId/consents` erstellen
- [ ] Route `GET /api/groups/:groupId/consents` erstellen
- [ ] Route `GET /api/admin/groups/by-consent` für Moderation-Filter
- [ ] Route `GET /api/admin/consents/export` für CSV/JSON Export
- [ ] Validierung und Error-Handling
- [ ] Integration-Tests für Routes
**Task 1.3: API-Routes** ⏱️ 3-4h ✅ ERLEDIGT
- [x] Route `GET /api/social-media/platforms` erstellen
- [x] Route `POST /api/groups/:groupId/consents` erstellen
- [x] Route `GET /api/groups/:groupId/consents` erstellen
- [x] Route `GET /api/admin/groups/by-consent` für Moderation-Filter
- [x] Route `GET /api/admin/consents/export` für CSV/JSON Export
- [x] Validierung und Error-Handling
- [ ] Integration-Tests für Routes (TODO: später)
**Task 1.4: Upload-Route Anpassung** ⏱️ 2h
- [ ] `batchUpload.js`: Consent-Parameter entgegennehmen
- [ ] Validierung: `workshopConsent` muss true sein
- [ ] Consent-Daten mit Gruppe speichern
- [ ] Timestamp setzen
- [ ] Response um `groupId` erweitern
- [ ] Error-Handling bei fehlender Zustimmung
**Task 1.4: Upload-Route Anpassung** ⏱️ 2h ✅ ERLEDIGT
- [x] `batchUpload.js`: Consent-Parameter entgegennehmen
- [x] Validierung: `workshopConsent` muss true sein
- [x] Consent-Daten mit Gruppe speichern
- [x] Timestamp setzen
- [x] Response um `groupId` erweitern
- [x] Error-Handling bei fehlender Zustimmung
#### Frontend Tasks
**Task 1.5: ConsentCheckboxes Komponente** ⏱️ 4-5h
- [ ] Komponente erstellen mit Material-UI
- [ ] Aufklärungstext-Alert implementieren
- [ ] Pflicht-Checkbox für Werkstatt-Anzeige
- [ ] Dynamische Plattform-Liste vom Backend laden
- [ ] Social Media Checkboxen generieren
- [ ] Icon-Mapping für Plattformen
- [ ] Widerrufs-Hinweis anzeigen
- [ ] Responsive Design
- [ ] Props für Disabled-State und onChange-Callback
**Task 1.5: ConsentCheckboxes Komponente** ⏱️ 4-5h ✅ ERLEDIGT
- [x] Komponente erstellen mit Material-UI
- [x] Aufklärungstext-Alert implementieren
- [x] Pflicht-Checkbox für Werkstatt-Anzeige
- [x] Dynamische Plattform-Liste vom Backend laden
- [x] Social Media Checkboxen generieren
- [x] Icon-Mapping für Plattformen
- [x] Widerrufs-Hinweis anzeigen
- [x] Responsive Design
- [x] Props für Disabled-State und onChange-Callback
**Task 1.6: UploadSuccessDialog Komponente** ⏱️ 2-3h
- [ ] Dialog-Komponente mit Material-UI erstellen
- [ ] Gruppen-ID prominent anzeigen
- [ ] Copy-to-Clipboard für Gruppen-ID
- [ ] Aufklärungstext über Prüfung anzeigen
- [ ] Kontakt-Information einbinden
- [ ] Responsive Design
- [ ] Animation für Success-State
**Task 1.6: UploadSuccessDialog Komponente** ⏱️ 2-3h ✅ ERLEDIGT (als inline Content)
- [x] Success-Content mit Gruppen-ID prominent anzeigen
- [x] Aufklärungstext über Prüfung anzeigen
- [x] Kontakt-Information einbinden
- [x] Responsive Design
- [x] Animation für Success-State
- [x] Inline statt Dialog (User-Request)
**Task 1.7: MultiUploadPage Integration** ⏱️ 2-3h
- [ ] State für Consents hinzufügen
- [ ] ConsentCheckboxes einbinden (vor Upload-Button)
- [ ] Upload-Button nur aktivieren wenn `workshopConsent = true`
- [ ] Consents-Validation in `handleUpload()`
- [ ] Consents an Backend senden
- [ ] UploadSuccessDialog nach Upload anzeigen
- [ ] Gruppen-ID aus Response verarbeiten
- [ ] Error-Handling für fehlende Zustimmung
**Task 1.7: MultiUploadPage Integration** ⏱️ 2-3h ✅ ERLEDIGT
- [x] State für Consents hinzufügen
- [x] ConsentCheckboxes einbinden (nach DescriptionInput - User-Request)
- [x] Upload-Button nur aktivieren wenn `workshopConsent = true`
- [x] Consents-Validation in `handleUpload()`
- [x] Consents an Backend senden
- [x] Success-Content nach Upload anzeigen (inline)
- [x] Gruppen-ID aus Response verarbeiten
- [x] Error-Handling für fehlende Zustimmung
**Task 1.8: Moderation Panel - Consent-Anzeige** ⏱️ 3-4h
**Task 1.8: Moderation Panel - Consent-Anzeige** ⏱️ 3-4h ⏳ TODO
- [ ] ConsentBadges Komponente erstellen
- [ ] Social Media Icons/Chips anzeigen
- [ ] Badges in Gruppen-Liste integrieren
@ -785,7 +784,7 @@ export const uploadImageBatch = async (files, metadata, descriptions, consents)
- [ ] Tooltip mit Consent-Timestamp
- [ ] Visuelle Unterscheidung (Werkstatt-only vs. Social Media)
**Task 1.9: Moderation Panel - Filter & Export** ⏱️ 3-4h
**Task 1.9: Moderation Panel - Filter & Export** ⏱️ 3-4h ⏳ TODO
- [ ] Filter-Dropdown für Consent-Status
- [ ] API-Abfrage mit Filter-Parametern
- [ ] Export-Button implementieren

View File

@ -0,0 +1,82 @@
import React from 'react';
import { Box, Chip, Tooltip } from '@mui/material';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import FacebookIcon from '@mui/icons-material/Facebook';
import InstagramIcon from '@mui/icons-material/Instagram';
import MusicNoteIcon from '@mui/icons-material/MusicNote';
import WorkIcon from '@mui/icons-material/Work';
const ICON_MAP = {
'Facebook': FacebookIcon,
'Instagram': InstagramIcon,
'MusicNote': MusicNoteIcon,
};
const ConsentBadges = ({ group }) => {
// Workshop consent badge (always show if consented)
const workshopBadge = group.display_in_workshop && (
<Tooltip
title={`Werkstatt-Anzeige zugestimmt am ${new Date(group.consent_timestamp).toLocaleString('de-DE')}`}
arrow
>
<Chip
icon={<WorkIcon />}
label="Werkstatt"
size="small"
sx={{
bgcolor: '#4CAF50',
color: 'white',
'& .MuiChip-icon': { color: 'white' }
}}
/>
</Tooltip>
);
// Social media consent badges
const socialMediaBadges = group.socialMediaConsents?.map(consent => {
const IconComponent = ICON_MAP[consent.icon_name] || CheckCircleIcon;
return (
<Tooltip
key={consent.platform_id}
title={`${consent.display_name} Consent am ${new Date(consent.consent_timestamp).toLocaleString('de-DE')}`}
arrow
>
<Chip
icon={<IconComponent />}
label={consent.display_name}
size="small"
variant="outlined"
sx={{
borderColor: '#2196F3',
color: '#2196F3',
'& .MuiChip-icon': { color: '#2196F3' }
}}
/>
</Tooltip>
);
});
// If no consents at all, show nothing or a neutral indicator
if (!group.display_in_workshop && (!group.socialMediaConsents || group.socialMediaConsents.length === 0)) {
return (
<Chip
label="Kein Consent"
size="small"
variant="outlined"
sx={{
borderColor: '#757575',
color: '#757575'
}}
/>
);
}
return (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
{workshopBadge}
{socialMediaBadges}
</Box>
);
};
export default ConsentBadges;

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import ConsentBadges from './ConsentBadges';
import './Css/ImageGallery.css';
import { getImageSrc, getGroupPreviewSrc } from '../../Utils/imageUtils';
@ -147,6 +148,14 @@ const ImageGalleryCard = ({
<div className="image-gallery-card-info">
<h3>{title}</h3>
{subtitle && <p className="image-gallery-card-meta">{subtitle}</p>}
{/* Consent Badges (only in moderation mode for groups) */}
{mode === 'moderation' && item.groupId && (
<div style={{ marginTop: '8px', marginBottom: '8px' }}>
<ConsentBadges group={item} />
</div>
)}
{description && (
<p className="image-gallery-card-description">{description}</p>
)}

View File

@ -1,12 +1,15 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { Container } from '@mui/material';
import { Container, Box, FormControl, InputLabel, Select, MenuItem, Button } from '@mui/material';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import FilterListIcon from '@mui/icons-material/FilterList';
import Swal from 'sweetalert2/dist/sweetalert2.js';
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 = () => {
@ -15,16 +18,53 @@ const ModerationGroupsPage = () => {
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 response = await fetch('/api/social-media/platforms');
if (response.ok) {
const data = await response.json();
setPlatforms(data);
}
} catch (error) {
console.error('Fehler beim Laden der Plattformen:', error);
}
};
const loadModerationGroups = async () => {
try {
setLoading(true);
const response = await fetch('/moderation/groups');
// Build URL with filter params
let url = '/moderation/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 response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@ -155,6 +195,41 @@ const ModerationGroupsPage = () => {
navigate(`/moderation/groups/${group.groupId}`);
};
const exportConsentData = async () => {
try {
const response = await fetch('/api/admin/consents/export?format=csv');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
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) {
console.error('Fehler beim Export:', error);
await Swal.fire({
icon: 'error',
title: 'Fehler',
text: 'Fehler beim Export der Consent-Daten: ' + error.message
});
}
};
if (loading) {
return <div className="moderation-loading">Lade Gruppen...</div>;
}
@ -194,6 +269,48 @@ const ModerationGroupsPage = () => {
</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
variant="contained"
startIcon={<FileDownloadIcon />}
onClick={exportConsentData}
sx={{
bgcolor: '#2196F3',
'&:hover': { bgcolor: '#1976D2' }
}}
>
Consent-Daten exportieren
</Button>
</Box>
{/* Wartende Gruppen */}
<section className="moderation-section">
<ImageGallery