Implement cross-club friendly match concept with invitations and shared matches
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 49s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 49s
- Added controllers for handling friendly match invitations and shared matches. - Created migration scripts for `friendly_match_invitation` and `friendly_match_shared` tables. - Developed models for `FriendlyMatchInvitation` and `FriendlyMatchShared`. - Established routes for managing invitations and shared matches. - Implemented services for business logic related to invitations and shared matches. - Documented the concept plan for the new feature including API endpoints and data models.
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
# composeApp (Play Store / „Über die App“-Build)
|
||||
appVersionCode = "18"
|
||||
appVersionName = "1.6.2"
|
||||
appVersionCode = "20"
|
||||
appVersionName = "1.7.0"
|
||||
agp = "9.2.1"
|
||||
android-compileSdk = "35"
|
||||
android-minSdk = "24"
|
||||
|
||||
@@ -4,6 +4,8 @@ import de.tsschulz.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.LeaguePlayerStatDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.LeagueTableRowDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchSaveBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchInvitationCreateBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchInvitationDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.UpdateMatchPlayersBody
|
||||
import io.ktor.client.call.body
|
||||
@@ -50,6 +52,10 @@ class MatchesApi(
|
||||
return client.http.get("/api/friendly-matches/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun listSharedFriendlyMatches(clubId: Int): List<ScheduleMatchDto> {
|
||||
return client.http.get("/api/friendly-matches/shared/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun createFriendlyMatch(clubId: Int, body: FriendlyMatchSaveBody): ScheduleMatchDto {
|
||||
return client.http.post("/api/friendly-matches/$clubId") {
|
||||
setBody(body)
|
||||
@@ -62,7 +68,45 @@ class MatchesApi(
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun updateSharedFriendlyMatch(clubId: Int, matchId: Int, body: FriendlyMatchSaveBody): ScheduleMatchDto {
|
||||
return client.http.put("/api/friendly-matches/shared/$clubId/$matchId") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun updateSharedFriendlyMatchPlayers(clubId: Int, matchId: Int, body: UpdateMatchPlayersBody) {
|
||||
client.http.patch("/api/friendly-matches/shared/$clubId/$matchId/players") {
|
||||
setBody(body)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteFriendlyMatch(clubId: Int, matchId: Int) {
|
||||
client.http.delete("/api/friendly-matches/$clubId/$matchId")
|
||||
}
|
||||
|
||||
suspend fun deleteSharedFriendlyMatch(clubId: Int, matchId: Int) {
|
||||
client.http.delete("/api/friendly-matches/shared/$clubId/$matchId")
|
||||
}
|
||||
|
||||
suspend fun createFriendlyInvitation(clubId: Int, body: FriendlyMatchInvitationCreateBody): FriendlyMatchInvitationDto {
|
||||
return client.http.post("/api/friendly-match-invitations/$clubId") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun listIncomingFriendlyInvitations(clubId: Int): List<FriendlyMatchInvitationDto> {
|
||||
return client.http.get("/api/friendly-match-invitations/$clubId/incoming").body()
|
||||
}
|
||||
|
||||
suspend fun listOutgoingFriendlyInvitations(clubId: Int): List<FriendlyMatchInvitationDto> {
|
||||
return client.http.get("/api/friendly-match-invitations/$clubId/outgoing").body()
|
||||
}
|
||||
|
||||
suspend fun acceptFriendlyInvitation(clubId: Int, invitationId: Int): ScheduleMatchDto {
|
||||
return client.http.post("/api/friendly-match-invitations/$clubId/$invitationId/accept").body()
|
||||
}
|
||||
|
||||
suspend fun declineFriendlyInvitation(clubId: Int, invitationId: Int): FriendlyMatchInvitationDto {
|
||||
return client.http.post("/api/friendly-match-invitations/$clubId/$invitationId/decline").body()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,11 @@ class SocketService(private val socketUrl: String) {
|
||||
"tournament:changed",
|
||||
"schedule:match:updated",
|
||||
"schedule:match-report:submitted",
|
||||
"friendly:invitation:created",
|
||||
"friendly:invitation:accepted",
|
||||
"friendly:invitation:declined",
|
||||
"friendly:shared:match:updated",
|
||||
"friendly:shared:match:deleted",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +83,13 @@ data class ScheduleLeagueDetailsDto(
|
||||
data class ScheduleMatchDto(
|
||||
val id: Int,
|
||||
val friendlyMatchId: Int? = null,
|
||||
val sharedMatchId: Int? = null,
|
||||
val isFriendly: Boolean = false,
|
||||
val isSharedFriendly: Boolean = false,
|
||||
val date: String? = null,
|
||||
val time: String? = null,
|
||||
val homeClubId: Int? = null,
|
||||
val guestClubId: Int? = null,
|
||||
val homeTeamId: Int? = null,
|
||||
val guestTeamId: Int? = null,
|
||||
val locationId: Int? = null,
|
||||
@@ -150,6 +154,30 @@ data class FriendlyMatchSaveBody(
|
||||
val resultDetails: List<FriendlyResultRowDto> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FriendlyMatchInvitationDto(
|
||||
val id: Int,
|
||||
val fromClubId: Int,
|
||||
val toClubId: Int,
|
||||
val proposedDate: String,
|
||||
val proposedStartTime: String? = null,
|
||||
val proposedMatchName: String,
|
||||
val message: String? = null,
|
||||
val status: String = "pending",
|
||||
val createdByUserId: Int? = null,
|
||||
val acceptedByUserId: Int? = null,
|
||||
val acceptedAt: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FriendlyMatchInvitationCreateBody(
|
||||
val toClubId: Int,
|
||||
val date: String,
|
||||
val startTime: String? = null,
|
||||
val matchName: String,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LeagueTableRowDto(
|
||||
val teamId: Int,
|
||||
|
||||
@@ -4,6 +4,8 @@ import de.tsschulz.tt_tagebuch.shared.api.ClubTeamsApi
|
||||
import de.tsschulz.tt_tagebuch.shared.api.MatchesApi
|
||||
import de.tsschulz.tt_tagebuch.shared.api.ScheduleLogic
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ClubTeamDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchInvitationCreateBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchInvitationDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.FriendlyMatchSaveBody
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.LeagueTableRowDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
|
||||
@@ -23,6 +25,8 @@ data class ScheduleState(
|
||||
val allMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val overallMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val friendlyMatches: List<ScheduleMatchDto> = emptyList(),
|
||||
val incomingFriendlyInvitations: List<FriendlyMatchInvitationDto> = emptyList(),
|
||||
val outgoingFriendlyInvitations: List<FriendlyMatchInvitationDto> = emptyList(),
|
||||
val leagueTable: List<LeagueTableRowDto> = emptyList(),
|
||||
val matchScope: ScheduleMatchScope = ScheduleMatchScope.Own,
|
||||
val otherTeamName: String = "",
|
||||
@@ -78,7 +82,10 @@ class ScheduleManager(
|
||||
}
|
||||
ScheduleViewMode.Overall -> loadOverallSchedule(clubId)
|
||||
ScheduleViewMode.Adult -> loadAdultSchedule(clubId)
|
||||
ScheduleViewMode.Friendly -> loadFriendlyMatches(clubId)
|
||||
ScheduleViewMode.Friendly -> {
|
||||
loadFriendlyMatches(clubId)
|
||||
loadFriendlyInvitations(clubId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,8 +255,11 @@ class ScheduleManager(
|
||||
)
|
||||
}
|
||||
try {
|
||||
val matches = matchesApi.listFriendlyMatches(clubId)
|
||||
_state.update { it.copy(friendlyMatches = matches, isLoading = false, error = null) }
|
||||
val localMatches = matchesApi.listFriendlyMatches(clubId)
|
||||
val sharedMatches = matchesApi.listSharedFriendlyMatches(clubId)
|
||||
val combined = ScheduleLogic.sortMatches(localMatches + sharedMatches)
|
||||
_state.update { it.copy(friendlyMatches = combined, isLoading = false, error = null) }
|
||||
loadFriendlyInvitations(clubId)
|
||||
} catch (t: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -294,8 +304,44 @@ class ScheduleManager(
|
||||
_state.update { it.copy(friendlyMatches = ScheduleLogic.sortMatches(it.friendlyMatches + saved)) }
|
||||
}
|
||||
|
||||
suspend fun loadFriendlyInvitations(clubId: Int) {
|
||||
try {
|
||||
val incoming = matchesApi.listIncomingFriendlyInvitations(clubId)
|
||||
val outgoing = matchesApi.listOutgoingFriendlyInvitations(clubId)
|
||||
_state.update {
|
||||
it.copy(
|
||||
incomingFriendlyInvitations = incoming,
|
||||
outgoingFriendlyInvitations = outgoing,
|
||||
)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
// Invitations sind ein Zusatzbereich; Fehler sollen den Hauptscreen nicht blockieren.
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createFriendlyInvitation(clubId: Int, body: FriendlyMatchInvitationCreateBody) {
|
||||
matchesApi.createFriendlyInvitation(clubId, body)
|
||||
loadFriendlyInvitations(clubId)
|
||||
}
|
||||
|
||||
suspend fun acceptFriendlyInvitation(clubId: Int, invitationId: Int) {
|
||||
val sharedMatch = matchesApi.acceptFriendlyInvitation(clubId, invitationId)
|
||||
_state.update { it.copy(friendlyMatches = ScheduleLogic.sortMatches(it.friendlyMatches + sharedMatch)) }
|
||||
loadFriendlyInvitations(clubId)
|
||||
}
|
||||
|
||||
suspend fun declineFriendlyInvitation(clubId: Int, invitationId: Int) {
|
||||
matchesApi.declineFriendlyInvitation(clubId, invitationId)
|
||||
loadFriendlyInvitations(clubId)
|
||||
}
|
||||
|
||||
suspend fun updateFriendlyMatch(clubId: Int, matchId: Int, body: FriendlyMatchSaveBody) {
|
||||
val saved = matchesApi.updateFriendlyMatch(clubId, matchId, body)
|
||||
val existing = _state.value.friendlyMatches.find { it.id == matchId }
|
||||
val saved = if (existing?.isSharedFriendly == true) {
|
||||
matchesApi.updateSharedFriendlyMatch(clubId, matchId, body)
|
||||
} else {
|
||||
matchesApi.updateFriendlyMatch(clubId, matchId, body)
|
||||
}
|
||||
_state.update {
|
||||
it.copy(
|
||||
friendlyMatches = ScheduleLogic.sortMatches(
|
||||
@@ -306,7 +352,12 @@ class ScheduleManager(
|
||||
}
|
||||
|
||||
suspend fun deleteFriendlyMatch(clubId: Int, matchId: Int) {
|
||||
matchesApi.deleteFriendlyMatch(clubId, matchId)
|
||||
val existing = _state.value.friendlyMatches.find { it.id == matchId }
|
||||
if (existing?.isSharedFriendly == true) {
|
||||
matchesApi.deleteSharedFriendlyMatch(clubId, matchId)
|
||||
} else {
|
||||
matchesApi.deleteFriendlyMatch(clubId, matchId)
|
||||
}
|
||||
_state.update { it.copy(friendlyMatches = it.friendlyMatches.filterNot { match -> match.id == matchId }) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user