feat(TrainingGroup): add excludeFromQuickDiaryCreate feature and update related logic
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Introduced a new boolean field `excludeFromQuickDiaryCreate` in the TrainingGroup model to control group visibility in quick diary creation.
- Updated the `updateTrainingGroup` service method to handle the new field, allowing for dynamic updates based on user input.
- Enhanced the TrainingTimesTab component to include a checkbox for excluding groups from quick diary creation, improving user interaction.
- Updated localization files to include new strings related to the exclude feature, ensuring clarity in multiple languages.
- Refactored logic in DiaryView and mobile app to consider the new exclusion criteria when suggesting training slots.
This commit is contained in:
Torsten Schulz (local)
2026-05-14 22:47:02 +02:00
parent 83294406a4
commit bf3e1af084
12 changed files with 239 additions and 63 deletions

View File

@@ -10,12 +10,22 @@
>
<div class="group-header">
<h3>{{ group.name }}</h3>
<button
@click="showAddTimeForm(group.id)"
class="btn-primary btn-small"
>
{{ $t('trainingTimesTab.addTime') }}
</button>
<div class="group-header-actions">
<label class="exclude-quick-diary-label">
<input
type="checkbox"
:checked="Boolean(group.excludeFromQuickDiaryCreate)"
@change="onExcludeQuickChange(group, $event)"
/>
<span>{{ $t('trainingTimesTab.excludeFromQuickDiaryCreate') }}</span>
</label>
<button
@click="showAddTimeForm(group.id)"
class="btn-primary btn-small"
>
{{ $t('trainingTimesTab.addTime') }}
</button>
</div>
</div>
<!-- Add Time Form -->
@@ -165,7 +175,12 @@ export default {
this.loading = true;
try {
const response = await apiClient.get(`/training-times/${this.currentClub}`);
this.groups = Array.isArray(response.data) ? response.data : [];
this.groups = Array.isArray(response.data)
? response.data.map((g) => ({
...g,
excludeFromQuickDiaryCreate: Boolean(g.excludeFromQuickDiaryCreate),
}))
: [];
} catch (error) {
console.error('[loadTrainingTimes] Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
@@ -176,6 +191,23 @@ export default {
}
},
async onExcludeQuickChange(group, event) {
const exclude = Boolean(event.target.checked);
try {
await apiClient.put(`/training-groups/${this.currentClub}/${group.id}`, {
name: group.name,
sortOrder: group.sortOrder,
excludeFromQuickDiaryCreate: exclude,
});
group.excludeFromQuickDiaryCreate = exclude;
} catch (error) {
console.error('[onExcludeQuickChange]', error);
event.target.checked = !exclude;
const msg = getSafeErrorMessage(error, this.$t('trainingTimesTab.excludeFromQuickDiarySaveError'));
alert(msg);
}
},
showAddTimeForm(groupId) {
this.addTimeFormGroupId = groupId;
this.newTime = {
@@ -267,7 +299,8 @@ export default {
getWeekdayName(weekday) {
const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return days[weekday] || '';
const i = Number(weekday);
return days[i] || '';
},
formatTime(time) {
@@ -311,11 +344,37 @@ export default {
.group-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
}
.group-header-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.exclude-quick-diary-label {
display: inline-flex;
align-items: flex-start;
gap: 8px;
font-size: 0.85rem;
color: #444;
cursor: pointer;
margin: 0;
max-width: 22rem;
line-height: 1.35;
}
.exclude-quick-diary-label input {
margin-top: 0.2rem;
flex-shrink: 0;
}
.group-header h3 {
margin: 0;
font-size: 1.2rem;

View File

@@ -2402,7 +2402,9 @@
"noTimes": "Keine Trainingszeiten definiert",
"editTime": "Trainingszeit bearbeiten",
"save": "Speichern",
"saveError": "Fehler beim Speichern der Trainingszeit"
"saveError": "Fehler beim Speichern der Trainingszeit",
"excludeFromQuickDiaryCreate": "Vom Schnellanlegen (Tagebuch) ausnehmen",
"excludeFromQuickDiarySaveError": "Die Einstellung konnte nicht gespeichert werden."
},
"trainingGroupsTab": {
"groups": "Gruppen",

View File

@@ -2464,7 +2464,9 @@
"noTimes": "Keine Trainingszeiten definiert",
"editTime": "Trainingszeit bearbeiten",
"save": "Speichern",
"saveError": "Fehler beim Speichern der Trainingszeit"
"saveError": "Fehler beim Speichern der Trainingszeit",
"excludeFromQuickDiaryCreate": "Vom Schnellanlegen (Tagebuch) ausnehmen",
"excludeFromQuickDiarySaveError": "Die Einstellung konnte nicht gespeichert werden."
},
"trainingGroupsTab": {
"groups": "Gruppen",

View File

@@ -1640,14 +1640,27 @@ export default {
return `${y}-${m}-${day}`;
},
/** Wochentag aus API (0=So … 6=Sa) zuverlässig als Zahl vermeidet Strict-===-Fehler bei String-Werten. */
trainingWeekdayNumber(tt) {
if (tt == null || tt.weekday === undefined || tt.weekday === null) return NaN;
const w = Number(tt.weekday);
return Number.isFinite(w) ? w : parseInt(String(tt.weekday), 10);
},
trainingStartToMinutes(start) {
const s = String(start || '').trim();
const base = s.length >= 5 ? s.substring(0, 5) : s;
const parts = base.split(':');
if (parts.length < 2) return null;
const h = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
return h * 60 + m;
},
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 eligible = groups.filter((g) => !g.excludeFromQuickDiaryCreate);
const existing = new Set((this.dates || []).map((d) => this.diaryDateKey(d)));
const today = new Date();
for (let dayOffset = 0; dayOffset <= 366; dayOffset++) {
@@ -1655,19 +1668,37 @@ export default {
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),
};
const candidates = [];
for (const g of eligible) {
for (const tt of g.trainingTimes || []) {
if (this.trainingWeekdayNumber(tt) !== checkWeekday) continue;
if (!tt.startTime || !String(tt.startTime).trim()) continue;
const startMin = this.trainingStartToMinutes(tt.startTime);
if (startMin == null) continue;
candidates.push({
startMin,
dateStr,
sortOrder: Number.isFinite(Number(g.sortOrder)) ? Number(g.sortOrder) : 1e9,
groupId: Number(g.id) || 0,
timeId: Number(tt.id) || 0,
startTime: this.formatTimeForInput(tt.startTime),
endTime: this.formatTimeForInput(tt.endTime),
});
}
}
if (!candidates.length) continue;
candidates.sort((a, b) => {
if (a.startMin !== b.startMin) return a.startMin - b.startMin;
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
if (a.groupId !== b.groupId) return a.groupId - b.groupId;
return a.timeId - b.timeId;
});
const best = candidates[0];
return {
date: best.dateStr,
startTime: best.startTime,
endTime: best.endTime,
};
}
return null;
},
@@ -1848,7 +1879,7 @@ export default {
const checkWeekday = checkDate.getDay();
// Finde Trainingszeiten für diesen Wochentag
const timesForWeekday = sortedTimes.filter(tt => tt.weekday === checkWeekday);
const timesForWeekday = sortedTimes.filter(tt => this.trainingWeekdayNumber(tt) === checkWeekday);
if (timesForWeekday.length > 0) {
// Nimm die erste Trainingszeit für diesen Tag