diff --git a/backend/src/routes/groups.js b/backend/src/routes/groups.js index 5523578..0c2e2af 100644 --- a/backend/src/routes/groups.js +++ b/backend/src/routes/groups.js @@ -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); diff --git a/docs/FEATURE_PLAN-social-media.md b/docs/FEATURE_PLAN-social-media.md index 931e123..d1b2ea5 100644 --- a/docs/FEATURE_PLAN-social-media.md +++ b/docs/FEATURE_PLAN-social-media.md @@ -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 diff --git a/frontend/src/Components/ComponentUtils/ConsentBadges.js b/frontend/src/Components/ComponentUtils/ConsentBadges.js new file mode 100644 index 0000000..421c51c --- /dev/null +++ b/frontend/src/Components/ComponentUtils/ConsentBadges.js @@ -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 && ( + + } + label="Werkstatt" + size="small" + sx={{ + bgcolor: '#4CAF50', + color: 'white', + '& .MuiChip-icon': { color: 'white' } + }} + /> + + ); + + // Social media consent badges + const socialMediaBadges = group.socialMediaConsents?.map(consent => { + const IconComponent = ICON_MAP[consent.icon_name] || CheckCircleIcon; + return ( + + } + label={consent.display_name} + size="small" + variant="outlined" + sx={{ + borderColor: '#2196F3', + color: '#2196F3', + '& .MuiChip-icon': { color: '#2196F3' } + }} + /> + + ); + }); + + // If no consents at all, show nothing or a neutral indicator + if (!group.display_in_workshop && (!group.socialMediaConsents || group.socialMediaConsents.length === 0)) { + return ( + + ); + } + + return ( + + {workshopBadge} + {socialMediaBadges} + + ); +}; + +export default ConsentBadges; diff --git a/frontend/src/Components/ComponentUtils/ImageGalleryCard.js b/frontend/src/Components/ComponentUtils/ImageGalleryCard.js index bc96879..229adb5 100644 --- a/frontend/src/Components/ComponentUtils/ImageGalleryCard.js +++ b/frontend/src/Components/ComponentUtils/ImageGalleryCard.js @@ -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 = ({

{title}

{subtitle &&

{subtitle}

} + + {/* Consent Badges (only in moderation mode for groups) */} + {mode === 'moderation' && item.groupId && ( +
+ +
+ )} + {description && (

{description}

)} diff --git a/frontend/src/Components/Pages/ModerationGroupsPage.js b/frontend/src/Components/Pages/ModerationGroupsPage.js index d26fe8e..a0776a6 100644 --- a/frontend/src/Components/Pages/ModerationGroupsPage.js +++ b/frontend/src/Components/Pages/ModerationGroupsPage.js @@ -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
Lade Gruppen...
; } @@ -194,6 +269,48 @@ const ModerationGroupsPage = () => {
+ {/* Filter und Export Controls */} + + + + + Consent-Filter + + + + + + + {/* Wartende Gruppen */}