feat(ClubSettings): add country and state code fields for regional calendar data
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
- Introduced `countryCode` and `stateCode` fields in the Club model to support regional calendar data. - Updated ClubSettings component to allow users to select their country and state, enhancing the configuration options for clubs. - Enhanced the ClubService to handle normalization of country and state codes during updates. - Added new routes and middleware to support the training cancellation feature and calendar integration in the backend. - Updated frontend navigation to include a calendar link, improving user access to scheduling features.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogDetailDto
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogDetailEnvelopeDto
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogsListEnvelopeDto
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogsListPageDto
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
|
||||
class ApiLogsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun listLogs(
|
||||
limit: Int = 50,
|
||||
offset: Int = 0,
|
||||
logType: String? = null,
|
||||
method: String? = null,
|
||||
statusCode: Int? = null,
|
||||
pathContains: String? = null,
|
||||
): ApiLogsListPageDto {
|
||||
val env = client.http.get("/api/logs") {
|
||||
parameter("limit", limit)
|
||||
parameter("offset", offset)
|
||||
logType?.takeIf { it.isNotBlank() }?.let { parameter("logType", it) }
|
||||
method?.takeIf { it.isNotBlank() }?.let { parameter("method", it) }
|
||||
statusCode?.let { parameter("statusCode", it) }
|
||||
pathContains?.takeIf { it.isNotBlank() }?.let { parameter("path", it) }
|
||||
}.body<ApiLogsListEnvelopeDto>()
|
||||
return env.data ?: ApiLogsListPageDto()
|
||||
}
|
||||
|
||||
suspend fun getLog(id: Int): ApiLogDetailDto? {
|
||||
val env = client.http.get("/api/logs/$id").body<ApiLogDetailEnvelopeDto>()
|
||||
return env.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.ClubAccessDecisionBody
|
||||
import de.tt_tagebuch.shared.api.models.PendingUserClubJoinDto
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
|
||||
class ClubApprovalsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun listPending(clubId: Int): List<PendingUserClubJoinDto> {
|
||||
return client.http.get("/api/clubs/pending/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun approve(body: ClubAccessDecisionBody) {
|
||||
client.http.post("/api/clubs/approve") { setBody(body) }
|
||||
}
|
||||
|
||||
suspend fun reject(body: ClubAccessDecisionBody) {
|
||||
client.http.post("/api/clubs/reject") { setBody(body) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.ClubTeamDto
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
|
||||
class ClubTeamsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun listClubTeams(clubId: Int, seasonId: Int? = null): List<ClubTeamDto> {
|
||||
return client.http.get("/api/club-teams/club/$clubId") {
|
||||
seasonId?.let { parameter("seasonid", it) }
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.LeagueTableRowDto
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
import de.tt_tagebuch.shared.api.models.UpdateMatchPlayersBody
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.setBody
|
||||
|
||||
class MatchesApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun listMatchesForLeagues(clubId: Int, seasonId: Int? = null): List<ScheduleMatchDto> {
|
||||
return client.http.get("/api/matches/leagues/$clubId/matches") {
|
||||
seasonId?.let { parameter("seasonid", it) }
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun listMatchesForLeague(clubId: Int, leagueId: Int, scope: String = "own"): List<ScheduleMatchDto> {
|
||||
return client.http.get("/api/matches/leagues/$clubId/matches/$leagueId") {
|
||||
parameter("scope", scope)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun leagueTable(clubId: Int, leagueId: Int): List<LeagueTableRowDto> {
|
||||
return client.http.get("/api/matches/leagues/$clubId/table/$leagueId").body()
|
||||
}
|
||||
|
||||
suspend fun updateMatchPlayers(matchId: Int, body: UpdateMatchPlayersBody) {
|
||||
client.http.patch("/api/matches/$matchId/players") {
|
||||
setBody(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.OfficialParticipationBucketDto
|
||||
import de.tt_tagebuch.shared.api.models.OfficialTournamentListRowDto
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
|
||||
class OfficialTournamentsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun listForClub(clubId: Int): List<OfficialTournamentListRowDto> {
|
||||
return client.http.get("/api/official-tournaments/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun listParticipationSummary(clubId: Int): List<OfficialParticipationBucketDto> {
|
||||
return client.http.get("/api/official-tournaments/$clubId/participations/summary").body()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.AvailableRoleDto
|
||||
import de.tt_tagebuch.shared.api.models.ClubPermissionMemberDto
|
||||
import de.tt_tagebuch.shared.api.models.PermissionResourceDto
|
||||
import de.tt_tagebuch.shared.api.models.UpdateUserApprovedBody
|
||||
import de.tt_tagebuch.shared.api.models.UpdateUserCustomPermissionsBody
|
||||
import de.tt_tagebuch.shared.api.models.UpdateUserRoleBody
|
||||
import de.tt_tagebuch.shared.api.models.UserClubPermissions
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
|
||||
class PermissionsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
@@ -11,5 +20,37 @@ class PermissionsApi(
|
||||
suspend fun getUserPermissions(clubId: Int): UserClubPermissions {
|
||||
return client.http.get("/api/permissions/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun listAvailableRoles(): List<AvailableRoleDto> {
|
||||
return client.http.get("/api/permissions/roles/available").body()
|
||||
}
|
||||
|
||||
suspend fun getPermissionStructure(): Map<String, PermissionResourceDto> {
|
||||
return client.http.get("/api/permissions/structure/all").body()
|
||||
}
|
||||
|
||||
suspend fun listClubMembersWithPermissions(clubId: Int, cacheBust: Boolean = false): List<ClubPermissionMemberDto> {
|
||||
return client.http.get("/api/permissions/$clubId/members") {
|
||||
if (cacheBust) parameter("t", kotlin.random.Random.nextLong().toString())
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun updateUserRole(clubId: Int, userId: Int, role: String) {
|
||||
client.http.put("/api/permissions/$clubId/user/$userId/role") {
|
||||
setBody(UpdateUserRoleBody(role = role))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserStatus(clubId: Int, userId: Int, approved: Boolean) {
|
||||
client.http.put("/api/permissions/$clubId/user/$userId/status") {
|
||||
setBody(UpdateUserApprovedBody(approved = approved))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserCustomPermissions(clubId: Int, userId: Int, permissions: kotlinx.serialization.json.JsonObject) {
|
||||
client.http.put("/api/permissions/$clubId/user/$userId/permissions") {
|
||||
setBody(UpdateUserCustomPermissionsBody(permissions = permissions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
/**
|
||||
* Rollen-Standardrechte (analog [backend/services/permissionService.js] ROLE_PERMISSIONS)
|
||||
* für die mobile Berechtigungs-UI (Erbe / explizit erlauben / explizit verbieten).
|
||||
*/
|
||||
object RolePermissionMatrix {
|
||||
|
||||
private val admin = resourceMap(
|
||||
diary = triple(true, true, true),
|
||||
members = triple(true, true, true),
|
||||
teams = triple(true, true, true),
|
||||
schedule = triple(true, true, true),
|
||||
tournaments = triple(true, true, true),
|
||||
statistics = pair(true, true),
|
||||
settings = pair(true, true),
|
||||
permissions = pair(true, true),
|
||||
approvals = pair(true, true),
|
||||
mytischtennis_admin = pair(true, true),
|
||||
predefined_activities = triple(true, true, true),
|
||||
)
|
||||
|
||||
private val trainer = resourceMap(
|
||||
diary = triple(true, true, true),
|
||||
members = triple(true, true, false),
|
||||
teams = triple(true, true, false),
|
||||
schedule = triple(true, false, false),
|
||||
tournaments = triple(true, true, false),
|
||||
statistics = pair(true, false),
|
||||
settings = pair(false, false),
|
||||
permissions = pair(false, false),
|
||||
approvals = pair(false, false),
|
||||
mytischtennis_admin = pair(false, false),
|
||||
predefined_activities = triple(true, true, true),
|
||||
)
|
||||
|
||||
private val teamManager = resourceMap(
|
||||
diary = triple(false, false, false),
|
||||
members = triple(true, false, false),
|
||||
teams = triple(true, true, false),
|
||||
schedule = triple(true, true, false),
|
||||
tournaments = triple(true, false, false),
|
||||
statistics = pair(true, false),
|
||||
settings = pair(false, false),
|
||||
permissions = pair(false, false),
|
||||
approvals = pair(false, false),
|
||||
mytischtennis_admin = pair(false, false),
|
||||
predefined_activities = triple(false, false, false),
|
||||
)
|
||||
|
||||
private val tournamentManager = resourceMap(
|
||||
diary = triple(false, false, false),
|
||||
members = triple(true, false, false),
|
||||
teams = triple(false, false, false),
|
||||
schedule = triple(false, false, false),
|
||||
tournaments = triple(true, true, false),
|
||||
statistics = pair(true, false),
|
||||
settings = pair(false, false),
|
||||
permissions = pair(false, false),
|
||||
approvals = pair(false, false),
|
||||
mytischtennis_admin = pair(false, false),
|
||||
predefined_activities = triple(false, false, false),
|
||||
)
|
||||
|
||||
private val member = resourceMap(
|
||||
diary = triple(false, false, false),
|
||||
members = triple(false, false, false),
|
||||
teams = triple(false, false, false),
|
||||
schedule = triple(false, false, false),
|
||||
tournaments = triple(false, false, false),
|
||||
statistics = pair(true, false),
|
||||
settings = pair(false, false),
|
||||
permissions = pair(false, false),
|
||||
approvals = pair(false, false),
|
||||
mytischtennis_admin = pair(false, false),
|
||||
predefined_activities = triple(false, false, false),
|
||||
)
|
||||
|
||||
private fun triple(r: Boolean, w: Boolean, d: Boolean): Map<String, Boolean> =
|
||||
mapOf("read" to r, "write" to w, "delete" to d)
|
||||
|
||||
private fun pair(r: Boolean, w: Boolean): Map<String, Boolean> =
|
||||
mapOf("read" to r, "write" to w)
|
||||
|
||||
private fun resourceMap(
|
||||
diary: Map<String, Boolean>,
|
||||
members: Map<String, Boolean>,
|
||||
teams: Map<String, Boolean>,
|
||||
schedule: Map<String, Boolean>,
|
||||
tournaments: Map<String, Boolean>,
|
||||
statistics: Map<String, Boolean>,
|
||||
settings: Map<String, Boolean>,
|
||||
permissions: Map<String, Boolean>,
|
||||
approvals: Map<String, Boolean>,
|
||||
mytischtennis_admin: Map<String, Boolean>,
|
||||
predefined_activities: Map<String, Boolean>,
|
||||
): Map<String, Map<String, Boolean>> = mapOf(
|
||||
"diary" to diary,
|
||||
"members" to members,
|
||||
"teams" to teams,
|
||||
"schedule" to schedule,
|
||||
"tournaments" to tournaments,
|
||||
"statistics" to statistics,
|
||||
"settings" to settings,
|
||||
"permissions" to permissions,
|
||||
"approvals" to approvals,
|
||||
"mytischtennis_admin" to mytischtennis_admin,
|
||||
"predefined_activities" to predefined_activities,
|
||||
)
|
||||
|
||||
fun defaultsForRole(role: String): Map<String, Map<String, Boolean>> =
|
||||
when (role) {
|
||||
"admin" -> admin
|
||||
"trainer" -> trainer
|
||||
"team_manager" -> teamManager
|
||||
"tournament_manager" -> tournamentManager
|
||||
else -> member
|
||||
}
|
||||
|
||||
fun defaultAction(role: String, resource: String, action: String): Boolean =
|
||||
defaultsForRole(role)[resource]?.get(action) ?: false
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.models.ClubTeamDto
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleMatchScope
|
||||
|
||||
object ScheduleLogic {
|
||||
|
||||
fun sortClubTeams(teams: List<ClubTeamDto>): List<ClubTeamDto> =
|
||||
teams.sortedWith(compareBy({ it.league?.name ?: "" }, { it.name }))
|
||||
|
||||
fun sortMatches(matches: List<ScheduleMatchDto>): List<ScheduleMatchDto> =
|
||||
matches.sortedWith(
|
||||
compareBy(
|
||||
{ it.date ?: "" },
|
||||
{ it.time ?: "" },
|
||||
{ it.homeTeam?.name ?: "" },
|
||||
{ it.guestTeam?.name ?: "" },
|
||||
),
|
||||
)
|
||||
|
||||
fun mergeUniqueMatches(a: List<ScheduleMatchDto>, b: List<ScheduleMatchDto>): List<ScheduleMatchDto> {
|
||||
val seen = LinkedHashSet<Int>()
|
||||
val out = ArrayList<ScheduleMatchDto>(a.size + b.size)
|
||||
for (m in a + b) {
|
||||
if (seen.add(m.id)) out.add(m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fun leagueTeamNames(matches: List<ScheduleMatchDto>): List<String> {
|
||||
val names = LinkedHashSet<String>()
|
||||
for (m in matches) {
|
||||
m.homeTeam?.name?.takeIf { it.isNotBlank() }?.let(names::add)
|
||||
m.guestTeam?.name?.takeIf { it.isNotBlank() }?.let(names::add)
|
||||
}
|
||||
return names.sorted()
|
||||
}
|
||||
|
||||
fun filterAdultLeagues(matches: List<ScheduleMatchDto>): List<ScheduleMatchDto> {
|
||||
val youth = Regex("""[JM]\d|jugend""", RegexOption.IGNORE_CASE)
|
||||
return matches.filter { m ->
|
||||
val leagueName = m.leagueDetails?.name ?: ""
|
||||
!youth.containsMatchIn(leagueName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ownTeamName Name des gewählten Vereins-Teams (ClubTeam), wie im Web `selectedTeam.name`.
|
||||
*/
|
||||
fun applyTeamMatchScope(
|
||||
ownMatches: List<ScheduleMatchDto>,
|
||||
allMatches: List<ScheduleMatchDto>,
|
||||
scope: ScheduleMatchScope,
|
||||
ownTeamName: String,
|
||||
otherTeamName: String,
|
||||
): List<ScheduleMatchDto> {
|
||||
val combined = sortMatches(mergeUniqueMatches(allMatches, ownMatches))
|
||||
return when (scope) {
|
||||
ScheduleMatchScope.All -> combined
|
||||
ScheduleMatchScope.Other -> {
|
||||
if (otherTeamName.isBlank()) emptyList()
|
||||
else combined.filter { m ->
|
||||
m.homeTeam?.name == otherTeamName || m.guestTeam?.name == otherTeamName
|
||||
}
|
||||
}
|
||||
ScheduleMatchScope.Own ->
|
||||
if (ownMatches.isNotEmpty()) sortMatches(ownMatches)
|
||||
else combined.filter { m ->
|
||||
m.homeTeam?.name == ownTeamName || m.guestTeam?.name == ownTeamName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun teamsWithLeague(teams: List<ClubTeamDto>): List<ClubTeamDto> =
|
||||
teams.filter { it.league != null && (it.league?.id ?: 0) > 0 }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.InternalTournamentDetailDto
|
||||
import de.tt_tagebuch.shared.api.models.InternalTournamentSummaryDto
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
|
||||
class TournamentsApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
/** Query [type]: `mini` nur Minimeisterschaften; sonst alle (clientseitig nach intern/offen filtern). */
|
||||
suspend fun listTournaments(clubId: Int, type: String? = null): List<InternalTournamentSummaryDto> {
|
||||
return client.http.get("/api/tournament/$clubId") {
|
||||
type?.takeIf { it.isNotBlank() }?.let { parameter("type", it) }
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun getTournament(clubId: Int, tournamentId: Int): InternalTournamentDetailDto {
|
||||
return client.http.get("/api/tournament/$clubId/$tournamentId").body()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class PendingUserDto(
|
||||
val id: Int = 0,
|
||||
val email: String = "",
|
||||
val firstName: String? = null,
|
||||
val lastName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PendingUserClubJoinDto(
|
||||
val id: Int? = null,
|
||||
val userId: Int = 0,
|
||||
val clubId: Int? = null,
|
||||
val user: PendingUserDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClubAccessDecisionBody(
|
||||
val clubid: Int,
|
||||
val userid: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AvailableRoleDto(
|
||||
val value: String,
|
||||
val label: String,
|
||||
val description: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PermissionResourceDto(
|
||||
val label: String = "",
|
||||
val actions: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PermissionUserRefDto(
|
||||
val id: Int = 0,
|
||||
val email: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClubPermissionMemberDto(
|
||||
val userId: Int,
|
||||
val user: PermissionUserRefDto? = null,
|
||||
val role: String = "",
|
||||
val isOwner: Boolean = false,
|
||||
val approved: Boolean? = true,
|
||||
val permissions: JsonObject? = null,
|
||||
val effectivePermissions: JsonObject? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateUserRoleBody(
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateUserApprovedBody(
|
||||
val approved: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateUserCustomPermissionsBody(
|
||||
val permissions: JsonObject,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiLogListRowDto(
|
||||
val id: Int,
|
||||
val userId: Int? = null,
|
||||
val method: String? = null,
|
||||
val path: String? = null,
|
||||
val statusCode: Int? = null,
|
||||
val executionTime: Int? = null,
|
||||
val errorMessage: String? = null,
|
||||
val logType: String? = null,
|
||||
val schedulerJobType: String? = null,
|
||||
val createdAt: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiLogsListPageDto(
|
||||
val logs: List<ApiLogListRowDto> = emptyList(),
|
||||
val total: Int = 0,
|
||||
val limit: Int = 0,
|
||||
val offset: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiLogsListEnvelopeDto(
|
||||
val success: Boolean = false,
|
||||
val data: ApiLogsListPageDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiLogDetailEnvelopeDto(
|
||||
val success: Boolean = false,
|
||||
val data: ApiLogDetailDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiLogDetailDto(
|
||||
val id: Int = 0,
|
||||
val userId: Int? = null,
|
||||
val method: String? = null,
|
||||
val path: String? = null,
|
||||
val statusCode: Int? = null,
|
||||
val executionTime: Int? = null,
|
||||
val errorMessage: String? = null,
|
||||
val logType: String? = null,
|
||||
val schedulerJobType: String? = null,
|
||||
val createdAt: String? = null,
|
||||
val ipAddress: String? = null,
|
||||
val userAgent: String? = null,
|
||||
)
|
||||
@@ -30,3 +30,54 @@ fun UserClubPermissions.canWriteMembers(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("members", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadSchedule(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("schedule", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteSchedule(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("schedule", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadApprovals(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("approvals", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteApprovals(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("approvals", "write")
|
||||
}
|
||||
|
||||
/** Lesen der Berechtigungsverwaltung (Modul `permissions` im Backend). */
|
||||
fun UserClubPermissions.canReadClubPermissions(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("permissions", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteClubPermissions(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("permissions", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadTeams(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("teams", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteTeams(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("teams", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadTournaments(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("tournaments", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteTournaments(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("tournaments", "write")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ClubTeamLeagueDto(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
val myTischtennisGroupId: String? = null,
|
||||
val association: String? = null,
|
||||
val groupname: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClubTeamSeasonDto(
|
||||
val season: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClubTeamDto(
|
||||
val id: Int,
|
||||
val name: String = "",
|
||||
val clubId: Int = 0,
|
||||
val leagueId: Int? = null,
|
||||
val seasonId: Int? = null,
|
||||
val myTischtennisTeamId: String? = null,
|
||||
val teamGender: String? = null,
|
||||
val teamAgeGroup: String? = null,
|
||||
val plannedLeagueName: String? = null,
|
||||
val league: ClubTeamLeagueDto? = null,
|
||||
val season: ClubTeamSeasonDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScheduleTeamNameDto(
|
||||
val name: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScheduleLocationDto(
|
||||
val name: String = "",
|
||||
val address: String = "",
|
||||
val city: String = "",
|
||||
val zip: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScheduleLeagueDetailsDto(
|
||||
val name: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScheduleMatchDto(
|
||||
val id: Int,
|
||||
val date: String? = null,
|
||||
val time: String? = null,
|
||||
val homeTeamId: Int? = null,
|
||||
val guestTeamId: Int? = null,
|
||||
val locationId: Int? = null,
|
||||
val leagueId: Int? = null,
|
||||
val code: String? = null,
|
||||
val homePin: String? = null,
|
||||
val guestPin: String? = null,
|
||||
val homeMatchPoints: Int = 0,
|
||||
val guestMatchPoints: Int = 0,
|
||||
val isCompleted: Boolean = false,
|
||||
val pdfUrl: String? = null,
|
||||
val playersReady: List<Int> = emptyList(),
|
||||
val playersPlanned: List<Int> = emptyList(),
|
||||
val playersPlayed: List<Int> = emptyList(),
|
||||
val homeTeam: ScheduleTeamNameDto? = null,
|
||||
val guestTeam: ScheduleTeamNameDto? = null,
|
||||
val location: ScheduleLocationDto? = null,
|
||||
val leagueDetails: ScheduleLeagueDetailsDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LeagueTableRowDto(
|
||||
val teamId: Int,
|
||||
val teamName: String = "",
|
||||
val setsWon: Int = 0,
|
||||
val setsLost: Int = 0,
|
||||
/** z. B. \"3:1\" */
|
||||
val matchPoints: String = "",
|
||||
val tablePoints: String = "",
|
||||
val pointRatio: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateMatchPlayersBody(
|
||||
val clubId: Int,
|
||||
val playersReady: List<Int> = emptyList(),
|
||||
val playersPlanned: List<Int> = emptyList(),
|
||||
val playersPlayed: List<Int> = emptyList(),
|
||||
)
|
||||
|
||||
enum class ScheduleMatchScope {
|
||||
Own,
|
||||
All,
|
||||
Other,
|
||||
}
|
||||
|
||||
enum class ScheduleViewMode {
|
||||
Team,
|
||||
Overall,
|
||||
Adult,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class InternalTournamentSummaryDto(
|
||||
val id: Int = 0,
|
||||
val name: String? = null,
|
||||
val date: String? = null,
|
||||
val allowsExternal: Boolean? = null,
|
||||
val miniChampionshipYear: Int? = null,
|
||||
val isDoublesTournament: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class InternalTournamentDetailDto(
|
||||
val id: Int = 0,
|
||||
val name: String? = null,
|
||||
val date: String? = null,
|
||||
val type: String? = null,
|
||||
val clubId: Int? = null,
|
||||
val winningSets: Int? = null,
|
||||
val allowsExternal: Boolean? = null,
|
||||
val miniChampionshipYear: Int? = null,
|
||||
val numberOfTables: Int? = null,
|
||||
val numberOfGroups: Int? = null,
|
||||
val advancingPerGroup: Int? = null,
|
||||
val isDoublesTournament: Boolean? = null,
|
||||
val bestOfEndroundSize: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OfficialTournamentListRowDto(
|
||||
val id: Int = 0,
|
||||
val clubId: Int? = null,
|
||||
val title: String? = null,
|
||||
val eventDate: String? = null,
|
||||
val organizer: String? = null,
|
||||
val host: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OfficialParticipationEntryDto(
|
||||
val memberId: Int? = null,
|
||||
val memberName: String? = null,
|
||||
val competitionId: Int? = null,
|
||||
val competitionName: String? = null,
|
||||
val placement: String? = null,
|
||||
val date: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OfficialParticipationBucketDto(
|
||||
val tournamentId: String? = null,
|
||||
val title: String? = null,
|
||||
val startDate: String? = null,
|
||||
val endDate: String? = null,
|
||||
val entries: List<OfficialParticipationEntryDto> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.ApiLogsApi
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogDetailDto
|
||||
import de.tt_tagebuch.shared.api.models.ApiLogListRowDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class ApiLogsState(
|
||||
val logs: List<ApiLogListRowDto> = emptyList(),
|
||||
val total: Int = 0,
|
||||
val offset: Int = 0,
|
||||
val limit: Int = 50,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class ApiLogsManager(
|
||||
private val apiLogsApi: ApiLogsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(ApiLogsState())
|
||||
val state: StateFlow<ApiLogsState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = ApiLogsState()
|
||||
}
|
||||
|
||||
suspend fun load(
|
||||
logType: String? = null,
|
||||
method: String? = null,
|
||||
statusCode: Int? = null,
|
||||
pathContains: String? = null,
|
||||
resetOffset: Boolean = false,
|
||||
offsetOverride: Int? = null,
|
||||
) {
|
||||
val offset = offsetOverride ?: if (resetOffset) 0 else _state.value.offset
|
||||
val limit = _state.value.limit
|
||||
_state.update { it.copy(isLoading = true, error = null, offset = offset) }
|
||||
try {
|
||||
val page = apiLogsApi.listLogs(
|
||||
limit = limit,
|
||||
offset = offset,
|
||||
logType = logType,
|
||||
method = method,
|
||||
statusCode = statusCode,
|
||||
pathContains = pathContains,
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
logs = page.logs,
|
||||
total = page.total,
|
||||
limit = page.limit.takeIf { l -> l > 0 } ?: limit,
|
||||
offset = page.offset,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = t.toUserMessage("Logs konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun nextPage(
|
||||
logType: String? = null,
|
||||
method: String? = null,
|
||||
statusCode: Int? = null,
|
||||
pathContains: String? = null,
|
||||
) {
|
||||
val s = _state.value
|
||||
if (s.offset + s.logs.size >= s.total) return
|
||||
load(logType, method, statusCode, pathContains, offsetOverride = s.offset + s.limit)
|
||||
}
|
||||
|
||||
suspend fun previousPage(
|
||||
logType: String? = null,
|
||||
method: String? = null,
|
||||
statusCode: Int? = null,
|
||||
pathContains: String? = null,
|
||||
) {
|
||||
val s = _state.value
|
||||
if (s.offset <= 0) return
|
||||
load(logType, method, statusCode, pathContains, offsetOverride = (s.offset - s.limit).coerceAtLeast(0))
|
||||
}
|
||||
|
||||
suspend fun fetchDetail(id: Int): ApiLogDetailDto? =
|
||||
try {
|
||||
apiLogsApi.getLog(id)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.TournamentsApi
|
||||
import de.tt_tagebuch.shared.api.models.InternalTournamentDetailDto
|
||||
import de.tt_tagebuch.shared.api.models.InternalTournamentSummaryDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
enum class ClubTournamentDisplayFilter {
|
||||
Internal,
|
||||
External,
|
||||
Mini,
|
||||
}
|
||||
|
||||
data class ClubInternalTournamentsState(
|
||||
val filter: ClubTournamentDisplayFilter = ClubTournamentDisplayFilter.Internal,
|
||||
val tournaments: List<InternalTournamentSummaryDto> = emptyList(),
|
||||
val selectedId: Int? = null,
|
||||
val detail: InternalTournamentDetailDto? = null,
|
||||
val isLoadingList: Boolean = false,
|
||||
val isLoadingDetail: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class ClubInternalTournamentsManager(
|
||||
private val tournamentsApi: TournamentsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(ClubInternalTournamentsState())
|
||||
val state: StateFlow<ClubInternalTournamentsState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = ClubInternalTournamentsState()
|
||||
}
|
||||
|
||||
fun setFilter(filter: ClubTournamentDisplayFilter) {
|
||||
_state.update { it.copy(filter = filter, selectedId = null, detail = null, error = null) }
|
||||
}
|
||||
|
||||
fun selectTournament(id: Int?) {
|
||||
_state.update { it.copy(selectedId = id, detail = null) }
|
||||
}
|
||||
|
||||
suspend fun loadList(clubId: Int) {
|
||||
val filter = _state.value.filter
|
||||
_state.update { it.copy(isLoadingList = true, error = null) }
|
||||
try {
|
||||
val raw = when (filter) {
|
||||
ClubTournamentDisplayFilter.Mini -> tournamentsApi.listTournaments(clubId, type = "mini")
|
||||
else -> tournamentsApi.listTournaments(clubId, type = null)
|
||||
}
|
||||
val list = when (filter) {
|
||||
ClubTournamentDisplayFilter.Mini -> raw
|
||||
ClubTournamentDisplayFilter.Internal ->
|
||||
raw.filter { it.miniChampionshipYear == null && it.allowsExternal != true }
|
||||
ClubTournamentDisplayFilter.External ->
|
||||
raw.filter { it.miniChampionshipYear == null && it.allowsExternal == true }
|
||||
}
|
||||
_state.update { it.copy(tournaments = list, isLoadingList = false, error = null) }
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoadingList = false,
|
||||
error = t.toUserMessage("Vereins-Turniere konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadDetail(clubId: Int, tournamentId: Int) {
|
||||
_state.update { it.copy(isLoadingDetail = true, error = null) }
|
||||
try {
|
||||
val d = tournamentsApi.getTournament(clubId, tournamentId)
|
||||
_state.update { it.copy(detail = d, isLoadingDetail = false, error = null) }
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoadingDetail = false,
|
||||
error = t.toUserMessage("Turnierdetails konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.OfficialTournamentsApi
|
||||
import de.tt_tagebuch.shared.api.models.OfficialParticipationBucketDto
|
||||
import de.tt_tagebuch.shared.api.models.OfficialTournamentListRowDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class OfficialTournamentsReadState(
|
||||
val tournaments: List<OfficialTournamentListRowDto> = emptyList(),
|
||||
val participationBuckets: List<OfficialParticipationBucketDto> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class OfficialTournamentsReadManager(
|
||||
private val api: OfficialTournamentsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(OfficialTournamentsReadState())
|
||||
val state: StateFlow<OfficialTournamentsReadState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = OfficialTournamentsReadState()
|
||||
}
|
||||
|
||||
suspend fun load(clubId: Int) {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val list = api.listForClub(clubId)
|
||||
val summary = api.listParticipationSummary(clubId)
|
||||
_state.update {
|
||||
it.copy(
|
||||
tournaments = list,
|
||||
participationBuckets = summary,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = t.toUserMessage("Offizielle Turniere konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.ClubApprovalsApi
|
||||
import de.tt_tagebuch.shared.api.models.ClubAccessDecisionBody
|
||||
import de.tt_tagebuch.shared.api.models.PendingUserClubJoinDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class PendingApprovalsState(
|
||||
val pending: List<PendingUserClubJoinDto> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class PendingApprovalsManager(
|
||||
private val clubApprovalsApi: ClubApprovalsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(PendingApprovalsState())
|
||||
val state: StateFlow<PendingApprovalsState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = PendingApprovalsState()
|
||||
}
|
||||
|
||||
suspend fun load(clubId: Int) {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val list = clubApprovalsApi.listPending(clubId)
|
||||
_state.update { it.copy(pending = list, isLoading = false, error = null) }
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(isLoading = false, error = t.toUserMessage("Freigaben konnten nicht geladen werden"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun approve(clubId: Int, userId: Int) {
|
||||
clubApprovalsApi.approve(ClubAccessDecisionBody(clubid = clubId, userid = userId))
|
||||
load(clubId)
|
||||
}
|
||||
|
||||
suspend fun reject(clubId: Int, userId: Int) {
|
||||
clubApprovalsApi.reject(ClubAccessDecisionBody(clubid = clubId, userid = userId))
|
||||
load(clubId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.PermissionsApi
|
||||
import de.tt_tagebuch.shared.api.models.AvailableRoleDto
|
||||
import de.tt_tagebuch.shared.api.models.ClubPermissionMemberDto
|
||||
import de.tt_tagebuch.shared.api.models.PermissionResourceDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class PermissionsAdminState(
|
||||
val members: List<ClubPermissionMemberDto> = emptyList(),
|
||||
val availableRoles: List<AvailableRoleDto> = emptyList(),
|
||||
val permissionStructure: Map<String, PermissionResourceDto> = emptyMap(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class PermissionsAdminManager(
|
||||
private val permissionsApi: PermissionsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(PermissionsAdminState())
|
||||
val state: StateFlow<PermissionsAdminState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = PermissionsAdminState()
|
||||
}
|
||||
|
||||
suspend fun load(clubId: Int) {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val roles = permissionsApi.listAvailableRoles()
|
||||
val structure = permissionsApi.getPermissionStructure()
|
||||
val members = permissionsApi.listClubMembersWithPermissions(clubId)
|
||||
_state.update {
|
||||
it.copy(
|
||||
availableRoles = roles,
|
||||
permissionStructure = structure,
|
||||
members = members,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = t.toUserMessage("Berechtigungen konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reloadMembers(clubId: Int) {
|
||||
try {
|
||||
val members = permissionsApi.listClubMembersWithPermissions(clubId, cacheBust = true)
|
||||
_state.update { it.copy(members = members) }
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
|
||||
suspend fun updateRole(clubId: Int, userId: Int, role: String) {
|
||||
permissionsApi.updateUserRole(clubId, userId, role)
|
||||
load(clubId)
|
||||
}
|
||||
|
||||
suspend fun updateApproved(clubId: Int, userId: Int, approved: Boolean) {
|
||||
permissionsApi.updateUserStatus(clubId, userId, approved)
|
||||
load(clubId)
|
||||
}
|
||||
|
||||
suspend fun saveCustomPermissions(clubId: Int, userId: Int, permissions: kotlinx.serialization.json.JsonObject) {
|
||||
permissionsApi.updateUserCustomPermissions(clubId, userId, permissions)
|
||||
load(clubId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package de.tt_tagebuch.shared.state
|
||||
|
||||
import de.tt_tagebuch.shared.api.ClubTeamsApi
|
||||
import de.tt_tagebuch.shared.api.MatchesApi
|
||||
import de.tt_tagebuch.shared.api.ScheduleLogic
|
||||
import de.tt_tagebuch.shared.api.models.ClubTeamDto
|
||||
import de.tt_tagebuch.shared.api.models.LeagueTableRowDto
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleMatchScope
|
||||
import de.tt_tagebuch.shared.api.models.ScheduleViewMode
|
||||
import de.tt_tagebuch.shared.api.models.UpdateMatchPlayersBody
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class ScheduleState(
|
||||
val viewMode: ScheduleViewMode = ScheduleViewMode.Team,
|
||||
val teams: List<ClubTeamDto> = emptyList(),
|
||||
val selectedTeamId: Int? = null,
|
||||
val ownMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val allMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val overallMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val leagueTable: List<LeagueTableRowDto> = emptyList(),
|
||||
val matchScope: ScheduleMatchScope = ScheduleMatchScope.Own,
|
||||
val otherTeamName: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val seasonId: Int? = null,
|
||||
) {
|
||||
val selectedTeam: ClubTeamDto?
|
||||
get() = teams.find { it.id == selectedTeamId }
|
||||
|
||||
val displayedMatches: List<ScheduleMatchDto>
|
||||
get() = when (viewMode) {
|
||||
ScheduleViewMode.Overall -> ScheduleLogic.sortMatches(overallMatches)
|
||||
ScheduleViewMode.Adult ->
|
||||
ScheduleLogic.sortMatches(ScheduleLogic.filterAdultLeagues(overallMatches))
|
||||
ScheduleViewMode.Team -> {
|
||||
val t = selectedTeam ?: return emptyList()
|
||||
ScheduleLogic.applyTeamMatchScope(
|
||||
ownMatches = ownMatches,
|
||||
allMatches = allMatches,
|
||||
scope = matchScope,
|
||||
ownTeamName = t.name,
|
||||
otherTeamName = otherTeamName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val leagueTeamOptions: List<String>
|
||||
get() = ScheduleLogic.leagueTeamNames(ScheduleLogic.mergeUniqueMatches(allMatches, ownMatches))
|
||||
}
|
||||
|
||||
class ScheduleManager(
|
||||
private val clubTeamsApi: ClubTeamsApi,
|
||||
private val matchesApi: MatchesApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(ScheduleState())
|
||||
val state: StateFlow<ScheduleState> = _state.asStateFlow()
|
||||
|
||||
fun clear() {
|
||||
_state.value = ScheduleState()
|
||||
}
|
||||
|
||||
suspend fun refresh(clubId: Int) {
|
||||
when (_state.value.viewMode) {
|
||||
ScheduleViewMode.Team -> {
|
||||
val team = _state.value.selectedTeam
|
||||
if (team != null && (team.league?.id ?: 0) > 0) {
|
||||
loadMatchesForTeam(clubId, team)
|
||||
} else {
|
||||
loadClubTeams(clubId)
|
||||
}
|
||||
}
|
||||
ScheduleViewMode.Overall -> loadOverallSchedule(clubId)
|
||||
ScheduleViewMode.Adult -> loadAdultSchedule(clubId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadClubTeams(clubId: Int) {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val raw = clubTeamsApi.listClubTeams(clubId, _state.value.seasonId)
|
||||
val sorted = ScheduleLogic.sortClubTeams(raw)
|
||||
val withLeague = ScheduleLogic.teamsWithLeague(sorted)
|
||||
val pick = withLeague.firstOrNull() ?: sorted.firstOrNull()
|
||||
_state.update {
|
||||
it.copy(
|
||||
teams = sorted,
|
||||
selectedTeamId = pick?.id,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
if (pick != null && (pick.league?.id ?: 0) > 0) {
|
||||
loadMatchesForTeam(clubId, pick)
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
viewMode = ScheduleViewMode.Team,
|
||||
ownMatches = emptyList(),
|
||||
allMatches = emptyList(),
|
||||
leagueTable = emptyList(),
|
||||
overallMatches = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = t.toUserMessage("Mannschaften konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun selectTeam(clubId: Int, teamId: Int) {
|
||||
val team = _state.value.teams.find { it.id == teamId } ?: return
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedTeamId = teamId,
|
||||
matchScope = ScheduleMatchScope.Own,
|
||||
otherTeamName = "",
|
||||
)
|
||||
}
|
||||
if ((team.league?.id ?: 0) > 0) {
|
||||
loadMatchesForTeam(clubId, team)
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
ownMatches = emptyList(),
|
||||
allMatches = emptyList(),
|
||||
leagueTable = emptyList(),
|
||||
overallMatches = emptyList(),
|
||||
viewMode = ScheduleViewMode.Team,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadMatchesForTeam(clubId: Int, team: ClubTeamDto) {
|
||||
val leagueId = team.league?.id ?: return
|
||||
_state.update { it.copy(isLoading = true, error = null, viewMode = ScheduleViewMode.Team) }
|
||||
try {
|
||||
val own = matchesApi.listMatchesForLeague(clubId, leagueId, "own")
|
||||
val all = matchesApi.listMatchesForLeague(clubId, leagueId, "all")
|
||||
val table = runCatching { matchesApi.leagueTable(clubId, leagueId) }.getOrElse { emptyList() }
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedTeamId = team.id,
|
||||
ownMatches = own,
|
||||
allMatches = all,
|
||||
overallMatches = emptyList(),
|
||||
leagueTable = table,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
ensureOtherTeamDefault()
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
ownMatches = emptyList(),
|
||||
allMatches = emptyList(),
|
||||
leagueTable = emptyList(),
|
||||
error = t.toUserMessage("Spiele konnten nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadOverallSchedule(clubId: Int) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = true,
|
||||
error = null,
|
||||
viewMode = ScheduleViewMode.Overall,
|
||||
matchScope = ScheduleMatchScope.Own,
|
||||
otherTeamName = "",
|
||||
selectedTeamId = null,
|
||||
ownMatches = emptyList(),
|
||||
allMatches = emptyList(),
|
||||
leagueTable = emptyList(),
|
||||
)
|
||||
}
|
||||
try {
|
||||
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId)
|
||||
_state.update { it.copy(overallMatches = matches, isLoading = false, error = null) }
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
overallMatches = emptyList(),
|
||||
error = t.toUserMessage("Gesamtspielplan konnte nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadAdultSchedule(clubId: Int) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = true,
|
||||
error = null,
|
||||
viewMode = ScheduleViewMode.Adult,
|
||||
matchScope = ScheduleMatchScope.Own,
|
||||
otherTeamName = "",
|
||||
selectedTeamId = null,
|
||||
ownMatches = emptyList(),
|
||||
allMatches = emptyList(),
|
||||
leagueTable = emptyList(),
|
||||
)
|
||||
}
|
||||
try {
|
||||
val matches = matchesApi.listMatchesForLeagues(clubId, _state.value.seasonId)
|
||||
_state.update { it.copy(overallMatches = matches, isLoading = false, error = null) }
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
overallMatches = emptyList(),
|
||||
error = t.toUserMessage("Erwachsenen-Spielplan konnte nicht geladen werden"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMatchScope(scope: ScheduleMatchScope) {
|
||||
_state.update { it.copy(matchScope = scope) }
|
||||
if (scope == ScheduleMatchScope.Other) {
|
||||
ensureOtherTeamDefault()
|
||||
} else {
|
||||
_state.update { it.copy(otherTeamName = "") }
|
||||
}
|
||||
}
|
||||
|
||||
fun setOtherTeamName(name: String) {
|
||||
_state.update { it.copy(otherTeamName = name) }
|
||||
}
|
||||
|
||||
private fun ensureOtherTeamDefault() {
|
||||
val options = _state.value.leagueTeamOptions
|
||||
if (_state.value.otherTeamName.isBlank() && options.isNotEmpty()) {
|
||||
_state.update { it.copy(otherTeamName = options.first()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMatchPlayers(clubId: Int, matchId: Int, ready: List<Int>, planned: List<Int>, played: List<Int>) {
|
||||
matchesApi.updateMatchPlayers(
|
||||
matchId,
|
||||
UpdateMatchPlayersBody(clubId = clubId, playersReady = ready, playersPlanned = planned, playersPlayed = played),
|
||||
)
|
||||
refresh(clubId)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user