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

@@ -6,9 +6,25 @@
<div class="toolbar">
<button @click="startCreate" class="btn-primary">Neu</button>
<button @click="reload" class="btn-secondary">Neu laden</button>
<div>
<div>
<button @click="deduplicate" class="btn-secondary">Doppelungen zusammenführen</button>
</div
<div class="merge-tools">
<select v-model="mergeSourceId">
<option disabled value="">Quelle wählen</option>
<option v-for="a in activities" :key="'s'+a.id" :value="a.id">{{ formatItem(a) }}</option>
</select>
<span></span>
<select v-model="mergeTargetId">
<option disabled value="">Ziel wählen</option>
<option v-for="a in activities" :key="'t'+a.id" :value="a.id">{{ formatItem(a) }}</option>
</select>
<button class="btn-secondary" :disabled="!canMerge" @click="mergeSelected">Zusammenführen</button>
</div>
</div>
<ul class="items">
<li v-for="a in activities" :key="a.id" :class="{ active: selectedActivity && selectedActivity.id === a.id }" @click="select(a)">
<li v-for="a in sortedActivities" :key="a.id" :class="{ active: selectedActivity && selectedActivity.id === a.id }" @click="select(a)">
<div class="title">
<strong>{{ a.code ? '[' + a.code + '] ' : '' }}{{ a.name }}</strong>
</div>
@@ -76,12 +92,33 @@ export default {
editModel: null,
images: [],
selectedFile: null,
mergeSourceId: '',
mergeTargetId: '',
};
},
computed: {
sortedActivities() {
return [...(this.activities || [])].sort((a, b) => {
const ac = (a.code || '').toLocaleLowerCase('de-DE');
const bc = (b.code || '').toLocaleLowerCase('de-DE');
const aEmpty = ac === '';
const bEmpty = bc === '';
if (aEmpty !== bEmpty) return aEmpty ? 1 : -1; // leere Codes nach hinten
if (ac < bc) return -1; if (ac > bc) return 1;
const an = (a.name || '').toLocaleLowerCase('de-DE');
const bn = (b.name || '').toLocaleLowerCase('de-DE');
if (an < bn) return -1; if (an > bn) return 1;
return 0;
});
},
canMerge() {
return this.mergeSourceId && this.mergeTargetId && String(this.mergeSourceId) !== String(this.mergeTargetId);
}
},
methods: {
async reload() {
const r = await apiClient.get('/predefined-activities');
this.activities = r.data;
this.activities = r.data || [];
},
async select(a) {
this.selectedActivity = a;
@@ -90,6 +127,18 @@ export default {
this.images = images || [];
this.editModel = { ...activity };
},
formatItem(a) {
return `${a.code ? '[' + a.code + '] ' : ''}${a.name}`;
},
async mergeSelected() {
if (!this.canMerge) return;
const src = this.mergeSourceId; const tgt = this.mergeTargetId;
if (!confirm(`Eintrag #${src} in #${tgt} zusammenführen?\nAlle Verknüpfungen werden auf das Ziel umgebogen, die Quelle wird gelöscht.`)) return;
await apiClient.post('/predefined-activities/merge', { sourceId: src, targetId: tgt });
this.mergeSourceId = '';
this.mergeTargetId = '';
await this.reload();
},
startCreate() {
this.selectedActivity = null;
this.images = [];
@@ -135,6 +184,11 @@ export default {
// Nach Upload Details neu laden
await this.select(this.editModel);
this.selectedFile = null;
},
async deduplicate() {
if (!confirm('Alle Aktivitäten mit identischem Namen werden zusammengeführt. Fortfahren?')) return;
await apiClient.post('/predefined-activities/deduplicate', {});
await this.reload();
}
},
async mounted() {
@@ -164,6 +218,8 @@ export default {
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.merge-tools { display: inline-flex; align-items: center; gap: .35rem; margin-left: auto; }
select { max-width: 220px; }
.items {
list-style: none;
padding: 0;