Fix: Admin deletion log, CSV export revoked consents, consent filter UI
Backend Fixes:
- Admin deletions now create deletion_log entries (admin_moderation_deletion)
- Static mount for /previews added to serve preview images
- Admin groups endpoint supports consent filter parameter
Frontend Improvements:
- Replaced consent dropdown with checkbox UI (Workshop, Facebook, Instagram, TikTok)
- Checkboxes use OR logic for filtering
- Revoked consents excluded from filter counts
- Updated ModerationGroupsPage to send consents array to backend
Infrastructure:
- Simplified nginx.conf (proxy /api/* to backend, all else to frontend)
- Fixed docker-compose port mapping (5001:5000)
Tests: 11/11 passed ✅
This commit is contained in:
parent
7af14a162d
commit
98b3616dc4
|
|
@ -417,7 +417,7 @@ router.get('/groups', async (req, res) => {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
const { workshopOnly, platform } = req.query;
|
const { workshopOnly, platform, consents } = req.query;
|
||||||
|
|
||||||
// Hole alle Gruppen mit vollständigen Infos (inkl. Bilder)
|
// Hole alle Gruppen mit vollständigen Infos (inkl. Bilder)
|
||||||
let allGroups = await GroupRepository.getAllGroupsWithModerationInfo();
|
let allGroups = await GroupRepository.getAllGroupsWithModerationInfo();
|
||||||
|
|
@ -425,10 +425,10 @@ router.get('/groups', async (req, res) => {
|
||||||
// Füge Consent-Daten für jede Gruppe hinzu
|
// Füge Consent-Daten für jede Gruppe hinzu
|
||||||
const groupsWithConsents = await Promise.all(
|
const groupsWithConsents = await Promise.all(
|
||||||
allGroups.map(async (group) => {
|
allGroups.map(async (group) => {
|
||||||
const consents = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
|
const consentData = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId);
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
socialMediaConsents: consents
|
socialMediaConsents: consentData
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -436,7 +436,27 @@ router.get('/groups', async (req, res) => {
|
||||||
// Jetzt filtern wir basierend auf den Query-Parametern
|
// Jetzt filtern wir basierend auf den Query-Parametern
|
||||||
let filteredGroups = groupsWithConsents;
|
let filteredGroups = groupsWithConsents;
|
||||||
|
|
||||||
if (workshopOnly === 'true') {
|
// Neuer Multi-Checkbox Filter
|
||||||
|
if (consents) {
|
||||||
|
const selectedConsents = consents.split(','); // z.B. ['workshop', 'facebook', 'instagram']
|
||||||
|
|
||||||
|
filteredGroups = groupsWithConsents.filter(group => {
|
||||||
|
// Gruppe muss mindestens einen der ausgewählten Consents haben
|
||||||
|
return selectedConsents.some(consentType => {
|
||||||
|
if (consentType === 'workshop') {
|
||||||
|
return group.display_in_workshop === 1 || group.display_in_workshop === true;
|
||||||
|
} else {
|
||||||
|
// Social Media Platform (facebook, instagram, tiktok)
|
||||||
|
return group.socialMediaConsents &&
|
||||||
|
group.socialMediaConsents.some(consent =>
|
||||||
|
consent.platform_name === consentType &&
|
||||||
|
(consent.consented === 1 || consent.consented === true) &&
|
||||||
|
(consent.revoked !== 1 && consent.revoked !== true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (workshopOnly === 'true') {
|
||||||
// Filter: Nur Gruppen MIT Werkstatt-Consent aber OHNE zugestimmte Social Media Consents
|
// Filter: Nur Gruppen MIT Werkstatt-Consent aber OHNE zugestimmte Social Media Consents
|
||||||
filteredGroups = groupsWithConsents.filter(group => {
|
filteredGroups = groupsWithConsents.filter(group => {
|
||||||
// Muss Werkstatt-Consent haben
|
// Muss Werkstatt-Consent haben
|
||||||
|
|
@ -922,15 +942,32 @@ router.delete('/groups/:groupId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { groupId } = req.params;
|
const { groupId } = req.params;
|
||||||
|
|
||||||
const deleted = await GroupRepository.deleteGroup(groupId);
|
// Get group data before deletion for logging
|
||||||
|
const groupData = await GroupRepository.getGroupById(groupId);
|
||||||
|
|
||||||
if (!deleted) {
|
if (!groupData) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
error: 'Group not found',
|
error: 'Group not found',
|
||||||
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
|
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageCount = groupData.images ? groupData.images.length : 0;
|
||||||
|
const totalFileSize = groupData.images ? groupData.images.reduce((sum, img) => sum + (img.fileSize || 0), 0) : 0;
|
||||||
|
|
||||||
|
// Create deletion_log entry BEFORE deleting
|
||||||
|
await DeletionLogRepository.createDeletionEntry({
|
||||||
|
groupId: groupId,
|
||||||
|
year: groupData.year,
|
||||||
|
imageCount: imageCount,
|
||||||
|
uploadDate: groupData.uploadDate,
|
||||||
|
deletionReason: 'admin_moderation_deletion',
|
||||||
|
totalFileSize: totalFileSize
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the group
|
||||||
|
await GroupRepository.deleteGroup(groupId);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Gruppe erfolgreich gelöscht',
|
message: 'Gruppe erfolgreich gelöscht',
|
||||||
|
|
|
||||||
|
|
@ -403,7 +403,8 @@ router.get('/consents/export', async (req, res) => {
|
||||||
// Platform-Consents
|
// Platform-Consents
|
||||||
const consentMap = {};
|
const consentMap = {};
|
||||||
group.socialMediaConsents.forEach(consent => {
|
group.socialMediaConsents.forEach(consent => {
|
||||||
consentMap[consent.platform_name] = consent.consented === 1;
|
// Consent ist nur dann aktiv wenn consented=1 UND nicht revoked
|
||||||
|
consentMap[consent.platform_name] = consent.consented === 1 && consent.revoked !== 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
platformNames.forEach(platform => {
|
platformNames.forEach(platform => {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class Server {
|
||||||
// Starte Express Server
|
// Starte Express Server
|
||||||
initiateResources(this._app);
|
initiateResources(this._app);
|
||||||
this._app.use('/upload', express.static( __dirname + '/upload'));
|
this._app.use('/upload', express.static( __dirname + '/upload'));
|
||||||
|
this._app.use('/api/previews', express.static( __dirname + '/data/previews'));
|
||||||
|
|
||||||
// Mount Swagger UI in dev only when available
|
// Mount Swagger UI in dev only when available
|
||||||
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
|
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
|
||||||
|
|
@ -74,6 +75,7 @@ class Server {
|
||||||
await dbManager.initialize();
|
await dbManager.initialize();
|
||||||
initiateResources(this._app);
|
initiateResources(this._app);
|
||||||
this._app.use('/upload', express.static( __dirname + '/upload'));
|
this._app.use('/upload', express.static( __dirname + '/upload'));
|
||||||
|
this._app.use('/api/previews', express.static( __dirname + '/data/previews'));
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
|
if (process.env.NODE_ENV !== 'production' && swaggerUi && swaggerDocument) {
|
||||||
this._app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
this._app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ services:
|
||||||
|
|
||||||
backend-dev:
|
backend-dev:
|
||||||
container_name: image-uploader-backend-dev
|
container_name: image-uploader-backend-dev
|
||||||
|
user: "1000:1000"
|
||||||
build:
|
build:
|
||||||
context: ../../
|
context: ../../
|
||||||
dockerfile: docker/dev/backend/Dockerfile
|
dockerfile: docker/dev/backend/Dockerfile
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,34 @@
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
|
||||||
# Allow large uploads (50MB)
|
# Allow large uploads (100MB for batch uploads)
|
||||||
client_max_body_size 50M;
|
client_max_body_size 100M;
|
||||||
|
|
||||||
# API proxy to backend-dev service
|
# ========================================
|
||||||
location /upload {
|
# Backend API Routes (all under /api/)
|
||||||
|
# ========================================
|
||||||
|
# Basierend auf routeMappings.js:
|
||||||
|
# - /api/upload, /api/download, /api/groups (Public API)
|
||||||
|
# - /api/manage/* (Management Portal, Token-basiert)
|
||||||
|
# - /api/admin/* (Admin/Moderation, Bearer Token)
|
||||||
|
# - /api/system/migration/* (System API)
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
proxy_pass http://backend-dev:5000;
|
proxy_pass http://backend-dev:5000;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# Allow large uploads for API too
|
# Large uploads für Batch-Upload
|
||||||
client_max_body_size 50M;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API routes for new multi-upload features
|
|
||||||
location /api/upload {
|
|
||||||
proxy_pass http://backend-dev:5000/upload;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Allow large uploads for batch upload
|
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API - Download original images
|
# ========================================
|
||||||
location /api/download {
|
# Frontend Routes (React Dev Server)
|
||||||
proxy_pass http://backend-dev:5000/download;
|
# ========================================
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API - Preview/thumbnail images (optimized for gallery views)
|
# Protected route - Moderation (HTTP Basic Auth)
|
||||||
location /api/previews {
|
|
||||||
proxy_pass http://backend-dev:5000/previews;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API - Groups (NO PASSWORD PROTECTION)
|
|
||||||
location /api/groups {
|
|
||||||
proxy_pass http://backend-dev:5000/groups;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API - Social Media Consent Management (NO PASSWORD PROTECTION)
|
|
||||||
location /api/social-media {
|
|
||||||
proxy_pass http://backend-dev:5000/api/social-media;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API - Management Portal (NO PASSWORD PROTECTION - Token-based auth)
|
|
||||||
location /api/manage {
|
|
||||||
proxy_pass http://backend-dev:5000/api/manage;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Admin API routes (NO password protection - protected by /moderation page access)
|
|
||||||
location /api/admin {
|
|
||||||
proxy_pass http://backend-dev:5000/api/admin;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Protected API - Moderation API routes (password protected) - must come before /groups
|
|
||||||
location /moderation/groups {
|
|
||||||
auth_basic "Restricted Area - Moderation API";
|
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
|
||||||
|
|
||||||
proxy_pass http://backend-dev:5000/moderation/groups;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API - Groups API routes (NO PASSWORD PROTECTION)
|
|
||||||
location ~ ^/groups/[a-zA-Z0-9_-]+(/.*)?$ {
|
|
||||||
proxy_pass http://backend-dev:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /download {
|
|
||||||
proxy_pass http://backend-dev:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Frontend page - Groups overview (NO PASSWORD PROTECTION) - React Dev Server
|
|
||||||
location /groups {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Protected routes - Moderation (password protected) - React Dev Server
|
|
||||||
location /moderation {
|
location /moderation {
|
||||||
auth_basic "Restricted Area - Moderation";
|
auth_basic "Restricted Area - Moderation";
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
|
@ -136,18 +42,16 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket support for hot reloading (React Dev Server)
|
# WebSocket support for React Hot Reloading
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "Upgrade";
|
proxy_set_header Connection "Upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Frontend files - React Dev Server
|
# All other routes → React Dev Server (Client-side routing)
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
|
"proxy": "http://localhost:5001",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
||||||
import { Box, Typography, Paper } from '@mui/material';
|
import { Box, Typography, Paper } from '@mui/material';
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import DescriptionInput from './MultiUpload/DescriptionInput';
|
import DescriptionInput from './MultiUpload/DescriptionInput';
|
||||||
|
import { adminRequest } from '../../services/adminApi';
|
||||||
|
import { handleAdminError } from '../../services/adminErrorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages group metadata with save functionality
|
* Manages group metadata with save functionality
|
||||||
|
|
@ -66,11 +68,14 @@ function GroupMetadataEditor({
|
||||||
|
|
||||||
// Different API endpoints for manage vs moderate
|
// Different API endpoints for manage vs moderate
|
||||||
const endpoint = isModerateMode
|
const endpoint = isModerateMode
|
||||||
? `/groups/${groupId}`
|
? `/api/admin/groups/${groupId}`
|
||||||
: `/api/manage/${token}/metadata`;
|
: `/api/manage/${token}/metadata`;
|
||||||
|
|
||||||
const method = isModerateMode ? 'PATCH' : 'PUT';
|
const method = isModerateMode ? 'PATCH' : 'PUT';
|
||||||
|
|
||||||
|
if (isModerateMode) {
|
||||||
|
await adminRequest(endpoint, method, metadata);
|
||||||
|
} else {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -81,6 +86,7 @@ function GroupMetadataEditor({
|
||||||
const body = await res.json().catch(() => ({}));
|
const body = await res.json().catch(() => ({}));
|
||||||
throw new Error(body.error || 'Fehler beim Speichern der Metadaten');
|
throw new Error(body.error || 'Fehler beim Speichern der Metadaten');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
|
|
@ -100,11 +106,16 @@ function GroupMetadataEditor({
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving metadata:', error);
|
console.error('Error saving metadata:', error);
|
||||||
|
|
||||||
|
if (isModerateMode) {
|
||||||
|
handleAdminError(error, 'Metadaten speichern');
|
||||||
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Fehler',
|
title: 'Fehler',
|
||||||
text: error.message || 'Metadaten konnten nicht gespeichert werden'
|
text: error.message || 'Metadaten konnten nicht gespeichert werden'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
||||||
import { Box, Typography, Paper } from '@mui/material';
|
import { Box, Typography, Paper } from '@mui/material';
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import ImageGallery from './ImageGallery';
|
import ImageGallery from './ImageGallery';
|
||||||
|
import { adminRequest } from '../../services/adminApi';
|
||||||
|
import { handleAdminError } from '../../services/adminErrorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages image descriptions with save functionality
|
* Manages image descriptions with save functionality
|
||||||
|
|
@ -41,9 +43,12 @@ function ImageDescriptionManager({
|
||||||
try {
|
try {
|
||||||
// Different API endpoints for manage vs moderate
|
// Different API endpoints for manage vs moderate
|
||||||
const endpoint = mode === 'moderate'
|
const endpoint = mode === 'moderate'
|
||||||
? `/groups/${groupId}/images/${imageId}`
|
? `/api/admin/groups/${groupId}/images/${imageId}`
|
||||||
: `/api/manage/${token}/images/${imageId}`;
|
: `/api/manage/${token}/images/${imageId}`;
|
||||||
|
|
||||||
|
if (mode === 'moderate') {
|
||||||
|
await adminRequest(endpoint, 'DELETE');
|
||||||
|
} else {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
@ -52,6 +57,7 @@ function ImageDescriptionManager({
|
||||||
const body = await res.json().catch(() => ({}));
|
const body = await res.json().catch(() => ({}));
|
||||||
throw new Error(body.error || 'Fehler beim Löschen des Bildes');
|
throw new Error(body.error || 'Fehler beim Löschen des Bildes');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
|
|
@ -67,6 +73,9 @@ function ImageDescriptionManager({
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (mode === 'moderate') {
|
||||||
|
await handleAdminError(error, 'Bild löschen');
|
||||||
|
} else {
|
||||||
console.error('Error deleting image:', error);
|
console.error('Error deleting image:', error);
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
|
|
@ -74,6 +83,7 @@ function ImageDescriptionManager({
|
||||||
text: error.message || 'Bild konnte nicht gelöscht werden'
|
text: error.message || 'Bild konnte nicht gelöscht werden'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize descriptions from images
|
// Initialize descriptions from images
|
||||||
|
|
@ -120,11 +130,14 @@ function ImageDescriptionManager({
|
||||||
|
|
||||||
// Different API endpoints for manage vs moderate
|
// Different API endpoints for manage vs moderate
|
||||||
const endpoint = mode === 'moderate'
|
const endpoint = mode === 'moderate'
|
||||||
? `/groups/${groupId}/images/batch-description`
|
? `/api/admin/groups/${groupId}/images/batch-description`
|
||||||
: `/api/manage/${token}/images/descriptions`;
|
: `/api/manage/${token}/images/descriptions`;
|
||||||
|
|
||||||
const method = mode === 'moderate' ? 'PATCH' : 'PUT';
|
const method = mode === 'moderate' ? 'PATCH' : 'PUT';
|
||||||
|
|
||||||
|
if (mode === 'moderate') {
|
||||||
|
await adminRequest(endpoint, method, { descriptions });
|
||||||
|
} else {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -133,7 +146,8 @@ function ImageDescriptionManager({
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const body = await res.json().catch(() => ({}));
|
const body = await res.json().catch(() => ({}));
|
||||||
throw new Error(body.error || 'Fehler beim Speichern der Beschreibungen');
|
throw new Error(body.error || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
|
|
@ -154,11 +168,16 @@ function ImageDescriptionManager({
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving descriptions:', error);
|
console.error('Error saving descriptions:', error);
|
||||||
|
|
||||||
|
if (mode === 'moderate') {
|
||||||
|
handleAdminError(error, 'Beschreibungen speichern');
|
||||||
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Fehler',
|
title: 'Fehler',
|
||||||
text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
|
text: error.message || 'Beschreibungen konnten nicht gespeichert werden'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Container, Box, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
import { Container, Box, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@mui/material';
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||||
|
|
||||||
|
|
@ -22,7 +22,12 @@ const ModerationGroupsPage = () => {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||||
const [showImages, setShowImages] = useState(false);
|
const [showImages, setShowImages] = useState(false);
|
||||||
const [consentFilter, setConsentFilter] = useState('all');
|
const [consentFilters, setConsentFilters] = useState({
|
||||||
|
workshop: false,
|
||||||
|
facebook: false,
|
||||||
|
instagram: false,
|
||||||
|
tiktok: false
|
||||||
|
});
|
||||||
const [platforms, setPlatforms] = useState([]);
|
const [platforms, setPlatforms] = useState([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -33,7 +38,7 @@ const ModerationGroupsPage = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadModerationGroups();
|
loadModerationGroups();
|
||||||
}, [consentFilter]);
|
}, [consentFilters]);
|
||||||
|
|
||||||
const loadPlatforms = async () => {
|
const loadPlatforms = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -52,13 +57,10 @@ const ModerationGroupsPage = () => {
|
||||||
let url = '/api/admin/groups';
|
let url = '/api/admin/groups';
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (consentFilter !== 'all') {
|
// Sende alle aktivierten Filter
|
||||||
if (consentFilter === 'workshop-only') {
|
const activeFilters = Object.keys(consentFilters).filter(key => consentFilters[key]);
|
||||||
params.append('workshopOnly', 'true');
|
if (activeFilters.length > 0) {
|
||||||
} else {
|
params.append('consents', activeFilters.join(','));
|
||||||
// Platform filter (facebook, instagram, tiktok)
|
|
||||||
params.append('platform', consentFilter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.toString()) {
|
if (params.toString()) {
|
||||||
|
|
@ -238,25 +240,53 @@ const ModerationGroupsPage = () => {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap'
|
||||||
}}>
|
}}>
|
||||||
<FormControl sx={{ minWidth: 250 }} size="small">
|
<FormControl component="fieldset" sx={{ minWidth: 250 }}>
|
||||||
<InputLabel id="consent-filter-label">
|
<FormLabel component="legend" sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||||
<FilterListIcon sx={{ mr: 0.5, fontSize: 18, verticalAlign: 'middle' }} />
|
<FilterListIcon sx={{ mr: 0.5, fontSize: 18 }} />
|
||||||
Consent-Filter
|
Consent-Filter
|
||||||
</InputLabel>
|
</FormLabel>
|
||||||
<Select
|
<FormGroup>
|
||||||
labelId="consent-filter-label"
|
<FormControlLabel
|
||||||
value={consentFilter}
|
control={
|
||||||
label="Consent-Filter"
|
<Checkbox
|
||||||
onChange={(e) => setConsentFilter(e.target.value)}
|
checked={consentFilters.workshop}
|
||||||
>
|
onChange={(e) => setConsentFilters({...consentFilters, workshop: e.target.checked})}
|
||||||
<MenuItem value="all">Alle Gruppen</MenuItem>
|
size="small"
|
||||||
<MenuItem value="workshop-only">Nur Werkstatt-Consent</MenuItem>
|
/>
|
||||||
{platforms.map(platform => (
|
}
|
||||||
<MenuItem key={platform.id} value={platform.platform_name}>
|
label="Werkstatt"
|
||||||
{platform.display_name}
|
/>
|
||||||
</MenuItem>
|
<FormControlLabel
|
||||||
))}
|
control={
|
||||||
</Select>
|
<Checkbox
|
||||||
|
checked={consentFilters.facebook}
|
||||||
|
onChange={(e) => setConsentFilters({...consentFilters, facebook: e.target.checked})}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Facebook"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={consentFilters.instagram}
|
||||||
|
onChange={(e) => setConsentFilters({...consentFilters, instagram: e.target.checked})}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Instagram"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={consentFilters.tiktok}
|
||||||
|
onChange={(e) => setConsentFilters({...consentFilters, tiktok: e.target.checked})}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="TikTok"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user