From f29425c987dbb55a4f0ef93039e480e4c78578c5 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 31 Aug 2025 21:09:48 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCgt=20Funktionen=20zum=20Zusammenf=C3=BCh?= =?UTF-8?q?ren=20und=20Entfernen=20von=20Duplikaten=20vordefinierter=20Akt?= =?UTF-8?q?ivit=C3=A4ten=20hinzu.=20Implementiert=20die=20entsprechenden?= =?UTF-8?q?=20Controller-Methoden=20und=20Routen.=20Aktualisiert=20die=20B?= =?UTF-8?q?enutzeroberfl=C3=A4che=20in=20PredefinedActivities.vue,=20um=20?= =?UTF-8?q?die=20neuen=20Funktionen=20zur=20Verf=C3=BCgung=20zu=20stellen?= =?UTF-8?q?=20und=20die=20Aktivit=C3=A4ten=20nach=20Namen=20und=20Code=20z?= =?UTF-8?q?u=20sortieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predefinedActivityController.js | 21 +++++ backend/routes/predefinedActivityRoutes.js | 4 + backend/services/predefinedActivityService.js | 85 ++++++++++++++++++- frontend/src/views/PredefinedActivities.vue | 60 ++++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) diff --git a/backend/controllers/predefinedActivityController.js b/backend/controllers/predefinedActivityController.js index 3fee1f7..102a2f1 100644 --- a/backend/controllers/predefinedActivityController.js +++ b/backend/controllers/predefinedActivityController.js @@ -61,3 +61,24 @@ export const searchPredefinedActivities = async (req, res) => { res.status(500).json({ error: 'Error searching predefined activities' }); } }; + +export const mergePredefinedActivities = async (req, res) => { + try { + const { sourceId, targetId } = req.body; + await predefinedActivityService.mergeActivities(sourceId, targetId); + res.status(200).json({ ok: true }); + } catch (error) { + console.error('[mergePredefinedActivities] - Error:', error); + res.status(500).json({ error: 'Error merging predefined activities' }); + } +}; + +export const deduplicatePredefinedActivities = async (req, res) => { + try { + const result = await predefinedActivityService.deduplicateActivities(); + res.status(200).json(result); + } catch (error) { + console.error('[deduplicatePredefinedActivities] - Error:', error); + res.status(500).json({ error: 'Error deduplicating predefined activities' }); + } +}; diff --git a/backend/routes/predefinedActivityRoutes.js b/backend/routes/predefinedActivityRoutes.js index e4ef704..0fbefcd 100644 --- a/backend/routes/predefinedActivityRoutes.js +++ b/backend/routes/predefinedActivityRoutes.js @@ -5,6 +5,8 @@ import { getPredefinedActivityById, updatePredefinedActivity, searchPredefinedActivities, + mergePredefinedActivities, + deduplicatePredefinedActivities, } from '../controllers/predefinedActivityController.js'; import multer from 'multer'; import { authenticate } from '../middleware/authMiddleware.js'; @@ -22,6 +24,8 @@ router.get('/:id', authenticate, getPredefinedActivityById); router.put('/:id', authenticate, updatePredefinedActivity); router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage); router.get('/search/query', authenticate, searchPredefinedActivities); +router.post('/merge', authenticate, mergePredefinedActivities); +router.post('/deduplicate', authenticate, deduplicatePredefinedActivities); router.get('/:id/image/:imageId', authenticate, async (req, res) => { try { const { id, imageId } = req.params; diff --git a/backend/services/predefinedActivityService.js b/backend/services/predefinedActivityService.js index 5a023da..8152d3a 100644 --- a/backend/services/predefinedActivityService.js +++ b/backend/services/predefinedActivityService.js @@ -1,4 +1,8 @@ import PredefinedActivity from '../models/PredefinedActivity.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import GroupActivity from '../models/GroupActivity.js'; +import PredefinedActivityImage from '../models/PredefinedActivityImage.js'; +import sequelize from '../database.js'; import { Op } from 'sequelize'; class PredefinedActivityService { @@ -33,7 +37,13 @@ class PredefinedActivityService { async getAllPredefinedActivities() { console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities'); - return await PredefinedActivity.findAll(); + return await PredefinedActivity.findAll({ + order: [ + [sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first + ['code', 'ASC'], + ['name', 'ASC'], + ], + }); } async getPredefinedActivityById(id) { @@ -58,10 +68,81 @@ class PredefinedActivityService { { code: { [Op.like]: `%${q}%` } }, ], }, - order: [['name', 'ASC']], + order: [ + [sequelize.literal('code IS NULL'), 'ASC'], + ['code', 'ASC'], + ['name', 'ASC'], + ], limit: Math.min(parseInt(limit || 20, 10), 50), }); } + + async mergeActivities(sourceId, targetId) { + console.log(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`); + if (!sourceId || !targetId) throw new Error('sourceId and targetId are required'); + if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ'); + + const tx = await sequelize.transaction(); + try { + const source = await PredefinedActivity.findByPk(sourceId, { transaction: tx }); + const target = await PredefinedActivity.findByPk(targetId, { transaction: tx }); + if (!source) throw new Error('Source activity not found'); + if (!target) throw new Error('Target activity not found'); + + // Reassign references + await DiaryDateActivity.update( + { predefinedActivityId: targetId }, + { where: { predefinedActivityId: sourceId }, transaction: tx } + ); + + await GroupActivity.update( + { customActivity: targetId }, + { where: { customActivity: sourceId }, transaction: tx } + ); + + await PredefinedActivityImage.update( + { predefinedActivityId: targetId }, + { where: { predefinedActivityId: sourceId }, transaction: tx } + ); + + // Finally delete source + await source.destroy({ transaction: tx }); + + await tx.commit(); + return { ok: true }; + } catch (err) { + await tx.rollback(); + console.error('[PredefinedActivityService::mergeActivities] - Error:', err); + throw err; + } + } + + async deduplicateActivities() { + console.log('[PredefinedActivityService::deduplicateActivities] - Start'); + const all = await PredefinedActivity.findAll(); + const nameToActivities = new Map(); + for (const activity of all) { + const key = (activity.name || '').trim().toLowerCase(); + if (!key) continue; + if (!nameToActivities.has(key)) nameToActivities.set(key, []); + nameToActivities.get(key).push(activity); + } + let mergedCount = 0; + let groupCount = 0; + for (const list of nameToActivities.values()) { + if (!list || list.length <= 1) continue; + groupCount++; + // Stable target: kleinste ID + list.sort((a, b) => a.id - b.id); + const target = list[0]; + for (const src of list.slice(1)) { + await this.mergeActivities(src.id, target.id); + mergedCount++; + } + } + console.log('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount }); + return { mergedCount, groupCount }; + } } export default new PredefinedActivityService(); diff --git a/frontend/src/views/PredefinedActivities.vue b/frontend/src/views/PredefinedActivities.vue index fff4a04..f2253f9 100644 --- a/frontend/src/views/PredefinedActivities.vue +++ b/frontend/src/views/PredefinedActivities.vue @@ -6,9 +6,25 @@
+
+
+ +
+ + + + +