feat(Diary): implement quick create functionality for training days and enhance localization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Added a new button for quick creation of training days in the DiaryView, improving user experience.
- Implemented logic to find the next available training slot across groups and create a training day entry.
- Enhanced localization by adding new keys for quick create messages in multiple languages, ensuring better accessibility for users.
- Updated the DiaryManager to handle quick create operations and clear errors effectively.
This commit is contained in:
Torsten Schulz (local)
2026-05-14 22:35:29 +02:00
parent 95a3e9438a
commit 83294406a4
29 changed files with 1976 additions and 46 deletions

View File

@@ -828,6 +828,9 @@
"noEntries": "Keine Einträge",
"deleteDate": "Datum löschen",
"createNew": "Neu anlegen",
"quickCreate": "Schnellanlegen",
"quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).",
"quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.",
"gallery": "Mitglieder-Galerie",
"galleryCreating": "Galerie wird erstellt…",
"selectTrainingGroup": "Trainingsgruppe auswählen",

View File

@@ -575,6 +575,9 @@
"noEntries": "Keine Einträge",
"deleteDate": "Datum löschen",
"createNew": "Neu anlegen",
"quickCreate": "Schnellanlegen",
"quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).",
"quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.",
"gallery": "Mitglieder-Galerie",
"galleryCreating": "Galerie wird erstellt…",
"selectTrainingGroup": "Trainingsgruppe auswählen",

View File

@@ -650,6 +650,9 @@
"noEntries": "Keine Einträge",
"deleteDate": "Datum löschen",
"createNew": "Neu anlegen",
"quickCreate": "Schnellanlegen",
"quickCreateNoSlot": "Kein freier Trainingstermin gefunden (gemäß Gruppenzeiten, ein Jahr voraus).",
"quickCreateFailed": "Der Trainingstag konnte nicht angelegt werden.",
"gallery": "Mitglieder-Galerie",
"galleryCreating": "Galerie wird erstellt…",
"selectTrainingGroup": "Trainingsgruppe auswählen",

View File

@@ -827,6 +827,9 @@
"noEntries": "No entries",
"deleteDate": "Delete date",
"createNew": "Create new",
"quickCreate": "Quick add",
"quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).",
"quickCreateFailed": "Could not create the training day entry.",
"gallery": "Member gallery",
"galleryCreating": "Creating gallery…",
"selectTrainingGroup": "Select training group",

View File

@@ -320,6 +320,9 @@
"noEntries": "No entries",
"deleteDate": "Delete date",
"createNew": "Create new",
"quickCreate": "Quick add",
"quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).",
"quickCreateFailed": "Could not create the training day entry.",
"gallery": "Member gallery",
"galleryCreating": "Creating gallery…",
"selectTrainingGroup": "Select training group",

View File

@@ -827,6 +827,9 @@
"noEntries": "No entries",
"deleteDate": "Delete date",
"createNew": "Create new",
"quickCreate": "Quick add",
"quickCreateNoSlot": "No free training slot found (per group schedule, one year ahead).",
"quickCreateFailed": "Could not create the training day entry.",
"gallery": "Member gallery",
"galleryCreating": "Creating gallery…",
"selectTrainingGroup": "Select training group",

View File

@@ -793,6 +793,9 @@
"noEntries": "No hay entradas",
"deleteDate": "Eliminar fecha",
"createNew": "Crear",
"quickCreate": "Alta rápida",
"quickCreateNoSlot": "No se encontró un día de entrenamiento libre (según horarios de grupos, un año adelante).",
"quickCreateFailed": "No se pudo crear el día de entrenamiento.",
"gallery": "Galería de miembros",
"galleryCreating": "Creando galería…",
"selectTrainingGroup": "Seleccionar grupo de entrenamiento",

View File

@@ -793,6 +793,9 @@
"noEntries": "Walang entry",
"deleteDate": "Burahin ang petsa",
"createNew": "Lumikha ng bago",
"quickCreate": "Mabilis na idagdag",
"quickCreateNoSlot": "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).",
"quickCreateFailed": "Hindi malikha ang araw ng training.",
"gallery": "Gallery ng mga kasapi",
"galleryCreating": "Ginagawa ang gallery…",
"selectTrainingGroup": "Pumili ng grupo ng pagsasanay",

View File

