feat(TrainingCancellation): enhance cancellation functionality and localization support
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:
Torsten Schulz (local)
2026-05-13 10:57:23 +02:00
parent 004801b1a6
commit 7981371136
22 changed files with 2306 additions and 275 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Aujourdhui",
"loading": "Chargement des données du calendrier...",
"noClub": "Veuillez dabord 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 lannulation de lentraînement",
"message": "Vraiment annuler lannulation « {title} » le {date} ?",
"confirm": "Annuler lannulation"
}
},
"navigation": {
"home": "Accueil",
"members": "Membres",

View File

@@ -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",

View File

@@ -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": "メンバー",

View File

@@ -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",

View File

@@ -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": "สมาชิก",

View File

@@ -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",

View File

@@ -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": "成员",

View File

@@ -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;