Trainingstage, Vereinsturniere und Punktspiele in einer Monatsansicht.
+Hier eingetragene Tage blenden die regelmäßigen Trainingszeiten aus.
+Keine Termine in diesem Monat.
+ +Die Region wird für Schulferien und gesetzliche Feiertage im Kalender genutzt.
+{{ $t('clubSettings.myTischtennisRankingsHint') }}
@@ -133,6 +155,25 @@ const defaultMemberDataQualityRequirements = () => ({ requireEmail: true, }); +const GERMAN_STATES = [ + { code: 'DE-BW', name: 'Baden-Württemberg' }, + { code: 'DE-BY', name: 'Bayern' }, + { code: 'DE-BE', name: 'Berlin' }, + { code: 'DE-BB', name: 'Brandenburg' }, + { code: 'DE-HB', name: 'Bremen' }, + { code: 'DE-HH', name: 'Hamburg' }, + { code: 'DE-HE', name: 'Hessen' }, + { code: 'DE-MV', name: 'Mecklenburg-Vorpommern' }, + { code: 'DE-NI', name: 'Niedersachsen' }, + { code: 'DE-NW', name: 'Nordrhein-Westfalen' }, + { code: 'DE-RP', name: 'Rheinland-Pfalz' }, + { code: 'DE-SL', name: 'Saarland' }, + { code: 'DE-SN', name: 'Sachsen' }, + { code: 'DE-ST', name: 'Sachsen-Anhalt' }, + { code: 'DE-SH', name: 'Schleswig-Holstein' }, + { code: 'DE-TH', name: 'Thüringen' }, +]; + export default { name: 'ClubSettings', components: { @@ -144,6 +185,8 @@ export default { activeTab: 'settings', greeting: '', associationMemberNumber: '', + countryCode: 'DE', + stateCode: '', myTischtennisFedNickname: '', autoFetchRankings: false, memberDataQualityRequirements: defaultMemberDataQualityRequirements(), @@ -154,6 +197,9 @@ export default { }, computed: { ...mapGetters(['currentClub']), + germanStates() { + return GERMAN_STATES; + }, }, watch: { currentClub: { @@ -168,6 +214,8 @@ export default { if (!this.currentClub) { this.greeting = ''; this.associationMemberNumber = ''; + this.countryCode = 'DE'; + this.stateCode = ''; this.myTischtennisFedNickname = ''; this.autoFetchRankings = false; this.memberDataQualityRequirements = defaultMemberDataQualityRequirements(); @@ -181,6 +229,8 @@ export default { const club = response.data; this.greeting = club?.greetingText ?? ''; this.associationMemberNumber = club?.associationMemberNumber ?? ''; + this.countryCode = club?.countryCode ?? 'DE'; + this.stateCode = club?.stateCode ?? ''; this.myTischtennisFedNickname = club?.myTischtennisFedNickname ?? ''; this.autoFetchRankings = !!club?.autoFetchRankings; this.memberDataQualityRequirements = this.normalizeMemberDataQualityRequirements(club?.memberDataQualityRequirements); @@ -188,6 +238,8 @@ export default { this.loadError = this.$t('clubSettings.loadFailed'); this.greeting = ''; this.associationMemberNumber = ''; + this.countryCode = 'DE'; + this.stateCode = ''; this.myTischtennisFedNickname = ''; this.autoFetchRankings = false; this.memberDataQualityRequirements = defaultMemberDataQualityRequirements(); @@ -224,6 +276,8 @@ export default { await apiClient.put(`/clubs/${this.currentClub}/settings`, { greetingText: this.greeting, associationMemberNumber: this.associationMemberNumber, + countryCode: this.countryCode, + stateCode: this.stateCode || null, myTischtennisFedNickname: this.myTischtennisFedNickname || null, autoFetchRankings: this.autoFetchRankings, memberDataQualityRequirements: this.normalizeMemberDataQualityRequirements(this.memberDataQualityRequirements), @@ -264,6 +318,7 @@ export default { .text-input { width: 100%; border: 1px solid #ddd; border-radius: 6px; padding: 8px; font-size: 14px; } .rankings-row { margin-bottom: 12px; } .rankings-fields { margin-top: 12px; } +.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .quality-options { display: grid; gap: 10px; margin-top: 12px; } .field-group label { display: block; margin-bottom: 4px; font-weight: 500; color: #333; } .checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; } @@ -305,5 +360,8 @@ export default { color: #28a745; border-bottom-color: #28a745; } - +@media (max-width: 720px) { + .field-grid { grid-template-columns: 1fr; } +} + diff --git a/mobile-app/TODO.md b/mobile-app/TODO.md index bf5c27f1..3b699aff 100644 --- a/mobile-app/TODO.md +++ b/mobile-app/TODO.md @@ -140,10 +140,15 @@ Web: `DiaryView.vue` (sehr groß). API-Cluster (Auszug – in Web nach `apiClien - [x] **i18n-Local:** `LanguageLocals.kt` (`LocalLanguageCode`) aus `AppRoot.kt` ausgelagert - [x] **Hinweis:** Web hat keinen CSV-Export für diese Statistik; mobil zusätzlich **CSV exportieren** (gefilterte/sortierte Mitgliederliste) -## Phase 6 – Terminplan (ScheduleView) +## Phase 6 – Terminplan (ScheduleView) — erledigt -- [ ] Kalender-/Listenansicht, CRUD oder Sync wie Web -- [ ] API-Endpunkte aus `ScheduleView.vue` ins `shared` übernehmen +- [x] **DTOs:** `Schedule.kt` – `ClubTeamDto`, `ScheduleMatchDto`, `LeagueTableRowDto`, `UpdateMatchPlayersBody`, `ScheduleMatchScope`, `ScheduleViewMode` +- [x] **APIs:** `ClubTeamsApi` (`GET /api/club-teams/club/:clubId`), `MatchesApi` (`/api/matches/leagues/...` matches + Tabelle, `PATCH /api/matches/:matchId/players`) +- [x] **Logik:** `ScheduleLogic.kt` – Sortierung, Merge, Filter „Erwachsene“, Mannschafts-Scope wie Web +- [x] **State:** `ScheduleManager.kt` – Mannschaften laden, Mannschafts-/Gesamt-/Erwachsenen-Ansicht, Tabelle, Spieler-Patch + Refresh +- [x] **Berechtigungen:** `canReadSchedule` / `canWriteSchedule` in `ClubPermissionHelpers.kt` +- [x] **UI:** `ScheduleScreen.kt` – Tab **Terminplan** (`MainTab.Schedule`), Home-Kachel bei Lese-Recht, Liste + Detail, Aufstellung (R/P/S) bei Schreib-Recht +- [x] **Noch nicht mobil:** CSV-Import (`POST /api/matches/import`), MyTT-Tabellen-Fetch (`POST .../table/.../fetch`), Galerie/Lineup wie Web – bei Bedarf spätere Phase --- @@ -158,10 +163,10 @@ Web: `DiaryView.vue` (sehr groß). API-Cluster (Auszug – in Web nach `apiClien ## Phase 8 – Freigaben & Verwaltung -- [ ] **Ausstehende Freigaben** (`PendingApprovalsView.vue`) -- [ ] **Team-Management** (`TeamManagementView.vue`) -- [ ] **Berechtigungen** (`PermissionsView.vue`) – rollenbasiert -- [ ] **Logs** (`LogsView.vue`) – eher Admin; nur wenn nötig mobil +- [x] **Ausstehende Freigaben** – `ClubApprovalsApi`, `PendingApprovalsManager`, Screen unter „Mehr“ → Club-Verwaltung (`ClubAdminScreens.kt`) +- [x] **Team-Management** – Deep-Link `openBackendPath("/team-management")` bei `canReadTeams()` (volle Parität zur Web-`TeamManagementView` bewusst nicht mobil) +- [x] **Berechtigungen** – erweiterte `PermissionsApi`, `PermissionsAdminManager`, UI Rolle/Status/Anpassen mit `RolePermissionMatrix` (`ClubAdminScreens.kt`) +- [x] **Logs** – `ApiLogsApi`, `ApiLogsManager`, Liste + Pagination + Detail (`ClubAdminScreens.kt`); `AppDependencies` / Logout / 401 räumen Manager auf --- diff --git a/mobile-app/composeApp/build.gradle.kts b/mobile-app/composeApp/build.gradle.kts index 6893ad6b..c971703c 100644 --- a/mobile-app/composeApp/build.gradle.kts +++ b/mobile-app/composeApp/build.gradle.kts @@ -41,6 +41,7 @@ kotlin { implementation(libs.koin.android) implementation(libs.coil.compose) implementation(libs.yalantis.ucrop) + implementation(libs.ktor.serialization.kotlinx.json) } } } diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/AppDependencies.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/AppDependencies.kt index 5d01a5d2..0fbc0f93 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/AppDependencies.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/AppDependencies.kt @@ -4,9 +4,12 @@ import android.content.Context import android.content.Intent import android.net.Uri import de.tt_tagebuch.shared.api.AccidentApi +import de.tt_tagebuch.shared.api.ApiLogsApi +import de.tt_tagebuch.shared.api.ClubApprovalsApi import de.tt_tagebuch.shared.api.ApiConfig import de.tt_tagebuch.shared.api.AuthApi import de.tt_tagebuch.shared.api.PublicAuthApi +import de.tt_tagebuch.shared.api.ClubTeamsApi import de.tt_tagebuch.shared.api.ClubsApi import de.tt_tagebuch.shared.api.DiaryApi import de.tt_tagebuch.shared.api.DiaryMemberActivitiesApi @@ -14,26 +17,36 @@ import de.tt_tagebuch.shared.api.DiaryMemberApi import de.tt_tagebuch.shared.api.GroupApi import de.tt_tagebuch.shared.api.ParticipantsApi import de.tt_tagebuch.shared.api.PredefinedActivitiesApi +import de.tt_tagebuch.shared.api.MatchesApi import de.tt_tagebuch.shared.api.MemberActivitiesApi import de.tt_tagebuch.shared.api.MemberGroupPhotosApi import de.tt_tagebuch.shared.api.MembersApi +import de.tt_tagebuch.shared.api.OfficialTournamentsApi import de.tt_tagebuch.shared.api.PermissionsApi import de.tt_tagebuch.shared.api.SessionApi import de.tt_tagebuch.shared.api.TrainingGroupsApi import de.tt_tagebuch.shared.api.TrainingStatsApi import de.tt_tagebuch.shared.api.TrainingTimesApi +import de.tt_tagebuch.shared.api.TournamentsApi import de.tt_tagebuch.shared.api.http.AndroidHttpClientEngineFactory import de.tt_tagebuch.shared.api.http.AuthedHttpClient import de.tt_tagebuch.shared.api.http.PublicHttpClient import de.tt_tagebuch.shared.state.AndroidClubStorage import de.tt_tagebuch.shared.state.AndroidLanguageStorage import de.tt_tagebuch.shared.state.AndroidTokenStorage +import de.tt_tagebuch.shared.state.ApiLogsManager import de.tt_tagebuch.shared.state.AuthManager +import de.tt_tagebuch.shared.state.ClubInternalTournamentsManager import de.tt_tagebuch.shared.state.ClubManager import de.tt_tagebuch.shared.state.DiaryManager import de.tt_tagebuch.shared.state.LanguageManager import de.tt_tagebuch.shared.state.MembersManager import de.tt_tagebuch.shared.state.MutableTokenProvider +import de.tt_tagebuch.shared.state.OfficialTournamentsReadManager +import de.tt_tagebuch.shared.state.PendingApprovalsManager +import de.tt_tagebuch.shared.state.PermissionsAdminManager +import de.tt_tagebuch.shared.state.MutableTokenProvider +import de.tt_tagebuch.shared.state.ScheduleManager import de.tt_tagebuch.shared.state.TrainingStatsManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -73,12 +86,20 @@ class AppDependencies(context: Context) { sessionApi = SessionApi(client), ) + private val permissionsApi = PermissionsApi(client) + val clubManager = ClubManager( clubStorage = AndroidClubStorage(context.applicationContext), clubsApi = ClubsApi(client), - permissionsApi = PermissionsApi(client), + permissionsApi = permissionsApi, ) + val pendingApprovalsManager = PendingApprovalsManager(ClubApprovalsApi(client)) + val permissionsAdminManager = PermissionsAdminManager(permissionsApi) + val apiLogsManager = ApiLogsManager(ApiLogsApi(client)) + val clubInternalTournamentsManager = ClubInternalTournamentsManager(TournamentsApi(client)) + val officialTournamentsReadManager = OfficialTournamentsReadManager(OfficialTournamentsApi(client)) + val diaryManager = DiaryManager( DiaryApi(client), ParticipantsApi(client), @@ -96,6 +117,10 @@ class AppDependencies(context: Context) { TrainingTimesApi(client), ) val trainingStatsManager = TrainingStatsManager(TrainingStatsApi(client)) + val scheduleManager = ScheduleManager( + ClubTeamsApi(client), + MatchesApi(client), + ) val languageManager = LanguageManager(AndroidLanguageStorage(context.applicationContext)) val sessionApi = SessionApi(client) diff --git a/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/ui/AppRoot.kt b/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/ui/AppRoot.kt index 43a6e843..533dbfeb 100644 --- a/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/ui/AppRoot.kt +++ b/mobile-app/composeApp/src/androidMain/kotlin/de/tt_tagebuch/app/ui/AppRoot.kt @@ -47,8 +47,10 @@ 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.automirrored.filled.List import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.EmojiEvents import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Home @@ -81,8 +83,13 @@ import de.tt_tagebuch.app.pdf.writeTrainingPlanPdf import de.tt_tagebuch.shared.api.memberProfileImagePath import de.tt_tagebuch.shared.api.toAbsoluteUrl import de.tt_tagebuch.shared.api.models.MemberGroupPhotoDto +import de.tt_tagebuch.shared.api.models.canReadApprovals +import de.tt_tagebuch.shared.api.models.canReadClubPermissions import de.tt_tagebuch.shared.api.models.canReadDiary +import de.tt_tagebuch.shared.api.models.canReadTeams import de.tt_tagebuch.shared.api.models.canReadMembers +import de.tt_tagebuch.shared.api.models.canReadSchedule +import de.tt_tagebuch.shared.api.models.canReadTournaments import de.tt_tagebuch.shared.api.models.canWriteDiary import de.tt_tagebuch.shared.api.models.canWriteMembers import de.tt_tagebuch.shared.api.models.mainActivityImagePath @@ -115,6 +122,7 @@ import de.tt_tagebuch.shared.api.models.TrainingGroupDto import de.tt_tagebuch.shared.api.models.TrainingTimeDto import de.tt_tagebuch.shared.api.models.toSetBody import de.tt_tagebuch.shared.api.models.Member +import de.tt_tagebuch.shared.api.models.UserClubPermissions import de.tt_tagebuch.shared.i18n.MobileStrings import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -129,6 +137,14 @@ private const val MAIN_NAV_RAIL_MIN_WIDTH_DP = 600 private val ScreenHorizontalPadding = 20.dp private val TouchMinHeight = 48.dp +private fun visibleMainTabs(perms: UserClubPermissions?): List