@@ -793,6 +793,9 @@
"noEntries": "Aucune entrée",
"deleteDate": "Supprimer la date",
"createNew": "Créer",
"quickCreate": "Création rapide",
"quickCreateNoSlot": "Aucun créneau dentraînement libre trouvé (selon les horaires des groupes, sur un an).",
"quickCreateFailed": "Impossible de créer la journée dentraînement.",
"gallery": "Galerie des membres",
"galleryCreating": "Création de la galerie…",
"selectTrainingGroup": "Sélectionner un groupe d'entraînement",

View File

@@ -793,6 +793,9 @@
"noEntries": "Nessuna voce",
"deleteDate": "Elimina data",
"createNew": "Crea",
"quickCreate": "Creazione rapida",
"quickCreateNoSlot": "Nessun giorno di allenamento libero trovato (in base agli orari dei gruppi, entro un anno).",
"quickCreateFailed": "Impossibile creare la giornata di allenamento.",
"gallery": "Galleria membri",
"galleryCreating": "Creazione galleria…",
"selectTrainingGroup": "Seleziona gruppo di allenamento",

View File

@@ -793,6 +793,9 @@
"noEntries": "エントリがありません",
"deleteDate": "日付を削除",
"createNew": "新規作成",
"quickCreate": "クイック作成",
"quickCreateNoSlot": "空きの練習日が見つかりませんグループのスケジュールに基づき、1年先まで。",
"quickCreateFailed": "練習日を作成できませんでした。",
"gallery": "メンバーギャラリー",
"galleryCreating": "ギャラリーを作成中…",
"selectTrainingGroup": "練習グループを選択",

View File

@@ -793,6 +793,9 @@
"noEntries": "Brak wpisów",
"deleteDate": "Usuń datę",
"createNew": "Utwórz",
"quickCreate": "Szybkie dodawanie",
"quickCreateNoSlot": "Nie znaleziono wolnego terminu treningu (wg harmonogramów grup, rok do przodu).",
"quickCreateFailed": "Nie udało się utworzyć dnia treningowego.",
"gallery": "Galeria członków",
"galleryCreating": "Tworzenie galerii…",
"selectTrainingGroup": "Wybierz grupę treningową",

View File

@@ -793,6 +793,9 @@
"noEntries": "ไม่มีรายการ",
"deleteDate": "ลบวันที่",
"createNew": "สร้างใหม่",
"quickCreate": "เพิ่มด่วน",
"quickCreateNoSlot": "ไม่พบวันฝึกที่ว่าง (ตามตารางกลุ่ม ล่วงหน้าหนึ่งปี)",
"quickCreateFailed": "ไม่สามารถสร้างวันฝึกได้",
"gallery": "แกลเลอรีสมาชิก",
"galleryCreating": "กำลังสร้างแกลเลอรี…",
"selectTrainingGroup": "เลือกกลุ่มฝึกซ้อม",

View File

@@ -793,6 +793,9 @@
"noEntries": "Walang entry",
"deleteDate": "Burahin ang petsa",
"createNew": "Lumikha ng bago",
"quickCreate": "Mabilis na idagdag",
"quickCreateNoSlot": "Walang nahanap na libreng araw ng training (ayon sa iskedyul ng grupo, isang taon pasulong).",
"quickCreateFailed": "Hindi malikha ang araw ng training.",
"gallery": "Gallery ng mga miyembro",
"galleryCreating": "Ginagawa ang gallery…",
"selectTrainingGroup": "Pumili ng grupo ng pagsasanay",

View File

@@ -793,6 +793,9 @@
"noEntries": "没有条目",
"deleteDate": "删除日期",
"createNew": "新建",
"quickCreate": "快速创建",
"quickCreateNoSlot": "未找到空闲训练日(按小组时间表,向前查找一年)。",
"quickCreateFailed": "无法创建训练日。",
"gallery": "成员图库",
"galleryCreating": "正在生成图库…",
"selectTrainingGroup": "选择训练组",

View File

