feat(DiaryView): implement grouped plan table for enhanced activity display
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 42s

- Added a new grouped plan table view in the DiaryView component to display activities organized by groups.
- Introduced computed properties `showGroupedPlanTable` and `groupedPlanRows` to manage the display logic and data structure for grouped activities.
- Enhanced the template to conditionally render the grouped view, improving user experience and clarity in activity management.
This commit is contained in:
Torsten Schulz (local)
2026-05-08 13:11:56 +02:00
parent d1fb6d4e74
commit 9622e9bdb7

View File

@@ -412,7 +412,12 @@
</div>
<table>
<thead>
<tr>
<tr v-if="showGroupedPlanTable">
<th>{{ $t('diary.startTime') }}</th>
<th>Gemeinsam</th>
<th v-for="group in groups" :key="`group-col-${group.id}`">{{ group.name }}</th>
</tr>
<tr v-else>
<th></th> <!-- Neue Spalte für Drag-Handle -->
<th>{{ $t('diary.startTime') }}</th>
<th>{{ $t('diary.activityOrTimeblock') }}</th>
@@ -421,7 +426,54 @@
<th></th>
</tr>
</thead>
<tbody ref="sortableList">
<tbody v-if="showGroupedPlanTable">
<tr v-for="row in groupedPlanRows" :key="row.key">
<td>{{ formatDisplayTime(row.startTime) }}</td>
<td>
<div v-if="row.sharedItems.length" class="plan-cell-stack">
<div v-for="item in row.sharedItems" :key="`shared-${item.id}`" class="plan-cell-item">
<div class="plan-activity-main">
<span @click="startActivityEdit(item)" class="clickable activity-label">{{ getPlanItemDisplayLabel(item) }}</span>
<span v-if="!isStructuralPlanItem(item)" class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(item).tone}`">
{{ getPlanItemStatus(item).label }}
</span>
</div>
<div class="plan-row-actions">
<button @click="startActivityEdit(item)" class="plan-row-action-button">{{ $t('common.edit') }}</button>
<button v-if="!isStructuralPlanItem(item)" @click="toggleActivityMembers(item)" class="plan-row-action-button">{{ $t('diary.assignShort') }}</button>
<button @click="removePlanItem(item.id)" class="plan-row-action-button plan-row-action-button-danger">{{ $t('common.delete') }}</button>
</div>
</div>
</div>
<span v-else class="plan-row-muted">-</span>
</td>
<td v-for="group in groups" :key="`row-${row.key}-group-${group.id}`">
<div v-if="(row.groupItems[group.id] || []).length" class="plan-cell-stack">
<div v-for="item in (row.groupItems[group.id] || [])" :key="`group-item-${item.id}`" class="plan-cell-item">
<div class="plan-activity-main">
<span @click="startActivityEdit(item)" class="clickable activity-label">{{ getPlanItemDisplayLabel(item) }}</span>
<span v-if="!isStructuralPlanItem(item)" class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(item).tone}`">
{{ getPlanItemStatus(item).label }}
</span>
</div>
<div class="plan-row-actions">
<button @click="startActivityEdit(item)" class="plan-row-action-button">{{ $t('common.edit') }}</button>
<button v-if="!isStructuralPlanItem(item)" @click="toggleActivityMembers(item)" class="plan-row-action-button">{{ $t('diary.assignShort') }}</button>
<button @click="removePlanItem(item.id)" class="plan-row-action-button plan-row-action-button-danger">{{ $t('common.delete') }}</button>
</div>
</div>
</div>
<span v-else class="plan-row-muted">-</span>
</td>
</tr>
<tr>
<td>{{ calculateNextTime }}</td>
<td :colspan="groups.length + 1">
<span class="plan-add-hint">{{ $t('diary.planAddHint') }}</span>
</td>
</tr>
</tbody>
<tbody v-else ref="sortableList">
<template v-for="(item, index) in filteredTrainingPlan" :key="item.id">
<tr :class="{ 'plan-timeblock-row': item.isTimeblock }" class="plan-sortable-row" :data-plan-id="item.id">
<td class="drag-handle" style="cursor: move;"></td> <!-- Drag-Handle -->
@@ -1047,6 +1099,71 @@ export default {
standalonePlanItemCount() {
return (this.trainingPlan || []).filter(item => item && !item.isTimeblock).length;
},
showGroupedPlanTable() {
return this.planGroupFilter === '__all__' && Array.isArray(this.groups) && this.groups.length > 0;
},
groupedPlanRows() {
if (!this.showGroupedPlanTable) return [];
const rowsByKey = new Map();
const toMinutes = (timeValue) => {
if (!timeValue || typeof timeValue !== 'string' || !timeValue.includes(':')) {
return Number.MAX_SAFE_INTEGER;
}
const [h, m] = timeValue.split(':').map((part) => parseInt(part, 10));
if (!Number.isFinite(h) || !Number.isFinite(m)) {
return Number.MAX_SAFE_INTEGER;
}
return (h * 60) + m;
};
for (const item of (this.trainingPlan || [])) {
if (!item || item.isTimeblock) continue;
const key = item.startTime || `unknown-${item.id}`;
if (!rowsByKey.has(key)) {
rowsByKey.set(key, {
key,
startTime: item.startTime || '',
sharedItems: [],
groupItems: {}
});
}
const row = rowsByKey.get(key);
const groupId = Number(item.groupId);
if (Number.isFinite(groupId) && groupId > 0) {
if (!Array.isArray(row.groupItems[groupId])) {
row.groupItems[groupId] = [];
}
row.groupItems[groupId].push(item);
} else {
row.sharedItems.push(item);
}
}
const sortItems = (items) => items.slice().sort((a, b) => {
const orderA = Number.isFinite(Number(a?.orderId)) ? Number(a.orderId) : Number.MAX_SAFE_INTEGER;
const orderB = Number.isFinite(Number(b?.orderId)) ? Number(b.orderId) : Number.MAX_SAFE_INTEGER;
if (orderA !== orderB) return orderA - orderB;
return Number(a?.id || 0) - Number(b?.id || 0);
});
const rows = Array.from(rowsByKey.values()).map((row) => {
row.sharedItems = sortItems(row.sharedItems);
for (const group of this.groups || []) {
const groupId = Number(group.id);
if (Array.isArray(row.groupItems[groupId])) {
row.groupItems[groupId] = sortItems(row.groupItems[groupId]);
}
}
return row;
});
return rows.sort((a, b) => {
const startA = toMinutes(a?.startTime);
const startB = toMinutes(b?.startTime);
if (startA !== startB) return startA - startB;
return String(a?.key || '').localeCompare(String(b?.key || ''));
});
},
filteredTrainingPlan() {
const allItems = Array.isArray(this.trainingPlan) ? this.trainingPlan : [];
const toMinutes = (timeValue) => {
@@ -1541,6 +1658,7 @@ export default {
initializeSortable() {
const el = this.$refs.sortableList;
if (!el) return;
Sortable.create(el, {
draggable: '.plan-sortable-row',
handle: ".drag-handle",
@@ -2836,6 +2954,17 @@ export default {
const value = String(timeValue);
return value.length >= 5 ? value.slice(0, 5) : value;
},
getPlanItemDisplayLabel(item) {
if (!item) return '';
const predefined = item.predefinedActivity || item.groupPredefinedActivity;
if (predefined?.code && predefined.code.trim() !== '') {
return predefined.code;
}
if (predefined?.name) {
return predefined.name;
}
return item.activity || '';
},
editGroup(groupId) {
this.editingGroupId = groupId;
},
@@ -5297,6 +5426,22 @@ img {
flex-wrap: wrap;
}
.plan-cell-stack {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.plan-cell-item {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.35rem 0.4rem;
border: 1px solid #e4edf2;
border-radius: 8px;
background: #f9fcfe;
}
.plan-row-action-button {
border: 1px solid #cfdbe3;
background: #f3f7fa;