Phase 1 (Backend API): ✅ GroupRepository.updateImageOrder() with SQL transactions ✅ PUT /api/groups/:groupId/reorder API route with validation ✅ Manual testing: Reordering verified working (group qion_-lT1) ✅ Error handling: Invalid IDs, missing groups, empty arrays Phase 2 (Frontend DnD): ✅ @dnd-kit/core packages installed ✅ ReorderService.js for API communication ✅ useReordering.js custom hook with optimistic updates ✅ ImageGalleryCard.js extended with drag handles & sortable ✅ ImageGallery.js with DndContext and SortableContext ✅ CSS styles for drag states, handles, touch-friendly mobile Next: Integration with ModerationGroupImagesPage
379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
const dbManager = require('../database/DatabaseManager');
|
|
|
|
class GroupRepository {
|
|
|
|
// Erstelle neue Gruppe mit Bildern (Transaction)
|
|
async createGroup(groupData) {
|
|
return await dbManager.transaction(async (db) => {
|
|
// Füge Gruppe hinzu
|
|
const groupResult = await db.run(`
|
|
INSERT INTO groups (group_id, year, title, description, name, upload_date)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
groupData.groupId,
|
|
groupData.year,
|
|
groupData.title,
|
|
groupData.description || null,
|
|
groupData.name || null,
|
|
groupData.uploadDate
|
|
]);
|
|
|
|
// Füge Bilder hinzu
|
|
if (groupData.images && groupData.images.length > 0) {
|
|
for (const image of groupData.images) {
|
|
await db.run(`
|
|
INSERT INTO images (group_id, file_name, original_name, file_path, upload_order, file_size, mime_type, preview_path)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
groupData.groupId,
|
|
image.fileName,
|
|
image.originalName,
|
|
image.filePath,
|
|
image.uploadOrder,
|
|
image.fileSize || null,
|
|
image.mimeType || null,
|
|
image.previewPath || null
|
|
]);
|
|
}
|
|
}
|
|
|
|
return groupResult.id;
|
|
});
|
|
}
|
|
|
|
// Hole Gruppe mit Bildern nach Group-ID
|
|
async getGroupById(groupId) {
|
|
const group = await dbManager.get(`
|
|
SELECT * FROM groups WHERE group_id = ?
|
|
`, [groupId]);
|
|
|
|
if (!group) {
|
|
return null;
|
|
}
|
|
|
|
const images = await dbManager.all(`
|
|
SELECT * FROM images
|
|
WHERE group_id = ?
|
|
ORDER BY upload_order ASC
|
|
`, [groupId]);
|
|
|
|
return {
|
|
groupId: group.group_id,
|
|
year: group.year,
|
|
title: group.title,
|
|
description: group.description,
|
|
name: group.name,
|
|
uploadDate: group.upload_date,
|
|
images: images.map(img => ({
|
|
fileName: img.file_name,
|
|
originalName: img.original_name,
|
|
filePath: img.file_path,
|
|
previewPath: img.preview_path,
|
|
uploadOrder: img.upload_order,
|
|
fileSize: img.file_size,
|
|
mimeType: img.mime_type
|
|
})),
|
|
imageCount: images.length
|
|
};
|
|
}
|
|
|
|
// Hole alle Gruppen (mit Paginierung)
|
|
async getAllGroups(limit = null, offset = 0) {
|
|
let sql = `
|
|
SELECT g.*, COUNT(i.id) as image_count
|
|
FROM groups g
|
|
LEFT JOIN images i ON g.group_id = i.group_id
|
|
GROUP BY g.group_id
|
|
ORDER BY g.upload_date DESC
|
|
`;
|
|
|
|
const params = [];
|
|
if (limit) {
|
|
sql += ` LIMIT ? OFFSET ?`;
|
|
params.push(limit, offset);
|
|
}
|
|
|
|
const groups = await dbManager.all(sql, params);
|
|
|
|
return {
|
|
groups: groups.map(group => ({
|
|
groupId: group.group_id,
|
|
year: group.year,
|
|
title: group.title,
|
|
description: group.description,
|
|
name: group.name,
|
|
uploadDate: group.upload_date,
|
|
imageCount: group.image_count
|
|
})),
|
|
total: groups.length
|
|
};
|
|
}
|
|
|
|
// Hole alle Gruppen mit Bildern für Slideshow (nur freigegebene)
|
|
async getAllGroupsWithImages() {
|
|
const groupFormatter = require('../utils/groupFormatter');
|
|
|
|
const groups = await dbManager.all(`
|
|
SELECT * FROM groups
|
|
WHERE approved = TRUE
|
|
ORDER BY upload_date DESC
|
|
`);
|
|
|
|
const result = [];
|
|
for (const group of groups) {
|
|
const images = await dbManager.all(`
|
|
SELECT * FROM images
|
|
WHERE group_id = ?
|
|
ORDER BY upload_order ASC
|
|
`, [group.group_id]);
|
|
|
|
result.push(groupFormatter.formatGroupDetail(group, images));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Lösche Gruppe und alle Bilder
|
|
async deleteGroup(groupId) {
|
|
return await dbManager.transaction(async (db) => {
|
|
// Erst alle Bilddateien physisch löschen
|
|
const images = await db.all(`
|
|
SELECT * FROM images WHERE group_id = ?
|
|
`, [groupId]);
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
for (const image of images) {
|
|
try {
|
|
const absolutePath = path.join(__dirname, '..', image.file_path);
|
|
await fs.unlink(absolutePath);
|
|
console.log(`✓ Bilddatei gelöscht: ${absolutePath}`);
|
|
} catch (error) {
|
|
console.warn(`⚠️ Konnte Bilddatei nicht löschen: ${image.file_path}`, error.message);
|
|
}
|
|
}
|
|
|
|
// Dann Gruppe aus Datenbank löschen (Bilder werden durch CASCADE gelöscht)
|
|
const result = await db.run(`
|
|
DELETE FROM groups WHERE group_id = ?
|
|
`, [groupId]);
|
|
|
|
console.log(`✓ Gruppe gelöscht: ${groupId} (${images.length} Bilder)`);
|
|
return result.changes > 0;
|
|
});
|
|
}
|
|
|
|
// Update Gruppe
|
|
async updateGroup(groupId, updates) {
|
|
const setClause = [];
|
|
const params = [];
|
|
|
|
if (updates.year !== undefined) {
|
|
setClause.push('year = ?');
|
|
params.push(updates.year);
|
|
}
|
|
if (updates.title !== undefined) {
|
|
setClause.push('title = ?');
|
|
params.push(updates.title);
|
|
}
|
|
if (updates.description !== undefined) {
|
|
setClause.push('description = ?');
|
|
params.push(updates.description);
|
|
}
|
|
if (updates.name !== undefined) {
|
|
setClause.push('name = ?');
|
|
params.push(updates.name);
|
|
}
|
|
|
|
if (setClause.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
params.push(groupId);
|
|
|
|
const result = await dbManager.run(`
|
|
UPDATE groups SET ${setClause.join(', ')} WHERE group_id = ?
|
|
`, params);
|
|
|
|
return result.changes > 0;
|
|
}
|
|
|
|
// Gruppe Freigabe-Status aktualisieren
|
|
async updateGroupApproval(groupId, approved) {
|
|
const result = await dbManager.run(`
|
|
UPDATE groups SET approved = ? WHERE group_id = ?
|
|
`, [approved, groupId]);
|
|
|
|
return result.changes > 0;
|
|
}
|
|
|
|
// Einzelnes Bild löschen
|
|
async deleteImage(groupId, imageId) {
|
|
return await dbManager.transaction(async (db) => {
|
|
// Prüfe ob Bild existiert
|
|
const image = await db.get(`
|
|
SELECT * FROM images WHERE id = ? AND group_id = ?
|
|
`, [imageId, groupId]);
|
|
|
|
if (!image) {
|
|
return false;
|
|
}
|
|
|
|
// Lösche Datei vom Dateisystem
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
try {
|
|
// Konvertiere relativen Pfad zu absolutem Pfad im Container
|
|
// image.file_path ist "/upload/dateiname.ext", wir brauchen "/usr/src/app/src/upload/dateiname.ext"
|
|
const absolutePath = path.join(__dirname, '..', image.file_path);
|
|
await fs.unlink(absolutePath);
|
|
console.log(`✓ Bilddatei gelöscht: ${absolutePath}`);
|
|
} catch (error) {
|
|
console.warn(`⚠️ Konnte Bilddatei nicht löschen: ${image.file_path}`, error.message);
|
|
// Datei-Löschfehler sollen nicht das Löschen aus der Datenbank verhindern
|
|
}
|
|
|
|
// Lösche aus Datenbank
|
|
const result = await db.run(`
|
|
DELETE FROM images WHERE id = ? AND group_id = ?
|
|
`, [imageId, groupId]);
|
|
|
|
// Aktualisiere upload_order der verbleibenden Bilder
|
|
await db.run(`
|
|
UPDATE images
|
|
SET upload_order = upload_order - 1
|
|
WHERE group_id = ? AND upload_order > ?
|
|
`, [groupId, image.upload_order]);
|
|
|
|
return result.changes > 0;
|
|
});
|
|
}
|
|
|
|
// Alle Gruppen für Moderation (mit Freigabestatus und Bildanzahl)
|
|
async getAllGroupsWithModerationInfo() {
|
|
const groupFormatter = require('../utils/groupFormatter');
|
|
|
|
const groups = await dbManager.all(`
|
|
SELECT * FROM groups
|
|
ORDER BY approved ASC, upload_date DESC
|
|
`);
|
|
|
|
const result = [];
|
|
for (const group of groups) {
|
|
const images = await dbManager.all(`
|
|
SELECT * FROM images
|
|
WHERE group_id = ?
|
|
ORDER BY upload_order ASC
|
|
`, [group.group_id]);
|
|
|
|
result.push(groupFormatter.formatGroupDetail(group, images));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Hole Gruppe für Moderation (inkl. nicht-freigegebene)
|
|
async getGroupForModeration(groupId) {
|
|
const group = await dbManager.get(`
|
|
SELECT * FROM groups WHERE group_id = ?
|
|
`, [groupId]);
|
|
|
|
if (!group) {
|
|
return null;
|
|
}
|
|
|
|
const images = await dbManager.all(`
|
|
SELECT * FROM images
|
|
WHERE group_id = ?
|
|
ORDER BY upload_order ASC
|
|
`, [groupId]);
|
|
|
|
const groupFormatter = require('../utils/groupFormatter');
|
|
return groupFormatter.formatGroupDetail(group, images);
|
|
}
|
|
|
|
// Statistiken (erweitert um Freigabe-Status)
|
|
async getStats() {
|
|
const groupCount = await dbManager.get('SELECT COUNT(*) as count FROM groups');
|
|
const imageCount = await dbManager.get('SELECT COUNT(*) as count FROM images');
|
|
const approvedGroups = await dbManager.get('SELECT COUNT(*) as count FROM groups WHERE approved = TRUE');
|
|
const pendingGroups = await dbManager.get('SELECT COUNT(*) as count FROM groups WHERE approved = FALSE');
|
|
const latestGroup = await dbManager.get(`
|
|
SELECT upload_date FROM groups ORDER BY upload_date DESC LIMIT 1
|
|
`);
|
|
|
|
return {
|
|
totalGroups: groupCount.count,
|
|
totalImages: imageCount.count,
|
|
approvedGroups: approvedGroups.count,
|
|
pendingGroups: pendingGroups.count,
|
|
latestUpload: latestGroup ? latestGroup.upload_date : null
|
|
};
|
|
}
|
|
|
|
// Aktualisiere die Reihenfolge der Bilder in einer Gruppe
|
|
async updateImageOrder(groupId, imageIds) {
|
|
if (!groupId) {
|
|
throw new Error('Group ID is required');
|
|
}
|
|
|
|
if (!Array.isArray(imageIds) || imageIds.length === 0) {
|
|
throw new Error('Image IDs array is required and cannot be empty');
|
|
}
|
|
|
|
return await dbManager.transaction(async (db) => {
|
|
// Zunächst prüfen, ob die Gruppe existiert
|
|
const group = await db.get('SELECT group_id FROM groups WHERE group_id = ?', [groupId]);
|
|
if (!group) {
|
|
throw new Error(`Group with ID ${groupId} not found`);
|
|
}
|
|
|
|
// Alle Bilder der Gruppe laden und prüfen ob alle IDs gültig sind
|
|
const existingImages = await db.all(
|
|
'SELECT id, file_name FROM images WHERE group_id = ? ORDER BY upload_order ASC',
|
|
[groupId]
|
|
);
|
|
|
|
const existingImageIds = existingImages.map(img => img.id);
|
|
|
|
// Prüfen ob alle übergebenen IDs zur Gruppe gehören
|
|
const invalidIds = imageIds.filter(id => !existingImageIds.includes(id));
|
|
if (invalidIds.length > 0) {
|
|
throw new Error(`Invalid image IDs found: ${invalidIds.join(', ')}. These images do not belong to group ${groupId}`);
|
|
}
|
|
|
|
// Prüfen ob alle Bilder der Gruppe in der Liste enthalten sind
|
|
const missingIds = existingImageIds.filter(id => !imageIds.includes(id));
|
|
if (missingIds.length > 0) {
|
|
throw new Error(`Missing image IDs: ${missingIds.join(', ')}. All images of the group must be included in the reorder operation`);
|
|
}
|
|
|
|
// Batch-Update der upload_order Werte
|
|
let updateCount = 0;
|
|
for (let i = 0; i < imageIds.length; i++) {
|
|
const imageId = imageIds[i];
|
|
const newOrder = i + 1; // upload_order beginnt bei 1
|
|
|
|
const result = await db.run(
|
|
'UPDATE images SET upload_order = ? WHERE id = ? AND group_id = ?',
|
|
[newOrder, imageId, groupId]
|
|
);
|
|
|
|
if (result.changes === 0) {
|
|
throw new Error(`Failed to update image with ID ${imageId}`);
|
|
}
|
|
|
|
updateCount += result.changes;
|
|
}
|
|
|
|
return {
|
|
groupId: groupId,
|
|
updatedImages: updateCount,
|
|
newOrder: imageIds
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = new GroupRepository(); |