feat(Tournament): add official tournament participation feature
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s

- Introduced functionality to load and display official tournament participation events in the CalendarView.
- Updated the API client to fetch official tournament data, enhancing the event management capabilities.
- Added new UI elements to represent official tournaments, including visual indicators and event details.
- Enhanced the ClubManager to support fetching and managing official tournament data.
- Updated the mobile app's TODO list to reflect progress on tournament-related features.
This commit is contained in:
Torsten Schulz (local)
2026-05-12 23:52:54 +02:00
parent bea5facb7d
commit 57468f1efb
15 changed files with 931 additions and 11 deletions

View File

@@ -2,9 +2,11 @@ package de.tt_tagebuch.shared.api
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.models.Club
import de.tt_tagebuch.shared.api.models.UpdateClubSettingsBody
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import kotlinx.serialization.Serializable
@@ -28,6 +30,12 @@ class ClubsApi(
return client.http.get("/api/clubs/$clubId").body()
}
suspend fun updateClubSettings(clubId: Int, body: UpdateClubSettingsBody) {
client.http.put("/api/clubs/$clubId/settings") {
setBody(body)
}
}
suspend fun requestAccess(clubId: Int) {
client.http.get("/api/clubs/request/$clubId")
}

View File

@@ -0,0 +1,33 @@
package de.tt_tagebuch.shared.api
import de.tt_tagebuch.shared.api.http.ApiException
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.models.MemberTransferConfigEnvelope
import de.tt_tagebuch.shared.api.models.MemberTransferConfigSaveBody
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
class MemberTransferConfigApi(
private val client: AuthedHttpClient,
) {
suspend fun get(clubId: Int): MemberTransferConfigEnvelope? {
return try {
client.http.get("/api/member-transfer-config/$clubId").body()
} catch (e: ApiException) {
if (e.statusCode == 404) null else throw e
}
}
suspend fun save(clubId: Int, body: MemberTransferConfigSaveBody): MemberTransferConfigEnvelope {
return client.http.post("/api/member-transfer-config/$clubId") {
setBody(body)
}.body()
}
suspend fun delete(clubId: Int): MemberTransferConfigEnvelope {
return client.http.delete("/api/member-transfer-config/$clubId").body()
}
}

View File

@@ -2,9 +2,13 @@ package de.tt_tagebuch.shared.api
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.models.PredefinedActivityDto
import de.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
class PredefinedActivitiesApi(
private val client: AuthedHttpClient,
@@ -25,4 +29,16 @@ class PredefinedActivitiesApi(
suspend fun getById(id: Int): PredefinedActivityDto {
return client.http.get("/api/predefined-activities/$id").body()
}
suspend fun create(body: PredefinedActivityUpsertBody): PredefinedActivityDto {
return client.http.post("/api/predefined-activities") {
setBody(body)
}.body()
}
suspend fun update(id: Int, body: PredefinedActivityUpsertBody): PredefinedActivityDto {
return client.http.put("/api/predefined-activities/$id") {
setBody(body)
}.body()
}
}

View File

@@ -2,6 +2,26 @@ package de.tt_tagebuch.shared.api.models
import kotlinx.serialization.Serializable
@Serializable
data class MemberDataQualityRequirements(
val requireStreet: Boolean = true,
val requirePostalCode: Boolean = true,
val requireCity: Boolean = true,
val requirePhone: Boolean = true,
val requireEmail: Boolean = true,
)
@Serializable
data class UpdateClubSettingsBody(
val greetingText: String? = null,
val associationMemberNumber: String? = null,
val countryCode: String? = null,
val stateCode: String? = null,
val myTischtennisFedNickname: String? = null,
val autoFetchRankings: Boolean? = null,
val memberDataQualityRequirements: MemberDataQualityRequirements? = null,
)
@Serializable
data class Club(
val id: Int,
@@ -10,5 +30,8 @@ data class Club(
val associationMemberNumber: String? = null,
val myTischtennisFedNickname: String? = null,
val autoFetchRankings: Boolean? = null,
val countryCode: String? = null,
val stateCode: String? = null,
val memberDataQualityRequirements: MemberDataQualityRequirements? = null,
)

View File

@@ -81,3 +81,23 @@ fun UserClubPermissions.canWriteTournaments(): Boolean {
if (isOwner) return true
return permissions.boolAt("tournaments", "write")
}
fun UserClubPermissions.canReadClubSettings(): Boolean {
if (isOwner) return true
return permissions.boolAt("settings", "read")
}
fun UserClubPermissions.canWriteClubSettings(): Boolean {
if (isOwner) return true
return permissions.boolAt("settings", "write")
}
fun UserClubPermissions.canReadPredefinedActivities(): Boolean {
if (isOwner) return true
return permissions.boolAt("predefined_activities", "read")
}
fun UserClubPermissions.canWritePredefinedActivities(): Boolean {
if (isOwner) return true
return permissions.boolAt("predefined_activities", "write")
}

View File

@@ -0,0 +1,47 @@
package de.tt_tagebuch.shared.api.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class MemberTransferLoginCredentialsDto(
val username: String? = null,
)
@Serializable
data class MemberTransferConfigDto(
val id: Int? = null,
val clubId: Int? = null,
val server: String? = null,
val loginEndpoint: String? = null,
val loginFormat: String? = null,
val loginCredentials: MemberTransferLoginCredentialsDto? = null,
val transferEndpoint: String? = null,
val transferMethod: String? = null,
val transferFormat: String? = null,
val transferTemplate: String? = null,
val useBulkMode: Boolean? = null,
val bulkWrapperTemplate: String? = null,
)
@Serializable
data class MemberTransferConfigEnvelope(
val success: Boolean = false,
val config: MemberTransferConfigDto? = null,
val message: String? = null,
val error: String? = null,
)
@Serializable
data class MemberTransferConfigSaveBody(
val server: String,
val loginEndpoint: String? = null,
val loginFormat: String = "json",
val loginCredentials: JsonObject? = null,
val transferEndpoint: String,
val transferMethod: String = "POST",
val transferFormat: String = "json",
val transferTemplate: String,
val useBulkMode: Boolean = false,
val bulkWrapperTemplate: String? = null,
)

View File

@@ -13,6 +13,18 @@ data class PredefinedActivityDto(
val excludeFromStats: Boolean? = null,
)
@Serializable
data class PredefinedActivityUpsertBody(
val name: String? = null,
val code: String? = null,
val description: String? = null,
val duration: Int? = null,
val durationText: String? = null,
val imageLink: String? = null,
val drawingData: String? = null,
val excludeFromStats: Boolean? = null,
)
fun PredefinedActivityDto.displayLabel(): String {
val n = name?.trim().orEmpty()
if (n.isNotEmpty()) return n

View File

@@ -3,6 +3,7 @@ package de.tt_tagebuch.shared.state
import de.tt_tagebuch.shared.api.ClubsApi
import de.tt_tagebuch.shared.api.PermissionsApi
import de.tt_tagebuch.shared.api.models.Club
import de.tt_tagebuch.shared.api.models.UpdateClubSettingsBody
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -50,6 +51,10 @@ class ClubManager(
return clubsApi.getClub(clubId)
}
suspend fun updateClubSettings(clubId: Int, body: UpdateClubSettingsBody) {
clubsApi.updateClubSettings(clubId, body)
}
suspend fun selectClub(clubId: Int) {
_state.value = _state.value.copy(isLoading = true, error = null)
try {