feat(TrainingCancellation): enhance cancellation functionality and localization support
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
- Updated the training cancellation controller to accept training group IDs, improving the cancellation process. - Modified the database schema to include a JSON field for training group IDs in the training cancellations table. - Enhanced the TrainingCancellation model to support the new training group IDs field. - Updated the training cancellation service to normalize and handle training group IDs effectively. - Added localization support for training cancellation features across multiple languages, improving user experience.
This commit is contained in:
@@ -19,13 +19,14 @@ export const upsertTrainingCancellation = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const { date, startDate, endDate, reason } = req.body;
|
||||
const { date, startDate, endDate, reason, trainingGroupIds } = req.body;
|
||||
const result = await trainingCancellationService.upsertTrainingCancellation(
|
||||
userToken,
|
||||
clubId,
|
||||
startDate || date,
|
||||
reason,
|
||||
endDate || date || startDate
|
||||
endDate || date || startDate,
|
||||
trainingGroupIds
|
||||
);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE training_cancellations
|
||||
ADD COLUMN IF NOT EXISTS training_group_ids JSON NULL;
|
||||
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS training_cancellations (
|
||||
end_date DATE NOT NULL,
|
||||
date DATE NULL,
|
||||
reason VARCHAR(255) NULL,
|
||||
training_group_ids JSON NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_training_cancellation_club_range (club_id, start_date, end_date),
|
||||
|
||||
@@ -36,6 +36,11 @@ const TrainingCancellation = sequelize.define('TrainingCancellation', {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
trainingGroupIds: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
field: 'training_group_ids',
|
||||
},
|
||||
}, {
|
||||
tableName: 'training_cancellations',
|
||||
underscored: true,
|
||||
|
||||
@@ -24,10 +24,11 @@ class TrainingCancellationService {
|
||||
});
|
||||
}
|
||||
|
||||
async upsertTrainingCancellation(userToken, clubId, date, reason, endDate = null) {
|
||||
async upsertTrainingCancellation(userToken, clubId, date, reason, endDate = null, trainingGroupIds = []) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const normalizedStartDate = this.normalizeDate(date);
|
||||
const normalizedEndDate = this.normalizeDate(endDate || date);
|
||||
const normalizedTrainingGroupIds = this.normalizeTrainingGroupIds(trainingGroupIds);
|
||||
if (!normalizedStartDate || !normalizedEndDate) {
|
||||
throw new HttpError('Ungültiges Datum', 400);
|
||||
}
|
||||
@@ -41,6 +42,7 @@ class TrainingCancellationService {
|
||||
endDate: normalizedEndDate,
|
||||
date: normalizedStartDate,
|
||||
reason: String(reason || '').trim() || null,
|
||||
trainingGroupIds: normalizedTrainingGroupIds,
|
||||
});
|
||||
return cancellation || await TrainingCancellation.findOne({
|
||||
where: { clubId, startDate: normalizedStartDate, endDate: normalizedEndDate },
|
||||
@@ -71,6 +73,30 @@ class TrainingCancellationService {
|
||||
const text = String(date || '').slice(0, 10);
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : null;
|
||||
}
|
||||
|
||||
normalizeTrainingGroupIds(trainingGroupIds) {
|
||||
const values = this.parseTrainingGroupIdsValue(trainingGroupIds);
|
||||
return [...new Set(
|
||||
values
|
||||
.map(id => Number.parseInt(id, 10))
|
||||
.filter(id => Number.isInteger(id) && id > 0)
|
||||
)];
|
||||
}
|
||||
|
||||
parseTrainingGroupIdsValue(trainingGroupIds) {
|
||||
if (Array.isArray(trainingGroupIds)) return trainingGroupIds;
|
||||
if (trainingGroupIds === null || trainingGroupIds === undefined || trainingGroupIds === '') return [];
|
||||
if (typeof trainingGroupIds === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(trainingGroupIds);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
if (parsed !== null && parsed !== undefined) return [parsed];
|
||||
} catch (error) {
|
||||
return trainingGroupIds.split(',');
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default new TrainingCancellationService();
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"saving": "Speichere..."
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Vereinskalender",
|
||||
"title": "Kalender",
|
||||
"description": "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.",
|
||||
"today": "Heute",
|
||||
"loading": "Kalenderdaten werden geladen...",
|
||||
"noClub": "Bitte zuerst einen Verein auswählen.",
|
||||
"loadError": "Kalenderdaten konnten nicht geladen werden.",
|
||||
"sourceWarning": "{source} konnte nicht geladen werden",
|
||||
"agendaTitle": "Termine im Monat",
|
||||
"agendaEmpty": "Keine Termine in diesem Monat.",
|
||||
"recurringTrainingTime": "Regelmäßige Trainingszeit",
|
||||
"participants": "{count} Teilnehmer",
|
||||
"starts": "{count} Starts",
|
||||
"options": {
|
||||
"title": "Optionen",
|
||||
"subtitle": "Ausfälle & eigene Termine"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Teilnahme",
|
||||
"match": "Punktspiel",
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Ferien",
|
||||
"trainingCancellation": "Ausfall",
|
||||
"customEvent": "Termin"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training fällt aus",
|
||||
"description": "Blendet regelmäßige Trainingszeiten aus.",
|
||||
"date": "Datum",
|
||||
"untilOptional": "Bis optional",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"reasonPlaceholder": "Grund (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Eintragen",
|
||||
"fallbackTitle": "Training fällt aus",
|
||||
"subtitle": "Trainingsausfall"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Eigene Termine",
|
||||
"description": "Kreistage, Sitzungen, interne Meetings, ...",
|
||||
"titlePlaceholder": "Titel",
|
||||
"categoryPlaceholder": "Kategorie (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Anlegen",
|
||||
"subtitleFallback": "Eigener Termin"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Trainingsausfall",
|
||||
"message": "„{title}“ als Trainingsausfall markieren?",
|
||||
"useWholeRange": "Für den gesamten Zeitraum ({start} bis {end}) eintragen",
|
||||
"trainingGroups": "Betroffene Trainingsgruppen",
|
||||
"confirm": "Ausfall eintragen",
|
||||
"noGroups": "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Trainingstage",
|
||||
"trainingTimes": "Trainingszeiten",
|
||||
"trainingCancellations": "Trainingsausfälle",
|
||||
"customEvents": "Eigene Termine",
|
||||
"tournaments": "Turniere",
|
||||
"officialTournaments": "Turnierteilnahmen",
|
||||
"matches": "Punktspiele",
|
||||
"holidays": "Ferien/Feiertage"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Turnierteilnahme",
|
||||
"match": "Punktspiel"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Offenes Turnier",
|
||||
"club": "Vereinsturnier"
|
||||
},
|
||||
"match": {
|
||||
"home": "Heim",
|
||||
"guest": "Gast"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Schulferien"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mo",
|
||||
"tuesday": "Di",
|
||||
"wednesday": "Mi",
|
||||
"thursday": "Do",
|
||||
"friday": "Fr",
|
||||
"saturday": "Sa",
|
||||
"sunday": "So"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Trainingsausfall stornieren",
|
||||
"message": "Trainingsausfall „{title}“ am {date} wirklich stornieren?",
|
||||
"confirm": "Ausfall stornieren"
|
||||
}
|
||||
},
|
||||
"unknown": "Unbekannt",
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"period": "Zeitraum",
|
||||
"saving": "Speichere..."
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Vereinskalender",
|
||||
"title": "Kalender",
|
||||
"description": "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.",
|
||||
"today": "Heute",
|
||||
"loading": "Kalenderdaten werden geladen...",
|
||||
"noClub": "Bitte zuerst einen Verein auswählen.",
|
||||
"loadError": "Kalenderdaten konnten nicht geladen werden.",
|
||||
"sourceWarning": "{source} konnte nicht geladen werden",
|
||||
"agendaTitle": "Termine im Monat",
|
||||
"agendaEmpty": "Keine Termine in diesem Monat.",
|
||||
"recurringTrainingTime": "Regelmäßige Trainingszeit",
|
||||
"participants": "{count} Teilnehmer",
|
||||
"starts": "{count} Starts",
|
||||
"options": {
|
||||
"title": "Optionen",
|
||||
"subtitle": "Ausfälle & eigene Termine"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Teilnahme",
|
||||
"match": "Punktspiel",
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Ferien",
|
||||
"trainingCancellation": "Ausfall",
|
||||
"customEvent": "Termin"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training fällt aus",
|
||||
"description": "Blendet regelmäßige Trainingszeiten aus.",
|
||||
"date": "Datum",
|
||||
"untilOptional": "Bis optional",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"reasonPlaceholder": "Grund (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Eintragen",
|
||||
"fallbackTitle": "Training fällt aus",
|
||||
"subtitle": "Trainingsausfall"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Eigene Termine",
|
||||
"description": "Kreistage, Sitzungen, interne Meetings, ...",
|
||||
"titlePlaceholder": "Titel",
|
||||
"categoryPlaceholder": "Kategorie (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Anlegen",
|
||||
"subtitleFallback": "Eigener Termin"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Trainingsausfall",
|
||||
"message": "„{title}“ als Trainingsausfall markieren?",
|
||||
"useWholeRange": "Für den gesamten Zeitraum ({start} bis {end}) eintragen",
|
||||
"trainingGroups": "Betroffene Trainingsgruppen",
|
||||
"confirm": "Ausfall eintragen",
|
||||
"noGroups": "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Trainingstage",
|
||||
"trainingTimes": "Trainingszeiten",
|
||||
"trainingCancellations": "Trainingsausfälle",
|
||||
"customEvents": "Eigene Termine",
|
||||
"tournaments": "Turniere",
|
||||
"officialTournaments": "Turnierteilnahmen",
|
||||
"matches": "Punktspiele",
|
||||
"holidays": "Ferien/Feiertage"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Turnierteilnahme",
|
||||
"match": "Punktspiel"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Offenes Turnier",
|
||||
"club": "Vereinsturnier"
|
||||
},
|
||||
"match": {
|
||||
"home": "Heim",
|
||||
"guest": "Gast"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Schulferien"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mo",
|
||||
"tuesday": "Di",
|
||||
"wednesday": "Mi",
|
||||
"thursday": "Do",
|
||||
"friday": "Fr",
|
||||
"saturday": "Sa",
|
||||
"sunday": "So"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Trainingsausfall stornieren",
|
||||
"message": "Trainingsausfall „{title}“ am {date} wirklich stornieren?",
|
||||
"confirm": "Ausfall stornieren"
|
||||
}
|
||||
},
|
||||
"unknown": "Unbekannt",
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"period": "Zeitraum",
|
||||
"saving": "Speichere..."
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Vereinskalender",
|
||||
"title": "Kalender",
|
||||
"description": "Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.",
|
||||
"today": "Heute",
|
||||
"loading": "Kalenderdaten werden geladen...",
|
||||
"noClub": "Bitte zuerst einen Verein auswählen.",
|
||||
"loadError": "Kalenderdaten konnten nicht geladen werden.",
|
||||
"sourceWarning": "{source} konnte nicht geladen werden",
|
||||
"agendaTitle": "Termine im Monat",
|
||||
"agendaEmpty": "Keine Termine in diesem Monat.",
|
||||
"recurringTrainingTime": "Regelmäßige Trainingszeit",
|
||||
"participants": "{count} Teilnehmer",
|
||||
"starts": "{count} Starts",
|
||||
"options": {
|
||||
"title": "Optionen",
|
||||
"subtitle": "Ausfälle & eigene Termine"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Teilnahme",
|
||||
"match": "Punktspiel",
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Ferien",
|
||||
"trainingCancellation": "Ausfall",
|
||||
"customEvent": "Termin"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training fällt aus",
|
||||
"description": "Blendet regelmäßige Trainingszeiten aus.",
|
||||
"date": "Datum",
|
||||
"untilOptional": "Bis optional",
|
||||
"trainingGroups": "Trainingsgruppen",
|
||||
"reasonPlaceholder": "Grund (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Eintragen",
|
||||
"fallbackTitle": "Training fällt aus",
|
||||
"subtitle": "Trainingsausfall"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Eigene Termine",
|
||||
"description": "Kreistage, Sitzungen, interne Meetings, ...",
|
||||
"titlePlaceholder": "Titel",
|
||||
"categoryPlaceholder": "Kategorie (optional)",
|
||||
"saving": "Speichern...",
|
||||
"submit": "Anlegen",
|
||||
"subtitleFallback": "Eigener Termin"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Trainingsausfall",
|
||||
"message": "„{title}“ als Trainingsausfall markieren?",
|
||||
"useWholeRange": "Für den gesamten Zeitraum ({start} bis {end}) eintragen",
|
||||
"trainingGroups": "Betroffene Trainingsgruppen",
|
||||
"confirm": "Ausfall eintragen",
|
||||
"noGroups": "Es sind keine Trainingsgruppen mit Trainingszeiten vorhanden."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Trainingstage",
|
||||
"trainingTimes": "Trainingszeiten",
|
||||
"trainingCancellations": "Trainingsausfälle",
|
||||
"customEvents": "Eigene Termine",
|
||||
"tournaments": "Turniere",
|
||||
"officialTournaments": "Turnierteilnahmen",
|
||||
"matches": "Punktspiele",
|
||||
"holidays": "Ferien/Feiertage"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Turnier",
|
||||
"officialTournament": "Turnierteilnahme",
|
||||
"match": "Punktspiel"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Offenes Turnier",
|
||||
"club": "Vereinsturnier"
|
||||
},
|
||||
"match": {
|
||||
"home": "Heim",
|
||||
"guest": "Gast"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Feiertag",
|
||||
"schoolHoliday": "Schulferien"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mo",
|
||||
"tuesday": "Di",
|
||||
"wednesday": "Mi",
|
||||
"thursday": "Do",
|
||||
"friday": "Fr",
|
||||
"saturday": "Sa",
|
||||
"sunday": "So"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Trainingsausfall stornieren",
|
||||
"message": "Trainingsausfall „{title}“ am {date} wirklich stornieren?",
|
||||
"confirm": "Ausfall stornieren"
|
||||
}
|
||||
},
|
||||
"unknown": "Unbekannt",
|
||||
"navigation": {
|
||||
"home": "Startseite",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"years": "years",
|
||||
"ok": "OK"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Club calendar",
|
||||
"title": "Calendar",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Today",
|
||||
"loading": "Loading calendar data...",
|
||||
"noClub": "Please select a club first.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Events this month",
|
||||
"agendaEmpty": "No events this month.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Cancel training cancellation",
|
||||
"message": "Really cancel the training cancellation “{title}” on {date}?",
|
||||
"confirm": "Cancel cancellation"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"years": "years",
|
||||
"ok": "OK"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Club calendar",
|
||||
"title": "Calendar",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Today",
|
||||
"loading": "Loading calendar data...",
|
||||
"noClub": "Please select a club first.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Events this month",
|
||||
"agendaEmpty": "No events this month.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Cancel training cancellation",
|
||||
"message": "Really cancel the training cancellation “{title}” on {date}?",
|
||||
"confirm": "Cancel cancellation"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"years": "years",
|
||||
"ok": "OK"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Club calendar",
|
||||
"title": "Calendar",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Today",
|
||||
"loading": "Loading calendar data...",
|
||||
"noClub": "Please select a club first.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Events this month",
|
||||
"agendaEmpty": "No events this month.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Cancel training cancellation",
|
||||
"message": "Really cancel the training cancellation “{title}” on {date}?",
|
||||
"confirm": "Cancel cancellation"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Members",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Calendario del club",
|
||||
"title": "Calendario",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Hoy",
|
||||
"loading": "Cargando datos del calendario...",
|
||||
"noClub": "Seleccione primero un club.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Eventos del mes",
|
||||
"agendaEmpty": "No hay eventos este mes.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lun",
|
||||
"tuesday": "Mar",
|
||||
"wednesday": "Mié",
|
||||
"thursday": "Jue",
|
||||
"friday": "Vie",
|
||||
"saturday": "Sáb",
|
||||
"sunday": "Dom"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Cancelar ausencia de entrenamiento",
|
||||
"message": "¿Cancelar realmente la ausencia “{title}” el {date}?",
|
||||
"confirm": "Cancelar ausencia"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Inicio",
|
||||
"members": "Miembros",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Panahon"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Kalendaryo ng club",
|
||||
"title": "Kalendaryo",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Ngayon",
|
||||
"loading": "Nilo-load ang datos ng kalendaryo...",
|
||||
"noClub": "Pumili muna ng club.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Mga event ngayong buwan",
|
||||
"agendaEmpty": "Walang event ngayong buwan.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lun",
|
||||
"tuesday": "Mar",
|
||||
"wednesday": "Miy",
|
||||
"thursday": "Huw",
|
||||
"friday": "Biy",
|
||||
"saturday": "Sab",
|
||||
"sunday": "Lin"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Bawiin ang pagkansela ng training",
|
||||
"message": "Bawiin talaga ang pagkansela ng training “{title}” sa {date}?",
|
||||
"confirm": "Bawiin ang pagkansela"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Mga miyembro",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Période"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Calendrier du club",
|
||||
"title": "Calendrier",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Aujourd’hui",
|
||||
"loading": "Chargement des données du calendrier...",
|
||||
"noClub": "Veuillez d’abord sélectionner un club.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Événements du mois",
|
||||
"agendaEmpty": "Aucun événement ce mois-ci.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lun",
|
||||
"tuesday": "Mar",
|
||||
"wednesday": "Mer",
|
||||
"thursday": "Jeu",
|
||||
"friday": "Ven",
|
||||
"saturday": "Sam",
|
||||
"sunday": "Dim"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Annuler l’annulation de l’entraînement",
|
||||
"message": "Vraiment annuler l’annulation « {title} » le {date} ?",
|
||||
"confirm": "Annuler l’annulation"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Accueil",
|
||||
"members": "Membres",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Calendario del club",
|
||||
"title": "Calendario",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Oggi",
|
||||
"loading": "Caricamento dati calendario...",
|
||||
"noClub": "Seleziona prima un club.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Eventi del mese",
|
||||
"agendaEmpty": "Nessun evento questo mese.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lun",
|
||||
"tuesday": "Mar",
|
||||
"wednesday": "Mer",
|
||||
"thursday": "Gio",
|
||||
"friday": "Ven",
|
||||
"saturday": "Sab",
|
||||
"sunday": "Dom"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Annulla cancellazione allenamento",
|
||||
"message": "Annullare davvero la cancellazione “{title}” del {date}?",
|
||||
"confirm": "Annulla cancellazione"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Membri",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "期間"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "クラブカレンダー",
|
||||
"title": "カレンダー",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "今日",
|
||||
"loading": "カレンダーデータを読み込んでいます...",
|
||||
"noClub": "最初にクラブを選択してください。",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "今月の予定",
|
||||
"agendaEmpty": "今月の予定はありません。",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "月",
|
||||
"tuesday": "火",
|
||||
"wednesday": "水",
|
||||
"thursday": "木",
|
||||
"friday": "金",
|
||||
"saturday": "土",
|
||||
"sunday": "日"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "練習中止を取り消す",
|
||||
"message": "{date} の「{title}」の練習中止を本当に取り消しますか?",
|
||||
"confirm": "中止を取り消す"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "ホーム",
|
||||
"members": "メンバー",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Okres"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Kalendarz klubu",
|
||||
"title": "Kalendarz",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Dzisiaj",
|
||||
"loading": "Ładowanie danych kalendarza...",
|
||||
"noClub": "Najpierw wybierz klub.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Wydarzenia w tym miesiącu",
|
||||
"agendaEmpty": "Brak wydarzeń w tym miesiącu.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Pon",
|
||||
"tuesday": "Wt",
|
||||
"wednesday": "Śr",
|
||||
"thursday": "Czw",
|
||||
"friday": "Pt",
|
||||
"saturday": "Sob",
|
||||
"sunday": "Nd"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Cofnij odwołanie treningu",
|
||||
"message": "Na pewno cofnąć odwołanie „{title}” w dniu {date}?",
|
||||
"confirm": "Cofnij odwołanie"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Strona główna",
|
||||
"members": "Członkowie",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "ตกลง",
|
||||
"period": "ช่วงเวลา"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "ปฏิทินสโมสร",
|
||||
"title": "ปฏิทิน",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "วันนี้",
|
||||
"loading": "กำลังโหลดข้อมูลปฏิทิน...",
|
||||
"noClub": "โปรดเลือกสโมสรก่อน",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "กิจกรรมในเดือนนี้",
|
||||
"agendaEmpty": "ไม่มีกิจกรรมในเดือนนี้",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "จ.",
|
||||
"tuesday": "อ.",
|
||||
"wednesday": "พ.",
|
||||
"thursday": "พฤ.",
|
||||
"friday": "ศ.",
|
||||
"saturday": "ส.",
|
||||
"sunday": "อา."
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "ยกเลิกการงดซ้อม",
|
||||
"message": "ต้องการยกเลิกการงดซ้อม “{title}” วันที่ {date} หรือไม่?",
|
||||
"confirm": "ยกเลิกการงดซ้อม"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "หน้าแรก",
|
||||
"members": "สมาชิก",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "OK",
|
||||
"period": "Panahon"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "Kalendaryo ng club",
|
||||
"title": "Kalendaryo",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "Ngayon",
|
||||
"loading": "Nilo-load ang datos ng kalendaryo...",
|
||||
"noClub": "Pumili muna ng club.",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "Mga event ngayong buwan",
|
||||
"agendaEmpty": "Walang event ngayong buwan.",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Lun",
|
||||
"tuesday": "Mar",
|
||||
"wednesday": "Miy",
|
||||
"thursday": "Huw",
|
||||
"friday": "Biy",
|
||||
"saturday": "Sab",
|
||||
"sunday": "Lin"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "Bawiin ang pagkansela ng training",
|
||||
"message": "Bawiin talaga ang pagkansela ng training “{title}” sa {date}?",
|
||||
"confirm": "Bawiin ang pagkansela"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"members": "Mga miyembro",
|
||||
|
||||
@@ -62,6 +62,106 @@
|
||||
"ok": "确定",
|
||||
"period": "期间"
|
||||
},
|
||||
"calendar": {
|
||||
"eyebrow": "俱乐部日历",
|
||||
"title": "日历",
|
||||
"description": "Training days, club tournaments and league matches in a monthly view.",
|
||||
"today": "今天",
|
||||
"loading": "正在加载日历数据...",
|
||||
"noClub": "请先选择俱乐部。",
|
||||
"loadError": "Calendar data could not be loaded.",
|
||||
"sourceWarning": "{source} could not be loaded",
|
||||
"agendaTitle": "本月活动",
|
||||
"agendaEmpty": "本月没有活动。",
|
||||
"recurringTrainingTime": "Regular training time",
|
||||
"participants": "{count} participants",
|
||||
"starts": "{count} starts",
|
||||
"options": {
|
||||
"title": "Options",
|
||||
"subtitle": "Cancellations & own events"
|
||||
},
|
||||
"legend": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Participation",
|
||||
"match": "League match",
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays",
|
||||
"trainingCancellation": "Cancelled",
|
||||
"customEvent": "Event"
|
||||
},
|
||||
"cancellation": {
|
||||
"title": "Training cancelled",
|
||||
"description": "Hides regular training times.",
|
||||
"date": "Date",
|
||||
"untilOptional": "Until optional",
|
||||
"trainingGroups": "Training groups",
|
||||
"reasonPlaceholder": "Reason (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Add",
|
||||
"fallbackTitle": "Training cancelled",
|
||||
"subtitle": "Training cancellation"
|
||||
},
|
||||
"customEvent": {
|
||||
"title": "Own events",
|
||||
"description": "District meetings, sessions, internal meetings, ...",
|
||||
"titlePlaceholder": "Title",
|
||||
"categoryPlaceholder": "Category (optional)",
|
||||
"saving": "Saving...",
|
||||
"submit": "Create",
|
||||
"subtitleFallback": "Own event"
|
||||
},
|
||||
"quickCancellation": {
|
||||
"title": "Training cancellation",
|
||||
"message": "Mark “{title}” as training cancellation?",
|
||||
"useWholeRange": "Apply to the full period ({start} to {end})",
|
||||
"trainingGroups": "Affected training groups",
|
||||
"confirm": "Add cancellation",
|
||||
"noGroups": "No training groups with training times are available."
|
||||
},
|
||||
"sources": {
|
||||
"trainingDays": "Training days",
|
||||
"trainingTimes": "Training times",
|
||||
"trainingCancellations": "Training cancellations",
|
||||
"customEvents": "Own events",
|
||||
"tournaments": "Tournaments",
|
||||
"officialTournaments": "Tournament participations",
|
||||
"matches": "League matches",
|
||||
"holidays": "Holidays/school holidays"
|
||||
},
|
||||
"eventTitles": {
|
||||
"training": "Training",
|
||||
"tournament": "Tournament",
|
||||
"officialTournament": "Tournament participation",
|
||||
"match": "League match"
|
||||
},
|
||||
"tournament": {
|
||||
"open": "Open tournament",
|
||||
"club": "Club tournament"
|
||||
},
|
||||
"match": {
|
||||
"home": "Home",
|
||||
"guest": "Away"
|
||||
},
|
||||
"holidayTypes": {
|
||||
"holiday": "Holiday",
|
||||
"schoolHoliday": "School holidays"
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "周一",
|
||||
"tuesday": "周二",
|
||||
"wednesday": "周三",
|
||||
"thursday": "周四",
|
||||
"friday": "周五",
|
||||
"saturday": "周六",
|
||||
"sunday": "周日"
|
||||
},
|
||||
"cancelCancellation": {
|
||||
"title": "取消训练停课",
|
||||
"message": "确定要取消 {date} 的“{title}”训练停课吗?",
|
||||
"confirm": "取消停课"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
"members": "成员",
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div class="calendar-page">
|
||||
<header class="calendar-header">
|
||||
<div>
|
||||
<span class="calendar-eyebrow">Vereinskalender</span>
|
||||
<h2>Kalender</h2>
|
||||
<p>Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.</p>
|
||||
<span class="calendar-eyebrow">{{ $t('calendar.eyebrow') }}</span>
|
||||
<h2>{{ $t('calendar.title') }}</h2>
|
||||
<p>{{ $t('calendar.description') }}</p>
|
||||
</div>
|
||||
<div class="calendar-actions">
|
||||
<button type="button" class="calendar-nav-button" @click="goToPreviousMonth">‹</button>
|
||||
<button type="button" class="calendar-today-button" @click="goToToday">Heute</button>
|
||||
<button type="button" class="calendar-today-button" @click="goToToday">{{ $t('calendar.today') }}</button>
|
||||
<button type="button" class="calendar-nav-button" @click="goToNextMonth">›</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -31,27 +31,39 @@
|
||||
|
||||
<details v-if="currentClub" class="calendar-options" :open="optionsOpen">
|
||||
<summary class="calendar-options-summary" @click.prevent="optionsOpen = !optionsOpen">
|
||||
<span>Optionen</span>
|
||||
<small>Ausfälle & eigene Termine</small>
|
||||
<span>{{ $t('calendar.options.title') }}</span>
|
||||
<small>{{ $t('calendar.options.subtitle') }}</small>
|
||||
</summary>
|
||||
<div class="calendar-options-body">
|
||||
<section class="training-cancellation-panel">
|
||||
<div>
|
||||
<h3>Training fällt aus</h3>
|
||||
<p>Blendet regelmäßige Trainingszeiten aus.</p>
|
||||
<h3>{{ $t('calendar.cancellation.title') }}</h3>
|
||||
<p>{{ $t('calendar.cancellation.description') }}</p>
|
||||
</div>
|
||||
<form class="training-cancellation-form" @submit.prevent="saveTrainingCancellation">
|
||||
<label>
|
||||
<span>Datum</span>
|
||||
<span>{{ $t('calendar.cancellation.date') }}</span>
|
||||
<input v-model="cancellationForm.startDate" type="date" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis optional</span>
|
||||
<span>{{ $t('calendar.cancellation.untilOptional') }}</span>
|
||||
<input v-model="cancellationForm.endDate" type="date" />
|
||||
</label>
|
||||
<input v-model="cancellationForm.reason" type="text" placeholder="Grund (optional)" />
|
||||
<label>
|
||||
<span>{{ $t('calendar.cancellation.trainingGroups') }}</span>
|
||||
<select v-model="cancellationForm.trainingGroupIds" multiple>
|
||||
<option
|
||||
v-for="group in trainingGroupsWithTimes"
|
||||
:key="`cancel-group-${group.id}`"
|
||||
:value="String(group.id)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<input v-model="cancellationForm.reason" type="text" :placeholder="$t('calendar.cancellation.reasonPlaceholder')" />
|
||||
<button type="submit" :disabled="cancellationSaving">
|
||||
{{ cancellationSaving ? 'Speichern...' : 'Eintragen' }}
|
||||
{{ cancellationSaving ? $t('calendar.cancellation.saving') : $t('calendar.cancellation.submit') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="visibleTrainingCancellations.length" class="training-cancellation-list">
|
||||
@@ -60,28 +72,28 @@
|
||||
:key="`cancel-${cancellation.cancellationId}`"
|
||||
type="button"
|
||||
class="training-cancellation-item"
|
||||
@click="deleteTrainingCancellation(cancellation)"
|
||||
title="Löschen"
|
||||
@click="openTrainingCancellationDeleteDialog(cancellation)"
|
||||
:title="$t('common.delete')"
|
||||
>
|
||||
<strong>{{ formatShortDate(cancellation.date) }}</strong>
|
||||
<span>{{ cancellation.title }}</span>
|
||||
<small>Löschen</small>
|
||||
<small>{{ $t('common.delete') }}</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="custom-event-panel">
|
||||
<div>
|
||||
<h3>Eigene Termine</h3>
|
||||
<p>Kreistage, Sitzungen, interne Meetings, ...</p>
|
||||
<h3>{{ $t('calendar.customEvent.title') }}</h3>
|
||||
<p>{{ $t('calendar.customEvent.description') }}</p>
|
||||
</div>
|
||||
<form class="custom-event-form" @submit.prevent="saveCustomEvent">
|
||||
<input v-model="customEventForm.title" type="text" placeholder="Titel" required />
|
||||
<input v-model="customEventForm.title" type="text" :placeholder="$t('calendar.customEvent.titlePlaceholder')" required />
|
||||
<input v-model="customEventForm.startDate" type="date" required />
|
||||
<input v-model="customEventForm.endDate" type="date" />
|
||||
<input v-model="customEventForm.category" type="text" placeholder="Kategorie (optional)" />
|
||||
<input v-model="customEventForm.category" type="text" :placeholder="$t('calendar.customEvent.categoryPlaceholder')" />
|
||||
<button type="submit" :disabled="customEventSaving">
|
||||
{{ customEventSaving ? 'Speichern...' : 'Anlegen' }}
|
||||
{{ customEventSaving ? $t('calendar.customEvent.saving') : $t('calendar.customEvent.submit') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="visibleCustomEvents.length" class="custom-event-list">
|
||||
@@ -91,11 +103,11 @@
|
||||
type="button"
|
||||
class="custom-event-item"
|
||||
@click="deleteCustomEvent(event)"
|
||||
title="Löschen"
|
||||
:title="$t('common.delete')"
|
||||
>
|
||||
<strong>{{ formatEventDate(event) }}</strong>
|
||||
<span>{{ event.title }}</span>
|
||||
<small>Löschen</small>
|
||||
<small>{{ $t('common.delete') }}</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -105,12 +117,12 @@
|
||||
<section v-if="sourceWarnings.length" class="calendar-state calendar-state-warning">
|
||||
{{ sourceWarnings.join(' · ') }}
|
||||
</section>
|
||||
<section v-if="loading" class="calendar-state">Kalenderdaten werden geladen...</section>
|
||||
<section v-if="loading" class="calendar-state">{{ $t('calendar.loading') }}</section>
|
||||
<section v-else-if="error" class="calendar-state calendar-state-error">{{ error }}</section>
|
||||
|
||||
<section v-if="!currentClub" class="calendar-state">Bitte zuerst einen Verein auswählen.</section>
|
||||
<section v-if="!currentClub" class="calendar-state">{{ $t('calendar.noClub') }}</section>
|
||||
|
||||
<section v-else class="calendar-grid" aria-label="Kalender">
|
||||
<section v-else class="calendar-grid" :aria-label="$t('calendar.title')">
|
||||
<div v-for="day in weekdays" :key="day" class="calendar-weekday">{{ day }}</div>
|
||||
<article
|
||||
v-for="day in calendarDays"
|
||||
@@ -137,8 +149,8 @@
|
||||
</section>
|
||||
|
||||
<section class="calendar-agenda">
|
||||
<h3>Termine im Monat</h3>
|
||||
<p v-if="visibleEvents.length === 0" class="agenda-empty">Keine Termine in diesem Monat.</p>
|
||||
<h3>{{ $t('calendar.agendaTitle') }}</h3>
|
||||
<p v-if="visibleEvents.length === 0" class="agenda-empty">{{ $t('calendar.agendaEmpty') }}</p>
|
||||
<button
|
||||
v-for="event in visibleEvents"
|
||||
:key="`agenda-${event.id}`"
|
||||
@@ -156,32 +168,98 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
:confirm-text="confirmDialog.confirmText"
|
||||
:cancel-text="confirmDialog.cancelText"
|
||||
:show-cancel="confirmDialog.showCancel"
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
<BaseDialog
|
||||
v-model="quickCancellationDialog.isOpen"
|
||||
:title="$t('calendar.quickCancellation.title')"
|
||||
size="small"
|
||||
:close-on-overlay="false"
|
||||
@close="closeQuickCancellationDialog"
|
||||
>
|
||||
<div class="quick-cancellation-dialog">
|
||||
<p>
|
||||
{{ $t('calendar.quickCancellation.message', { title: quickCancellationDialog.eventTitle }) }}
|
||||
</p>
|
||||
<label v-if="quickCancellationDialog.isRange" class="quick-cancellation-checkbox">
|
||||
<input v-model="quickCancellationDialog.useRange" type="checkbox" />
|
||||
<span>
|
||||
{{ $t('calendar.quickCancellation.useWholeRange', {
|
||||
start: quickCancellationDialog.startDate,
|
||||
end: quickCancellationDialog.endDate
|
||||
}) }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="quick-cancellation-groups">
|
||||
<span>{{ $t('calendar.quickCancellation.trainingGroups') }}</span>
|
||||
<select v-model="quickCancellationDialog.trainingGroupIds" multiple>
|
||||
<option
|
||||
v-for="group in trainingGroupsWithTimes"
|
||||
:key="`quick-cancel-group-${group.id}`"
|
||||
:value="String(group.id)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<p v-if="!trainingGroupsWithTimes.length" class="quick-cancellation-hint">
|
||||
{{ $t('calendar.quickCancellation.noGroups') }}
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button type="button" class="dialog-secondary-button" @click="closeQuickCancellationDialog">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dialog-primary-button"
|
||||
:disabled="quickCancellationDialog.trainingGroupIds.length === 0"
|
||||
@click="confirmQuickCancellation"
|
||||
>
|
||||
{{ $t('calendar.quickCancellation.confirm') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="cancellationDeleteDialog.isOpen"
|
||||
:title="$t('calendar.cancelCancellation.title')"
|
||||
size="small"
|
||||
:close-on-overlay="false"
|
||||
@close="closeTrainingCancellationDeleteDialog"
|
||||
>
|
||||
<div class="quick-cancellation-dialog">
|
||||
<p>
|
||||
{{ $t('calendar.cancelCancellation.message', {
|
||||
title: cancellationDeleteDialog.title,
|
||||
date: cancellationDeleteDialog.dateLabel
|
||||
}) }}
|
||||
</p>
|
||||
<p v-if="cancellationDeleteDialog.subtitle" class="quick-cancellation-hint">
|
||||
{{ cancellationDeleteDialog.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button type="button" class="dialog-secondary-button" @click="closeTrainingCancellationDeleteDialog">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
<button type="button" class="dialog-danger-button" @click="confirmTrainingCancellationDelete">
|
||||
{{ $t('calendar.cancelCancellation.confirm') }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
|
||||
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
const WEEKDAY_KEYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
|
||||
export default {
|
||||
name: 'CalendarView',
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
BaseDialog,
|
||||
},
|
||||
data() {
|
||||
const today = new Date();
|
||||
@@ -205,7 +283,8 @@ export default {
|
||||
cancellationForm: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
reason: ''
|
||||
reason: '',
|
||||
trainingGroupIds: []
|
||||
},
|
||||
customEventSaving: false,
|
||||
customEventForm: {
|
||||
@@ -215,39 +294,47 @@ export default {
|
||||
category: ''
|
||||
},
|
||||
optionsOpen: false,
|
||||
confirmDialog: {
|
||||
quickCancellationDialog: {
|
||||
isOpen: false,
|
||||
eventTitle: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
isRange: false,
|
||||
useRange: false,
|
||||
reason: '',
|
||||
trainingGroupIds: [],
|
||||
},
|
||||
cancellationDeleteDialog: {
|
||||
isOpen: false,
|
||||
cancellation: null,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
confirmText: '',
|
||||
cancelText: '',
|
||||
showCancel: true,
|
||||
resolveCallback: null,
|
||||
subtitle: '',
|
||||
dateLabel: '',
|
||||
},
|
||||
plannedTrainingByDateKey: {},
|
||||
plannedTrainingGroupIdsByDateKey: {},
|
||||
trainingGroups: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentClub', 'currentClubName']),
|
||||
weekdays() {
|
||||
return WEEKDAYS;
|
||||
return WEEKDAY_KEYS.map(key => this.$t(`calendar.weekdays.${key}`));
|
||||
},
|
||||
eventTypes() {
|
||||
return [
|
||||
{ key: 'training', label: 'Training' },
|
||||
{ key: 'tournament', label: 'Turnier' },
|
||||
{ key: 'officialTournament', label: 'Teilnahme' },
|
||||
{ key: 'match', label: 'Punktspiel' },
|
||||
{ key: 'holiday', label: 'Feiertag' },
|
||||
{ key: 'schoolHoliday', label: 'Ferien' },
|
||||
{ key: 'trainingCancellation', label: 'Ausfall' },
|
||||
{ key: 'customEvent', label: 'Termin' }
|
||||
{ key: 'training', label: this.$t('calendar.legend.training') },
|
||||
{ key: 'tournament', label: this.$t('calendar.legend.tournament') },
|
||||
{ key: 'officialTournament', label: this.$t('calendar.legend.officialTournament') },
|
||||
{ key: 'match', label: this.$t('calendar.legend.match') },
|
||||
{ key: 'holiday', label: this.$t('calendar.legend.holiday') },
|
||||
{ key: 'schoolHoliday', label: this.$t('calendar.legend.schoolHoliday') },
|
||||
{ key: 'trainingCancellation', label: this.$t('calendar.legend.trainingCancellation') },
|
||||
{ key: 'customEvent', label: this.$t('calendar.legend.customEvent') }
|
||||
];
|
||||
},
|
||||
monthLabel() {
|
||||
return this.cursor.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||
return this.cursor.toLocaleDateString(this.$i18n?.locale || 'de-DE', { month: 'long', year: 'numeric' });
|
||||
},
|
||||
displayedYear() {
|
||||
return this.cursor.getFullYear();
|
||||
@@ -288,7 +375,10 @@ export default {
|
||||
.sort((a, b) => a.startsAt - b.startsAt);
|
||||
},
|
||||
sourceWarnings() {
|
||||
return this.sourceErrors.map(source => `${source} konnte nicht geladen werden`);
|
||||
return this.sourceErrors.map(source => this.$t('calendar.sourceWarning', { source }));
|
||||
},
|
||||
trainingGroupsWithTimes() {
|
||||
return this.trainingGroups.filter(group => Array.isArray(group.trainingTimes) && group.trainingTimes.length > 0);
|
||||
},
|
||||
calendarDays() {
|
||||
const year = this.cursor.getFullYear();
|
||||
@@ -328,28 +418,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async showConfirm(title, message, details = '', type = 'info', options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
confirmText: options.confirmText || '',
|
||||
cancelText: options.cancelText || '',
|
||||
showCancel: options.showCancel !== undefined ? !!options.showCancel : true,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
handleConfirmResult(confirmed) {
|
||||
if (this.confirmDialog.resolveCallback) {
|
||||
this.confirmDialog.resolveCallback(confirmed);
|
||||
}
|
||||
this.confirmDialog.isOpen = false;
|
||||
this.confirmDialog.resolveCallback = null;
|
||||
},
|
||||
isOurTeam(teamName) {
|
||||
if (!teamName || !this.currentClubName) return false;
|
||||
return String(teamName).startsWith(this.currentClubName);
|
||||
@@ -365,35 +433,40 @@ export default {
|
||||
this.sourceErrors = [];
|
||||
|
||||
const sources = await Promise.allSettled([
|
||||
this.loadSource('Trainingstage', () => this.loadTrainingEvents()),
|
||||
this.loadSource('Trainingszeiten', () => this.loadRecurringTrainingEvents()),
|
||||
this.loadSource('Trainingsausfälle', () => this.loadTrainingCancellationEvents()),
|
||||
this.loadSource('Eigene Termine', () => this.loadCustomEvents()),
|
||||
this.loadSource('Turniere', () => this.loadTournamentEvents()),
|
||||
this.loadSource('Turnierteilnahmen', () => this.loadOfficialTournamentEvents()),
|
||||
this.loadSource('Punktspiele', () => this.loadMatchEvents()),
|
||||
this.loadSource('Ferien/Feiertage', () => this.loadHolidayEvents())
|
||||
this.loadSource(this.$t('calendar.sources.trainingDays'), () => this.loadTrainingEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.trainingTimes'), () => this.loadRecurringTrainingEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.trainingCancellations'), () => this.loadTrainingCancellationEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.customEvents'), () => this.loadCustomEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.tournaments'), () => this.loadTournamentEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.officialTournaments'), () => this.loadOfficialTournamentEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.matches'), () => this.loadMatchEvents()),
|
||||
this.loadSource(this.$t('calendar.sources.holidays'), () => this.loadHolidayEvents())
|
||||
]);
|
||||
|
||||
const loadedEvents = sources
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.flatMap(result => result.value.events)
|
||||
.filter(event => event.date && !Number.isNaN(event.date.getTime()));
|
||||
const calendarEvents = this.enrichTrainingCancellationSubtitles(loadedEvents);
|
||||
|
||||
this.plannedTrainingByDateKey = loadedEvents
|
||||
this.plannedTrainingByDateKey = calendarEvents
|
||||
.filter(event => event.type === 'training')
|
||||
.reduce((map, event) => {
|
||||
map[this.toDateKey(event.date)] = true;
|
||||
return map;
|
||||
}, {});
|
||||
this.plannedTrainingGroupIdsByDateKey = calendarEvents
|
||||
.filter(event => event.type === 'training' && event.trainingGroupId)
|
||||
.reduce((map, event) => {
|
||||
const dateKey = this.toDateKey(event.date);
|
||||
if (!map[dateKey]) map[dateKey] = {};
|
||||
map[dateKey][String(event.trainingGroupId)] = true;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const cancellationDates = new Set(
|
||||
loadedEvents
|
||||
.filter(event => event.type === 'trainingCancellation')
|
||||
.flatMap(event => this.getDateKeysForRange(event.date, event.endDate || event.date))
|
||||
);
|
||||
const afterCancellations = loadedEvents.filter(event => (
|
||||
!event.isRecurringTraining || !cancellationDates.has(this.toDateKey(event.date))
|
||||
const cancellationEvents = calendarEvents.filter(event => event.type === 'trainingCancellation');
|
||||
const afterCancellations = calendarEvents.filter(event => (
|
||||
!event.isRecurringTraining || !this.isRecurringTrainingCancelled(event, cancellationEvents)
|
||||
));
|
||||
this.events = this.mergeRecurringTrainingSlots(afterCancellations);
|
||||
this.sourceErrors = sources
|
||||
@@ -402,7 +475,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
|
||||
if (sources.every(result => result.status === 'rejected')) {
|
||||
this.error = 'Kalenderdaten konnten nicht geladen werden.';
|
||||
this.error = this.$t('calendar.loadError');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
@@ -452,14 +525,15 @@ export default {
|
||||
const titleJoined = specific.length ? specific.join(' · ') : (rawTitles.join(' · ') || base.title);
|
||||
const safeIdKey = slotKey.replace(/\|/g, '-');
|
||||
const hasRecurring = sorted.some((x) => x.isRecurringTraining);
|
||||
const recurringSubtitle = this.$t('calendar.recurringTrainingTime');
|
||||
const otherSubtitles = [...new Set(
|
||||
sorted.map((x) => String(x.subtitle || '').trim()).filter((s) => s && s !== 'Regelmäßige Trainingszeit')
|
||||
sorted.map((x) => String(x.subtitle || '').trim()).filter((s) => s && s !== recurringSubtitle)
|
||||
)];
|
||||
let subtitleJoined = '';
|
||||
if (hasRecurring) {
|
||||
subtitleJoined = otherSubtitles.length
|
||||
? `Regelmäßige Trainingszeit · ${otherSubtitles.join(' · ')}`
|
||||
: 'Regelmäßige Trainingszeit';
|
||||
? `${recurringSubtitle} · ${otherSubtitles.join(' · ')}`
|
||||
: recurringSubtitle;
|
||||
} else {
|
||||
subtitleJoined = otherSubtitles.join(' · ') || String(base.subtitle || '').trim() || '';
|
||||
}
|
||||
@@ -483,9 +557,16 @@ export default {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enrichTrainingCancellationSubtitles(events) {
|
||||
return events.map(event => (
|
||||
event.type === 'trainingCancellation'
|
||||
? { ...event, subtitle: this.formatCancellationSubtitle(event.trainingGroupIds) }
|
||||
: event
|
||||
));
|
||||
},
|
||||
async loadTrainingEvents() {
|
||||
const response = await apiClient.get(`/diary/${this.currentClub}`);
|
||||
this.ensureSuccess(response, 'Trainingstage');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingDays'));
|
||||
return (response.data || []).map(entry => {
|
||||
const date = this.parseDate(entry.date);
|
||||
return {
|
||||
@@ -494,7 +575,7 @@ export default {
|
||||
date,
|
||||
startsAt: this.combineDateTime(date, entry.trainingStart),
|
||||
time: this.formatTimeRange(entry.trainingStart, entry.trainingEnd),
|
||||
title: 'Training',
|
||||
title: this.$t('calendar.eventTitles.training'),
|
||||
subtitle: entry.diaryTags?.map(tag => tag.name).join(', ') || '',
|
||||
route: '/diary'
|
||||
};
|
||||
@@ -502,8 +583,9 @@ export default {
|
||||
},
|
||||
async loadRecurringTrainingEvents() {
|
||||
const response = await apiClient.get(`/training-times/${this.currentClub}`);
|
||||
this.ensureSuccess(response, 'Trainingszeiten');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingTimes'));
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.trainingGroups = groups.filter(group => Array.isArray(group.trainingTimes) && group.trainingTimes.length > 0);
|
||||
return groups.flatMap(group => this.mapGroupTrainingTimesToEvents(group));
|
||||
},
|
||||
mapGroupTrainingTimesToEvents(group) {
|
||||
@@ -530,10 +612,11 @@ export default {
|
||||
date: eventDate,
|
||||
startsAt: this.combineDateTime(eventDate, time.startTime),
|
||||
time: this.formatTimeRange(time.startTime, time.endTime),
|
||||
title: group?.name || 'Training',
|
||||
subtitle: 'Regelmäßige Trainingszeit',
|
||||
title: group?.name || this.$t('calendar.eventTitles.training'),
|
||||
subtitle: this.$t('calendar.recurringTrainingTime'),
|
||||
route: '/diary',
|
||||
isRecurringTraining: true
|
||||
isRecurringTraining: true,
|
||||
trainingGroupId: group?.id
|
||||
});
|
||||
date.setDate(date.getDate() + 7);
|
||||
}
|
||||
@@ -544,10 +627,11 @@ export default {
|
||||
const response = await apiClient.get(`/training-cancellations/${this.currentClub}`, {
|
||||
params: { year: this.displayedYear }
|
||||
});
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingCancellations'));
|
||||
return (response.data || []).map(cancellation => {
|
||||
const date = this.parseDate(cancellation.startDate || cancellation.date);
|
||||
const endDate = this.parseDate(cancellation.endDate || cancellation.startDate || cancellation.date);
|
||||
const trainingGroupIds = this.normalizeTrainingGroupIds(cancellation.trainingGroupIds);
|
||||
return {
|
||||
id: `training-cancellation-${cancellation.id}`,
|
||||
cancellationId: cancellation.id,
|
||||
@@ -556,8 +640,9 @@ export default {
|
||||
endDate,
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: cancellation.reason || 'Training fällt aus',
|
||||
subtitle: 'Trainingsausfall'
|
||||
title: cancellation.reason || this.$t('calendar.cancellation.fallbackTitle'),
|
||||
subtitle: this.formatCancellationSubtitle(trainingGroupIds),
|
||||
trainingGroupIds
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -568,10 +653,11 @@ export default {
|
||||
const response = await apiClient.post(`/training-cancellations/${this.currentClub}`, {
|
||||
startDate: this.cancellationForm.startDate,
|
||||
endDate: this.cancellationForm.endDate || this.cancellationForm.startDate,
|
||||
reason: this.cancellationForm.reason
|
||||
reason: this.cancellationForm.reason,
|
||||
trainingGroupIds: this.normalizeTrainingGroupIds(this.cancellationForm.trainingGroupIds)
|
||||
});
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
this.cancellationForm = { startDate: '', endDate: '', reason: '' };
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingCancellations'));
|
||||
this.cancellationForm = { startDate: '', endDate: '', reason: '', trainingGroupIds: [] };
|
||||
await this.loadCalendarEvents();
|
||||
} finally {
|
||||
this.cancellationSaving = false;
|
||||
@@ -590,14 +676,39 @@ export default {
|
||||
async deleteTrainingCancellation(cancellation) {
|
||||
if (!this.currentClub || !cancellation?.cancellationId) return;
|
||||
const response = await apiClient.delete(`/training-cancellations/${this.currentClub}/${cancellation.cancellationId}`);
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingCancellations'));
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
openTrainingCancellationDeleteDialog(cancellation) {
|
||||
if (!cancellation?.cancellationId) return;
|
||||
this.cancellationDeleteDialog = {
|
||||
isOpen: true,
|
||||
cancellation,
|
||||
title: cancellation.title || this.$t('calendar.cancellation.fallbackTitle'),
|
||||
subtitle: cancellation.subtitle || '',
|
||||
dateLabel: this.formatEventDate(cancellation),
|
||||
};
|
||||
},
|
||||
closeTrainingCancellationDeleteDialog() {
|
||||
this.cancellationDeleteDialog = {
|
||||
isOpen: false,
|
||||
cancellation: null,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
dateLabel: '',
|
||||
};
|
||||
},
|
||||
async confirmTrainingCancellationDelete() {
|
||||
const cancellation = this.cancellationDeleteDialog.cancellation;
|
||||
if (!cancellation) return;
|
||||
await this.deleteTrainingCancellation(cancellation);
|
||||
this.closeTrainingCancellationDeleteDialog();
|
||||
},
|
||||
async loadCustomEvents() {
|
||||
const response = await apiClient.get(`/calendar-events/${this.currentClub}`, {
|
||||
params: { year: this.displayedYear },
|
||||
});
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.customEvents'));
|
||||
return (response.data || []).map(event => {
|
||||
const date = this.parseDate(event.startDate);
|
||||
const endDate = this.parseDate(event.endDate || event.startDate);
|
||||
@@ -610,7 +721,7 @@ export default {
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: event.title,
|
||||
subtitle: event.category || 'Eigener Termin',
|
||||
subtitle: event.category || this.$t('calendar.customEvent.subtitleFallback'),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -624,7 +735,7 @@ export default {
|
||||
endDate: this.customEventForm.endDate || this.customEventForm.startDate,
|
||||
category: this.customEventForm.category || null,
|
||||
});
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.customEvents'));
|
||||
this.customEventForm = { title: '', startDate: '', endDate: '', category: '' };
|
||||
await this.loadCalendarEvents();
|
||||
} finally {
|
||||
@@ -634,12 +745,12 @@ export default {
|
||||
async deleteCustomEvent(event) {
|
||||
if (!this.currentClub || !event?.customEventId) return;
|
||||
const response = await apiClient.delete(`/calendar-events/${this.currentClub}/${event.customEventId}`);
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.customEvents'));
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
async loadTournamentEvents() {
|
||||
const response = await apiClient.get(`/tournament/${this.currentClub}`);
|
||||
this.ensureSuccess(response, 'Turniere');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.tournaments'));
|
||||
return (response.data || []).map(tournament => {
|
||||
const date = this.parseDate(tournament.date);
|
||||
return {
|
||||
@@ -648,21 +759,21 @@ export default {
|
||||
date,
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: tournament.name || tournament.tournamentName || 'Turnier',
|
||||
subtitle: tournament.allowsExternal ? 'Offenes Turnier' : 'Vereinsturnier',
|
||||
title: tournament.name || tournament.tournamentName || this.$t('calendar.eventTitles.tournament'),
|
||||
subtitle: tournament.allowsExternal ? this.$t('calendar.tournament.open') : this.$t('calendar.tournament.club'),
|
||||
route: '/tournaments'
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadMatchEvents() {
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
|
||||
this.ensureSuccess(response, 'Punktspiele');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.matches'));
|
||||
return (response.data || [])
|
||||
.filter(match => this.isOurTeam(match.homeTeam?.name) || this.isOurTeam(match.guestTeam?.name))
|
||||
.map(match => {
|
||||
const date = this.parseDate(match.date);
|
||||
const home = match.homeTeam?.name || 'Heim';
|
||||
const guest = match.guestTeam?.name || 'Gast';
|
||||
const home = match.homeTeam?.name || this.$t('calendar.match.home');
|
||||
const guest = match.guestTeam?.name || this.$t('calendar.match.guest');
|
||||
return {
|
||||
id: `match-${match.id}`,
|
||||
type: 'match',
|
||||
@@ -670,14 +781,14 @@ export default {
|
||||
startsAt: this.combineDateTime(date, match.time),
|
||||
time: this.formatTime(match.time),
|
||||
title: `${home} - ${guest}`,
|
||||
subtitle: match.leagueDetails?.name || match.league?.name || 'Punktspiel',
|
||||
subtitle: match.leagueDetails?.name || match.league?.name || this.$t('calendar.eventTitles.match'),
|
||||
route: '/schedule'
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadOfficialTournamentEvents() {
|
||||
const response = await apiClient.get(`/official-tournaments/${this.currentClub}/participations/summary`);
|
||||
this.ensureSuccess(response, 'Turnierteilnahmen');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.officialTournaments'));
|
||||
return (response.data || [])
|
||||
.filter(tournament => Array.isArray(tournament.entries) && tournament.entries.length > 0)
|
||||
.map(tournament => {
|
||||
@@ -693,8 +804,10 @@ export default {
|
||||
endDate: endDate || date,
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: tournament.tournamentName || tournament.title || 'Turnierteilnahme',
|
||||
subtitle: participantCount > 0 ? `${participantCount} Teilnehmer` : `${entries.length} Starts`,
|
||||
title: tournament.tournamentName || tournament.title || this.$t('calendar.eventTitles.officialTournament'),
|
||||
subtitle: participantCount > 0
|
||||
? this.$t('calendar.participants', { count: participantCount })
|
||||
: this.$t('calendar.starts', { count: entries.length }),
|
||||
route: '/tournament-participations'
|
||||
};
|
||||
})
|
||||
@@ -704,12 +817,12 @@ export default {
|
||||
const response = await apiClient.get(`/calendar/club/${this.currentClub}/holidays`, {
|
||||
params: { year: this.displayedYear }
|
||||
});
|
||||
this.ensureSuccess(response, 'Ferien/Feiertage');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.holidays'));
|
||||
const holidays = response.data?.holidays || [];
|
||||
const schoolHolidays = response.data?.schoolHolidays || [];
|
||||
return [
|
||||
...holidays.map(entry => this.mapCalendarDayEvent(entry, 'holiday', 'Feiertag')),
|
||||
...schoolHolidays.map(entry => this.mapCalendarDayEvent(entry, 'schoolHoliday', 'Schulferien'))
|
||||
...holidays.map(entry => this.mapCalendarDayEvent(entry, 'holiday', this.$t('calendar.holidayTypes.holiday'))),
|
||||
...schoolHolidays.map(entry => this.mapCalendarDayEvent(entry, 'schoolHoliday', this.$t('calendar.holidayTypes.schoolHoliday')))
|
||||
].filter(Boolean);
|
||||
},
|
||||
mapCalendarDayEvent(entry, type, fallbackTitle) {
|
||||
@@ -774,7 +887,52 @@ export default {
|
||||
},
|
||||
shouldShowEventOnDate(event, dateKey) {
|
||||
if (event.type !== 'trainingCancellation') return true;
|
||||
return Boolean(this.plannedTrainingByDateKey[dateKey]);
|
||||
const trainingGroupIds = this.normalizeTrainingGroupIds(event.trainingGroupIds);
|
||||
if (!trainingGroupIds.length) return Boolean(this.plannedTrainingByDateKey[dateKey]);
|
||||
const plannedGroupIds = this.plannedTrainingGroupIdsByDateKey[dateKey] || {};
|
||||
return trainingGroupIds.some(groupId => Boolean(plannedGroupIds[String(groupId)]));
|
||||
},
|
||||
isRecurringTrainingCancelled(event, cancellationEvents) {
|
||||
return cancellationEvents.some(cancellation => (
|
||||
this.isEventOnDate(cancellation, event.date)
|
||||
&& this.matchesCancellationGroup(cancellation, event.trainingGroupId)
|
||||
));
|
||||
},
|
||||
matchesCancellationGroup(cancellation, trainingGroupId) {
|
||||
const trainingGroupIds = this.normalizeTrainingGroupIds(cancellation.trainingGroupIds);
|
||||
return trainingGroupIds.length === 0 || trainingGroupIds.includes(Number.parseInt(trainingGroupId, 10));
|
||||
},
|
||||
normalizeTrainingGroupIds(trainingGroupIds) {
|
||||
const values = this.parseTrainingGroupIdsValue(trainingGroupIds);
|
||||
return [...new Set(
|
||||
values
|
||||
.map(groupId => Number.parseInt(groupId, 10))
|
||||
.filter(groupId => Number.isInteger(groupId) && groupId > 0)
|
||||
)];
|
||||
},
|
||||
parseTrainingGroupIdsValue(trainingGroupIds) {
|
||||
if (Array.isArray(trainingGroupIds)) return trainingGroupIds;
|
||||
if (trainingGroupIds === null || trainingGroupIds === undefined || trainingGroupIds === '') return [];
|
||||
if (typeof trainingGroupIds === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(trainingGroupIds);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
if (parsed !== null && parsed !== undefined) return [parsed];
|
||||
} catch (error) {
|
||||
return trainingGroupIds.split(',');
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
formatCancellationSubtitle(trainingGroupIds) {
|
||||
const normalizedIds = this.normalizeTrainingGroupIds(trainingGroupIds);
|
||||
if (!normalizedIds.length) return this.$t('calendar.cancellation.subtitle');
|
||||
const names = normalizedIds
|
||||
.map(groupId => this.trainingGroupsWithTimes.find(group => Number(group.id) === groupId)?.name)
|
||||
.filter(Boolean);
|
||||
return names.length
|
||||
? `${this.$t('calendar.cancellation.subtitle')} · ${names.join(', ')}`
|
||||
: this.$t('calendar.cancellation.subtitle');
|
||||
},
|
||||
startOfDay(date) {
|
||||
const result = new Date(date);
|
||||
@@ -798,7 +956,7 @@ export default {
|
||||
return startTime || endTime || '';
|
||||
},
|
||||
formatShortDate(date) {
|
||||
return date.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' });
|
||||
return date.toLocaleDateString(this.$i18n?.locale || 'de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' });
|
||||
},
|
||||
formatEventDate(event) {
|
||||
if (!event.endDate || this.toDateKey(event.date) === this.toDateKey(event.endDate)) {
|
||||
@@ -821,47 +979,56 @@ export default {
|
||||
this.offerTrainingCancellationForHoliday(event);
|
||||
return;
|
||||
}
|
||||
if (event?.type === 'trainingCancellation') {
|
||||
this.openTrainingCancellationDeleteDialog(event);
|
||||
return;
|
||||
}
|
||||
if (event.route) {
|
||||
this.$router.push(event.route);
|
||||
}
|
||||
},
|
||||
async offerTrainingCancellationForHoliday(event) {
|
||||
offerTrainingCancellationForHoliday(event) {
|
||||
if (!this.currentClub) return;
|
||||
const startDateKey = this.toDateKey(event.date);
|
||||
const endDateKey = this.toDateKey(event.endDate || event.date);
|
||||
const isRange = startDateKey !== endDateKey;
|
||||
|
||||
const shouldCreate = await this.showConfirm(
|
||||
'Trainingsausfall',
|
||||
'Training als Ausfall markieren?',
|
||||
'',
|
||||
'warning',
|
||||
{ confirmText: 'Ja', cancelText: 'Nein' }
|
||||
);
|
||||
if (!shouldCreate) return;
|
||||
|
||||
let useRange = false;
|
||||
if (isRange) {
|
||||
useRange = await this.showConfirm(
|
||||
'Zeitraum',
|
||||
`Gilt der Ausfall für den gesamten Zeitraum (${startDateKey} bis ${endDateKey})?`,
|
||||
'',
|
||||
'info',
|
||||
{ confirmText: 'Zeitraum', cancelText: 'Nur Tag' }
|
||||
);
|
||||
}
|
||||
|
||||
const reason = `${event.title}${event.subtitle ? ` (${event.subtitle})` : ''}`.trim();
|
||||
await this.createTrainingCancellation(startDateKey, useRange ? endDateKey : startDateKey, reason);
|
||||
|
||||
this.quickCancellationDialog = {
|
||||
isOpen: true,
|
||||
eventTitle: event.title,
|
||||
startDate: startDateKey,
|
||||
endDate: endDateKey,
|
||||
isRange,
|
||||
useRange: isRange,
|
||||
reason,
|
||||
trainingGroupIds: this.trainingGroupsWithTimes.map(group => String(group.id)),
|
||||
};
|
||||
},
|
||||
closeQuickCancellationDialog() {
|
||||
this.quickCancellationDialog.isOpen = false;
|
||||
},
|
||||
async confirmQuickCancellation() {
|
||||
const dialog = this.quickCancellationDialog;
|
||||
if (!this.currentClub || !dialog.startDate || dialog.trainingGroupIds.length === 0) return;
|
||||
const endDate = dialog.isRange && dialog.useRange ? dialog.endDate : dialog.startDate;
|
||||
await this.createTrainingCancellation(
|
||||
dialog.startDate,
|
||||
endDate,
|
||||
dialog.reason,
|
||||
dialog.trainingGroupIds
|
||||
);
|
||||
this.closeQuickCancellationDialog();
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
async createTrainingCancellation(startDate, endDate, reason) {
|
||||
async createTrainingCancellation(startDate, endDate, reason, trainingGroupIds = []) {
|
||||
const response = await apiClient.post(`/training-cancellations/${this.currentClub}`, {
|
||||
startDate,
|
||||
endDate,
|
||||
reason
|
||||
reason,
|
||||
trainingGroupIds: this.normalizeTrainingGroupIds(trainingGroupIds)
|
||||
});
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
this.ensureSuccess(response, this.$t('calendar.sources.trainingCancellations'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1104,7 +1271,7 @@ export default {
|
||||
|
||||
.training-cancellation-form {
|
||||
display: grid;
|
||||
grid-template-columns: 10rem 10rem minmax(12rem, 1fr) auto;
|
||||
grid-template-columns: 10rem 10rem 12rem minmax(12rem, 1fr) auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1121,6 +1288,7 @@ export default {
|
||||
}
|
||||
|
||||
.training-cancellation-form input,
|
||||
.training-cancellation-form select,
|
||||
.training-cancellation-form button {
|
||||
border: 1px solid #cfdad4;
|
||||
border-radius: 8px;
|
||||
@@ -1128,6 +1296,11 @@ export default {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.training-cancellation-form select {
|
||||
min-height: 4.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
.training-cancellation-form button {
|
||||
background: #2f7a5f;
|
||||
color: #fff;
|
||||
@@ -1318,6 +1491,82 @@ export default {
|
||||
color: #607169;
|
||||
}
|
||||
|
||||
.quick-cancellation-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.quick-cancellation-dialog p {
|
||||
margin: 0;
|
||||
color: #40524b;
|
||||
}
|
||||
|
||||
.quick-cancellation-checkbox,
|
||||
.quick-cancellation-groups {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-cancellation-checkbox {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.quick-cancellation-groups {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-cancellation-groups span {
|
||||
color: #40524b;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.quick-cancellation-groups select {
|
||||
min-height: 8rem;
|
||||
border: 1px solid #cfdad4;
|
||||
border-radius: 8px;
|
||||
padding: 0.45rem;
|
||||
}
|
||||
|
||||
.quick-cancellation-hint {
|
||||
color: #8a4b11;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dialog-primary-button,
|
||||
.dialog-danger-button,
|
||||
.dialog-secondary-button {
|
||||
min-height: 2.35rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
padding: 0 0.85rem;
|
||||
}
|
||||
|
||||
.dialog-primary-button {
|
||||
border: 1px solid #2f7a5f;
|
||||
background: #2f7a5f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-danger-button {
|
||||
border: 1px solid #b91c1c;
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-primary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.dialog-secondary-button {
|
||||
border: 1px solid #cfdad4;
|
||||
background: #f8fbf9;
|
||||
color: #173d31;
|
||||
}
|
||||
|
||||
.agenda-time {
|
||||
color: #40524b;
|
||||
font-weight: 800;
|
||||
|
||||
@@ -50,8 +50,12 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -74,10 +78,13 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.max
|
||||
import de.tt_tagebuch.app.AppDependencies
|
||||
import de.tt_tagebuch.app.pdf.sharePdfFile
|
||||
import de.tt_tagebuch.app.pdf.writeTrainingDaySummaryPdf
|
||||
@@ -1455,6 +1462,7 @@ private fun DiaryDetailScreen(
|
||||
var planGroups by remember { mutableStateOf<List<DiaryPlanGroup>>(emptyList()) }
|
||||
var planMutating by remember { mutableStateOf(false) }
|
||||
var planActionError by remember { mutableStateOf<String?>(null) }
|
||||
var expandedPlanActionsItemId by remember { mutableStateOf<Int?>(null) }
|
||||
var showAddPlanActivity by rememberSaveable { mutableStateOf(false) }
|
||||
var showAddPlanGroupActivity by rememberSaveable { mutableStateOf(false) }
|
||||
var showAddTrainingGroup by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -1474,6 +1482,8 @@ private fun DiaryDetailScreen(
|
||||
var editPlanDuration by remember { mutableStateOf("") }
|
||||
var editPlanDurationText by remember { mutableStateOf("") }
|
||||
var editPlanGroupId by remember { mutableStateOf<Int?>(null) }
|
||||
var assigningPlanItem by remember { mutableStateOf<DiaryDateActivityItem?>(null) }
|
||||
var assignPlanGroupId by remember { mutableStateOf<Int?>(null) }
|
||||
var participants by remember { mutableStateOf<List<DiaryTrainingParticipant>>(emptyList()) }
|
||||
var participantsLoading by remember { mutableStateOf(false) }
|
||||
var participantsError by remember { mutableStateOf<String?>(null) }
|
||||
@@ -3161,103 +3171,220 @@ private fun DiaryDetailScreen(
|
||||
val planStartTimes = remember(sortedPlan, entry.trainingStart) {
|
||||
calculatePlanStartLabels(sortedPlan, entry.trainingStart)
|
||||
}
|
||||
if (expandedPlanActionsItemId != null && sortedPlan.none { it.id == expandedPlanActionsItemId }) {
|
||||
expandedPlanActionsItemId = null
|
||||
}
|
||||
val planTableScroll = rememberScrollState()
|
||||
val planTableWidth = max(LocalConfiguration.current.screenWidthDp, 380)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
.horizontalScroll(planTableScroll)
|
||||
.clickable { expandedPlanActionsItemId = null },
|
||||
) {
|
||||
Text("STARTZEIT", style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, modifier = Modifier.width(72.dp))
|
||||
Text("AKTIVITÄT / ZEITBLOCK", style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Text("GRUPPE", style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, modifier = Modifier.width(90.dp))
|
||||
Text("DAUER", style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, modifier = Modifier.width(64.dp))
|
||||
Spacer(modifier = Modifier.width(140.dp))
|
||||
}
|
||||
Divider()
|
||||
sortedPlan.forEach { item ->
|
||||
val cfg = dependencies.apiConfig
|
||||
val mainImg = item.mainActivityImagePath()?.let { cfg.toAbsoluteUrl(it) }
|
||||
val nestedImg = item.groupActivities.firstNotNullOfOrNull { ga ->
|
||||
ga.nestedActivityImagePath()?.let { cfg.toAbsoluteUrl(it) }
|
||||
Column(modifier = Modifier.width(planTableWidth.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
"STARTZEIT",
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(DiaryPlanColStart),
|
||||
)
|
||||
Text(
|
||||
"AKTIVITÄT / ZEITBLOCK",
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
"GRUPPE",
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(DiaryPlanColGroup),
|
||||
)
|
||||
Text(
|
||||
"DAUER",
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.width(DiaryPlanColDuration),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(26.dp))
|
||||
}
|
||||
Divider()
|
||||
sortedPlan.forEach { item ->
|
||||
val cfg = dependencies.apiConfig
|
||||
val mainImg = item.mainActivityImagePath()?.let { cfg.toAbsoluteUrl(it) }
|
||||
val nestedImg = item.groupActivities.firstNotNullOfOrNull { ga ->
|
||||
ga.nestedActivityImagePath()?.let { cfg.toAbsoluteUrl(it) }
|
||||
}
|
||||
DiaryPlanEditableCard(
|
||||
item = item,
|
||||
allPlanItems = planItems,
|
||||
scheduledStart = planStartTimes[item.id],
|
||||
planGroups = planGroups,
|
||||
planMutating = planMutating,
|
||||
canWriteDiary = canWriteDiary,
|
||||
mainImageUrl = mainImg,
|
||||
nestedImageUrl = nestedImg,
|
||||
canReadImages = canReadDiary,
|
||||
isExpanded = expandedPlanActionsItemId == item.id,
|
||||
onToggleExpand = {
|
||||
expandedPlanActionsItemId = if (expandedPlanActionsItemId == item.id) null else item.id
|
||||
},
|
||||
onAssign = {
|
||||
assignPlanGroupId = item.groupId
|
||||
assigningPlanItem = item
|
||||
},
|
||||
onOpenImage = { url -> planImageViewerUrl = url },
|
||||
onEdit = { editingPlanItem = item },
|
||||
onDelete = {
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.deletePlanActivity(clubId, item.id)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
planGroups = dependencies.diaryManager.listTrainingGroups(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMoveUp = {
|
||||
val scope = sameTrainingPlanScope(planItems, item)
|
||||
val idx = scope.indexOfFirst { it.id == item.id }
|
||||
if (idx > 0) {
|
||||
val targetOrder = scope[idx - 1].orderId
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.updatePlanActivityOrder(clubId, item.id, targetOrder)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMoveDown = {
|
||||
val scope = sameTrainingPlanScope(planItems, item)
|
||||
val idx = scope.indexOfFirst { it.id == item.id }
|
||||
if (idx >= 0 && idx < scope.lastIndex) {
|
||||
val targetOrder = scope[idx + 1].orderId
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.updatePlanActivityOrder(clubId, item.id, targetOrder)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDeleteNested = { nestedId ->
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.deletePlanNestedGroupActivity(clubId, nestedId)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
DiaryPlanEditableCard(
|
||||
item = item,
|
||||
allPlanItems = planItems,
|
||||
scheduledStart = planStartTimes[item.id],
|
||||
planGroups = planGroups,
|
||||
planMutating = planMutating,
|
||||
canWriteDiary = canWriteDiary,
|
||||
mainImageUrl = mainImg,
|
||||
nestedImageUrl = nestedImg,
|
||||
canReadImages = canReadDiary,
|
||||
onOpenImage = { url -> planImageViewerUrl = url },
|
||||
onEdit = { editingPlanItem = item },
|
||||
onDelete = {
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.deletePlanActivity(clubId, item.id)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
planGroups = dependencies.diaryManager.listTrainingGroups(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onMoveUp = {
|
||||
val scope = sameTrainingPlanScope(planItems, item)
|
||||
val idx = scope.indexOfFirst { it.id == item.id }
|
||||
if (idx > 0) {
|
||||
val targetOrder = scope[idx - 1].orderId
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.updatePlanActivityOrder(clubId, item.id, targetOrder)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
|
||||
assigningPlanItem?.let { assignItem ->
|
||||
var assignGroupMenu by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!planMutating) assigningPlanItem = null },
|
||||
title = { Text(tr("diary.planAssignGroup", "Zuordnen")) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
assignItem.displayTitle(tr("diary.timeblock", "Zeitblock")),
|
||||
style = MaterialTheme.typography.body2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
val selectedLabel = assignPlanGroupId?.let { id ->
|
||||
planGroups.find { it.id == id }?.name ?: "Gruppe $id"
|
||||
} ?: tr("diary.planGroupGlobal", "Alle / keine Gruppe")
|
||||
OutlinedButton(
|
||||
onClick = { assignGroupMenu = true },
|
||||
enabled = canWriteDiary && !planMutating,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("${tr("diary.planAssignGroup", "Zuordnen")}: $selectedLabel") }
|
||||
DropdownMenu(expanded = assignGroupMenu, onDismissRequest = { assignGroupMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
assignPlanGroupId = null
|
||||
assignGroupMenu = false
|
||||
},
|
||||
) { Text(tr("diary.planGroupGlobal", "Alle / keine Gruppe")) }
|
||||
planGroups.forEach { g ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
assignPlanGroupId = g.id
|
||||
assignGroupMenu = false
|
||||
},
|
||||
) { Text(g.name ?: "Gruppe ${g.id}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMoveDown = {
|
||||
val scope = sameTrainingPlanScope(planItems, item)
|
||||
val idx = scope.indexOfFirst { it.id == item.id }
|
||||
if (idx >= 0 && idx < scope.lastIndex) {
|
||||
val targetOrder = scope[idx + 1].orderId
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.updatePlanActivityOrder(clubId, item.id, targetOrder)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = canWriteDiary && !planMutating,
|
||||
onClick = {
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.updatePlanActivity(
|
||||
clubId,
|
||||
assignItem.id,
|
||||
UpdateDiaryPlanActivityRequest(groupId = assignPlanGroupId),
|
||||
)
|
||||
assigningPlanItem = null
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
planGroups = dependencies.diaryManager.listTrainingGroups(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { Text(tr("common.save", "Speichern")) }
|
||||
},
|
||||
onDeleteNested = { nestedId ->
|
||||
dependencies.applicationScope.launch {
|
||||
planMutating = true
|
||||
planActionError = null
|
||||
try {
|
||||
dependencies.diaryManager.deletePlanNestedGroupActivity(clubId, nestedId)
|
||||
planItems = dependencies.diaryManager.fetchDateActivities(clubId, entry.id)
|
||||
} catch (t: Throwable) {
|
||||
planActionError = t.message
|
||||
} finally {
|
||||
planMutating = false
|
||||
}
|
||||
}
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = canWriteDiary && !planMutating,
|
||||
onClick = { assigningPlanItem = null },
|
||||
) { Text(tr("mobile.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -4735,6 +4862,48 @@ private fun diaryPlanItemComparator(a: DiaryDateActivityItem, b: DiaryDateActivi
|
||||
return a.id.compareTo(b.id)
|
||||
}
|
||||
|
||||
private val DiaryPlanColStart = 60.dp
|
||||
private val DiaryPlanColGroup = 68.dp
|
||||
private val DiaryPlanColDuration = 48.dp
|
||||
|
||||
@Composable
|
||||
private fun DiaryPlanQuickIconAction(
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = 33.dp, height = 30.dp)
|
||||
.clickable(enabled = enabled, onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (enabled) MaterialTheme.colors.primary
|
||||
else MaterialTheme.colors.onSurface.copy(alpha = 0.38f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiaryPlanQuickTextAction(
|
||||
label: String,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.heightIn(min = 30.dp),
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.caption, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sameTrainingPlanScope(items: List<DiaryDateActivityItem>, item: DiaryDateActivityItem): List<DiaryDateActivityItem> {
|
||||
return items.filter { it.groupId == item.groupId }.sortedWith(::diaryPlanItemComparator)
|
||||
}
|
||||
@@ -4778,6 +4947,9 @@ private fun DiaryPlanEditableCard(
|
||||
mainImageUrl: String?,
|
||||
nestedImageUrl: String?,
|
||||
canReadImages: Boolean,
|
||||
isExpanded: Boolean,
|
||||
onToggleExpand: () -> Unit,
|
||||
onAssign: () -> Unit,
|
||||
onOpenImage: (String) -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
@@ -4803,42 +4975,108 @@ private fun DiaryPlanEditableCard(
|
||||
?: item.groupId?.let { gid -> planGroups.find { it.id == gid }?.name ?: "Gruppe $gid" }
|
||||
val showImageUrl = mainImageUrl ?: nestedImageUrl
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
val canMutate = canWriteDiary && !planMutating
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleExpand() }
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
scheduledStart ?: "—",
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.width(72.dp),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.width(DiaryPlanColStart),
|
||||
)
|
||||
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(title, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
||||
Text(
|
||||
title,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (item.isTimeblock) {
|
||||
Text(" · $timeblockLabel", style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary)
|
||||
Text(
|
||||
" · $timeblockLabel",
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
groupLine ?: "—",
|
||||
style = MaterialTheme.typography.caption,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.width(90.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.width(DiaryPlanColGroup),
|
||||
)
|
||||
Text(duration ?: "—", style = MaterialTheme.typography.body2, modifier = Modifier.width(64.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.width(140.dp)) {
|
||||
TextButton(onClick = onMoveUp, enabled = canWriteDiary && !planMutating && canUp) { Text("↑") }
|
||||
TextButton(onClick = onMoveDown, enabled = canWriteDiary && !planMutating && canDown) { Text("↓") }
|
||||
TextButton(onClick = onEdit, enabled = canWriteDiary && !planMutating) { Text(tr("common.edit", "Bearbeiten")) }
|
||||
TextButton(onClick = onDelete, enabled = canWriteDiary && !planMutating) { Text(tr("common.delete", "Löschen")) }
|
||||
Text(
|
||||
duration ?: "—",
|
||||
style = MaterialTheme.typography.caption,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.width(DiaryPlanColDuration),
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||
contentDescription = tr("mobile.more", "Mehr"),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.75f),
|
||||
)
|
||||
}
|
||||
if (isExpanded) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = DiaryPlanColStart, top = 3.dp, bottom = 3.dp, end = 4.dp)
|
||||
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.04f))
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
DiaryPlanQuickTextAction(
|
||||
label = tr("diary.planActionUp", "↑ Hoch"),
|
||||
enabled = canMutate && canUp,
|
||||
onClick = onMoveUp,
|
||||
)
|
||||
DiaryPlanQuickTextAction(
|
||||
label = tr("diary.planActionDown", "↓ Runter"),
|
||||
enabled = canMutate && canDown,
|
||||
onClick = onMoveDown,
|
||||
)
|
||||
DiaryPlanQuickTextAction(
|
||||
label = tr("diary.planAssignGroup", "Zuordnen"),
|
||||
enabled = canMutate,
|
||||
onClick = onAssign,
|
||||
)
|
||||
DiaryPlanQuickTextAction(
|
||||
label = tr("common.edit", "Bearbeiten"),
|
||||
enabled = canMutate,
|
||||
onClick = onEdit,
|
||||
)
|
||||
DiaryPlanQuickTextAction(
|
||||
label = tr("common.delete", "Löschen"),
|
||||
enabled = canMutate,
|
||||
onClick = onDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (canReadImages && showImageUrl != null) {
|
||||
TextButton(
|
||||
onClick = { onOpenImage(showImageUrl) },
|
||||
enabled = !planMutating,
|
||||
modifier = Modifier.padding(start = 72.dp),
|
||||
) { Text(tr("diary.planShowImage", "Übungsbild anzeigen")) }
|
||||
Text(
|
||||
tr("diary.planShowImage", "Übungsbild anzeigen"),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.padding(start = DiaryPlanColStart, top = 0.dp, bottom = 0.dp)
|
||||
.clickable(enabled = !planMutating) { onOpenImage(showImageUrl) },
|
||||
)
|
||||
}
|
||||
item.groupActivities
|
||||
.sortedBy { it.orderId ?: it.id ?: 0 }
|
||||
@@ -4846,15 +5084,24 @@ private fun DiaryPlanEditableCard(
|
||||
val sub = ga.groupPredefinedActivity.displayLabel()
|
||||
val line = if (sub.isNotEmpty()) sub else "${tr("mobile.activityFallback", "Aktivität")} ${ga.id}"
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 72.dp, top = 2.dp, bottom = 2.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(start = DiaryPlanColStart, top = 0.dp, bottom = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("· $line", style = MaterialTheme.typography.caption, modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
"· $line",
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val nid = ga.id
|
||||
if (nid != null) {
|
||||
TextButton(onClick = { onDeleteNested(nid) }, enabled = canWriteDiary && !planMutating) {
|
||||
Text(tr("common.delete", "Löschen"))
|
||||
}
|
||||
DiaryPlanQuickIconAction(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = tr("diary.planDeleteNested", "Gruppenübung löschen"),
|
||||
enabled = canWriteDiary && !planMutating,
|
||||
onClick = { onDeleteNested(nid) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user