diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/network/ApiService.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/network/ApiService.kt new file mode 100644 index 00000000..b2383ee5 --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/network/ApiService.kt @@ -0,0 +1,557 @@ +package de.trainingstagebuch.app.network + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import okhttp3.MultipartBody +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.Multipart +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.QueryMap + +@Suppress("LongMethod", "TooManyFunctions") +interface ApiService { + + // Auth & Session + @POST("auth/register") + suspend fun register(@Body request: RegisterRequest): BaseResponse + + @POST("auth/login") + suspend fun login(@Body request: LoginRequest): JsonObject + + @GET("auth/activate/{activationCode}") + suspend fun activate(@Path("activationCode") activationCode: String): BaseResponse + + @POST("auth/forgot-password") + suspend fun forgotPassword(@Body body: JsonObject): BaseResponse + + @POST("auth/reset-password") + suspend fun resetPassword(@Body body: JsonObject): BaseResponse + + @GET("session/status") + suspend fun getSessionStatus(): BaseResponse + + // Clubs + @GET("clubs") + suspend fun getClubs(): JsonElement + + @POST("clubs") + suspend fun createClub(@Body request: CreateClubRequest): JsonElement + + @GET("clubs/{clubId}") + suspend fun getClub(@Path("clubId") clubId: String): JsonElement + + @PUT("clubs/{clubId}/settings") + suspend fun updateClubSettings(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @GET("clubs/request/{clubId}") + suspend fun requestClubAccess(@Path("clubId") clubId: String): BaseResponse + + @GET("clubs/pending/{clubId}") + suspend fun getPendingApprovals(@Path("clubId") clubId: String): JsonElement + + @POST("clubs/approve") + suspend fun approveClubAccess(@Body body: JsonObject): BaseResponse + + @POST("clubs/reject") + suspend fun rejectClubAccess(@Body body: JsonObject): BaseResponse + + // Permissions + @GET("permissions/{clubId}") + suspend fun getPermissions(@Path("clubId") clubId: String): JsonElement + + @GET("permissions/roles/available") + suspend fun getAvailableRoles(): JsonElement + + @GET("permissions/structure/all") + suspend fun getPermissionStructure(): JsonElement + + @GET("permissions/{clubId}/members") + suspend fun getPermissionMembers(@Path("clubId") clubId: String, @Query("t") cacheBust: Long? = null): JsonElement + + @PUT("permissions/{clubId}/user/{userId}/role") + suspend fun updateUserRole(@Path("clubId") clubId: String, @Path("userId") userId: String, @Body body: JsonObject): BaseResponse + + @PUT("permissions/{clubId}/user/{userId}/status") + suspend fun updateUserStatus(@Path("clubId") clubId: String, @Path("userId") userId: String, @Body body: JsonObject): BaseResponse + + @PUT("permissions/{clubId}/user/{userId}/permissions") + suspend fun updateUserPermissions(@Path("clubId") clubId: String, @Path("userId") userId: String, @Body body: JsonObject): BaseResponse + + // Members + @GET("clubmembers/get/{clubId}/{showAll}") + suspend fun getClubMembers(@Path("clubId") clubId: String, @Path("showAll") showAll: Boolean): JsonElement + + @GET("clubmembers/notapproved/{clubId}") + suspend fun getNotApprovedMembers(@Path("clubId") clubId: String): JsonElement + + @POST("clubmembers/set/{clubId}") + suspend fun setClubMembers(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @POST("clubmembers/update-ratings/{clubId}") + suspend fun updateRatings(@Path("clubId") clubId: String): BaseResponse + + @POST("clubmembers/quick-update-test-membership/{clubId}/{memberId}") + suspend fun quickUpdateTestMembership(@Path("clubId") clubId: String, @Path("memberId") memberId: String): JsonElement + + @POST("clubmembers/quick-deactivate/{clubId}/{memberId}") + suspend fun quickDeactivateMember(@Path("clubId") clubId: String, @Path("memberId") memberId: String): JsonElement + + @Multipart + @POST("clubmembers/image/{clubId}/{memberId}") + suspend fun uploadClubMemberImage( + @Path("clubId") clubId: String, + @Path("memberId") memberId: String, + @Part image: MultipartBody.Part + ): JsonElement + + @POST("clubmembers/image/{clubId}/{memberId}/{imageId}/primary") + suspend fun setClubMemberPrimaryImage( + @Path("clubId") clubId: String, + @Path("memberId") memberId: String, + @Path("imageId") imageId: String + ): JsonElement + + @DELETE("clubmembers/image/{clubId}/{memberId}/{imageId}") + suspend fun deleteClubMemberImage( + @Path("clubId") clubId: String, + @Path("memberId") memberId: String, + @Path("imageId") imageId: String + ): JsonElement + + @GET("clubmembers/gallery/{clubId}") + suspend fun getClubMemberGallery(@Path("clubId") clubId: String, @Query("format") format: String = "json", @Query("size") size: Int? = null): JsonElement + + @GET("member-transfer-config/{clubId}") + suspend fun getMemberTransferConfig(@Path("clubId") clubId: String): JsonElement + + @POST("member-transfer-config/{clubId}") + suspend fun saveMemberTransferConfig(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @DELETE("member-transfer-config/{clubId}") + suspend fun deleteMemberTransferConfig(@Path("clubId") clubId: String): JsonElement + + @POST("clubmembers/transfer/{clubId}") + suspend fun transferClubMembers(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @GET("membernotes/{memberId}") + suspend fun getMemberNotes(@Path("memberId") memberId: String, @Query("clubId") clubId: String): JsonElement + + @POST("membernotes") + suspend fun addMemberNote(@Body body: JsonObject): JsonElement + + @HTTP(method = "DELETE", path = "membernotes/{noteId}", hasBody = true) + suspend fun deleteMemberNote(@Path("noteId") noteId: String, @Body body: JsonObject): JsonElement + + // Diary + @GET("diary/{clubId}") + suspend fun getDiaryDates(@Path("clubId") clubId: String): JsonElement + + @POST("diary/{clubId}") + suspend fun createDiaryDate(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @PUT("diary/{clubId}") + suspend fun updateDiaryTrainingTime(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @DELETE("diary/{clubId}/{dateId}") + suspend fun deleteDiaryDate(@Path("clubId") clubId: String, @Path("dateId") dateId: String): BaseResponse + + @GET("participants/{dateId}") + suspend fun getParticipants(@Path("dateId") dateId: String): JsonElement + + @POST("participants/add") + suspend fun addParticipant(@Body body: JsonObject): JsonElement + + @POST("participants/remove") + suspend fun removeParticipant(@Body body: JsonObject): BaseResponse + + @PUT("participants/{dateId}/{memberId}/group") + suspend fun updateParticipantGroup(@Path("dateId") dateId: String, @Path("memberId") memberId: String, @Body body: JsonObject): BaseResponse + + @GET("activities/{dateId}") + suspend fun getActivities(@Path("dateId") dateId: String): JsonElement + + @POST("activities/add") + suspend fun addActivity(@Body body: JsonObject): JsonElement + + @GET("group/{clubId}/{dateId}") + suspend fun getGroups(@Path("clubId") clubId: String, @Path("dateId") dateId: String): JsonElement + + @POST("group") + suspend fun createGroup(@Body body: JsonObject): BaseResponse + + @PUT("group/{groupId}") + suspend fun updateGroup(@Path("groupId") groupId: String, @Body body: JsonObject): BaseResponse + + @DELETE("group/{groupId}") + suspend fun deleteGroup(@Path("groupId") groupId: String): BaseResponse + + @HTTP(method = "DELETE", path = "group/{groupId}", hasBody = true) + suspend fun deleteGroupWithBody(@Path("groupId") groupId: String, @Body body: JsonObject): BaseResponse + + @GET("tags") + suspend fun getTags(): JsonElement + + @POST("tags") + suspend fun createTag(@Body body: JsonObject): JsonElement + + @POST("diary/tag/{clubId}/add-tag") + suspend fun addTagToDiaryDate(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @HTTP(method = "DELETE", path = "diary/{clubId}/tag", hasBody = true) + suspend fun deleteTagFromDiaryDate(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @GET("notes") + suspend fun getNotes(@QueryMap query: Map): JsonElement + + @GET("diarymember/{clubId}/note") + suspend fun getDiaryMemberNotes(@Path("clubId") clubId: String, @QueryMap query: Map): JsonElement + + @POST("diarymember/{clubId}/note") + suspend fun addDiaryMemberNote(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @DELETE("diarymember/{clubId}/note/{noteId}") + suspend fun deleteDiaryMemberNote(@Path("clubId") clubId: String, @Path("noteId") noteId: String): JsonElement + + // Diary activities + @GET("diary-date-activities/{clubId}/{dateId}") + suspend fun getDiaryDateActivities(@Path("clubId") clubId: String, @Path("dateId") dateId: String): JsonElement + + @POST("diary-date-activities/{clubId}") + suspend fun createDiaryDateActivity(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @PUT("diary-date-activities/{clubId}/{activityId}") + suspend fun updateDiaryDateActivity(@Path("clubId") clubId: String, @Path("activityId") activityId: String, @Body body: JsonObject): BaseResponse + + @PUT("diary-date-activities/{clubId}/{activityId}/order") + suspend fun updateDiaryDateActivityOrder(@Path("clubId") clubId: String, @Path("activityId") activityId: String, @Body body: JsonObject): BaseResponse + + @DELETE("diary-date-activities/{clubId}/{activityId}") + suspend fun deleteDiaryDateActivity(@Path("clubId") clubId: String, @Path("activityId") activityId: String): BaseResponse + + @POST("diary-date-activities/group") + suspend fun createGroupActivity(@Body body: JsonObject): BaseResponse + + @PUT("diary-date-activities/group/{clubId}/{groupActivityId}") + suspend fun updateGroupActivity(@Path("clubId") clubId: String, @Path("groupActivityId") groupActivityId: String, @Body body: JsonObject): BaseResponse + + @DELETE("diary-date-activities/group/{clubId}/{groupActivityId}") + suspend fun deleteGroupActivity(@Path("clubId") clubId: String, @Path("groupActivityId") groupActivityId: String): BaseResponse + + @GET("diary-member-activities/{clubId}/{diaryDateActivityId}") + suspend fun getDiaryMemberActivities( + @Path("clubId") clubId: String, + @Path("diaryDateActivityId") diaryDateActivityId: String + ): JsonElement + + @POST("diary-member-activities/{clubId}/{diaryDateActivityId}") + suspend fun setDiaryMemberActivities( + @Path("clubId") clubId: String, + @Path("diaryDateActivityId") diaryDateActivityId: String, + @Body body: JsonObject + ): BaseResponse + + @DELETE("diary-member-activities/{clubId}/{diaryDateActivityId}/{participantId}") + suspend fun deleteDiaryMemberActivity( + @Path("clubId") clubId: String, + @Path("diaryDateActivityId") diaryDateActivityId: String, + @Path("participantId") participantId: String + ): BaseResponse + + @POST("accident") + suspend fun createAccident(@Body body: JsonObject): BaseResponse + + @GET("accident/{clubId}/{dateId}") + suspend fun getAccidents( + @Path("clubId") clubId: String, + @Path("dateId") dateId: String + ): JsonElement + + @GET("member-activities/{clubId}/{memberId}") + suspend fun getMemberActivities( + @Path("clubId") clubId: String, + @Path("memberId") memberId: String, + @Query("period") period: String? = null + ): JsonElement + + @GET("member-activities/{clubId}/{memberId}/last-participations") + suspend fun getMemberLastParticipations( + @Path("clubId") clubId: String, + @Path("memberId") memberId: String, + @Query("limit") limit: Int = 3 + ): JsonElement + + @GET("predefined-activities") + suspend fun getPredefinedActivities(): JsonElement + + @GET("predefined-activities/search/query") + suspend fun searchPredefinedActivities( + @Query("q") query: String, + @Query("limit") limit: Int = 10 + ): JsonElement + + // Training + @GET("training-groups/{clubId}") + suspend fun getTrainingGroups(@Path("clubId") clubId: String): JsonElement + + @GET("training-groups/{clubId}/member/{memberId}") + suspend fun getMemberTrainingGroups(@Path("clubId") clubId: String, @Path("memberId") memberId: String): JsonElement + + @POST("training-groups/{clubId}") + suspend fun createTrainingGroup(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @PUT("training-groups/{clubId}/{groupId}") + suspend fun updateTrainingGroup(@Path("clubId") clubId: String, @Path("groupId") groupId: String, @Body body: JsonObject): BaseResponse + + @DELETE("training-groups/{clubId}/{groupId}") + suspend fun deleteTrainingGroup(@Path("clubId") clubId: String, @Path("groupId") groupId: String): BaseResponse + + @POST("training-groups/{clubId}/{groupId}/member/{memberId}") + suspend fun addMemberToTrainingGroup(@Path("clubId") clubId: String, @Path("groupId") groupId: String, @Path("memberId") memberId: String): BaseResponse + + @DELETE("training-groups/{clubId}/{groupId}/member/{memberId}") + suspend fun removeMemberFromTrainingGroup(@Path("clubId") clubId: String, @Path("groupId") groupId: String, @Path("memberId") memberId: String): BaseResponse + + @GET("training-times/{clubId}") + suspend fun getTrainingTimes(@Path("clubId") clubId: String): JsonElement + + @POST("training-times/{clubId}") + suspend fun createTrainingTime(@Path("clubId") clubId: String, @Body body: JsonObject): BaseResponse + + @PUT("training-times/{clubId}/{timeId}") + suspend fun updateTrainingTime(@Path("clubId") clubId: String, @Path("timeId") timeId: String, @Body body: JsonObject): BaseResponse + + @DELETE("training-times/{clubId}/{timeId}") + suspend fun deleteTrainingTime(@Path("clubId") clubId: String, @Path("timeId") timeId: String): BaseResponse + + @GET("training-stats/{clubId}") + suspend fun getTrainingStats(@Path("clubId") clubId: String): JsonElement + + // Match / Schedule + @GET("matches/leagues/{clubId}/matches") + suspend fun getLeagueMatches(@Path("clubId") clubId: String, @Query("seasonid") seasonId: String? = null): JsonElement + + @GET("matches/leagues/{clubId}/matches/{leagueId}") + suspend fun getMatchesForLeague(@Path("clubId") clubId: String, @Path("leagueId") leagueId: String): JsonElement + + @GET("matches/leagues/{clubId}/table/{leagueId}") + suspend fun getLeagueTable(@Path("clubId") clubId: String, @Path("leagueId") leagueId: String): JsonElement + + @POST("matches/leagues/{clubId}/table/{leagueId}/fetch") + suspend fun fetchLeagueTable(@Path("clubId") clubId: String, @Path("leagueId") leagueId: String): BaseResponse + + @PATCH("matches/{matchId}/players") + suspend fun updateMatchPlayers(@Path("matchId") matchId: String, @Body body: JsonObject): BaseResponse + + // Teams & documents + @GET("club-teams/club/{clubId}") + suspend fun getClubTeams(@Path("clubId") clubId: String, @Query("seasonid") seasonId: String? = null): JsonElement + + @GET("club-teams/leagues/{clubId}") + suspend fun getClubTeamLeagues(@Path("clubId") clubId: String, @Query("seasonid") seasonId: String? = null): JsonElement + + @POST("club-teams/club/{clubId}") + suspend fun createClubTeam(@Path("clubId") clubId: String, @Body body: JsonObject): JsonElement + + @PUT("club-teams/{clubTeamId}") + suspend fun updateClubTeam(@Path("clubTeamId") clubTeamId: String, @Body body: JsonObject): BaseResponse + + @DELETE("club-teams/{clubTeamId}") + suspend fun deleteClubTeam(@Path("clubTeamId") clubTeamId: String): BaseResponse + + @GET("team-documents/club-team/{clubTeamId}") + suspend fun getTeamDocuments(@Path("clubTeamId") clubTeamId: String): JsonElement + + @Multipart + @POST("team-documents/club-team/{clubTeamId}/upload") + suspend fun uploadTeamDocument(@Path("clubTeamId") clubTeamId: String, @retrofit2.http.Part file: MultipartBody.Part): JsonElement + + @POST("team-documents/{documentId}/parse") + suspend fun parseTeamDocument(@Path("documentId") documentId: String, @Query("leagueid") leagueId: String): JsonElement + + @GET("team-documents/{documentId}/download") + suspend fun downloadTeamDocument(@Path("documentId") documentId: String): JsonElement + + // Tournament + @GET("tournament/{clubId}") + suspend fun getTournaments(@Path("clubId") clubId: String, @Query("type") type: String? = null): JsonElement + + @GET("tournament/{clubId}/{tournamentId}") + suspend fun getTournament(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String): JsonElement + + @POST("tournament") + suspend fun createTournament(@Body body: JsonObject): BaseResponse + + @POST("tournament/mini") + suspend fun createMiniTournament(@Body body: JsonObject): BaseResponse + + @PUT("tournament/{clubId}/{tournamentId}") + suspend fun updateTournament(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String, @Body body: JsonObject): BaseResponse + + @POST("tournament/participant") + suspend fun addTournamentParticipant(@Body body: JsonObject): BaseResponse + + @HTTP(method = "DELETE", path = "tournament/participant", hasBody = true) + suspend fun removeTournamentParticipant(@Body body: JsonObject): BaseResponse + + @POST("tournament/participants") + suspend fun getTournamentParticipants(@Body body: JsonObject): JsonElement + + @POST("tournament/external-participant") + suspend fun addExternalTournamentParticipant(@Body body: JsonObject): BaseResponse + + @HTTP(method = "DELETE", path = "tournament/external-participant", hasBody = true) + suspend fun removeExternalTournamentParticipant(@Body body: JsonObject): BaseResponse + + @POST("tournament/external-participants") + suspend fun getExternalParticipants(@Body body: JsonObject): JsonElement + + @GET("tournament/groups") + suspend fun getTournamentGroups(@Query("clubId") clubId: String, @Query("tournamentId") tournamentId: String): JsonElement + + @PUT("tournament/groups") + suspend fun createTournamentGroups(@Body body: JsonObject): BaseResponse + + @POST("tournament/groups/create") + suspend fun createTournamentGroupsPerClass(@Body body: JsonObject): BaseResponse + + @POST("tournament/groups") + suspend fun fillTournamentGroups(@Body body: JsonObject): BaseResponse + + @POST("tournament/matches/create") + suspend fun createTournamentMatches(@Body body: JsonObject): BaseResponse + + @POST("tournament/match/result") + suspend fun addTournamentMatchResult(@Body body: JsonObject): BaseResponse + + @HTTP(method = "DELETE", path = "tournament/match/result", hasBody = true) + suspend fun deleteTournamentMatchResult(@Body body: JsonObject): BaseResponse + + @POST("tournament/match/finish") + suspend fun finishTournamentMatch(@Body body: JsonObject): BaseResponse + + @POST("tournament/match/reopen") + suspend fun reopenTournamentMatch(@Body body: JsonObject): BaseResponse + + @POST("tournament/knockout") + suspend fun startKnockout(@Body body: JsonObject): BaseResponse + + @HTTP(method = "DELETE", path = "tournament/matches/knockout", hasBody = true) + suspend fun deleteKnockoutMatches(@Body body: JsonObject): BaseResponse + + @POST("tournament/modus") + suspend fun setTournamentModus(@Body body: JsonObject): BaseResponse + + @GET("tournament/matches/{clubId}/{tournamentId}") + suspend fun getTournamentMatches(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String): JsonElement + + @GET("tournament/classes/{clubId}/{tournamentId}") + suspend fun getTournamentClasses(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String): JsonElement + + @POST("tournament/class/{clubId}/{tournamentId}") + suspend fun createTournamentClass(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String, @Body body: JsonObject): BaseResponse + + @PUT("tournament/class/{clubId}/{tournamentId}/{classId}") + suspend fun updateTournamentClass(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String, @Path("classId") classId: String, @Body body: JsonObject): BaseResponse + + @DELETE("tournament/class/{clubId}/{tournamentId}/{classId}") + suspend fun deleteTournamentClass(@Path("clubId") clubId: String, @Path("tournamentId") tournamentId: String, @Path("classId") classId: String): BaseResponse + + @GET("tournament/stages") + suspend fun getTournamentStages(@Query("clubId") clubId: String, @Query("tournamentId") tournamentId: String): JsonElement + + @PUT("tournament/stages") + suspend fun updateTournamentStages(@Body body: JsonObject): BaseResponse + + @POST("tournament/stages/advance") + suspend fun advanceTournamentStage(@Body body: JsonObject): BaseResponse + + // Official tournaments + @GET("official-tournaments/{clubId}") + suspend fun getOfficialTournaments(@Path("clubId") clubId: String): JsonElement + + @GET("official-tournaments/{clubId}/participations/summary") + suspend fun getOfficialTournamentSummary(@Path("clubId") clubId: String): JsonElement + + @GET("official-tournaments/{clubId}/{id}") + suspend fun getOfficialTournament(@Path("clubId") clubId: String, @Path("id") id: String): JsonElement + + @POST("official-tournaments/{clubId}/{id}/participation") + suspend fun upsertOfficialParticipation(@Path("clubId") clubId: String, @Path("id") id: String, @Body body: JsonObject): BaseResponse + + @POST("official-tournaments/{clubId}/{id}/status") + suspend fun updateOfficialStatus(@Path("clubId") clubId: String, @Path("id") id: String, @Body body: JsonObject): BaseResponse + + @DELETE("official-tournaments/{clubId}/{id}") + suspend fun deleteOfficialTournament(@Path("clubId") clubId: String, @Path("id") id: String): BaseResponse + + // MyTischtennis + @GET("mytischtennis/account") + suspend fun getMyTischtennisAccount(): JsonElement + + @GET("mytischtennis/status") + suspend fun getMyTischtennisStatus(): JsonElement + + @POST("mytischtennis/verify") + suspend fun verifyMyTischtennis(@Body body: JsonObject): JsonElement + + @POST("mytischtennis/account") + suspend fun upsertMyTischtennisAccount(@Body body: JsonObject): BaseResponse + + @DELETE("mytischtennis/account") + suspend fun deleteMyTischtennisAccount(): BaseResponse + + @GET("mytischtennis/update-history") + suspend fun getMyTischtennisUpdateHistory(): JsonElement + + @POST("mytischtennis/parse-url") + suspend fun parseMyTischtennisUrl(@Body body: JsonObject): JsonElement + + @POST("mytischtennis/configure-team") + suspend fun configureMyTischtennisTeam(@Body body: JsonObject): BaseResponse + + @POST("mytischtennis/configure-league") + suspend fun configureMyTischtennisLeague(@Body body: JsonObject): BaseResponse + + @POST("mytischtennis/fetch-team-data/async") + suspend fun startMyTischtennisFetchJob(@Body body: JsonObject): JsonElement + + @GET("mytischtennis/fetch-team-data/jobs/{jobId}") + suspend fun getMyTischtennisFetchJob(@Path("jobId") jobId: String): JsonElement + + // Logs + @GET("logs") + suspend fun getLogs(@QueryMap params: Map): JsonElement + + @GET("logs/{id}") + suspend fun getLogDetail(@Path("id") id: String): JsonElement + + @GET("logs/scheduler/last-executions") + suspend fun getSchedulerExecutions(@Query("clubId") clubId: String? = null): JsonElement + + // NuScore + @POST("nuscore/init-cookies/{code}") + suspend fun initNuscoreCookies(@Path("code") code: String): BaseResponse + + @GET("nuscore/meetinginfo/{code}") + suspend fun getNuscoreMeetingInfo(@Path("code") code: String): JsonElement + + @GET("nuscore/meetingdetails/{uuid}") + suspend fun getNuscoreMeetingDetails(@Path("uuid") uuid: String): JsonElement + + @PUT("nuscore/validate/{uuid}") + suspend fun validateNuscoreReport(@Path("uuid") uuid: String, @Body body: JsonObject): JsonElement + + @PUT("nuscore/submit/{uuid}") + suspend fun submitNuscoreReport(@Path("uuid") uuid: String, @Body body: JsonObject): JsonElement + + @POST("nuscore/broadcast-draft") + suspend fun broadcastNuscoreDraft(@Body body: JsonObject): BaseResponse +} diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/network/SocketModels.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/network/SocketModels.kt new file mode 100644 index 00000000..de1fa758 --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/network/SocketModels.kt @@ -0,0 +1,58 @@ +package de.trainingstagebuch.app.network + +import kotlinx.serialization.json.JsonElement +import de.trainingstagebuch.app.utils.SocketMessageUtils + +sealed interface SocketConnectionState { + data object Disconnected : SocketConnectionState + data object Connecting : SocketConnectionState + data class Connected(val attempts: Int) : SocketConnectionState + data class Reconnecting(val attempts: Int, val delayMs: Long) : SocketConnectionState + data class Failed(val reason: String?) : SocketConnectionState +} + +data class SocketEvent( + val type: SocketEventType, + val rawType: String, + val payload: JsonElement? +) + +enum class SocketEventType(val wireName: String) { + ParticipantAdded("participant:added"), + ParticipantRemoved("participant:removed"), + ParticipantUpdated("participant:updated"), + DiaryNoteAdded("diary:note:added"), + DiaryNoteUpdated("diary:note:updated"), + DiaryNoteDeleted("diary:note:deleted"), + DiaryTagAdded("diary:tag:added"), + DiaryTagRemoved("diary:tag:removed"), + DiaryDateUpdated("diary:date:updated"), + ActivityMemberAdded("activity-member:added"), + ActivityMemberRemoved("activity-member:removed"), + ActivityChanged("activity:changed"), + MemberChanged("member:changed"), + GroupChanged("group:changed"), + TournamentChanged("tournament:changed"), + ScheduleMatchUpdated("schedule:match:updated"), + ScheduleMatchReportSubmitted("schedule:match-report:submitted"), + Unknown("unknown"); + + companion object { + fun fromWireName(name: String): SocketEventType = + entries.firstOrNull { it.wireName == name } ?: Unknown + } +} + +object SocketEventParser { + fun parseSocketIoEvent(rawMessage: String): SocketEvent? { + return SocketMessageUtils.parseSocketIoEvent(rawMessage) + } + + fun buildAuthMessage(authCode: String, userId: String): String { + return SocketMessageUtils.buildAuthMessage(authCode, userId) + } + + fun buildJoinClubMessage(clubId: String): String { + return SocketMessageUtils.buildJoinClubMessage(clubId) + } +} diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/repository/RepositoryMappers.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/repository/RepositoryMappers.kt new file mode 100644 index 00000000..755bd89e --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/repository/RepositoryMappers.kt @@ -0,0 +1,442 @@ +package de.trainingstagebuch.app.repository + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import de.trainingstagebuch.app.utils.JsonSafeUtils + +internal object RepositoryMappers { + fun parseErrorPayload(raw: String): BackendError? { + val obj = JsonSafeUtils.parseObject(raw) ?: return null + return BackendError( + code = JsonSafeUtils.getString(obj, "code"), + message = JsonSafeUtils.getString(obj, "error", "message") + ) + } + + fun mapClubsToHomeData(clubs: JsonElement): HomeData { + val array = JsonSafeUtils.asArray(clubs) + val names = array.mapNotNull { element -> + (element as? JsonObject) + ?.get("name") + ?.let(JsonSafeUtils::getString) + ?.takeIf { it.isNotBlank() } + } + return HomeData( + clubCount = array.size, + clubNames = names + ) + } + + fun mapMembersToMemberItems(members: JsonElement): List { + val array = JsonSafeUtils.asArray(members) + return array.mapNotNull { element -> + val obj = element as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id") ?: return@mapNotNull null + val firstName = JsonSafeUtils.getString(obj, "firstName", "firstname").orEmpty().trim() + val lastName = JsonSafeUtils.getString(obj, "lastName", "lastname").orEmpty().trim() + val active = JsonSafeUtils.getString(obj, "active") + ?.equals("true", ignoreCase = true) + ?: true + val testMembership = JsonSafeUtils.getString(obj, "testMembership") + ?.equals("true", ignoreCase = true) + ?: false + val contacts = JsonSafeUtils.asArray(obj["contacts"]).mapNotNull { rawContact -> + val contactObj = rawContact as? JsonObject ?: return@mapNotNull null + val type = JsonSafeUtils.getString(contactObj, "type")?.lowercase() ?: return@mapNotNull null + val value = JsonSafeUtils.getString(contactObj, "value") ?: return@mapNotNull null + MemberContactItem( + type = type, + value = value + ) + } + + val displayName = listOf(firstName, lastName) + .filter { it.isNotBlank() } + .joinToString(" ") + .ifBlank { id } + + MemberItem( + id = id, + firstName = firstName, + lastName = lastName, + displayName = displayName, + active = active, + testMembership = testMembership, + contacts = contacts + ) + } + } + + fun mapMemberGalleryImages(gallery: JsonElement, memberId: String): List { + val array = JsonSafeUtils.asArray(gallery) + return array.mapNotNull { element -> + val obj = element as? JsonObject ?: return@mapNotNull null + val imageId = JsonSafeUtils.getString(obj, "imageId", "id", "_id") ?: return@mapNotNull null + val itemMemberId = JsonSafeUtils.getString(obj, "memberId") + ?: runCatching { + val memberObj = obj["member"] as? JsonObject + memberObj?.let { JsonSafeUtils.getString(it, "id") } + }.getOrNull() + ?: return@mapNotNull null + if (itemMemberId != memberId) return@mapNotNull null + + val url = JsonSafeUtils.getString(obj, "url", "imageUrl", "latestImageUrl", "path") + val isPrimary = JsonSafeUtils.getString(obj, "primary", "isPrimary") + ?.equals("true", ignoreCase = true) + ?: false + + MemberImageItem( + imageId = imageId, + memberId = itemMemberId, + url = url, + isPrimary = isPrimary + ) + }.sortedWith(compareByDescending { it.isPrimary }.thenBy { it.imageId }) + } + + fun mapTrainingGroups(groups: JsonElement): List { + return JsonSafeUtils.asArray(groups).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val name = JsonSafeUtils.getString(obj, "name") + ?: JsonSafeUtils.getString(obj, "title") + ?: id + TrainingGroupItem(id = id, name = name) + } + } + + fun mapMemberNotes(notes: JsonElement): List { + return JsonSafeUtils.asArray(notes).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val content = JsonSafeUtils.getString(obj, "content", "text", "note") + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return@mapNotNull null + val createdAt = JsonSafeUtils.getString(obj, "createdAt", "created_at", "date") + MemberNoteItem( + id = id, + content = content, + createdAt = createdAt + ) + } + } + + fun mapMemberTransferConfig(config: JsonObject): MemberTransferConfig { + val loginCredentialsObj = config["loginCredentials"] as? JsonObject + val additional = loginCredentialsObj + ?.entries + ?.filter { it.key != "username" && it.key != "password" } + ?.mapNotNull { entry -> + val value = JsonSafeUtils.getString(entry.value) + if (value.isNullOrBlank()) null else "${entry.key}:$value" + } + ?: emptyList() + + return MemberTransferConfig( + id = JsonSafeUtils.getString(config, "id"), + server = JsonSafeUtils.getString(config, "server").orEmpty(), + loginEndpoint = JsonSafeUtils.getString(config, "loginEndpoint"), + loginFormat = JsonSafeUtils.getString(config, "loginFormat").orEmpty().ifBlank { "json" }, + loginUsername = loginCredentialsObj?.let { JsonSafeUtils.getString(it, "username") }, + loginPassword = null, + loginAdditionalField1 = additional.getOrNull(0), + loginAdditionalField2 = additional.getOrNull(1), + transferEndpoint = JsonSafeUtils.getString(config, "transferEndpoint").orEmpty(), + transferMethod = JsonSafeUtils.getString(config, "transferMethod").orEmpty().ifBlank { "POST" }, + transferFormat = JsonSafeUtils.getString(config, "transferFormat").orEmpty().ifBlank { "json" }, + transferTemplate = JsonSafeUtils.getString(config, "transferTemplate").orEmpty(), + useBulkMode = JsonSafeUtils.getString(config, "useBulkMode")?.equals("true", ignoreCase = true) ?: false, + bulkWrapperTemplate = JsonSafeUtils.getString(config, "bulkWrapperTemplate") + ) + } + + fun mapMemberTransferSummary(response: JsonObject): MemberTransferSummary { + val transferred = numberOrStringInt(response, "transferred") + val total = numberOrStringInt(response, "total") + + val invalidMembers = JsonSafeUtils.asArray(response["invalidMembers"]) + .mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val memberObj = obj["member"] as? JsonObject + val firstName = memberObj?.let { JsonSafeUtils.getString(it, "firstName") }.orEmpty() + val lastName = memberObj?.let { JsonSafeUtils.getString(it, "lastName") }.orEmpty() + listOf(firstName, lastName).filter { it.isNotBlank() }.joinToString(" ").ifBlank { + memberObj?.let { JsonSafeUtils.getString(it, "id") } ?: "Unknown" + } + } + + val errors = JsonSafeUtils.asArray(response["errors"]) + .mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull JsonSafeUtils.getString(raw) + val member = JsonSafeUtils.getString(obj, "member").orEmpty() + val error = JsonSafeUtils.getString(obj, "error").orEmpty() + listOf(member, error).filter { it.isNotBlank() }.joinToString(": ").ifBlank { null } + } + + return MemberTransferSummary( + success = JsonSafeUtils.getString(response, "success")?.equals("true", ignoreCase = true) ?: false, + message = JsonSafeUtils.getString(response, "message") + ?: JsonSafeUtils.getString(response, "error"), + transferred = transferred, + total = total, + invalidMembers = invalidMembers, + errors = errors + ) + } + + fun mapClubDetails(club: JsonObject): ClubDetails { + val id = JsonSafeUtils.getString(club, "id", "_id") + val name = JsonSafeUtils.getString(club, "name").orEmpty() + val members = mapMembersToMemberItems(club["members"] ?: JsonArray(emptyList())) + return ClubDetails( + id = id, + name = name, + members = members + ) + } + + fun mapClubSettings(club: JsonObject): ClubSettingsData { + val greetingText = JsonSafeUtils.getString(club, "greetingText") + ?: (club["settings"] as? JsonObject)?.let { JsonSafeUtils.getString(it, "greetingText") } + ?: "" + val associationMemberNumber = JsonSafeUtils.getString(club, "associationMemberNumber") + ?: (club["settings"] as? JsonObject)?.let { JsonSafeUtils.getString(it, "associationMemberNumber") } + ?: "" + + return ClubSettingsData( + greetingText = greetingText, + associationMemberNumber = associationMemberNumber + ) + } + + fun mapClubTrainingGroups(groups: JsonElement): List { + return JsonSafeUtils.asArray(groups).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val name = JsonSafeUtils.getString(obj, "name")?.trim().orEmpty().ifBlank { id } + ClubTrainingGroup(id = id, name = name) + } + } + + fun mapClubTrainingTimeGroups(groups: JsonElement): List { + return JsonSafeUtils.asArray(groups).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val name = JsonSafeUtils.getString(obj, "name")?.trim().orEmpty().ifBlank { id } + val times = JsonSafeUtils.asArray(obj["trainingTimes"]).mapNotNull { rawTime -> + val timeObj = rawTime as? JsonObject ?: return@mapNotNull null + val timeId = JsonSafeUtils.getString(timeObj, "id", "_id") ?: return@mapNotNull null + val weekday = JsonSafeUtils.getString(timeObj, "weekday")?.toIntOrNull() ?: 0 + val startTime = JsonSafeUtils.getString(timeObj, "startTime").orEmpty() + val endTime = JsonSafeUtils.getString(timeObj, "endTime").orEmpty() + ClubTrainingTime( + id = timeId, + weekday = weekday, + startTime = startTime, + endTime = endTime + ) + } + ClubTrainingTimeGroup( + id = id, + name = name, + trainingTimes = times + ) + } + } + + fun mapPendingApprovals(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val userObj = (obj["user"] as? JsonObject) ?: obj + val id = JsonSafeUtils.getString(userObj, "id", "_id") ?: return@mapNotNull null + val firstName = JsonSafeUtils.getString(userObj, "firstName", "firstname").orEmpty() + val lastName = JsonSafeUtils.getString(userObj, "lastName", "lastname").orEmpty() + val email = JsonSafeUtils.getString(userObj, "email") + PendingApprovalUser( + id = id, + firstName = firstName, + lastName = lastName, + email = email + ) + } + } + + fun mapDiaryDates(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + mapDiaryDate(raw) + } + } + + fun mapDiaryDate(raw: JsonElement): DiaryDateEntry? { + val obj = raw as? JsonObject ?: return null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return null + val date = JsonSafeUtils.getString(obj, "date") ?: return null + val tags = JsonSafeUtils.asArray(obj["diaryTags"]).mapNotNull { rawTag -> + mapTag(rawTag) + } + return DiaryDateEntry( + id = id, + date = date, + trainingStart = JsonSafeUtils.getString(obj, "trainingStart"), + trainingEnd = JsonSafeUtils.getString(obj, "trainingEnd"), + tags = tags + ) + } + + fun mapDiaryParticipants(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val memberId = JsonSafeUtils.getString(obj, "memberId") ?: return@mapNotNull null + val groupId = JsonSafeUtils.getString(obj, "groupId") + DiaryParticipant(id = id, memberId = memberId, groupId = groupId) + } + } + + fun mapDiaryActivities(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + mapDiaryActivity(raw) + } + } + + fun mapDiaryActivity(raw: JsonElement): DiaryActivityItem? { + val obj = raw as? JsonObject ?: return null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return null + val description = JsonSafeUtils.getString(obj, "description", "activity") + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + return DiaryActivityItem(id = id, description = description) + } + + private fun numberOrStringInt(obj: JsonObject, key: String): Int { + return obj[key]?.let { element -> + JsonSafeUtils.getString(element)?.toIntOrNull() + ?: runCatching { element.toString().trim('"').toInt() }.getOrNull() + } ?: 0 + } + + fun mapTags(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + mapTag(raw) + } + } + + fun mapDiaryPlanItems(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val isTimeblock = JsonSafeUtils.getString(obj, "isTimeblock") + ?.equals("true", ignoreCase = true) + ?: false + val activity = JsonSafeUtils.getString(obj, "activity", "customActivityName") + ?: (obj["predefinedActivity"] as? JsonObject)?.let { + JsonSafeUtils.getString(it, "name", "code") + } + ?: "" + val duration = JsonSafeUtils.getString(obj, "duration") + val durationText = JsonSafeUtils.getString(obj, "durationText") + val groupId = JsonSafeUtils.getString(obj, "groupId") + val orderId = JsonSafeUtils.getString(obj, "orderId")?.toIntOrNull() + val predefinedActivityId = JsonSafeUtils.getString(obj, "predefinedActivityId") + ?: (obj["predefinedActivity"] as? JsonObject)?.let { + JsonSafeUtils.getString(it, "id", "_id") + } + DiaryPlanItem( + id = id, + activity = activity, + duration = duration, + durationText = durationText, + isTimeblock = isTimeblock, + groupId = groupId, + orderId = orderId, + predefinedActivityId = predefinedActivityId + ) + } + } + + fun mapDiaryGroups(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val name = JsonSafeUtils.getString(obj, "name") + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: id + val lead = JsonSafeUtils.getString(obj, "lead").orEmpty() + DiaryGroupItem(id = id, name = name, lead = lead) + } + } + + fun mapTag(raw: JsonElement): DiaryTagItem? { + val obj = raw as? JsonObject ?: return null + val nestedTag = obj["tag"] as? JsonObject + val source = nestedTag ?: obj + val id = JsonSafeUtils.getString(source, "id", "_id") ?: return null + val name = JsonSafeUtils.getString(source, "name", "label") + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: return null + return DiaryTagItem(id = id, name = name) + } + + fun mapParticipantIds(entries: JsonElement): Set { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + JsonSafeUtils.getString(obj, "participantId") + }.toSet() + } + + fun mapAccidents(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + DiaryAccidentItem( + id = id, + accident = JsonSafeUtils.getString(obj, "accident", "description").orEmpty(), + happenedAt = JsonSafeUtils.getString(obj, "createdAt", "date") + ) + } + } + + fun mapMemberActivityStats(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val activity = JsonSafeUtils.getString(obj, "activity", "name") ?: return@mapNotNull null + val count = JsonSafeUtils.getString(obj, "count", "value")?.toIntOrNull() ?: 0 + MemberActivityStatItem(activity = activity, count = count) + } + } + + fun mapLastParticipations(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val date = JsonSafeUtils.getString(obj, "date", "diaryDate") ?: return@mapNotNull null + val activity = JsonSafeUtils.getString(obj, "activity", "name").orEmpty() + MemberParticipationItem( + date = date, + activity = activity + ) + } + } + + fun mapPredefinedActivities(entries: JsonElement): List { + return JsonSafeUtils.asArray(entries).mapNotNull { raw -> + val obj = raw as? JsonObject ?: return@mapNotNull null + val id = JsonSafeUtils.getString(obj, "id", "_id") ?: return@mapNotNull null + val name = JsonSafeUtils.getString(obj, "name").orEmpty() + val code = JsonSafeUtils.getString(obj, "code") + val duration = JsonSafeUtils.getString(obj, "duration") + val durationText = JsonSafeUtils.getString(obj, "durationText") + PredefinedActivityItem( + id = id, + name = name, + code = code, + duration = duration, + durationText = durationText + ) + } + } +} diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/repository/TrainingstagebuchRepository.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/repository/TrainingstagebuchRepository.kt new file mode 100644 index 00000000..67819701 --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/repository/TrainingstagebuchRepository.kt @@ -0,0 +1,2159 @@ +package de.trainingstagebuch.app.repository + +import de.trainingstagebuch.app.network.ApiService +import de.trainingstagebuch.app.network.BaseResponse +import de.trainingstagebuch.app.network.CreateClubRequest +import de.trainingstagebuch.app.network.RegisterRequest +import de.trainingstagebuch.app.network.SocketManager +import de.trainingstagebuch.app.utils.AppErrorUtils +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import retrofit2.HttpException + +class TrainingstagebuchRepository( + private val apiService: ApiService +) { + suspend fun checkSession(): BaseResponse = apiService.getSessionStatus() + suspend fun getClubs(): JsonElement = apiService.getClubs() + suspend fun loadHomeData(): HomeResult { + return try { + val clubs = apiService.getClubs() + HomeResult.Success(RepositoryMappers.mapClubsToHomeData(clubs)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + HomeResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + HomeResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createClub(name: String): CreateClubResult { + return try { + val response = apiService.createClub(CreateClubRequest(name = name)) + val createdClubId = extractCreatedClubId(response) + CreateClubResult.Success(createdClubId = createdClubId) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + CreateClubResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + CreateClubResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadClub(clubId: String): ClubResult { + return try { + val response = apiService.getClub(clubId) + val obj = response as? JsonObject ?: return ClubResult.Error(message = AppErrorUtils.unknown()) + ClubResult.Success(data = RepositoryMappers.mapClubDetails(obj)) + } catch (e: HttpException) { + if (e.code() == 403) { + val raw = e.response()?.errorBody()?.string() + val parsedObj = raw?.let(de.trainingstagebuch.app.utils.JsonSafeUtils::parseObject) + val status = parsedObj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "status") } + val requested = status.equals("requested", ignoreCase = true) + val message = parsedObj?.let { + de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "message", "error") + } + return ClubResult.AccessDenied( + requested = requested, + message = message + ) + } + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun requestClubAccess(clubId: String): ClubAccessRequestResult { + return try { + val response = apiService.requestClubAccess(clubId = clubId) + if (response.success == false) { + ClubAccessRequestResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + ClubAccessRequestResult.Success(message = response.message) + } + } catch (e: HttpException) { + if (e.code() == 409) { + return ClubAccessRequestResult.AlreadyRequested( + message = AppErrorUtils.backendOrThrowable(null, e) + ) + } + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubAccessRequestResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubAccessRequestResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadPendingApprovals(clubId: String): PendingApprovalsResult { + return try { + val response = apiService.getPendingApprovals(clubId = clubId) + PendingApprovalsResult.Success(users = RepositoryMappers.mapPendingApprovals(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + PendingApprovalsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + PendingApprovalsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun approvePendingUser(clubId: String, userId: String): ClubActionResult { + return try { + val response = apiService.approveClubAccess( + buildJsonObject { + put("clubid", clubId) + put("userid", userId) + } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun rejectPendingUser(clubId: String, userId: String): ClubActionResult { + return try { + val response = apiService.rejectClubAccess( + buildJsonObject { + put("clubid", clubId) + put("userid", userId) + } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadClubSettings(clubId: String): ClubSettingsResult { + return try { + val response = apiService.getClub(clubId) + val obj = response as? JsonObject ?: return ClubSettingsResult.Error(message = AppErrorUtils.unknown()) + val settings = RepositoryMappers.mapClubSettings(obj) + ClubSettingsResult.Success(settings = settings) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubSettingsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubSettingsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun saveClubSettings( + clubId: String, + greetingText: String, + associationMemberNumber: String + ): ClubSettingsActionResult { + return try { + val response = apiService.updateClubSettings( + clubId = clubId, + body = buildJsonObject { + put("greetingText", greetingText) + put("associationMemberNumber", associationMemberNumber) + } + ) + if (response.success == false) { + ClubSettingsActionResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + ClubSettingsActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubSettingsActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubSettingsActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadClubTrainingGroups(clubId: String): ClubTrainingGroupsResult { + return try { + val response = apiService.getTrainingGroups(clubId = clubId) + ClubTrainingGroupsResult.Success(groups = RepositoryMappers.mapClubTrainingGroups(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubTrainingGroupsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubTrainingGroupsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createClubTrainingGroup(clubId: String, name: String): ClubActionResult { + return try { + val response = apiService.createTrainingGroup( + clubId = clubId, + body = buildJsonObject { put("name", name) } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteClubTrainingGroup(clubId: String, groupId: String): ClubActionResult { + return try { + val response = apiService.deleteTrainingGroup(clubId = clubId, groupId = groupId) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateClubTrainingGroup(clubId: String, groupId: String, name: String): ClubActionResult { + return try { + val response = apiService.updateTrainingGroup( + clubId = clubId, + groupId = groupId, + body = buildJsonObject { put("name", name) } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadClubTrainingTimes(clubId: String): ClubTrainingTimesResult { + return try { + val response = apiService.getTrainingTimes(clubId = clubId) + ClubTrainingTimesResult.Success(groups = RepositoryMappers.mapClubTrainingTimeGroups(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubTrainingTimesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubTrainingTimesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createClubTrainingTime( + clubId: String, + trainingGroupId: String, + weekday: Int, + startTime: String, + endTime: String + ): ClubActionResult { + return try { + val response = apiService.createTrainingTime( + clubId = clubId, + body = buildJsonObject { + put("trainingGroupId", trainingGroupId) + put("weekday", weekday) + put("startTime", startTime) + put("endTime", endTime) + } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteClubTrainingTime(clubId: String, timeId: String): ClubActionResult { + return try { + val response = apiService.deleteTrainingTime(clubId = clubId, timeId = timeId) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateClubTrainingTime( + clubId: String, + timeId: String, + weekday: Int, + startTime: String, + endTime: String + ): ClubActionResult { + return try { + val response = apiService.updateTrainingTime( + clubId = clubId, + timeId = timeId, + body = buildJsonObject { + put("weekday", weekday) + put("startTime", startTime) + put("endTime", endTime) + } + ) + if (response.success == false) { + ClubActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + ClubActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + ClubActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ClubActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadMembers(clubId: String, showAll: Boolean = true): MembersResult { + return try { + val members = apiService.getClubMembers(clubId = clubId, showAll = showAll) + MembersResult.Success(RepositoryMappers.mapMembersToMemberItems(members)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MembersResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + MembersResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadNotApprovedMembers(clubId: String): MembersResult { + return try { + val members = apiService.getNotApprovedMembers(clubId = clubId) + MembersResult.Success(RepositoryMappers.mapMembersToMemberItems(members)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MembersResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + MembersResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadDiaryDates(clubId: String): DiaryDatesResult { + return try { + val dates = apiService.getDiaryDates(clubId = clubId) + DiaryDatesResult.Success(dates = RepositoryMappers.mapDiaryDates(dates)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryDatesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryDatesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createDiaryDate( + clubId: String, + date: String, + trainingStart: String?, + trainingEnd: String? + ): DiaryCreateDateResult { + return try { + val response = apiService.createDiaryDate( + clubId = clubId, + body = buildJsonObject { + put("date", date) + put("trainingStart", trainingStart) + put("trainingEnd", trainingEnd) + } + ) + val created = RepositoryMappers.mapDiaryDate(response) + ?: (response as? JsonObject)?.get("data")?.let(RepositoryMappers::mapDiaryDate) + ?: return DiaryCreateDateResult.Error(code = null, message = AppErrorUtils.unknown()) + DiaryCreateDateResult.Success(entry = created) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryCreateDateResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryCreateDateResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateDiaryTrainingTime( + clubId: String, + dateId: String, + trainingStart: String?, + trainingEnd: String? + ): DiaryActionResult { + return try { + val response = apiService.updateDiaryTrainingTime( + clubId = clubId, + body = buildJsonObject { + put("dateId", dateId) + put("trainingStart", trainingStart) + put("trainingEnd", trainingEnd) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteDiaryDate(clubId: String, dateId: String): DiaryActionResult { + return try { + val response = apiService.deleteDiaryDate(clubId = clubId, dateId = dateId) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadParticipants(dateId: String): DiaryParticipantsResult { + return try { + val participants = apiService.getParticipants(dateId = dateId) + DiaryParticipantsResult.Success( + participants = RepositoryMappers.mapDiaryParticipants(participants) + ) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryParticipantsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryParticipantsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addParticipant(dateId: String, memberId: String): DiaryParticipantActionResult { + return try { + val response = apiService.addParticipant( + buildJsonObject { + put("diaryDateId", dateId) + put("memberId", memberId) + } + ) + val participant = RepositoryMappers.mapDiaryParticipants( + buildJsonArray { add(response) } + ).firstOrNull() + DiaryParticipantActionResult.Success(participant = participant) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryParticipantActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryParticipantActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun removeParticipant(dateId: String, memberId: String): DiaryActionResult { + return try { + val response = apiService.removeParticipant( + buildJsonObject { + put("diaryDateId", dateId) + put("memberId", memberId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadActivities(dateId: String): DiaryActivitiesResult { + return try { + val activities = apiService.getActivities(dateId = dateId) + DiaryActivitiesResult.Success(activities = RepositoryMappers.mapDiaryActivities(activities)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActivitiesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActivitiesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addActivity( + dateId: String, + description: String + ): DiaryAddActivityResult { + return try { + val response = apiService.addActivity( + buildJsonObject { + put("diaryDateId", dateId) + put("description", description) + put("tags", buildJsonArray { }) + } + ) + val created = RepositoryMappers.mapDiaryActivities(response).firstOrNull() + ?: RepositoryMappers.mapDiaryActivity(response) + if (created == null) { + DiaryAddActivityResult.Error(code = null, message = AppErrorUtils.unknown()) + } else { + DiaryAddActivityResult.Success(activity = created) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryAddActivityResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryAddActivityResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadDiaryMemberNotes( + clubId: String, + diaryDateId: String, + memberId: String + ): MemberNotesResult { + return try { + val notes = apiService.getDiaryMemberNotes( + clubId = clubId, + query = mapOf("diaryDateId" to diaryDateId, "memberId" to memberId) + ) + MemberNotesResult.Success(notes = RepositoryMappers.mapMemberNotes(notes)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberNotesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberNotesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addDiaryMemberNote( + clubId: String, + diaryDateId: String, + memberId: String, + content: String + ): DiaryActionResult { + return try { + apiService.addDiaryMemberNote( + clubId = clubId, + body = buildJsonObject { + put("memberId", memberId) + put("diaryDateId", diaryDateId) + put("content", content) + } + ) + DiaryActionResult.Success(message = null) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteDiaryMemberNote(clubId: String, noteId: String): DiaryActionResult { + return try { + apiService.deleteDiaryMemberNote(clubId = clubId, noteId = noteId) + DiaryActionResult.Success(message = null) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadTags(): DiaryTagsResult { + return try { + val tags = apiService.getTags() + DiaryTagsResult.Success(tags = RepositoryMappers.mapTags(tags)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryTagsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryTagsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createTag(name: String): DiaryCreateTagResult { + return try { + val response = apiService.createTag( + buildJsonObject { + put("name", name) + } + ) + val created = RepositoryMappers.mapTag(response) + ?: RepositoryMappers.mapTags(response).firstOrNull() + if (created == null) { + DiaryCreateTagResult.Error(code = null, message = AppErrorUtils.unknown()) + } else { + DiaryCreateTagResult.Success(tag = created) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryCreateTagResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryCreateTagResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addTagToDiaryDate(clubId: String, diaryDateId: String, tagId: String): DiaryActionResult { + return try { + val response = apiService.addTagToDiaryDate( + clubId = clubId, + body = buildJsonObject { + put("diaryDateId", diaryDateId) + put("tagId", tagId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun removeTagFromDiaryDate(clubId: String, diaryDateId: String, tagId: String): DiaryActionResult { + return try { + val response = apiService.deleteTagFromDiaryDate( + clubId = clubId, + body = buildJsonObject { + put("diaryDateId", diaryDateId) + put("tagId", tagId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadDiaryPlan(clubId: String, dateId: String): DiaryPlanResult { + return try { + val response = apiService.getDiaryDateActivities(clubId = clubId, dateId = dateId) + DiaryPlanResult.Success(items = RepositoryMappers.mapDiaryPlanItems(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryPlanResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryPlanResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addDiaryPlanItem( + clubId: String, + dateId: String, + activity: String, + orderId: Int + ): DiaryActionResult { + return try { + val response = apiService.createDiaryDateActivity( + clubId = clubId, + body = buildJsonObject { + put("diaryDateId", dateId) + put("activity", activity) + put("isTimeblock", false) + put("duration", "") + put("durationText", "") + put("orderId", orderId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateDiaryPlanItem( + clubId: String, + planItemId: String, + activity: String + ): DiaryActionResult { + return try { + val response = apiService.updateDiaryDateActivity( + clubId = clubId, + activityId = planItemId, + body = buildJsonObject { + put("customActivityName", activity) + put("duration", "") + put("durationText", "") + put("groupId", null) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteDiaryPlanItem(clubId: String, planItemId: String): DiaryActionResult { + return try { + val response = apiService.deleteDiaryDateActivity(clubId = clubId, activityId = planItemId) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateDiaryPlanItemOrder(clubId: String, planItemId: String, order: Int): DiaryActionResult { + return try { + val response = apiService.updateDiaryDateActivityOrder( + clubId = clubId, + activityId = planItemId, + body = buildJsonObject { + put("order", order) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadDiaryGroups(clubId: String, dateId: String): DiaryGroupsResult { + return try { + val response = apiService.getGroups(clubId = clubId, dateId = dateId) + DiaryGroupsResult.Success(groups = RepositoryMappers.mapDiaryGroups(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryGroupsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryGroupsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateParticipantGroup(dateId: String, memberId: String, groupId: String?): DiaryActionResult { + return try { + val response = apiService.updateParticipantGroup( + dateId = dateId, + memberId = memberId, + body = buildJsonObject { + put("groupId", groupId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createDiaryGroup(clubId: String, dateId: String, name: String, lead: String): DiaryActionResult { + return try { + val response = apiService.createGroup( + buildJsonObject { + put("clubid", clubId) + put("dateid", dateId) + put("name", name) + put("lead", lead) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun updateDiaryGroup(groupId: String, clubId: String, dateId: String, name: String, lead: String): DiaryActionResult { + return try { + val response = apiService.updateGroup( + groupId = groupId, + body = buildJsonObject { + put("clubid", clubId) + put("dateid", dateId) + put("name", name) + put("lead", lead) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteDiaryGroup(groupId: String, clubId: String, dateId: String): DiaryActionResult { + return try { + val response = apiService.deleteGroupWithBody( + groupId = groupId, + body = buildJsonObject { + put("clubid", clubId) + put("dateid", dateId) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createDiaryGroupActivity( + clubId: String, + dateId: String, + groupId: String, + activity: String, + predefinedActivityId: String? + ): DiaryActionResult { + return try { + val response = apiService.createGroupActivity( + buildJsonObject { + put("clubId", clubId) + put("diaryDateId", dateId) + put("groupId", groupId) + put("activity", activity) + if (!predefinedActivityId.isNullOrBlank()) { + put("predefinedActivityId", predefinedActivityId) + } + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadActivityParticipantIds(clubId: String, activityId: String): DiaryActivityParticipantsResult { + return try { + val response = apiService.getDiaryMemberActivities(clubId = clubId, diaryDateActivityId = activityId) + DiaryActivityParticipantsResult.Success(participantIds = RepositoryMappers.mapParticipantIds(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActivityParticipantsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActivityParticipantsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun assignActivityParticipants(clubId: String, activityId: String, participantIds: List): DiaryActionResult { + return try { + val response = apiService.setDiaryMemberActivities( + clubId = clubId, + diaryDateActivityId = activityId, + body = buildJsonObject { + put( + "participantIds", + buildJsonArray { + participantIds.forEach { id -> add(JsonPrimitive(id)) } + } + ) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun removeActivityParticipant(clubId: String, activityId: String, participantId: String): DiaryActionResult { + return try { + val response = apiService.deleteDiaryMemberActivity( + clubId = clubId, + diaryDateActivityId = activityId, + participantId = participantId + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun createAccident(clubId: String, dateId: String, accident: String): DiaryActionResult { + return try { + val response = apiService.createAccident( + buildJsonObject { + put("clubId", clubId) + put("diaryDateId", dateId) + put("accident", accident) + } + ) + if (response.success == false) { + DiaryActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + DiaryActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadAccidents(clubId: String, dateId: String): DiaryAccidentsResult { + return try { + val response = apiService.getAccidents(clubId = clubId, dateId = dateId) + DiaryAccidentsResult.Success(items = RepositoryMappers.mapAccidents(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + DiaryAccidentsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + DiaryAccidentsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadMemberActivityStats(clubId: String, memberId: String): MemberActivityStatsResult { + return try { + val stats = apiService.getMemberActivities(clubId = clubId, memberId = memberId, period = "all") + val last = apiService.getMemberLastParticipations(clubId = clubId, memberId = memberId, limit = 3) + MemberActivityStatsResult.Success( + stats = RepositoryMappers.mapMemberActivityStats(stats), + lastParticipations = RepositoryMappers.mapLastParticipations(last) + ) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberActivityStatsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberActivityStatsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun searchPredefinedActivities(term: String, limit: Int = 10): PredefinedActivitiesResult { + return try { + val response = apiService.searchPredefinedActivities(query = term, limit = limit) + PredefinedActivitiesResult.Success(items = RepositoryMappers.mapPredefinedActivities(response)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + PredefinedActivitiesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + PredefinedActivitiesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun saveMember( + clubId: String, + memberId: String?, + firstName: String, + lastName: String, + active: Boolean, + testMembership: Boolean, + contacts: List + ): MemberSaveResult { + return try { + val response = apiService.setClubMembers( + clubId = clubId, + body = buildJsonObject { + put("id", memberId) + put("firstname", firstName) + put("lastname", lastName) + put("active", active) + put("testMembership", testMembership) + put( + "contacts", + buildJsonArray { + contacts.forEach { contact -> + add( + buildJsonObject { + put("type", contact.type) + put("value", contact.value) + } + ) + } + } + ) + } + ) + val responseObj = response as? kotlinx.serialization.json.JsonObject + val code = responseObj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "code") } + val error = responseObj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "error") } + val message = responseObj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "message") } + if (!error.isNullOrBlank()) { + MemberSaveResult.Error(code = code, message = error) + } else { + MemberSaveResult.Success(message = message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberSaveResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberSaveResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadMemberImages(clubId: String, memberId: String): MemberImagesResult { + return try { + val gallery = apiService.getClubMemberGallery(clubId = clubId, format = "json") + MemberImagesResult.Success( + images = RepositoryMappers.mapMemberGalleryImages(gallery, memberId = memberId) + ) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberImagesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberImagesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun uploadMemberImage( + clubId: String, + memberId: String, + fileName: String, + bytes: ByteArray + ): MemberActionResult { + return runQuickMemberAction { + val requestBody = bytes.toRequestBody("image/*".toMediaType()) + val part = MultipartBody.Part.createFormData("image", fileName, requestBody) + apiService.uploadClubMemberImage(clubId = clubId, memberId = memberId, image = part) + } + } + + suspend fun deleteMemberImage( + clubId: String, + memberId: String, + imageId: String + ): MemberActionResult { + return runQuickMemberAction { + apiService.deleteClubMemberImage(clubId = clubId, memberId = memberId, imageId = imageId) + } + } + + suspend fun setPrimaryMemberImage( + clubId: String, + memberId: String, + imageId: String + ): MemberActionResult { + return runQuickMemberAction { + apiService.setClubMemberPrimaryImage(clubId = clubId, memberId = memberId, imageId = imageId) + } + } + + suspend fun loadMemberNotes(clubId: String, memberId: String): MemberNotesResult { + return try { + val notes = apiService.getMemberNotes(memberId = memberId, clubId = clubId) + MemberNotesResult.Success(notes = RepositoryMappers.mapMemberNotes(notes)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberNotesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberNotesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addMemberNote(clubId: String, memberId: String, content: String): MemberNotesResult { + return try { + val notes = apiService.addMemberNote( + buildJsonObject { + put("clubId", clubId) + put("memberId", memberId) + put("content", content) + } + ) + MemberNotesResult.Success(notes = RepositoryMappers.mapMemberNotes(notes)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberNotesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberNotesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteMemberNote(clubId: String, noteId: String): MemberNotesResult { + return try { + val notes = apiService.deleteMemberNote( + noteId = noteId, + body = buildJsonObject { put("clubId", clubId) } + ) + MemberNotesResult.Success(notes = RepositoryMappers.mapMemberNotes(notes)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberNotesResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberNotesResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadMemberTransferConfig(clubId: String): MemberTransferConfigResult { + return try { + val response = apiService.getMemberTransferConfig(clubId = clubId) as? JsonObject + ?: return MemberTransferConfigResult.Success(config = null) + val success = response.string("success")?.equals("true", ignoreCase = true) ?: false + if (!success) { + MemberTransferConfigResult.Error( + message = response.string("error") + ?: response.string("message") + ?: AppErrorUtils.unknown() + ) + } else { + val configObj = response["config"] as? JsonObject + MemberTransferConfigResult.Success(config = configObj?.let(RepositoryMappers::mapMemberTransferConfig)) + } + } catch (e: HttpException) { + if (e.code() == 404) return MemberTransferConfigResult.Success(config = null) + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTransferConfigResult.Error( + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberTransferConfigResult.Error(message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun saveMemberTransferConfig(clubId: String, input: MemberTransferConfigInput): MemberTransferActionResult { + return try { + val payload = buildJsonObject { + put("server", input.server) + if (input.loginEndpoint != null) put("loginEndpoint", input.loginEndpoint) + put("loginFormat", input.loginFormat) + if (!input.loginCredentials.isNullOrEmpty()) { + put( + "loginCredentials", + buildJsonObject { + input.loginCredentials.forEach { (key, value) -> + if (key.isNotBlank() && value.isNotBlank()) put(key, value) + } + } + ) + } + put("transferEndpoint", input.transferEndpoint) + put("transferMethod", input.transferMethod) + put("transferFormat", input.transferFormat) + put("transferTemplate", input.transferTemplate) + put("useBulkMode", input.useBulkMode) + if (input.bulkWrapperTemplate != null) put("bulkWrapperTemplate", input.bulkWrapperTemplate) + } + val response = apiService.saveMemberTransferConfig(clubId = clubId, body = payload) as? JsonObject + ?: return MemberTransferActionResult.Error(AppErrorUtils.unknown()) + val success = response.string("success")?.equals("true", ignoreCase = true) ?: false + if (!success) { + MemberTransferActionResult.Error( + response.string("error") ?: response.string("message") ?: AppErrorUtils.unknown() + ) + } else { + val configObj = response["config"] as? JsonObject + val configId = configObj?.string("id") + MemberTransferActionResult.Success( + message = response.string("message"), + configId = configId + ) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTransferActionResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + MemberTransferActionResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun deleteMemberTransferConfig(clubId: String): MemberTransferActionResult { + return try { + val response = apiService.deleteMemberTransferConfig(clubId = clubId) as? JsonObject + ?: return MemberTransferActionResult.Error(AppErrorUtils.unknown()) + val success = response.string("success")?.equals("true", ignoreCase = true) ?: false + if (!success) { + MemberTransferActionResult.Error( + response.string("error") ?: response.string("message") ?: AppErrorUtils.unknown() + ) + } else { + MemberTransferActionResult.Success(message = response.string("message"), configId = null) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTransferActionResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + MemberTransferActionResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun executeMemberTransfer(clubId: String, input: MemberTransferConfigInput): MemberTransferExecutionResult { + return try { + val payload = buildJsonObject { + if (input.loginEndpoint != null) put("loginEndpoint", input.loginEndpoint) + put("loginFormat", input.loginFormat) + if (!input.loginCredentials.isNullOrEmpty()) { + put( + "loginCredentials", + buildJsonObject { + input.loginCredentials.forEach { (key, value) -> + if (key.isNotBlank() && value.isNotBlank()) put(key, value) + } + } + ) + } + put("transferEndpoint", input.transferEndpoint) + put("transferMethod", input.transferMethod) + put("transferFormat", input.transferFormat) + put("transferTemplate", input.transferTemplate) + put("useBulkMode", input.useBulkMode) + if (input.bulkWrapperTemplate != null) put("bulkWrapperTemplate", input.bulkWrapperTemplate) + } + val response = apiService.transferClubMembers(clubId = clubId, body = payload) as? JsonObject + ?: return MemberTransferExecutionResult.Error(AppErrorUtils.unknown()) + val success = response.string("success")?.equals("true", ignoreCase = true) ?: false + val summary = RepositoryMappers.mapMemberTransferSummary(response) + if (success) { + MemberTransferExecutionResult.Success(summary) + } else { + MemberTransferExecutionResult.Error( + message = summary.message ?: response.string("error") ?: AppErrorUtils.unknown(), + summary = summary + ) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTransferExecutionResult.Error(AppErrorUtils.backendOrThrowable(parsed?.message, e)) + } catch (e: Exception) { + MemberTransferExecutionResult.Error(AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadTrainingGroups(clubId: String): MemberTrainingGroupsResult { + return try { + val groups = apiService.getTrainingGroups(clubId = clubId) + MemberTrainingGroupsResult.Success(groups = RepositoryMappers.mapTrainingGroups(groups)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTrainingGroupsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberTrainingGroupsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun loadMemberTrainingGroups(clubId: String, memberId: String): MemberTrainingGroupsResult { + return try { + val groups = apiService.getMemberTrainingGroups(clubId = clubId, memberId = memberId) + MemberTrainingGroupsResult.Success(groups = RepositoryMappers.mapTrainingGroups(groups)) + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberTrainingGroupsResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberTrainingGroupsResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun addMemberToTrainingGroup( + clubId: String, + memberId: String, + groupId: String + ): MemberActionResult { + return try { + val response = apiService.addMemberToTrainingGroup( + clubId = clubId, + groupId = groupId, + memberId = memberId + ) + if (response.success == false) { + MemberActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + MemberActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun removeMemberFromTrainingGroup( + clubId: String, + memberId: String, + groupId: String + ): MemberActionResult { + return try { + val response = apiService.removeMemberFromTrainingGroup( + clubId = clubId, + groupId = groupId, + memberId = memberId + ) + if (response.success == false) { + MemberActionResult.Error(code = response.code, message = response.error ?: response.message) + } else { + MemberActionResult.Success(message = response.message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun quickRemoveTestMembership(clubId: String, memberId: String): MemberActionResult { + return runQuickMemberAction { + apiService.quickUpdateTestMembership(clubId = clubId, memberId = memberId) + } + } + + suspend fun quickDeactivateMember(clubId: String, memberId: String): MemberActionResult { + return runQuickMemberAction { + apiService.quickDeactivateMember(clubId = clubId, memberId = memberId) + } + } + + private suspend fun runQuickMemberAction(call: suspend () -> JsonElement): MemberActionResult { + return try { + val response = call() + val obj = response as? JsonObject + val success = obj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "success") } + ?.equals("true", ignoreCase = true) + ?: true + val code = obj?.let { de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "code") } + val message = obj?.let { + de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "message", "error") + } + if (success) { + MemberActionResult.Success(message) + } else { + MemberActionResult.Error(code = code, message = message) + } + } catch (e: HttpException) { + val parsed = e.response()?.errorBody()?.string()?.let(RepositoryMappers::parseErrorPayload) + MemberActionResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + MemberActionResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun login(email: String, password: String): LoginResult { + return try { + val response = apiService.login( + de.trainingstagebuch.app.network.LoginRequest( + email = email, + password = password + ) + ) + val token = response["token"]?.jsonPrimitive?.contentOrNull + if (token.isNullOrBlank()) { + val code = response["code"]?.jsonPrimitive?.contentOrNull + val message = response["error"]?.jsonPrimitive?.contentOrNull + ?: response["message"]?.jsonPrimitive?.contentOrNull + LoginResult.Error(code = code, message = message) + } else { + SocketManager.requestConnect() + LoginResult.Success(token = token) + } + } catch (e: HttpException) { + val rawError = e.response()?.errorBody()?.string() + val parsed = rawError?.let(RepositoryMappers::parseErrorPayload) + LoginResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + LoginResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun register(email: String, password: String): RegisterResult { + return try { + val response = apiService.register(RegisterRequest(email = email, password = password)) + if (response.success == false) { + RegisterResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + RegisterResult.Success(message = response.message) + } + } catch (e: HttpException) { + val rawError = e.response()?.errorBody()?.string() + val parsed = rawError?.let(RepositoryMappers::parseErrorPayload) + RegisterResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + RegisterResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun activate(activationCode: String): ActivateResult { + return try { + val response = apiService.activate(activationCode) + if (response.success == false) { + ActivateResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + ActivateResult.Success(message = response.message) + } + } catch (e: HttpException) { + val rawError = e.response()?.errorBody()?.string() + val parsed = rawError?.let(RepositoryMappers::parseErrorPayload) + ActivateResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ActivateResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun forgotPassword(email: String): ForgotPasswordResult { + return try { + val response = apiService.forgotPassword( + buildJsonObject { put("email", email) } + ) + if (response.success == false) { + ForgotPasswordResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + ForgotPasswordResult.Success(message = response.message) + } + } catch (e: HttpException) { + val rawError = e.response()?.errorBody()?.string() + val parsed = rawError?.let(RepositoryMappers::parseErrorPayload) + ForgotPasswordResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ForgotPasswordResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + suspend fun resetPassword(token: String, password: String): ResetPasswordResult { + return try { + val response = apiService.resetPassword( + buildJsonObject { + put("token", token) + put("password", password) + } + ) + if (response.success == false) { + ResetPasswordResult.Error( + code = response.code, + message = response.error ?: response.message + ) + } else { + ResetPasswordResult.Success(message = response.message) + } + } catch (e: HttpException) { + val rawError = e.response()?.errorBody()?.string() + val parsed = rawError?.let(RepositoryMappers::parseErrorPayload) + ResetPasswordResult.Error( + code = parsed?.code, + message = AppErrorUtils.backendOrThrowable(parsed?.message, e) + ) + } catch (e: Exception) { + ResetPasswordResult.Error(code = null, message = AppErrorUtils.fromThrowable(e)) + } + } + + fun disconnectSocketOnLogout() { + SocketManager.requestDisconnect() + } + + private fun extractCreatedClubId(response: JsonElement): String? { + val obj = response as? JsonObject + if (obj == null) { + return response.jsonPrimitive.contentOrNull?.trim()?.ifBlank { null } + } + + val dataElement = obj["data"] + val fromDataElement = dataElement?.jsonPrimitive?.contentOrNull?.trim()?.ifBlank { null } + if (!fromDataElement.isNullOrBlank()) return fromDataElement + + val dataObject = dataElement as? JsonObject + val fromDataObject = dataObject?.let { + de.trainingstagebuch.app.utils.JsonSafeUtils.getString(it, "id", "clubId") + }?.trim()?.ifBlank { null } + if (!fromDataObject.isNullOrBlank()) return fromDataObject + + return de.trainingstagebuch.app.utils.JsonSafeUtils.getString(obj, "id", "clubId") + ?.trim() + ?.ifBlank { null } + } + + private fun JsonObject.string(key: String): String? { + return de.trainingstagebuch.app.utils.JsonSafeUtils.getString(this, key) + } + +} + +sealed interface LoginResult { + data class Success(val token: String) : LoginResult + data class Error(val code: String?, val message: String?) : LoginResult +} + +sealed interface RegisterResult { + data class Success(val message: String?) : RegisterResult + data class Error(val code: String?, val message: String?) : RegisterResult +} + +sealed interface HomeResult { + data class Success(val data: HomeData) : HomeResult + data class Error(val message: String) : HomeResult +} + +sealed interface CreateClubResult { + data class Success(val createdClubId: String?) : CreateClubResult + data class Error(val code: String?, val message: String?) : CreateClubResult +} + +sealed interface ClubResult { + data class Success(val data: ClubDetails) : ClubResult + data class AccessDenied(val requested: Boolean, val message: String?) : ClubResult + data class Error(val code: String? = null, val message: String) : ClubResult +} + +sealed interface ClubAccessRequestResult { + data class Success(val message: String?) : ClubAccessRequestResult + data class AlreadyRequested(val message: String?) : ClubAccessRequestResult + data class Error(val code: String?, val message: String?) : ClubAccessRequestResult +} + +sealed interface ClubSettingsResult { + data class Success(val settings: ClubSettingsData) : ClubSettingsResult + data class Error(val code: String? = null, val message: String) : ClubSettingsResult +} + +sealed interface ClubSettingsActionResult { + data class Success(val message: String?) : ClubSettingsActionResult + data class Error(val code: String?, val message: String?) : ClubSettingsActionResult +} + +sealed interface PendingApprovalsResult { + data class Success(val users: List) : PendingApprovalsResult + data class Error(val code: String?, val message: String?) : PendingApprovalsResult +} + +sealed interface ClubTrainingGroupsResult { + data class Success(val groups: List) : ClubTrainingGroupsResult + data class Error(val code: String?, val message: String?) : ClubTrainingGroupsResult +} + +sealed interface ClubTrainingTimesResult { + data class Success(val groups: List) : ClubTrainingTimesResult + data class Error(val code: String?, val message: String?) : ClubTrainingTimesResult +} + +sealed interface ClubActionResult { + data class Success(val message: String?) : ClubActionResult + data class Error(val code: String?, val message: String?) : ClubActionResult +} + +sealed interface ActivateResult { + data class Success(val message: String?) : ActivateResult + data class Error(val code: String?, val message: String?) : ActivateResult +} + +sealed interface ForgotPasswordResult { + data class Success(val message: String?) : ForgotPasswordResult + data class Error(val code: String?, val message: String?) : ForgotPasswordResult +} + +sealed interface ResetPasswordResult { + data class Success(val message: String?) : ResetPasswordResult + data class Error(val code: String?, val message: String?) : ResetPasswordResult +} + +data class HomeData( + val clubCount: Int, + val clubNames: List +) + +data class ClubDetails( + val id: String?, + val name: String, + val members: List +) + +data class ClubSettingsData( + val greetingText: String, + val associationMemberNumber: String +) + +data class ClubTrainingGroup( + val id: String, + val name: String +) + +data class ClubTrainingTimeGroup( + val id: String, + val name: String, + val trainingTimes: List +) + +data class ClubTrainingTime( + val id: String, + val weekday: Int, + val startTime: String, + val endTime: String +) + +data class PendingApprovalUser( + val id: String, + val firstName: String, + val lastName: String, + val email: String? +) + +data class MemberItem( + val id: String, + val firstName: String, + val lastName: String, + val displayName: String, + val active: Boolean, + val testMembership: Boolean, + val contacts: List = emptyList() +) + +data class MemberImageItem( + val imageId: String, + val memberId: String, + val url: String?, + val isPrimary: Boolean +) + +data class MemberContactItem( + val type: String, + val value: String +) + +data class TrainingGroupItem( + val id: String, + val name: String +) + +data class DiaryDateEntry( + val id: String, + val date: String, + val trainingStart: String?, + val trainingEnd: String?, + val tags: List = emptyList() +) + +data class DiaryParticipant( + val id: String, + val memberId: String, + val groupId: String? +) + +data class DiaryActivityItem( + val id: String, + val description: String +) + +data class DiaryTagItem( + val id: String, + val name: String +) + +data class DiaryPlanItem( + val id: String, + val activity: String, + val duration: String?, + val durationText: String?, + val isTimeblock: Boolean, + val groupId: String? = null, + val orderId: Int? = null, + val predefinedActivityId: String? = null +) + +data class DiaryGroupItem( + val id: String, + val name: String, + val lead: String +) + +data class MemberNoteItem( + val id: String, + val content: String, + val createdAt: String? +) + +data class DiaryAccidentItem( + val id: String, + val accident: String, + val happenedAt: String? +) + +data class MemberActivityStatItem( + val activity: String, + val count: Int +) + +data class MemberParticipationItem( + val date: String, + val activity: String +) + +data class PredefinedActivityItem( + val id: String, + val name: String, + val code: String?, + val duration: String?, + val durationText: String? +) + +data class MemberTransferConfig( + val id: String?, + val server: String, + val loginEndpoint: String?, + val loginFormat: String, + val loginUsername: String?, + val loginPassword: String?, + val loginAdditionalField1: String?, + val loginAdditionalField2: String?, + val transferEndpoint: String, + val transferMethod: String, + val transferFormat: String, + val transferTemplate: String, + val useBulkMode: Boolean, + val bulkWrapperTemplate: String? +) + +data class MemberTransferConfigInput( + val server: String, + val loginEndpoint: String?, + val loginFormat: String, + val loginCredentials: Map?, + val transferEndpoint: String, + val transferMethod: String, + val transferFormat: String, + val transferTemplate: String, + val useBulkMode: Boolean, + val bulkWrapperTemplate: String? +) + +data class MemberTransferSummary( + val success: Boolean, + val message: String?, + val transferred: Int, + val total: Int, + val invalidMembers: List, + val errors: List +) + +sealed interface MembersResult { + data class Success(val members: List) : MembersResult + data class Error(val message: String) : MembersResult +} + +sealed interface DiaryDatesResult { + data class Success(val dates: List) : DiaryDatesResult + data class Error(val code: String?, val message: String?) : DiaryDatesResult +} + +sealed interface DiaryCreateDateResult { + data class Success(val entry: DiaryDateEntry) : DiaryCreateDateResult + data class Error(val code: String?, val message: String?) : DiaryCreateDateResult +} + +sealed interface DiaryActionResult { + data class Success(val message: String?) : DiaryActionResult + data class Error(val code: String?, val message: String?) : DiaryActionResult +} + +sealed interface DiaryParticipantsResult { + data class Success(val participants: List) : DiaryParticipantsResult + data class Error(val code: String?, val message: String?) : DiaryParticipantsResult +} + +sealed interface DiaryParticipantActionResult { + data class Success(val participant: DiaryParticipant?) : DiaryParticipantActionResult + data class Error(val code: String?, val message: String?) : DiaryParticipantActionResult +} + +sealed interface DiaryActivitiesResult { + data class Success(val activities: List) : DiaryActivitiesResult + data class Error(val code: String?, val message: String?) : DiaryActivitiesResult +} + +sealed interface DiaryAddActivityResult { + data class Success(val activity: DiaryActivityItem) : DiaryAddActivityResult + data class Error(val code: String?, val message: String?) : DiaryAddActivityResult +} + +sealed interface DiaryTagsResult { + data class Success(val tags: List) : DiaryTagsResult + data class Error(val code: String?, val message: String?) : DiaryTagsResult +} + +sealed interface DiaryCreateTagResult { + data class Success(val tag: DiaryTagItem) : DiaryCreateTagResult + data class Error(val code: String?, val message: String?) : DiaryCreateTagResult +} + +sealed interface DiaryPlanResult { + data class Success(val items: List) : DiaryPlanResult + data class Error(val code: String?, val message: String?) : DiaryPlanResult +} + +sealed interface DiaryGroupsResult { + data class Success(val groups: List) : DiaryGroupsResult + data class Error(val code: String?, val message: String?) : DiaryGroupsResult +} + +sealed interface DiaryActivityParticipantsResult { + data class Success(val participantIds: Set) : DiaryActivityParticipantsResult + data class Error(val code: String?, val message: String?) : DiaryActivityParticipantsResult +} + +sealed interface DiaryAccidentsResult { + data class Success(val items: List) : DiaryAccidentsResult + data class Error(val code: String?, val message: String?) : DiaryAccidentsResult +} + +sealed interface MemberActivityStatsResult { + data class Success( + val stats: List, + val lastParticipations: List + ) : MemberActivityStatsResult + data class Error(val code: String?, val message: String?) : MemberActivityStatsResult +} + +sealed interface PredefinedActivitiesResult { + data class Success(val items: List) : PredefinedActivitiesResult + data class Error(val code: String?, val message: String?) : PredefinedActivitiesResult +} + +sealed interface MemberImagesResult { + data class Success(val images: List) : MemberImagesResult + data class Error(val code: String?, val message: String?) : MemberImagesResult +} + +sealed interface MemberTrainingGroupsResult { + data class Success(val groups: List) : MemberTrainingGroupsResult + data class Error(val code: String?, val message: String?) : MemberTrainingGroupsResult +} + +sealed interface MemberNotesResult { + data class Success(val notes: List) : MemberNotesResult + data class Error(val code: String?, val message: String?) : MemberNotesResult +} + +sealed interface MemberTransferConfigResult { + data class Success(val config: MemberTransferConfig?) : MemberTransferConfigResult + data class Error(val message: String) : MemberTransferConfigResult +} + +sealed interface MemberTransferActionResult { + data class Success(val message: String?, val configId: String?) : MemberTransferActionResult + data class Error(val message: String) : MemberTransferActionResult +} + +sealed interface MemberTransferExecutionResult { + data class Success(val summary: MemberTransferSummary) : MemberTransferExecutionResult + data class Error(val message: String, val summary: MemberTransferSummary? = null) : MemberTransferExecutionResult +} + +sealed interface MemberSaveResult { + data class Success(val message: String?) : MemberSaveResult + data class Error(val code: String?, val message: String?) : MemberSaveResult +} + +sealed interface MemberActionResult { + data class Success(val message: String?) : MemberActionResult + data class Error(val code: String?, val message: String?) : MemberActionResult +} + +internal data class BackendError( + val code: String?, + val message: String? +) diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/services/DiaryService.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/services/DiaryService.kt new file mode 100644 index 00000000..9f126077 --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/services/DiaryService.kt @@ -0,0 +1,140 @@ +package de.trainingstagebuch.app.services + +import de.trainingstagebuch.app.repository.DiaryActionResult +import de.trainingstagebuch.app.repository.DiaryAccidentsResult +import de.trainingstagebuch.app.repository.DiaryActivitiesResult +import de.trainingstagebuch.app.repository.DiaryAddActivityResult +import de.trainingstagebuch.app.repository.DiaryActivityParticipantsResult +import de.trainingstagebuch.app.repository.DiaryCreateTagResult +import de.trainingstagebuch.app.repository.DiaryParticipantActionResult +import de.trainingstagebuch.app.repository.DiaryParticipantsResult +import de.trainingstagebuch.app.repository.MemberActivityStatsResult +import de.trainingstagebuch.app.repository.MemberNotesResult +import de.trainingstagebuch.app.repository.DiaryCreateDateResult +import de.trainingstagebuch.app.repository.DiaryDatesResult +import de.trainingstagebuch.app.repository.DiaryGroupsResult +import de.trainingstagebuch.app.repository.DiaryPlanResult +import de.trainingstagebuch.app.repository.DiaryTagsResult +import de.trainingstagebuch.app.repository.MembersResult +import de.trainingstagebuch.app.repository.PredefinedActivitiesResult +import de.trainingstagebuch.app.repository.TrainingstagebuchRepository + +class DiaryService( + private val repository: TrainingstagebuchRepository +) { + suspend fun loadDiaryDates(clubId: String): DiaryDatesResult = repository.loadDiaryDates(clubId) + + suspend fun createDiaryDate( + clubId: String, + date: String, + trainingStart: String?, + trainingEnd: String? + ): DiaryCreateDateResult = repository.createDiaryDate(clubId, date, trainingStart, trainingEnd) + + suspend fun updateDiaryTrainingTime( + clubId: String, + dateId: String, + trainingStart: String?, + trainingEnd: String? + ): DiaryActionResult = repository.updateDiaryTrainingTime(clubId, dateId, trainingStart, trainingEnd) + + suspend fun deleteDiaryDate(clubId: String, dateId: String): DiaryActionResult = + repository.deleteDiaryDate(clubId, dateId) + + suspend fun loadMembers(clubId: String): MembersResult = repository.loadMembers(clubId, showAll = false) + + suspend fun loadParticipants(dateId: String): DiaryParticipantsResult = repository.loadParticipants(dateId) + + suspend fun addParticipant(dateId: String, memberId: String): DiaryParticipantActionResult = + repository.addParticipant(dateId, memberId) + + suspend fun removeParticipant(dateId: String, memberId: String): DiaryActionResult = + repository.removeParticipant(dateId, memberId) + + suspend fun loadActivities(dateId: String): DiaryActivitiesResult = repository.loadActivities(dateId) + + suspend fun addActivity(dateId: String, description: String): DiaryAddActivityResult = + repository.addActivity(dateId, description) + + suspend fun loadDiaryMemberNotes(clubId: String, diaryDateId: String, memberId: String): MemberNotesResult = + repository.loadDiaryMemberNotes(clubId, diaryDateId, memberId) + + suspend fun addDiaryMemberNote( + clubId: String, + diaryDateId: String, + memberId: String, + content: String + ): DiaryActionResult = repository.addDiaryMemberNote(clubId, diaryDateId, memberId, content) + + suspend fun deleteDiaryMemberNote(clubId: String, noteId: String): DiaryActionResult = + repository.deleteDiaryMemberNote(clubId, noteId) + + suspend fun loadTags(): DiaryTagsResult = repository.loadTags() + + suspend fun createTag(name: String): DiaryCreateTagResult = repository.createTag(name) + + suspend fun addTagToDiaryDate(clubId: String, diaryDateId: String, tagId: String): DiaryActionResult = + repository.addTagToDiaryDate(clubId, diaryDateId, tagId) + + suspend fun removeTagFromDiaryDate(clubId: String, diaryDateId: String, tagId: String): DiaryActionResult = + repository.removeTagFromDiaryDate(clubId, diaryDateId, tagId) + + suspend fun loadDiaryPlan(clubId: String, dateId: String): DiaryPlanResult = + repository.loadDiaryPlan(clubId, dateId) + + suspend fun addDiaryPlanItem(clubId: String, dateId: String, activity: String, orderId: Int): DiaryActionResult = + repository.addDiaryPlanItem(clubId, dateId, activity, orderId) + + suspend fun updateDiaryPlanItem(clubId: String, planItemId: String, activity: String): DiaryActionResult = + repository.updateDiaryPlanItem(clubId, planItemId, activity) + + suspend fun deleteDiaryPlanItem(clubId: String, planItemId: String): DiaryActionResult = + repository.deleteDiaryPlanItem(clubId, planItemId) + + suspend fun updateDiaryPlanItemOrder(clubId: String, planItemId: String, order: Int): DiaryActionResult = + repository.updateDiaryPlanItemOrder(clubId, planItemId, order) + + suspend fun loadDiaryGroups(clubId: String, dateId: String): DiaryGroupsResult = + repository.loadDiaryGroups(clubId, dateId) + + suspend fun updateParticipantGroup(dateId: String, memberId: String, groupId: String?): DiaryActionResult = + repository.updateParticipantGroup(dateId, memberId, groupId) + + suspend fun createDiaryGroup(clubId: String, dateId: String, name: String, lead: String): DiaryActionResult = + repository.createDiaryGroup(clubId, dateId, name, lead) + + suspend fun updateDiaryGroup(groupId: String, clubId: String, dateId: String, name: String, lead: String): DiaryActionResult = + repository.updateDiaryGroup(groupId, clubId, dateId, name, lead) + + suspend fun deleteDiaryGroup(groupId: String, clubId: String, dateId: String): DiaryActionResult = + repository.deleteDiaryGroup(groupId, clubId, dateId) + + suspend fun createDiaryGroupActivity( + clubId: String, + dateId: String, + groupId: String, + activity: String, + predefinedActivityId: String? + ): DiaryActionResult = repository.createDiaryGroupActivity(clubId, dateId, groupId, activity, predefinedActivityId) + + suspend fun loadActivityParticipantIds(clubId: String, activityId: String): DiaryActivityParticipantsResult = + repository.loadActivityParticipantIds(clubId, activityId) + + suspend fun assignActivityParticipants(clubId: String, activityId: String, participantIds: List): DiaryActionResult = + repository.assignActivityParticipants(clubId, activityId, participantIds) + + suspend fun removeActivityParticipant(clubId: String, activityId: String, participantId: String): DiaryActionResult = + repository.removeActivityParticipant(clubId, activityId, participantId) + + suspend fun createAccident(clubId: String, dateId: String, accident: String): DiaryActionResult = + repository.createAccident(clubId, dateId, accident) + + suspend fun loadAccidents(clubId: String, dateId: String): DiaryAccidentsResult = + repository.loadAccidents(clubId, dateId) + + suspend fun loadMemberActivityStats(clubId: String, memberId: String): MemberActivityStatsResult = + repository.loadMemberActivityStats(clubId, memberId) + + suspend fun searchPredefinedActivities(term: String, limit: Int = 10): PredefinedActivitiesResult = + repository.searchPredefinedActivities(term, limit) +} diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/ui/screens/DiaryScreen.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/ui/screens/DiaryScreen.kt new file mode 100644 index 00000000..8e200451 --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/ui/screens/DiaryScreen.kt @@ -0,0 +1,1120 @@ +package de.trainingstagebuch.app.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.trainingstagebuch.app.R +import de.trainingstagebuch.app.repository.DiaryDateEntry +import de.trainingstagebuch.app.repository.DiaryActivityItem +import de.trainingstagebuch.app.repository.DiaryGroupItem +import de.trainingstagebuch.app.repository.DiaryPlanItem +import de.trainingstagebuch.app.repository.DiaryTagItem +import de.trainingstagebuch.app.repository.MemberItem +import de.trainingstagebuch.app.repository.MemberNoteItem +import de.trainingstagebuch.app.ui.ErrorBanner +import de.trainingstagebuch.app.ui.ErrorSnackbarHost +import de.trainingstagebuch.app.viewmodel.DiaryStatus +import de.trainingstagebuch.app.viewmodel.DiaryViewModel + +@Composable +fun DiaryScreen( + diaryViewModel: DiaryViewModel = viewModel() +) { + val state by diaryViewModel.state.collectAsState() + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } + var dismissedError by remember { mutableStateOf(null) } + + LaunchedEffect(state.errorMessage, state.successMessage) { + state.errorMessage?.let { + dismissedError = null + snackbarHostState.showSnackbar(it.resolve(context)) + } + state.successMessage?.let { + snackbarHostState.showSnackbar(it.resolve(context)) + } + } + + Scaffold( + snackbarHost = { ErrorSnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.diary_title), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(bottom = 12.dp) + ) + + val currentError = state.errorMessage + if (currentError != null && currentError != dismissedError) { + ErrorBanner( + message = currentError.resolve(context), + onDismiss = { dismissedError = currentError }, + modifier = Modifier.padding(bottom = 8.dp) + ) + } + + when (state.status) { + DiaryStatus.Idle, DiaryStatus.Loading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + Text( + text = stringResource(R.string.diary_loading), + modifier = Modifier.padding(top = 12.dp) + ) + } + } + + DiaryStatus.Error -> { + Button(onClick = diaryViewModel::load) { + Text(stringResource(R.string.home_retry)) + } + } + + DiaryStatus.Success -> { + DiaryContent( + dates = state.dates, + members = state.members, + participantMemberIds = state.participantMemberIds, + participantMapByMemberId = state.participantMapByMemberId, + participantGroupByMemberId = state.participantGroupByMemberId, + availableGroups = state.availableGroups, + newGroupName = state.newGroupName, + newGroupLead = state.newGroupLead, + newGroupActivity = state.newGroupActivity, + newGroupActivityGroupId = state.newGroupActivityGroupId, + editingGroupId = state.editingGroupId, + editingGroupName = state.editingGroupName, + editingGroupLead = state.editingGroupLead, + activities = state.activities, + planItems = state.planItems, + notes = state.notes, + availableTags = state.availableTags, + selectedDateTags = state.selectedDateTags, + selectedDateId = state.selectedDateId, + selectedNoteMemberId = state.selectedNoteMemberId, + newDate = state.newDate, + newTrainingStart = state.newTrainingStart, + newTrainingEnd = state.newTrainingEnd, + newActivityDescription = state.newActivityDescription, + newNoteContent = state.newNoteContent, + newTagName = state.newTagName, + newPlanActivity = state.newPlanActivity, + editingPlanItemId = state.editingPlanItemId, + editingPlanActivity = state.editingPlanActivity, + activityParticipantIdsByActivityId = state.activityParticipantIdsByActivityId, + accidents = state.accidents, + newAccident = state.newAccident, + showQuickAddDialog = state.showQuickAddDialog, + quickAddFirstName = state.quickAddFirstName, + quickAddLastName = state.quickAddLastName, + quickAddBirthDate = state.quickAddBirthDate, + quickAddGender = state.quickAddGender, + statsMemberId = state.statsMemberId, + memberActivityStats = state.memberActivityStats, + memberLastParticipations = state.memberLastParticipations, + createLoading = state.createLoading, + selectedTrainingStart = state.selectedTrainingStart, + selectedTrainingEnd = state.selectedTrainingEnd, + actionLoading = state.actionLoading, + onSelectDate = diaryViewModel::onSelectDate, + onNewDateChanged = diaryViewModel::onNewDateChanged, + onTrainingStartChanged = diaryViewModel::onTrainingStartChanged, + onTrainingEndChanged = diaryViewModel::onTrainingEndChanged, + onNewActivityDescriptionChanged = diaryViewModel::onNewActivityDescriptionChanged, + onNewTagNameChanged = diaryViewModel::onNewTagNameChanged, + onNewPlanActivityChanged = diaryViewModel::onNewPlanActivityChanged, + onStartEditPlanItem = diaryViewModel::startEditPlanItem, + onEditingPlanActivityChanged = diaryViewModel::onEditingPlanActivityChanged, + onCancelEditPlanItem = diaryViewModel::cancelEditPlanItem, + onSelectedNoteMemberChanged = diaryViewModel::onSelectedNoteMemberChanged, + onNewNoteContentChanged = diaryViewModel::onNewNoteContentChanged, + onSelectedTrainingStartChanged = diaryViewModel::onSelectedTrainingStartChanged, + onSelectedTrainingEndChanged = diaryViewModel::onSelectedTrainingEndChanged, + onCreateDate = diaryViewModel::createDate, + onAddActivity = diaryViewModel::addActivity, + onCreateTagAndAdd = diaryViewModel::createTagAndAddToSelectedDate, + onAddExistingTag = diaryViewModel::addExistingTagToSelectedDate, + onRemoveTag = diaryViewModel::removeTagFromSelectedDate, + onAddPlanItem = diaryViewModel::addPlanItem, + onSaveEditPlanItem = diaryViewModel::saveEditPlanItem, + onDeletePlanItem = diaryViewModel::deletePlanItem, + onMovePlanItemUp = diaryViewModel::movePlanItemUp, + onMovePlanItemDown = diaryViewModel::movePlanItemDown, + onAddNote = diaryViewModel::addNote, + onReload = diaryViewModel::load, + onUpdateSelectedTimes = diaryViewModel::updateSelectedDateTimes, + onDeleteSelectedDate = diaryViewModel::deleteSelectedDate, + onDeleteNotAllowed = diaryViewModel::notifyDeleteNotAllowed, + onToggleParticipant = diaryViewModel::toggleParticipant, + onParticipantGroupChanged = diaryViewModel::onParticipantGroupChanged, + onNewGroupNameChanged = diaryViewModel::onNewGroupNameChanged, + onNewGroupLeadChanged = diaryViewModel::onNewGroupLeadChanged, + onNewGroupActivityChanged = diaryViewModel::onNewGroupActivityChanged, + onNewGroupActivityGroupChanged = diaryViewModel::onNewGroupActivityGroupChanged, + onCreateGroup = diaryViewModel::createGroup, + onAddGroupActivity = diaryViewModel::addGroupActivity, + onStartEditGroup = diaryViewModel::startEditGroup, + onEditingGroupNameChanged = diaryViewModel::onEditingGroupNameChanged, + onEditingGroupLeadChanged = diaryViewModel::onEditingGroupLeadChanged, + onSaveEditGroup = diaryViewModel::saveEditGroup, + onCancelEditGroup = diaryViewModel::cancelEditGroup, + onDeleteGroup = diaryViewModel::deleteGroup, + onDeleteNote = diaryViewModel::deleteNote, + onToggleActivityMember = diaryViewModel::toggleActivityMember, + onOpenQuickAddDialog = diaryViewModel::openQuickAddDialog, + onCloseQuickAddDialog = diaryViewModel::closeQuickAddDialog, + onQuickAddFirstNameChanged = diaryViewModel::onQuickAddFirstNameChanged, + onQuickAddLastNameChanged = diaryViewModel::onQuickAddLastNameChanged, + onQuickAddBirthDateChanged = diaryViewModel::onQuickAddBirthDateChanged, + onQuickAddGenderChanged = diaryViewModel::onQuickAddGenderChanged, + onCreateAndAddMember = diaryViewModel::createAndAddMember, + onNewAccidentChanged = diaryViewModel::onNewAccidentChanged, + onSaveAccident = diaryViewModel::saveAccident, + onRefreshAccidents = diaryViewModel::refreshAccidents, + onOpenStatsForMember = diaryViewModel::openStatsForMember, + onDismissStatsDialog = diaryViewModel::dismissStatsDialog + ) + } + } + } + } +} + +@Composable +private fun DiaryContent( + dates: List, + members: List, + participantMemberIds: List, + participantMapByMemberId: Map, + participantGroupByMemberId: Map, + availableGroups: List, + newGroupName: String, + newGroupLead: String, + newGroupActivity: String, + newGroupActivityGroupId: String, + editingGroupId: String?, + editingGroupName: String, + editingGroupLead: String, + activities: List, + planItems: List, + notes: List, + availableTags: List, + selectedDateTags: List, + selectedDateId: String?, + selectedNoteMemberId: String?, + newDate: String, + newTrainingStart: String, + newTrainingEnd: String, + newActivityDescription: String, + newNoteContent: String, + newTagName: String, + newPlanActivity: String, + editingPlanItemId: String?, + editingPlanActivity: String, + activityParticipantIdsByActivityId: Map>, + accidents: List, + newAccident: String, + showQuickAddDialog: Boolean, + quickAddFirstName: String, + quickAddLastName: String, + quickAddBirthDate: String, + quickAddGender: String, + statsMemberId: String?, + memberActivityStats: List, + memberLastParticipations: List, + createLoading: Boolean, + selectedTrainingStart: String, + selectedTrainingEnd: String, + actionLoading: Boolean, + onSelectDate: (String) -> Unit, + onNewDateChanged: (String) -> Unit, + onTrainingStartChanged: (String) -> Unit, + onTrainingEndChanged: (String) -> Unit, + onNewActivityDescriptionChanged: (String) -> Unit, + onNewTagNameChanged: (String) -> Unit, + onNewPlanActivityChanged: (String) -> Unit, + onStartEditPlanItem: (String, String) -> Unit, + onEditingPlanActivityChanged: (String) -> Unit, + onCancelEditPlanItem: () -> Unit, + onSelectedNoteMemberChanged: (String) -> Unit, + onNewNoteContentChanged: (String) -> Unit, + onSelectedTrainingStartChanged: (String) -> Unit, + onSelectedTrainingEndChanged: (String) -> Unit, + onCreateDate: () -> Unit, + onAddActivity: () -> Unit, + onCreateTagAndAdd: () -> Unit, + onAddExistingTag: (String) -> Unit, + onRemoveTag: (String) -> Unit, + onAddPlanItem: () -> Unit, + onSaveEditPlanItem: () -> Unit, + onDeletePlanItem: (String) -> Unit, + onMovePlanItemUp: (String) -> Unit, + onMovePlanItemDown: (String) -> Unit, + onAddNote: () -> Unit, + onReload: () -> Unit, + onUpdateSelectedTimes: () -> Unit, + onDeleteSelectedDate: () -> Unit, + onDeleteNotAllowed: () -> Unit, + onToggleParticipant: (String) -> Unit, + onParticipantGroupChanged: (String, String?) -> Unit, + onNewGroupNameChanged: (String) -> Unit, + onNewGroupLeadChanged: (String) -> Unit, + onNewGroupActivityChanged: (String) -> Unit, + onNewGroupActivityGroupChanged: (String) -> Unit, + onCreateGroup: () -> Unit, + onAddGroupActivity: () -> Unit, + onStartEditGroup: (String, String, String) -> Unit, + onEditingGroupNameChanged: (String) -> Unit, + onEditingGroupLeadChanged: (String) -> Unit, + onSaveEditGroup: () -> Unit, + onCancelEditGroup: () -> Unit, + onDeleteGroup: (String) -> Unit, + onDeleteNote: (String) -> Unit, + onToggleActivityMember: (String, String) -> Unit, + onOpenQuickAddDialog: () -> Unit, + onCloseQuickAddDialog: () -> Unit, + onQuickAddFirstNameChanged: (String) -> Unit, + onQuickAddLastNameChanged: (String) -> Unit, + onQuickAddBirthDateChanged: (String) -> Unit, + onQuickAddGenderChanged: (String) -> Unit, + onCreateAndAddMember: () -> Unit, + onNewAccidentChanged: (String) -> Unit, + onSaveAccident: () -> Unit, + onRefreshAccidents: () -> Unit, + onOpenStatsForMember: (String) -> Unit, + onDismissStatsDialog: () -> Unit +) { + val participantMembers = members.filter { participantMemberIds.contains(it.id) } + var expandedGroupMemberId by remember { mutableStateOf(null) } + var showDeleteConfirm by remember { mutableStateOf(false) } + val canDeleteSelectedDate = selectedDateId != null && + planItems.isEmpty() && + participantMemberIds.isEmpty() && + activities.isEmpty() && + notes.isEmpty() + OutlinedTextField( + value = newDate, + onValueChange = onNewDateChanged, + label = { Text(stringResource(R.string.diary_new_date_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newTrainingStart, + onValueChange = onTrainingStartChanged, + label = { Text(stringResource(R.string.diary_training_start_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = newTrainingEnd, + onValueChange = onTrainingEndChanged, + label = { Text(stringResource(R.string.diary_training_end_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = onCreateDate, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_create_button)) + } + Button( + onClick = onReload, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.home_retry)) + } + } + + if (dates.isEmpty()) { + Text(text = stringResource(R.string.diary_empty)) + return + } + + Text( + text = stringResource(R.string.diary_selected_times_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 6.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = selectedTrainingStart, + onValueChange = onSelectedTrainingStartChanged, + label = { Text(stringResource(R.string.diary_training_start_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = selectedTrainingEnd, + onValueChange = onSelectedTrainingEndChanged, + label = { Text(stringResource(R.string.diary_training_end_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = onUpdateSelectedTimes, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_update_times_button)) + } + Button( + onClick = { + if (canDeleteSelectedDate) { + showDeleteConfirm = true + } else { + onDeleteNotAllowed() + } + }, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_delete_button)) + } + } + + Text( + text = stringResource(R.string.diary_participants_title, participantMemberIds.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + Button( + onClick = onOpenQuickAddDialog, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_quick_add_button)) + } + if (members.isEmpty()) { + Text(text = stringResource(R.string.diary_no_members)) + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(members, key = { it.id }) { member -> + val isParticipant = participantMemberIds.contains(member.id) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = member.displayName, + modifier = Modifier.weight(1f) + ) + if (isParticipant) { + Button( + onClick = { onOpenStatsForMember(member.id) }, + enabled = !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_stats_button)) + } + } + if (isParticipant) { + val currentGroupId = participantGroupByMemberId[member.id] + val currentGroupName = availableGroups + .firstOrNull { it.id == currentGroupId } + ?.name + ?: stringResource(R.string.diary_group_none) + Button( + onClick = { expandedGroupMemberId = member.id }, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(currentGroupName) + } + DropdownMenu( + expanded = expandedGroupMemberId == member.id, + onDismissRequest = { expandedGroupMemberId = null } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.diary_group_none)) }, + onClick = { + expandedGroupMemberId = null + onParticipantGroupChanged(member.id, null) + } + ) + availableGroups.forEach { group -> + DropdownMenuItem( + text = { Text(group.name) }, + onClick = { + expandedGroupMemberId = null + onParticipantGroupChanged(member.id, group.id) + } + ) + } + } + } + Button( + onClick = { onToggleParticipant(member.id) }, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { + Text( + text = if (isParticipant) { + stringResource(R.string.diary_remove_participant_button) + } else { + stringResource(R.string.diary_add_participant_button) + } + ) + } + } + } + } + } + + Text( + text = stringResource(R.string.diary_groups_title, availableGroups.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newGroupName, + onValueChange = onNewGroupNameChanged, + label = { Text(stringResource(R.string.diary_group_name_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = newGroupLead, + onValueChange = onNewGroupLeadChanged, + label = { Text(stringResource(R.string.diary_group_lead_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + } + Button( + onClick = onCreateGroup, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(top = 6.dp, bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_group_create_button)) + } + if (availableGroups.isEmpty()) { + Text( + text = stringResource(R.string.diary_groups_empty), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + availableGroups.forEach { group -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (editingGroupId == group.id) { + OutlinedTextField( + value = editingGroupName, + onValueChange = onEditingGroupNameChanged, + label = { Text(stringResource(R.string.diary_group_name_label)) }, + singleLine = true, + modifier = Modifier + .weight(1f) + .padding(end = 6.dp) + ) + OutlinedTextField( + value = editingGroupLead, + onValueChange = onEditingGroupLeadChanged, + label = { Text(stringResource(R.string.diary_group_lead_label)) }, + singleLine = true, + modifier = Modifier + .weight(1f) + .padding(end = 6.dp) + ) + Button( + onClick = onSaveEditGroup, + enabled = !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_group_save_button)) + } + Button( + onClick = onCancelEditGroup, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_group_cancel_button)) + } + } else { + Text( + text = "${group.name} (${group.lead})", + modifier = Modifier.weight(1f) + ) + Button( + onClick = { onStartEditGroup(group.id, group.name, group.lead) }, + enabled = !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_group_edit_button)) + } + Button( + onClick = { onDeleteGroup(group.id) }, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_group_delete_button)) + } + } + } + } + } + + Text( + text = stringResource(R.string.diary_activities_title, activities.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + OutlinedTextField( + value = newActivityDescription, + onValueChange = onNewActivityDescriptionChanged, + label = { Text(stringResource(R.string.diary_activity_input_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + ) + Button( + onClick = onAddActivity, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_add_activity_button)) + } + if (activities.isEmpty()) { + Text( + text = stringResource(R.string.diary_activities_empty), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + activities.forEach { activity -> + Text( + text = "• ${activity.description}", + modifier = Modifier.padding(bottom = 2.dp) + ) + } + } + + Text( + text = stringResource(R.string.diary_tags_title, selectedDateTags.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + OutlinedTextField( + value = newTagName, + onValueChange = onNewTagNameChanged, + label = { Text(stringResource(R.string.diary_new_tag_input_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + ) + Button( + onClick = onCreateTagAndAdd, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_create_tag_button)) + } + if (availableTags.isEmpty()) { + Text( + text = stringResource(R.string.diary_no_tags_available), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + availableTags.forEach { tag -> + val selected = selectedDateTags.any { it.id == tag.id } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = tag.name, + modifier = Modifier.weight(1f) + ) + Button( + onClick = { + if (selected) onRemoveTag(tag.id) else onAddExistingTag(tag.id) + }, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { + Text( + text = if (selected) { + stringResource(R.string.diary_remove_tag_button) + } else { + stringResource(R.string.diary_add_tag_button) + } + ) + } + } + } + } + + Text( + text = stringResource(R.string.diary_plan_title, planItems.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + Text( + text = stringResource(R.string.diary_group_activity_title), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(bottom = 4.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newGroupActivity, + onValueChange = onNewGroupActivityChanged, + label = { Text(stringResource(R.string.diary_group_activity_input_label)) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + var groupSelectExpanded by remember { mutableStateOf(false) } + val selectedGroupLabel = availableGroups.firstOrNull { it.id == newGroupActivityGroupId }?.name + ?: stringResource(R.string.diary_group_none) + Button( + onClick = { groupSelectExpanded = true }, + enabled = !createLoading && !actionLoading + ) { + Text(selectedGroupLabel) + } + DropdownMenu( + expanded = groupSelectExpanded, + onDismissRequest = { groupSelectExpanded = false } + ) { + availableGroups.forEach { group -> + DropdownMenuItem( + text = { Text(group.name) }, + onClick = { + onNewGroupActivityGroupChanged(group.id) + groupSelectExpanded = false + } + ) + } + } + } + Button( + onClick = onAddGroupActivity, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(top = 6.dp, bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_group_activity_add_button)) + } + OutlinedTextField( + value = newPlanActivity, + onValueChange = onNewPlanActivityChanged, + label = { Text(stringResource(R.string.diary_plan_new_activity_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + ) + Button( + onClick = onAddPlanItem, + enabled = selectedDateId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_plan_add_button)) + } + if (planItems.isEmpty()) { + Text( + text = stringResource(R.string.diary_plan_empty), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + planItems.forEachIndexed { index, item -> + val itemText = if (item.isTimeblock) { + stringResource(R.string.diary_plan_timeblock_label) + } else { + item.activity + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (editingPlanItemId == item.id) { + OutlinedTextField( + value = editingPlanActivity, + onValueChange = onEditingPlanActivityChanged, + label = { Text(stringResource(R.string.diary_plan_new_activity_label)) }, + singleLine = true, + modifier = Modifier + .weight(1f) + .padding(end = 6.dp) + ) + Button( + onClick = onSaveEditPlanItem, + enabled = !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_plan_save_button)) + } + Button( + onClick = onCancelEditPlanItem, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_plan_cancel_button)) + } + } else { + Text( + text = "• $itemText", + modifier = Modifier.weight(1f) + ) + Button( + onClick = { onMovePlanItemUp(item.id) }, + enabled = index > 0 && !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_plan_up_button)) + } + Button( + onClick = { onMovePlanItemDown(item.id) }, + enabled = index < planItems.lastIndex && !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_plan_down_button)) + } + if (!item.isTimeblock) { + Button( + onClick = { onStartEditPlanItem(item.id, item.activity) }, + enabled = !createLoading && !actionLoading, + modifier = Modifier.padding(end = 4.dp) + ) { + Text(stringResource(R.string.diary_plan_edit_button)) + } + } + Button( + onClick = { onDeletePlanItem(item.id) }, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_plan_delete_button)) + } + } + } + if (!item.isTimeblock && participantMembers.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + participantMembers.forEach { member -> + val assigned = activityParticipantIdsByActivityId[item.id] + ?.contains(participantMapByMemberId[member.id]) + ?: false + Button( + onClick = { onToggleActivityMember(item.id, member.id) }, + enabled = !createLoading && !actionLoading + ) { + Text( + if (assigned) { + "${member.displayName} ✓" + } else { + member.displayName + } + ) + } + } + } + } + } + } + + Text( + text = stringResource(R.string.diary_notes_title, notes.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + if (participantMembers.isEmpty()) { + Text( + text = stringResource(R.string.diary_no_participants_for_notes), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + participantMembers.forEach { member -> + Button( + onClick = { onSelectedNoteMemberChanged(member.id) }, + enabled = !createLoading && !actionLoading + ) { + Text(text = member.displayName) + } + } + } + OutlinedTextField( + value = newNoteContent, + onValueChange = onNewNoteContentChanged, + label = { Text(stringResource(R.string.diary_note_input_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + ) + Button( + onClick = onAddNote, + enabled = selectedNoteMemberId != null && !createLoading && !actionLoading, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Text(stringResource(R.string.diary_add_note_button)) + } + if (notes.isEmpty()) { + Text( + text = stringResource(R.string.diary_notes_empty), + modifier = Modifier.padding(bottom = 8.dp) + ) + } else { + notes.forEach { note -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = note.content, + modifier = Modifier.weight(1f) + ) + Button( + onClick = { onDeleteNote(note.id) }, + enabled = !createLoading && !actionLoading + ) { + Text(stringResource(R.string.diary_delete_note_button)) + } + } + } + } + } + + Text( + text = stringResource(R.string.diary_accidents_title, accidents.size), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 6.dp) + ) + OutlinedTextField( + value = newAccident, + onValueChange = onNewAccidentChanged, + label = { Text(stringResource(R.string.diary_accident_input_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + ) + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = onSaveAccident, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { Text(stringResource(R.string.diary_accident_save_button)) } + Button( + onClick = onRefreshAccidents, + enabled = selectedDateId != null && !createLoading && !actionLoading + ) { Text(stringResource(R.string.home_retry)) } + } + accidents.forEach { item -> + Text(stringResource(R.string.diary_accident_item_value, item.accident)) + } + + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(dates, key = { it.id }) { item -> + val selected = item.id == selectedDateId + val times = listOf(item.trainingStart, item.trainingEnd) + .filterNotNull() + .filter { it.isNotBlank() } + .joinToString(" - ") + .ifBlank { "-" } + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelectDate(item.id) } + .padding(vertical = 8.dp) + ) { + Text( + text = item.date, + style = if (selected) MaterialTheme.typography.titleMedium else MaterialTheme.typography.bodyLarge, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.diary_times_value, times), + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + if (showDeleteConfirm) { + AlertDialog( + onDismissRequest = { showDeleteConfirm = false }, + title = { Text(stringResource(R.string.diary_confirm_delete_title)) }, + text = { Text(stringResource(R.string.diary_confirm_delete_message)) }, + confirmButton = { + TextButton( + onClick = { + showDeleteConfirm = false + onDeleteSelectedDate() + } + ) { + Text(stringResource(R.string.diary_confirm_delete_confirm)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteConfirm = false }) { + Text(stringResource(R.string.diary_confirm_delete_cancel)) + } + } + ) + } + + if (showQuickAddDialog) { + AlertDialog( + onDismissRequest = onCloseQuickAddDialog, + title = { Text(stringResource(R.string.diary_quick_add_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = quickAddFirstName, + onValueChange = onQuickAddFirstNameChanged, + label = { Text(stringResource(R.string.diary_quick_add_firstname_label)) }, + singleLine = true + ) + OutlinedTextField( + value = quickAddLastName, + onValueChange = onQuickAddLastNameChanged, + label = { Text(stringResource(R.string.diary_quick_add_lastname_label)) }, + singleLine = true + ) + OutlinedTextField( + value = quickAddBirthDate, + onValueChange = onQuickAddBirthDateChanged, + label = { Text(stringResource(R.string.diary_quick_add_birthdate_label)) }, + singleLine = true + ) + OutlinedTextField( + value = quickAddGender, + onValueChange = onQuickAddGenderChanged, + label = { Text(stringResource(R.string.diary_quick_add_gender_label)) }, + singleLine = true + ) + } + }, + confirmButton = { + TextButton(onClick = onCreateAndAddMember) { + Text(stringResource(R.string.diary_quick_add_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onCloseQuickAddDialog) { + Text(stringResource(R.string.diary_quick_add_cancel)) + } + } + ) + } + + if (statsMemberId != null) { + AlertDialog( + onDismissRequest = onDismissStatsDialog, + title = { Text(stringResource(R.string.diary_stats_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(stringResource(R.string.diary_stats_recent_title)) + if (memberLastParticipations.isEmpty()) { + Text(stringResource(R.string.diary_stats_empty)) + } else { + memberLastParticipations.forEach { participation -> + Text( + stringResource( + R.string.diary_participation_item_value, + participation.date, + participation.activity + ) + ) + } + } + Text(stringResource(R.string.diary_stats_activity_title)) + if (memberActivityStats.isEmpty()) { + Text(stringResource(R.string.diary_stats_empty)) + } else { + memberActivityStats.forEach { stat -> + Text(stringResource(R.string.diary_stat_item_value, stat.activity, stat.count)) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismissStatsDialog) { + Text(stringResource(R.string.diary_stats_close)) + } + } + ) + } +} diff --git a/android-app/app/src/main/java/de/trainingstagebuch/app/viewmodel/DiaryViewModel.kt b/android-app/app/src/main/java/de/trainingstagebuch/app/viewmodel/DiaryViewModel.kt new file mode 100644 index 00000000..35cc8dbe --- /dev/null +++ b/android-app/app/src/main/java/de/trainingstagebuch/app/viewmodel/DiaryViewModel.kt @@ -0,0 +1,1687 @@ +package de.trainingstagebuch.app.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import de.trainingstagebuch.app.R +import de.trainingstagebuch.app.repository.DiaryActionResult +import de.trainingstagebuch.app.repository.DiaryAccidentItem +import de.trainingstagebuch.app.repository.DiaryAccidentsResult +import de.trainingstagebuch.app.repository.DiaryActivitiesResult +import de.trainingstagebuch.app.repository.DiaryActivityItem +import de.trainingstagebuch.app.repository.DiaryActivityParticipantsResult +import de.trainingstagebuch.app.repository.DiaryAddActivityResult +import de.trainingstagebuch.app.repository.DiaryCreateTagResult +import de.trainingstagebuch.app.repository.DiaryCreateDateResult +import de.trainingstagebuch.app.repository.DiaryDateEntry +import de.trainingstagebuch.app.repository.DiaryDatesResult +import de.trainingstagebuch.app.repository.DiaryGroupItem +import de.trainingstagebuch.app.repository.DiaryGroupsResult +import de.trainingstagebuch.app.repository.DiaryParticipantActionResult +import de.trainingstagebuch.app.repository.DiaryParticipantsResult +import de.trainingstagebuch.app.repository.DiaryPlanItem +import de.trainingstagebuch.app.repository.DiaryPlanResult +import de.trainingstagebuch.app.repository.DiaryTagItem +import de.trainingstagebuch.app.repository.DiaryTagsResult +import de.trainingstagebuch.app.repository.MemberActivityStatItem +import de.trainingstagebuch.app.repository.MemberActivityStatsResult +import de.trainingstagebuch.app.repository.MemberItem +import de.trainingstagebuch.app.repository.MemberNoteItem +import de.trainingstagebuch.app.repository.MemberNotesResult +import de.trainingstagebuch.app.repository.MemberParticipationItem +import de.trainingstagebuch.app.repository.MembersResult +import de.trainingstagebuch.app.repository.PredefinedActivitiesResult +import de.trainingstagebuch.app.repository.PredefinedActivityItem +import de.trainingstagebuch.app.network.SocketEventType +import de.trainingstagebuch.app.network.SocketManager +import de.trainingstagebuch.app.services.AppServices +import de.trainingstagebuch.app.utils.ErrorMessageFormatter +import de.trainingstagebuch.app.utils.UiText +import de.trainingstagebuch.app.utils.ViewModelStateUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class DiaryViewModel(application: Application) : AndroidViewModel(application) { + + private val authService = AppServices.authService + private val diaryService = AppServices.diaryService + + private val _state = MutableStateFlow(DiaryUiState()) + val state: StateFlow = _state.asStateFlow() + + init { + load() + observeSocketEvents() + } + + fun load() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + _state.update { + it.copy( + status = DiaryStatus.Loading, + errorMessage = null, + successMessage = null + ) + } + when (val result = diaryService.loadDiaryDates(clubId)) { + is DiaryDatesResult.Success -> { + val sorted = result.dates.sortedByDescending { it.date } + val selectedId = sorted.firstOrNull()?.id + val selectedEntry = sorted.firstOrNull() + val members = when (val membersResult = diaryService.loadMembers(clubId)) { + is MembersResult.Success -> membersResult.members + is MembersResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.dynamicOrFallback(membersResult.message) + ) + } + emptyList() + } + } + _state.update { + it.copy( + status = DiaryStatus.Success, + dates = sorted, + members = members, + selectedDateId = selectedId, + selectedDateTags = selectedEntry?.tags.orEmpty(), + selectedTrainingStart = selectedEntry?.trainingStart.orEmpty(), + selectedTrainingEnd = selectedEntry?.trainingEnd.orEmpty() + ) + } + loadAvailableTags() + if (!selectedId.isNullOrBlank()) { + loadParticipantsForDate(selectedId) + loadActivitiesForDate(selectedId) + loadPlanForDate(selectedId) + loadGroupsForDate(selectedId) + refreshAccidents() + loadNotesForSelection() + } + } + + is DiaryDatesResult.Error -> { + _state.update { + it.copy( + status = DiaryStatus.Error, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun onNewDateChanged(value: String) { + _state.update { it.copy(newDate = value) } + } + + fun onTrainingStartChanged(value: String) { + _state.update { it.copy(newTrainingStart = value) } + } + + fun onTrainingEndChanged(value: String) { + _state.update { it.copy(newTrainingEnd = value) } + } + + fun onSelectDate(dateId: String) { + _state.update { current -> + val selected = current.dates.firstOrNull { it.id == dateId } + current.copy( + selectedDateId = dateId, + selectedDateTags = selected?.tags.orEmpty(), + selectedTrainingStart = selected?.trainingStart.orEmpty(), + selectedTrainingEnd = selected?.trainingEnd.orEmpty(), + activityParticipantIdsByActivityId = emptyMap(), + accidents = emptyList() + ) + } + viewModelScope.launch { + loadParticipantsForDate(dateId) + loadActivitiesForDate(dateId) + loadPlanForDate(dateId) + loadGroupsForDate(dateId) + refreshAccidents() + loadNotesForSelection() + } + } + + fun onNewActivityDescriptionChanged(value: String) { + _state.update { it.copy(newActivityDescription = value) } + } + + fun onSelectedNoteMemberChanged(memberId: String) { + _state.update { it.copy(selectedNoteMemberId = memberId) } + viewModelScope.launch { + loadNotesForSelection() + } + } + + fun onNewNoteContentChanged(value: String) { + _state.update { it.copy(newNoteContent = value) } + } + + fun onNewTagNameChanged(value: String) { + _state.update { it.copy(newTagName = value) } + } + + fun onNewPlanActivityChanged(value: String) { + _state.update { it.copy(newPlanActivity = value) } + } + + fun onNewGroupNameChanged(value: String) { + _state.update { it.copy(newGroupName = value) } + } + + fun onNewGroupLeadChanged(value: String) { + _state.update { it.copy(newGroupLead = value) } + } + + fun onNewGroupActivityChanged(value: String) { + _state.update { it.copy(newGroupActivity = value) } + searchPredefinedActivities(value) + } + + fun onNewGroupActivityGroupChanged(groupId: String) { + _state.update { it.copy(newGroupActivityGroupId = groupId) } + } + + fun onQuickAddFirstNameChanged(value: String) { + _state.update { it.copy(quickAddFirstName = value) } + } + + fun onQuickAddLastNameChanged(value: String) { + _state.update { it.copy(quickAddLastName = value) } + } + + fun onQuickAddBirthDateChanged(value: String) { + _state.update { it.copy(quickAddBirthDate = value) } + } + + fun onQuickAddGenderChanged(value: String) { + _state.update { it.copy(quickAddGender = value) } + } + + fun onNewAccidentChanged(value: String) { + _state.update { it.copy(newAccident = value) } + } + + fun dismissStatsDialog() { + _state.update { + it.copy( + statsMemberId = null, + memberActivityStats = emptyList(), + memberLastParticipations = emptyList() + ) + } + } + + fun startEditPlanItem(itemId: String, activity: String) { + _state.update { + it.copy( + editingPlanItemId = itemId, + editingPlanActivity = activity + ) + } + } + + fun onEditingPlanActivityChanged(value: String) { + _state.update { it.copy(editingPlanActivity = value) } + } + + fun cancelEditPlanItem() { + _state.update { + it.copy( + editingPlanItemId = null, + editingPlanActivity = "" + ) + } + } + + fun startEditGroup(groupId: String, name: String, lead: String) { + _state.update { + it.copy( + editingGroupId = groupId, + editingGroupName = name, + editingGroupLead = lead + ) + } + } + + fun onEditingGroupNameChanged(value: String) { + _state.update { it.copy(editingGroupName = value) } + } + + fun onEditingGroupLeadChanged(value: String) { + _state.update { it.copy(editingGroupLead = value) } + } + + fun cancelEditGroup() { + _state.update { + it.copy( + editingGroupId = null, + editingGroupName = "", + editingGroupLead = "" + ) + } + } + + fun onSelectedTrainingStartChanged(value: String) { + _state.update { it.copy(selectedTrainingStart = value) } + } + + fun onSelectedTrainingEndChanged(value: String) { + _state.update { it.copy(selectedTrainingEnd = value) } + } + + fun createDate() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val date = _state.value.newDate.trim() + if (date.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_date_required)) } + return@launch + } + _state.update { it.copy(createLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.createDiaryDate( + clubId = clubId, + date = date, + trainingStart = _state.value.newTrainingStart.trim().ifBlank { null }, + trainingEnd = _state.value.newTrainingEnd.trim().ifBlank { null } + ) + ) { + is DiaryCreateDateResult.Success -> { + _state.update { current -> + val updated = (current.dates + result.entry) + .distinctBy { it.id } + .sortedByDescending { it.date } + current.copy( + createLoading = false, + dates = updated, + selectedDateId = result.entry.id, + newDate = "", + newTrainingStart = "", + newTrainingEnd = "", + successMessage = UiText(R.string.diary_create_success) + ) + } + } + + is DiaryCreateDateResult.Error -> { + _state.update { + it.copy( + createLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun updateSelectedDateTimes() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.updateDiaryTrainingTime( + clubId = clubId, + dateId = dateId, + trainingStart = _state.value.selectedTrainingStart.trim().ifBlank { null }, + trainingEnd = _state.value.selectedTrainingEnd.trim().ifBlank { null } + ) + ) { + is DiaryActionResult.Success -> { + _state.update { current -> + val updated = current.dates.map { entry -> + if (entry.id == dateId) { + entry.copy( + trainingStart = current.selectedTrainingStart.trim().ifBlank { null }, + trainingEnd = current.selectedTrainingEnd.trim().ifBlank { null } + ) + } else { + entry + } + } + current.copy( + actionLoading = false, + dates = updated, + successMessage = UiText(R.string.diary_update_success) + ) + } + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun deleteSelectedDate() { + viewModelScope.launch { + if (!canDeleteSelectedDate()) { + _state.update { + it.copy( + errorMessage = UiText(R.string.diary_date_cannot_be_deleted) + ) + } + return@launch + } + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.deleteDiaryDate(clubId = clubId, dateId = dateId)) { + is DiaryActionResult.Success -> { + _state.update { current -> + val remaining = current.dates.filterNot { it.id == dateId } + val nextSelected = remaining.firstOrNull() + current.copy( + actionLoading = false, + dates = remaining, + selectedDateId = nextSelected?.id, + selectedTrainingStart = nextSelected?.trainingStart.orEmpty(), + selectedTrainingEnd = nextSelected?.trainingEnd.orEmpty(), + participantMemberIds = emptyList(), + participantMapByMemberId = emptyMap(), + participantGroupByMemberId = emptyMap(), + activities = emptyList(), + planItems = emptyList(), + availableGroups = emptyList(), + accidents = emptyList(), + activityParticipantIdsByActivityId = emptyMap(), + selectedDateTags = emptyList(), + editingPlanItemId = null, + editingPlanActivity = "", + successMessage = UiText(R.string.diary_delete_success) + ) + } + val nextDateId = _state.value.selectedDateId + if (!nextDateId.isNullOrBlank()) { + loadParticipantsForDate(nextDateId) + loadActivitiesForDate(nextDateId) + loadPlanForDate(nextDateId) + loadGroupsForDate(nextDateId) + refreshAccidents() + loadNotesForSelection() + } + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun notifyDeleteNotAllowed() { + _state.update { + it.copy( + errorMessage = UiText(R.string.diary_date_cannot_be_deleted) + ) + } + } + + fun addExistingTagToSelectedDate(tagId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + if (tagId.isBlank()) return@launch + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.addTagToDiaryDate(clubId = clubId, diaryDateId = dateId, tagId = tagId)) { + is DiaryActionResult.Success -> { + val tag = _state.value.availableTags.firstOrNull { it.id == tagId } + if (tag != null) { + _state.update { current -> + current.copy( + actionLoading = false, + selectedDateTags = (current.selectedDateTags + tag).distinctBy { it.id }, + successMessage = UiText(R.string.diary_tag_added) + ) + } + syncSelectedDateTagsToDates() + } else { + _state.update { it.copy(actionLoading = false, successMessage = UiText(R.string.diary_tag_added)) } + } + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun removeTagFromSelectedDate(tagId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + if (tagId.isBlank()) return@launch + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.removeTagFromDiaryDate(clubId = clubId, diaryDateId = dateId, tagId = tagId)) { + is DiaryActionResult.Success -> { + _state.update { current -> + current.copy( + actionLoading = false, + selectedDateTags = current.selectedDateTags.filterNot { it.id == tagId }, + successMessage = UiText(R.string.diary_tag_removed) + ) + } + syncSelectedDateTagsToDates() + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun createTagAndAddToSelectedDate() { + viewModelScope.launch { + val name = _state.value.newTagName.trim() + if (name.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_tag_name_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.createTag(name)) { + is DiaryCreateTagResult.Success -> { + _state.update { current -> + current.copy( + actionLoading = false, + newTagName = "", + availableTags = (current.availableTags + result.tag).distinctBy { it.id } + ) + } + addExistingTagToSelectedDate(result.tag.id) + } + + is DiaryCreateTagResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun createGroup() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val name = _state.value.newGroupName.trim() + val lead = _state.value.newGroupLead.trim() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + if (name.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_group_name_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.createDiaryGroup(clubId = clubId, dateId = dateId, name = name, lead = lead)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + newGroupName = "", + newGroupLead = "", + successMessage = UiText(R.string.diary_group_created) + ) + } + loadGroupsForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun saveEditGroup() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val groupId = _state.value.editingGroupId?.trim().orEmpty() + val name = _state.value.editingGroupName.trim() + val lead = _state.value.editingGroupLead.trim() + if (dateId.isBlank() || groupId.isBlank()) return@launch + if (name.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_group_name_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.updateDiaryGroup( + groupId = groupId, + clubId = clubId, + dateId = dateId, + name = name, + lead = lead + ) + ) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + editingGroupId = null, + editingGroupName = "", + editingGroupLead = "", + successMessage = UiText(R.string.diary_group_updated) + ) + } + loadGroupsForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun deleteGroup(groupId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank() || groupId.isBlank()) return@launch + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.deleteDiaryGroup(groupId = groupId, clubId = clubId, dateId = dateId)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + successMessage = UiText(R.string.diary_group_deleted) + ) + } + loadGroupsForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun addGroupActivity() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val groupId = _state.value.newGroupActivityGroupId.trim() + val activity = _state.value.newGroupActivity.trim() + val predefinedActivityId = _state.value.selectedPredefinedActivityId + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + if (groupId.isBlank() || activity.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_group_and_activity)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.createDiaryGroupActivity( + clubId = clubId, + dateId = dateId, + groupId = groupId, + activity = activity, + predefinedActivityId = predefinedActivityId + ) + ) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + newGroupActivity = "", + newGroupActivityGroupId = "", + selectedPredefinedActivityId = null, + predefinedSuggestions = emptyList(), + successMessage = UiText(R.string.diary_group_activity_added) + ) + } + loadPlanForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun toggleActivityMember(activityId: String, memberId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + val participantId = _state.value.participantMapByMemberId[memberId] + val existingSet = _state.value.activityParticipantIdsByActivityId[activityId].orEmpty() + val isAssigned = participantId != null && existingSet.contains(participantId) + + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + if (isAssigned) { + when (val result = diaryService.removeActivityParticipant(clubId, activityId, participantId)) { + is DiaryActionResult.Success -> { + _state.update { + val updated = it.activityParticipantIdsByActivityId.toMutableMap() + updated[activityId] = updated[activityId].orEmpty() - participantId + it.copy( + actionLoading = false, + activityParticipantIdsByActivityId = updated, + successMessage = UiText(R.string.diary_activity_assignment_updated) + ) + } + } + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } else { + val resolvedParticipantId = participantId ?: run { + when (val addResult = diaryService.addParticipant(dateId, memberId)) { + is DiaryParticipantActionResult.Success -> addResult.participant?.id + is DiaryParticipantActionResult.Error -> null + } + } + if (resolvedParticipantId.isNullOrBlank()) { + _state.update { it.copy(actionLoading = false, errorMessage = UiText(R.string.diary_participant_add_failed)) } + return@launch + } + if (participantId == null) { + _state.update { + it.copy( + participantMemberIds = (it.participantMemberIds + memberId).distinct(), + participantMapByMemberId = it.participantMapByMemberId + (memberId to resolvedParticipantId) + ) + } + } + when (val result = diaryService.assignActivityParticipants(clubId, activityId, listOf(resolvedParticipantId))) { + is DiaryActionResult.Success -> { + _state.update { + val updated = it.activityParticipantIdsByActivityId.toMutableMap() + updated[activityId] = updated[activityId].orEmpty() + resolvedParticipantId + it.copy( + actionLoading = false, + activityParticipantIdsByActivityId = updated, + successMessage = UiText(R.string.diary_activity_assignment_updated) + ) + } + } + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + } + + fun openQuickAddDialog() { + _state.update { + it.copy( + showQuickAddDialog = true, + quickAddFirstName = "", + quickAddLastName = "", + quickAddBirthDate = "", + quickAddGender = "" + ) + } + } + + fun closeQuickAddDialog() { + _state.update { it.copy(showQuickAddDialog = false) } + } + + fun createAndAddMember() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val firstName = _state.value.quickAddFirstName.trim() + val lastName = _state.value.quickAddLastName.trim() + if (dateId.isBlank()) return@launch + if (firstName.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_quick_add_firstname_required)) } + return@launch + } + + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val saveResult = AppServices.membersService.saveMember( + clubId = clubId, + memberId = null, + firstName = firstName, + lastName = lastName, + active = true, + testMembership = true, + contacts = emptyList() + ) + ) { + is de.trainingstagebuch.app.repository.MemberSaveResult.Success -> { + when (val membersResult = diaryService.loadMembers(clubId)) { + is MembersResult.Success -> { + val newMember = membersResult.members.lastOrNull { it.firstName == firstName && it.lastName == lastName } + ?: membersResult.members.lastOrNull() + if (newMember != null) { + toggleParticipant(newMember.id) + } + _state.update { + it.copy( + actionLoading = false, + members = membersResult.members, + showQuickAddDialog = false, + successMessage = UiText(R.string.diary_quick_add_success) + ) + } + } + is MembersResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.dynamicOrFallback(membersResult.message) + ) + } + } + } + } + is de.trainingstagebuch.app.repository.MemberSaveResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(saveResult.message, saveResult.code) + ) + ) + } + } + } + } + } + + fun refreshAccidents() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) return@launch + when (val result = diaryService.loadAccidents(clubId, dateId)) { + is DiaryAccidentsResult.Success -> _state.update { it.copy(accidents = result.items) } + is DiaryAccidentsResult.Error -> Unit + } + } + } + + fun saveAccident() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val accident = _state.value.newAccident.trim() + if (dateId.isBlank()) return@launch + if (accident.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_accident_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.createAccident(clubId, dateId, accident)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + newAccident = "", + successMessage = UiText(R.string.diary_accident_saved) + ) + } + refreshAccidents() + } + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun openStatsForMember(memberId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + _state.update { it.copy(actionLoading = true, statsMemberId = memberId) } + when (val result = diaryService.loadMemberActivityStats(clubId, memberId)) { + is MemberActivityStatsResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + memberActivityStats = result.stats, + memberLastParticipations = result.lastParticipations + ) + } + } + is MemberActivityStatsResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun addPlanItem() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val activity = _state.value.newPlanActivity.trim() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + if (activity.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_plan_activity_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.addDiaryPlanItem( + clubId = clubId, + dateId = dateId, + activity = activity, + orderId = _state.value.planItems.size + ) + ) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + newPlanActivity = "", + successMessage = UiText(R.string.diary_plan_item_added) + ) + } + loadPlanForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun saveEditPlanItem() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val planItemId = _state.value.editingPlanItemId?.trim().orEmpty() + val activity = _state.value.editingPlanActivity.trim() + if (planItemId.isBlank()) return@launch + if (activity.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_plan_activity_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.updateDiaryPlanItem(clubId = clubId, planItemId = planItemId, activity = activity)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + editingPlanItemId = null, + editingPlanActivity = "", + successMessage = UiText(R.string.diary_plan_item_updated) + ) + } + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isNotBlank()) loadPlanForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun deletePlanItem(planItemId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + if (planItemId.isBlank()) return@launch + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.deleteDiaryPlanItem(clubId = clubId, planItemId = planItemId)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + successMessage = UiText(R.string.diary_plan_item_deleted) + ) + } + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isNotBlank()) loadPlanForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun movePlanItemUp(planItemId: String) { + movePlanItem(planItemId = planItemId, direction = -1) + } + + fun movePlanItemDown(planItemId: String) { + movePlanItem(planItemId = planItemId, direction = 1) + } + + fun addActivity() { + viewModelScope.launch { + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + val description = _state.value.newActivityDescription.trim() + if (description.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_activity_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.addActivity(dateId = dateId, description = description)) { + is DiaryAddActivityResult.Success -> { + _state.update { current -> + current.copy( + actionLoading = false, + activities = (current.activities + result.activity).distinctBy { it.id }, + newActivityDescription = "", + successMessage = UiText(R.string.diary_activity_added) + ) + } + } + + is DiaryAddActivityResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun toggleParticipant(memberId: String) { + viewModelScope.launch { + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + + val currentlyParticipant = _state.value.participantMemberIds.contains(memberId) + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + + if (currentlyParticipant) { + when (val result = diaryService.removeParticipant(dateId = dateId, memberId = memberId)) { + is DiaryActionResult.Success -> { + _state.update { current -> + current.copy( + actionLoading = false, + participantMemberIds = current.participantMemberIds.filterNot { it == memberId }, + participantMapByMemberId = current.participantMapByMemberId - memberId, + participantGroupByMemberId = current.participantGroupByMemberId - memberId, + selectedNoteMemberId = if (current.selectedNoteMemberId == memberId) { + current.participantMemberIds.filterNot { it == memberId }.firstOrNull() + } else { + current.selectedNoteMemberId + }, + successMessage = UiText(R.string.diary_participant_removed) + ) + } + loadNotesForSelection() + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } else { + when (val result = diaryService.addParticipant(dateId = dateId, memberId = memberId)) { + is DiaryParticipantActionResult.Success -> { + _state.update { current -> + val updatedParticipants = (current.participantMemberIds + memberId).distinct() + current.copy( + actionLoading = false, + participantMemberIds = updatedParticipants, + participantMapByMemberId = if (result.participant != null) { + current.participantMapByMemberId + (memberId to result.participant.id) + } else { + current.participantMapByMemberId + }, + participantGroupByMemberId = if (result.participant?.groupId != null) { + current.participantGroupByMemberId + (memberId to result.participant.groupId) + } else { + current.participantGroupByMemberId + }, + selectedNoteMemberId = current.selectedNoteMemberId ?: updatedParticipants.firstOrNull(), + successMessage = UiText(R.string.diary_participant_added) + ) + } + loadNotesForSelection() + } + + is DiaryParticipantActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + } + + private suspend fun loadParticipantsForDate(dateId: String) { + when (val result = diaryService.loadParticipants(dateId)) { + is DiaryParticipantsResult.Success -> { + _state.update { + val memberIds = result.participants.map { participant -> participant.memberId } + it.copy( + participantMemberIds = memberIds, + participantMapByMemberId = result.participants.associate { participant -> + participant.memberId to participant.id + }, + participantGroupByMemberId = result.participants.associate { participant -> + participant.memberId to participant.groupId + }, + selectedNoteMemberId = it.selectedNoteMemberId + ?.takeIf { existing -> memberIds.contains(existing) } + ?: memberIds.firstOrNull() + ) + } + } + + is DiaryParticipantsResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + fun onParticipantGroupChanged(memberId: String, groupId: String?) { + viewModelScope.launch { + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_date_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.updateParticipantGroup(dateId = dateId, memberId = memberId, groupId = groupId)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + participantGroupByMemberId = it.participantGroupByMemberId + (memberId to groupId), + successMessage = UiText(R.string.diary_participant_group_updated) + ) + } + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun addNote() { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val memberId = _state.value.selectedNoteMemberId?.trim().orEmpty() + val content = _state.value.newNoteContent.trim() + if (dateId.isBlank() || memberId.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_select_note_member_required)) } + return@launch + } + if (content.isBlank()) { + _state.update { it.copy(errorMessage = UiText(R.string.diary_note_required)) } + return@launch + } + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when ( + val result = diaryService.addDiaryMemberNote( + clubId = clubId, + diaryDateId = dateId, + memberId = memberId, + content = content + ) + ) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + newNoteContent = "", + successMessage = UiText(R.string.diary_note_added) + ) + } + loadNotesForSelection() + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + fun deleteNote(noteId: String) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.deleteDiaryMemberNote(clubId = clubId, noteId = noteId)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + successMessage = UiText(R.string.diary_note_deleted) + ) + } + loadNotesForSelection() + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + private suspend fun loadNotesForSelection() { + val clubId = resolveClubId() ?: return + val dateId = _state.value.selectedDateId?.trim().orEmpty() + val memberId = _state.value.selectedNoteMemberId?.trim().orEmpty() + if (dateId.isBlank() || memberId.isBlank()) { + _state.update { it.copy(notes = emptyList()) } + return + } + when ( + val result = diaryService.loadDiaryMemberNotes( + clubId = clubId, + diaryDateId = dateId, + memberId = memberId + ) + ) { + is MemberNotesResult.Success -> { + _state.update { it.copy(notes = result.notes) } + } + + is MemberNotesResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + private suspend fun loadActivitiesForDate(dateId: String) { + when (val result = diaryService.loadActivities(dateId)) { + is DiaryActivitiesResult.Success -> { + _state.update { it.copy(activities = result.activities) } + } + + is DiaryActivitiesResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + private suspend fun loadAvailableTags() { + when (val result = diaryService.loadTags()) { + is DiaryTagsResult.Success -> { + _state.update { it.copy(availableTags = result.tags) } + } + + is DiaryTagsResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + private fun movePlanItem(planItemId: String, direction: Int) { + viewModelScope.launch { + val clubId = resolveClubId() ?: return@launch + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) return@launch + val items = _state.value.planItems + val currentIndex = items.indexOfFirst { it.id == planItemId } + if (currentIndex < 0) return@launch + val targetIndex = currentIndex + direction + if (targetIndex !in items.indices) return@launch + + _state.update { it.copy(actionLoading = true, errorMessage = null, successMessage = null) } + when (val result = diaryService.updateDiaryPlanItemOrder(clubId = clubId, planItemId = planItemId, order = targetIndex)) { + is DiaryActionResult.Success -> { + _state.update { + it.copy( + actionLoading = false, + successMessage = UiText(R.string.diary_plan_order_updated) + ) + } + loadPlanForDate(dateId) + } + + is DiaryActionResult.Error -> { + _state.update { + it.copy( + actionLoading = false, + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + } + + private suspend fun loadPlanForDate(dateId: String) { + val clubId = resolveClubId() ?: return + when (val result = diaryService.loadDiaryPlan(clubId = clubId, dateId = dateId)) { + is DiaryPlanResult.Success -> { + _state.update { it.copy(planItems = result.items) } + result.items.forEach { item -> + when (val assignmentResult = diaryService.loadActivityParticipantIds(clubId = clubId, activityId = item.id)) { + is DiaryActivityParticipantsResult.Success -> { + _state.update { + it.copy( + activityParticipantIdsByActivityId = it.activityParticipantIdsByActivityId + (item.id to assignmentResult.participantIds) + ) + } + } + is DiaryActivityParticipantsResult.Error -> Unit + } + } + } + + is DiaryPlanResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + private fun searchPredefinedActivities(term: String) { + viewModelScope.launch { + val query = term.trim() + if (query.length < 2) { + _state.update { it.copy(predefinedSuggestions = emptyList(), selectedPredefinedActivityId = null) } + return@launch + } + when (val result = diaryService.searchPredefinedActivities(query)) { + is PredefinedActivitiesResult.Success -> { + val chosen = result.items.firstOrNull { item -> + item.name.equals(query, ignoreCase = true) || + item.code?.equals(query, ignoreCase = true) == true + } + _state.update { + it.copy( + predefinedSuggestions = result.items, + selectedPredefinedActivityId = chosen?.id + ) + } + } + is PredefinedActivitiesResult.Error -> Unit + } + } + } + + private suspend fun loadGroupsForDate(dateId: String) { + val clubId = resolveClubId() ?: return + when (val result = diaryService.loadDiaryGroups(clubId = clubId, dateId = dateId)) { + is DiaryGroupsResult.Success -> { + _state.update { it.copy(availableGroups = result.groups) } + } + + is DiaryGroupsResult.Error -> { + _state.update { + it.copy( + errorMessage = ViewModelStateUtils.normalizeUiText( + ErrorMessageFormatter.dynamicWithOptionalCode(result.message, result.code) + ) + ) + } + } + } + } + + private fun observeSocketEvents() { + viewModelScope.launch { + SocketManager.events.collect { event -> + val dateId = _state.value.selectedDateId?.trim().orEmpty() + if (dateId.isBlank()) return@collect + when (event.type) { + SocketEventType.ParticipantAdded, + SocketEventType.ParticipantRemoved, + SocketEventType.ParticipantUpdated -> { + loadParticipantsForDate(dateId) + } + SocketEventType.DiaryNoteAdded, + SocketEventType.DiaryNoteDeleted, + SocketEventType.DiaryNoteUpdated -> { + loadNotesForSelection() + } + SocketEventType.DiaryTagAdded, + SocketEventType.DiaryTagRemoved, + SocketEventType.DiaryDateUpdated -> { + load() + } + SocketEventType.ActivityChanged, + SocketEventType.ActivityMemberAdded, + SocketEventType.ActivityMemberRemoved, + SocketEventType.GroupChanged -> { + loadPlanForDate(dateId) + } + SocketEventType.MemberChanged -> { + val clubId = resolveClubId() ?: return@collect + when (val membersResult = diaryService.loadMembers(clubId)) { + is MembersResult.Success -> _state.update { it.copy(members = membersResult.members) } + is MembersResult.Error -> Unit + } + } + else -> Unit + } + } + } + } + + private fun syncSelectedDateTagsToDates() { + _state.update { current -> + val selectedDateId = current.selectedDateId ?: return@update current + current.copy( + dates = current.dates.map { date -> + if (date.id == selectedDateId) { + date.copy(tags = current.selectedDateTags) + } else { + date + } + } + ) + } + } + + private fun canDeleteSelectedDate(): Boolean { + val current = _state.value + return current.planItems.isEmpty() && + current.participantMemberIds.isEmpty() && + current.activities.isEmpty() && + current.notes.isEmpty() + } + + private suspend fun resolveClubId(): String? { + val clubId = authService.session.first().currentClub?.trim().orEmpty() + if (clubId.isBlank()) { + _state.update { + it.copy( + status = DiaryStatus.Error, + errorMessage = UiText(R.string.error_no_current_club) + ) + } + return null + } + return clubId + } +} + +data class DiaryUiState( + val status: DiaryStatus = DiaryStatus.Idle, + val dates: List = emptyList(), + val members: List = emptyList(), + val participantMemberIds: List = emptyList(), + val participantMapByMemberId: Map = emptyMap(), + val participantGroupByMemberId: Map = emptyMap(), + val availableGroups: List = emptyList(), + val newGroupName: String = "", + val newGroupLead: String = "", + val newGroupActivity: String = "", + val newGroupActivityGroupId: String = "", + val predefinedSuggestions: List = emptyList(), + val selectedPredefinedActivityId: String? = null, + val editingGroupId: String? = null, + val editingGroupName: String = "", + val editingGroupLead: String = "", + val activities: List = emptyList(), + val newActivityDescription: String = "", + val selectedNoteMemberId: String? = null, + val notes: List = emptyList(), + val newNoteContent: String = "", + val availableTags: List = emptyList(), + val selectedDateTags: List = emptyList(), + val newTagName: String = "", + val planItems: List = emptyList(), + val activityParticipantIdsByActivityId: Map> = emptyMap(), + val newPlanActivity: String = "", + val editingPlanItemId: String? = null, + val editingPlanActivity: String = "", + val accidents: List = emptyList(), + val newAccident: String = "", + val showQuickAddDialog: Boolean = false, + val quickAddFirstName: String = "", + val quickAddLastName: String = "", + val quickAddBirthDate: String = "", + val quickAddGender: String = "", + val statsMemberId: String? = null, + val memberActivityStats: List = emptyList(), + val memberLastParticipations: List = emptyList(), + val selectedDateId: String? = null, + val newDate: String = "", + val newTrainingStart: String = "", + val newTrainingEnd: String = "", + val createLoading: Boolean = false, + val selectedTrainingStart: String = "", + val selectedTrainingEnd: String = "", + val actionLoading: Boolean = false, + val errorMessage: UiText? = null, + val successMessage: UiText? = null +) + +enum class DiaryStatus { + Idle, + Loading, + Success, + Error +} diff --git a/android-app/app/src/main/res/values-en/strings.xml b/android-app/app/src/main/res/values-en/strings.xml new file mode 100644 index 00000000..064a0d6f --- /dev/null +++ b/android-app/app/src/main/res/values-en/strings.xml @@ -0,0 +1,339 @@ + + Trainingstagebuch + Login + Registration + Email + Password + Confirm password + Sign in + Register + New here? Register now + Already have an account? Go to login + Forgot password? + Forgot password + Enter your email. We will send you a reset link. + Send reset link + If an account exists, a reset email has been sent + Reset password + New password + Confirm password + Save new password + Password changed successfully + Back to login + Activate account + Retry activation + Activating your account... + Account activated successfully + + Home / Dashboard + Loading home data... + Try again + Clubs: %1$d + First club: %1$s + Open current club + Open club settings + Open approvals + Open diary + Create club + Club name + Create club + Back + Please enter a meaningful club name. + Club created successfully + Loading club... + Club: %1$s + Active members: %1$d + You currently do not have access to this club. + Access request already sent. Please wait for approval. + Request access + Open access requests: %1$d + Open requests + Members + Club settings + Loading club settings... + Greeting text + Association member number + Save settings + Club settings saved + Settings + Groups + Times + Group name + Create group + Please enter a group name + Training group created + Training group deleted + Training group updated + Group ID + Weekday (0-6) + Start (HH:mm) + End (HH:mm) + Create time + Please provide group, weekday, start and end time + Training time created + Training time deleted + Training time updated + Day %1$d: %2$s - %3$s + Edit + Save + Cancel + Delete + Edit training group + Edit training time + Sunday + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Pending approvals + Loading requests... + No pending approval requests + Approve + Reject + Request updated + Diary + Loading diary data... + No diary entries yet + Date (YYYY-MM-DD) + Training start (HH:mm) + Training end (HH:mm) + Create date + Times: %1$s + Diary date created + Please provide a date + Please select a date first + Edit selected date + Save times + Delete date + Training times updated + Diary date deleted + Participants (%1$d) + No members available + Add + Remove + No group + Groups (%1$d) + Group name + Lead + Create group + No groups available + Please enter a group name + Group created + Group updated + Group deleted + Edit + Delete + Save + Cancel + Group activity + Activity (group) + Add group activity + Please select group and activity + Group activity added + Activity assignment updated + Participant could not be created + Quick add + Quick add member + First name + Last name + Birth date + Gender + Create + add + Cancel + First name is required + Member created and added + Accidents (%1$d) + Accident description + Save accident + Please enter accident description + Accident saved + Stats + Member stats + Last participations + Activities + No data + Close + Participant added + Participant removed + Participant group updated + Activities (%1$d) + No activities available + Activity description + Add activity + Please enter an activity + Activity added + Notes (%1$d) + No participants available for notes + New note + Add note + No notes available + Delete + Please enter a note + Please select participant and date + Note saved + Note deleted + Date tags (%1$d) + New tag + Create and attach tag + No tags available + Attach + Remove + Please enter a tag name + Tag attached + Tag removed + Training plan (%1$d) + New plan activity + Add plan item + No plan items available + Plan item added + Plan item updated + Plan item deleted + Plan order updated + Please enter a plan activity + Timeblock + Up + Down + Edit + Delete + Save + Cancel + Date can only be deleted when it has no content. + Delete date + Do you really want to delete this date? + Delete + Cancel + Members + New + Edit + Create member + Edit member + First name + Last name + Contacts + Active: %1$d + Test: %1$d + Inactive: %1$d + Phone + Email + Add phone + Add email + Remove + Training groups + No groups assigned + Select group + Add to group + Remove from group + Notes + New note + Save note + Delete + No notes available + Group assigned + Group removed + Create member + Save member + Member saved + Deactivate + Remove test status + Member deactivated + Test status removed + Upload image + Set primary + Delete + Primary + No images available + Image uploaded + Image updated + Loading members... + Members view ready + No members found + Search by name + Show inactive + Sort: %1$s + Name A-Z + Name Z-A + Status + Active + Test member + Inactive + OK + Logout + + Unknown error + Request timed out. Please try again. + No network connection. Please check your connection. + Network error. Please try again. + Invalid request (%1$d). Please check your input. + Server error (%1$d). Please try again later. + HTTP error (%1$d) + Please enter email and password + Please fill all fields + Passwords do not match + Please enter email + Password must be at least 6 characters + Invalid reset token + Invalid activation code + First name and last name are required + Please select a member first + Please select a member first + Note must not be empty + No active club selected + Please provide server, transfer endpoint and transfer template + Missing authentication + Registration successful + %1$s + %1$s + %1$s (%2$s) + Member Transfer Settings + Loading transfer configuration... + Server URL + Login endpoint + Login format (json/form) + Login username + Login password + Login additional field 1 (key:value) + Login additional field 2 (key:value) + Transfer endpoint + Transfer method + Transfer format (json/form) + Transfer template (JSON) + Use bulk mode + Bulk wrapper template + Save configuration + Delete configuration + Execute transfer + Transfer configuration saved + Transfer configuration deleted + Member transfer executed successfully + Transfer summary + Transferred: %1$d of %2$d + Invalid members: %1$s + Errors: %1$s + + Activate: %1$s + Forgot Password + Reset Password: %1$s + Create Club + Show Club: %1$s + Members + Diary + Pending Approvals + Schedule + Tournaments + Training Stats + Club Settings + Predefined Activities + MyTischtennis Account + Team Management + Permissions + Logs + Member Transfer Settings + Personal Settings + Impressum + Data Privacy + • %1$s + %1$s: %2$s + %1$s: %2$d + diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..7406225e --- /dev/null +++ b/android-app/app/src/main/res/values/strings.xml @@ -0,0 +1,339 @@ + + Trainingstagebuch + Anmeldung + Registrierung + E-Mail + Passwort + Passwort wiederholen + Einloggen + Registrieren + Neu hier? Jetzt registrieren + Schon ein Konto? Zum Login + Passwort vergessen? + Passwort vergessen + Gib deine E-Mail ein. Wir senden dir einen Link zum Zurücksetzen. + Reset-Link senden + Falls ein Konto existiert, wurde eine E-Mail zum Zurücksetzen gesendet + Passwort zurücksetzen + Neues Passwort + Passwort bestätigen + Neues Passwort speichern + Passwort erfolgreich geändert + Zurück zum Login + Account aktivieren + Erneut aktivieren + Aktivierung wird ausgeführt... + Account wurde erfolgreich aktiviert + + Home / Dashboard + Lade Home-Daten... + Erneut versuchen + Clubs: %1$d + Erster Club: %1$s + Aktuellen Club öffnen + Club-Einstellungen öffnen + Freigaben öffnen + Tagebuch öffnen + Verein erstellen + Vereinsname + Verein anlegen + Zurück + Bitte gib dem Verein einen aussagekräftigen Namen. + Verein erfolgreich erstellt + Lade Club... + Club: %1$s + Aktive Mitglieder: %1$d + Du hast aktuell keinen Zugriff auf diesen Club. + Zugriffsanfrage bereits gestellt. Bitte auf Freigabe warten. + Zugriff anfragen + Offene Zugriffsanfragen: %1$d + Offene Anfragen + Mitglieder + Club Settings + Lade Club-Einstellungen... + Begrüßungstext + Verband-Mitgliedsnummer + Einstellungen speichern + Club-Einstellungen gespeichert + Settings + Gruppen + Zeiten + Gruppenname + Gruppe anlegen + Bitte Gruppennamen eingeben + Trainingsgruppe erstellt + Trainingsgruppe gelöscht + Trainingsgruppe aktualisiert + Gruppen-ID + Wochentag (0-6) + Start (HH:mm) + Ende (HH:mm) + Zeit anlegen + Bitte Gruppe, Wochentag sowie Start/Ende korrekt ausfüllen + Trainingszeit erstellt + Trainingszeit gelöscht + Trainingszeit aktualisiert + Tag %1$d: %2$s - %3$s + Bearbeiten + Speichern + Abbrechen + Löschen + Trainingsgruppe bearbeiten + Trainingszeit bearbeiten + Sonntag + Montag + Dienstag + Mittwoch + Donnerstag + Freitag + Samstag + Ausstehende Freigaben + Lade Anfragen... + Keine offenen Freigabe-Anfragen + Freigeben + Ablehnen + Anfrage aktualisiert + Tagebuch + Lade Tagebuchdaten... + Noch keine Tagebucheinträge vorhanden + Datum (YYYY-MM-DD) + Trainingsstart (HH:mm) + Trainingsende (HH:mm) + Datum anlegen + Zeiten: %1$s + Tagebuchdatum erstellt + Bitte ein Datum angeben + Bitte zuerst ein Datum auswählen + Ausgewähltes Datum bearbeiten + Zeiten speichern + Datum löschen + Trainingszeiten aktualisiert + Tagebuchdatum gelöscht + Teilnehmer (%1$d) + Keine Mitglieder verfügbar + Hinzufügen + Entfernen + Ohne Gruppe + Gruppen (%1$d) + Gruppenname + Leitung + Gruppe erstellen + Keine Gruppen vorhanden + Bitte Gruppennamen eingeben + Gruppe erstellt + Gruppe aktualisiert + Gruppe gelöscht + Bearbeiten + Löschen + Speichern + Abbrechen + Gruppenaktivität + Aktivität (Gruppe) + Gruppenaktivität hinzufügen + Bitte Gruppe und Aktivität wählen + Gruppenaktivität hinzugefügt + Aktivitätszuordnung aktualisiert + Teilnehmer konnte nicht erstellt werden + Schnell hinzufügen + Mitglied schnell hinzufügen + Vorname + Nachname + Geburtsdatum + Geschlecht + Erstellen + hinzufügen + Abbrechen + Vorname ist erforderlich + Mitglied erstellt und hinzugefügt + Unfälle (%1$d) + Unfallbeschreibung + Unfall speichern + Bitte Unfallbeschreibung eingeben + Unfall gespeichert + Statistik + Mitgliederstatistik + Letzte Teilnahmen + Aktivitäten + Keine Daten + Schließen + Teilnehmer hinzugefügt + Teilnehmer entfernt + Teilnehmer-Gruppe aktualisiert + Aktivitäten (%1$d) + Keine Aktivitäten vorhanden + Aktivitätsbeschreibung + Aktivität hinzufügen + Bitte eine Aktivität eingeben + Aktivität hinzugefügt + Notizen (%1$d) + Keine Teilnehmer für Notizen ausgewählt + Neue Notiz + Notiz hinzufügen + Keine Notizen vorhanden + Löschen + Bitte eine Notiz eingeben + Bitte Teilnehmer und Datum auswählen + Notiz gespeichert + Notiz gelöscht + Datumstags (%1$d) + Neues Tag + Tag erstellen und zuordnen + Keine Tags verfügbar + Zuordnen + Entfernen + Bitte einen Tag-Namen eingeben + Tag zugeordnet + Tag entfernt + Trainingsplan (%1$d) + Neue Plan-Aktivität + Plan-Item hinzufügen + Keine Plan-Items vorhanden + Plan-Item hinzugefügt + Plan-Item aktualisiert + Plan-Item gelöscht + Plan-Reihenfolge aktualisiert + Bitte eine Plan-Aktivität eingeben + Zeitblock + Hoch + Runter + Bearbeiten + Löschen + Speichern + Abbrechen + Datum kann nur gelöscht werden, wenn keine Inhalte vorhanden sind. + Datum löschen + Möchtest du dieses Datum wirklich löschen? + Löschen + Abbrechen + Mitglieder + Neu + Bearbeiten + Neues Mitglied + Mitglied bearbeiten + Vorname + Nachname + Kontakte + Aktiv: %1$d + Test: %1$d + Inaktiv: %1$d + Telefon + E-Mail + Telefon hinzufügen + E-Mail hinzufügen + Entfernen + Trainingsgruppen + Keine Gruppen zugewiesen + Gruppe auswählen + Zu Gruppe hinzufügen + Aus Gruppe entfernen + Notizen + Neue Notiz + Notiz speichern + Löschen + Keine Notizen vorhanden + Gruppe zugewiesen + Gruppe entfernt + Mitglied anlegen + Mitglied speichern + Mitglied gespeichert + Deaktivieren + Teststatus entfernen + Mitglied deaktiviert + Teststatus entfernt + Bild hochladen + Primär + Löschen + Primär + Keine Bilder vorhanden + Bild hochgeladen + Bild aktualisiert + Lade Mitglieder... + Mitgliederansicht bereit + Keine Mitglieder gefunden + Suche nach Name + Inaktive anzeigen + Sortierung: %1$s + Name A-Z + Name Z-A + Status + Aktiv + Testmitglied + Inaktiv + OK + Logout + + Unbekannter Fehler + Zeitüberschreitung. Bitte erneut versuchen. + Keine Netzwerkverbindung. Bitte Verbindung prüfen. + Netzwerkfehler. Bitte erneut versuchen. + Ungültige Anfrage (%1$d). Bitte Eingaben prüfen. + Serverfehler (%1$d). Bitte später erneut versuchen. + HTTP-Fehler (%1$d) + Bitte E-Mail und Passwort eingeben + Bitte alle Felder ausfüllen + Passwörter stimmen nicht überein + Bitte E-Mail eingeben + Passwort muss mindestens 6 Zeichen haben + Ungültiger Reset-Token + Ungültiger Aktivierungscode + Vorname und Nachname sind erforderlich + Bitte zuerst ein Mitglied auswählen + Bitte zuerst ein Mitglied auswählen + Notiz darf nicht leer sein + Kein aktiver Club gewählt + Bitte Server, Transfer-Endpunkt und Transfer-Template ausfüllen + Authentifizierung fehlt + Registrierung erfolgreich + %1$s + %1$s + %1$s (%2$s) + Member Transfer Settings + Lade Transfer-Konfiguration... + Server URL + Login Endpoint + Login Format (json/form) + Login Username + Login Password + Login Zusatzfeld 1 (key:value) + Login Zusatzfeld 2 (key:value) + Transfer Endpoint + Transfer Methode + Transfer Format (json/form) + Transfer Template (JSON) + Bulk-Modus nutzen + Bulk Wrapper Template + Konfiguration speichern + Konfiguration löschen + Transfer ausführen + Transfer-Konfiguration gespeichert + Transfer-Konfiguration gelöscht + Member-Transfer erfolgreich gestartet + Transfer Ergebnis + Übertragen: %1$d von %2$d + Ungültige Mitglieder: %1$s + Fehler: %1$s + + Activate: %1$s + Forgot Password + Reset Password: %1$s + Create Club + Show Club: %1$s + Members + Diary + Pending Approvals + Schedule + Tournaments + Training Stats + Club Settings + Predefined Activities + MyTischtennis Account + Team Management + Permissions + Logs + Member Transfer Settings + Personal Settings + Impressum + Datenschutz + • %1$s + %1$s: %2$s + %1$s: %2$d +