From 324c46d7358501bb13b26d066c77ed6264ee8328 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Fri, 14 Nov 2025 14:38:03 +0100 Subject: [PATCH] feat(phase2): Complete Management Portal with reusable ConsentCheckboxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 Frontend completed (Tasks 12-17, 19-20) - 14. Nov 2025 Backend Enhancements: - Enhanced PUT /api/manage/:token/consents to support creating new consents - INSERT new consent row when restoring consent for platform not selected during upload - Enables granting consents for previously unselected platforms Frontend Refactoring (Code Deduplizierung): - Extended ConsentCheckboxes component for both modes (upload & manage) - Removed ~150 lines of duplicated consent UI code from ManagementPortalPage - New mode prop: 'upload' (default) | 'manage' - Dynamic hint texts and validation rules based on mode - Workshop consent required only in upload mode ManagementPortalPage Updates: - Replaced custom consent UI with reusable ConsentCheckboxes component - New state currentConsents tracks checkbox values - New handler handleConsentChange() computes changes vs original - Local change collection with batch save on button click - Email link for social media post deletion (mailto workaround) - Save/Discard buttons only visible when pending changes exist ConsentBadges Fix: - Now correctly displays only active (non-revoked) consents - Updates properly after consent revocation Documentation: - Updated FEATURE_PLAN with Phase 2 Frontend completion status - Added refactoring section documenting code deduplizierung - Updated README with Management Portal features - Documented email backend solution requirement (future work) Results: ✅ 100% consistent UI between upload and management ✅ Zero code duplication for consent handling ✅ ConsentBadges correctly filters revoked consents ✅ Backend supports granting new consents after upload ✅ Management link displayed on upload success page ✅ All manual tests passed Tasks Completed: - Task 12: Management Portal UI (/manage/:token) - Task 13: Consent Management (revoke/restore) - Task 14: Metadata Editor (title/description) - Task 15: Image Management (add/delete) - Task 16: Group Deletion (with confirmation) - Task 17: Upload Success Page (management link) - Task 19: Documentation updates - Task 20: nginx routing configuration Pending: - Task 18: E2E Testing (formal test suite) --- README.md | 28 +- backend/src/routes/management.js | 35 +- docs/FEATURE_PLAN-social-media.md | 89 +++- .../ComponentUtils/ConsentBadges.js | 48 +- .../MultiUpload/ConsentCheckboxes.js | 68 ++- .../Components/Pages/ManagementPortalPage.js | 465 ++++++++++-------- 6 files changed, 458 insertions(+), 275 deletions(-) diff --git a/README.md b/README.md index 27c4419..7fae74e 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,18 @@ This project extends the original [Image-Uploader by vallezw](https://github.com - Consent badges and filtering in moderation panel - CSV/JSON export for legal documentation - Group ID tracking for consent withdrawal requests -- **🔑 Self-Service Management Portal** (Phase 2 Backend Complete - Nov 11): +- **🔑 Self-Service Management Portal** (Phase 2 Complete - Nov 11-14): - Secure UUID-based management tokens for user self-service - - Token-based API for consent revocation and metadata editing + - Frontend portal at `/manage/:token` for consent management + - Revoke/restore consents for workshop and social media + - Edit metadata (title, description) after upload - Add/delete images after upload (with moderation re-approval) - Complete group deletion with audit trail + - Reusable ConsentCheckboxes component (no code duplication) + - Email link for social media post deletion requests - IP-based rate limiting (10 requests/hour) - Brute-force protection (20 failed attempts → 24h ban) - Management audit log for security tracking - - Frontend portal coming soon (Tasks 12-18) - **� Slideshow Optimization**: Intelligent image preloading eliminates loading delays and duplicate images - **📅 Chronological Display**: Slideshows now play in chronological order (year → upload date) - **Automatic Cleanup**: Unapproved groups are automatically deleted after 7 days @@ -108,9 +111,26 @@ docker compose -f docker/dev/docker-compose.yml up -d - ✅ **Workshop Display**: Required consent to display images on local monitor - ☐ **Social Media** (optional): Per-platform consent for Facebook, Instagram, TikTok 5. Click "Upload Images" to process the batch -6. Receive your **Group ID** as reference for future contact +6. Receive your **Group ID** and **Management Link** as reference 7. Images are grouped and await moderation approval +### Self-Service Management Portal + +After upload, users receive a unique management link (`/manage/:token`) to: + +- **View Upload**: See all images and metadata +- **Manage Consents**: Revoke or restore workshop/social media consents +- **Edit Metadata**: Update title, description, year (triggers re-moderation) +- **Manage Images**: Add new images or delete existing ones +- **Delete Group**: Complete removal with double-confirmation +- **Email Contact**: Request deletion of already published social media posts + +**Security Features**: +- No authentication required (token-based access) +- Rate limiting: 10 requests per hour per IP +- Brute-force protection: 20 failed attempts → 24h ban +- Complete audit trail of all management actions + ### Slideshow Mode - **Automatic Access**: Navigate to `http://localhost/slideshow` diff --git a/backend/src/routes/management.js b/backend/src/routes/management.js index 49d9eb0..f36baa1 100644 --- a/backend/src/routes/management.js +++ b/backend/src/routes/management.js @@ -160,9 +160,40 @@ router.put('/:token/consents', async (req, res) => { const socialMediaRepo = new SocialMediaRepository(dbManager); if (action === 'revoke') { - await socialMediaRepo.revokeConsent(groupData.groupId, platformId); + // Check if consent exists before revoking + const existing = await dbManager.get( + 'SELECT id FROM group_social_media_consents WHERE group_id = ? AND platform_id = ?', + [groupData.groupId, platformId] + ); + + if (existing) { + await socialMediaRepo.revokeConsent(groupData.groupId, platformId); + } else { + // Can't revoke what doesn't exist - return error + return res.status(400).json({ + success: false, + error: 'Cannot revoke consent that was never granted' + }); + } } else { - await socialMediaRepo.restoreConsent(groupData.groupId, platformId); + // action === 'restore' + // Check if consent exists + const existing = await dbManager.get( + 'SELECT id, revoked FROM group_social_media_consents WHERE group_id = ? AND platform_id = ?', + [groupData.groupId, platformId] + ); + + if (existing) { + // Restore existing consent + await socialMediaRepo.restoreConsent(groupData.groupId, platformId); + } else { + // Create new consent (user wants to grant consent for a platform they didn't select during upload) + await dbManager.run( + `INSERT INTO group_social_media_consents (group_id, platform_id, consented, consent_timestamp) + VALUES (?, ?, 1, CURRENT_TIMESTAMP)`, + [groupData.groupId, platformId] + ); + } } return res.json({ diff --git a/docs/FEATURE_PLAN-social-media.md b/docs/FEATURE_PLAN-social-media.md index 17f2487..2d18493 100644 --- a/docs/FEATURE_PLAN-social-media.md +++ b/docs/FEATURE_PLAN-social-media.md @@ -5,7 +5,7 @@ **Feature**: Einwilligungsverwaltung für Bildveröffentlichung in Werkstatt und Social Media **Ziel**: Rechtskonforme Einholung und Verwaltung von Nutzerzustimmungen für die Anzeige von Bildern in der Werkstatt und Veröffentlichung auf Social Media Plattformen **Priorität**: High (Rechtliche Anforderung) -**Status**: ✅ Phase 1 komplett (9-10. Nov 2025) | ✅ Phase 2 Backend komplett (11. Nov 2025) +**Status**: ✅ Phase 1 komplett (9-10. Nov 2025) | ✅ Phase 2 Backend komplett (11. Nov 2025) | ✅ Phase 2 Frontend komplett (13-14. Nov 2025) **API-Endpoints**: - ✅ `GET /api/social-media/platforms` - Liste aktiver Social Media Plattformen - ✅ `POST /api/groups/:groupId/consents` - Consents speichern @@ -1066,23 +1066,23 @@ MANAGEMENT_TOKEN_EXPIRY=90 - ✅ Task 10: Management Audit-Log (Migration 007, Repository, Admin-Endpoints) - ✅ Task 11: Widerruf-Verhalten validiert (Workshop: display_in_workshop=0, Social Media: revoked=1) -**Frontend (Tasks 12-18) - ⏳ IN ARBEIT (13. Nov 2025)**: +**Frontend (Tasks 12-18) - ✅ KOMPLETT (14. Nov 2025)**: - ✅ Task 12: Management Portal Grundgerüst (/manage/:token Route) - KOMPLETT -- ⏳ Task 13: Consent-Management UI (Widerruf/Wiederherstellen) - KOMPLETT (in Task 12 integriert) -- ⏳ Task 14: Metadata-Edit UI (Titel/Beschreibung ändern) - KOMPLETT (in Task 12 integriert) -- ⏳ Task 15: Bilder-Management UI (Hinzufügen/Löschen) - KOMPLETT (in Task 12 integriert) -- ⏳ Task 16: Gruppe löschen UI (mit Bestätigung) - KOMPLETT (in Task 12 integriert) -- ⏳ Task 17: Upload-Erfolgsseite (Management-Link prominent anzeigen) +- ✅ Task 13: Consent-Management UI (Widerruf/Wiederherstellen) - KOMPLETT +- ✅ Task 14: Metadata-Edit UI (Titel/Beschreibung ändern) - KOMPLETT +- ✅ Task 15: Bilder-Management UI (Hinzufügen/Löschen) - KOMPLETT +- ✅ Task 16: Gruppe löschen UI (mit Bestätigung) - KOMPLETT +- ✅ Task 17: Upload-Erfolgsseite (Management-Link prominent angezeigt) - ⏳ Task 18: E2E Testing (alle Flows testen) -**Dokumentation & Deployment (Tasks 19-20) - ⏳ IN ARBEIT (13. Nov 2025)**: -- ⏳ Task 19: Dokumentation aktualisieren +**Dokumentation & Deployment (Tasks 19-20) - ✅ KOMPLETT (14. Nov 2025)**: +- ✅ Task 19: Dokumentation aktualisieren - ✅ Task 20: nginx Konfiguration (/api/manage/* Routing) - KOMPLETT **Zeitaufwand Phase 2**: - Backend: 1 Tag (11. Nov 2025) - ✅ komplett -- Frontend Tasks 12 & 20: 1 Tag (13. Nov 2025) - ✅ komplett -- Testing & Deployment: Geplant ~1 Tag +- Frontend Tasks 12-16 & 20: 2 Tage (13-14. Nov 2025) - ✅ komplett +- Testing & Deployment: Tasks 17-18 geplant ~0.5 Tag ## 🐛 Bekannte Issues & Fixes @@ -1291,6 +1291,50 @@ MANAGEMENT_TOKEN_EXPIRY=90 --- +### Phase 2 Frontend Refactoring (14. Nov 2025) + +**Ziel**: Code-Deduplizierung durch Wiederverwendung der `ConsentCheckboxes` Komponente + +**Problem**: +- ManagementPortalPage hatte komplett eigene Consent-UI (Buttons, Chips, Status-Anzeige) +- ConsentCheckboxes wurde nur beim Upload verwendet +- ~150 Zeilen duplizierter UI-Code für die gleiche Funktionalität +- User-Feedback: "Warum haben wir beim Upload eine andere GUI als beim ManagementPortalPage.js obwohl ich ausdrücklich auf Wiederverwendung hingewiesen habe?" + +**Lösung**: +- ✅ **ConsentCheckboxes erweitert** für beide Modi (`mode='upload'` | `mode='manage'`) + - Neue Props: `mode`, `groupId` + - Dynamische Hinweis-Texte je nach Modus + - Werkstatt-Pflichtfeld nur im Upload-Modus + - Widerrufs-Hinweis nur im Upload-Modus + +- ✅ **ManagementPortalPage refactored**: + - Custom Consent-UI komplett entfernt (~150 Zeilen gelöscht) + - Ersetzt durch `` + - Neuer State `currentConsents` - speichert Checkbox-Zustände + - Neue Funktion `handleConsentChange()` - berechnet Änderungen vs. Original + - Speicher-Button-Sektion separat (nur bei pending changes sichtbar) + - Email-Link für Social Media Widerruf unterhalb der Checkboxen + +- ✅ **ConsentBadges gefixed**: + - Filter für Social Media Consents: `consented && !revoked` + - Zeigt nur **aktive** (nicht-widerrufene) Consents an + - Aktualisiert sich korrekt nach Consent-Widerruf + +**Ergebnis**: +- ✅ Gleiche UI für Upload und Management (100% konsistent) +- ✅ ~150 Zeilen Code eliminiert +- ✅ Keine Duplikation mehr +- ✅ Wartbarkeit verbessert (nur eine Komponente zu pflegen) +- ✅ ConsentBadges zeigt korrekten Status nach Änderungen + +**Geänderte Dateien**: +- `frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js` - Mode-Support hinzugefügt +- `frontend/src/Components/Pages/ManagementPortalPage.js` - Custom UI entfernt, ConsentCheckboxes integriert +- `frontend/src/Components/ComponentUtils/ConsentBadges.js` - Filter für revoked Consents + +--- + **Management Portal APIs** (alle getestet): - ✅ `GET /api/manage/:token` - Token validieren & Gruppendaten laden - ✅ `PUT /api/manage/:token/consents` - Consents widerrufen/wiederherstellen @@ -1336,7 +1380,28 @@ MANAGEMENT_TOKEN_EXPIRY=90 - Nutzt vorhandene Datenbank-Infrastruktur - Integration in bestehendes Moderation-Panel -## 📚 Referenzen +## � Bekannte Einschränkungen & Verbesserungsvorschläge + +### mailto: Link Problem (14. Nov 2025) +**Problem**: Der mailto: Link zum Kontakt für Löschung bereits veröffentlichter Social Media Posts öffnet nicht zuverlässig den nativen Mail-Client in allen Browser/OS-Kombinationen. + +**Aktueller Workaround**: Einfacher HTML `` Link mit vereinfachtem Body-Text (keine Zeilenumbrüche). + +**Geplante Lösung**: +- **E-Mail Backend-Service** implementieren +- Backend-Endpoint: `POST /api/manage/:token/request-deletion` +- Payload: `{ platforms: ['facebook', 'instagram'], message: string }` +- Backend sendet E-Mail via `nodemailer` an it@hobbyhimmel.de +- Vorteile: + - Unabhängig von Browser/OS Mail-Client Konfiguration + - Bessere Nachverfolgbarkeit (Audit-Log) + - Strukturierte E-Mail-Vorlage mit allen relevanten Infos (Gruppen-ID, Plattformen, Timestamp) + - User-Feedback (Bestätigung dass Anfrage eingegangen ist) + - Spam-Schutz & Rate-Limiting möglich + +**Priorität**: Medium (funktionaler Workaround vorhanden, aber UX nicht optimal) + +## �📚 Referenzen - [DSGVO Art. 7 - Bedingungen für die Einwilligung](https://dsgvo-gesetz.de/art-7-dsgvo/) - [Material-UI Checkbox Documentation](https://mui.com/material-ui/react-checkbox/) diff --git a/frontend/src/Components/ComponentUtils/ConsentBadges.js b/frontend/src/Components/ComponentUtils/ConsentBadges.js index 421c51c..7a516de 100644 --- a/frontend/src/Components/ComponentUtils/ConsentBadges.js +++ b/frontend/src/Components/ComponentUtils/ConsentBadges.js @@ -32,29 +32,31 @@ const ConsentBadges = ({ group }) => { ); - // 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' } - }} - /> - - ); - }); + // Social media consent badges - only show active (not revoked) consents + const socialMediaBadges = group.socialMediaConsents + ?.filter(consent => consent.consented && !consent.revoked) + .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)) { diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js index 9b30d41..a4b489e 100644 --- a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js +++ b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js @@ -25,8 +25,21 @@ const ICON_MAP = { * GDPR-konforme Einwilligungsabfrage für Bildveröffentlichung * - Pflicht: Werkstatt-Anzeige Zustimmung * - Optional: Social Media Plattform-Zustimmungen + * + * @param {Object} props + * @param {Function} props.onConsentChange - Callback wenn sich Consents ändern + * @param {Object} props.consents - Aktueller Consent-Status + * @param {boolean} props.disabled - Ob Checkboxen deaktiviert sind + * @param {string} props.mode - 'upload' (default) oder 'manage' (für Management Portal) + * @param {string} props.groupId - Gruppen-ID (nur für 'manage' Modus) */ -function ConsentCheckboxes({ onConsentChange, consents, disabled = false }) { +function ConsentCheckboxes({ + onConsentChange, + consents, + disabled = false, + mode = 'upload', + groupId = null +}) { const [platforms, setPlatforms] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -83,6 +96,9 @@ function ConsentCheckboxes({ onConsentChange, consents, disabled = false }) { return consents.socialMediaConsents?.some(c => c.platformId === platformId) || false; }; + const isManageMode = mode === 'manage'; + const isUploadMode = mode === 'upload'; + return ( } sx={{ mb: 3 }}> - Wichtiger Hinweis - - - Alle hochgeladenen Inhalte werden vom Hobbyhimmel-Team geprüft, bevor sie - angezeigt oder veröffentlicht werden. Wir behalten uns vor, Inhalte nicht - zu zeigen oder rechtswidrige Inhalte zu entfernen. - - - Nach dem Upload erhalten Sie eine Gruppen-ID als Referenz für die Kontaktaufnahme. + {isUploadMode ? 'Wichtiger Hinweis' : 'Einwilligungen verwalten'} + {isUploadMode ? ( + <> + + Alle hochgeladenen Inhalte werden vom Hobbyhimmel-Team geprüft, bevor sie + angezeigt oder veröffentlicht werden. Wir behalten uns vor, Inhalte nicht + zu zeigen oder rechtswidrige Inhalte zu entfernen. + + + Nach dem Upload erhalten Sie eine Gruppen-ID als Referenz für die Kontaktaufnahme. + + + ) : ( + + Sie können Ihre Einwilligungen jederzeit widerrufen oder erteilen. + Änderungen werden erst nach dem Speichern übernommen. + + )} {/* Pflicht-Zustimmung: Werkstatt-Anzeige */} - Anzeige in der Werkstatt * + Anzeige in der Werkstatt {isUploadMode && '*'} (Pflichtfeld) + über das Internet zugänglich gemacht. + {isUploadMode && (Pflichtfeld)} } /> @@ -192,13 +218,15 @@ function ConsentCheckboxes({ onConsentChange, consents, disabled = false }) { {/* Widerrufs-Hinweis */} - - - Widerruf Ihrer Einwilligung: Sie können Ihre Einwilligung - jederzeit widerrufen. Kontaktieren Sie uns hierzu mit Ihrer Gruppen-ID unter:{' '} - it@hobbyhimmel.de - - + {isUploadMode && ( + + + Widerruf Ihrer Einwilligung: Sie können Ihre Einwilligung + jederzeit widerrufen. Kontaktieren Sie uns hierzu mit Ihrer Gruppen-ID unter:{' '} + it@hobbyhimmel.de + + + )} ); } diff --git a/frontend/src/Components/Pages/ManagementPortalPage.js b/frontend/src/Components/Pages/ManagementPortalPage.js index 3d0bdce..be03046 100644 --- a/frontend/src/Components/Pages/ManagementPortalPage.js +++ b/frontend/src/Components/Pages/ManagementPortalPage.js @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Button, Container, Box, Typography, Paper, Divider, Chip } from '@mui/material'; +import { Button, Container, Box, Typography, Paper } from '@mui/material'; import Swal from 'sweetalert2/dist/sweetalert2.js'; import 'sweetalert2/src/sweetalert2.scss'; @@ -11,11 +11,11 @@ import ImageGallery from '../ComponentUtils/ImageGallery'; import ImageGalleryCard from '../ComponentUtils/ImageGalleryCard'; import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput'; import ConsentBadges from '../ComponentUtils/ConsentBadges'; +import ConsentCheckboxes from '../ComponentUtils/MultiUpload/ConsentCheckboxes'; // Icons -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import CancelIcon from '@mui/icons-material/Cancel'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import CancelIcon from '@mui/icons-material/Cancel'; const ManagementPortalPage = () => { const { token } = useParams(); @@ -36,11 +36,56 @@ const ManagementPortalPage = () => { }); const [imageDescriptions, setImageDescriptions] = useState({}); const [isEditMode, setIsEditMode] = useState(false); + + // Pending consent changes (collected locally before saving) + const [pendingConsentChanges, setPendingConsentChanges] = useState([]); + + // Current consents (for ConsentCheckboxes component - includes pending changes) + const [currentConsents, setCurrentConsents] = useState({ + workshopConsent: false, + socialMediaConsents: [] + }); + + // All available social media platforms + const [allPlatforms, setAllPlatforms] = useState([]); useEffect(() => { loadGroup(); + loadAllPlatforms(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); + + // Reset pending changes when group is reloaded + useEffect(() => { + if (group) { + setPendingConsentChanges([]); + // Initialize currentConsents from group data + const workshopStatus = group.consents?.workshopConsent || false; + const socialMediaStatus = allPlatforms.map(platform => { + const consent = group.consents?.socialMediaConsents?.find(c => c.platformId === platform.id); + const isActive = consent ? (consent.consented && !consent.revoked) : false; + return isActive ? { platformId: platform.id, consented: true } : null; + }).filter(Boolean); + + setCurrentConsents({ + workshopConsent: workshopStatus, + socialMediaConsents: socialMediaStatus + }); + } + }, [group, allPlatforms]); + + const loadAllPlatforms = async () => { + try { + const res = await fetch('/api/social-media/platforms'); + if (res.ok) { + const data = await res.json(); + // Backend returns array directly, not wrapped in {platforms: [...]} + setAllPlatforms(Array.isArray(data) ? data : []); + } + } catch (e) { + console.error('Error loading platforms:', e); + } + }; const loadGroup = useCallback(async () => { try { @@ -221,141 +266,94 @@ const ManagementPortalPage = () => { } }; - // Handle consent revocation - const handleRevokeConsent = async (consentType, platformId = null) => { - const consentName = consentType === 'workshop' - ? 'Werkstatt-Anzeige' - : group.consents.socialMediaConsents.find(c => c.platformId === platformId)?.platformDisplayName || 'Social Media'; - - if (consentType === 'workshop') { - const result = await Swal.fire({ - title: `Einwilligung widerrufen?`, - html: `Möchten Sie Ihre Einwilligung für ${consentName} widerrufen?

- Ihre Bilder werden aus der Werkstatt-Anzeige entfernt.`, - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - confirmButtonText: 'Ja, widerrufen', - cancelButtonText: 'Abbrechen' - }); - - if (!result.isConfirmed) return; - } else { - // Social Media Widerruf - const result = await Swal.fire({ - title: `Einwilligung widerrufen?`, - html: `Möchten Sie Ihre Einwilligung für ${consentName} widerrufen?

- Ihre Bilder werden nicht mehr auf ${consentName} veröffentlicht.
- Bereits veröffentlichte Beiträge bleiben bestehen, aber es werden keine neuen Posts mit Ihren Bildern erstellt.
`, - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - confirmButtonText: 'Ja, widerrufen', - cancelButtonText: 'Abbrechen', - footer: `
Wenn Sie die Löschung bereits veröffentlichter Beiträge wünschen, kontaktieren Sie uns nach dem Widerruf.
` - }); - - if (!result.isConfirmed) return; - } - - try { - const payload = consentType === 'workshop' - ? { consentType: 'workshop', action: 'revoke' } - : { consentType: 'social_media', action: 'revoke', platformId }; - - const res = await fetch(`/api/manage/${token}/consents`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error(body.error || 'Fehler beim Widerrufen'); - } - - // Erfolg - zeige Bestätigung mit Kontaktinfo für Social Media - if (consentType === 'social-media') { - const mailtoLink = `mailto:it@hobbyhimmel.de?subject=${encodeURIComponent(`Löschung Social Media Post - Gruppe ${group.groupId}`)}&body=${encodeURIComponent(`Hallo,\n\nBitte löschen Sie die bereits veröffentlichten Beiträge meiner Gruppe ${group.groupId} von ${consentName}.\n\nVielen Dank`)}`; - - await Swal.fire({ - icon: 'success', - title: 'Einwilligung widerrufen', - html: `Ihre Einwilligung für ${consentName} wurde widerrufen.

-
- Bereits veröffentlichte Beiträge löschen?
- Kontaktieren Sie uns mit Ihrer Gruppen-ID:
-
- Gruppen-ID: ${group.groupId}
- E-Mail: it@hobbyhimmel.de -
-
`, - confirmButtonText: 'Verstanden' - }); - } else { - await Swal.fire({ - icon: 'success', - title: 'Einwilligung widerrufen', - text: `Ihre Einwilligung für ${consentName} wurde widerrufen.`, - timer: 2000, - showConfirmButton: false - }); - } - - // Reload group to get updated consent status - await loadGroup(); - - } catch (error) { - console.error('Error revoking consent:', error); - Swal.fire({ - icon: 'error', - title: 'Fehler', - text: error.message || 'Einwilligung konnte nicht widerrufen werden' + // Handle consent changes from ConsentCheckboxes component + const handleConsentChange = (newConsents) => { + setCurrentConsents(newConsents); + + if (!group) return; + + const changes = []; + + // Check workshop consent change + const originalWorkshop = group.consents?.workshopConsent || false; + if (newConsents.workshopConsent !== originalWorkshop) { + changes.push({ + consentType: 'workshop', + action: newConsents.workshopConsent ? 'restore' : 'revoke', + platformId: null }); } + + // Check social media consent changes + allPlatforms.forEach(platform => { + const consent = group.consents?.socialMediaConsents?.find(c => c.platformId === platform.id); + const originalStatus = consent ? (consent.consented && !consent.revoked) : false; + const newStatus = newConsents.socialMediaConsents?.some(c => c.platformId === platform.id) || false; + + if (newStatus !== originalStatus) { + changes.push({ + consentType: 'social_media', + action: newStatus ? 'restore' : 'revoke', + platformId: platform.id + }); + } + }); + + setPendingConsentChanges(changes); }; - // Handle consent restoration - const handleRestoreConsent = async (consentType, platformId = null) => { - const consentName = consentType === 'workshop' - ? 'Werkstatt-Anzeige' - : group.consents.socialMediaConsents.find(c => c.platformId === platformId)?.platformDisplayName || 'Social Media'; - - const result = await Swal.fire({ - title: `Einwilligung wiederherstellen?`, - html: `Möchten Sie Ihre Einwilligung für ${consentName} wiederherstellen?`, - icon: 'question', - showCancelButton: true, - confirmButtonColor: '#28a745', - cancelButtonColor: '#6c757d', - confirmButtonText: 'Ja, wiederherstellen', - cancelButtonText: 'Abbrechen' + // Handle consent revocation (collect locally, don't save yet) + const handleRevokeConsent = (consentType, platformId = null) => { + const change = { consentType, action: 'revoke', platformId }; + setPendingConsentChanges(prev => { + // Remove any previous change for the same consent + const filtered = prev.filter(c => + !(c.consentType === consentType && c.platformId === platformId) + ); + return [...filtered, change]; }); + }; - if (!result.isConfirmed) return; - + // Handle consent restoration (collect locally, don't save yet) + const handleRestoreConsent = (consentType, platformId = null) => { + const change = { consentType, action: 'restore', platformId }; + setPendingConsentChanges(prev => { + // Remove any previous change for the same consent + const filtered = prev.filter(c => + !(c.consentType === consentType && c.platformId === platformId) + ); + return [...filtered, change]; + }); + }; + + // Save all pending consent changes + const handleSaveConsentChanges = async () => { + if (pendingConsentChanges.length === 0) return; + + setSaving(true); try { - const payload = consentType === 'workshop' - ? { consentType: 'workshop', action: 'restore' } - : { consentType: 'social_media', action: 'restore', platformId }; + // Send all changes to backend + for (const change of pendingConsentChanges) { + const payload = change.consentType === 'workshop' + ? { consentType: 'workshop', action: change.action } + : { consentType: 'social_media', action: change.action, platformId: change.platformId }; - const res = await fetch(`/api/manage/${token}/consents`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + const res = await fetch(`/api/manage/${token}/consents`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); - if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error(body.error || 'Fehler beim Wiederherstellen'); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || 'Fehler beim Speichern'); + } } - + await Swal.fire({ icon: 'success', - title: 'Einwilligung wiederhergestellt', - text: `Ihre Einwilligung für ${consentName} wurde wiederhergestellt.`, + title: 'Änderungen gespeichert', + text: 'Ihre Einwilligungsänderungen wurden erfolgreich gespeichert.', timer: 2000, showConfirmButton: false }); @@ -364,14 +362,58 @@ const ManagementPortalPage = () => { await loadGroup(); } catch (error) { - console.error('Error restoring consent:', error); + console.error('Error saving consent changes:', error); Swal.fire({ icon: 'error', title: 'Fehler', - text: error.message || 'Einwilligung konnte nicht wiederhergestellt werden' + text: error.message || 'Änderungen konnten nicht gespeichert werden' }); + } finally { + setSaving(false); } }; + + // Helper: Get effective consent status considering pending changes + const getEffectiveConsentStatus = (consentType, platformId = null) => { + // Check if there's a pending change for this consent + const pendingChange = pendingConsentChanges.find(c => + c.consentType === consentType && c.platformId === platformId + ); + + if (pendingChange) { + return pendingChange.action === 'restore'; // true if restoring, false if revoking + } + + // No pending change, return current status + if (consentType === 'workshop') { + return group?.consents?.workshopConsent || false; + } else if (consentType === 'social_media') { + const consent = group?.consents?.socialMediaConsents?.find(c => c.platformId === platformId); + return consent ? (consent.consented && !consent.revoked) : false; + } + + return false; + }; + + // Helper: Generate mailto link for revoked social media consents + const getMailtoLink = () => { + if (!group) return ''; + + const revokedPlatforms = pendingConsentChanges + .filter(c => c.consentType === 'social_media' && c.action === 'revoke') + .map(c => { + // Look up platform name in allPlatforms (works even if consent was never granted) + const platform = allPlatforms.find(p => p.id === c.platformId); + return platform?.display_name || 'Unbekannte Plattform'; + }); + + if (revokedPlatforms.length === 0) return ''; + + const subject = `Löschung Social Media Posts - Gruppe ${group.groupId}`; + const body = `Hallo, ich habe die Einwilligung zur Veröffentlichung auf folgenden Plattformen widerrufen: ${revokedPlatforms.join(', ')}. Bitte löschen Sie die bereits veröffentlichten Beiträge meiner Gruppe ${group.groupId}. Vielen Dank`; + + return `mailto:it@hobbyhimmel.de?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + }; // Handle group deletion const handleDeleteGroup = async () => { @@ -527,96 +569,91 @@ const ManagementPortalPage = () => { {/* Consent Management Section */} {group.consents && ( - - - Einwilligungen verwalten - - - Sie können Ihre Einwilligungen jederzeit widerrufen oder wiederherstellen. - + <> + - - - {/* Workshop Consent */} - - - - - Werkstatt-Anzeige - - {group.consents.workshopConsent ? ( - } /> - ) : ( - } /> - )} - - {group.consents.workshopConsent ? ( - - ) : ( - - )} - - - - {/* Social Media Consents */} - {group.consents.socialMediaConsents && group.consents.socialMediaConsents.length > 0 && ( - <> - - - Social Media Plattformen: + {/* Save Changes Section (only if there are pending changes) */} + {pendingConsentChanges.length > 0 && ( + + + ⚠️ Sie haben {pendingConsentChanges.length} ungespeicherte Änderung{pendingConsentChanges.length !== 1 ? 'en' : ''} - {group.consents.socialMediaConsents.map(consent => ( - - - - - {consent.platformDisplayName} - - {consent.consented && !consent.revoked ? ( - } /> - ) : ( - } /> - )} - - {consent.consented && !consent.revoked ? ( - - ) : ( - - )} - + + {/* Show mailto link if social media consents are being revoked */} + {getMailtoLink() && ( + + + Bereits veröffentlichte Social Media Beiträge löschen? + + + Kontaktieren Sie uns für die Löschung bereits veröffentlichter Beiträge: + +
{ + e.currentTarget.style.backgroundColor = '#e3f2fd'; + }} + onMouseOut={(e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + }} + > + 📧 E-Mail an it@hobbyhimmel.de + - ))} - + )} + + + + )} - + )} {/* Image Gallery */}