Fügt Funktionen zum Zusammenführen und Entfernen von Duplikaten vordefinierter Aktivitäten hinzu. Implementiert die entsprechenden Controller-Methoden und Routen. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue, um die neuen Funktionen zur Verfügung zu stellen und die Aktivitäten nach Namen und Code zu sortieren.

This commit is contained in:
Torsten Schulz (local)
2025-08-31 21:09:48 +02:00
parent e3b8488d2b
commit f29425c987
4 changed files with 166 additions and 4 deletions

View File

@@ -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();