feat(DiaryView, i18n): enhance diary view with new training day features and localization updates

- Added new sections in the DiaryView for displaying active training day details, including training window, participants, and free activities.
- Implemented toggle functionality for displaying training day and group details, improving user interaction.
- Expanded German localization file with new keys and translations related to training days, groups, and activities, enhancing the user experience for German-speaking users.
This commit is contained in:
Torsten Schulz (local)
2026-03-17 00:16:19 +01:00
parent 2347dccafe
commit bf770291f6
4 changed files with 1305 additions and 417 deletions

View File

@@ -0,0 +1,87 @@
<template>
<section class="diary-sidebar-section">
<h3 class="clickable" @click="$emit('toggle-box')">
{{ $t('diary.activities') }} ({{ activities.length }}) <span>{{ showActivitiesBox ? '-' : '+' }}</span>
</h3>
<div v-if="showActivitiesBox" class="collapsible-box">
<textarea :value="newActivity" @input="$emit('update:newActivity', $event.target.value)"></textarea>
<button @click="$emit('add-activity')">{{ $t('diary.addActivity') }}</button>
<ul v-if="activities.length">
<li v-for="activity in activities" :key="activity.id">
{{ activity.description }}
</li>
</ul>
<div v-else class="activity-empty-state">{{ $t('diary.noFreeActivitiesYet') }}</div>
<multiselect
:model-value="selectedActivityTags"
:options="availableTags"
:placeholder="$t('diary.selectTags')"
label="name"
track-by="id"
multiple
:close-on-select="true"
:allow-empty="false"
@update:model-value="$emit('update:selectedActivityTags', $event)"
@tag="$emit('add-tag', $event)"
@remove="$emit('remove-tag', $event)"
@keydown.enter.prevent="$emit('add-tag-from-input')"
/>
</div>
</section>
</template>
<script>
import Multiselect from 'vue-multiselect';
export default {
name: 'DiaryActivitiesPanel',
components: {
Multiselect
},
props: {
showActivitiesBox: {
type: Boolean,
required: true
},
newActivity: {
type: String,
required: true
},
activities: {
type: Array,
required: true
},
availableTags: {
type: Array,
required: true
},
selectedActivityTags: {
type: Array,
required: true
}
},
emits: [
'toggle-box',
'update:newActivity',
'add-activity',
'update:selectedActivityTags',
'add-tag',
'remove-tag',
'add-tag-from-input'
]
};
</script>
<style scoped>
.diary-sidebar-section {
margin-top: 1rem;
}
.activity-empty-state {
padding: 0.7rem 0.75rem;
border-radius: 8px;
background: #f3f7fa;
color: #516978;
margin-bottom: 0.75rem;
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<section class="diary-sidebar-section">
<h3>{{ $t('diary.participants') }} ({{ participants.length }})</h3>
<div class="participant-toolbar">
<input
:value="participantSearchQuery"
type="search"
class="participant-search-input"
:placeholder="$t('diary.searchParticipants')"
@input="$emit('update:participantSearchQuery', $event.target.value)"
>
<div class="participant-filter-chips">
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'all' }" @click="$emit('update:participantFilter', 'all')">{{ $t('diary.filterAll') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'present' }" @click="$emit('update:participantFilter', 'present')">{{ $t('diary.filterPresent') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'absent' }" @click="$emit('update:participantFilter', 'absent')">{{ $t('diary.filterAbsent') }}</button>
<button type="button" class="participant-filter-chip" :class="{ active: participantFilter === 'test' }" @click="$emit('update:participantFilter', 'test')">{{ $t('diary.filterTest') }}</button>
</div>
</div>
<ul>
<li v-for="member in members" :key="member.id" class="checkbox-item participant-row"
:class="{
'row-inactive': !member.active,
'row-test': member.testMembership && !member.memberFormHandedOver,
'row-test-form': member.testMembership && member.memberFormHandedOver
}">
<label class="checkbox-label">
<input
type="checkbox"
:value="member.id"
:checked="participants.includes(member.id)"
@change="$emit('toggle-participant', member.id)"
>
</label>
<span class="clickable participant-name" @click.stop="$emit('open-notes', member)">
<span v-if="member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" :title="$t('members.sixOrMoreParticipations')">🛑</span>
<span v-else-if="member.testMembership && member.trainingParticipations >= 3" class="warning-icon" :title="$t('members.threeOrMoreParticipations')"></span>
{{ member.firstName }} {{ member.lastName }}
</span>
<div class="participant-actions">
<select
v-if="participants.includes(member.id) && groups.length > 0"
:value="memberGroupsMap[member.id] ?? ''"
class="member-group-select"
@change="$emit('update-member-group', { memberId: member.id, groupId: $event.target.value })"
>
<option value="">-</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}
</option>
</select>
<span @click="$emit('show-pic', member)" class="img-icon" v-if="member.hasImage">&#x1F5BC;</span>
<span
v-if="member.testMembership === true && member.memberFormHandedOver !== true"
@click.stop="$emit('mark-form', member)"
class="pointer form-handover-icon"
:title="$t('diary.formHandedOver')"
>
📄
</span>
<span class="pointer" @click="$emit('open-tags', member)"></span>
</div>
</li>
</ul>
<div class="add-participant">
<button @click="$emit('open-quick-add')" class="quick-add-btn">{{ $t('diary.quickAdd') }}</button>
</div>
</section>
</template>
<script>
export default {
name: 'DiaryParticipantsPanel',
props: {
members: {
type: Array,
required: true
},
participants: {
type: Array,
required: true
},
participantSearchQuery: {
type: String,
required: true
},
participantFilter: {
type: String,
required: true
},
groups: {
type: Array,
required: true
},
memberGroupsMap: {
type: Object,
required: true
}
},
emits: [
'update:participantSearchQuery',
'update:participantFilter',
'toggle-participant',
'update-member-group',
'open-notes',
'show-pic',
'mark-form',
'open-tags',
'open-quick-add'
]
};
</script>
<style scoped>
.diary-sidebar-section {
margin-top: 1rem;
}
.participant-toolbar {
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-bottom: 0.85rem;
}
.participant-search-input {
width: 100%;
padding: 0.55rem 0.7rem;
border: 1px solid #ccd8e0;
border-radius: 8px;
background: #fff;
}
.participant-filter-chips {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.participant-filter-chip {
border: 1px solid #ccd8e0;
background: #f3f7fa;
color: #365266;
border-radius: 999px;
padding: 0.35rem 0.75rem;
font-size: 0.85rem;
cursor: pointer;
}
.participant-filter-chip.active {
background: #dcebf5;
border-color: #8eb7d1;
color: #14374c;
font-weight: 600;
}
.member-group-select {
margin-left: 10px;
font-size: 10px;
width: 5em;
padding: 2px 4px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: #333;
}
</style>

View File

@@ -400,20 +400,32 @@
"applySuggestion": "Vorschlag übernehmen",
"skipSuggestion": "Ohne Vorschlag fortfahren",
"createNewDate": "Neues Datum anlegen",
"activeTrainingDay": "Aktiver Trainingstag",
"trainingDaySection": "Trainingstag",
"trainingStart": "Trainingsbeginn",
"trainingEnd": "Trainingsende",
"trainingWindow": "Trainingsfenster",
"trainingWindowUnset": "Noch nicht gesetzt",
"groupsLabel": "Gruppen",
"groupsSection": "Gruppen",
"createDate": "Datum anlegen",
"editTrainingTimes": "Trainingszeiten bearbeiten",
"updateTimes": "Zeiten aktualisieren",
"groupManagement": "Gruppenverwaltung",
"createGroups": "Gruppen erstellen",
"trainingPlan": "Trainingsplan",
"planActivitiesCount": "Plan-Aktivitäten",
"timeblocksCount": "Zeitblöcke",
"planEmptyState": "Im Trainingsplan ist noch nichts eingetragen.",
"planAddHint": "Neue Plan-Elemente fügst du über die Aktionen oben hinzu.",
"startTime": "Startzeit",
"group": "Gruppe...",
"timeblock": "Zeitblock",
"assignParticipants": "Teilnehmer zuordnen",
"addTimeblock": "Zeitblock",
"activities": "Aktivitäten",
"freeActivities": "Freie Aktivitäten",
"noFreeActivitiesYet": "Noch keine freien Aktivitäten erfasst.",
"addActivity": "Aktivität hinzufügen",
"bookAccident": "Unfall buchen",
"activity": "Aktivität",
@@ -433,12 +445,21 @@
"all": "Alle",
"selectGroup": "Gruppe auswählen...",
"activityPlaceholder": "Aktivität",
"assignShort": "Zuordnen",
"showImage": "Bild/Zeichnung anzeigen",
"participants": "Teilnehmer",
"searchParticipants": "Teilnehmer suchen",
"filterAll": "Alle",
"filterPresent": "Anwesend",
"filterAbsent": "Abwesend",
"filterTest": "Probe",
"quickAdd": "+ Schnell hinzufügen",
"selectTags": "Tags auswählen",
"createDrawing": "Übungszeichnung erstellen",
"overallActivity": "Gesamt-Aktivität",
"editActivity": "Aktivität bearbeiten",
"editGroupActivity": "Gruppen-Aktivität bearbeiten",
"assignParticipantsForGroupActivity": "Teilnehmer für Gruppen-Aktivität zuordnen",
"delete": "Löschen",
"min": "Min",
"errorLoadingPredefinedActivities": "Fehler beim Laden der vordefinierten Aktivitäten",
@@ -459,11 +480,18 @@
"errorDeletingGroup": "Fehler beim Löschen der Gruppe",
"errorCreatingActivity": "Fehler beim Erstellen der Aktivität",
"trainingPlanAsPDF": "Trainingsplan als PDF",
"trainingPlanPdfShort": "Ablaufplan als PDF",
"trainingDayAsPDF": "Trainingstag als PDF herunterladen",
"trainingDayAsPDFShort": "Trainingstag als PDF",
"trainingDaySummaryPdfShort": "Teilnehmerübersicht als PDF",
"minutes": "Minuten",
"formHandedOver": "Mitgliedsformular ausgehändigt",
"errorOccurred": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"trainingTimesUpdated": "Trainingszeiten erfolgreich aktualisiert.",
"noActiveTrainingDay": "Kein Trainingstag ausgewählt.",
"statusReady": "Zeiten, Teilnehmer und Trainingsplan sind gepflegt.",
"statusEmpty": "Dieser Trainingstag ist noch leer.",
"statusInProgress": "Dieser Trainingstag ist teilweise vorbereitet.",
"formMarkedAsHandedOver": "Mitgliedsformular als ausgehändigt markiert",
"errorMarkingForm": "Fehler beim Markieren des Mitgliedsformulars",
"dateNoLongerCurrent": "Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen.",

File diff suppressed because it is too large Load Diff