@@ -11,6 +11,9 @@
<button v-if="date && canDeleteCurrentDate" class="btn-secondary"
@click="deleteCurrentDate">{{ $t('diary.deleteDate') }}</button>
<button @click="openNewDateDialog" class="btn-primary">{{ $t('diary.createNew') }}</button>
<button type="button" @click="quickCreateNextTraining" class="btn-secondary" :disabled="quickCreateBusy">
{{ $t('diary.quickCreate') }}
</button>
</label>
</div>
<!-- Training Group Selection Dialog -->
@@ -1052,7 +1055,8 @@ export default {
isMobileView: typeof window !== 'undefined' ? window.innerWidth <= 768 : false,
participantSearchQuery: '',
participantFilter: 'all',
planGroupFilter: '__all__'
planGroupFilter: '__all__',
quickCreateBusy: false
};
},
watch: {
@@ -1623,6 +1627,76 @@ export default {
this.showTrainingGroupDialog = true;
this.showForm = false;
},
diaryDateKey(entry) {
if (!entry || entry.date == null) return '';
return String(entry.date).substring(0, 10);
},
localYyyyMmDd(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
},
findNextQuickSlotAcrossGroups(groups) {
if (!Array.isArray(groups) || groups.length === 0) return null;
const sorted = [...groups].sort((a, b) => {
const oa = Number.isFinite(Number(a.sortOrder)) ? Number(a.sortOrder) : 1e9;
const ob = Number.isFinite(Number(b.sortOrder)) ? Number(b.sortOrder) : 1e9;
if (oa !== ob) return oa - ob;
return (Number(a.id) || 0) - (Number(b.id) || 0);
});
const existing = new Set((this.dates || []).map((d) => this.diaryDateKey(d)));
const today = new Date();
for (let dayOffset = 0; dayOffset <= 366; dayOffset++) {
const checkDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + dayOffset);
const checkWeekday = checkDate.getDay();
const dateStr = this.localYyyyMmDd(checkDate);
if (existing.has(dateStr)) continue;
for (const g of sorted) {
const times = (g.trainingTimes || [])
.filter((tt) => tt.weekday === checkWeekday && tt.startTime && String(tt.startTime).trim());
if (!times.length) continue;
times.sort((a, b) => String(a.startTime).localeCompare(String(b.startTime))
|| (Number(a.id) || 0) - (Number(b.id) || 0));
const time = times[0];
return {
date: dateStr,
startTime: this.formatTimeForInput(time.startTime),
endTime: this.formatTimeForInput(time.endTime),
};
}
}
return null;
},
async quickCreateNextTraining() {
if (!this.currentClub || this.quickCreateBusy) return;
this.quickCreateBusy = true;
try {
const response = await apiClient.get(`/training-times/${this.currentClub}`);
const groups = Array.isArray(response.data) ? response.data : [];
const slot = this.findNextQuickSlotAcrossGroups(groups);
if (!slot) {
this.showInfo(this.$t('messages.info'), this.$t('diary.quickCreateNoSlot'), '', 'info');
return;
}
const post = await apiClient.post(`/diary/${this.currentClub}`, {
date: slot.date,
trainingStart: slot.startTime || null,
trainingEnd: slot.endTime || null,
});
await this.refreshDates(post.data.id);
await this.handleDateChange();
} catch (error) {
console.error('[quickCreateNextTraining]', error);
this.showInfo(this.$t('messages.error'), this.$t('diary.quickCreateFailed'), '', 'error');
} finally {
this.quickCreateBusy = false;
}
},
async handleDateChange() {
this.showForm = false;
@@ -1780,8 +1854,8 @@ export default {
// Nimm die erste Trainingszeit für diesen Tag
const time = timesForWeekday[0];
// Prüfe, ob dieses Datum bereits existiert
const dateStr = checkDate.toISOString().split('T')[0];
const exists = this.dates.some(d => d.date === dateStr);
const dateStr = this.localYyyyMmDd(checkDate);
const exists = this.dates.some(d => this.diaryDateKey(d) === dateStr);
if (!exists) {
return {
@@ -4403,6 +4477,31 @@ h3 {
margin-bottom: 1rem;
}
/* Global `label { display: inline-flex }` würde sonst alle Kinder in eine Zeile zwängen und Buttons schrumpfen lassen. */
.diary-header-row > label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
margin: 0;
cursor: default;
}
.diary-header-row > label > select {
flex: 1 1 12rem;
min-width: min(12rem, 100%);
max-width: 100%;
}
.diary-header-row > label > .btn-primary,
.diary-header-row > label > .btn-secondary {
flex-shrink: 0;
white-space: nowrap;
}
.gallery-trigger {
align-self: flex-end;
}