feat(DiaryView, i18n): improve diary overview with new readiness indicators and localization updates

- Refactored the DiaryView to implement a tabbed overview for training day, details, and groups, enhancing user navigation.
- Added a readiness panel to display the status of training day elements, including training window, participants, and open plan items.
- Expanded the German localization file with new keys for readiness indicators and checklist items, improving the experience for German-speaking users.
This commit is contained in:
Torsten Schulz (local)
2026-03-17 00:28:36 +01:00
parent bf770291f6
commit d0b6e6f0ac
2 changed files with 334 additions and 54 deletions

View File

@@ -402,6 +402,7 @@
"createNewDate": "Neues Datum anlegen",
"activeTrainingDay": "Aktiver Trainingstag",
"trainingDaySection": "Trainingstag",
"trainingDayChecklist": "Abschlusscheck",
"trainingStart": "Trainingsbeginn",
"trainingEnd": "Trainingsende",
"trainingWindow": "Trainingsfenster",
@@ -446,6 +447,13 @@
"selectGroup": "Gruppe auswählen...",
"activityPlaceholder": "Aktivität",
"assignShort": "Zuordnen",
"statusReadyShort": "Bereit",
"statusOpenShort": "Offen",
"openPlanItemsLabel": "Plan-Status",
"openPlanItems": "{count} offen",
"unassignedPlanItems": "{count} offen",
"durationExampleLong": "z.B. 2x7 oder 3*5",
"durationExampleShort": "z.B. 2x7",
"showImage": "Bild/Zeichnung anzeigen",
"participants": "Teilnehmer",
"searchParticipants": "Teilnehmer suchen",

View File

