feat(TrainingStats): enhance training statistics view with collapsible panels and localization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
- Refactored the TrainingStatsView to implement collapsible sections for better organization of training statistics. - Added new localization keys for training statistics panels in both German and English. - Updated the mobile app's TrainingStatsScreen to utilize the new collapsible panel structure, improving user experience. - Enhanced the MembersManager to merge training statistics into member data, providing a comprehensive view of member participation. - Introduced new API methods for quick updates and transfers of member data, streamlining member management processes.
This commit is contained in:
@@ -2,7 +2,9 @@ package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.Member
|
||||
import de.tt_tagebuch.shared.api.models.MemberQuickMutationResponse
|
||||
import de.tt_tagebuch.shared.api.models.MemberSetBody
|
||||
import de.tt_tagebuch.shared.api.models.MemberTransferRunBody
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.get
|
||||
@@ -14,6 +16,7 @@ import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
class MembersApi(
|
||||
private val client: AuthedHttpClient,
|
||||
@@ -28,6 +31,24 @@ class MembersApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateRatingsFromMyTischtennis(clubId: Int) {
|
||||
client.http.post("/api/clubmembers/update-ratings/$clubId")
|
||||
}
|
||||
|
||||
suspend fun quickUpdateTestMembership(clubId: Int, memberId: Int): MemberQuickMutationResponse {
|
||||
return client.http.post("/api/clubmembers/quick-update-test-membership/$clubId/$memberId").body()
|
||||
}
|
||||
|
||||
suspend fun quickUpdateMemberFormHandedOver(clubId: Int, memberId: Int): MemberQuickMutationResponse {
|
||||
return client.http.post("/api/clubmembers/quick-update-member-form/$clubId/$memberId").body()
|
||||
}
|
||||
|
||||
suspend fun transferMembers(clubId: Int, body: MemberTransferRunBody): JsonObject {
|
||||
return client.http.post("/api/clubmembers/transfer/$clubId") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun uploadMemberImage(clubId: Int, memberId: Int, imageBytes: ByteArray, makePrimary: Boolean = true) {
|
||||
client.http.post("/api/clubmembers/image/$clubId/$memberId") {
|
||||
if (makePrimary) {
|
||||
|
||||
@@ -107,3 +107,8 @@ fun UserClubPermissions.canReadStatistics(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("statistics", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteMyTischtennis(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("mytischtennis", "write")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ data class Member(
|
||||
val trainingParticipations: Int? = null,
|
||||
val notInTraining: Boolean? = null,
|
||||
val missedTrainingWeeks: Int? = null,
|
||||
/** Aus Training-Stats gemergt (aktive Mitglieder); leer wenn keine Zuordnung. */
|
||||
val trainingGroups: List<TrainingStatsTrainingGroup> = emptyList(),
|
||||
val contacts: List<MemberContactDto> = emptyList(),
|
||||
val images: List<MemberImageDto> = emptyList(),
|
||||
val primaryImageId: Int? = null,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MemberQuickMutationResponse(
|
||||
val success: Boolean? = null,
|
||||
val message: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
@@ -45,3 +45,16 @@ data class MemberTransferConfigSaveBody(
|
||||
val useBulkMode: Boolean = false,
|
||||
val bulkWrapperTemplate: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberTransferRunBody(
|
||||
val transferEndpoint: String,
|
||||
val transferMethod: String = "POST",
|
||||
val transferFormat: String = "json",
|
||||
val transferTemplate: String,
|
||||
val useBulkMode: Boolean = false,
|
||||
val bulkWrapperTemplate: String? = null,
|
||||
val loginEndpoint: String? = null,
|
||||
val loginFormat: String? = null,
|
||||
val loginCredentials: JsonObject? = null,
|
||||
)
|
||||
|
||||
@@ -3,24 +3,30 @@ package de.tt_tagebuch.shared.state
|
||||
import de.tt_tagebuch.shared.api.MemberActivitiesApi
|
||||
import de.tt_tagebuch.shared.api.MembersApi
|
||||
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.models.CreateTrainingTimeBody
|
||||
import de.tt_tagebuch.shared.api.models.Member
|
||||
import de.tt_tagebuch.shared.api.models.MemberActivityStatDto
|
||||
import de.tt_tagebuch.shared.api.models.MemberQuickMutationResponse
|
||||
import de.tt_tagebuch.shared.api.models.MemberLastParticipationDto
|
||||
import de.tt_tagebuch.shared.api.models.MemberSetBody
|
||||
import de.tt_tagebuch.shared.api.models.MemberTransferRunBody
|
||||
import de.tt_tagebuch.shared.api.models.TrainingGroupDto
|
||||
import de.tt_tagebuch.shared.api.models.TrainingStatsMember
|
||||
import de.tt_tagebuch.shared.api.models.TrainingTimeDto
|
||||
import de.tt_tagebuch.shared.api.models.UpdateTrainingTimeBody
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
class MembersManager(
|
||||
private val membersApi: MembersApi,
|
||||
private val trainingGroupsApi: TrainingGroupsApi,
|
||||
private val memberActivitiesApi: MemberActivitiesApi,
|
||||
private val trainingTimesApi: TrainingTimesApi,
|
||||
private val trainingStatsApi: TrainingStatsApi,
|
||||
) {
|
||||
private val _state = MutableStateFlow(MembersState())
|
||||
val state: StateFlow<MembersState> = _state.asStateFlow()
|
||||
@@ -29,19 +35,37 @@ class MembersManager(
|
||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||
try {
|
||||
val members = membersApi.listMembers(clubId)
|
||||
val merged = runCatching { trainingStatsApi.getStats(clubId) }
|
||||
.fold(
|
||||
onSuccess = { mergeTrainingStatsIntoMembers(members, it.members) },
|
||||
onFailure = { members },
|
||||
)
|
||||
.sortedWith(compareBy<Member> { it.lastName.lowercase() }.thenBy { it.firstName.lowercase() })
|
||||
_state.value = _state.value.copy(members = members, isLoading = false)
|
||||
_state.value = _state.value.copy(members = merged, isLoading = false)
|
||||
} catch (t: Throwable) {
|
||||
_state.value = _state.value.copy(isLoading = false, error = t.toUserMessage("Mitglieder konnten nicht geladen werden"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateRatingsFromMyTischtennis(clubId: Int) {
|
||||
membersApi.updateRatingsFromMyTischtennis(clubId)
|
||||
}
|
||||
|
||||
suspend fun quickUpdateTestMembership(clubId: Int, memberId: Int): MemberQuickMutationResponse =
|
||||
membersApi.quickUpdateTestMembership(clubId, memberId)
|
||||
|
||||
suspend fun quickUpdateMemberFormHandedOver(clubId: Int, memberId: Int): MemberQuickMutationResponse =
|
||||
membersApi.quickUpdateMemberFormHandedOver(clubId, memberId)
|
||||
|
||||
suspend fun transferMembers(clubId: Int, body: MemberTransferRunBody): JsonObject =
|
||||
membersApi.transferMembers(clubId, body)
|
||||
|
||||
suspend fun saveMember(clubId: Int, body: MemberSetBody) {
|
||||
membersApi.setMember(clubId, body)
|
||||
}
|
||||
|
||||
suspend fun uploadMemberPortrait(clubId: Int, memberId: Int, imageBytes: ByteArray) {
|
||||
membersApi.uploadMemberImage(clubId, memberId, imageBytes, makePrimary = true)
|
||||
suspend fun uploadMemberPortrait(clubId: Int, memberId: Int, imageBytes: ByteArray, makePrimary: Boolean = true) {
|
||||
membersApi.uploadMemberImage(clubId, memberId, imageBytes, makePrimary = makePrimary)
|
||||
}
|
||||
|
||||
suspend fun listTrainingGroups(clubId: Int): List<TrainingGroupDto> = trainingGroupsApi.listGroups(clubId)
|
||||
@@ -94,3 +118,27 @@ class MembersManager(
|
||||
_state.value = MembersState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeTrainingStatsIntoMembers(members: List<Member>, stats: List<TrainingStatsMember>): List<Member> {
|
||||
val byId = stats.associateBy { it.id }
|
||||
return members.map { m ->
|
||||
val s = byId[m.id]
|
||||
if (s == null) {
|
||||
m.copy(
|
||||
trainingParticipations = 0,
|
||||
missedTrainingWeeks = 0,
|
||||
notInTraining = false,
|
||||
lastTraining = null,
|
||||
trainingGroups = emptyList(),
|
||||
)
|
||||
} else {
|
||||
m.copy(
|
||||
trainingParticipations = s.participationTotal,
|
||||
lastTraining = s.lastTraining,
|
||||
notInTraining = s.notInTraining,
|
||||
missedTrainingWeeks = s.missedTrainingWeeks,
|
||||
trainingGroups = s.trainingGroups,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user