Project-Image-Uploader/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js
matthias.lotz 4b9feec887 Refactor: Create modular component architecture for ManagementPortalPage
- Created new modular components:
  * ConsentManager: Manages workshop + social media consents with individual save
  * GroupMetadataEditor: Manages group metadata (title, description, name, year) with save
  * ImageDescriptionManager: Manages image descriptions with batch save
  * DeleteGroupButton: Standalone group deletion component

- Refactored ManagementPortalPage to use modular components:
  * Each component in Paper box with heading inside (not outside)
  * HTML buttons with CSS classes (btn btn-success, btn btn-secondary)
  * Inline feedback with Material-UI Alert instead of SweetAlert2 popups
  * Icons: 💾 save, ↩ discard, 🗑️ delete
  * Individual save/discard functionality per component

- Enhanced ConsentCheckboxes component:
  * Added children prop for flexible composition
  * Conditional heading for manage mode inside Paper box

- Fixed DescriptionInput:
  * Removed duplicate heading (now only in parent component)

- React state management improvements:
  * Deep copy pattern for nested objects/arrays
  * Sorted array comparison for order-insensitive change detection
  * Set-based comparison for detecting removed items
  * Initialization guard to prevent useEffect overwrites

- Bug fixes:
  * Fixed image reordering using existing /api/groups/:groupId/reorder route
  * Fixed edit mode toggle with unsaved changes warning
  * Fixed consent state updates with proper object references
  * Fixed uploadImageBatch signature to use object destructuring
  * Removed unnecessary /api/manage/:token/reorder route from backend

Next: Apply same modular pattern to MultiUploadPage and ModerationGroupImagesPage
2025-11-15 17:25:51 +01:00

246 lines
9.7 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Box,
FormControlLabel,
Checkbox,
Typography,
Paper,
Divider,
Alert
} from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import FacebookIcon from '@mui/icons-material/Facebook';
import InstagramIcon from '@mui/icons-material/Instagram';
import MusicNoteIcon from '@mui/icons-material/MusicNote';
const ICON_MAP = {
'Facebook': FacebookIcon,
'Instagram': InstagramIcon,
'MusicNote': MusicNoteIcon,
};
/**
* ConsentCheckboxes Component
*
* 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,
mode = 'upload',
groupId = null,
children
}) {
const [platforms, setPlatforms] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Lade verfügbare Plattformen vom Backend
fetchPlatforms();
}, []);
const fetchPlatforms = async () => {
try {
const response = await fetch('/api/social-media/platforms');
if (!response.ok) {
throw new Error('Failed to load platforms');
}
const data = await response.json();
setPlatforms(data);
setError(null);
} catch (error) {
console.error('Error loading platforms:', error);
setError('Plattformen konnten nicht geladen werden');
} finally {
setLoading(false);
}
};
const handleWorkshopChange = (event) => {
onConsentChange({
...consents,
workshopConsent: event.target.checked
});
};
const handleSocialMediaChange = (platformId) => (event) => {
const updatedConsents = { ...consents };
const platformConsents = updatedConsents.socialMediaConsents || [];
if (event.target.checked) {
// Füge Consent hinzu
platformConsents.push({ platformId, consented: true });
} else {
// Entferne Consent
const index = platformConsents.findIndex(c => c.platformId === platformId);
if (index > -1) {
platformConsents.splice(index, 1);
}
}
updatedConsents.socialMediaConsents = platformConsents;
onConsentChange(updatedConsents);
};
const isPlatformChecked = (platformId) => {
return consents.socialMediaConsents?.some(c => c.platformId === platformId) || false;
};
const isManageMode = mode === 'manage';
const isUploadMode = mode === 'upload';
return (
<Paper
sx={{
p: 3,
mb: 3,
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '2px solid #e0e0e0'
}}
>
{/* Component Header for manage mode */}
{isManageMode && (
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
Einwilligungen
</Typography>
)}
{/* Aufklärungshinweis */}
<Alert severity="info" icon={<InfoIcon />} sx={{ mb: 3 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
{isUploadMode ? 'Wichtiger Hinweis' : 'Einwilligungen verwalten'}
</Typography>
{isUploadMode ? (
<>
<Typography variant="body2">
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.
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Nach dem Upload erhalten Sie eine Gruppen-ID als Referenz für die Kontaktaufnahme.
</Typography>
</>
) : (
<Typography variant="body2">
Sie können Ihre Einwilligungen jederzeit widerrufen oder erteilen.
Änderungen werden erst nach dem Speichern übernommen.
</Typography>
)}
</Alert>
{/* Pflicht-Zustimmung: Werkstatt-Anzeige */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#333' }}>
Anzeige in der Werkstatt {isUploadMode && '*'}
</Typography>
<FormControlLabel
control={
<Checkbox
checked={consents.workshopConsent || false}
onChange={handleWorkshopChange}
disabled={disabled}
required={isUploadMode}
sx={{
color: '#4CAF50',
'&.Mui-checked': { color: '#4CAF50' }
}}
/>
}
label={
<Typography variant="body2" sx={{ color: '#555' }}>
Ich willige ein, dass meine hochgeladenen Bilder auf dem Monitor in
der offenen Werkstatt des Hobbyhimmels angezeigt werden dürfen.
Die Bilder sind nur lokal im Hobbyhimmel sichtbar und werden nicht
über das Internet zugänglich gemacht.
{isUploadMode && <strong> (Pflichtfeld)</strong>}
</Typography>
}
/>
</Box>
<Divider sx={{ my: 3 }} />
{/* Optional: Social Media Veröffentlichung */}
<Box>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#333' }}>
Social Media Veröffentlichung (optional)
</Typography>
<Typography variant="body2" sx={{ mb: 2, color: '#666' }}>
Ich willige ein, dass meine Bilder und Texte auf folgenden Social Media
Plattformen veröffentlicht werden dürfen (inklusive aller angegebenen
Informationen wie Name und Beschreibung):
</Typography>
{loading ? (
<Typography sx={{ color: '#666', fontStyle: 'italic' }}>
Lade Plattformen...
</Typography>
) : error ? (
<Alert severity="warning" sx={{ mb: 2 }}>
{error}
</Alert>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{platforms.map(platform => {
const IconComponent = ICON_MAP[platform.icon_name] || InfoIcon;
return (
<FormControlLabel
key={platform.id}
control={
<Checkbox
checked={isPlatformChecked(platform.id)}
onChange={handleSocialMediaChange(platform.id)}
disabled={disabled}
sx={{
color: '#2196F3',
'&.Mui-checked': { color: '#2196F3' }
}}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconComponent fontSize="small" sx={{ color: '#2196F3' }} />
<Typography variant="body2">
{platform.display_name}
</Typography>
</Box>
}
/>
);
})}
</Box>
)}
</Box>
{/* Widerrufs-Hinweis */}
{isUploadMode && (
<Alert severity="info" sx={{ mt: 3 }}>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Widerruf Ihrer Einwilligung:</strong> Sie können Ihre Einwilligung
jederzeit widerrufen. Kontaktieren Sie uns hierzu mit Ihrer Gruppen-ID unter:{' '}
<strong>it@hobbyhimmel.de</strong>
</Typography>
</Alert>
)}
{/* Additional content from parent (e.g., save buttons) */}
{children}
</Paper>
);
}
export default ConsentCheckboxes;