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:
parent
6745f89f38
commit
a27a66f6ee
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
82
frontend/src/Components/ComponentUtils/ConsentBadges.js
Normal file
82
frontend/src/Components/ComponentUtils/ConsentBadges.js
Normal 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;
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user