@@ -78,15 +78,34 @@
<div v-if="date !== null && !showForm" class="diary-content">
<div class="diary-overview-panels">
<section class="diary-toggle-card">
<button type="button" class="diary-toggle-head" @click="showTrainingDayPanel = !showTrainingDayPanel">
<div>
<div class="diary-general-label">{{ $t('diary.trainingDaySection') }}</div>
<strong>{{ getFormattedDate(date.date) }}</strong>
</div>
<span class="diary-toggle-symbol">{{ showTrainingDayPanel ? '' : '+' }}</span>
<div class="diary-overview-switcher">
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'trainingDay' }"
@click="toggleOverviewPanel('trainingDay')"
>
{{ $t('diary.trainingDaySection') }}
</button>
<div v-if="showTrainingDayPanel" class="diary-toggle-body">
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'details' }"
@click="toggleOverviewPanel('details')"
>
{{ $t('common.details') }}
</button>
<button
type="button"
class="diary-overview-switch"
:class="{ active: activeOverviewPanel === 'groups' }"
@click="toggleOverviewPanel('groups')"
>
{{ $t('diary.groupsSection') }}
</button>
</div>
<section v-if="activeOverviewPanel === 'trainingDay'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-workspace-header">
<div class="diary-workspace-copy">
<div class="diary-workspace-label">{{ $t('diary.activeTrainingDay') }}</div>
@@ -114,15 +133,8 @@
</div>
</div>
</section>
<section class="diary-toggle-card">
<button type="button" class="diary-toggle-head" @click="showDetailsPanel = !showDetailsPanel">
<div>
<div class="diary-general-label">{{ $t('common.details') }}</div>
<strong>{{ diaryTimeRangeLabel }}</strong>
</div>
<span class="diary-toggle-symbol">{{ showDetailsPanel ? '' : '+' }}</span>
</button>
<div v-if="showDetailsPanel" class="diary-toggle-body">
<section v-if="activeOverviewPanel === 'details'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<form @submit.prevent="updateTrainingTimes" class="diary-general-form">
<div>
<label for="editTrainingStart">{{ $t('diary.trainingStart') }}:</label>
@@ -136,15 +148,8 @@
</form>
</div>
</section>
<section class="diary-toggle-card">
<button type="button" class="diary-toggle-head" @click="showGroupsPanel = !showGroupsPanel">
<div>
<div class="diary-general-label">{{ $t('diary.groupsSection') }}</div>
<strong>{{ groups.length }} {{ $t('diary.groupsLabel') }}</strong>
</div>
<span class="diary-toggle-symbol">{{ showGroupsPanel ? '' : '+' }}</span>
</button>
<div v-if="showGroupsPanel" class="diary-toggle-body">
<section v-if="activeOverviewPanel === 'groups'" class="diary-toggle-card">
<div class="diary-toggle-body diary-toggle-body-open">
<div class="diary-groups-grid">
<div>
<h4>{{ $t('diary.existingGroups') }}</h4>
@@ -249,6 +254,33 @@
<div v-if="!trainingPlan || trainingPlan.length === 0" class="plan-empty-state">
{{ $t('diary.planEmptyState') }}
</div>
<div v-else class="diary-readiness-panel">
<div class="diary-readiness-header">
<strong>{{ $t('diary.trainingDayChecklist') }}</strong>
</div>
<div class="diary-readiness-grid">
<div class="diary-readiness-item" :class="{ 'ready': Boolean(trainingStart || trainingEnd) }">
<span>{{ $t('diary.trainingWindow') }}</span>
<strong>{{ Boolean(trainingStart || trainingEnd) ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
</div>
<div class="diary-readiness-item" :class="{ 'ready': participants.length > 0 }">
<span>{{ $t('diary.participants') }}</span>
<strong>{{ participants.length > 0 ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
</div>
<div class="diary-readiness-item" :class="{ 'ready': trainingPlan.length > 0 }">
<span>{{ $t('diary.trainingPlan') }}</span>
<strong>{{ trainingPlan.length > 0 ? $t('diary.statusReadyShort') : $t('diary.statusOpenShort') }}</strong>
</div>
<div class="diary-readiness-item" :class="{ 'ready': openPlanItems.length === 0 }">
<span>{{ $t('diary.openPlanItemsLabel') }}</span>
<strong>
{{ openPlanItems.length === 0
? $t('diary.statusReadyShort')
: $t('diary.openPlanItems', { count: openPlanItems.length }) }}
</strong>
</div>
</div>
</div>
<div v-if="addNewItem || addNewTimeblock || addNewGroupActivity" class="plan-composer">
<div class="plan-composer-header">
<strong>
@@ -296,7 +328,7 @@
<div v-if="addNewItem || addNewTimeblock" class="plan-composer-field">
<label>{{ $t('diary.durationMinutes') }}</label>
<div class="plan-composer-duration">
<input type="text" v-model="newPlanItem.durationText" @input="calculateDuration" placeholder="z.B. 2x7 oder 3*5" />
<input type="text" v-model="newPlanItem.durationText" @input="calculateDuration" :placeholder="$t('diary.durationExampleLong')" />
<input type="number" v-model="newPlanItem.duration" :placeholder="$t('diary.minutes')" />
</div>
</div>
@@ -343,7 +375,7 @@
<div class="plan-editor-field">
<label>{{ $t('diary.durationMinutes') }}</label>
<div class="plan-composer-duration">
<input type="text" v-model="editingDurationText" @input="calculateDurationForEdit" placeholder="z.B. 2x7" />
<input type="text" v-model="editingDurationText" @input="calculateDurationForEdit" :placeholder="$t('diary.durationExampleShort')" />
<input type="number" v-model="editingDuration" :placeholder="$t('diary.min')" @keyup.enter="saveActivityEdit(activePlanEditorItem)" />
</div>
</div>
@@ -449,20 +481,25 @@
<td class="drag-handle" style="cursor: move;"></td> <!-- Drag-Handle -->
<td>{{ formatDisplayTime(item.startTime) }}</td>
<td>
<span v-if="item.isTimeblock" class="plan-type-badge plan-type-badge-timeblock">{{ $t('diary.timeblock') }}</span>
<span v-else @click="startActivityEdit(item)"
class="clickable activity-label"
:title="item.predefinedActivity && item.predefinedActivity.name ? item.predefinedActivity.name : ''">
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
<span v-if="hasActivityVisual(item.predefinedActivity)"
@click.stop="openActivityVisual(item.predefinedActivity)"
class="image-icon" :title="$t('diary.showImage')">🖼</span>
{{ (item.predefinedActivity && item.predefinedActivity.code &&
item.predefinedActivity.code.trim() !== '')
? item.predefinedActivity.code
: (item.predefinedActivity ? item.predefinedActivity.name :
item.activity) }}
</span>
<div class="plan-activity-main">
<span v-if="item.isTimeblock" class="plan-type-badge plan-type-badge-timeblock">{{ $t('diary.timeblock') }}</span>
<span v-else @click="startActivityEdit(item)"
class="clickable activity-label"
:title="item.predefinedActivity && item.predefinedActivity.name ? item.predefinedActivity.name : ''">
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
<span v-if="hasActivityVisual(item.predefinedActivity)"
@click.stop="openActivityVisual(item.predefinedActivity)"
class="image-icon" :title="$t('diary.showImage')">🖼</span>
{{ (item.predefinedActivity && item.predefinedActivity.code &&
item.predefinedActivity.code.trim() !== '')
? item.predefinedActivity.code
: (item.predefinedActivity ? item.predefinedActivity.name :
item.activity) }}
</span>
<span class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(item).tone}`" :title="getPlanItemStatus(item).reason || ''">
{{ getPlanItemStatus(item).label }}
</span>
</div>
</td>
<td>
<span v-if="item.isTimeblock && item.groupActivities && item.groupActivities.length" class="plan-row-muted">
@@ -540,6 +577,9 @@
? groupItem.groupPredefinedActivity.code
: groupItem.groupPredefinedActivity.name }}
</span>
<span class="plan-status-badge" :class="`plan-status-badge-${getPlanItemStatus(groupItem).tone}`" :title="getPlanItemStatus(groupItem).reason || ''">
{{ getPlanItemStatus(groupItem).label }}
</span>
</div>
</td>
<td>{{ groupItem.groupsGroupActivity.name }}</td>
@@ -860,9 +900,7 @@ export default {
addNewItem: false,
addNewGroupActivity: false,
addNewTimeblock: false,
showTrainingDayPanel: true,
showDetailsPanel: false,
showGroupsPanel: false,
activeOverviewPanel: 'trainingDay',
editingGroupId: null,
doMemberTagUpdates: true,
showTagHistoryModal: false,
@@ -1005,6 +1043,20 @@ export default {
}
return null;
},
openPlanItems() {
const result = [];
for (const item of this.trainingPlan || []) {
if (!item?.isTimeblock && this.getPlanItemStatus(item).key === 'open') {
result.push(item);
}
for (const groupItem of item.groupActivities || []) {
if (this.getPlanItemStatus(groupItem).key === 'open') {
result.push(groupItem);
}
}
}
return result;
},
filteredDiaryMembers() {
const search = (this.participantSearchQuery || '').trim().toLowerCase();
@@ -1077,7 +1129,7 @@ export default {
const hasPlan = this.trainingPlan.length > 0;
const hasActivities = this.activities.length > 0;
if (hasTimes && hasParticipants && hasPlan) {
if (hasTimes && hasParticipants && hasPlan && this.openPlanItems.length === 0) {
return this.$t('diary.statusReady');
}
@@ -1095,6 +1147,9 @@ export default {
const hasPlan = Array.isArray(this.trainingPlan) && this.trainingPlan.length > 0;
return hasTimes && hasParticipants && hasPlan;
},
toggleOverviewPanel(panel) {
this.activeOverviewPanel = this.activeOverviewPanel === panel ? null : panel;
},
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = {
@@ -1357,9 +1412,7 @@ export default {
this.calculateIntermediateTimes();
this.initializeSortable();
await this.loadGroups();
this.showTrainingDayPanel = !this.isDiaryDayConfigured();
this.showDetailsPanel = false;
this.showGroupsPanel = false;
this.activeOverviewPanel = this.isDiaryDayConfigured() ? null : 'trainingDay';
this.startCheckingTime();
} else {
this.newDate = '';
@@ -2965,6 +3018,32 @@ export default {
const map = this.participantMapByMemberId || {};
return map[memberId];
},
getPlanItemStatus(item) {
if (!item) {
return { key: 'open', label: this.$t('diary.statusOpenShort'), tone: 'open' };
}
if (item.isTimeblock) {
const groupActivities = item.groupActivities || [];
if (groupActivities.length === 0) {
return { key: 'open', label: this.$t('diary.statusOpenShort'), tone: 'open' };
}
const allReady = groupActivities.every(groupItem => this.getPlanItemStatus(groupItem).key === 'ready');
return allReady
? { key: 'ready', label: this.$t('diary.statusReadyShort'), tone: 'ready' }
: { key: 'open', label: this.$t('diary.statusOpenShort'), tone: 'open' };
}
const assignedCount = item.groupPredefinedActivity
? this.presentMembers.filter(member => this.isAssignedToGroupActivity(item.id, member.id)).length
: this.presentMembers.filter(member => this.isAssignedToActivity(item.id, member.id)).length;
if (assignedCount > 0) {
return { key: 'ready', label: this.$t('diary.statusReadyShort'), tone: 'ready' };
}
return { key: 'open', label: this.$t('diary.statusOpenShort'), tone: 'open' };
},
async toggleMemberForActivity(activityId, memberId, checked) {
let participantId = this.participantIdForMember(memberId);
@@ -3610,6 +3689,28 @@ form div {
margin-bottom: 1rem;
}
.diary-overview-switcher {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.diary-overview-switch {
padding: 0.5rem 0.8rem;
border: 1px solid #cfdbe3;
border-radius: 999px;
background: #fff;
color: #315066;
font-weight: 600;
cursor: pointer;
}
.diary-overview-switch.active {
background: #e6f0f6;
border-color: #9bb9cb;
color: #173042;
}
.diary-toggle-card {
border: 1px solid #d9e4ec;
border-radius: 12px;
@@ -3622,22 +3723,38 @@ form div {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.95rem 1.15rem;
gap: 0.75rem;
padding: 0.65rem 0.9rem;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
}
.diary-toggle-head > div {
min-width: 0;
line-height: 1.2;
}
.diary-toggle-head strong {
display: block;
margin-top: 0.1rem;
font-size: 0.98rem;
line-height: 1.2;
}
.diary-toggle-symbol {
font-size: 1.4rem;
font-size: 1.15rem;
line-height: 1;
color: #315066;
}
.diary-toggle-body {
padding: 0 1.15rem 1.1rem;
padding: 0 0.9rem 0.9rem;
}
.diary-toggle-body-open {
padding-top: 0.9rem;
}
.diary-groups-grid {
@@ -4380,7 +4497,8 @@ img {
}
.diary-workspace-copy h3 {
margin: 0.2rem 0 0.35rem;
margin: 0.1rem 0 0.25rem;
font-size: 1.15rem;
}
.diary-workspace-label {
@@ -4393,6 +4511,7 @@ img {
.diary-workspace-status {
margin: 0;
font-size: 0.92rem;
color: #45606f;
}
@@ -4426,6 +4545,91 @@ img {
color: #173042;
}
@media (min-width: 769px) and (max-width: 1100px) {
.diary-overview-panels {
display: grid;
grid-template-columns: 1fr;
gap: 0.65rem;
margin-bottom: 0.8rem;
}
.diary-overview-switcher {
gap: 0.4rem;
}
.diary-overview-switch {
padding: 0.42rem 0.68rem;
font-size: 0.9rem;
}
.diary-toggle-card {
border-radius: 10px;
}
.diary-toggle-head {
gap: 0.7rem;
padding: 0.7rem 0.85rem;
}
.diary-toggle-body {
padding: 0 0.85rem 0.8rem;
}
.diary-general-label {
font-size: 0.72rem;
}
.diary-toggle-head strong {
display: block;
font-size: 0.95rem;
line-height: 1.25;
}
.diary-toggle-symbol {
font-size: 1.1rem;
}
.diary-workspace-header {
gap: 0.75rem;
margin-bottom: 0;
padding: 0.75rem 0.85rem;
border-radius: 10px;
}
.diary-workspace-copy h3 {
font-size: 1rem;
}
.diary-workspace-status {
font-size: 0.88rem;
}
.diary-workspace-stats {
grid-template-columns: repeat(2, minmax(92px, 1fr));
gap: 0.5rem;
width: 100%;
}
.diary-stat-card {
padding: 0.6rem 0.7rem;
border-radius: 8px;
}
.diary-stat-label {
font-size: 0.68rem;
}
.diary-stat-value {
margin-top: 0.15rem;
font-size: 0.92rem;
}
.diary-groups-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
}
.participant-toolbar {
display: flex;
flex-direction: column;
@@ -4612,6 +4816,13 @@ img {
padding-left: 0.75rem;
}
.plan-activity-main {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}
.plan-type-badge {
display: inline-flex;
align-items: center;
@@ -4638,6 +4849,63 @@ img {
font-size: 0.84rem;
}
.plan-status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.1rem 0.45rem;
font-size: 0.72rem;
font-weight: 700;
}
.plan-status-badge-ready {
background: #e9f3e6;
color: #245037;
}
.plan-status-badge-open {
background: #fff2d9;
color: #7a5a12;
}
.diary-readiness-panel {
margin-bottom: 1rem;
padding: 0.9rem 1rem;
border: 1px solid #d9e4ec;
border-radius: 10px;
background: #f8fbfd;
}
.diary-readiness-header {
margin-bottom: 0.75rem;
}
.diary-readiness-grid {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 0.75rem;
}
.diary-readiness-item {
padding: 0.7rem 0.8rem;
border-radius: 8px;
background: #fff2d9;
color: #7a5a12;
}
.diary-readiness-item.ready {
background: #e9f3e6;
color: #245037;
}
.diary-readiness-item span {
display: block;
font-size: 0.76rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.plan-editor {
margin-bottom: 1rem;
padding: 0.95rem 1rem;
@@ -4795,6 +5063,10 @@ img {
grid-template-columns: 1fr;
}
.diary-readiness-grid {
grid-template-columns: 1fr 1fr;
}
.plan-assignment-list {
grid-template-columns: 1fr;
}