feat(Diary): implement quick create functionality for training days and enhance localization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -793,6 +793,9 @@
|
||||
"noEntries": "Aucune entrée",
|
||||
"deleteDate": "Supprimer la date",
|
||||
"createNew": "Créer",
|
||||
"quickCreate": "Création rapide",
|
||||
"quickCreateNoSlot": "Aucun créneau d’entraînement libre trouvé (selon les horaires des groupes, sur un an).",
|
||||
"quickCreateFailed": "Impossible de créer la journée d’entraînement.",
|
||||
"gallery": "Galerie des membres",
|
||||
"galleryCreating": "Création de la galerie…",
|
||||
"selectTrainingGroup": "Sélectionner un groupe d'entraînement",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -793,6 +793,9 @@
|
||||
"noEntries": "エントリがありません",
|
||||
"deleteDate": "日付を削除",
|
||||
"createNew": "新規作成",
|
||||
"quickCreate": "クイック作成",
|
||||
"quickCreateNoSlot": "空きの練習日が見つかりません(グループのスケジュールに基づき、1年先まで)。",
|
||||
"quickCreateFailed": "練習日を作成できませんでした。",
|
||||
"gallery": "メンバーギャラリー",
|
||||
"galleryCreating": "ギャラリーを作成中…",
|
||||
"selectTrainingGroup": "練習グループを選択",
|
||||
|
||||
@@ -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ą",
|
||||
|
||||
@@ -793,6 +793,9 @@
|
||||
"noEntries": "ไม่มีรายการ",
|
||||
"deleteDate": "ลบวันที่",
|
||||
"createNew": "สร้างใหม่",
|
||||
"quickCreate": "เพิ่มด่วน",
|
||||
"quickCreateNoSlot": "ไม่พบวันฝึกที่ว่าง (ตามตารางกลุ่ม ล่วงหน้าหนึ่งปี)",
|
||||
"quickCreateFailed": "ไม่สามารถสร้างวันฝึกได้",
|
||||
"gallery": "แกลเลอรีสมาชิก",
|
||||
"galleryCreating": "กำลังสร้างแกลเลอรี…",
|
||||
"selectTrainingGroup": "เลือกกลุ่มฝึกซ้อม",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -793,6 +793,9 @@
|
||||
"noEntries": "没有条目",
|
||||
"deleteDate": "删除日期",
|
||||
"createNew": "新建",
|
||||
"quickCreate": "快速创建",
|
||||
"quickCreateNoSlot": "未找到空闲训练日(按小组时间表,向前查找一年)。",
|
||||
"quickCreateFailed": "无法创建训练日。",
|
||||
"gallery": "成员图库",
|
||||
"galleryCreating": "正在生成图库…",
|
||||
"selectTrainingGroup": "选择训练组",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user