From 214847926b6a0296de694fa5197d8f6ecbcd1be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Tue, 28 Jan 2025 01:40:09 +0100 Subject: [PATCH 01/89] tournament component - 1 attribute, working backend&frontend for it --- .../tournament/TournamentApiController.kt | 34 +++++++ .../tournament/TournamentComponent.kt | 42 +++++++++ .../TournamentComponentController.kt | 33 +++++++ .../TournamentComponentEntityConfiguration.kt | 11 +++ .../tournament/TournamentController.kt | 57 ++++++++++++ .../component/tournament/TournamentEntity.kt | 59 ++++++++++++ .../tournament/TournamentRepository.kt | 15 +++ .../component/tournament/TournamentService.kt | 19 ++++ .../component/tournament/TournamentsView.kt | 10 ++ .../tournament/tournament-features.md | 64 +++++++++++++ .../sch/cmsch/config/ComponentLoadConfig.kt | 1 + .../sch/cmsch/service/PermissionsService.kt | 92 +++++++++++++++++++ .../resources/config/application.properties | 8 +- frontend/src/api/contexts/config/types.ts | 5 + .../api/contexts/service/ServiceContext.tsx | 2 +- frontend/src/api/hooks/queryKeys.ts | 3 +- .../hooks/tournament/useTournamentsQuery.ts | 17 ++++ .../pages/tournament/tournamentList.page.tsx | 54 +++++++++++ .../src/route-modules/Tournament.module.tsx | 12 +++ frontend/src/util/configs/modules.config.tsx | 10 +- frontend/src/util/paths.ts | 6 +- frontend/src/util/views/tournament.view.ts | 4 + 22 files changed, 548 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md create mode 100644 frontend/src/api/hooks/tournament/useTournamentsQuery.ts create mode 100644 frontend/src/pages/tournament/tournamentList.page.tsx create mode 100644 frontend/src/route-modules/Tournament.module.tsx create mode 100644 frontend/src/util/views/tournament.view.ts diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt new file mode 100644 index 00000000..2b979230 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -0,0 +1,34 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.dto.Preview +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +@ConditionalOnBean(TournamentComponent::class) +class TournamentApiController( + private val tournamentComponent: TournamentComponent, + private val tournamentService: TournamentService +) { + @JsonView(Preview::class) + @GetMapping("/tournament") + @Operation( + summary = "List all tournaments.", + ) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "List of tournaments") + ]) + fun tournaments(): ResponseEntity> { + val tournaments = tournamentService.findAll() + return ResponseEntity.ok(tournaments) + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt new file mode 100644 index 00000000..3dc6eace --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -0,0 +1,42 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.component.* +import hu.bme.sch.cmsch.component.app.ComponentSettingService +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.ControlPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service + + +@Service +@ConditionalOnProperty( + prefix = "hu.bme.sch.cmsch.component.load", + name = ["tournament"], + havingValue = "true", + matchIfMissing = false +) +class TournamentComponent ( + componentSettingService: ComponentSettingService, + env: Environment +) : ComponentBase( + "tournament", + "/tournament", + "Tournament", + ControlPermissions.PERMISSION_CONTROL_TOURNAMENT, + listOf(), + componentSettingService, env +){ + final override val allSettings by lazy { + listOf( + minRole, + ) + } + + final override val menuDisplayName = null + + final override val minRole = MinRoleSettingProxy(componentSettingService, component, + "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.ADMIN, + fieldName = "Minimum jogosultság", description = "A komponens eléréséhez szükséges minimum jogosultság" + ) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt new file mode 100644 index 00000000..60b24ba9 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt @@ -0,0 +1,33 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.component.ComponentApiBase +import hu.bme.sch.cmsch.component.app.MenuService +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ControlPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/component/tournaments") +@ConditionalOnBean(TournamentComponent::class) +class TournamentComponentController( + adminMenuService: AdminMenuService, + component: TournamentComponent, + private val menuService: MenuService, + private val tournamentService: TournamentService, + private val auditLogService: AuditLogService, + service: MenuService +) : ComponentApiBase( + adminMenuService, + TournamentComponent::class.java, + component, + ControlPermissions.PERMISSION_CONTROL_TOURNAMENT, + "Tournament", + "Tournament beállítások", + auditLogService = auditLogService, + menuService = menuService +) { + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt new file mode 100644 index 00000000..489e5030 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt @@ -0,0 +1,11 @@ +package hu.bme.sch.cmsch.component.tournament + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration + + +@Configuration +@ConditionalOnBean(TournamentComponent::class) +@EntityScan(basePackageClasses = [TournamentComponent::class]) +class TournamentComponentEntityConfiguration diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt new file mode 100644 index 00000000..9c7f01db --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt @@ -0,0 +1,57 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.OneDeepEntityPage +import hu.bme.sch.cmsch.controller.admin.calculateSearchSettings +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/tournament") +@ConditionalOnBean(TournamentComponent::class) +class TournamentController( + repo: TournamentRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : OneDeepEntityPage( + "tournament", + TournamentEntity::class, ::TournamentEntity, + "Verseny", "Versenyek", + "A rendezvény versenyeinek kezelése.", + + transactionManager, + repo, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = true, + editEnabled = true, + deleteEnabled = true, + importEnabled = true, + exportEnabled = true, + + adminMenuIcon = "sports_esports", + adminMenuPriority = 1, + searchSettings = calculateSearchSettings(true) +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt new file mode 100644 index 00000000..0fcfc9fd --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -0,0 +1,59 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.Edit +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.hibernate.Hibernate +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment + +@Entity +@Table(name="tournaments") +@ConditionalOnBean(TournamentComponent::class) +data class TournamentEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 1, label = "Verseny neve") + @property:GenerateOverview(columnName = "Név", order = 1) + @property:ImportFormat + var displayName: String = "", + + //TODO: Add more fields + +): ManagedEntity { + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "Tournament", + view = "control/tournaments", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as TournamentEntity + + return id != 0 && id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() + + override fun toString(): String { + return this::class.simpleName + "(id = $id, name = '$displayName')" + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt new file mode 100644 index 00000000..ad80ea0e --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt @@ -0,0 +1,15 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface TournamentRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt new file mode 100644 index 00000000..ccd70922 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -0,0 +1,19 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.GroupRepository +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@ConditionalOnBean(TournamentComponent::class) +open class TournamentService( + private val tournamentRepository: TournamentRepository, + private val groupRepository: GroupRepository, + private val tournamentComponent: TournamentComponent +) { + @Transactional(readOnly = true) + open fun findAll(): List { + return tournamentRepository.findAll() + } +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt new file mode 100644 index 00000000..b1b67072 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt @@ -0,0 +1,10 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview + +data class TournamentsView ( + @field:JsonView(value = [Preview::class, FullDetails::class]) + val tournaments: List +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md new file mode 100644 index 00000000..89fff3ee --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md @@ -0,0 +1,64 @@ +# Tournament life cycle +1. Previously: we have booked the fields for football, volleyball (and streetball?) +2. Tournament creation + 1. Football, volleyball, streetball; chess, beer pong, video games + 2. Name, max participants, type of tournament (ie. group stage?, knockout phase?) etc. +3. Applications come in for every event +4. Application ends, we need to generate the brackets/groups + 1. Set the seedings for the events + 2. Football, Volleyball, Streetball: group phases for all, others: knockout brackets + - group sizes should be dependent on the count of the playing fields (how many matches can we play overall and concurrently) + - we might want to avoid duplicate pairings over different sports to maintain diversity +5. set the times and places for matches + - we should avoid teams being represented in different sports at the same time (at least in football, volleyball and streetball) +6. group stages for football, volleyball and streetball go, finish after a couple of days +7. get advancing teams, generate knockout brackets for them + - once again, avoid teams being represented at the same time, also try to make the finals be at a different time + +- register game results (possibly even have live scores updated) + +## Views/Pages + +### Registration +- That should be a form (don't know if it can be used as easily) + +### Tournaments +- Lists the different tournaments + - Outside sports might be on the same page, the online tourneys as well + +### Tournament (group) view page +- Tab type 1: Table/bracket for a tournament + - different tournament, different tab for bracket/group standings +- Tab type 2: Game schedules + - ? every tournament's schedule in a group on the same page ? + - Should we even group tournaments? If so, how? + + +## Schema + +### Tournament Group (?) +- Id +- Name +- url + +### Tournament +- Id +- Name +- groupId (optional) (?) +- url + +### TournamentStage +- Id +- tournamentId + +### GroupStage: TournamentStage +- teamsToAdvance + +### TournamentRegistration +- Id +- groupId +- tournamentId +- seed +#### +//- opponents: Opponents + diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt index bc952135..f65be9d5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt @@ -32,6 +32,7 @@ data class ComponentLoadConfig @ConstructorBinding constructor( var task: Boolean, var team: Boolean, var token: Boolean, + var tournament: Boolean, var accessKeys: Boolean, var conference: Boolean, var email: Boolean, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 20f9d089..a7bc1dfb 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -34,6 +34,7 @@ import hu.bme.sch.cmsch.component.staticpage.StaticPageComponent import hu.bme.sch.cmsch.component.task.TaskComponent import hu.bme.sch.cmsch.component.team.TeamComponent import hu.bme.sch.cmsch.component.token.TokenComponent +import hu.bme.sch.cmsch.component.tournament.TournamentComponent import hu.bme.sch.cmsch.extending.CmschPermissionSource import hu.bme.sch.cmsch.util.DI import org.springframework.stereotype.Component @@ -431,6 +432,13 @@ object ControlPermissions : PermissionGroup { component = SheetsComponent::class ) + val PERMISSION_CONTROL_TOURNAMENT = PermissionValidator( + "TOURNAMENT_CONTROL", + "Tournament komponens testreszabása", + readOnly = false, + component = TournamentComponent::class + ) + override fun allPermissions() = listOf( PERMISSION_CONTROL_NEWS, PERMISSION_CONTROL_TASKS, @@ -475,6 +483,7 @@ object ControlPermissions : PermissionGroup { PERMISSION_CONTROL_PROTO, PERMISSION_CONTROL_CONFERENCE, PERMISSION_CONTROL_SHEETS, + PERMISSION_CONTROL_TOURNAMENT, ) } @@ -1582,6 +1591,78 @@ object StaffPermissions : PermissionGroup { component = SheetsComponent::class ) + /// TournamentComponent + + val PERMISSION_SHOW_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_SHOW", + "Verseny megtekintése", + readOnly = true, + component = TournamentComponent::class + ) + + val PERMISSION_CREATE_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_CREATE", + "Versenyek létrehozása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_DELETE_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_DELETE", + "Versenyek törlése", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_EDIT_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_EDIT", + "Versenyek szerkesztése", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS = PermissionValidator( + "TOURNAMENT_PARTICIPANTS_SHOW", + "Verseny résztvevők megtekintése", + readOnly = true, + component = TournamentComponent::class + ) + + val PERMISSION_SET_SEEDS = PermissionValidator( + "TOURNAMENT_SET_SEEDS", + "Versenyzők seedjeinek állítása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_GROUPS = PermissionValidator( + "TOURNAMENT_GENERATE_GROUPS", + "Verseny csoportok generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_BRACKETS = PermissionValidator( + "TOURNAMENT_GENERATE_BRACKETS", + "Verseny táblák generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_MATCHES = PermissionValidator( + "TOURNAMENT_GENERATE_MATCHES", + "Verseny meccsek generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_EDIT_RESULTS = PermissionValidator( + "TOURNAMENT_EDIT_RESULTS", + "Verseny eredmények szerkesztése", + readOnly = false, + component = TournamentComponent::class + ) + override fun allPermissions() = listOf( PERMISSION_RATE_TASKS, PERMISSION_SHOW_TASKS, @@ -1756,6 +1837,17 @@ object StaffPermissions : PermissionGroup { PERMISSION_EDIT_SHEETS, PERMISSION_CREATE_SHEETS, PERMISSION_DELETE_SHEETS, + + PERMISSION_SHOW_TOURNAMENTS, + PERMISSION_CREATE_TOURNAMENTS, + PERMISSION_DELETE_TOURNAMENTS, + PERMISSION_EDIT_TOURNAMENTS, + PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS, + PERMISSION_SET_SEEDS, + PERMISSION_GENERATE_GROUPS, + PERMISSION_GENERATE_BRACKETS, + PERMISSION_GENERATE_MATCHES, + PERMISSION_EDIT_RESULTS, ) } diff --git a/backend/src/main/resources/config/application.properties b/backend/src/main/resources/config/application.properties index 378bbf01..88c5945c 100644 --- a/backend/src/main/resources/config/application.properties +++ b/backend/src/main/resources/config/application.properties @@ -45,10 +45,10 @@ server.servlet.session.persistent=false server.servlet.session.cookie.same-site=lax spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.username=sa -#spring.datasource.password=password +spring.datasource.username=sa +spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -#spring.datasource.url=jdbc:h2:file:./temp/db +spring.datasource.url=jdbc:h2:file:./temp/db spring.h2.console.enabled=false spring.jpa.hibernate.ddl-auto=update @@ -89,6 +89,7 @@ hu.bme.sch.cmsch.component.load.staticPage=true hu.bme.sch.cmsch.component.load.task=true hu.bme.sch.cmsch.component.load.team=true hu.bme.sch.cmsch.component.load.token=true +hu.bme.sch.cmsch.component.load.tournament=true hu.bme.sch.cmsch.component.load.test=true hu.bme.sch.cmsch.component.load.stats=true @@ -120,6 +121,7 @@ hu.bme.sch.cmsch.proto.priority=128 hu.bme.sch.cmsch.conference.priority=129 hu.bme.sch.cmsch.gallery.priority=130 hu.bme.sch.cmsch.sheets.priority=131 +hu.bme.sch.cmsch.tournament.priority=132 hu.bme.sch.cmsch.app.priority=150 hu.bme.sch.cmsch.app.content.priority=151 hu.bme.sch.cmsch.app.style.priority=152 diff --git a/frontend/src/api/contexts/config/types.ts b/frontend/src/api/contexts/config/types.ts index 7b6b327d..82b59d3a 100644 --- a/frontend/src/api/contexts/config/types.ts +++ b/frontend/src/api/contexts/config/types.ts @@ -39,6 +39,7 @@ export interface Components { qrFight: QrFight communities: Communities footer: Footer + tournament: Tournament } export interface App { @@ -330,3 +331,7 @@ export interface Signup { export interface Communities { title: string } + +export interface Tournament { + title: string +} diff --git a/frontend/src/api/contexts/service/ServiceContext.tsx b/frontend/src/api/contexts/service/ServiceContext.tsx index 60da9f63..5595ee12 100644 --- a/frontend/src/api/contexts/service/ServiceContext.tsx +++ b/frontend/src/api/contexts/service/ServiceContext.tsx @@ -17,7 +17,7 @@ export interface MessageOptions { } export type ServiceContextType = { - sendMessage: (message: string, options?: MessageOptions) => void + sendMessage: (message: string | undefined, options?: MessageOptions) => void clearMessage: () => void message?: string type: MessageTypes diff --git a/frontend/src/api/hooks/queryKeys.ts b/frontend/src/api/hooks/queryKeys.ts index d3716566..4d9dd9ae 100644 --- a/frontend/src/api/hooks/queryKeys.ts +++ b/frontend/src/api/hooks/queryKeys.ts @@ -24,5 +24,6 @@ export enum QueryKeys { COMMUNITY = 'COMMUNITY', ORGANIZATION = 'ORGANIZATION', ACCESS_KEY = 'ACCESS_KEY', - HOME_NEWS = 'HOME_NEWS' + HOME_NEWS = 'HOME_NEWS', + TOURNAMENTS = 'TOURNAMENTS', } diff --git a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts new file mode 100644 index 00000000..22d20e35 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts @@ -0,0 +1,17 @@ +import { TournamentView } from '../../../util/views/tournament.view.ts' +import { useQuery } from 'react-query' +import { QueryKeys } from '../queryKeys.ts' +import axios from 'axios' +import { ApiPaths } from '../../../util/paths.ts' + + +export const useTournamentsQuery = (onError?: (err: any) => void) => { + return useQuery( + QueryKeys.TOURNAMENTS, + async () => { + const response = await axios.get(ApiPaths.TOURNAMENTS) + return response.data + }, + { onError: onError } + ) +} diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx new file mode 100644 index 00000000..398158b5 --- /dev/null +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -0,0 +1,54 @@ +import { useTournamentsQuery } from '../../api/hooks/tournament/useTournamentsQuery.ts' +import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' +import { Box, Heading, useBreakpoint, useBreakpointValue, useDisclosure, VStack } from '@chakra-ui/react' +import { createRef, useState } from 'react' +import { TournamentView } from '../../util/views/tournament.view.ts' +import { ComponentUnavailable } from '../../common-components/ComponentUnavailable.tsx' +import { PageStatus } from '../../common-components/PageStatus.tsx' +import { CmschPage } from '../../common-components/layout/CmschPage.tsx' +import { Helmet } from 'react-helmet-async' + + +const TournamentListPage = () => { + const { isLoading, isError, data } = useTournamentsQuery() + const component = useConfigContext()?.components?.tournament + const { isOpen, onToggle } = useDisclosure() + const tabsSize = useBreakpointValue({ base: 'sm', md: 'md' }) + const breakpoint = useBreakpoint() + const inputRef = createRef() + + if (!component) return + + if (isError || isLoading || !data) return + return ( + + + + + {component.title} + + + {data.length} verseny található. + + + + {(data ?? []).length > 0 ? ( + data.map((tournament: TournamentView) => ( + + + {tournament.displayName} + + + {tournament.displayName} + + + )) + ) : ( + Nincs egyetlen verseny sem. + )} + + + ) +} + +export default TournamentListPage diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx new file mode 100644 index 00000000..08e3c22f --- /dev/null +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -0,0 +1,12 @@ +import { Route } from 'react-router-dom' +import { Paths } from '../util/paths.ts' +import TournamentListPage from '../pages/tournament/TournamentList.page.tsx' + +export function TournamentModule() { + return ( + + {/*} />*/} + } /> + + ) +} diff --git a/frontend/src/util/configs/modules.config.tsx b/frontend/src/util/configs/modules.config.tsx index 5742a00e..34410c8b 100644 --- a/frontend/src/util/configs/modules.config.tsx +++ b/frontend/src/util/configs/modules.config.tsx @@ -17,6 +17,7 @@ import { TeamModule } from '../../route-modules/Team.module' import { RaceModule } from '../../route-modules/Race.module' import { QRFightModule } from '../../route-modules/QRFight.module' import { AccessKeyModule } from '../../route-modules/AccessKey.module' +import { TournamentModule } from '../../route-modules/Tournament.module.tsx' export enum AvailableModules { HOME = 'HOME', @@ -35,7 +36,8 @@ export enum AvailableModules { RACE = 'RACE', QR_FIGHT = 'QR_FIGHT', ACCESS_KEY = 'ACCESS_KEY', - MAP = 'MAP' + MAP = 'MAP', + TOURNAMENT = 'TOURNAMENT', } export const RoutesForModules: Record = { @@ -55,7 +57,8 @@ export const RoutesForModules: Record = { [AvailableModules.QR_FIGHT]: QRFightModule, [AvailableModules.TEAM]: TeamModule, [AvailableModules.ACCESS_KEY]: AccessKeyModule, - [AvailableModules.MAP]: MapModule + [AvailableModules.MAP]: MapModule, + [AvailableModules.TOURNAMENT]: TournamentModule } export function GetRoutesForModules(modules: AvailableModules[]) { @@ -79,5 +82,6 @@ export const EnabledModules: AvailableModules[] = [ AvailableModules.QR_FIGHT, AvailableModules.TEAM, AvailableModules.ACCESS_KEY, - AvailableModules.MAP + AvailableModules.MAP, + AvailableModules.TOURNAMENT ] diff --git a/frontend/src/util/paths.ts b/frontend/src/util/paths.ts index 5530d023..0ad32c61 100644 --- a/frontend/src/util/paths.ts +++ b/frontend/src/util/paths.ts @@ -26,7 +26,8 @@ export enum Paths { MY_TEAM = 'my-team', TEAM_ADMIN = 'team-admin', ACCESS_KEY = 'access-key', - MAP = 'map' + MAP = 'map', + TOURNAMENT = 'tournament' } export enum AbsolutePaths { @@ -87,5 +88,6 @@ export enum ApiPaths { ACCESS_KEY = '/api/access-key', HOME_NEWS = '/api/home/news', ADD_PUSH_NOTIFICATION_TOKEN = '/api/pushnotification/add-token', - DELETE_PUSH_NOTIFICATION_TOKEN = '/api/pushnotification/delete-token' + DELETE_PUSH_NOTIFICATION_TOKEN = '/api/pushnotification/delete-token', + TOURNAMENTS = '/api/tournament', } diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts new file mode 100644 index 00000000..99824252 --- /dev/null +++ b/frontend/src/util/views/tournament.view.ts @@ -0,0 +1,4 @@ +export type TournamentView = { + id: number + displayName: string +} From afb7edac34df72e98e681b5c93a2bacf76f3384a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Tue, 28 Jan 2025 19:30:51 +0100 Subject: [PATCH 02/89] tournament component - 2nd attribute --- .../cmsch/component/tournament/TournamentEntity.kt | 11 +++++++++-- frontend/src/pages/tournament/tournamentList.page.tsx | 4 ++-- frontend/src/util/views/tournament.view.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 0fcfc9fd..31a3a48c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -31,7 +31,14 @@ data class TournamentEntity( @property:GenerateInput(maxLength = 64, order = 1, label = "Verseny neve") @property:GenerateOverview(columnName = "Név", order = 1) @property:ImportFormat - var displayName: String = "", + var title: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 2, label = "Verseny leírása") + @property:GenerateOverview(columnName = "Leírás", order = 2) + @property:ImportFormat + var description: String = "", //TODO: Add more fields @@ -54,6 +61,6 @@ data class TournamentEntity( override fun hashCode(): Int = javaClass.hashCode() override fun toString(): String { - return this::class.simpleName + "(id = $id, name = '$displayName')" + return this::class.simpleName + "(id = $id, name = '$title')" } } \ No newline at end of file diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 398158b5..1c6cf087 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -36,10 +36,10 @@ const TournamentListPage = () => { data.map((tournament: TournamentView) => ( - {tournament.displayName} + {tournament.title} - {tournament.displayName} + {tournament.description} )) diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 99824252..52c4e8d8 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -1,4 +1,5 @@ export type TournamentView = { id: number - displayName: string + title: string + description: string } From 050e4d05c0ac00017c5f9bd3c0ce0b7c6d0c6b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Thu, 30 Jan 2025 22:38:03 +0100 Subject: [PATCH 03/89] tournament: Component Settings fixed, some reformat, new attributes --- .../tournament/TournamentComponent.kt | 7 +++++++ .../TournamentComponentController.kt | 2 +- .../component/tournament/TournamentEntity.kt | 20 ++++++++++++++++--- .../sch/cmsch/service/PermissionsService.kt | 16 +++++++-------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index 3dc6eace..d42b6f00 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -29,12 +29,19 @@ class TournamentComponent ( ){ final override val allSettings by lazy { listOf( + tournamentGroup, minRole, ) } final override val menuDisplayName = null + final val tournamentGroup = SettingProxy(componentSettingService, component, + "tournamentGroup", "", type = SettingType.COMPONENT_GROUP, persist = false, + fieldName = "Tournament", + description = "Jelenleg nincs mit beállítani itt" + ) + final override val minRole = MinRoleSettingProxy(componentSettingService, component, "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.ADMIN, fieldName = "Minimum jogosultság", description = "A komponens eléréséhez szükséges minimum jogosultság" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt index 60b24ba9..e4288145 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt @@ -10,7 +10,7 @@ import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.RequestMapping @Controller -@RequestMapping("/admin/control/component/tournaments") +@RequestMapping("/admin/control/component/tournament") @ConditionalOnBean(TournamentComponent::class) class TournamentComponentController( adminMenuService: AdminMenuService, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 31a3a48c..d1a49dc2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -35,12 +35,26 @@ data class TournamentEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(maxLength = 64, order = 2, label = "Verseny leírása") - @property:GenerateOverview(columnName = "Leírás", order = 2) + @property:GenerateInput(maxLength = 16, order = 2, label = "URL") + @property:GenerateOverview(columnName = "URL", order = 2) + @property:ImportFormat + var url: String = "", + + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 3, label = "Verseny leírása") + @property:GenerateOverview(columnName = "Leírás", order = 3) @property:ImportFormat var description: String = "", - //TODO: Add more fields + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 4, label = "Verseny helyszíne") + @property:GenerateOverview(columnName = "Helyszín", order = 4) + @property:ImportFormat + var location: String = "", + + //TODO: Add more fields ? ): ManagedEntity { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index a7bc1dfb..9657ed1a 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -55,8 +55,8 @@ class PermissionValidator internal constructor( val description: String = "", val component: KClass? = null, val readOnly: Boolean = false, // Note: this is just a label but used for giving read-only permissions - val validate: Function1 = { - user -> user.isAdmin() || (permissionString.isNotEmpty() && user.hasPermission(permissionString)) + val validate: Function1 = { user -> + user.isAdmin() || (permissionString.isNotEmpty() && user.hasPermission(permissionString)) } ) @@ -91,20 +91,20 @@ object ImplicitPermissions : PermissionGroup { val PERMISSION_IMPLICIT_HAS_GROUP = PermissionValidator( description = "The user has a group", readOnly = false, - permissionString = "HAS_GROUP") - { user -> DI.instance.userService.getById(user.internalId).group != null } + permissionString = "HAS_GROUP" + ) { user -> DI.instance.userService.getById(user.internalId).group != null } val PERMISSION_IMPLICIT_ANYONE = PermissionValidator( description = "Everyone has this permission", readOnly = false, - permissionString = "ANYONE") - { _ -> true } + permissionString = "ANYONE" + ) { _ -> true } val PERMISSION_NOBODY = PermissionValidator( description = "Nobody has this permission", readOnly = false, - permissionString = "NOBODY") - { _ -> false } + permissionString = "NOBODY" + ) { _ -> false } val PERMISSION_SUPERUSER_ONLY = PermissionValidator { user -> user.isSuperuser() } From c1e9a3a8dca93faf5ddff64f4fb4a0daa6f1c44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 3 Feb 2025 16:41:20 +0100 Subject: [PATCH 04/89] tournament - knockout stage, registration - backend stuff --- .../component/tournament/KnockoutGroupDto.kt | 24 +++++ .../tournament/KnockoutStageController.kt | 77 +++++++++++++++ .../tournament/KnockoutStageEntity.kt | 96 +++++++++++++++++++ .../tournament/KnockoutStageRepository.kt | 40 ++++++++ .../component/tournament/ParticipantDto.kt | 6 ++ .../component/tournament/TournamentEntity.kt | 26 ++++- .../component/tournament/TournamentService.kt | 41 +++++++- .../component/tournament/TournamentsView.kt | 10 -- .../src/route-modules/Tournament.module.tsx | 2 +- 9 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt delete mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt new file mode 100644 index 00000000..7b9ae050 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt @@ -0,0 +1,24 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.admin.GenerateOverview +import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.model.IdentifiableEntity + +data class KnockoutGroupDto( + + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @property:GenerateOverview(columnName = "Név", order = 1) + var name: String = "", + + @property:GenerateOverview(columnName = "Helyszín", order = 4) + var location: String = "", + + @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + var participantCount: Int = 0, + + @property:GenerateOverview(columnName = "Szakaszok száma", order = 6) + var stageCount: Int = 0 + +): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt new file mode 100644 index 00000000..9990c7f1 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -0,0 +1,77 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.ButtonAction +import hu.bme.sch.cmsch.controller.admin.TwoDeepEntityPage +import hu.bme.sch.cmsch.repository.ManualRepository +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + + +@Controller +@RequestMapping("/admin/control/knockout-stage") +@ConditionalOnBean(TournamentComponent::class) +class KnockoutStageController( + private val stageRepository: KnockoutStageRepository, + private val tournamentRepository: TournamentRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : TwoDeepEntityPage( + "knockout-stage", + KnockoutGroupDto::class, + KnockoutStageEntity::class, ::KnockoutStageEntity, + "Kiesési szakasz", "Kiesési szakaszok", + "A kiesési szakaszok kezelése.", + transactionManager, + object : ManualRepository() { + override fun findAll(): Iterable { + val stages = stageRepository.findAllAggregated() + val tournaments = tournamentRepository.findAll().associateBy { it.id } + return stages.map { + KnockoutGroupDto( + it.tournamentId, + it.tournamentName, + it.tournamentLocation, + it.participantCount, + it.stageCount.toInt() + ) + }.sortedByDescending { it.stageCount }.toList() + } + }, + stageRepository, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = true, + editEnabled = true, + deleteEnabled = true, + importEnabled = false, + exportEnabled = false, + + adminMenuIcon = "lan", +) { + override fun fetchSublist(id: Int): Iterable { + return stageRepository.findAllByTournamentId(id) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt new file mode 100644 index 00000000..567941d6 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -0,0 +1,96 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.Edit +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import kotlin.math.ceil +import kotlin.math.log2 + + +@Entity +@Table(name = "knockout_stage") +@ConditionalOnBean(TournamentComponent::class) +data class KnockoutStageEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 1, label = "Szakasz neve") + @property:GenerateOverview(columnName = "Név", order = 1) + @property:ImportFormat + var name: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") + @property:GenerateOverview(columnName = "Verseny ID", order = 2) + @property:ImportFormat + var tournamentId: Int = 0, + + @ManyToOne(targetEntity = TournamentEntity::class) + @JoinColumn(name = "tournamentId", insertable = false, updatable = false) + var tournament: TournamentEntity? = null, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Szint") + @property:GenerateOverview(columnName = "Szint", order = 3, centered = true) + @property:ImportFormat + var level: Int = 1, //ie. Csoportkör-1, Csoportkör-2, Kieséses szakasz-3 + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Résztvevők száma") + @property:GenerateOverview(columnName = "RésztvevőSzám", order = 3, centered = true) + @property:ImportFormat + var participantCount: Int = 1, + + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateOverview(columnName = "Következő kör", order = 4, centered = true) + @property:ImportFormat + var nextRound: Int = 0, + +): ManagedEntity { + + fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "KnockoutStage", + view = "control/tournament/knockout-stage", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KnockoutStageEntity) return false + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int = javaClass.hashCode() + + override fun toString(): String { + return this::class.simpleName + "(id = $id, name = $name, tournamentId = $tournament.id, participantCount = $participantCount)" + } + + + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt new file mode 100644 index 00000000..083fb858 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -0,0 +1,40 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.* + +data class StageCountDto( + var tournamentId: Int = 0, + var tournamentName: String = "", + var tournamentLocation : String = "", + var participantCount: Int = 0, + var stageCount: Long = 0 +) + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface KnockoutStageRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + override fun findById(id: Int): Optional + fun findAllByTournamentId(tournamentId: Int): List + + @Query(""" + SELECT NEW hu.bme.sch.cmsch.component.tournament.StageCountDto( + s.tournament.id, + s.tournament.title, + s.tournament.location, + s.tournament.participantCount, + COUNT(s.id) + ) + FROM KnockoutStageEntity s + GROUP BY s.tournament + """) + fun findAllAggregated(): List + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt new file mode 100644 index 00000000..5b7eb28a --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt @@ -0,0 +1,6 @@ +package hu.bme.sch.cmsch.component.tournament + +data class ParticipantDto( + var teamId: Int = 0, + var teamName: String = "", +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index d1a49dc2..d8aee7f4 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -1,6 +1,7 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.annotation.JsonView +import com.google.j2objc.annotations.GenerateObjectiveCGenerics import hu.bme.sch.cmsch.admin.* import hu.bme.sch.cmsch.component.EntityConfig import hu.bme.sch.cmsch.dto.Edit @@ -14,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment @Entity -@Table(name="tournaments") +@Table(name="tournament") @ConditionalOnBean(TournamentComponent::class) data class TournamentEntity( @@ -54,13 +55,32 @@ data class TournamentEntity( @property:ImportFormat var location: String = "", - //TODO: Add more fields ? + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + @property:ImportFormat + var participantCount: Int = 0, + + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var participants: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var status: Int = 0, ): ManagedEntity { override fun getEntityConfig(env: Environment) = EntityConfig( name = "Tournament", - view = "control/tournaments", + view = "control/tournament", showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, ) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index ccd70922..867832a4 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -1,19 +1,58 @@ package hu.bme.sch.cmsch.component.tournament +import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.util.* @Service @ConditionalOnBean(TournamentComponent::class) open class TournamentService( private val tournamentRepository: TournamentRepository, + private val stageRepository: KnockoutStageRepository, private val groupRepository: GroupRepository, - private val tournamentComponent: TournamentComponent + private val tournamentComponent: TournamentComponent, + private val objectMapper: ObjectMapper ) { @Transactional(readOnly = true) open fun findAll(): List { return tournamentRepository.findAll() } + + @Transactional(readOnly = true) + open fun findTournamentById(id: Int) : Optional { + return tournamentRepository.findById(id) + } + + @Transactional(readOnly = true) + open fun findStagesByTournamentId(tournamentId: Int) : List { + return stageRepository.findAllByTournamentId(tournamentId) + } + + @Transactional + fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { + val tournament = tournamentRepository.findById(tournamentId) + if (tournament.isEmpty) { + return false + } + val group = groupRepository.findById(teamId) + if (group.isEmpty) { + return false + } + val participants = tournament.get().participants + val parsed = mutableListOf(ParticipantDto(teamId, teamName)) + parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) + + parsed.add(ParticipantDto(teamId, teamName)) + + tournament.get().participants = parsed.joinToString("\n") { objectMapper.writeValueAsString(it) } + tournament.get().participantCount = parsed.size + + tournamentRepository.save(tournament.get()) + return true + } + + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt deleted file mode 100644 index b1b67072..00000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt +++ /dev/null @@ -1,10 +0,0 @@ -package hu.bme.sch.cmsch.component.tournament - -import com.fasterxml.jackson.annotation.JsonView -import hu.bme.sch.cmsch.dto.FullDetails -import hu.bme.sch.cmsch.dto.Preview - -data class TournamentsView ( - @field:JsonView(value = [Preview::class, FullDetails::class]) - val tournaments: List -) diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx index 08e3c22f..4fbccc79 100644 --- a/frontend/src/route-modules/Tournament.module.tsx +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -1,6 +1,6 @@ import { Route } from 'react-router-dom' import { Paths } from '../util/paths.ts' -import TournamentListPage from '../pages/tournament/TournamentList.page.tsx' +import TournamentListPage from '../pages/tournament/tournamentList.page.tsx' export function TournamentModule() { return ( From d3f32adce9658a3224150465696215c86cfdfa5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Wed, 5 Feb 2025 18:23:41 +0100 Subject: [PATCH 05/89] tournament match - entity, repository done, controller done --- .../component/tournament/KnockoutGroupDto.kt | 6 +- .../tournament/KnockoutStageEntity.kt | 13 ++ .../tournament/KnockoutStageService.kt | 25 ++++ .../component/tournament/MatchGroupDto.kt | 21 +++ .../tournament/TournamentMatchController.kt | 72 +++++++++++ .../tournament/TournamentMatchEntity.kt | 121 ++++++++++++++++++ .../tournament/TournamentMatchRepository.kt | 40 ++++++ 7 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt index 7b9ae050..f3cac939 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt @@ -12,13 +12,13 @@ data class KnockoutGroupDto( @property:GenerateOverview(columnName = "Név", order = 1) var name: String = "", - @property:GenerateOverview(columnName = "Helyszín", order = 4) + @property:GenerateOverview(columnName = "Helyszín", order = 2) var location: String = "", - @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + @property:GenerateOverview(columnName = "Résztvevők száma", order = 3) var participantCount: Int = 0, - @property:GenerateOverview(columnName = "Szakaszok száma", order = 6) + @property:GenerateOverview(columnName = "Szakaszok száma", order = 4) var stageCount: Int = 0 ): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 567941d6..3abb3f5e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -9,10 +9,14 @@ import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.ManagedEntity import hu.bme.sch.cmsch.service.StaffPermissions import jakarta.persistence.* +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext import org.springframework.core.env.Environment +import java.security.Provider import kotlin.math.ceil import kotlin.math.log2 +import kotlin.math.pow @Entity @@ -68,7 +72,12 @@ data class KnockoutStageEntity( ): ManagedEntity { + @Autowired + @Transient + private lateinit var knockoutStageService: KnockoutStageService + fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 + fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", @@ -92,5 +101,9 @@ data class KnockoutStageEntity( } + @PrePersist + fun onPrePersist(){ + knockoutStageService.createMatchesForStage(this) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt new file mode 100644 index 00000000..6f2bee68 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -0,0 +1,25 @@ +package hu.bme.sch.cmsch.component.tournament + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@ConditionalOnBean(TournamentComponent::class) +class KnockoutStageService( + private val matchRepository: TournamentMatchRepository +) { + + @Transactional + fun createMatchesForStage(stage: KnockoutStageEntity) { + for (i in 1..stage.matches()) { + val match = TournamentMatchEntity( + stageId = stage.id, + id = i, + // Set other necessary fields TODO + ) + matchRepository.save(match) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt new file mode 100644 index 00000000..b306b37b --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt @@ -0,0 +1,21 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.admin.GenerateOverview +import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.model.IdentifiableEntity + +data class MatchGroupDto( + + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @property:GenerateOverview(columnName = "Név", order = 1) + var name: String = "", + + @property:GenerateOverview(columnName = "Helyszín", order = 2) + var location: String = "", + + @property:GenerateOverview(columnName = "Közeli meccsek száma", order = 3) + var matchCount: Int = 0, + +): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt new file mode 100644 index 00000000..84313521 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -0,0 +1,72 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.TwoDeepEntityPage +import hu.bme.sch.cmsch.repository.ManualRepository +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/tournament-match") +@ConditionalOnBean(TournamentComponent::class) +class TournamentMatchController( + private val matchRepository: TournamentMatchRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : TwoDeepEntityPage( + "tournament-match", + MatchGroupDto::class, + TournamentMatchEntity::class, ::TournamentMatchEntity, + "Mérkőzés", "Mérkőzések", + "A mérkőzések kezelése.", + transactionManager, + object : ManualRepository() { + override fun findAll(): Iterable { + val matches = matchRepository.findAllAggregated() + return matches.map { + MatchGroupDto( + it.tournamentId, + it.tournamentName, + it.tournamentLocation, + it.matchCount.toInt() + ) + }.sortedByDescending { it.matchCount }.toList() + } + }, + matchRepository, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = false, + editEnabled = true, + deleteEnabled = false, + importEnabled = false, + exportEnabled = false, + + adminMenuIcon = "compare_arrows", +) { + override fun fetchSublist(id: Int): Iterable { + return matchRepository.findAllByStageTournamentId(id) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt new file mode 100644 index 00000000..a000314a --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -0,0 +1,121 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.* +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.hibernate.Hibernate +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment + +enum class MatchStatus { + NOT_STARTED, + FIRST_HALF, + HALF_TIME, + SECOND_HALF, + FULL_TIME, + EXTRA_TIME, + PENALTY_KICKS, + CANCELLED, + FINISHED +} + +@Entity +@Table(name = "tournament_match") +@ConditionalOnBean(TournamentComponent::class) +data class TournamentMatchEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") + @property:GenerateOverview(columnName = "Stage ID", order = 1) + @property:ImportFormat + var stageId: Int = 0, + + @ManyToOne(targetEntity = KnockoutStageEntity::class) + @JoinColumn(name = "stageId", insertable = false, updatable = false) + var stage: KnockoutStageEntity? = null, + + @Column(nullable = false) + var homeTeamId: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Home team name") + @property:GenerateOverview(columnName = "Home team name", order = 2) + @property:ImportFormat + var homeTeamName: String = "", + + @Column(nullable = false) + var awayTeamId: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Away team name") + @property:GenerateOverview(columnName = "Away team name", order = 3) + @property:ImportFormat + var awayTeamName: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_DATE, order = 4, label = "Kickoff time") + @property:GenerateOverview(columnName = "Kickoff time", order = 4) + @property:ImportFormat + var kickoffTime: Long = 0, + + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 5, label = "Home team score") + @property:GenerateOverview(columnName = "Home team score", order = 5) + @property:ImportFormat + var homeTeamScore: Int? = null, + + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 6, label = "Away team score") + @property:GenerateOverview(columnName = "Away team score", order = 6) + @property:ImportFormat + var awayTeamScore: Int? = null, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 7, label = "Match status", + source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP","CANCELLED" ], + visible = false, ignore = true + ) + @property:GenerateOverview(columnName = "Match status", order = 7) + @property:ImportFormat + val status: MatchStatus = MatchStatus.NOT_STARTED, + +): ManagedEntity{ + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "TournamentMatch", + view = "control/tournament/match", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as TournamentMatchEntity + + return id != 0 && id == other.id + } + + override fun toString(): String { + return javaClass.simpleName + "(id = $id)" + } + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt new file mode 100644 index 00000000..0473573f --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -0,0 +1,40 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.* + +data class MatchCountDto( + var tournamentId: Int = 0, + var tournamentName: String = "", + var tournamentLocation : String = "", + var matchCount: Long = 0 +) + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface TournamentMatchRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + override fun findById(id: Int): Optional + fun findAllByStageId(stageId: Int): List + @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") + fun findAllByStageTournamentId(tournamentId: Int): List + + @Query(""" + SELECT NEW hu.bme.sch.cmsch.component.tournament.MatchCountDto( + s.tournament.id, + s.tournament.title, + s.tournament.location, + COUNT(t.id) + ) + FROM TournamentMatchEntity t + JOIN t.stage s + GROUP BY s.tournament + """) + fun findAllAggregated(): List +} \ No newline at end of file From c87d1fc280c926c2ab888ef2224129aac20a503a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 10 Feb 2025 22:59:39 +0100 Subject: [PATCH 06/89] some test data; still figuring out the schema --- .../tournament/KnockoutStageEntity.kt | 27 ++++++--------- .../tournament/KnockoutStageRepository.kt | 1 + .../tournament/KnockoutStageService.kt | 22 +++++++++--- .../component/tournament/TournamentEntity.kt | 2 +- .../tournament/TournamentMatchEntity.kt | 34 ++++++++++++------- .../tournament/TournamentMatchRepository.kt | 2 +- .../component/tournament/TournamentService.kt | 11 +----- .../hu/bme/sch/cmsch/config/TestConfig.kt | 31 ++++++++++++++++- .../sch/cmsch/service/PermissionsService.kt | 8 ++--- 9 files changed, 87 insertions(+), 51 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 3abb3f5e..0022d364 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -9,11 +9,8 @@ import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.ManagedEntity import hu.bme.sch.cmsch.service.StaffPermissions import jakarta.persistence.* -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.ApplicationContext import org.springframework.core.env.Environment -import java.security.Provider import kotlin.math.ceil import kotlin.math.log2 import kotlin.math.pow @@ -39,15 +36,7 @@ data class KnockoutStageEntity( @property:ImportFormat var name: String = "", - @Column(nullable = false) - @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") - @property:GenerateOverview(columnName = "Verseny ID", order = 2) - @property:ImportFormat - var tournamentId: Int = 0, - @ManyToOne(targetEntity = TournamentEntity::class) - @JoinColumn(name = "tournamentId", insertable = false, updatable = false) var tournament: TournamentEntity? = null, @Column(nullable = false) @@ -64,6 +53,13 @@ data class KnockoutStageEntity( @property:ImportFormat var participantCount: Int = 1, + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var participants: String = "", + @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateOverview(columnName = "Következő kör", order = 4, centered = true) @@ -72,10 +68,6 @@ data class KnockoutStageEntity( ): ManagedEntity { - @Autowired - @Transient - private lateinit var knockoutStageService: KnockoutStageService - fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 @@ -102,8 +94,9 @@ data class KnockoutStageEntity( @PrePersist - fun onPrePersist(){ - knockoutStageService.createMatchesForStage(this) + fun prePersist() { + val stageService = KnockoutStageService.getBean() + stageService.createMatchesForStage(this) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 083fb858..989724da 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -22,6 +22,7 @@ interface KnockoutStageRepository : CrudRepository, override fun findAll(): List override fun findById(id: Int): Optional + @Query("select k from KnockoutStageEntity k where k.tournament.id = ?1") fun findAllByTournamentId(tournamentId: Int): List @Query(""" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 6f2bee68..c7ca57a2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -1,7 +1,8 @@ package hu.bme.sch.cmsch.component.tournament -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -9,17 +10,28 @@ import org.springframework.transaction.annotation.Transactional @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( private val matchRepository: TournamentMatchRepository -) { +): ApplicationContextAware { @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { for (i in 1..stage.matches()) { val match = TournamentMatchEntity( - stageId = stage.id, - id = i, - // Set other necessary fields TODO + stage = stage, + gameId = i, + ) matchRepository.save(match) } } + + companion object { + private var applicationContext: ApplicationContext? = null + + fun getBean(): KnockoutStageService = applicationContext?.getBean(KnockoutStageService::class.java) + ?: throw IllegalStateException("Application context is not initialized.") + } + + override fun setApplicationContext(context: ApplicationContext) { + applicationContext = context + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index d8aee7f4..9985f0be 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -65,7 +65,7 @@ data class TournamentEntity( @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(visible = false) + @property:GenerateOverview(visible = true) @property:ImportFormat var participants: String = "", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index a000314a..bb975579 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -14,13 +14,15 @@ import org.springframework.core.env.Environment enum class MatchStatus { NOT_STARTED, FIRST_HALF, - HALF_TIME, + HT, SECOND_HALF, - FULL_TIME, + FT, EXTRA_TIME, + AET, PENALTY_KICKS, - CANCELLED, - FINISHED + AP, + IN_PROGRESS, + CANCELLED } @Entity @@ -38,32 +40,40 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") - @property:GenerateOverview(columnName = "Stage ID", order = 1) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "Game ID") @property:ImportFormat - var stageId: Int = 0, + var gameId: Int = 0, @ManyToOne(targetEntity = KnockoutStageEntity::class) @JoinColumn(name = "stageId", insertable = false, updatable = false) var stage: KnockoutStageEntity? = null, + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 2, label = "Home seed") + var homeSeed: Int = 0, + @Column(nullable = false) var homeTeamId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Home team name") - @property:GenerateOverview(columnName = "Home team name", order = 2) + @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 2, label = "Home team name") + //@property:GenerateOverview(columnName = "Home team name", order = 2) @property:ImportFormat var homeTeamName: String = "", + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 3, label = "Away seed") + var awaySeed: Int = 0, + @Column(nullable = false) var awayTeamId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Away team name") - @property:GenerateOverview(columnName = "Away team name", order = 3) + @property:GenerateInput(type = INPUT_TYPE_TEXT, min = 1, order = 3, label = "Away team name") + //@property:GenerateOverview(columnName = "Away team name", order = 3) @property:ImportFormat var awayTeamName: String = "", @@ -91,7 +101,7 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 7, label = "Match status", - source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP","CANCELLED" ], + source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP", "IN_PROGRESS", "CANCELLED" ], visible = false, ignore = true ) @property:GenerateOverview(columnName = "Match status", order = 7) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt index 0473573f..977777dd 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -22,7 +22,7 @@ interface TournamentMatchRepository : CrudRepository override fun findAll(): List override fun findById(id: Int): Optional fun findAllByStageId(stageId: Int): List - @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") + @Query("select t from TournamentMatchEntity t where t.stage.tournament.id = ?1") fun findAllByStageTournamentId(tournamentId: Int): List @Query(""" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 867832a4..c7051b96 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,15 +21,6 @@ open class TournamentService( return tournamentRepository.findAll() } - @Transactional(readOnly = true) - open fun findTournamentById(id: Int) : Optional { - return tournamentRepository.findById(id) - } - - @Transactional(readOnly = true) - open fun findStagesByTournamentId(tournamentId: Int) : List { - return stageRepository.findAllByTournamentId(tournamentId) - } @Transactional fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { @@ -42,7 +33,7 @@ open class TournamentService( return false } val participants = tournament.get().participants - val parsed = mutableListOf(ParticipantDto(teamId, teamName)) + val parsed = mutableListOf() parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) parsed.add(ParticipantDto(teamId, teamName)) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt index 6f8dd5a7..2025726d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt @@ -1,5 +1,6 @@ package hu.bme.sch.cmsch.config +import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.component.app.ExtraMenuEntity import hu.bme.sch.cmsch.component.app.ExtraMenuRepository import hu.bme.sch.cmsch.component.debt.ProductEntity @@ -23,6 +24,7 @@ import hu.bme.sch.cmsch.component.token.TokenEntity import hu.bme.sch.cmsch.component.token.TokenPropertyEntity import hu.bme.sch.cmsch.component.token.TokenPropertyRepository import hu.bme.sch.cmsch.component.token.TokenRepository +import hu.bme.sch.cmsch.component.tournament.* import hu.bme.sch.cmsch.model.* import hu.bme.sch.cmsch.repository.GroupRepository import hu.bme.sch.cmsch.repository.GroupToUserMappingRepository @@ -82,7 +84,10 @@ open class TestConfig( private val formResponseRepository: Optional, private val extraMenuRepository: ExtraMenuRepository, private val riddleCacheManager: Optional, + private val tournamentRepository: Optional, + private val stageRepository: Optional, private val startupPropertyConfig: StartupPropertyConfig, + private val objectMapper: ObjectMapper, ) { private var now = System.currentTimeMillis() / 1000 @@ -146,7 +151,7 @@ open class TestConfig( "Teszt Form", "test-from", "Form", - "[{\"fieldName\":\"phone\",\"label\":\"Telefonszám\",\"type\":\"PHONE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"allergy\",\"label\":\"Étel érzékenység\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Nincs, Glutén, Laktóz, Glutés és laktóz\",\"note\":\"Ha egyéb is van, kérem írja megjegyzésbe\",\"required\":true,\"permanent\":true},{\"fieldName\":\"love-trains\",\"label\":\"Szereted a mozdonyokat?\",\"type\":\"CHECKBOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"warn1\",\"label\":\"FIGYELEM\",\"type\":\"WARNING_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Ha nem szereti a mozdonyokat, akkor nagyon kellemetlen élete lesz magának kolléga!\",\"required\":false,\"permanent\":false},{\"fieldName\":\"text1\",\"label\":\"Szabályzat\",\"type\":\"TEXT_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"A tábor szabályzata itt olvasható: https://szabalyzat.ssl.nincs.ilyen.domain.hu/asdasdasd/kutya\",\"note\":\"\",\"required\":false,\"permanent\":false},{\"fieldName\":\"agree\",\"label\":\"A szabályzatot elfogadom\",\"type\":\"MUST_AGREE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Különben nem jöhet am\",\"required\":false,\"permanent\":false},{\"fieldName\":\"food\",\"label\":\"Mit enne?\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Gyros tál, Brassói, Pho Leves\",\"note\":\"Első napi kaja\",\"required\":true,\"permanent\":true}]", + "[{\"fieldName\":\"phone\",\"label\":\"Telefonszám\",\"type\":\"PHONE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"allergy\",\"label\":\"Étel érzékenység\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Nincs, Glutén, Laktóz, Glutén és laktóz\",\"note\":\"Ha egyéb is van, kérem írja megjegyzésbe\",\"required\":true,\"permanent\":true},{\"fieldName\":\"love-trains\",\"label\":\"Szereted a mozdonyokat?\",\"type\":\"CHECKBOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"warn1\",\"label\":\"FIGYELEM\",\"type\":\"WARNING_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Ha nem szereti a mozdonyokat, akkor nagyon kellemetlen élete lesz magának kolléga!\",\"required\":false,\"permanent\":false},{\"fieldName\":\"text1\",\"label\":\"Szabályzat\",\"type\":\"TEXT_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"A tábor szabályzata itt olvasható: https://szabalyzat.ssl.nincs.ilyen.domain.hu/asdasdasd/kutya\",\"note\":\"\",\"required\":false,\"permanent\":false},{\"fieldName\":\"agree\",\"label\":\"A szabályzatot elfogadom\",\"type\":\"MUST_AGREE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Különben nem jöhet am\",\"required\":false,\"permanent\":false},{\"fieldName\":\"food\",\"label\":\"Mit enne?\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Gyros tál, Brassói, Pho Leves\",\"note\":\"Első napi kaja\",\"required\":true,\"permanent\":true}]", RoleType.BASIC, RoleType.SUPERUSER, "form submitted", @@ -1125,4 +1130,28 @@ open class TestConfig( extraMenuRepository.save(ExtraMenuEntity(0, "Facebook", "https://facebook.com/xddddddddddd", true)) } + private fun addTournaments(repository: TournamentRepository, stageRepository: KnockoutStageRepository, matchRepository: TournamentMatchRepository){ + val participants1 = mutableListOf() + participants1.add(ParticipantDto(groupRepository.findByName("V10").orElseThrow().id, "V10")) + participants1.add(ParticipantDto(groupRepository.findByName("I16").orElseThrow().id, "I16")) + participants1.add(ParticipantDto(groupRepository.findByName("I09").orElseThrow().id, "I09")) + participants1.add(ParticipantDto(groupRepository.findByName("Vendég").orElseThrow().id, "Vendég")) + participants1.add(ParticipantDto(groupRepository.findByName("Kiállító").orElseThrow().id, "Kiállító")) + val tournament1 = TournamentEntity( + title = "Foci verseny", + description = "A legjobb foci csapat nyer", + location = "BME Sporttelep", + participants = participants1.joinToString("\n") { objectMapper.writeValueAsString(it) }, + participantCount = participants1.size, + ) + repository.save(tournament1) + val stage1 = KnockoutStageEntity( + name = "Kieséses szakasz", + tournament = tournament1, + level = 1, + participantCount = participants1.size, + ) + stageRepository.save(stage1) + } + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 9657ed1a..53095f32 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -1595,7 +1595,7 @@ object StaffPermissions : PermissionGroup { val PERMISSION_SHOW_TOURNAMENTS = PermissionValidator( "TOURNAMENTS_SHOW", - "Verseny megtekintése", + "Versenyek megtekintése", readOnly = true, component = TournamentComponent::class ) @@ -1621,9 +1621,9 @@ object StaffPermissions : PermissionGroup { component = TournamentComponent::class ) - val PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS = PermissionValidator( + val PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS = PermissionValidator( "TOURNAMENT_PARTICIPANTS_SHOW", - "Verseny résztvevők megtekintése", + "Verseny résztvevők kezelése", readOnly = true, component = TournamentComponent::class ) @@ -1842,7 +1842,7 @@ object StaffPermissions : PermissionGroup { PERMISSION_CREATE_TOURNAMENTS, PERMISSION_DELETE_TOURNAMENTS, PERMISSION_EDIT_TOURNAMENTS, - PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS, + PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS, PERMISSION_SET_SEEDS, PERMISSION_GENERATE_GROUPS, PERMISSION_GENERATE_BRACKETS, From 908686f78e890a86e0cf96aeda998b73116e12e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Fri, 14 Feb 2025 12:46:08 +0100 Subject: [PATCH 07/89] still figuring out the schema --- .../tournament/KnockoutStageEntity.kt | 2 +- .../tournament/KnockoutStageService.kt | 11 ++++------- .../tournament/TournamentMatchEntity.kt | 19 +++++++++++++++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 0022d364..d451764d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -69,7 +69,7 @@ data class KnockoutStageEntity( ): ManagedEntity { fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 - fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 + fun matches() = participantCount - 1 override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index c7ca57a2..53a77b6e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -5,6 +5,7 @@ import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import kotlin.math.pow @Service @ConditionalOnBean(TournamentComponent::class) @@ -14,14 +15,10 @@ class KnockoutStageService( @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { - for (i in 1..stage.matches()) { - val match = TournamentMatchEntity( - stage = stage, - gameId = i, + val secondRoundGames = 2.0.pow(stage.rounds().toDouble() - 2).toInt() + val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 + val byeWeekParticipants = stage.participantCount - firstRoundGames * 2 - ) - matchRepository.save(match) - } } companion object { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index bb975579..47244a50 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -53,8 +53,8 @@ data class TournamentMatchEntity( @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 2, label = "Home seed") var homeSeed: Int = 0, - @Column(nullable = false) - var homeTeamId: Int = 0, + @Column(nullable = true) + var homeTeamId: Int? = null, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @@ -67,8 +67,8 @@ data class TournamentMatchEntity( @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 3, label = "Away seed") var awaySeed: Int = 0, - @Column(nullable = false) - var awayTeamId: Int = 0, + @Column(nullable = true) + var awayTeamId: Int? = null, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @@ -128,4 +128,15 @@ data class TournamentMatchEntity( return javaClass.simpleName + "(id = $id)" } + override fun hashCode(): Int = javaClass.hashCode() + + fun winnerId(): Int? { + return when { + homeTeamScore == null || awayTeamScore == null -> null + homeTeamScore!! > awayTeamScore!! -> homeTeamId + homeTeamScore!! < awayTeamScore!! -> awayTeamId + else -> null + } + } + } From a801397a1834644f51fdfc9c1f1f3c3dccdc283d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Wed, 19 Feb 2025 00:51:38 +0100 Subject: [PATCH 08/89] generate matches for knockoutstage --- .../tournament/KnockoutStageEntity.kt | 17 ++++- .../tournament/KnockoutStageRepository.kt | 2 + .../tournament/KnockoutStageService.kt | 67 ++++++++++++++++++- .../component/tournament/StageResultDto.kt | 17 +++++ .../tournament/TournamentMatchEntity.kt | 14 ++-- .../component/tournament/TournamentService.kt | 39 +++++++++++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index d451764d..a0cb398c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -13,7 +13,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import kotlin.math.ceil import kotlin.math.log2 -import kotlin.math.pow + + +enum class TournamentStatus { + CREATED, + DRAFT, + SET, + ONGOING, + FINISHED, + CANCELLED +} @Entity @@ -66,6 +75,12 @@ data class KnockoutStageEntity( @property:ImportFormat var nextRound: Int = 0, + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateOverview(columnName = "Status", order = 5, centered = true) + @property:ImportFormat + var status: TournamentStatus = TournamentStatus.CREATED, + ): ManagedEntity { fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 989724da..8575b2da 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -38,4 +38,6 @@ interface KnockoutStageRepository : CrudRepository, """) fun findAllAggregated(): List + fun findAllByTournamentIdAndLevel(tournamentId: Int, level: Int): List + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 53a77b6e..091d23d7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -1,26 +1,89 @@ package hu.bme.sch.cmsch.component.tournament +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import kotlin.math.pow +import kotlin.random.Random @Service @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( - private val matchRepository: TournamentMatchRepository + private val tournamentService: TournamentService, + private val stageRepository: KnockoutStageRepository, + private val matchRepository: TournamentMatchRepository, + private val objectMapper: ObjectMapper ): ApplicationContextAware { @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { val secondRoundGames = 2.0.pow(stage.rounds().toDouble() - 2).toInt() val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 - val byeWeekParticipants = stage.participantCount - firstRoundGames * 2 + val byeWeekParticipantCount = stage.participantCount - firstRoundGames * 2 + val seedSpots = (1..2*secondRoundGames).asIterable().shuffled().subList(0, byeWeekParticipantCount) + // TODO do better seeding, this is just random stuff + val matches = mutableListOf() + + for (i in 0 until firstRoundGames) { + matches.add(TournamentMatchEntity( + gameId = i + 1, + stage = stage, + homeSeed = i + 1 + byeWeekParticipantCount, + awaySeed = i + 2 + byeWeekParticipantCount + )) + } + var j = 1; var k = 1 + for (i in 1 until secondRoundGames + 1) { + matches.add(TournamentMatchEntity( + gameId = firstRoundGames + j, + stage = stage, + homeSeed = if(seedSpots.contains(2*i-1)) j++ else -(k++), + awaySeed = if(seedSpots.contains(2*i)) j++ else -(k++) + )) + } + for (i in firstRoundGames + secondRoundGames until stage.matches()) { + matches.add(TournamentMatchEntity( + gameId = i + 1, + stage = stage, + homeSeed = -(k++), + awaySeed = -(k++) + )) + } + for (match in matches) { + matchRepository.save(match) + } + + val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() + val participants = tournamentService.getResultsFromLevel(stage.tournament!!.id, stage.level - 1).subList(0, stage.participantCount) + .map { StageResultDto(stage.id, stage.name, it.teamId, it.teamName) } + for (i in 0 until stage.participantCount) { + participants[i].seed = teamSeeds[i] + } + stage.participants = participants.joinToString("\n") { objectMapper.writeValueAsString(it) } + stageRepository.save(stage) + calculateTeamsFromSeeds(stage) + } + + @Transactional + fun calculateTeamsFromSeeds(stage: KnockoutStageEntity) { + val matches = matchRepository.findAllByStageId(stage.id) + val teams = stage.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + for (match in matches){ + val homeTeam = teams.find { it.seed == match.homeSeed } + val awayTeam = teams.find { it.seed == match.awaySeed } + match.homeTeamId = homeTeam?.teamId + match.homeTeamName = homeTeam?.teamName ?: "TBD" + match.awayTeamId = awayTeam?.teamId + match.awayTeamName = awayTeam?.teamName ?: "TBD" + matchRepository.save(match) + } } + companion object { private var applicationContext: ApplicationContext? = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt new file mode 100644 index 00000000..efb2270d --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt @@ -0,0 +1,17 @@ +package hu.bme.sch.cmsch.component.tournament + +data class StageResultDto( + var stageId: Int, + var stageName: String, + var teamId: Int, + var teamName: String, + var seed: Int = 0, + var position: Int = 0, + var points: Int = 0, + var won: Int = 0, + var drawn: Int = 0, + var lost: Int = 0, + var goalsFor: Int = 0, + var goalsAgainst: Int = 0, + var goalDifference: Int = 0 +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index 47244a50..1ea0da0d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -13,13 +13,9 @@ import org.springframework.core.env.Environment enum class MatchStatus { NOT_STARTED, - FIRST_HALF, HT, - SECOND_HALF, FT, - EXTRA_TIME, AET, - PENALTY_KICKS, AP, IN_PROGRESS, CANCELLED @@ -132,6 +128,7 @@ data class TournamentMatchEntity( fun winnerId(): Int? { return when { + status in listOf(MatchStatus.NOT_STARTED, MatchStatus.IN_PROGRESS, MatchStatus.CANCELLED) -> null homeTeamScore == null || awayTeamScore == null -> null homeTeamScore!! > awayTeamScore!! -> homeTeamId homeTeamScore!! < awayTeamScore!! -> awayTeamId @@ -139,4 +136,13 @@ data class TournamentMatchEntity( } } + fun isDraw(): Boolean { + return when { + status in listOf(MatchStatus.NOT_STARTED, MatchStatus.IN_PROGRESS, MatchStatus.CANCELLED) -> false + homeTeamScore == null || awayTeamScore == null -> false + homeTeamScore!! == awayTeamScore!! -> true + else -> false + } + } + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index c7051b96..bf97f6d0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,6 +21,45 @@ open class TournamentService( return tournamentRepository.findAll() } + @Transactional(readOnly = true) + fun getParticipants(tournamentId: Int): List { + val tournament = tournamentRepository.findById(tournamentId) + if (tournament.isEmpty) { + return emptyList() + } + return tournament.get().participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } + } + + @Transactional(readOnly = true) + fun getResultsInStage(tournamentId: Int, stageId: Int): List { + val stage = stageRepository.findById(stageId) + if (stage.isEmpty || stage.get().tournament?.id != tournamentId) { + return emptyList() + } + return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + } + + + @Transactional(readOnly = true) + fun getResultsFromLevel(tournamentId: Int, level: Int): List { + if (level < 1) { + return getParticipants(tournamentId).map { StageResultDto(0, "Lobby", it.teamId, it.teamName) } + } + val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) + if (stages.isEmpty()) { + return emptyList() + } + return stages.flatMap { it.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } }.sortedWith( + compareBy( + { it.position }, + { it.points }, + { it.won }, + { it.goalDifference }, + { it.goalsFor } + ) + ) + } + @Transactional fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { From affbbb3ed57f418405b3cfdc4d769af28e6e0d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Wed, 19 Feb 2025 23:31:32 +0100 Subject: [PATCH 09/89] Api + frontend hook NOT TESTED! --- .../tournament/KnockoutStageEntity.kt | 4 +- .../tournament/KnockoutStageService.kt | 8 +++ .../tournament/TournamentApiController.kt | 49 +++++++++++++- .../tournament/TournamentDetailedView.kt | 37 +++++++++++ .../component/tournament/TournamentEntity.kt | 7 -- .../component/tournament/TournamentService.kt | 6 +- .../tournament/useTournamentListQuery.ts | 17 +++++ .../hooks/tournament/useTournamentQuery.ts | 19 ++++++ .../hooks/tournament/useTournamentsQuery.ts | 17 ----- .../tournament/components/Tournament.tsx | 17 +++++ .../src/pages/tournament/tournament.page.tsx | 24 +++++++ .../pages/tournament/tournamentList.page.tsx | 15 ++--- .../src/route-modules/Tournament.module.tsx | 3 +- frontend/src/util/views/tournament.view.ts | 64 ++++++++++++++++++- 14 files changed, 247 insertions(+), 40 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt create mode 100644 frontend/src/api/hooks/tournament/useTournamentListQuery.ts create mode 100644 frontend/src/api/hooks/tournament/useTournamentQuery.ts delete mode 100644 frontend/src/api/hooks/tournament/useTournamentsQuery.ts create mode 100644 frontend/src/pages/tournament/components/Tournament.tsx create mode 100644 frontend/src/pages/tournament/tournament.page.tsx diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index a0cb398c..0fee2c22 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -15,7 +15,7 @@ import kotlin.math.ceil import kotlin.math.log2 -enum class TournamentStatus { +enum class StageStatus { CREATED, DRAFT, SET, @@ -79,7 +79,7 @@ data class KnockoutStageEntity( @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateOverview(columnName = "Status", order = 5, centered = true) @property:ImportFormat - var status: TournamentStatus = TournamentStatus.CREATED, + var status: StageStatus = StageStatus.CREATED, ): ManagedEntity { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 091d23d7..1eff299f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -94,4 +94,12 @@ class KnockoutStageService( override fun setApplicationContext(context: ApplicationContext) { applicationContext = context } + + fun findStagesByTournamentId(tournamentId: Int): List { + return stageRepository.findAllByTournamentId(tournamentId) + } + + fun findMatchesByStageId(stageId: Int): List { + return matchRepository.findAllByStageId(stageId) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 2b979230..d743d6e8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -16,7 +17,8 @@ import org.springframework.web.bind.annotation.RestController @ConditionalOnBean(TournamentComponent::class) class TournamentApiController( private val tournamentComponent: TournamentComponent, - private val tournamentService: TournamentService + private val tournamentService: TournamentService, + private val stageService: KnockoutStageService ) { @JsonView(Preview::class) @GetMapping("/tournament") @@ -31,4 +33,49 @@ class TournamentApiController( return ResponseEntity.ok(tournaments) } + + @GetMapping("/tournament/{tournamentId}") + @Operation( + summary = "Get details of a tournament.", + ) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Details of the tournament"), + ApiResponse(responseCode = "404", description = "Tournament not found") + ]) + fun tournamentDetails( + @PathVariable tournamentId: Int + ): ResponseEntity{ + val tournament = tournamentService.findById(tournamentId) + if (tournament.isEmpty) { + return ResponseEntity.notFound().build() + } + val stages = stageService.findStagesByTournamentId(tournamentId) + return ResponseEntity.ok(TournamentDetailedView( + TournamentWithParticipants( + tournament.get().id, + tournament.get().title, + tournament.get().description, + tournament.get().location, + tournament.get().participantCount, + tournamentService.getParticipants(tournamentId), + tournament.get().status + ), stages.map { KnockoutStageDetailedView( + it.id, + it.name, + it.level, + it.participantCount, + it.nextRound, + it.status, + stageService.findMatchesByStageId(it.id).map { MatchDto( + it.id, + it.gameId, + if(it.homeTeamId!=null) ParticipantDto(it.homeTeamId!!, it.homeTeamName) else null, + if(it.awayTeamId!=null) ParticipantDto(it.awayTeamId!!, it.awayTeamName) else null, + it.homeTeamScore, + it.awayTeamScore, + it.status + ) } + ) })) + } + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt new file mode 100644 index 00000000..31835657 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -0,0 +1,37 @@ +package hu.bme.sch.cmsch.component.tournament + +data class TournamentWithParticipants( + val id: Int, + val title: String, + val description: String, + val location: String, + val participantCount: Int, + val participants: List, + val status: Int, +) + +data class MatchDto( + val id: Int, + val gameId: Int, + val home: ParticipantDto?, + val away: ParticipantDto?, + val homeScore: Int?, + val awayScore: Int?, + val status: MatchStatus +) + +data class KnockoutStageDetailedView( + val id: Int, + val name: String, + val level: Int, + val participantCount: Int, + val nextRound: Int, + val status: StageStatus, + val matches: List, +) + + +data class TournamentDetailedView( + val tournament: TournamentWithParticipants, + val stages: List, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 9985f0be..bf8c43ac 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -34,13 +34,6 @@ data class TournamentEntity( @property:ImportFormat var title: String = "", - @Column(nullable = false) - @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(maxLength = 16, order = 2, label = "URL") - @property:GenerateOverview(columnName = "URL", order = 2) - @property:ImportFormat - var url: String = "", - @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(maxLength = 64, order = 3, label = "Verseny leírása") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index bf97f6d0..a1921cc2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,6 +21,11 @@ open class TournamentService( return tournamentRepository.findAll() } + @Transactional(readOnly = true) + fun findById(tournamentId: Int): Optional { + return tournamentRepository.findById(tournamentId) + } + @Transactional(readOnly = true) fun getParticipants(tournamentId: Int): List { val tournament = tournamentRepository.findById(tournamentId) @@ -84,5 +89,4 @@ open class TournamentService( return true } - } diff --git a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts new file mode 100644 index 00000000..83c132a9 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts @@ -0,0 +1,17 @@ +import { TournamentPreview } from '../../../util/views/tournament.view.ts' +import { useQuery } from 'react-query' +import { QueryKeys } from '../queryKeys.ts' +import axios from 'axios' +import { ApiPaths } from '../../../util/paths.ts' + + +export const useTournamentListQuery = (onError?: (err: any) => void) => { + return useQuery( + QueryKeys.TOURNAMENTS, + async () => { + const response = await axios.get(ApiPaths.TOURNAMENTS) + return response.data + }, + { onError: onError } + ) +} diff --git a/frontend/src/api/hooks/tournament/useTournamentQuery.ts b/frontend/src/api/hooks/tournament/useTournamentQuery.ts new file mode 100644 index 00000000..ec4bb1e1 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentQuery.ts @@ -0,0 +1,19 @@ +import {useQuery} from "react-query"; +import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; +import {QueryKeys} from "../queryKeys.ts"; +import axios from "axios"; +import {joinPath} from "../../../util/core-functions.util.ts"; +import {ApiPaths} from "../../../util/paths.ts"; + + +export const useTournamentQuery = (id: number, onError?: (err: any) => void) => { + return useQuery( + [QueryKeys.TOURNAMENTS, id], + async () => { + const response = await axios.get(joinPath(ApiPaths.TOURNAMENTS, id)) + return response.data + }, + { onError: onError } + ) + +} diff --git a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts deleted file mode 100644 index 22d20e35..00000000 --- a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TournamentView } from '../../../util/views/tournament.view.ts' -import { useQuery } from 'react-query' -import { QueryKeys } from '../queryKeys.ts' -import axios from 'axios' -import { ApiPaths } from '../../../util/paths.ts' - - -export const useTournamentsQuery = (onError?: (err: any) => void) => { - return useQuery( - QueryKeys.TOURNAMENTS, - async () => { - const response = await axios.get(ApiPaths.TOURNAMENTS) - return response.data - }, - { onError: onError } - ) -} diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx new file mode 100644 index 00000000..459d534f --- /dev/null +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -0,0 +1,17 @@ +import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; + + +interface TournamentProps { + tournament: TournamentDetailsView +} + +const Tournament = ({tournament}: TournamentProps) => { + return ( +
+

{tournament.tournament.title}

+

{tournament.tournament.description}

+
+ ) +} + +export default Tournament diff --git a/frontend/src/pages/tournament/tournament.page.tsx b/frontend/src/pages/tournament/tournament.page.tsx new file mode 100644 index 00000000..a9f7c319 --- /dev/null +++ b/frontend/src/pages/tournament/tournament.page.tsx @@ -0,0 +1,24 @@ +import {useTournamentQuery} from "../../api/hooks/tournament/useTournamentQuery.ts"; +import {useParams} from "react-router-dom"; +import {toInteger} from "lodash"; +import {PageStatus} from "../../common-components/PageStatus.tsx"; +import {CmschPage} from "../../common-components/layout/CmschPage.tsx"; +import Tournament from "./components/Tournament.tsx"; +import {Helmet} from "react-helmet-async"; + + +const TournamentPage = () => { + const { id } = useParams() + const { isLoading, isError, data } = useTournamentQuery(toInteger(id) || 0) + + if (isError || isLoading || !data) return + + return ( + + + + + ) +} + +export default TournamentPage diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 1c6cf087..5745a930 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -1,8 +1,7 @@ -import { useTournamentsQuery } from '../../api/hooks/tournament/useTournamentsQuery.ts' +import { useTournamentListQuery } from '../../api/hooks/tournament/useTournamentListQuery.ts' import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' -import { Box, Heading, useBreakpoint, useBreakpointValue, useDisclosure, VStack } from '@chakra-ui/react' -import { createRef, useState } from 'react' -import { TournamentView } from '../../util/views/tournament.view.ts' +import { Box, Heading, VStack } from '@chakra-ui/react' +import { TournamentPreview } from '../../util/views/tournament.view.ts' import { ComponentUnavailable } from '../../common-components/ComponentUnavailable.tsx' import { PageStatus } from '../../common-components/PageStatus.tsx' import { CmschPage } from '../../common-components/layout/CmschPage.tsx' @@ -10,12 +9,8 @@ import { Helmet } from 'react-helmet-async' const TournamentListPage = () => { - const { isLoading, isError, data } = useTournamentsQuery() + const { isLoading, isError, data } = useTournamentListQuery() const component = useConfigContext()?.components?.tournament - const { isOpen, onToggle } = useDisclosure() - const tabsSize = useBreakpointValue({ base: 'sm', md: 'md' }) - const breakpoint = useBreakpoint() - const inputRef = createRef() if (!component) return @@ -33,7 +28,7 @@ const TournamentListPage = () => { {(data ?? []).length > 0 ? ( - data.map((tournament: TournamentView) => ( + data.map((tournament: TournamentPreview) => ( {tournament.title} diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx index 4fbccc79..8af1a8f7 100644 --- a/frontend/src/route-modules/Tournament.module.tsx +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -1,11 +1,12 @@ import { Route } from 'react-router-dom' import { Paths } from '../util/paths.ts' +import TournamentPage from '../pages/tournament/tournament.page.tsx' import TournamentListPage from '../pages/tournament/tournamentList.page.tsx' export function TournamentModule() { return ( - {/*} />*/} + } /> } /> ) diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 52c4e8d8..d6b77fa9 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -1,5 +1,67 @@ -export type TournamentView = { +export type TournamentPreview = { id: number title: string description: string + location: string + startDate: string + endDate: string + status: string } + +type TournamentWithParticipantsView = { + id: number + title: string + description: string + location: string + participants: ParticipantView[] +} + +type ParticipantView = { + id: number + name: string +} + +enum StageStatus { + CREATED= 'CREATED', + DRAFT = 'DRAFT', + SET = 'SET', + ONGOING = 'ONGOING', + FINISHED = 'FINISHED', + CANCELLED = 'CANCELLED', +} + +enum MatchStatus { + NOT_STARTED = 'NOT_STARTED', + HT = 'HT', + FT = 'FT', + AET = 'AET', + AP = 'AP', + IN_PROGRESS = 'IN_PROGRESS', + CANCELLED = 'CANCELLED', +} + +type MatchView = { + id: number + gameId: number + participant1: ParticipantView + participant2: ParticipantView + score1: number + score2: number + status: MatchStatus +} + +type TournamentStageView = { + id: number + name: string + level: number + participantCount: number + nextRound: number + status: StageStatus + matches: MatchView[] +} + +export type TournamentDetailsView = { + tournament: TournamentWithParticipantsView + stages: TournamentStageView[] +} + From 727f7e1b9668b1d2af599d66eb093adb5778e20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sun, 23 Feb 2025 20:54:41 +0100 Subject: [PATCH 10/89] when did I even touch this file? --- frontend/src/api/contexts/service/ServiceContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/api/contexts/service/ServiceContext.tsx b/frontend/src/api/contexts/service/ServiceContext.tsx index 5595ee12..60da9f63 100644 --- a/frontend/src/api/contexts/service/ServiceContext.tsx +++ b/frontend/src/api/contexts/service/ServiceContext.tsx @@ -17,7 +17,7 @@ export interface MessageOptions { } export type ServiceContextType = { - sendMessage: (message: string | undefined, options?: MessageOptions) => void + sendMessage: (message: string, options?: MessageOptions) => void clearMessage: () => void message?: string type: MessageTypes From e448ffe7a55353dec0d616621991a65beb6cc204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Tue, 28 Jan 2025 01:40:09 +0100 Subject: [PATCH 11/89] rebase onto rebase --- .../tournament/TournamentApiController.kt | 34 +++++++ .../tournament/TournamentComponent.kt | 42 +++++++++ .../TournamentComponentController.kt | 33 +++++++ .../TournamentComponentEntityConfiguration.kt | 11 +++ .../tournament/TournamentController.kt | 57 ++++++++++++ .../component/tournament/TournamentEntity.kt | 59 ++++++++++++ .../tournament/TournamentRepository.kt | 15 +++ .../component/tournament/TournamentService.kt | 19 ++++ .../component/tournament/TournamentsView.kt | 10 ++ .../tournament/tournament-features.md | 64 +++++++++++++ .../sch/cmsch/config/ComponentLoadConfig.kt | 1 + .../sch/cmsch/service/PermissionsService.kt | 92 +++++++++++++++++++ .../resources/config/application.properties | 8 +- frontend/src/api/contexts/config/types.ts | 5 + .../api/contexts/service/ServiceContext.tsx | 2 +- frontend/src/api/hooks/queryKeys.ts | 3 +- .../hooks/tournament/useTournamentsQuery.ts | 17 ++++ .../pages/tournament/tournamentList.page.tsx | 54 +++++++++++ .../src/route-modules/Tournament.module.tsx | 12 +++ frontend/src/util/configs/modules.config.tsx | 87 ++++++++++++++++++ frontend/src/util/paths.ts | 4 +- frontend/src/util/views/tournament.view.ts | 4 + 22 files changed, 627 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md create mode 100644 frontend/src/api/hooks/tournament/useTournamentsQuery.ts create mode 100644 frontend/src/pages/tournament/tournamentList.page.tsx create mode 100644 frontend/src/route-modules/Tournament.module.tsx create mode 100644 frontend/src/util/configs/modules.config.tsx create mode 100644 frontend/src/util/views/tournament.view.ts diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt new file mode 100644 index 00000000..2b979230 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -0,0 +1,34 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.dto.Preview +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +@ConditionalOnBean(TournamentComponent::class) +class TournamentApiController( + private val tournamentComponent: TournamentComponent, + private val tournamentService: TournamentService +) { + @JsonView(Preview::class) + @GetMapping("/tournament") + @Operation( + summary = "List all tournaments.", + ) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "List of tournaments") + ]) + fun tournaments(): ResponseEntity> { + val tournaments = tournamentService.findAll() + return ResponseEntity.ok(tournaments) + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt new file mode 100644 index 00000000..3dc6eace --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -0,0 +1,42 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.component.* +import hu.bme.sch.cmsch.component.app.ComponentSettingService +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.service.ControlPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service + + +@Service +@ConditionalOnProperty( + prefix = "hu.bme.sch.cmsch.component.load", + name = ["tournament"], + havingValue = "true", + matchIfMissing = false +) +class TournamentComponent ( + componentSettingService: ComponentSettingService, + env: Environment +) : ComponentBase( + "tournament", + "/tournament", + "Tournament", + ControlPermissions.PERMISSION_CONTROL_TOURNAMENT, + listOf(), + componentSettingService, env +){ + final override val allSettings by lazy { + listOf( + minRole, + ) + } + + final override val menuDisplayName = null + + final override val minRole = MinRoleSettingProxy(componentSettingService, component, + "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.ADMIN, + fieldName = "Minimum jogosultság", description = "A komponens eléréséhez szükséges minimum jogosultság" + ) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt new file mode 100644 index 00000000..60b24ba9 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt @@ -0,0 +1,33 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.component.ComponentApiBase +import hu.bme.sch.cmsch.component.app.MenuService +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ControlPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/component/tournaments") +@ConditionalOnBean(TournamentComponent::class) +class TournamentComponentController( + adminMenuService: AdminMenuService, + component: TournamentComponent, + private val menuService: MenuService, + private val tournamentService: TournamentService, + private val auditLogService: AuditLogService, + service: MenuService +) : ComponentApiBase( + adminMenuService, + TournamentComponent::class.java, + component, + ControlPermissions.PERMISSION_CONTROL_TOURNAMENT, + "Tournament", + "Tournament beállítások", + auditLogService = auditLogService, + menuService = menuService +) { + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt new file mode 100644 index 00000000..489e5030 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentEntityConfiguration.kt @@ -0,0 +1,11 @@ +package hu.bme.sch.cmsch.component.tournament + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration + + +@Configuration +@ConditionalOnBean(TournamentComponent::class) +@EntityScan(basePackageClasses = [TournamentComponent::class]) +class TournamentComponentEntityConfiguration diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt new file mode 100644 index 00000000..9c7f01db --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt @@ -0,0 +1,57 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.OneDeepEntityPage +import hu.bme.sch.cmsch.controller.admin.calculateSearchSettings +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/tournament") +@ConditionalOnBean(TournamentComponent::class) +class TournamentController( + repo: TournamentRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : OneDeepEntityPage( + "tournament", + TournamentEntity::class, ::TournamentEntity, + "Verseny", "Versenyek", + "A rendezvény versenyeinek kezelése.", + + transactionManager, + repo, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = true, + editEnabled = true, + deleteEnabled = true, + importEnabled = true, + exportEnabled = true, + + adminMenuIcon = "sports_esports", + adminMenuPriority = 1, + searchSettings = calculateSearchSettings(true) +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt new file mode 100644 index 00000000..0fcfc9fd --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -0,0 +1,59 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.Edit +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.hibernate.Hibernate +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment + +@Entity +@Table(name="tournaments") +@ConditionalOnBean(TournamentComponent::class) +data class TournamentEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 1, label = "Verseny neve") + @property:GenerateOverview(columnName = "Név", order = 1) + @property:ImportFormat + var displayName: String = "", + + //TODO: Add more fields + +): ManagedEntity { + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "Tournament", + view = "control/tournaments", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as TournamentEntity + + return id != 0 && id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() + + override fun toString(): String { + return this::class.simpleName + "(id = $id, name = '$displayName')" + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt new file mode 100644 index 00000000..ad80ea0e --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentRepository.kt @@ -0,0 +1,15 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface TournamentRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt new file mode 100644 index 00000000..ccd70922 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -0,0 +1,19 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.GroupRepository +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@ConditionalOnBean(TournamentComponent::class) +open class TournamentService( + private val tournamentRepository: TournamentRepository, + private val groupRepository: GroupRepository, + private val tournamentComponent: TournamentComponent +) { + @Transactional(readOnly = true) + open fun findAll(): List { + return tournamentRepository.findAll() + } +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt new file mode 100644 index 00000000..b1b67072 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt @@ -0,0 +1,10 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview + +data class TournamentsView ( + @field:JsonView(value = [Preview::class, FullDetails::class]) + val tournaments: List +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md new file mode 100644 index 00000000..89fff3ee --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/tournament-features.md @@ -0,0 +1,64 @@ +# Tournament life cycle +1. Previously: we have booked the fields for football, volleyball (and streetball?) +2. Tournament creation + 1. Football, volleyball, streetball; chess, beer pong, video games + 2. Name, max participants, type of tournament (ie. group stage?, knockout phase?) etc. +3. Applications come in for every event +4. Application ends, we need to generate the brackets/groups + 1. Set the seedings for the events + 2. Football, Volleyball, Streetball: group phases for all, others: knockout brackets + - group sizes should be dependent on the count of the playing fields (how many matches can we play overall and concurrently) + - we might want to avoid duplicate pairings over different sports to maintain diversity +5. set the times and places for matches + - we should avoid teams being represented in different sports at the same time (at least in football, volleyball and streetball) +6. group stages for football, volleyball and streetball go, finish after a couple of days +7. get advancing teams, generate knockout brackets for them + - once again, avoid teams being represented at the same time, also try to make the finals be at a different time + +- register game results (possibly even have live scores updated) + +## Views/Pages + +### Registration +- That should be a form (don't know if it can be used as easily) + +### Tournaments +- Lists the different tournaments + - Outside sports might be on the same page, the online tourneys as well + +### Tournament (group) view page +- Tab type 1: Table/bracket for a tournament + - different tournament, different tab for bracket/group standings +- Tab type 2: Game schedules + - ? every tournament's schedule in a group on the same page ? + - Should we even group tournaments? If so, how? + + +## Schema + +### Tournament Group (?) +- Id +- Name +- url + +### Tournament +- Id +- Name +- groupId (optional) (?) +- url + +### TournamentStage +- Id +- tournamentId + +### GroupStage: TournamentStage +- teamsToAdvance + +### TournamentRegistration +- Id +- groupId +- tournamentId +- seed +#### +//- opponents: Opponents + diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt index bc952135..f65be9d5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/ComponentLoadConfig.kt @@ -32,6 +32,7 @@ data class ComponentLoadConfig @ConstructorBinding constructor( var task: Boolean, var team: Boolean, var token: Boolean, + var tournament: Boolean, var accessKeys: Boolean, var conference: Boolean, var email: Boolean, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 95a1a57a..69c5bde1 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -34,6 +34,7 @@ import hu.bme.sch.cmsch.component.staticpage.StaticPageComponent import hu.bme.sch.cmsch.component.task.TaskComponent import hu.bme.sch.cmsch.component.team.TeamComponent import hu.bme.sch.cmsch.component.token.TokenComponent +import hu.bme.sch.cmsch.component.tournament.TournamentComponent import hu.bme.sch.cmsch.extending.CmschPermissionSource import hu.bme.sch.cmsch.util.DI import org.springframework.stereotype.Component @@ -431,6 +432,13 @@ object ControlPermissions : PermissionGroup { component = SheetsComponent::class ) + val PERMISSION_CONTROL_TOURNAMENT = PermissionValidator( + "TOURNAMENT_CONTROL", + "Tournament komponens testreszabása", + readOnly = false, + component = TournamentComponent::class + ) + override fun allPermissions() = listOf( PERMISSION_CONTROL_NEWS, PERMISSION_CONTROL_TASKS, @@ -475,6 +483,7 @@ object ControlPermissions : PermissionGroup { PERMISSION_CONTROL_PROTO, PERMISSION_CONTROL_CONFERENCE, PERMISSION_CONTROL_SHEETS, + PERMISSION_CONTROL_TOURNAMENT, ) } @@ -1582,6 +1591,78 @@ object StaffPermissions : PermissionGroup { component = SheetsComponent::class ) + /// TournamentComponent + + val PERMISSION_SHOW_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_SHOW", + "Verseny megtekintése", + readOnly = true, + component = TournamentComponent::class + ) + + val PERMISSION_CREATE_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_CREATE", + "Versenyek létrehozása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_DELETE_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_DELETE", + "Versenyek törlése", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_EDIT_TOURNAMENTS = PermissionValidator( + "TOURNAMENTS_EDIT", + "Versenyek szerkesztése", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS = PermissionValidator( + "TOURNAMENT_PARTICIPANTS_SHOW", + "Verseny résztvevők megtekintése", + readOnly = true, + component = TournamentComponent::class + ) + + val PERMISSION_SET_SEEDS = PermissionValidator( + "TOURNAMENT_SET_SEEDS", + "Versenyzők seedjeinek állítása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_GROUPS = PermissionValidator( + "TOURNAMENT_GENERATE_GROUPS", + "Verseny csoportok generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_BRACKETS = PermissionValidator( + "TOURNAMENT_GENERATE_BRACKETS", + "Verseny táblák generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_GENERATE_MATCHES = PermissionValidator( + "TOURNAMENT_GENERATE_MATCHES", + "Verseny meccsek generálása", + readOnly = false, + component = TournamentComponent::class + ) + + val PERMISSION_EDIT_RESULTS = PermissionValidator( + "TOURNAMENT_EDIT_RESULTS", + "Verseny eredmények szerkesztése", + readOnly = false, + component = TournamentComponent::class + ) + override fun allPermissions() = listOf( PERMISSION_RATE_TASKS, PERMISSION_SHOW_TASKS, @@ -1756,6 +1837,17 @@ object StaffPermissions : PermissionGroup { PERMISSION_EDIT_SHEETS, PERMISSION_CREATE_SHEETS, PERMISSION_DELETE_SHEETS, + + PERMISSION_SHOW_TOURNAMENTS, + PERMISSION_CREATE_TOURNAMENTS, + PERMISSION_DELETE_TOURNAMENTS, + PERMISSION_EDIT_TOURNAMENTS, + PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS, + PERMISSION_SET_SEEDS, + PERMISSION_GENERATE_GROUPS, + PERMISSION_GENERATE_BRACKETS, + PERMISSION_GENERATE_MATCHES, + PERMISSION_EDIT_RESULTS, ) } diff --git a/backend/src/main/resources/config/application.properties b/backend/src/main/resources/config/application.properties index ed4449cd..9d08cb6f 100644 --- a/backend/src/main/resources/config/application.properties +++ b/backend/src/main/resources/config/application.properties @@ -46,10 +46,10 @@ server.servlet.session.persistent=false server.servlet.session.cookie.same-site=lax spring.datasource.driverClassName=org.h2.Driver -#spring.datasource.username=sa -#spring.datasource.password=password +spring.datasource.username=sa +spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -#spring.datasource.url=jdbc:h2:file:./temp/db +spring.datasource.url=jdbc:h2:file:./temp/db spring.h2.console.enabled=false spring.jpa.hibernate.ddl-auto=update @@ -90,6 +90,7 @@ hu.bme.sch.cmsch.component.load.staticPage=true hu.bme.sch.cmsch.component.load.task=true hu.bme.sch.cmsch.component.load.team=true hu.bme.sch.cmsch.component.load.token=true +hu.bme.sch.cmsch.component.load.tournament=true hu.bme.sch.cmsch.component.load.test=true hu.bme.sch.cmsch.component.load.stats=true @@ -121,6 +122,7 @@ hu.bme.sch.cmsch.proto.priority=128 hu.bme.sch.cmsch.conference.priority=129 hu.bme.sch.cmsch.gallery.priority=130 hu.bme.sch.cmsch.sheets.priority=131 +hu.bme.sch.cmsch.tournament.priority=132 hu.bme.sch.cmsch.app.priority=150 hu.bme.sch.cmsch.app.content.priority=151 hu.bme.sch.cmsch.app.style.priority=152 diff --git a/frontend/src/api/contexts/config/types.ts b/frontend/src/api/contexts/config/types.ts index 7b6b327d..82b59d3a 100644 --- a/frontend/src/api/contexts/config/types.ts +++ b/frontend/src/api/contexts/config/types.ts @@ -39,6 +39,7 @@ export interface Components { qrFight: QrFight communities: Communities footer: Footer + tournament: Tournament } export interface App { @@ -330,3 +331,7 @@ export interface Signup { export interface Communities { title: string } + +export interface Tournament { + title: string +} diff --git a/frontend/src/api/contexts/service/ServiceContext.tsx b/frontend/src/api/contexts/service/ServiceContext.tsx index 60da9f63..5595ee12 100644 --- a/frontend/src/api/contexts/service/ServiceContext.tsx +++ b/frontend/src/api/contexts/service/ServiceContext.tsx @@ -17,7 +17,7 @@ export interface MessageOptions { } export type ServiceContextType = { - sendMessage: (message: string, options?: MessageOptions) => void + sendMessage: (message: string | undefined, options?: MessageOptions) => void clearMessage: () => void message?: string type: MessageTypes diff --git a/frontend/src/api/hooks/queryKeys.ts b/frontend/src/api/hooks/queryKeys.ts index ab96ef4a..62ef917c 100644 --- a/frontend/src/api/hooks/queryKeys.ts +++ b/frontend/src/api/hooks/queryKeys.ts @@ -28,5 +28,6 @@ export enum QueryKeys { COMMUNITY = 'COMMUNITY', ORGANIZATION = 'ORGANIZATION', ACCESS_KEY = 'ACCESS_KEY', - HOME_NEWS = 'HOME_NEWS' + HOME_NEWS = 'HOME_NEWS', + TOURNAMENTS = 'TOURNAMENTS', } diff --git a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts new file mode 100644 index 00000000..22d20e35 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts @@ -0,0 +1,17 @@ +import { TournamentView } from '../../../util/views/tournament.view.ts' +import { useQuery } from 'react-query' +import { QueryKeys } from '../queryKeys.ts' +import axios from 'axios' +import { ApiPaths } from '../../../util/paths.ts' + + +export const useTournamentsQuery = (onError?: (err: any) => void) => { + return useQuery( + QueryKeys.TOURNAMENTS, + async () => { + const response = await axios.get(ApiPaths.TOURNAMENTS) + return response.data + }, + { onError: onError } + ) +} diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx new file mode 100644 index 00000000..398158b5 --- /dev/null +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -0,0 +1,54 @@ +import { useTournamentsQuery } from '../../api/hooks/tournament/useTournamentsQuery.ts' +import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' +import { Box, Heading, useBreakpoint, useBreakpointValue, useDisclosure, VStack } from '@chakra-ui/react' +import { createRef, useState } from 'react' +import { TournamentView } from '../../util/views/tournament.view.ts' +import { ComponentUnavailable } from '../../common-components/ComponentUnavailable.tsx' +import { PageStatus } from '../../common-components/PageStatus.tsx' +import { CmschPage } from '../../common-components/layout/CmschPage.tsx' +import { Helmet } from 'react-helmet-async' + + +const TournamentListPage = () => { + const { isLoading, isError, data } = useTournamentsQuery() + const component = useConfigContext()?.components?.tournament + const { isOpen, onToggle } = useDisclosure() + const tabsSize = useBreakpointValue({ base: 'sm', md: 'md' }) + const breakpoint = useBreakpoint() + const inputRef = createRef() + + if (!component) return + + if (isError || isLoading || !data) return + return ( + + + + + {component.title} + + + {data.length} verseny található. + + + + {(data ?? []).length > 0 ? ( + data.map((tournament: TournamentView) => ( + + + {tournament.displayName} + + + {tournament.displayName} + + + )) + ) : ( + Nincs egyetlen verseny sem. + )} + + + ) +} + +export default TournamentListPage diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx new file mode 100644 index 00000000..08e3c22f --- /dev/null +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -0,0 +1,12 @@ +import { Route } from 'react-router-dom' +import { Paths } from '../util/paths.ts' +import TournamentListPage from '../pages/tournament/TournamentList.page.tsx' + +export function TournamentModule() { + return ( + + {/*} />*/} + } /> + + ) +} diff --git a/frontend/src/util/configs/modules.config.tsx b/frontend/src/util/configs/modules.config.tsx new file mode 100644 index 00000000..34410c8b --- /dev/null +++ b/frontend/src/util/configs/modules.config.tsx @@ -0,0 +1,87 @@ +import React, { FunctionComponent } from 'react' +import { MapModule } from '../../route-modules/Map.module' +import { RiddleModule } from '../../route-modules/Riddle.module' +import { TaskModule } from '../../route-modules/Task.module' +import { NewsModule } from '../../route-modules/News.module' +import { ProfileModule } from '../../route-modules/Profile.module' +import { CommunitiesModule } from '../../route-modules/Communities.module' +import { TokenModule } from '../../route-modules/Token.module' +import { ImpressumModule } from '../../route-modules/Impressum.module' +import { EventsModule } from '../../route-modules/Events.module' +import { LeaderBoardModule } from '../../route-modules/LeaderBoard.module' + +import { ExtraPageModule } from '../../route-modules/ExtraPage.module' +import { HomeModule } from '../../route-modules/Home.module' +import { FormModule } from '../../route-modules/Form.module' +import { TeamModule } from '../../route-modules/Team.module' +import { RaceModule } from '../../route-modules/Race.module' +import { QRFightModule } from '../../route-modules/QRFight.module' +import { AccessKeyModule } from '../../route-modules/AccessKey.module' +import { TournamentModule } from '../../route-modules/Tournament.module.tsx' + +export enum AvailableModules { + HOME = 'HOME', + RIDDLE = 'RIDDLE', + TASK = 'TASK', + IMPRESSUM = 'IMPRESSUM', + NEWS = 'NEWS', + PROFILE = 'PROFILE', + COMMUNITIES = 'COMMUNITIES', + TOKEN = 'TOKEN', + EVENTS = 'EVENTS', + EXTRA = 'EXTRA', + FORM = 'FORM', + LEADER_BOARD = 'LEADER_BOARD', + TEAM = 'TEAM', + RACE = 'RACE', + QR_FIGHT = 'QR_FIGHT', + ACCESS_KEY = 'ACCESS_KEY', + MAP = 'MAP', + TOURNAMENT = 'TOURNAMENT', +} + +export const RoutesForModules: Record = { + [AvailableModules.HOME]: HomeModule, + [AvailableModules.RIDDLE]: RiddleModule, + [AvailableModules.TASK]: TaskModule, + [AvailableModules.IMPRESSUM]: ImpressumModule, + [AvailableModules.NEWS]: NewsModule, + [AvailableModules.PROFILE]: ProfileModule, + [AvailableModules.COMMUNITIES]: CommunitiesModule, + [AvailableModules.TOKEN]: TokenModule, + [AvailableModules.EVENTS]: EventsModule, + [AvailableModules.EXTRA]: ExtraPageModule, + [AvailableModules.FORM]: FormModule, + [AvailableModules.LEADER_BOARD]: LeaderBoardModule, + [AvailableModules.RACE]: RaceModule, + [AvailableModules.QR_FIGHT]: QRFightModule, + [AvailableModules.TEAM]: TeamModule, + [AvailableModules.ACCESS_KEY]: AccessKeyModule, + [AvailableModules.MAP]: MapModule, + [AvailableModules.TOURNAMENT]: TournamentModule +} + +export function GetRoutesForModules(modules: AvailableModules[]) { + return modules.map((m) => {RoutesForModules[m]([])}) +} + +export const EnabledModules: AvailableModules[] = [ + AvailableModules.HOME, + AvailableModules.PROFILE, + AvailableModules.RIDDLE, + AvailableModules.COMMUNITIES, + AvailableModules.TOKEN, + AvailableModules.IMPRESSUM, + AvailableModules.TASK, + AvailableModules.EVENTS, + AvailableModules.NEWS, + AvailableModules.EXTRA, + AvailableModules.FORM, + AvailableModules.LEADER_BOARD, + AvailableModules.RACE, + AvailableModules.QR_FIGHT, + AvailableModules.TEAM, + AvailableModules.ACCESS_KEY, + AvailableModules.MAP, + AvailableModules.TOURNAMENT +] diff --git a/frontend/src/util/paths.ts b/frontend/src/util/paths.ts index f87be5e2..8ee1e2b9 100644 --- a/frontend/src/util/paths.ts +++ b/frontend/src/util/paths.ts @@ -26,7 +26,8 @@ export enum Paths { MY_TEAM = 'my-team', TEAM_ADMIN = 'team-admin', ACCESS_KEY = 'access-key', - MAP = 'map' + MAP = 'map', + TOURNAMENT = 'tournament' } export enum AbsolutePaths { @@ -88,6 +89,7 @@ export enum ApiPaths { HOME_NEWS = '/api/home/news', ADD_PUSH_NOTIFICATION_TOKEN = '/api/pushnotification/add-token', DELETE_PUSH_NOTIFICATION_TOKEN = '/api/pushnotification/delete-token', + TOURNAMENTS = '/api/tournament', MY_TEAM = '/api/team/my', TEAM = '/api/team', ALL_TEAMS = '/api/teams' diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts new file mode 100644 index 00000000..99824252 --- /dev/null +++ b/frontend/src/util/views/tournament.view.ts @@ -0,0 +1,4 @@ +export type TournamentView = { + id: number + displayName: string +} From 4f0aa851636d88265227f3dad10e8aa19bc8d11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Tue, 28 Jan 2025 19:30:51 +0100 Subject: [PATCH 12/89] tournament component - 2nd attribute --- .../cmsch/component/tournament/TournamentEntity.kt | 11 +++++++++-- frontend/src/pages/tournament/tournamentList.page.tsx | 4 ++-- frontend/src/util/views/tournament.view.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 0fcfc9fd..31a3a48c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -31,7 +31,14 @@ data class TournamentEntity( @property:GenerateInput(maxLength = 64, order = 1, label = "Verseny neve") @property:GenerateOverview(columnName = "Név", order = 1) @property:ImportFormat - var displayName: String = "", + var title: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 2, label = "Verseny leírása") + @property:GenerateOverview(columnName = "Leírás", order = 2) + @property:ImportFormat + var description: String = "", //TODO: Add more fields @@ -54,6 +61,6 @@ data class TournamentEntity( override fun hashCode(): Int = javaClass.hashCode() override fun toString(): String { - return this::class.simpleName + "(id = $id, name = '$displayName')" + return this::class.simpleName + "(id = $id, name = '$title')" } } \ No newline at end of file diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 398158b5..1c6cf087 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -36,10 +36,10 @@ const TournamentListPage = () => { data.map((tournament: TournamentView) => ( - {tournament.displayName} + {tournament.title} - {tournament.displayName} + {tournament.description} )) diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 99824252..52c4e8d8 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -1,4 +1,5 @@ export type TournamentView = { id: number - displayName: string + title: string + description: string } From eab29dcf1ebae5113a10a714c3fa19b5675b340b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Thu, 30 Jan 2025 22:38:03 +0100 Subject: [PATCH 13/89] tournament: Component Settings fixed, some reformat, new attributes --- .../tournament/TournamentComponent.kt | 7 +++++++ .../TournamentComponentController.kt | 2 +- .../component/tournament/TournamentEntity.kt | 20 ++++++++++++++++--- .../sch/cmsch/service/PermissionsService.kt | 16 +++++++-------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index 3dc6eace..d42b6f00 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -29,12 +29,19 @@ class TournamentComponent ( ){ final override val allSettings by lazy { listOf( + tournamentGroup, minRole, ) } final override val menuDisplayName = null + final val tournamentGroup = SettingProxy(componentSettingService, component, + "tournamentGroup", "", type = SettingType.COMPONENT_GROUP, persist = false, + fieldName = "Tournament", + description = "Jelenleg nincs mit beállítani itt" + ) + final override val minRole = MinRoleSettingProxy(componentSettingService, component, "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.ADMIN, fieldName = "Minimum jogosultság", description = "A komponens eléréséhez szükséges minimum jogosultság" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt index 60b24ba9..e4288145 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt @@ -10,7 +10,7 @@ import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.RequestMapping @Controller -@RequestMapping("/admin/control/component/tournaments") +@RequestMapping("/admin/control/component/tournament") @ConditionalOnBean(TournamentComponent::class) class TournamentComponentController( adminMenuService: AdminMenuService, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 31a3a48c..d1a49dc2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -35,12 +35,26 @@ data class TournamentEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(maxLength = 64, order = 2, label = "Verseny leírása") - @property:GenerateOverview(columnName = "Leírás", order = 2) + @property:GenerateInput(maxLength = 16, order = 2, label = "URL") + @property:GenerateOverview(columnName = "URL", order = 2) + @property:ImportFormat + var url: String = "", + + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 3, label = "Verseny leírása") + @property:GenerateOverview(columnName = "Leírás", order = 3) @property:ImportFormat var description: String = "", - //TODO: Add more fields + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 4, label = "Verseny helyszíne") + @property:GenerateOverview(columnName = "Helyszín", order = 4) + @property:ImportFormat + var location: String = "", + + //TODO: Add more fields ? ): ManagedEntity { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 69c5bde1..8e2dbe05 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -55,8 +55,8 @@ class PermissionValidator constructor( val description: String = "", val component: KClass? = null, val readOnly: Boolean = false, // Note: this is just a label but used for giving read-only permissions - val validate: Function1 = { - user -> user.isAdmin() || (permissionString.isNotEmpty() && user.hasPermission(permissionString)) + val validate: Function1 = { user -> + user.isAdmin() || (permissionString.isNotEmpty() && user.hasPermission(permissionString)) } ) @@ -91,20 +91,20 @@ object ImplicitPermissions : PermissionGroup { val PERMISSION_IMPLICIT_HAS_GROUP = PermissionValidator( description = "The user has a group", readOnly = false, - permissionString = "HAS_GROUP") - { user -> DI.instance.userService.getById(user.internalId).group != null } + permissionString = "HAS_GROUP" + ) { user -> DI.instance.userService.getById(user.internalId).group != null } val PERMISSION_IMPLICIT_ANYONE = PermissionValidator( description = "Everyone has this permission", readOnly = false, - permissionString = "ANYONE") - { _ -> true } + permissionString = "ANYONE" + ) { _ -> true } val PERMISSION_NOBODY = PermissionValidator( description = "Nobody has this permission", readOnly = false, - permissionString = "NOBODY") - { _ -> false } + permissionString = "NOBODY" + ) { _ -> false } val PERMISSION_SUPERUSER_ONLY = PermissionValidator { user -> user.isSuperuser() } From 340b1f40f4baca8775459e7227a9c62a7e54326b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 3 Feb 2025 16:41:20 +0100 Subject: [PATCH 14/89] tournament - knockout stage, registration - backend stuff --- .../component/tournament/KnockoutGroupDto.kt | 24 +++++ .../tournament/KnockoutStageController.kt | 77 +++++++++++++++ .../tournament/KnockoutStageEntity.kt | 96 +++++++++++++++++++ .../tournament/KnockoutStageRepository.kt | 40 ++++++++ .../component/tournament/ParticipantDto.kt | 6 ++ .../component/tournament/TournamentEntity.kt | 26 ++++- .../component/tournament/TournamentService.kt | 41 +++++++- .../component/tournament/TournamentsView.kt | 10 -- .../src/route-modules/Tournament.module.tsx | 2 +- 9 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt delete mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt new file mode 100644 index 00000000..7b9ae050 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt @@ -0,0 +1,24 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.admin.GenerateOverview +import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.model.IdentifiableEntity + +data class KnockoutGroupDto( + + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @property:GenerateOverview(columnName = "Név", order = 1) + var name: String = "", + + @property:GenerateOverview(columnName = "Helyszín", order = 4) + var location: String = "", + + @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + var participantCount: Int = 0, + + @property:GenerateOverview(columnName = "Szakaszok száma", order = 6) + var stageCount: Int = 0 + +): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt new file mode 100644 index 00000000..9990c7f1 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -0,0 +1,77 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.ButtonAction +import hu.bme.sch.cmsch.controller.admin.TwoDeepEntityPage +import hu.bme.sch.cmsch.repository.ManualRepository +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + + +@Controller +@RequestMapping("/admin/control/knockout-stage") +@ConditionalOnBean(TournamentComponent::class) +class KnockoutStageController( + private val stageRepository: KnockoutStageRepository, + private val tournamentRepository: TournamentRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : TwoDeepEntityPage( + "knockout-stage", + KnockoutGroupDto::class, + KnockoutStageEntity::class, ::KnockoutStageEntity, + "Kiesési szakasz", "Kiesési szakaszok", + "A kiesési szakaszok kezelése.", + transactionManager, + object : ManualRepository() { + override fun findAll(): Iterable { + val stages = stageRepository.findAllAggregated() + val tournaments = tournamentRepository.findAll().associateBy { it.id } + return stages.map { + KnockoutGroupDto( + it.tournamentId, + it.tournamentName, + it.tournamentLocation, + it.participantCount, + it.stageCount.toInt() + ) + }.sortedByDescending { it.stageCount }.toList() + } + }, + stageRepository, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = true, + editEnabled = true, + deleteEnabled = true, + importEnabled = false, + exportEnabled = false, + + adminMenuIcon = "lan", +) { + override fun fetchSublist(id: Int): Iterable { + return stageRepository.findAllByTournamentId(id) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt new file mode 100644 index 00000000..567941d6 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -0,0 +1,96 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.Edit +import hu.bme.sch.cmsch.dto.FullDetails +import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import kotlin.math.ceil +import kotlin.math.log2 + + +@Entity +@Table(name = "knockout_stage") +@ConditionalOnBean(TournamentComponent::class) +data class KnockoutStageEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(maxLength = 64, order = 1, label = "Szakasz neve") + @property:GenerateOverview(columnName = "Név", order = 1) + @property:ImportFormat + var name: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") + @property:GenerateOverview(columnName = "Verseny ID", order = 2) + @property:ImportFormat + var tournamentId: Int = 0, + + @ManyToOne(targetEntity = TournamentEntity::class) + @JoinColumn(name = "tournamentId", insertable = false, updatable = false) + var tournament: TournamentEntity? = null, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Szint") + @property:GenerateOverview(columnName = "Szint", order = 3, centered = true) + @property:ImportFormat + var level: Int = 1, //ie. Csoportkör-1, Csoportkör-2, Kieséses szakasz-3 + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Résztvevők száma") + @property:GenerateOverview(columnName = "RésztvevőSzám", order = 3, centered = true) + @property:ImportFormat + var participantCount: Int = 1, + + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateOverview(columnName = "Következő kör", order = 4, centered = true) + @property:ImportFormat + var nextRound: Int = 0, + +): ManagedEntity { + + fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "KnockoutStage", + view = "control/tournament/knockout-stage", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KnockoutStageEntity) return false + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int = javaClass.hashCode() + + override fun toString(): String { + return this::class.simpleName + "(id = $id, name = $name, tournamentId = $tournament.id, participantCount = $participantCount)" + } + + + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt new file mode 100644 index 00000000..083fb858 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -0,0 +1,40 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.* + +data class StageCountDto( + var tournamentId: Int = 0, + var tournamentName: String = "", + var tournamentLocation : String = "", + var participantCount: Int = 0, + var stageCount: Long = 0 +) + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface KnockoutStageRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + override fun findById(id: Int): Optional + fun findAllByTournamentId(tournamentId: Int): List + + @Query(""" + SELECT NEW hu.bme.sch.cmsch.component.tournament.StageCountDto( + s.tournament.id, + s.tournament.title, + s.tournament.location, + s.tournament.participantCount, + COUNT(s.id) + ) + FROM KnockoutStageEntity s + GROUP BY s.tournament + """) + fun findAllAggregated(): List + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt new file mode 100644 index 00000000..5b7eb28a --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt @@ -0,0 +1,6 @@ +package hu.bme.sch.cmsch.component.tournament + +data class ParticipantDto( + var teamId: Int = 0, + var teamName: String = "", +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index d1a49dc2..d8aee7f4 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -1,6 +1,7 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.annotation.JsonView +import com.google.j2objc.annotations.GenerateObjectiveCGenerics import hu.bme.sch.cmsch.admin.* import hu.bme.sch.cmsch.component.EntityConfig import hu.bme.sch.cmsch.dto.Edit @@ -14,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment @Entity -@Table(name="tournaments") +@Table(name="tournament") @ConditionalOnBean(TournamentComponent::class) data class TournamentEntity( @@ -54,13 +55,32 @@ data class TournamentEntity( @property:ImportFormat var location: String = "", - //TODO: Add more fields ? + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + @property:ImportFormat + var participantCount: Int = 0, + + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var participants: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var status: Int = 0, ): ManagedEntity { override fun getEntityConfig(env: Environment) = EntityConfig( name = "Tournament", - view = "control/tournaments", + view = "control/tournament", showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, ) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index ccd70922..867832a4 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -1,19 +1,58 @@ package hu.bme.sch.cmsch.component.tournament +import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.util.* @Service @ConditionalOnBean(TournamentComponent::class) open class TournamentService( private val tournamentRepository: TournamentRepository, + private val stageRepository: KnockoutStageRepository, private val groupRepository: GroupRepository, - private val tournamentComponent: TournamentComponent + private val tournamentComponent: TournamentComponent, + private val objectMapper: ObjectMapper ) { @Transactional(readOnly = true) open fun findAll(): List { return tournamentRepository.findAll() } + + @Transactional(readOnly = true) + open fun findTournamentById(id: Int) : Optional { + return tournamentRepository.findById(id) + } + + @Transactional(readOnly = true) + open fun findStagesByTournamentId(tournamentId: Int) : List { + return stageRepository.findAllByTournamentId(tournamentId) + } + + @Transactional + fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { + val tournament = tournamentRepository.findById(tournamentId) + if (tournament.isEmpty) { + return false + } + val group = groupRepository.findById(teamId) + if (group.isEmpty) { + return false + } + val participants = tournament.get().participants + val parsed = mutableListOf(ParticipantDto(teamId, teamName)) + parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) + + parsed.add(ParticipantDto(teamId, teamName)) + + tournament.get().participants = parsed.joinToString("\n") { objectMapper.writeValueAsString(it) } + tournament.get().participantCount = parsed.size + + tournamentRepository.save(tournament.get()) + return true + } + + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt deleted file mode 100644 index b1b67072..00000000 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentsView.kt +++ /dev/null @@ -1,10 +0,0 @@ -package hu.bme.sch.cmsch.component.tournament - -import com.fasterxml.jackson.annotation.JsonView -import hu.bme.sch.cmsch.dto.FullDetails -import hu.bme.sch.cmsch.dto.Preview - -data class TournamentsView ( - @field:JsonView(value = [Preview::class, FullDetails::class]) - val tournaments: List -) diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx index 08e3c22f..4fbccc79 100644 --- a/frontend/src/route-modules/Tournament.module.tsx +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -1,6 +1,6 @@ import { Route } from 'react-router-dom' import { Paths } from '../util/paths.ts' -import TournamentListPage from '../pages/tournament/TournamentList.page.tsx' +import TournamentListPage from '../pages/tournament/tournamentList.page.tsx' export function TournamentModule() { return ( From f1ba65d59af113dc32f54b9f56b2ced03a9b5bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek=C2=98?= Date: Wed, 5 Feb 2025 18:23:41 +0100 Subject: [PATCH 15/89] tournament match - entity, repository done, controller done --- .../component/tournament/KnockoutGroupDto.kt | 6 +- .../tournament/KnockoutStageEntity.kt | 13 ++ .../tournament/KnockoutStageService.kt | 25 ++++ .../component/tournament/MatchGroupDto.kt | 21 +++ .../tournament/TournamentMatchController.kt | 72 +++++++++++ .../tournament/TournamentMatchEntity.kt | 121 ++++++++++++++++++ .../tournament/TournamentMatchRepository.kt | 40 ++++++ 7 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt index 7b9ae050..f3cac939 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt @@ -12,13 +12,13 @@ data class KnockoutGroupDto( @property:GenerateOverview(columnName = "Név", order = 1) var name: String = "", - @property:GenerateOverview(columnName = "Helyszín", order = 4) + @property:GenerateOverview(columnName = "Helyszín", order = 2) var location: String = "", - @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + @property:GenerateOverview(columnName = "Résztvevők száma", order = 3) var participantCount: Int = 0, - @property:GenerateOverview(columnName = "Szakaszok száma", order = 6) + @property:GenerateOverview(columnName = "Szakaszok száma", order = 4) var stageCount: Int = 0 ): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 567941d6..3abb3f5e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -9,10 +9,14 @@ import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.ManagedEntity import hu.bme.sch.cmsch.service.StaffPermissions import jakarta.persistence.* +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext import org.springframework.core.env.Environment +import java.security.Provider import kotlin.math.ceil import kotlin.math.log2 +import kotlin.math.pow @Entity @@ -68,7 +72,12 @@ data class KnockoutStageEntity( ): ManagedEntity { + @Autowired + @Transient + private lateinit var knockoutStageService: KnockoutStageService + fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 + fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", @@ -92,5 +101,9 @@ data class KnockoutStageEntity( } + @PrePersist + fun onPrePersist(){ + knockoutStageService.createMatchesForStage(this) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt new file mode 100644 index 00000000..6f2bee68 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -0,0 +1,25 @@ +package hu.bme.sch.cmsch.component.tournament + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@ConditionalOnBean(TournamentComponent::class) +class KnockoutStageService( + private val matchRepository: TournamentMatchRepository +) { + + @Transactional + fun createMatchesForStage(stage: KnockoutStageEntity) { + for (i in 1..stage.matches()) { + val match = TournamentMatchEntity( + stageId = stage.id, + id = i, + // Set other necessary fields TODO + ) + matchRepository.save(match) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt new file mode 100644 index 00000000..b306b37b --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt @@ -0,0 +1,21 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.admin.GenerateOverview +import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.model.IdentifiableEntity + +data class MatchGroupDto( + + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + override var id: Int = 0, + + @property:GenerateOverview(columnName = "Név", order = 1) + var name: String = "", + + @property:GenerateOverview(columnName = "Helyszín", order = 2) + var location: String = "", + + @property:GenerateOverview(columnName = "Közeli meccsek száma", order = 3) + var matchCount: Int = 0, + +): IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt new file mode 100644 index 00000000..84313521 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -0,0 +1,72 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.controller.admin.TwoDeepEntityPage +import hu.bme.sch.cmsch.repository.ManualRepository +import hu.bme.sch.cmsch.service.AdminMenuService +import hu.bme.sch.cmsch.service.AuditLogService +import hu.bme.sch.cmsch.service.ImportService +import hu.bme.sch.cmsch.service.StaffPermissions +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment +import org.springframework.stereotype.Controller +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/admin/control/tournament-match") +@ConditionalOnBean(TournamentComponent::class) +class TournamentMatchController( + private val matchRepository: TournamentMatchRepository, + importService: ImportService, + adminMenuService: AdminMenuService, + component: TournamentComponent, + auditLog: AuditLogService, + objectMapper: ObjectMapper, + transactionManager: PlatformTransactionManager, + env: Environment +) : TwoDeepEntityPage( + "tournament-match", + MatchGroupDto::class, + TournamentMatchEntity::class, ::TournamentMatchEntity, + "Mérkőzés", "Mérkőzések", + "A mérkőzések kezelése.", + transactionManager, + object : ManualRepository() { + override fun findAll(): Iterable { + val matches = matchRepository.findAllAggregated() + return matches.map { + MatchGroupDto( + it.tournamentId, + it.tournamentName, + it.tournamentLocation, + it.matchCount.toInt() + ) + }.sortedByDescending { it.matchCount }.toList() + } + }, + matchRepository, + importService, + adminMenuService, + component, + auditLog, + objectMapper, + env, + + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + createPermission = StaffPermissions.PERMISSION_CREATE_TOURNAMENTS, + editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, + deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, + + createEnabled = false, + editEnabled = true, + deleteEnabled = false, + importEnabled = false, + exportEnabled = false, + + adminMenuIcon = "compare_arrows", +) { + override fun fetchSublist(id: Int): Iterable { + return matchRepository.findAllByStageTournamentId(id) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt new file mode 100644 index 00000000..a000314a --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -0,0 +1,121 @@ +package hu.bme.sch.cmsch.component.tournament + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.admin.* +import hu.bme.sch.cmsch.component.EntityConfig +import hu.bme.sch.cmsch.dto.* +import hu.bme.sch.cmsch.model.ManagedEntity +import hu.bme.sch.cmsch.service.StaffPermissions +import jakarta.persistence.* +import org.hibernate.Hibernate +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.core.env.Environment + +enum class MatchStatus { + NOT_STARTED, + FIRST_HALF, + HALF_TIME, + SECOND_HALF, + FULL_TIME, + EXTRA_TIME, + PENALTY_KICKS, + CANCELLED, + FINISHED +} + +@Entity +@Table(name = "tournament_match") +@ConditionalOnBean(TournamentComponent::class) +data class TournamentMatchEntity( + + @Id + @GeneratedValue + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + override var id: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") + @property:GenerateOverview(columnName = "Stage ID", order = 1) + @property:ImportFormat + var stageId: Int = 0, + + @ManyToOne(targetEntity = KnockoutStageEntity::class) + @JoinColumn(name = "stageId", insertable = false, updatable = false) + var stage: KnockoutStageEntity? = null, + + @Column(nullable = false) + var homeTeamId: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Home team name") + @property:GenerateOverview(columnName = "Home team name", order = 2) + @property:ImportFormat + var homeTeamName: String = "", + + @Column(nullable = false) + var awayTeamId: Int = 0, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Away team name") + @property:GenerateOverview(columnName = "Away team name", order = 3) + @property:ImportFormat + var awayTeamName: String = "", + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_DATE, order = 4, label = "Kickoff time") + @property:GenerateOverview(columnName = "Kickoff time", order = 4) + @property:ImportFormat + var kickoffTime: Long = 0, + + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 5, label = "Home team score") + @property:GenerateOverview(columnName = "Home team score", order = 5) + @property:ImportFormat + var homeTeamScore: Int? = null, + + @Column(nullable = true) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 6, label = "Away team score") + @property:GenerateOverview(columnName = "Away team score", order = 6) + @property:ImportFormat + var awayTeamScore: Int? = null, + + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 7, label = "Match status", + source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP","CANCELLED" ], + visible = false, ignore = true + ) + @property:GenerateOverview(columnName = "Match status", order = 7) + @property:ImportFormat + val status: MatchStatus = MatchStatus.NOT_STARTED, + +): ManagedEntity{ + + override fun getEntityConfig(env: Environment) = EntityConfig( + name = "TournamentMatch", + view = "control/tournament/match", + showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as TournamentMatchEntity + + return id != 0 && id == other.id + } + + override fun toString(): String { + return javaClass.simpleName + "(id = $id)" + } + +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt new file mode 100644 index 00000000..0473573f --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -0,0 +1,40 @@ +package hu.bme.sch.cmsch.component.tournament + +import hu.bme.sch.cmsch.repository.EntityPageDataSource +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.* + +data class MatchCountDto( + var tournamentId: Int = 0, + var tournamentName: String = "", + var tournamentLocation : String = "", + var matchCount: Long = 0 +) + +@Repository +@ConditionalOnBean(TournamentComponent::class) +interface TournamentMatchRepository : CrudRepository, + EntityPageDataSource { + + override fun findAll(): List + override fun findById(id: Int): Optional + fun findAllByStageId(stageId: Int): List + @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") + fun findAllByStageTournamentId(tournamentId: Int): List + + @Query(""" + SELECT NEW hu.bme.sch.cmsch.component.tournament.MatchCountDto( + s.tournament.id, + s.tournament.title, + s.tournament.location, + COUNT(t.id) + ) + FROM TournamentMatchEntity t + JOIN t.stage s + GROUP BY s.tournament + """) + fun findAllAggregated(): List +} \ No newline at end of file From 11197f0c9a20895433bdf28a0033cdfad4966ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 10 Feb 2025 22:59:39 +0100 Subject: [PATCH 16/89] some test data; still figuring out the schema --- .../tournament/KnockoutStageEntity.kt | 27 ++++++--------- .../tournament/KnockoutStageRepository.kt | 1 + .../tournament/KnockoutStageService.kt | 22 +++++++++--- .../component/tournament/TournamentEntity.kt | 2 +- .../tournament/TournamentMatchEntity.kt | 34 ++++++++++++------- .../tournament/TournamentMatchRepository.kt | 2 +- .../component/tournament/TournamentService.kt | 11 +----- .../hu/bme/sch/cmsch/config/TestConfig.kt | 31 ++++++++++++++++- .../sch/cmsch/service/PermissionsService.kt | 8 ++--- 9 files changed, 87 insertions(+), 51 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 3abb3f5e..0022d364 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -9,11 +9,8 @@ import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.ManagedEntity import hu.bme.sch.cmsch.service.StaffPermissions import jakarta.persistence.* -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.ApplicationContext import org.springframework.core.env.Environment -import java.security.Provider import kotlin.math.ceil import kotlin.math.log2 import kotlin.math.pow @@ -39,15 +36,7 @@ data class KnockoutStageEntity( @property:ImportFormat var name: String = "", - @Column(nullable = false) - @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") - @property:GenerateOverview(columnName = "Verseny ID", order = 2) - @property:ImportFormat - var tournamentId: Int = 0, - @ManyToOne(targetEntity = TournamentEntity::class) - @JoinColumn(name = "tournamentId", insertable = false, updatable = false) var tournament: TournamentEntity? = null, @Column(nullable = false) @@ -64,6 +53,13 @@ data class KnockoutStageEntity( @property:ImportFormat var participantCount: Int = 1, + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var participants: String = "", + @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateOverview(columnName = "Következő kör", order = 4, centered = true) @@ -72,10 +68,6 @@ data class KnockoutStageEntity( ): ManagedEntity { - @Autowired - @Transient - private lateinit var knockoutStageService: KnockoutStageService - fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 @@ -102,8 +94,9 @@ data class KnockoutStageEntity( @PrePersist - fun onPrePersist(){ - knockoutStageService.createMatchesForStage(this) + fun prePersist() { + val stageService = KnockoutStageService.getBean() + stageService.createMatchesForStage(this) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 083fb858..989724da 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -22,6 +22,7 @@ interface KnockoutStageRepository : CrudRepository, override fun findAll(): List override fun findById(id: Int): Optional + @Query("select k from KnockoutStageEntity k where k.tournament.id = ?1") fun findAllByTournamentId(tournamentId: Int): List @Query(""" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 6f2bee68..c7ca57a2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -1,7 +1,8 @@ package hu.bme.sch.cmsch.component.tournament -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -9,17 +10,28 @@ import org.springframework.transaction.annotation.Transactional @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( private val matchRepository: TournamentMatchRepository -) { +): ApplicationContextAware { @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { for (i in 1..stage.matches()) { val match = TournamentMatchEntity( - stageId = stage.id, - id = i, - // Set other necessary fields TODO + stage = stage, + gameId = i, + ) matchRepository.save(match) } } + + companion object { + private var applicationContext: ApplicationContext? = null + + fun getBean(): KnockoutStageService = applicationContext?.getBean(KnockoutStageService::class.java) + ?: throw IllegalStateException("Application context is not initialized.") + } + + override fun setApplicationContext(context: ApplicationContext) { + applicationContext = context + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index d8aee7f4..9985f0be 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -65,7 +65,7 @@ data class TournamentEntity( @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(visible = false) + @property:GenerateOverview(visible = true) @property:ImportFormat var participants: String = "", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index a000314a..bb975579 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -14,13 +14,15 @@ import org.springframework.core.env.Environment enum class MatchStatus { NOT_STARTED, FIRST_HALF, - HALF_TIME, + HT, SECOND_HALF, - FULL_TIME, + FT, EXTRA_TIME, + AET, PENALTY_KICKS, - CANCELLED, - FINISHED + AP, + IN_PROGRESS, + CANCELLED } @Entity @@ -38,32 +40,40 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") - @property:GenerateOverview(columnName = "Stage ID", order = 1) + @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "Game ID") @property:ImportFormat - var stageId: Int = 0, + var gameId: Int = 0, @ManyToOne(targetEntity = KnockoutStageEntity::class) @JoinColumn(name = "stageId", insertable = false, updatable = false) var stage: KnockoutStageEntity? = null, + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 2, label = "Home seed") + var homeSeed: Int = 0, + @Column(nullable = false) var homeTeamId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Home team name") - @property:GenerateOverview(columnName = "Home team name", order = 2) + @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 2, label = "Home team name") + //@property:GenerateOverview(columnName = "Home team name", order = 2) @property:ImportFormat var homeTeamName: String = "", + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 3, label = "Away seed") + var awaySeed: Int = 0, + @Column(nullable = false) var awayTeamId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Away team name") - @property:GenerateOverview(columnName = "Away team name", order = 3) + @property:GenerateInput(type = INPUT_TYPE_TEXT, min = 1, order = 3, label = "Away team name") + //@property:GenerateOverview(columnName = "Away team name", order = 3) @property:ImportFormat var awayTeamName: String = "", @@ -91,7 +101,7 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 7, label = "Match status", - source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP","CANCELLED" ], + source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP", "IN_PROGRESS", "CANCELLED" ], visible = false, ignore = true ) @property:GenerateOverview(columnName = "Match status", order = 7) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt index 0473573f..977777dd 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -22,7 +22,7 @@ interface TournamentMatchRepository : CrudRepository override fun findAll(): List override fun findById(id: Int): Optional fun findAllByStageId(stageId: Int): List - @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") + @Query("select t from TournamentMatchEntity t where t.stage.tournament.id = ?1") fun findAllByStageTournamentId(tournamentId: Int): List @Query(""" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 867832a4..c7051b96 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,15 +21,6 @@ open class TournamentService( return tournamentRepository.findAll() } - @Transactional(readOnly = true) - open fun findTournamentById(id: Int) : Optional { - return tournamentRepository.findById(id) - } - - @Transactional(readOnly = true) - open fun findStagesByTournamentId(tournamentId: Int) : List { - return stageRepository.findAllByTournamentId(tournamentId) - } @Transactional fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { @@ -42,7 +33,7 @@ open class TournamentService( return false } val participants = tournament.get().participants - val parsed = mutableListOf(ParticipantDto(teamId, teamName)) + val parsed = mutableListOf() parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) parsed.add(ParticipantDto(teamId, teamName)) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt index 3a2d3ff6..e3804f95 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt @@ -1,5 +1,6 @@ package hu.bme.sch.cmsch.config +import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.component.app.ExtraMenuEntity import hu.bme.sch.cmsch.component.app.ExtraMenuRepository import hu.bme.sch.cmsch.component.debt.ProductEntity @@ -23,6 +24,7 @@ import hu.bme.sch.cmsch.component.token.TokenEntity import hu.bme.sch.cmsch.component.token.TokenPropertyEntity import hu.bme.sch.cmsch.component.token.TokenPropertyRepository import hu.bme.sch.cmsch.component.token.TokenRepository +import hu.bme.sch.cmsch.component.tournament.* import hu.bme.sch.cmsch.model.* import hu.bme.sch.cmsch.repository.GroupRepository import hu.bme.sch.cmsch.repository.GroupToUserMappingRepository @@ -82,7 +84,10 @@ open class TestConfig( private val formResponseRepository: Optional, private val extraMenuRepository: ExtraMenuRepository, private val riddleCacheManager: Optional, + private val tournamentRepository: Optional, + private val stageRepository: Optional, private val startupPropertyConfig: StartupPropertyConfig, + private val objectMapper: ObjectMapper, ) { private var now = System.currentTimeMillis() / 1000 @@ -146,7 +151,7 @@ open class TestConfig( "Teszt Form", "test-from", "Form", - "[{\"fieldName\":\"phone\",\"label\":\"Telefonszám\",\"type\":\"PHONE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"allergy\",\"label\":\"Étel érzékenység\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Nincs, Glutén, Laktóz, Glutés és laktóz\",\"note\":\"Ha egyéb is van, kérem írja megjegyzésbe\",\"required\":true,\"permanent\":true},{\"fieldName\":\"love-trains\",\"label\":\"Szereted a mozdonyokat?\",\"type\":\"CHECKBOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"warn1\",\"label\":\"FIGYELEM\",\"type\":\"WARNING_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Ha nem szereti a mozdonyokat, akkor nagyon kellemetlen élete lesz magának kolléga!\",\"required\":false,\"permanent\":false},{\"fieldName\":\"text1\",\"label\":\"Szabályzat\",\"type\":\"TEXT_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"A tábor szabályzata itt olvasható: https://szabalyzat.ssl.nincs.ilyen.domain.hu/asdasdasd/kutya\",\"note\":\"\",\"required\":false,\"permanent\":false},{\"fieldName\":\"agree\",\"label\":\"A szabályzatot elfogadom\",\"type\":\"MUST_AGREE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Különben nem jöhet am\",\"required\":false,\"permanent\":false},{\"fieldName\":\"food\",\"label\":\"Mit enne?\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Gyros tál, Brassói, Pho Leves\",\"note\":\"Első napi kaja\",\"required\":true,\"permanent\":true}]", + "[{\"fieldName\":\"phone\",\"label\":\"Telefonszám\",\"type\":\"PHONE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"allergy\",\"label\":\"Étel érzékenység\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Nincs, Glutén, Laktóz, Glutén és laktóz\",\"note\":\"Ha egyéb is van, kérem írja megjegyzésbe\",\"required\":true,\"permanent\":true},{\"fieldName\":\"love-trains\",\"label\":\"Szereted a mozdonyokat?\",\"type\":\"CHECKBOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"\",\"required\":true,\"permanent\":true},{\"fieldName\":\"warn1\",\"label\":\"FIGYELEM\",\"type\":\"WARNING_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Ha nem szereti a mozdonyokat, akkor nagyon kellemetlen élete lesz magának kolléga!\",\"required\":false,\"permanent\":false},{\"fieldName\":\"text1\",\"label\":\"Szabályzat\",\"type\":\"TEXT_BOX\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"A tábor szabályzata itt olvasható: https://szabalyzat.ssl.nincs.ilyen.domain.hu/asdasdasd/kutya\",\"note\":\"\",\"required\":false,\"permanent\":false},{\"fieldName\":\"agree\",\"label\":\"A szabályzatot elfogadom\",\"type\":\"MUST_AGREE\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"\",\"note\":\"Különben nem jöhet am\",\"required\":false,\"permanent\":false},{\"fieldName\":\"food\",\"label\":\"Mit enne?\",\"type\":\"SELECT\",\"formatRegex\":\".*\",\"invalidFormatMessage\":\"\",\"values\":\"Gyros tál, Brassói, Pho Leves\",\"note\":\"Első napi kaja\",\"required\":true,\"permanent\":true}]", RoleType.BASIC, RoleType.SUPERUSER, "form submitted", @@ -1127,4 +1132,28 @@ open class TestConfig( extraMenuRepository.save(ExtraMenuEntity(0, "Facebook", "https://facebook.com/xddddddddddd", true)) } + private fun addTournaments(repository: TournamentRepository, stageRepository: KnockoutStageRepository, matchRepository: TournamentMatchRepository){ + val participants1 = mutableListOf() + participants1.add(ParticipantDto(groupRepository.findByName("V10").orElseThrow().id, "V10")) + participants1.add(ParticipantDto(groupRepository.findByName("I16").orElseThrow().id, "I16")) + participants1.add(ParticipantDto(groupRepository.findByName("I09").orElseThrow().id, "I09")) + participants1.add(ParticipantDto(groupRepository.findByName("Vendég").orElseThrow().id, "Vendég")) + participants1.add(ParticipantDto(groupRepository.findByName("Kiállító").orElseThrow().id, "Kiállító")) + val tournament1 = TournamentEntity( + title = "Foci verseny", + description = "A legjobb foci csapat nyer", + location = "BME Sporttelep", + participants = participants1.joinToString("\n") { objectMapper.writeValueAsString(it) }, + participantCount = participants1.size, + ) + repository.save(tournament1) + val stage1 = KnockoutStageEntity( + name = "Kieséses szakasz", + tournament = tournament1, + level = 1, + participantCount = participants1.size, + ) + stageRepository.save(stage1) + } + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 8e2dbe05..aeceb740 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -1595,7 +1595,7 @@ object StaffPermissions : PermissionGroup { val PERMISSION_SHOW_TOURNAMENTS = PermissionValidator( "TOURNAMENTS_SHOW", - "Verseny megtekintése", + "Versenyek megtekintése", readOnly = true, component = TournamentComponent::class ) @@ -1621,9 +1621,9 @@ object StaffPermissions : PermissionGroup { component = TournamentComponent::class ) - val PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS = PermissionValidator( + val PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS = PermissionValidator( "TOURNAMENT_PARTICIPANTS_SHOW", - "Verseny résztvevők megtekintése", + "Verseny résztvevők kezelése", readOnly = true, component = TournamentComponent::class ) @@ -1842,7 +1842,7 @@ object StaffPermissions : PermissionGroup { PERMISSION_CREATE_TOURNAMENTS, PERMISSION_DELETE_TOURNAMENTS, PERMISSION_EDIT_TOURNAMENTS, - PERMISSION_SHOW_TOURNAMENT_PARTICIPANTS, + PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS, PERMISSION_SET_SEEDS, PERMISSION_GENERATE_GROUPS, PERMISSION_GENERATE_BRACKETS, From 53a381620ba17b454add38c94541d1f3e22ae74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Fri, 14 Feb 2025 12:46:08 +0100 Subject: [PATCH 17/89] still figuring out the schema --- .../tournament/KnockoutStageEntity.kt | 2 +- .../tournament/KnockoutStageService.kt | 11 ++++------- .../tournament/TournamentMatchEntity.kt | 19 +++++++++++++++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 0022d364..d451764d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -69,7 +69,7 @@ data class KnockoutStageEntity( ): ManagedEntity { fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 - fun matches() = 2.0.pow(ceil(log2(participantCount.toDouble()))).toInt() - 1 + fun matches() = participantCount - 1 override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index c7ca57a2..53a77b6e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -5,6 +5,7 @@ import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import kotlin.math.pow @Service @ConditionalOnBean(TournamentComponent::class) @@ -14,14 +15,10 @@ class KnockoutStageService( @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { - for (i in 1..stage.matches()) { - val match = TournamentMatchEntity( - stage = stage, - gameId = i, + val secondRoundGames = 2.0.pow(stage.rounds().toDouble() - 2).toInt() + val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 + val byeWeekParticipants = stage.participantCount - firstRoundGames * 2 - ) - matchRepository.save(match) - } } companion object { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index bb975579..47244a50 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -53,8 +53,8 @@ data class TournamentMatchEntity( @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 2, label = "Home seed") var homeSeed: Int = 0, - @Column(nullable = false) - var homeTeamId: Int = 0, + @Column(nullable = true) + var homeTeamId: Int? = null, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @@ -67,8 +67,8 @@ data class TournamentMatchEntity( @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 3, label = "Away seed") var awaySeed: Int = 0, - @Column(nullable = false) - var awayTeamId: Int = 0, + @Column(nullable = true) + var awayTeamId: Int? = null, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @@ -128,4 +128,15 @@ data class TournamentMatchEntity( return javaClass.simpleName + "(id = $id)" } + override fun hashCode(): Int = javaClass.hashCode() + + fun winnerId(): Int? { + return when { + homeTeamScore == null || awayTeamScore == null -> null + homeTeamScore!! > awayTeamScore!! -> homeTeamId + homeTeamScore!! < awayTeamScore!! -> awayTeamId + else -> null + } + } + } From 2a7ae72b06f5e48295045a8080964077ca85aab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Wed, 19 Feb 2025 00:51:38 +0100 Subject: [PATCH 18/89] generate matches for knockoutstage --- .../tournament/KnockoutStageEntity.kt | 17 ++++- .../tournament/KnockoutStageRepository.kt | 2 + .../tournament/KnockoutStageService.kt | 67 ++++++++++++++++++- .../component/tournament/StageResultDto.kt | 17 +++++ .../tournament/TournamentMatchEntity.kt | 14 ++-- .../component/tournament/TournamentService.kt | 39 +++++++++++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index d451764d..a0cb398c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -13,7 +13,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import kotlin.math.ceil import kotlin.math.log2 -import kotlin.math.pow + + +enum class TournamentStatus { + CREATED, + DRAFT, + SET, + ONGOING, + FINISHED, + CANCELLED +} @Entity @@ -66,6 +75,12 @@ data class KnockoutStageEntity( @property:ImportFormat var nextRound: Int = 0, + @Column(nullable = false) + @field:JsonView(value = [ Preview::class, FullDetails::class ]) + @property:GenerateOverview(columnName = "Status", order = 5, centered = true) + @property:ImportFormat + var status: TournamentStatus = TournamentStatus.CREATED, + ): ManagedEntity { fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 989724da..8575b2da 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -38,4 +38,6 @@ interface KnockoutStageRepository : CrudRepository, """) fun findAllAggregated(): List + fun findAllByTournamentIdAndLevel(tournamentId: Int, level: Int): List + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 53a77b6e..091d23d7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -1,26 +1,89 @@ package hu.bme.sch.cmsch.component.tournament +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import kotlin.math.pow +import kotlin.random.Random @Service @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( - private val matchRepository: TournamentMatchRepository + private val tournamentService: TournamentService, + private val stageRepository: KnockoutStageRepository, + private val matchRepository: TournamentMatchRepository, + private val objectMapper: ObjectMapper ): ApplicationContextAware { @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { val secondRoundGames = 2.0.pow(stage.rounds().toDouble() - 2).toInt() val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 - val byeWeekParticipants = stage.participantCount - firstRoundGames * 2 + val byeWeekParticipantCount = stage.participantCount - firstRoundGames * 2 + val seedSpots = (1..2*secondRoundGames).asIterable().shuffled().subList(0, byeWeekParticipantCount) + // TODO do better seeding, this is just random stuff + val matches = mutableListOf() + + for (i in 0 until firstRoundGames) { + matches.add(TournamentMatchEntity( + gameId = i + 1, + stage = stage, + homeSeed = i + 1 + byeWeekParticipantCount, + awaySeed = i + 2 + byeWeekParticipantCount + )) + } + var j = 1; var k = 1 + for (i in 1 until secondRoundGames + 1) { + matches.add(TournamentMatchEntity( + gameId = firstRoundGames + j, + stage = stage, + homeSeed = if(seedSpots.contains(2*i-1)) j++ else -(k++), + awaySeed = if(seedSpots.contains(2*i)) j++ else -(k++) + )) + } + for (i in firstRoundGames + secondRoundGames until stage.matches()) { + matches.add(TournamentMatchEntity( + gameId = i + 1, + stage = stage, + homeSeed = -(k++), + awaySeed = -(k++) + )) + } + for (match in matches) { + matchRepository.save(match) + } + + val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() + val participants = tournamentService.getResultsFromLevel(stage.tournament!!.id, stage.level - 1).subList(0, stage.participantCount) + .map { StageResultDto(stage.id, stage.name, it.teamId, it.teamName) } + for (i in 0 until stage.participantCount) { + participants[i].seed = teamSeeds[i] + } + stage.participants = participants.joinToString("\n") { objectMapper.writeValueAsString(it) } + stageRepository.save(stage) + calculateTeamsFromSeeds(stage) + } + + @Transactional + fun calculateTeamsFromSeeds(stage: KnockoutStageEntity) { + val matches = matchRepository.findAllByStageId(stage.id) + val teams = stage.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + for (match in matches){ + val homeTeam = teams.find { it.seed == match.homeSeed } + val awayTeam = teams.find { it.seed == match.awaySeed } + match.homeTeamId = homeTeam?.teamId + match.homeTeamName = homeTeam?.teamName ?: "TBD" + match.awayTeamId = awayTeam?.teamId + match.awayTeamName = awayTeam?.teamName ?: "TBD" + matchRepository.save(match) + } } + companion object { private var applicationContext: ApplicationContext? = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt new file mode 100644 index 00000000..efb2270d --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt @@ -0,0 +1,17 @@ +package hu.bme.sch.cmsch.component.tournament + +data class StageResultDto( + var stageId: Int, + var stageName: String, + var teamId: Int, + var teamName: String, + var seed: Int = 0, + var position: Int = 0, + var points: Int = 0, + var won: Int = 0, + var drawn: Int = 0, + var lost: Int = 0, + var goalsFor: Int = 0, + var goalsAgainst: Int = 0, + var goalDifference: Int = 0 +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index 47244a50..1ea0da0d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -13,13 +13,9 @@ import org.springframework.core.env.Environment enum class MatchStatus { NOT_STARTED, - FIRST_HALF, HT, - SECOND_HALF, FT, - EXTRA_TIME, AET, - PENALTY_KICKS, AP, IN_PROGRESS, CANCELLED @@ -132,6 +128,7 @@ data class TournamentMatchEntity( fun winnerId(): Int? { return when { + status in listOf(MatchStatus.NOT_STARTED, MatchStatus.IN_PROGRESS, MatchStatus.CANCELLED) -> null homeTeamScore == null || awayTeamScore == null -> null homeTeamScore!! > awayTeamScore!! -> homeTeamId homeTeamScore!! < awayTeamScore!! -> awayTeamId @@ -139,4 +136,13 @@ data class TournamentMatchEntity( } } + fun isDraw(): Boolean { + return when { + status in listOf(MatchStatus.NOT_STARTED, MatchStatus.IN_PROGRESS, MatchStatus.CANCELLED) -> false + homeTeamScore == null || awayTeamScore == null -> false + homeTeamScore!! == awayTeamScore!! -> true + else -> false + } + } + } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index c7051b96..bf97f6d0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,6 +21,45 @@ open class TournamentService( return tournamentRepository.findAll() } + @Transactional(readOnly = true) + fun getParticipants(tournamentId: Int): List { + val tournament = tournamentRepository.findById(tournamentId) + if (tournament.isEmpty) { + return emptyList() + } + return tournament.get().participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } + } + + @Transactional(readOnly = true) + fun getResultsInStage(tournamentId: Int, stageId: Int): List { + val stage = stageRepository.findById(stageId) + if (stage.isEmpty || stage.get().tournament?.id != tournamentId) { + return emptyList() + } + return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + } + + + @Transactional(readOnly = true) + fun getResultsFromLevel(tournamentId: Int, level: Int): List { + if (level < 1) { + return getParticipants(tournamentId).map { StageResultDto(0, "Lobby", it.teamId, it.teamName) } + } + val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) + if (stages.isEmpty()) { + return emptyList() + } + return stages.flatMap { it.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } }.sortedWith( + compareBy( + { it.position }, + { it.points }, + { it.won }, + { it.goalDifference }, + { it.goalsFor } + ) + ) + } + @Transactional fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { From fd200f3b19cb0ba2e3a0d0b9232e4116f0b6d024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Wed, 19 Feb 2025 23:31:32 +0100 Subject: [PATCH 19/89] Api + frontend hook NOT TESTED! --- .../tournament/KnockoutStageEntity.kt | 4 +- .../tournament/KnockoutStageService.kt | 8 +++ .../tournament/TournamentApiController.kt | 49 +++++++++++++- .../tournament/TournamentDetailedView.kt | 37 +++++++++++ .../component/tournament/TournamentEntity.kt | 7 -- .../component/tournament/TournamentService.kt | 6 +- .../tournament/useTournamentListQuery.ts | 17 +++++ .../hooks/tournament/useTournamentQuery.ts | 19 ++++++ .../hooks/tournament/useTournamentsQuery.ts | 17 ----- .../tournament/components/Tournament.tsx | 17 +++++ .../src/pages/tournament/tournament.page.tsx | 24 +++++++ .../pages/tournament/tournamentList.page.tsx | 15 ++--- .../src/route-modules/Tournament.module.tsx | 3 +- frontend/src/util/views/tournament.view.ts | 64 ++++++++++++++++++- 14 files changed, 247 insertions(+), 40 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt create mode 100644 frontend/src/api/hooks/tournament/useTournamentListQuery.ts create mode 100644 frontend/src/api/hooks/tournament/useTournamentQuery.ts delete mode 100644 frontend/src/api/hooks/tournament/useTournamentsQuery.ts create mode 100644 frontend/src/pages/tournament/components/Tournament.tsx create mode 100644 frontend/src/pages/tournament/tournament.page.tsx diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index a0cb398c..0fee2c22 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -15,7 +15,7 @@ import kotlin.math.ceil import kotlin.math.log2 -enum class TournamentStatus { +enum class StageStatus { CREATED, DRAFT, SET, @@ -79,7 +79,7 @@ data class KnockoutStageEntity( @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateOverview(columnName = "Status", order = 5, centered = true) @property:ImportFormat - var status: TournamentStatus = TournamentStatus.CREATED, + var status: StageStatus = StageStatus.CREATED, ): ManagedEntity { diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 091d23d7..1eff299f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -94,4 +94,12 @@ class KnockoutStageService( override fun setApplicationContext(context: ApplicationContext) { applicationContext = context } + + fun findStagesByTournamentId(tournamentId: Int): List { + return stageRepository.findAllByTournamentId(tournamentId) + } + + fun findMatchesByStageId(stageId: Int): List { + return matchRepository.findAllByStageId(stageId) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 2b979230..d743d6e8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -16,7 +17,8 @@ import org.springframework.web.bind.annotation.RestController @ConditionalOnBean(TournamentComponent::class) class TournamentApiController( private val tournamentComponent: TournamentComponent, - private val tournamentService: TournamentService + private val tournamentService: TournamentService, + private val stageService: KnockoutStageService ) { @JsonView(Preview::class) @GetMapping("/tournament") @@ -31,4 +33,49 @@ class TournamentApiController( return ResponseEntity.ok(tournaments) } + + @GetMapping("/tournament/{tournamentId}") + @Operation( + summary = "Get details of a tournament.", + ) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Details of the tournament"), + ApiResponse(responseCode = "404", description = "Tournament not found") + ]) + fun tournamentDetails( + @PathVariable tournamentId: Int + ): ResponseEntity{ + val tournament = tournamentService.findById(tournamentId) + if (tournament.isEmpty) { + return ResponseEntity.notFound().build() + } + val stages = stageService.findStagesByTournamentId(tournamentId) + return ResponseEntity.ok(TournamentDetailedView( + TournamentWithParticipants( + tournament.get().id, + tournament.get().title, + tournament.get().description, + tournament.get().location, + tournament.get().participantCount, + tournamentService.getParticipants(tournamentId), + tournament.get().status + ), stages.map { KnockoutStageDetailedView( + it.id, + it.name, + it.level, + it.participantCount, + it.nextRound, + it.status, + stageService.findMatchesByStageId(it.id).map { MatchDto( + it.id, + it.gameId, + if(it.homeTeamId!=null) ParticipantDto(it.homeTeamId!!, it.homeTeamName) else null, + if(it.awayTeamId!=null) ParticipantDto(it.awayTeamId!!, it.awayTeamName) else null, + it.homeTeamScore, + it.awayTeamScore, + it.status + ) } + ) })) + } + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt new file mode 100644 index 00000000..31835657 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -0,0 +1,37 @@ +package hu.bme.sch.cmsch.component.tournament + +data class TournamentWithParticipants( + val id: Int, + val title: String, + val description: String, + val location: String, + val participantCount: Int, + val participants: List, + val status: Int, +) + +data class MatchDto( + val id: Int, + val gameId: Int, + val home: ParticipantDto?, + val away: ParticipantDto?, + val homeScore: Int?, + val awayScore: Int?, + val status: MatchStatus +) + +data class KnockoutStageDetailedView( + val id: Int, + val name: String, + val level: Int, + val participantCount: Int, + val nextRound: Int, + val status: StageStatus, + val matches: List, +) + + +data class TournamentDetailedView( + val tournament: TournamentWithParticipants, + val stages: List, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 9985f0be..bf8c43ac 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -34,13 +34,6 @@ data class TournamentEntity( @property:ImportFormat var title: String = "", - @Column(nullable = false) - @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(maxLength = 16, order = 2, label = "URL") - @property:GenerateOverview(columnName = "URL", order = 2) - @property:ImportFormat - var url: String = "", - @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(maxLength = 64, order = 3, label = "Verseny leírása") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index bf97f6d0..a1921cc2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -21,6 +21,11 @@ open class TournamentService( return tournamentRepository.findAll() } + @Transactional(readOnly = true) + fun findById(tournamentId: Int): Optional { + return tournamentRepository.findById(tournamentId) + } + @Transactional(readOnly = true) fun getParticipants(tournamentId: Int): List { val tournament = tournamentRepository.findById(tournamentId) @@ -84,5 +89,4 @@ open class TournamentService( return true } - } diff --git a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts new file mode 100644 index 00000000..83c132a9 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts @@ -0,0 +1,17 @@ +import { TournamentPreview } from '../../../util/views/tournament.view.ts' +import { useQuery } from 'react-query' +import { QueryKeys } from '../queryKeys.ts' +import axios from 'axios' +import { ApiPaths } from '../../../util/paths.ts' + + +export const useTournamentListQuery = (onError?: (err: any) => void) => { + return useQuery( + QueryKeys.TOURNAMENTS, + async () => { + const response = await axios.get(ApiPaths.TOURNAMENTS) + return response.data + }, + { onError: onError } + ) +} diff --git a/frontend/src/api/hooks/tournament/useTournamentQuery.ts b/frontend/src/api/hooks/tournament/useTournamentQuery.ts new file mode 100644 index 00000000..ec4bb1e1 --- /dev/null +++ b/frontend/src/api/hooks/tournament/useTournamentQuery.ts @@ -0,0 +1,19 @@ +import {useQuery} from "react-query"; +import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; +import {QueryKeys} from "../queryKeys.ts"; +import axios from "axios"; +import {joinPath} from "../../../util/core-functions.util.ts"; +import {ApiPaths} from "../../../util/paths.ts"; + + +export const useTournamentQuery = (id: number, onError?: (err: any) => void) => { + return useQuery( + [QueryKeys.TOURNAMENTS, id], + async () => { + const response = await axios.get(joinPath(ApiPaths.TOURNAMENTS, id)) + return response.data + }, + { onError: onError } + ) + +} diff --git a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts b/frontend/src/api/hooks/tournament/useTournamentsQuery.ts deleted file mode 100644 index 22d20e35..00000000 --- a/frontend/src/api/hooks/tournament/useTournamentsQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TournamentView } from '../../../util/views/tournament.view.ts' -import { useQuery } from 'react-query' -import { QueryKeys } from '../queryKeys.ts' -import axios from 'axios' -import { ApiPaths } from '../../../util/paths.ts' - - -export const useTournamentsQuery = (onError?: (err: any) => void) => { - return useQuery( - QueryKeys.TOURNAMENTS, - async () => { - const response = await axios.get(ApiPaths.TOURNAMENTS) - return response.data - }, - { onError: onError } - ) -} diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx new file mode 100644 index 00000000..459d534f --- /dev/null +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -0,0 +1,17 @@ +import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; + + +interface TournamentProps { + tournament: TournamentDetailsView +} + +const Tournament = ({tournament}: TournamentProps) => { + return ( +
+

{tournament.tournament.title}

+

{tournament.tournament.description}

+
+ ) +} + +export default Tournament diff --git a/frontend/src/pages/tournament/tournament.page.tsx b/frontend/src/pages/tournament/tournament.page.tsx new file mode 100644 index 00000000..a9f7c319 --- /dev/null +++ b/frontend/src/pages/tournament/tournament.page.tsx @@ -0,0 +1,24 @@ +import {useTournamentQuery} from "../../api/hooks/tournament/useTournamentQuery.ts"; +import {useParams} from "react-router-dom"; +import {toInteger} from "lodash"; +import {PageStatus} from "../../common-components/PageStatus.tsx"; +import {CmschPage} from "../../common-components/layout/CmschPage.tsx"; +import Tournament from "./components/Tournament.tsx"; +import {Helmet} from "react-helmet-async"; + + +const TournamentPage = () => { + const { id } = useParams() + const { isLoading, isError, data } = useTournamentQuery(toInteger(id) || 0) + + if (isError || isLoading || !data) return + + return ( + + + + + ) +} + +export default TournamentPage diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 1c6cf087..5745a930 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -1,8 +1,7 @@ -import { useTournamentsQuery } from '../../api/hooks/tournament/useTournamentsQuery.ts' +import { useTournamentListQuery } from '../../api/hooks/tournament/useTournamentListQuery.ts' import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' -import { Box, Heading, useBreakpoint, useBreakpointValue, useDisclosure, VStack } from '@chakra-ui/react' -import { createRef, useState } from 'react' -import { TournamentView } from '../../util/views/tournament.view.ts' +import { Box, Heading, VStack } from '@chakra-ui/react' +import { TournamentPreview } from '../../util/views/tournament.view.ts' import { ComponentUnavailable } from '../../common-components/ComponentUnavailable.tsx' import { PageStatus } from '../../common-components/PageStatus.tsx' import { CmschPage } from '../../common-components/layout/CmschPage.tsx' @@ -10,12 +9,8 @@ import { Helmet } from 'react-helmet-async' const TournamentListPage = () => { - const { isLoading, isError, data } = useTournamentsQuery() + const { isLoading, isError, data } = useTournamentListQuery() const component = useConfigContext()?.components?.tournament - const { isOpen, onToggle } = useDisclosure() - const tabsSize = useBreakpointValue({ base: 'sm', md: 'md' }) - const breakpoint = useBreakpoint() - const inputRef = createRef() if (!component) return @@ -33,7 +28,7 @@ const TournamentListPage = () => {
{(data ?? []).length > 0 ? ( - data.map((tournament: TournamentView) => ( + data.map((tournament: TournamentPreview) => ( {tournament.title} diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx index 4fbccc79..8af1a8f7 100644 --- a/frontend/src/route-modules/Tournament.module.tsx +++ b/frontend/src/route-modules/Tournament.module.tsx @@ -1,11 +1,12 @@ import { Route } from 'react-router-dom' import { Paths } from '../util/paths.ts' +import TournamentPage from '../pages/tournament/tournament.page.tsx' import TournamentListPage from '../pages/tournament/tournamentList.page.tsx' export function TournamentModule() { return ( - {/*} />*/} + } /> } /> ) diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 52c4e8d8..d6b77fa9 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -1,5 +1,67 @@ -export type TournamentView = { +export type TournamentPreview = { id: number title: string description: string + location: string + startDate: string + endDate: string + status: string } + +type TournamentWithParticipantsView = { + id: number + title: string + description: string + location: string + participants: ParticipantView[] +} + +type ParticipantView = { + id: number + name: string +} + +enum StageStatus { + CREATED= 'CREATED', + DRAFT = 'DRAFT', + SET = 'SET', + ONGOING = 'ONGOING', + FINISHED = 'FINISHED', + CANCELLED = 'CANCELLED', +} + +enum MatchStatus { + NOT_STARTED = 'NOT_STARTED', + HT = 'HT', + FT = 'FT', + AET = 'AET', + AP = 'AP', + IN_PROGRESS = 'IN_PROGRESS', + CANCELLED = 'CANCELLED', +} + +type MatchView = { + id: number + gameId: number + participant1: ParticipantView + participant2: ParticipantView + score1: number + score2: number + status: MatchStatus +} + +type TournamentStageView = { + id: number + name: string + level: number + participantCount: number + nextRound: number + status: StageStatus + matches: MatchView[] +} + +export type TournamentDetailsView = { + tournament: TournamentWithParticipantsView + stages: TournamentStageView[] +} + From 452bc731cb95174b2cbc3e306f353a6520f89490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sun, 23 Feb 2025 20:54:41 +0100 Subject: [PATCH 20/89] when did I even touch this file? --- frontend/src/api/contexts/service/ServiceContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/api/contexts/service/ServiceContext.tsx b/frontend/src/api/contexts/service/ServiceContext.tsx index 5595ee12..60da9f63 100644 --- a/frontend/src/api/contexts/service/ServiceContext.tsx +++ b/frontend/src/api/contexts/service/ServiceContext.tsx @@ -17,7 +17,7 @@ export interface MessageOptions { } export type ServiceContextType = { - sendMessage: (message: string | undefined, options?: MessageOptions) => void + sendMessage: (message: string, options?: MessageOptions) => void clearMessage: () => void message?: string type: MessageTypes From 1a46dc5d85817465eca5f33289099a0a820f1a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 24 Feb 2025 14:26:42 +0100 Subject: [PATCH 21/89] knockout stage frontend, cannot compile... --- .../tournament/TournamentApiController.kt | 4 + .../tournament/TournamentDetailedView.kt | 4 + .../tournament/TournamentMatchEntity.kt | 21 ++- .../components/BracketConnector.tsx | 30 +++++ .../tournament/components/KnockoutStage.tsx | 124 ++++++++++++++++++ .../src/pages/tournament/components/Match.tsx | 52 ++++++++ .../tournament/components/Tournament.tsx | 2 + frontend/src/util/views/tournament.view.ts | 25 ++-- 8 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 frontend/src/pages/tournament/components/BracketConnector.tsx create mode 100644 frontend/src/pages/tournament/components/KnockoutStage.tsx create mode 100644 frontend/src/pages/tournament/components/Match.tsx diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index d743d6e8..c1a98f32 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -69,6 +69,10 @@ class TournamentApiController( stageService.findMatchesByStageId(it.id).map { MatchDto( it.id, it.gameId, + it.kickoffTime, + it.location, + it.homeSeed, + it.awaySeed, if(it.homeTeamId!=null) ParticipantDto(it.homeTeamId!!, it.homeTeamName) else null, if(it.awayTeamId!=null) ParticipantDto(it.awayTeamId!!, it.awayTeamName) else null, it.homeTeamScore, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt index 31835657..d844a2f6 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -13,6 +13,10 @@ data class TournamentWithParticipants( data class MatchDto( val id: Int, val gameId: Int, + val kickoffTime: Long?, + val location: String, + val homeSeed: Int, + val awaySeed: Int, val home: ParticipantDto?, val away: ParticipantDto?, val homeScore: Int?, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index 1ea0da0d..b39b557b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -73,34 +73,41 @@ data class TournamentMatchEntity( @property:ImportFormat var awayTeamName: String = "", - @Column(nullable = false) + @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_DATE, order = 4, label = "Kickoff time") @property:GenerateOverview(columnName = "Kickoff time", order = 4) @property:ImportFormat var kickoffTime: Long = 0, + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 5, label = "Location") + @property:GenerateOverview(columnName = "Location", order = 5) + @property:ImportFormat + var location: String = "", + @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 5, label = "Home team score") - @property:GenerateOverview(columnName = "Home team score", order = 5) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 6, label = "Home team score") + @property:GenerateOverview(columnName = "Home team score", order = 6) @property:ImportFormat var homeTeamScore: Int? = null, @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 6, label = "Away team score") - @property:GenerateOverview(columnName = "Away team score", order = 6) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 7, label = "Away team score") + @property:GenerateOverview(columnName = "Away team score", order = 7) @property:ImportFormat var awayTeamScore: Int? = null, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 7, label = "Match status", + @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 8, label = "Match status", source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP", "IN_PROGRESS", "CANCELLED" ], visible = false, ignore = true ) - @property:GenerateOverview(columnName = "Match status", order = 7) + @property:GenerateOverview(columnName = "Match status", order = 8) @property:ImportFormat val status: MatchStatus = MatchStatus.NOT_STARTED, diff --git a/frontend/src/pages/tournament/components/BracketConnector.tsx b/frontend/src/pages/tournament/components/BracketConnector.tsx new file mode 100644 index 00000000..4d645dc9 --- /dev/null +++ b/frontend/src/pages/tournament/components/BracketConnector.tsx @@ -0,0 +1,30 @@ +import type React from "react" + +interface BracketConnectorProps { + startX: number + startY: number + endX: number + endY: number +} + +const BracketConnector: React.FC = ({ startX, startY, endX, endY }) => { + const midX = startX + (endX - startX) / 2 + + return ( + + + + ) +} + +export default BracketConnector + diff --git a/frontend/src/pages/tournament/components/KnockoutStage.tsx b/frontend/src/pages/tournament/components/KnockoutStage.tsx new file mode 100644 index 00000000..260fbc35 --- /dev/null +++ b/frontend/src/pages/tournament/components/KnockoutStage.tsx @@ -0,0 +1,124 @@ +"use client" + +import React, { useRef, useEffect, useState } from "react" +import { TournamentStageView, MatchView } from "../../../util/views/tournament.view.ts" +import Match from "./Match.tsx" +import BracketConnector from "./BracketConnector.tsx" + +interface TournamentBracketProps { + stage: TournamentStageView +} + +interface MatchPosition { + id: number + x: number + y: number + width: number + height: number +} + +const TournamentBracket: React.FC = ({ stage }) => { + const [matchPositions, setMatchPositions] = useState([]) + const bracketRef = useRef(null) + + const matchesByLevel = stage.matches.reduce( + (acc, match) => { + if (!acc[match.level]) { + acc[match.level] = [] + } + acc[match.level].push(match) + return acc + }, + {} as Record + ) + + const levels = Object.keys(matchesByLevel) + .map(Number) + .sort((a, b) => a - b) + const maxLevel = Math.max(...levels) + + useEffect(() => { + if (bracketRef.current) { + const positions: MatchPosition[] = [] + const matchElements = bracketRef.current.querySelectorAll('[data-match-id]') + + matchElements.forEach((el) => { + const rect = el.getBoundingClientRect() + const bracketRect = bracketRef.current!.getBoundingClientRect() + positions.push({ + id: Number.parseInt(el.getAttribute('data-match-id') || '0', 10), + x: rect.left - bracketRect.left + rect.width, + y: rect.top - bracketRect.top + rect.height / 2, + width: rect.width, + height: rect.height + }) + }) + + setMatchPositions(positions) + } + }, [stage.matches]) + + const getConnectorProps = (sourceId: number, targetId: number) => { + const sourceMatch = matchPositions.find((m) => m.id === sourceId) + const targetMatch = matchPositions.find((m) => m.id === targetId) + + if (!sourceMatch || !targetMatch) return null + + return { + startX: sourceMatch.x, + startY: sourceMatch.y, + endX: targetMatch.x - targetMatch.width, + endY: targetMatch.y + } + } + + const getMatchSpacing = (level: number) => { + const baseSpacing = 4 // rem + const spacingMultiplier = 2 ** (maxLevel - level) + return `${baseSpacing * spacingMultiplier}rem` + } + + return ( +
+

{stage.name}

+
+
+ {levels.map((level) => ( +
+

Round {level}

+
+ {matchesByLevel[level].map( + match => ( +
+ +
+ ) + )} +
+
+ ))} +
+ {matchPositions.length > 0 && + stage.matches.map((match: { seed1: number; seed2: number; id: number }) => { + if (match.seed1 < 0 || match.seed2 < 0) { + const sourceId1 = match.seed1 < 0 ? -match.seed1 : match.seed1 + const sourceId2 = match.seed2 < 0 ? -match.seed2 : match.seed2 + const connectorProps1 = getConnectorProps(sourceId1, match.id) + const connectorProps2 = getConnectorProps(sourceId2, match.id) + + return ( + + {connectorProps1 && } + {connectorProps2 && } + + ) + } + return null + })} +
+
+ ) +} + +export default TournamentBracket + diff --git a/frontend/src/pages/tournament/components/Match.tsx b/frontend/src/pages/tournament/components/Match.tsx new file mode 100644 index 00000000..f0cd7278 --- /dev/null +++ b/frontend/src/pages/tournament/components/Match.tsx @@ -0,0 +1,52 @@ +import type React from "react" +import { MatchView, MatchStatus } from "../../../util/views/tournament.view.ts" + +interface MatchProps { + match: MatchView +} + +const Match: React.FC = ({ match }) => { + const getScoreColor = (score1?: number, score2?: number) => { + if (match.status in [MatchStatus.CANCELLED, MatchStatus.NOT_STARTED] || score1 === undefined || score2 === undefined) return "text-gray-600" + return score1 > score2 ? "text-green-600 font-bold" : "text-red-600" + } + + const formatKickOffTime = (timestamp?: number) => { + if (!timestamp) return "TBD" + const date = new Date(timestamp) + return date.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + } + + const getParticipantName = (seed: number, participant?: { name: string }) => { + if (participant) return participant.name + if (seed < 0) return `Winner of Game ${-seed}` + return "TBD" + } + + return ( +
+
Game {match.gameId}
+
+ {getParticipantName(match.seed1, match.participant1)} + {match.score1 ?? "-"} +
+
+ {getParticipantName(match.seed2, match.participant2)} + {match.score2 ?? "-"} +
+
+ {match.status} + {formatKickOffTime(match.kickoffTime)} +
+
{match.location}
+
+ ) +} + +export default Match + diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 459d534f..caab35a0 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -10,6 +10,8 @@ const Tournament = ({tournament}: TournamentProps) => {

{tournament.tournament.title}

{tournament.tournament.description}

+

{tournament.tournament.location}

+
) } diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index d6b77fa9..d0404c5d 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -8,7 +8,7 @@ export type TournamentPreview = { status: string } -type TournamentWithParticipantsView = { +export type TournamentWithParticipantsView = { id: number title: string description: string @@ -16,12 +16,12 @@ type TournamentWithParticipantsView = { participants: ParticipantView[] } -type ParticipantView = { +export type ParticipantView = { id: number name: string } -enum StageStatus { +export enum StageStatus { CREATED= 'CREATED', DRAFT = 'DRAFT', SET = 'SET', @@ -30,7 +30,7 @@ enum StageStatus { CANCELLED = 'CANCELLED', } -enum MatchStatus { +export enum MatchStatus { NOT_STARTED = 'NOT_STARTED', HT = 'HT', FT = 'FT', @@ -40,17 +40,22 @@ enum MatchStatus { CANCELLED = 'CANCELLED', } -type MatchView = { +export type MatchView = { id: number gameId: number - participant1: ParticipantView - participant2: ParticipantView - score1: number - score2: number + kickoffTime?: number + level: number + location: string + seed1: number + seed2: number + participant1?: ParticipantView + participant2?: ParticipantView + score1?: number + score2?: number status: MatchStatus } -type TournamentStageView = { +export type TournamentStageView = { id: number name: string level: number From 2de413503356de40f087d1ebee1f2a16bea593ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Tue, 25 Feb 2025 14:52:09 +0100 Subject: [PATCH 22/89] api working, frontend for Detailed Tournament view (not tested yet) --- .../tournament/KnockoutStageController.kt | 16 ++++++++-------- .../component/tournament/KnockoutStageEntity.kt | 17 +++++++++++------ .../tournament/KnockoutStageRepository.kt | 13 ++++--------- .../tournament/KnockoutStageService.kt | 6 ++++-- .../tournament/TournamentApiController.kt | 12 ++++++++++-- .../component/tournament/TournamentComponent.kt | 2 +- .../tournament/TournamentMatchController.kt | 16 +++++++++------- .../tournament/TournamentMatchRepository.kt | 10 +++------- .../tournament/TournamentPreviewView.kt | 9 +++++++++ .../component/tournament/TournamentService.kt | 4 ++-- .../hu/bme/sch/cmsch/config/TestConfig.kt | 2 +- frontend/src/App.tsx | 6 ++++++ .../hooks/tournament/useTournamentListQuery.ts | 15 +++++++-------- .../api/hooks/tournament/useTournamentQuery.ts | 16 +++++++--------- .../src/route-modules/Tournament.module.tsx | 13 ------------- frontend/src/util/views/tournament.view.ts | 4 +--- 16 files changed, 83 insertions(+), 78 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentPreviewView.kt delete mode 100644 frontend/src/route-modules/Tournament.module.tsx diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt index 9990c7f1..7b757ffb 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -37,17 +37,17 @@ class KnockoutStageController( transactionManager, object : ManualRepository() { override fun findAll(): Iterable { - val stages = stageRepository.findAllAggregated() - val tournaments = tournamentRepository.findAll().associateBy { it.id } - return stages.map { + val stages = stageRepository.findAllAggregated().associateBy { it.tournamentId } + val tournaments = tournamentRepository.findAll() + return tournaments.map { KnockoutGroupDto( - it.tournamentId, - it.tournamentName, - it.tournamentLocation, + it.id, + it.title, + it.location, it.participantCount, - it.stageCount.toInt() + stages[it.id]?.stageCount?.toInt() ?: 0 ) - }.sortedByDescending { it.stageCount }.toList() + }.sortedByDescending { it.stageCount } } }, stageRepository, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 0fee2c22..956fe140 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -45,8 +45,11 @@ data class KnockoutStageEntity( @property:ImportFormat var name: String = "", - @ManyToOne(targetEntity = TournamentEntity::class) - var tournament: TournamentEntity? = null, + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") + @property:GenerateOverview(columnName = "Verseny ID", order = 2, centered = true) + @property:ImportFormat + var tournamentId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @@ -83,8 +86,10 @@ data class KnockoutStageEntity( ): ManagedEntity { - fun rounds() = ceil(log2(participantCount.toDouble())).toInt() + 1 + fun rounds() = ceil(log2(participantCount.toDouble())).toInt() fun matches() = participantCount - 1 + fun getStageService() = KnockoutStageService.getBean() + fun tournament(): TournamentEntity = getStageService().getTournamentService().findById(tournamentId).orElse(null) override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", @@ -104,14 +109,14 @@ data class KnockoutStageEntity( override fun hashCode(): Int = javaClass.hashCode() override fun toString(): String { - return this::class.simpleName + "(id = $id, name = $name, tournamentId = $tournament.id, participantCount = $participantCount)" + return this::class.simpleName + "(id = $id, name = $name, tournamentId = $tournamentId, participantCount = $participantCount)" } @PrePersist fun prePersist() { - val stageService = KnockoutStageService.getBean() - stageService.createMatchesForStage(this) + tournament()!! + getStageService().createMatchesForStage(this) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 8575b2da..035e9525 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -9,9 +9,6 @@ import java.util.* data class StageCountDto( var tournamentId: Int = 0, - var tournamentName: String = "", - var tournamentLocation : String = "", - var participantCount: Int = 0, var stageCount: Long = 0 ) @@ -22,22 +19,20 @@ interface KnockoutStageRepository : CrudRepository, override fun findAll(): List override fun findById(id: Int): Optional - @Query("select k from KnockoutStageEntity k where k.tournament.id = ?1") fun findAllByTournamentId(tournamentId: Int): List @Query(""" SELECT NEW hu.bme.sch.cmsch.component.tournament.StageCountDto( - s.tournament.id, - s.tournament.title, - s.tournament.location, - s.tournament.participantCount, + s.tournamentId, COUNT(s.id) ) FROM KnockoutStageEntity s - GROUP BY s.tournament + GROUP BY s.tournamentId """) fun findAllAggregated(): List + + @Query("select k from KnockoutStageEntity k where k.tournamentId = ?1 and k.level = ?2") fun findAllByTournamentIdAndLevel(tournamentId: Int, level: Int): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 1eff299f..1938c9dc 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -24,7 +24,7 @@ class KnockoutStageService( val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 val byeWeekParticipantCount = stage.participantCount - firstRoundGames * 2 - val seedSpots = (1..2*secondRoundGames).asIterable().shuffled().subList(0, byeWeekParticipantCount) + val seedSpots = (1..2*secondRoundGames).asIterable().shuffled().subList(0, byeWeekParticipantCount) //TODO bye week participant count wrong // TODO do better seeding, this is just random stuff val matches = mutableListOf() @@ -58,7 +58,7 @@ class KnockoutStageService( } val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() - val participants = tournamentService.getResultsFromLevel(stage.tournament!!.id, stage.level - 1).subList(0, stage.participantCount) + val participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1).subList(0, stage.participantCount) .map { StageResultDto(stage.id, stage.name, it.teamId, it.teamName) } for (i in 0 until stage.participantCount) { participants[i].seed = teamSeeds[i] @@ -83,6 +83,8 @@ class KnockoutStageService( } } + fun getTournamentService(): TournamentService = tournamentService + companion object { private var applicationContext: ApplicationContext? = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index c1a98f32..5eb9024e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -28,9 +28,17 @@ class TournamentApiController( @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "List of tournaments") ]) - fun tournaments(): ResponseEntity> { + fun tournaments(): ResponseEntity> { val tournaments = tournamentService.findAll() - return ResponseEntity.ok(tournaments) + return ResponseEntity.ok(tournaments.map { + TournamentPreviewView( + it.id, + it.title, + it.description, + it.location, + it.status + ) + }) } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index d42b6f00..b2ee254e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -1,7 +1,7 @@ package hu.bme.sch.cmsch.component.tournament import hu.bme.sch.cmsch.component.* -import hu.bme.sch.cmsch.component.app.ComponentSettingService +import hu.bme.sch.cmsch.setting.* import hu.bme.sch.cmsch.model.RoleType import hu.bme.sch.cmsch.service.ControlPermissions import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt index 84313521..498b8513 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping @ConditionalOnBean(TournamentComponent::class) class TournamentMatchController( private val matchRepository: TournamentMatchRepository, + private val tournamentRepository: TournamentRepository, importService: ImportService, adminMenuService: AdminMenuService, component: TournamentComponent, @@ -34,15 +35,16 @@ class TournamentMatchController( transactionManager, object : ManualRepository() { override fun findAll(): Iterable { - val matches = matchRepository.findAllAggregated() - return matches.map { + val matches = matchRepository.findAllAggregated().associateBy { it.tournamentId } + val tournaments = tournamentRepository.findAll() + return tournaments.map { MatchGroupDto( - it.tournamentId, - it.tournamentName, - it.tournamentLocation, - it.matchCount.toInt() + it.id, + it.title, + it.location, + matches[it.id]?.matchCount?.toInt() ?: 0 ) - }.sortedByDescending { it.matchCount }.toList() + }.sortedByDescending { it.matchCount } } }, matchRepository, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt index 977777dd..c108756b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -9,8 +9,6 @@ import java.util.* data class MatchCountDto( var tournamentId: Int = 0, - var tournamentName: String = "", - var tournamentLocation : String = "", var matchCount: Long = 0 ) @@ -22,19 +20,17 @@ interface TournamentMatchRepository : CrudRepository override fun findAll(): List override fun findById(id: Int): Optional fun findAllByStageId(stageId: Int): List - @Query("select t from TournamentMatchEntity t where t.stage.tournament.id = ?1") + @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") fun findAllByStageTournamentId(tournamentId: Int): List @Query(""" SELECT NEW hu.bme.sch.cmsch.component.tournament.MatchCountDto( - s.tournament.id, - s.tournament.title, - s.tournament.location, + s.tournamentId, COUNT(t.id) ) FROM TournamentMatchEntity t JOIN t.stage s - GROUP BY s.tournament + GROUP BY s.tournamentId """) fun findAllAggregated(): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentPreviewView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentPreviewView.kt new file mode 100644 index 00000000..5b91bfb7 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentPreviewView.kt @@ -0,0 +1,9 @@ +package hu.bme.sch.cmsch.component.tournament + +data class TournamentPreviewView( + val id: Int, + val title: String, + val description: String, + val location: String, + val status: Int, +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index a1921cc2..90d392a0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -29,7 +29,7 @@ open class TournamentService( @Transactional(readOnly = true) fun getParticipants(tournamentId: Int): List { val tournament = tournamentRepository.findById(tournamentId) - if (tournament.isEmpty) { + if (tournament.isEmpty || tournament.get().participants.isEmpty()) { return emptyList() } return tournament.get().participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } @@ -38,7 +38,7 @@ open class TournamentService( @Transactional(readOnly = true) fun getResultsInStage(tournamentId: Int, stageId: Int): List { val stage = stageRepository.findById(stageId) - if (stage.isEmpty || stage.get().tournament?.id != tournamentId) { + if (stage.isEmpty || stage.get().tournamentId != tournamentId) { return emptyList() } return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt index e3804f95..10a87906 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt @@ -1149,7 +1149,7 @@ open class TestConfig( repository.save(tournament1) val stage1 = KnockoutStageEntity( name = "Kieséses szakasz", - tournament = tournament1, + tournamentId = tournament1.id, level = 1, participantCount = participants1.size, ) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bad625fa..ae0d4fed 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -45,6 +45,8 @@ import RaceByTeamPage from './pages/race/raceByTeam.page.tsx' import CreateTeamPage from './pages/teams/createTeam.page.tsx' import EditMyTeamPage from './pages/teams/editMyTeam.page.tsx' import MyTeamPage from './pages/teams/myTeam.page.tsx' +import TournamentPage from "./pages/tournament/tournament.page.tsx"; +import TournamentListPage from "./pages/tournament/tournamentList.page.tsx"; export function App() { return ( @@ -127,6 +129,10 @@ export function App() { } /> } /> + + } /> + } /> + } /> } /> } /> diff --git a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts index 83c132a9..861bd23f 100644 --- a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts +++ b/frontend/src/api/hooks/tournament/useTournamentListQuery.ts @@ -1,17 +1,16 @@ import { TournamentPreview } from '../../../util/views/tournament.view.ts' -import { useQuery } from 'react-query' +import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../queryKeys.ts' import axios from 'axios' import { ApiPaths } from '../../../util/paths.ts' -export const useTournamentListQuery = (onError?: (err: any) => void) => { - return useQuery( - QueryKeys.TOURNAMENTS, - async () => { +export const useTournamentListQuery = () => { + return useQuery({ + queryKey: [QueryKeys.TOURNAMENTS], + queryFn: async () => { const response = await axios.get(ApiPaths.TOURNAMENTS) return response.data - }, - { onError: onError } - ) + } + }) } diff --git a/frontend/src/api/hooks/tournament/useTournamentQuery.ts b/frontend/src/api/hooks/tournament/useTournamentQuery.ts index ec4bb1e1..53ac297c 100644 --- a/frontend/src/api/hooks/tournament/useTournamentQuery.ts +++ b/frontend/src/api/hooks/tournament/useTournamentQuery.ts @@ -1,4 +1,4 @@ -import {useQuery} from "react-query"; +import {useQuery} from "@tanstack/react-query"; import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; import {QueryKeys} from "../queryKeys.ts"; import axios from "axios"; @@ -6,14 +6,12 @@ import {joinPath} from "../../../util/core-functions.util.ts"; import {ApiPaths} from "../../../util/paths.ts"; -export const useTournamentQuery = (id: number, onError?: (err: any) => void) => { - return useQuery( - [QueryKeys.TOURNAMENTS, id], - async () => { +export const useTournamentQuery = (id: number) => { + return useQuery({ + queryKey: [QueryKeys.TOURNAMENTS, id], + queryFn: async () => { const response = await axios.get(joinPath(ApiPaths.TOURNAMENTS, id)) return response.data - }, - { onError: onError } - ) - + } + }) } diff --git a/frontend/src/route-modules/Tournament.module.tsx b/frontend/src/route-modules/Tournament.module.tsx deleted file mode 100644 index 8af1a8f7..00000000 --- a/frontend/src/route-modules/Tournament.module.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Route } from 'react-router-dom' -import { Paths } from '../util/paths.ts' -import TournamentPage from '../pages/tournament/tournament.page.tsx' -import TournamentListPage from '../pages/tournament/tournamentList.page.tsx' - -export function TournamentModule() { - return ( - - } /> - } /> - - ) -} diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index d0404c5d..b81e7802 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -3,9 +3,7 @@ export type TournamentPreview = { title: string description: string location: string - startDate: string - endDate: string - status: string + status: number } export type TournamentWithParticipantsView = { From a152ea8cd75b7a75e674c6342a4aa82ab60f8f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Thu, 27 Feb 2025 19:44:39 +0100 Subject: [PATCH 23/89] Tournament Detailed View getting some clothes --- .../tournament/components/Tournament.tsx | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 147c63e4..dc0830f0 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -1,4 +1,7 @@ import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; +import {Tab, TabList, TabPanel, TabPanels, Tabs} from "@chakra-ui/react"; +import KnockoutStage from "./KnockoutStage.tsx"; +import {useState} from "react"; interface TournamentProps { @@ -6,12 +9,49 @@ interface TournamentProps { } const Tournament = ({tournament}: TournamentProps) => { + + + const [tabIndex, setTabIndex] = useState(0) + + const onTabSelected = (i: number) => { + setTabIndex(i) + } + return (

{tournament.tournament.title}

{tournament.tournament.description}

{tournament.tournament.location}

- + + + Résztvevők + { + tournament.stages.map((stage) => ( + {stage.name} + )) + } + + + +
+ { + tournament.tournament.participants.map((participant) => ( +
+

{participant.name}

+
+ )) + } +
+
+ + { + tournament.stages.map((stage) => ( + + )) + } + +
+
) } From 6d883fde965ccd2b22554c1e196b186cdf919123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 3 Mar 2025 15:11:32 +0100 Subject: [PATCH 24/89] removed some stuff so the backend runs --- .../tournament/KnockoutStageEntity.kt | 7 +-- .../tournament/KnockoutStageService.kt | 41 +++++++++++++-- .../tournament/TournamentApiController.kt | 1 + .../tournament/TournamentDetailedView.kt | 1 + .../component/tournament/TournamentEntity.kt | 4 +- .../tournament/TournamentMatchController.kt | 16 ++---- .../tournament/TournamentMatchEntity.kt | 26 ++++++---- .../tournament/TournamentMatchRepository.kt | 10 ++-- .../hu/bme/sch/cmsch/config/TestConfig.kt | 50 ++++++++++++++++++- 9 files changed, 117 insertions(+), 39 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 956fe140..814f7a91 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -113,10 +113,11 @@ data class KnockoutStageEntity( } - @PrePersist - fun prePersist() { + /*@PostPersist + fun postPersist() { tournament()!! getStageService().createMatchesForStage(this) - } + //getStageService().calculateTeamsFromSeeds(this) + }*/ } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 1938c9dc..38ed8825 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -24,14 +24,15 @@ class KnockoutStageService( val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 val byeWeekParticipantCount = stage.participantCount - firstRoundGames * 2 - val seedSpots = (1..2*secondRoundGames).asIterable().shuffled().subList(0, byeWeekParticipantCount) //TODO bye week participant count wrong + val seedSpots = (1..2*secondRoundGames).asIterable().shuffled() + .subList(0, byeWeekParticipantCount) //TODO bye week participant count wrong // TODO do better seeding, this is just random stuff val matches = mutableListOf() for (i in 0 until firstRoundGames) { matches.add(TournamentMatchEntity( gameId = i + 1, - stage = stage, + stageId = stage.id, homeSeed = i + 1 + byeWeekParticipantCount, awaySeed = i + 2 + byeWeekParticipantCount )) @@ -40,7 +41,7 @@ class KnockoutStageService( for (i in 1 until secondRoundGames + 1) { matches.add(TournamentMatchEntity( gameId = firstRoundGames + j, - stage = stage, + stageId = stage.id, homeSeed = if(seedSpots.contains(2*i-1)) j++ else -(k++), awaySeed = if(seedSpots.contains(2*i)) j++ else -(k++) )) @@ -48,7 +49,7 @@ class KnockoutStageService( for (i in firstRoundGames + secondRoundGames until stage.matches()) { matches.add(TournamentMatchEntity( gameId = i + 1, - stage = stage, + stageId = stage.id, homeSeed = -(k++), awaySeed = -(k++) )) @@ -65,7 +66,6 @@ class KnockoutStageService( } stage.participants = participants.joinToString("\n") { objectMapper.writeValueAsString(it) } stageRepository.save(stage) - calculateTeamsFromSeeds(stage) } @Transactional @@ -85,6 +85,37 @@ class KnockoutStageService( fun getTournamentService(): TournamentService = tournamentService + fun findById(id: Int): KnockoutStageEntity { + return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } + } + + + fun getMatchesByStageTournamentId(tournamentId: Int): List { + val stages = stageRepository.findAllByTournamentId(tournamentId) + val matches = mutableListOf() + for (stage in stages) { + matches.addAll(matchRepository.findAllByStageId(stage.id)) + } + return matches + } + + fun getAggregatedMatchesByTournamentId(): List { + val tournaments = tournamentService.findAll().associateBy { it.id } + val stages = stageRepository.findAll().associateBy { it.id } + val aggregatedByStageId = matchRepository.findAllAggregated() + val aggregated = mutableMapOf() + for(aggregatedStage in aggregatedByStageId) { + aggregated[stages[aggregatedStage.stageId]!!.tournamentId] = aggregated.getOrDefault(stages[aggregatedStage.stageId]!!.tournamentId, 0) + aggregatedStage.matchCount + } + return aggregated.map { + MatchGroupDto( + it.key, + tournaments[it.key]?.title ?:"", + tournaments[it.key]?.location ?:"", + it.value.toInt() + ) + }.sortedByDescending { it.matchCount } + } companion object { private var applicationContext: ApplicationContext? = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 5eb9024e..cc6cf024 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -78,6 +78,7 @@ class TournamentApiController( it.id, it.gameId, it.kickoffTime, + it.level, it.location, it.homeSeed, it.awaySeed, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt index d844a2f6..d551000e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -14,6 +14,7 @@ data class MatchDto( val id: Int, val gameId: Int, val kickoffTime: Long?, + val level: Int, val location: String, val homeSeed: Int, val awaySeed: Int, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index bf8c43ac..9bdc9d81 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -24,7 +24,7 @@ data class TournamentEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + @property:GenerateOverview(renderer = OVERVIEW_TYPE_NUMBER, columnName = "ID", order = -1) override var id: Int = 0, @Column(nullable = false) @@ -58,7 +58,7 @@ data class TournamentEntity( @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(visible = true) + @property:GenerateOverview(visible = false) @property:ImportFormat var participants: String = "", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt index 498b8513..4a95cb8e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping class TournamentMatchController( private val matchRepository: TournamentMatchRepository, private val tournamentRepository: TournamentRepository, + private val stageService: KnockoutStageService, importService: ImportService, adminMenuService: AdminMenuService, component: TournamentComponent, @@ -35,16 +36,7 @@ class TournamentMatchController( transactionManager, object : ManualRepository() { override fun findAll(): Iterable { - val matches = matchRepository.findAllAggregated().associateBy { it.tournamentId } - val tournaments = tournamentRepository.findAll() - return tournaments.map { - MatchGroupDto( - it.id, - it.title, - it.location, - matches[it.id]?.matchCount?.toInt() ?: 0 - ) - }.sortedByDescending { it.matchCount } + return stageService.getAggregatedMatchesByTournamentId() } }, matchRepository, @@ -60,7 +52,7 @@ class TournamentMatchController( editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, - createEnabled = false, + createEnabled = true, editEnabled = true, deleteEnabled = false, importEnabled = false, @@ -69,6 +61,6 @@ class TournamentMatchController( adminMenuIcon = "compare_arrows", ) { override fun fetchSublist(id: Int): Iterable { - return matchRepository.findAllByStageTournamentId(id) + return stageService.getMatchesByStageTournamentId(id) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index b39b557b..b27448de 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -41,12 +41,20 @@ data class TournamentMatchEntity( @property:ImportFormat var gameId: Int = 0, - @ManyToOne(targetEntity = KnockoutStageEntity::class) - @JoinColumn(name = "stageId", insertable = false, updatable = false) - var stage: KnockoutStageEntity? = null, + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") + @property:GenerateOverview(columnName = "Stage ID", order = 1) + @property:ImportFormat + var stageId: Int = 0, + + @Column(nullable = false) + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Level") + @property:GenerateOverview(columnName = "Level", order = 1) + @property:ImportFormat + var level: Int = 0, @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 2, label = "Home seed") + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = Int.MIN_VALUE, order = 2, label = "Home seed") var homeSeed: Int = 0, @Column(nullable = true) @@ -55,12 +63,12 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 2, label = "Home team name") - //@property:GenerateOverview(columnName = "Home team name", order = 2) + @property:GenerateOverview(columnName = "Home team name", order = 2) @property:ImportFormat var homeTeamName: String = "", @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 3, label = "Away seed") + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = Int.MIN_VALUE, order = 3, label = "Away seed") var awaySeed: Int = 0, @Column(nullable = true) @@ -69,7 +77,7 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_TEXT, min = 1, order = 3, label = "Away team name") - //@property:GenerateOverview(columnName = "Away team name", order = 3) + @property:GenerateOverview(columnName = "Away team name", order = 3) @property:ImportFormat var awayTeamName: String = "", @@ -89,14 +97,12 @@ data class TournamentMatchEntity( @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 6, label = "Home team score") @property:GenerateOverview(columnName = "Home team score", order = 6) @property:ImportFormat var homeTeamScore: Int? = null, @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 0, order = 7, label = "Away team score") @property:GenerateOverview(columnName = "Away team score", order = 7) @property:ImportFormat var awayTeamScore: Int? = null, @@ -113,6 +119,8 @@ data class TournamentMatchEntity( ): ManagedEntity{ + fun stage(): KnockoutStageEntity = KnockoutStageService.getBean().findById(stageId) + override fun getEntityConfig(env: Environment) = EntityConfig( name = "TournamentMatch", view = "control/tournament/match", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt index c108756b..c4c88ac3 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository import java.util.* data class MatchCountDto( - var tournamentId: Int = 0, + var stageId: Int = 0, var matchCount: Long = 0 ) @@ -19,18 +19,16 @@ interface TournamentMatchRepository : CrudRepository override fun findAll(): List override fun findById(id: Int): Optional + @Query("select t from TournamentMatchEntity t where t.stageId = ?1") fun findAllByStageId(stageId: Int): List - @Query("select t from TournamentMatchEntity t where t.stage.tournamentId = ?1") - fun findAllByStageTournamentId(tournamentId: Int): List @Query(""" SELECT NEW hu.bme.sch.cmsch.component.tournament.MatchCountDto( - s.tournamentId, + t.stageId, COUNT(t.id) ) FROM TournamentMatchEntity t - JOIN t.stage s - GROUP BY s.tournamentId + GROUP BY t.stageId """) fun findAllAggregated(): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt index 10a87906..394ddfe8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/TestConfig.kt @@ -135,6 +135,12 @@ open class TestConfig( addForms(form, response) } } + + tournamentRepository.ifPresent { tournament -> + stageRepository.ifPresent { stage -> + addTournaments(tournament, stage) + } + } } @Scheduled(fixedDelay = 3000L) @@ -463,12 +469,49 @@ open class TestConfig( races = false, selectable = false, leaveable = false + )), + + groupRepository.save(GroupEntity( + name = "Chillámák", + major = MajorType.UNKNOWN, + staff1 = "", + staff2 = "", + staff3 = "", + staff4 = "", + races = true, + selectable = true, + leaveable = false + )), + + groupRepository.save(GroupEntity( + name = "Bóbisch", + major = MajorType.UNKNOWN, + staff1 = "", + staff2 = "", + staff3 = "", + staff4 = "", + races = true, + selectable = true, + leaveable = false + )), + + groupRepository.save(GroupEntity( + name = "Schugár", + major = MajorType.UNKNOWN, + staff1 = "", + staff2 = "", + staff3 = "", + staff4 = "", + races = true, + selectable = true, + leaveable = false ))) + return groups } private fun addNews(news: NewsRepository) { - news.save(NewsEntity(title = "Az eslő hír", + news.save(NewsEntity(title = "Az első hír", content = LOREM_IPSUM_SHORT_1, visible = true, highlighted = false )) @@ -1132,13 +1175,16 @@ open class TestConfig( extraMenuRepository.save(ExtraMenuEntity(0, "Facebook", "https://facebook.com/xddddddddddd", true)) } - private fun addTournaments(repository: TournamentRepository, stageRepository: KnockoutStageRepository, matchRepository: TournamentMatchRepository){ + private fun addTournaments(repository: TournamentRepository, stageRepository: KnockoutStageRepository){ val participants1 = mutableListOf() participants1.add(ParticipantDto(groupRepository.findByName("V10").orElseThrow().id, "V10")) participants1.add(ParticipantDto(groupRepository.findByName("I16").orElseThrow().id, "I16")) participants1.add(ParticipantDto(groupRepository.findByName("I09").orElseThrow().id, "I09")) participants1.add(ParticipantDto(groupRepository.findByName("Vendég").orElseThrow().id, "Vendég")) participants1.add(ParticipantDto(groupRepository.findByName("Kiállító").orElseThrow().id, "Kiállító")) + participants1.add(ParticipantDto(groupRepository.findByName("Chillámák").orElseThrow().id, "Chillámák")) + participants1.add(ParticipantDto(groupRepository.findByName("Bóbisch").orElseThrow().id, "Bóbisch")) + participants1.add(ParticipantDto(groupRepository.findByName("Schugár").orElseThrow().id, "Schugár")) val tournament1 = TournamentEntity( title = "Foci verseny", description = "A legjobb foci csapat nyer", From 99d5e9b0bf45cba70cb086d87a326808d1a110ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 3 Mar 2025 17:50:47 +0100 Subject: [PATCH 25/89] frontend looks better --- .../tournament/KnockoutStageService.kt | 7 +- .../component/tournament/StageResultDto.kt | 2 - .../tournament/TournamentMatchEntity.kt | 8 ++ .../component/tournament/TournamentService.kt | 2 +- .../components/BracketConnector.tsx | 30 ----- .../tournament/components/KnockoutStage.tsx | 126 ++---------------- .../src/pages/tournament/components/Match.tsx | 71 ++++++---- .../tournament/components/Tournament.tsx | 24 ++-- .../pages/tournament/tournamentList.page.tsx | 8 +- frontend/src/util/paths.ts | 3 +- frontend/src/util/views/tournament.view.ts | 21 ++- 11 files changed, 104 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/pages/tournament/components/BracketConnector.tsx diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 38ed8825..fad48f65 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -60,7 +60,7 @@ class KnockoutStageService( val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() val participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1).subList(0, stage.participantCount) - .map { StageResultDto(stage.id, stage.name, it.teamId, it.teamName) } + .map { StageResultDto(it.teamId, it.teamName) } for (i in 0 until stage.participantCount) { participants[i].seed = teamSeeds[i] } @@ -89,6 +89,11 @@ class KnockoutStageService( return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } } + fun getParticipants(stageId: Int): List { + val stage = findById(stageId) + return stage.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + } + fun getMatchesByStageTournamentId(tournamentId: Int): List { val stages = stageRepository.findAllByTournamentId(tournamentId) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt index efb2270d..8a3cffbd 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt @@ -1,8 +1,6 @@ package hu.bme.sch.cmsch.component.tournament data class StageResultDto( - var stageId: Int, - var stageName: String, var teamId: Int, var teamName: String, var seed: Int = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index b27448de..6d76355e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -121,6 +121,14 @@ data class TournamentMatchEntity( fun stage(): KnockoutStageEntity = KnockoutStageService.getBean().findById(stageId) + @PrePersist + @PreUpdate + fun setTeams() { + val teams = KnockoutStageService.getBean().getParticipants(stageId) + homeTeamId = teams.find { it.teamName == homeTeamName }?.teamId ?: null + awayTeamId = teams.find { it.teamName == awayTeamName }?.teamId ?: null + } + override fun getEntityConfig(env: Environment) = EntityConfig( name = "TournamentMatch", view = "control/tournament/match", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 90d392a0..a539e824 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -48,7 +48,7 @@ open class TournamentService( @Transactional(readOnly = true) fun getResultsFromLevel(tournamentId: Int, level: Int): List { if (level < 1) { - return getParticipants(tournamentId).map { StageResultDto(0, "Lobby", it.teamId, it.teamName) } + return getParticipants(tournamentId).map { StageResultDto(it.teamId, it.teamName) } } val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) if (stages.isEmpty()) { diff --git a/frontend/src/pages/tournament/components/BracketConnector.tsx b/frontend/src/pages/tournament/components/BracketConnector.tsx deleted file mode 100644 index 4d645dc9..00000000 --- a/frontend/src/pages/tournament/components/BracketConnector.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type React from "react" - -interface BracketConnectorProps { - startX: number - startY: number - endX: number - endY: number -} - -const BracketConnector: React.FC = ({ startX, startY, endX, endY }) => { - const midX = startX + (endX - startX) / 2 - - return ( - - - - ) -} - -export default BracketConnector - diff --git a/frontend/src/pages/tournament/components/KnockoutStage.tsx b/frontend/src/pages/tournament/components/KnockoutStage.tsx index 260fbc35..2a48f37b 100644 --- a/frontend/src/pages/tournament/components/KnockoutStage.tsx +++ b/frontend/src/pages/tournament/components/KnockoutStage.tsx @@ -1,122 +1,24 @@ -"use client" - -import React, { useRef, useEffect, useState } from "react" +import React from "react" import { TournamentStageView, MatchView } from "../../../util/views/tournament.view.ts" -import Match from "./Match.tsx" -import BracketConnector from "./BracketConnector.tsx" +import {Heading, Stack} from "@chakra-ui/react"; +import Match from "./Match.tsx"; interface TournamentBracketProps { stage: TournamentStageView } -interface MatchPosition { - id: number - x: number - y: number - width: number - height: number -} - -const TournamentBracket: React.FC = ({ stage }) => { - const [matchPositions, setMatchPositions] = useState([]) - const bracketRef = useRef(null) - - const matchesByLevel = stage.matches.reduce( - (acc, match) => { - if (!acc[match.level]) { - acc[match.level] = [] - } - acc[match.level].push(match) - return acc - }, - {} as Record - ) - - const levels = Object.keys(matchesByLevel) - .map(Number) - .sort((a, b) => a - b) - const maxLevel = Math.max(...levels) - - useEffect(() => { - if (bracketRef.current) { - const positions: MatchPosition[] = [] - const matchElements = bracketRef.current.querySelectorAll('[data-match-id]') - - matchElements.forEach((el) => { - const rect = el.getBoundingClientRect() - const bracketRect = bracketRef.current!.getBoundingClientRect() - positions.push({ - id: Number.parseInt(el.getAttribute('data-match-id') || '0', 10), - x: rect.left - bracketRect.left + rect.width, - y: rect.top - bracketRect.top + rect.height / 2, - width: rect.width, - height: rect.height - }) - }) - - setMatchPositions(positions) - } - }, [stage.matches]) - - const getConnectorProps = (sourceId: number, targetId: number) => { - const sourceMatch = matchPositions.find((m) => m.id === sourceId) - const targetMatch = matchPositions.find((m) => m.id === targetId) - - if (!sourceMatch || !targetMatch) return null - - return { - startX: sourceMatch.x, - startY: sourceMatch.y, - endX: targetMatch.x - targetMatch.width, - endY: targetMatch.y - } - } - - const getMatchSpacing = (level: number) => { - const baseSpacing = 4 // rem - const spacingMultiplier = 2 ** (maxLevel - level) - return `${baseSpacing * spacingMultiplier}rem` - } - +const TournamentBracket: React.FC = ({ stage }: TournamentBracketProps) => { return ( -
-

{stage.name}

-
-
- {levels.map((level) => ( -
-

Round {level}

-
- {matchesByLevel[level].map( - match => ( -
- -
- ) - )} -
-
- ))} -
- {matchPositions.length > 0 && - stage.matches.map((match: { seed1: number; seed2: number; id: number }) => { - if (match.seed1 < 0 || match.seed2 < 0) { - const sourceId1 = match.seed1 < 0 ? -match.seed1 : match.seed1 - const sourceId2 = match.seed2 < 0 ? -match.seed2 : match.seed2 - const connectorProps1 = getConnectorProps(sourceId1, match.id) - const connectorProps2 = getConnectorProps(sourceId2, match.id) - - return ( - - {connectorProps1 && } - {connectorProps2 && } - - ) - } - return null - })} -
-
+ <> + + {stage.name} + + + {stage.matches.map((match: MatchView) => ( + + ))} + + ) } diff --git a/frontend/src/pages/tournament/components/Match.tsx b/frontend/src/pages/tournament/components/Match.tsx index f0cd7278..f00bee19 100644 --- a/frontend/src/pages/tournament/components/Match.tsx +++ b/frontend/src/pages/tournament/components/Match.tsx @@ -1,14 +1,14 @@ -import type React from "react" -import { MatchView, MatchStatus } from "../../../util/views/tournament.view.ts" +import {MatchView, ParticipantView} from "../../../util/views/tournament.view.ts"; +import {Box, Flex, Text} from "@chakra-ui/react"; interface MatchProps { match: MatchView } -const Match: React.FC = ({ match }) => { +const Match = ({match}: MatchProps) => { const getScoreColor = (score1?: number, score2?: number) => { - if (match.status in [MatchStatus.CANCELLED, MatchStatus.NOT_STARTED] || score1 === undefined || score2 === undefined) return "text-gray-600" - return score1 > score2 ? "text-green-600 font-bold" : "text-red-600" + if (match.status !== "COMPLETED" || score1 === undefined || score2 === undefined) return "gray.600" + return score1 > score2 ? "green.600" : "red.600" } const formatKickOffTime = (timestamp?: number) => { @@ -22,31 +22,54 @@ const Match: React.FC = ({ match }) => { }) } - const getParticipantName = (seed: number, participant?: { name: string }) => { - if (participant) return participant.name + const getParticipantName = (seed: number, participant?: ParticipantView) => { + if (participant) return participant.teamName if (seed < 0) return `Winner of Game ${-seed}` return "TBD" } return ( -
-
Game {match.gameId}
-
- {getParticipantName(match.seed1, match.participant1)} - {match.score1 ?? "-"} -
-
- {getParticipantName(match.seed2, match.participant2)} - {match.score2 ?? "-"} -
-
- {match.status} - {formatKickOffTime(match.kickoffTime)} -
-
{match.location}
-
+ + + Game {match.id} + + + + {getParticipantName(match.homeSeed, match.home)} + + + {match.homeScore ?? "-"} + + + + + {getParticipantName(match.awaySeed, match.away)} + + + {match.awayScore ?? "-"} + + + + + {match.status} + + + {formatKickOffTime(match.kickoffTime)} + + + + {match.location} + + ) } export default Match - diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index dc0830f0..ef09f9d0 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -32,24 +32,22 @@ const Tournament = ({tournament}: TournamentProps) => { } - -
- { - tournament.tournament.participants.map((participant) => ( -
-

{participant.name}

-
- )) - } -
+ + { + tournament.tournament.participants.map((participant) => ( +
+

{participant.teamName}

+
+ )) + }
- { tournament.stages.map((stage) => ( - + + + )) } -
diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 5745a930..2fc0bfe5 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -1,11 +1,13 @@ import { useTournamentListQuery } from '../../api/hooks/tournament/useTournamentListQuery.ts' import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' -import { Box, Heading, VStack } from '@chakra-ui/react' +import {Box, Heading, LinkOverlay, VStack} from '@chakra-ui/react' import { TournamentPreview } from '../../util/views/tournament.view.ts' import { ComponentUnavailable } from '../../common-components/ComponentUnavailable.tsx' import { PageStatus } from '../../common-components/PageStatus.tsx' import { CmschPage } from '../../common-components/layout/CmschPage.tsx' import { Helmet } from 'react-helmet-async' +import {Link} from "react-router-dom"; +import {AbsolutePaths} from "../../util/paths.ts"; const TournamentListPage = () => { @@ -31,7 +33,9 @@ const TournamentListPage = () => { data.map((tournament: TournamentPreview) => ( - {tournament.title} + + {tournament.title} + {tournament.description} diff --git a/frontend/src/util/paths.ts b/frontend/src/util/paths.ts index 8ee1e2b9..fd073df5 100644 --- a/frontend/src/util/paths.ts +++ b/frontend/src/util/paths.ts @@ -57,7 +57,8 @@ export enum AbsolutePaths { QR_FIGHT = '/qr-fight', LEADER_BOARD = '/leaderboard', ACCESS_KEY = '/access-key', - MAP = '/map' + MAP = '/map', + TOURNAMENTS = '/tournament', } export enum ApiPaths { diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index b81e7802..4e626b8b 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -15,8 +15,8 @@ export type TournamentWithParticipantsView = { } export type ParticipantView = { - id: number - name: string + teamId: number + teamName: string } export enum StageStatus { @@ -30,12 +30,9 @@ export enum StageStatus { export enum MatchStatus { NOT_STARTED = 'NOT_STARTED', - HT = 'HT', - FT = 'FT', - AET = 'AET', - AP = 'AP', IN_PROGRESS = 'IN_PROGRESS', CANCELLED = 'CANCELLED', + COMPLETED = 'COMPLETED', } export type MatchView = { @@ -44,12 +41,12 @@ export type MatchView = { kickoffTime?: number level: number location: string - seed1: number - seed2: number - participant1?: ParticipantView - participant2?: ParticipantView - score1?: number - score2?: number + homeSeed: number + awaySeed: number + home?: ParticipantView + away?: ParticipantView + homeScore?: number + awayScore?: number status: MatchStatus } From 8c1709ea3b725d025e7f43d0e2016fc06aa7d529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sat, 19 Apr 2025 21:13:54 +0200 Subject: [PATCH 26/89] bracket connectors --- .../tournament/components/KnockoutBracket.tsx | 54 +++++++++++++++++++ .../tournament/components/KnockoutStage.tsx | 45 +++++++++++++--- .../tournament/components/Tournament.tsx | 14 ++--- 3 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 frontend/src/pages/tournament/components/KnockoutBracket.tsx diff --git a/frontend/src/pages/tournament/components/KnockoutBracket.tsx b/frontend/src/pages/tournament/components/KnockoutBracket.tsx new file mode 100644 index 00000000..ffb43a35 --- /dev/null +++ b/frontend/src/pages/tournament/components/KnockoutBracket.tsx @@ -0,0 +1,54 @@ +import {MatchTree} from "./KnockoutStage.tsx"; +import {Box, Stack} from "@chakra-ui/react"; +import Match from "./Match.tsx"; + + +interface KnockoutBracketProps { + tree: MatchTree +} + + +const KnockoutBracket: React.FC = ({ tree }: KnockoutBracketProps) => { + return ( + <> + + { (tree.upperTree || tree.lowerTree) && ( + <> + + {tree.upperTree && } + {tree.lowerTree && } + + + { + tree.upperTree && tree.lowerTree && + <> + + + + + + } + { + tree.upperTree && !tree.lowerTree && + <> + + + } + { + !tree.upperTree && tree.lowerTree && + <> + + + } + + + )} + + + + + + ) +} + +export default KnockoutBracket diff --git a/frontend/src/pages/tournament/components/KnockoutStage.tsx b/frontend/src/pages/tournament/components/KnockoutStage.tsx index 2a48f37b..123be959 100644 --- a/frontend/src/pages/tournament/components/KnockoutStage.tsx +++ b/frontend/src/pages/tournament/components/KnockoutStage.tsx @@ -1,23 +1,52 @@ import React from "react" import { TournamentStageView, MatchView } from "../../../util/views/tournament.view.ts" -import {Heading, Stack} from "@chakra-ui/react"; -import Match from "./Match.tsx"; +import { Heading } from "@chakra-ui/react"; +import { groupBy, keys } from 'lodash' +import KnockoutBracket from './KnockoutBracket.tsx' interface TournamentBracketProps { stage: TournamentStageView } +export type MatchTree = { + root: MatchView + lowerTree: MatchTree | null + upperTree: MatchTree | null +} + const TournamentBracket: React.FC = ({ stage }: TournamentBracketProps) => { + let levels = groupBy(stage.matches, (match: MatchView) => match.level) + let levelCount = keys(levels).length + + + const buildTree = (level: number, rootNum: number): MatchTree => { + { + let root = levels[level][rootNum] + let upperTree = (level>1 && levels[level-1].length>2*rootNum) ? buildTree(level-1, 2*rootNum) : null + let lowerTree = (level>1 && levels[level-1].length>2*rootNum+1) ? buildTree(level-1, 2*rootNum+1) : null + return { + root: root, + lowerTree: lowerTree, + upperTree: upperTree + } + } + } + + let trees: MatchTree[] = [] + for (let i = 0; i < levels[levelCount].length; i++) { + trees.push(buildTree(levelCount, i)) + } + return ( <> - + {stage.name} - - {stage.matches.map((match: MatchView) => ( - - ))} - + { + trees.map((tree, index) => ( + + )) + } ) } diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index ef09f9d0..14011982 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -41,13 +41,13 @@ const Tournament = ({tournament}: TournamentProps) => { )) } - { - tournament.stages.map((stage) => ( - - - - )) - } + { + tournament.stages.map((stage) => ( + + + + )) + } From 78e30bd0f1d8efb46dc965526d9ce4262890ac71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sat, 19 Apr 2025 21:14:46 +0200 Subject: [PATCH 27/89] update component to support S3 buckets (and minor stuff) --- .../tournament/KnockoutStageController.kt | 3 ++ .../tournament/KnockoutStageService.kt | 51 ++++++++++--------- .../tournament/TournamentComponent.kt | 2 +- .../TournamentComponentController.kt | 5 +- .../tournament/TournamentController.kt | 3 ++ .../tournament/TournamentMatchController.kt | 3 ++ .../tournament/TournamentMatchEntity.kt | 4 +- 7 files changed, 44 insertions(+), 27 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt index 7b757ffb..9868b113 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -8,6 +8,7 @@ import hu.bme.sch.cmsch.service.AdminMenuService import hu.bme.sch.cmsch.service.AuditLogService import hu.bme.sch.cmsch.service.ImportService import hu.bme.sch.cmsch.service.StaffPermissions +import hu.bme.sch.cmsch.service.StorageService import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import org.springframework.stereotype.Controller @@ -27,6 +28,7 @@ class KnockoutStageController( auditLog: AuditLogService, objectMapper: ObjectMapper, transactionManager: PlatformTransactionManager, + storageService: StorageService, env: Environment ) : TwoDeepEntityPage( "knockout-stage", @@ -53,6 +55,7 @@ class KnockoutStageController( stageRepository, importService, adminMenuService, + storageService, component, auditLog, objectMapper, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index fad48f65..b78df974 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -20,44 +20,49 @@ class KnockoutStageService( @Transactional fun createMatchesForStage(stage: KnockoutStageEntity) { - val secondRoundGames = 2.0.pow(stage.rounds().toDouble() - 2).toInt() - val firstRoundGames = stage.matches() - 2 * secondRoundGames + 1 - val byeWeekParticipantCount = stage.participantCount - firstRoundGames * 2 + val firstRound = 2.0.pow(stage.rounds() - 1).toInt() + val gameCount = 2*firstRound + val byeSlotCount = 2*firstRound-stage.participantCount - val seedSpots = (1..2*secondRoundGames).asIterable().shuffled() - .subList(0, byeWeekParticipantCount) //TODO bye week participant count wrong - // TODO do better seeding, this is just random stuff + val byeGames = (1..firstRound).asIterable().shuffled().subList(0,byeSlotCount).sorted() val matches = mutableListOf() - for (i in 0 until firstRoundGames) { + var j = 0; var k = 0 + for (i in 0 until firstRound){ matches.add(TournamentMatchEntity( - gameId = i + 1, + gameId = i, stageId = stage.id, - homeSeed = i + 1 + byeWeekParticipantCount, - awaySeed = i + 2 + byeWeekParticipantCount + level = 1, + homeSeed = j++, + awaySeed = if (byeGames[k] == i) { + k++ + 0 + } else { + j++ + } )) } - var j = 1; var k = 1 - for (i in 1 until secondRoundGames + 1) { + var roundMatches = firstRound; j = 0; k = 2 + for (i in firstRound until gameCount){ matches.add(TournamentMatchEntity( - gameId = firstRoundGames + j, + gameId = i, stageId = stage.id, - homeSeed = if(seedSpots.contains(2*i-1)) j++ else -(k++), - awaySeed = if(seedSpots.contains(2*i)) j++ else -(k++) - )) - } - for (i in firstRoundGames + secondRoundGames until stage.matches()) { - matches.add(TournamentMatchEntity( - gameId = i + 1, - stageId = stage.id, - homeSeed = -(k++), - awaySeed = -(k++) + level = k, + homeSeed = -(j++), + awaySeed = -(j++) )) + if (j == roundMatches) { + j = 0 + k++ + roundMatches /= 2 + } } + for (match in matches) { matchRepository.save(match) } + //placeholder seeding for testing val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() val participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1).subList(0, stage.participantCount) .map { StageResultDto(it.teamId, it.teamName) } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index b2ee254e..3d2b5e33 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -25,7 +25,7 @@ class TournamentComponent ( "Tournament", ControlPermissions.PERMISSION_CONTROL_TOURNAMENT, listOf(), - componentSettingService, env + env ){ final override val allSettings by lazy { listOf( diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt index e4288145..da8dc488 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponentController.kt @@ -5,6 +5,7 @@ import hu.bme.sch.cmsch.component.app.MenuService import hu.bme.sch.cmsch.service.AdminMenuService import hu.bme.sch.cmsch.service.AuditLogService import hu.bme.sch.cmsch.service.ControlPermissions +import hu.bme.sch.cmsch.service.StorageService import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.RequestMapping @@ -18,6 +19,7 @@ class TournamentComponentController( private val menuService: MenuService, private val tournamentService: TournamentService, private val auditLogService: AuditLogService, + private val storageService: StorageService, service: MenuService ) : ComponentApiBase( adminMenuService, @@ -27,7 +29,8 @@ class TournamentComponentController( "Tournament", "Tournament beállítások", auditLogService = auditLogService, - menuService = menuService + menuService = menuService, + storageService = storageService ) { } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt index 9c7f01db..bb1c4805 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentController.kt @@ -7,6 +7,7 @@ import hu.bme.sch.cmsch.service.AdminMenuService import hu.bme.sch.cmsch.service.AuditLogService import hu.bme.sch.cmsch.service.ImportService import hu.bme.sch.cmsch.service.StaffPermissions +import hu.bme.sch.cmsch.service.StorageService import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import org.springframework.stereotype.Controller @@ -24,6 +25,7 @@ class TournamentController( auditLog: AuditLogService, objectMapper: ObjectMapper, transactionManager: PlatformTransactionManager, + storageService: StorageService, env: Environment ) : OneDeepEntityPage( "tournament", @@ -35,6 +37,7 @@ class TournamentController( repo, importService, adminMenuService, + storageService, component, auditLog, objectMapper, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt index 4a95cb8e..17cc9a29 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -7,6 +7,7 @@ import hu.bme.sch.cmsch.service.AdminMenuService import hu.bme.sch.cmsch.service.AuditLogService import hu.bme.sch.cmsch.service.ImportService import hu.bme.sch.cmsch.service.StaffPermissions +import hu.bme.sch.cmsch.service.StorageService import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import org.springframework.stereotype.Controller @@ -26,6 +27,7 @@ class TournamentMatchController( auditLog: AuditLogService, objectMapper: ObjectMapper, transactionManager: PlatformTransactionManager, + storageService: StorageService, env: Environment ) : TwoDeepEntityPage( "tournament-match", @@ -42,6 +44,7 @@ class TournamentMatchController( matchRepository, importService, adminMenuService, + storageService, component, auditLog, objectMapper, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index 6d76355e..d96442b3 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -125,8 +125,8 @@ data class TournamentMatchEntity( @PreUpdate fun setTeams() { val teams = KnockoutStageService.getBean().getParticipants(stageId) - homeTeamId = teams.find { it.teamName == homeTeamName }?.teamId ?: null - awayTeamId = teams.find { it.teamName == awayTeamName }?.teamId ?: null + homeTeamId = teams.find { it.teamName == homeTeamName }?.teamId + awayTeamId = teams.find { it.teamName == awayTeamName }?.teamId } override fun getEntityConfig(env: Environment) = EntityConfig( From 34202756230704003ffe37a1dbd8549d8c26e006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sat, 19 Apr 2025 21:51:27 +0200 Subject: [PATCH 28/89] small adjustments for Match.tsx --- frontend/src/pages/tournament/components/KnockoutStage.tsx | 3 +++ frontend/src/pages/tournament/components/Match.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/tournament/components/KnockoutStage.tsx b/frontend/src/pages/tournament/components/KnockoutStage.tsx index 123be959..05885e4d 100644 --- a/frontend/src/pages/tournament/components/KnockoutStage.tsx +++ b/frontend/src/pages/tournament/components/KnockoutStage.tsx @@ -33,6 +33,9 @@ const TournamentBracket: React.FC = ({ stage }: Tourname } let trees: MatchTree[] = [] + if (levelCount < 1) { + return <> + } for (let i = 0; i < levels[levelCount].length; i++) { trees.push(buildTree(levelCount, i)) } diff --git a/frontend/src/pages/tournament/components/Match.tsx b/frontend/src/pages/tournament/components/Match.tsx index f00bee19..65630e9a 100644 --- a/frontend/src/pages/tournament/components/Match.tsx +++ b/frontend/src/pages/tournament/components/Match.tsx @@ -66,7 +66,7 @@ const Match = ({match}: MatchProps) => { - {match.location} + {match.location!="" ? match.location : "Location TBD"} ) From fc6f543d6456fcc38acb2a1966bcca62398a82fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 9 Jun 2025 23:11:33 +0200 Subject: [PATCH 29/89] bulk deletion for matches and stages, working bracket generation --- .../tournament/KnockoutStageEntity.kt | 15 +++--- .../tournament/KnockoutStageRepository.kt | 2 + .../tournament/KnockoutStageService.kt | 47 ++++++++++++------- .../component/tournament/TournamentEntity.kt | 6 +++ .../tournament/TournamentMatchController.kt | 2 +- .../tournament/TournamentMatchEntity.kt | 2 +- .../tournament/TournamentMatchRepository.kt | 2 + .../component/tournament/TournamentService.kt | 12 +++++ .../src/pages/tournament/components/Match.tsx | 2 +- .../tournament/components/Tournament.tsx | 2 +- 10 files changed, 64 insertions(+), 28 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 814f7a91..0dcdbc56 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -60,7 +60,7 @@ data class KnockoutStageEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Résztvevők száma") + @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Résztvevők száma", note = "Legfeljebb annyi csapat, mint a versenyen résztvevők száma") @property:GenerateOverview(columnName = "RésztvevőSzám", order = 3, centered = true) @property:ImportFormat var participantCount: Int = 1, @@ -113,11 +113,14 @@ data class KnockoutStageEntity( } - /*@PostPersist - fun postPersist() { - tournament()!! + @PrePersist + fun prePersist() { getStageService().createMatchesForStage(this) - //getStageService().calculateTeamsFromSeeds(this) - }*/ + } + + @PreRemove + fun preRemove() { + getStageService().deleteMatchesForStage(this) + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt index 035e9525..99d13afb 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageRepository.kt @@ -35,4 +35,6 @@ interface KnockoutStageRepository : CrudRepository, @Query("select k from KnockoutStageEntity k where k.tournamentId = ?1 and k.level = ?2") fun findAllByTournamentIdAndLevel(tournamentId: Int, level: Int): List + fun deleteAllByTournamentId(tournamentId: Int): Int + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index b78df974..070201dc 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -5,7 +5,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional +import kotlin.jvm.optionals.getOrNull import kotlin.math.pow import kotlin.random.Random @@ -27,14 +29,14 @@ class KnockoutStageService( val byeGames = (1..firstRound).asIterable().shuffled().subList(0,byeSlotCount).sorted() val matches = mutableListOf() - var j = 0; var k = 0 - for (i in 0 until firstRound){ + var j = 1; var k = 0 + for (i in 1 until firstRound+1){ matches.add(TournamentMatchEntity( gameId = i, stageId = stage.id, level = 1, homeSeed = j++, - awaySeed = if (byeGames[k] == i) { + awaySeed = if (byeGames.size>k && byeGames[k] == i) { k++ 0 } else { @@ -42,8 +44,8 @@ class KnockoutStageService( } )) } - var roundMatches = firstRound; j = 0; k = 2 - for (i in firstRound until gameCount){ + var roundMatches = firstRound; j = 1; k = 2 + for (i in firstRound+1 until gameCount+1){ matches.add(TournamentMatchEntity( gameId = i, stageId = stage.id, @@ -51,21 +53,21 @@ class KnockoutStageService( homeSeed = -(j++), awaySeed = -(j++) )) - if (j == roundMatches) { - j = 0 + if (j == roundMatches+1) { k++ - roundMatches /= 2 + roundMatches += roundMatches/2 } } + matchRepository.saveAll(matches) + } - for (match in matches) { - matchRepository.save(match) - } - - //placeholder seeding for testing + @Transactional + fun getTeamsForStage(stage: KnockoutStageEntity){ val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() - val participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1).subList(0, stage.participantCount) - .map { StageResultDto(it.teamId, it.teamName) } + var participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1) + if (participants.size >= stage.participantCount) { + participants = participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) } + } for (i in 0 until stage.participantCount) { participants[i].seed = teamSeeds[i] } @@ -90,13 +92,17 @@ class KnockoutStageService( fun getTournamentService(): TournamentService = tournamentService - fun findById(id: Int): KnockoutStageEntity { - return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } + fun findById(id: Int): KnockoutStageEntity? { + //return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } + return stageRepository.findById(id).getOrNull() } fun getParticipants(stageId: Int): List { val stage = findById(stageId) - return stage.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + if (stage == null || stage.participants.isEmpty()) { + return emptyList() + } + return stage!!.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } } @@ -127,6 +133,11 @@ class KnockoutStageService( }.sortedByDescending { it.matchCount } } + @Transactional + fun deleteMatchesForStage(stage: KnockoutStageEntity) { + matchRepository.deleteAllByStageId(stage.id) + } + companion object { private var applicationContext: ApplicationContext? = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 9bdc9d81..5974e574 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -76,6 +76,7 @@ data class TournamentEntity( view = "control/tournament", showPermission = StaffPermissions.PERMISSION_SHOW_TOURNAMENTS, ) + fun getTournamentService() = TournamentService.getBean() override fun equals(other: Any?): Boolean { if (this === other) return true @@ -90,4 +91,9 @@ data class TournamentEntity( override fun toString(): String { return this::class.simpleName + "(id = $id, name = '$title')" } + + @PreRemove + fun preRemove() { + + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt index 17cc9a29..de1eda22 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -57,7 +57,7 @@ class TournamentMatchController( createEnabled = true, editEnabled = true, - deleteEnabled = false, + deleteEnabled = true, importEnabled = false, exportEnabled = false, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index d96442b3..cc366e2e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -119,7 +119,7 @@ data class TournamentMatchEntity( ): ManagedEntity{ - fun stage(): KnockoutStageEntity = KnockoutStageService.getBean().findById(stageId) + fun stage(): KnockoutStageEntity? = KnockoutStageService.getBean().findById(stageId) @PrePersist @PreUpdate diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt index c4c88ac3..70ff4543 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchRepository.kt @@ -31,4 +31,6 @@ interface TournamentMatchRepository : CrudRepository GROUP BY t.stageId """) fun findAllAggregated(): List + + fun deleteAllByStageId(stageId: Int): Int } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index a539e824..9491c4ec 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -3,6 +3,7 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.* @@ -89,4 +90,15 @@ open class TournamentService( return true } + companion object{ + private var applicationContext: ApplicationContext? = null + fun getBean(): TournamentService = applicationContext?.getBean(TournamentService::class.java) + ?: throw IllegalStateException("TournamentService is not initialized. Make sure TournamentComponent is enabled and application context is set.") + } + + @Transactional + fun deleteStagesForTournament(tournamentId: Int) { + stageRepository.deleteAllByTournamentId(tournamentId) + } + } diff --git a/frontend/src/pages/tournament/components/Match.tsx b/frontend/src/pages/tournament/components/Match.tsx index 65630e9a..698f2262 100644 --- a/frontend/src/pages/tournament/components/Match.tsx +++ b/frontend/src/pages/tournament/components/Match.tsx @@ -31,7 +31,7 @@ const Match = ({match}: MatchProps) => { return ( - Game {match.id} + Game {match.gameId} diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 14011982..0ac3a3d6 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -43,7 +43,7 @@ const Tournament = ({tournament}: TournamentProps) => { { tournament.stages.map((stage) => ( - + )) From 2de7f6d49f007274edd3f2ad7458ef0c9e2e580e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Sun, 15 Jun 2025 16:01:49 +0200 Subject: [PATCH 30/89] registerTeam WIP --- .../tournament/TournamentApiController.kt | 39 ++++++++++++++++++- .../component/tournament/TournamentEntity.kt | 2 +- .../component/tournament/TournamentService.kt | 4 +- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index cc6cf024..d7e89c75 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -1,7 +1,11 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.component.login.CmschUser +import hu.bme.sch.cmsch.component.team.TeamService import hu.bme.sch.cmsch.dto.Preview +import hu.bme.sch.cmsch.model.RoleType +import hu.bme.sch.cmsch.repository.GroupRepository import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses @@ -9,8 +13,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import kotlin.jvm.optionals.getOrNull @RestController @RequestMapping("/api") @@ -18,7 +24,8 @@ import org.springframework.web.bind.annotation.RestController class TournamentApiController( private val tournamentComponent: TournamentComponent, private val tournamentService: TournamentService, - private val stageService: KnockoutStageService + private val stageService: KnockoutStageService, + private val groupRepository: GroupRepository, ) { @JsonView(Preview::class) @GetMapping("/tournament") @@ -91,4 +98,34 @@ class TournamentApiController( ) })) } + @PostMapping("/tournament/{tournamentId}/register/{teamId}") + @Operation( + summary = "Register a team for a tournament.", + ) + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Team registered successfully"), + ApiResponse(responseCode = "401", description = "Not authorized, user must be group admin of the team"), + ApiResponse(responseCode = "404", description = "Tournament or team not found"), + ApiResponse(responseCode = "400", description = "Bad request, team already registered") + ]) + fun registerTeam( + @PathVariable tournamentId: Int, + @PathVariable teamId: Int, + user: CmschUser + ): ResponseEntity { + val team = groupRepository.findById(teamId).getOrNull() + if (team == null) { + return ResponseEntity.notFound().build() + } + if (user.groupId != team.id || user.role.value < RoleType.PRIVILEGED.value) { + return ResponseEntity.status(401).body("Not authorized, user must be group admin of the team") + } + val result = tournamentService.teamRegister(tournamentId, teamId, "") + return if (result) { + ResponseEntity.ok("Team registered successfully") + } else { + ResponseEntity.badRequest().body("Failed to register team") + } + } + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 5974e574..ebc5902b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -94,6 +94,6 @@ data class TournamentEntity( @PreRemove fun preRemove() { - + getTournamentService().deleteStagesForTournament(this) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 9491c4ec..47024a41 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -97,8 +97,8 @@ open class TournamentService( } @Transactional - fun deleteStagesForTournament(tournamentId: Int) { - stageRepository.deleteAllByTournamentId(tournamentId) + fun deleteStagesForTournament(tournament: TournamentEntity) { + stageRepository.deleteAllByTournamentId(tournament.id) } } From eaa3f1213232f2480eaa055878ce15ae41fd40dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 16 Jun 2025 23:18:28 +0200 Subject: [PATCH 31/89] registerTeam --- .../tournament/TournamentApiController.kt | 20 ++++++++++++++++--- .../component/tournament/TournamentEntity.kt | 7 +++++++ .../component/tournament/TournamentService.kt | 9 +++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index d7e89c75..4323ecbd 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -117,12 +117,26 @@ class TournamentApiController( if (team == null) { return ResponseEntity.notFound().build() } + val tournament = tournamentService.findById(tournamentId).getOrNull() + if (tournament == null) { + return ResponseEntity.notFound().build() + } + + if( tournamentService.isTeamRegistered(tournamentId, teamId)) { + return ResponseEntity.badRequest().build() + } + if (user.groupId != team.id || user.role.value < RoleType.PRIVILEGED.value) { - return ResponseEntity.status(401).body("Not authorized, user must be group admin of the team") + return ResponseEntity.status(401).build() + } + + if (!tournament.joinable) { + return ResponseEntity.badRequest().body("Tournament is not joinable") } - val result = tournamentService.teamRegister(tournamentId, teamId, "") + + val result = tournamentService.teamRegister(tournamentId, teamId, team.name) return if (result) { - ResponseEntity.ok("Team registered successfully") + ResponseEntity.ok().build() } else { ResponseEntity.badRequest().body("Failed to register team") } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index ebc5902b..761055c7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -48,6 +48,13 @@ data class TournamentEntity( @property:ImportFormat var location: String = "", + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(type = INPUT_TYPE_SWITCH, visible = true, ignore = true) + @property:GenerateOverview(columnName = "Joinable", order = 2) + @property:ImportFormat + var joinable: Boolean = false, + @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 47024a41..832f94f2 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -66,6 +66,15 @@ open class TournamentService( ) } + fun isTeamRegistered(tournamentId: Int, teamId: Int): Boolean { + val tournament = tournamentRepository.findById(tournamentId) + if (tournament.isEmpty) { + return false + } + val participants = tournament.get().participants + return participants.split("\n").any { it.contains("\"teamId\":$teamId") } + } + @Transactional fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { From 60f9df9f827a3fd8e2f3a08577e6d069533dbd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Thu, 19 Jun 2025 20:04:50 +0200 Subject: [PATCH 32/89] inputType, overviewType --- .../component/tournament/KnockoutGroupDto.kt | 4 +-- .../tournament/KnockoutStageEntity.kt | 12 ++++----- .../component/tournament/MatchGroupDto.kt | 4 +-- .../component/tournament/TournamentEntity.kt | 12 ++++----- .../tournament/TournamentMatchEntity.kt | 26 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt index f3cac939..42f2fb17 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutGroupDto.kt @@ -1,12 +1,12 @@ package hu.bme.sch.cmsch.component.tournament import hu.bme.sch.cmsch.admin.GenerateOverview -import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.admin.OverviewType import hu.bme.sch.cmsch.model.IdentifiableEntity data class KnockoutGroupDto( - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + @property:GenerateOverview(renderer = OverviewType.ID, columnName = "ID", order = -1) override var id: Int = 0, @property:GenerateOverview(columnName = "Név", order = 1) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 0dcdbc56..a10fddc6 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -34,8 +34,8 @@ data class KnockoutStageEntity( @GeneratedValue @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OverviewType.ID, columnName = "ID") override var id: Int = 0, @Column(nullable = false) @@ -46,28 +46,28 @@ data class KnockoutStageEntity( var name: String = "", @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 2, label = "Verseny ID") + @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 2, label = "Verseny ID") @property:GenerateOverview(columnName = "Verseny ID", order = 2, centered = true) @property:ImportFormat var tournamentId: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Szint") + @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 3, label = "Szint") @property:GenerateOverview(columnName = "Szint", order = 3, centered = true) @property:ImportFormat var level: Int = 1, //ie. Csoportkör-1, Csoportkör-2, Kieséses szakasz-3 @Column(nullable = false) @field:JsonView(value = [ Edit::class ]) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 3, label = "Résztvevők száma", note = "Legfeljebb annyi csapat, mint a versenyen résztvevők száma") + @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 3, label = "Résztvevők száma", note = "Legfeljebb annyi csapat, mint a versenyen résztvevők száma") @property:GenerateOverview(columnName = "RésztvevőSzám", order = 3, centered = true) @property:ImportFormat var participantCount: Int = 1, @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) @property:GenerateOverview(visible = false) @property:ImportFormat var participants: String = "", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt index b306b37b..32b2f3fa 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/MatchGroupDto.kt @@ -1,12 +1,12 @@ package hu.bme.sch.cmsch.component.tournament import hu.bme.sch.cmsch.admin.GenerateOverview -import hu.bme.sch.cmsch.admin.OVERVIEW_TYPE_ID +import hu.bme.sch.cmsch.admin.OverviewType import hu.bme.sch.cmsch.model.IdentifiableEntity data class MatchGroupDto( - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1) + @property:GenerateOverview(renderer = OverviewType.ID, columnName = "ID", order = -1) override var id: Int = 0, @property:GenerateOverview(columnName = "Név", order = 1) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 761055c7..486f62ef 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -23,8 +23,8 @@ data class TournamentEntity( @GeneratedValue @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(renderer = OVERVIEW_TYPE_NUMBER, columnName = "ID", order = -1) + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OverviewType.NUMBER, columnName = "ID", order = -1) override var id: Int = 0, @Column(nullable = false) @@ -50,28 +50,28 @@ data class TournamentEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_SWITCH, visible = true, ignore = true) + @property:GenerateInput(type = InputType.SWITCH, visible = true, ignore = true) @property:GenerateOverview(columnName = "Joinable", order = 2) @property:ImportFormat var joinable: Boolean = false, @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) @property:ImportFormat var participantCount: Int = 0, @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) @property:GenerateOverview(visible = false) @property:ImportFormat var participants: String = "", @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) @property:GenerateOverview(visible = false) @property:ImportFormat var status: Int = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt index cc366e2e..a69902ab 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchEntity.kt @@ -30,31 +30,31 @@ data class TournamentMatchEntity( @GeneratedValue @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID") + @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) + @property:GenerateOverview(renderer = OverviewType.ID, columnName = "ID") override var id: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "Game ID") + @property:GenerateInput(type = InputType.NUMBER, visible = true, ignore = true) + @property:GenerateOverview(renderer = OverviewType.ID, columnName = "Game ID") @property:ImportFormat var gameId: Int = 0, @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Stage ID") + @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 1, label = "Stage ID") @property:GenerateOverview(columnName = "Stage ID", order = 1) @property:ImportFormat var stageId: Int = 0, @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = 1, order = 1, label = "Level") + @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 1, label = "Level") @property:GenerateOverview(columnName = "Level", order = 1) @property:ImportFormat var level: Int = 0, @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = Int.MIN_VALUE, order = 2, label = "Home seed") + @property:GenerateInput(type = InputType.NUMBER, min = Int.MIN_VALUE, order = 2, label = "Home seed") var homeSeed: Int = 0, @Column(nullable = true) @@ -62,13 +62,13 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 2, label = "Home team name") + @property:GenerateInput(type = InputType.TEXT, order = 2, label = "Home team name") @property:GenerateOverview(columnName = "Home team name", order = 2) @property:ImportFormat var homeTeamName: String = "", @Column(nullable = false) - @property:GenerateInput(type = INPUT_TYPE_NUMBER, min = Int.MIN_VALUE, order = 3, label = "Away seed") + @property:GenerateInput(type = InputType.NUMBER, min = Int.MIN_VALUE, order = 3, label = "Away seed") var awaySeed: Int = 0, @Column(nullable = true) @@ -76,21 +76,21 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_TEXT, min = 1, order = 3, label = "Away team name") + @property:GenerateInput(type = InputType.TEXT, min = 1, order = 3, label = "Away team name") @property:GenerateOverview(columnName = "Away team name", order = 3) @property:ImportFormat var awayTeamName: String = "", @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_DATE, order = 4, label = "Kickoff time") + @property:GenerateInput(type = InputType.DATE, order = 4, label = "Kickoff time") @property:GenerateOverview(columnName = "Kickoff time", order = 4) @property:ImportFormat var kickoffTime: Long = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_TEXT, order = 5, label = "Location") + @property:GenerateInput(type = InputType.TEXT, order = 5, label = "Location") @property:GenerateOverview(columnName = "Location", order = 5) @property:ImportFormat var location: String = "", @@ -109,7 +109,7 @@ data class TournamentMatchEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = INPUT_TYPE_BLOCK_SELECT, order = 8, label = "Match status", + @property:GenerateInput(type = InputType.BLOCK_SELECT, order = 8, label = "Match status", source = [ "NOT_STARTED", "FIRST_HALF", "HT", "SECOND_HALF", "FT", "EXTRA_TIME", "AET", "PENALTY_KICKS", "AP", "IN_PROGRESS", "CANCELLED" ], visible = false, ignore = true ) From aa9fc4c0ca2fa4f7425a938e65ef4837443e9a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Thu, 19 Jun 2025 20:46:00 +0200 Subject: [PATCH 33/89] menu option available for tournaments --- .../sch/cmsch/component/tournament/TournamentComponent.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index 3d2b5e33..bf69c6e8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -34,7 +34,11 @@ class TournamentComponent ( ) } - final override val menuDisplayName = null + final override val menuDisplayName = SettingProxy( + componentSettingService, component, + "menuDisplayName", "Sportversenyek", serverSideOnly = true, + fieldName = "Menü neve", description = "Ez lesz a neve a menünek" + ) final val tournamentGroup = SettingProxy(componentSettingService, component, "tournamentGroup", "", type = SettingType.COMPONENT_GROUP, persist = false, From 5375720c4c293d5c329620eacad26f454724304b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Fri, 20 Jun 2025 11:44:05 +0200 Subject: [PATCH 34/89] create knockoutstage without memorizing the tournamentId (PLS CHECK this if safe) --- .../tournament/KnockoutStageController.kt | 137 +++++++++++++++++- .../tournament/KnockoutStageEntity.kt | 2 +- .../component/tournament/TournamentEntity.kt | 8 +- .../controller/admin/OneDeepEntityPage.kt | 10 +- .../controller/admin/TwoDeepEntityPage.kt | 2 +- 5 files changed, 147 insertions(+), 12 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt index 9868b113..2779bd8d 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -9,11 +9,23 @@ import hu.bme.sch.cmsch.service.AuditLogService import hu.bme.sch.cmsch.service.ImportService import hu.bme.sch.cmsch.service.StaffPermissions import hu.bme.sch.cmsch.service.StorageService +import hu.bme.sch.cmsch.util.getUser +import hu.bme.sch.cmsch.util.transaction import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment +import org.springframework.security.core.Authentication import org.springframework.stereotype.Controller import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionDefinition +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.multipart.MultipartFile @Controller @@ -66,7 +78,7 @@ class KnockoutStageController( editPermission = StaffPermissions.PERMISSION_EDIT_TOURNAMENTS, deletePermission = StaffPermissions.PERMISSION_DELETE_TOURNAMENTS, - createEnabled = true, + createEnabled = false, editEnabled = true, deleteEnabled = true, importEnabled = false, @@ -77,4 +89,127 @@ class KnockoutStageController( override fun fetchSublist(id: Int): Iterable { return stageRepository.findAllByTournamentId(id) } + + @GetMapping("/view/{id}") + override fun view(model: Model, auth: Authentication, @PathVariable id: Int): String { + val createButtonAction = ButtonAction( + "Új kiesési szakasz a tornához", + "create/$id", + createPermission, + 99, + "add_box", + true + ) + val newButtonActions = mutableListOf() + for (buttonAction in buttonActions) + newButtonActions.add(buttonAction) + newButtonActions.add(createButtonAction) + + val user = auth.getUser() + adminMenuService.addPartsForMenu(user, model) + if (viewPermission.validate(user).not()) { + model.addAttribute("permission", viewPermission.permissionString) + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "GET /$view/view/$id", viewPermission.permissionString) + return "admin403" + } + + model.addAttribute("title", titlePlural) + model.addAttribute("titleSingular", titleSingular) + model.addAttribute("description", description) + model.addAttribute("view", view) + + model.addAttribute("columnData", descriptor.getColumns()) + val overview = transactionManager.transaction(readOnly = true) { filterOverview(user, fetchSublist(id)) } + model.addAttribute("tableData", descriptor.getTableData(overview)) + + model.addAttribute("user", user) + model.addAttribute("controlActions", controlActions.filter { it.permission.validate(user) }) + model.addAttribute("allControlActions", controlActions) + model.addAttribute("buttonActions", newButtonActions.filter { it.permission.validate(user) }) + + attachPermissionInfo(model) + + return "overview4" + } + + @GetMapping("/create/{tournamentId}") + fun createStagePage(model: Model, auth: Authentication, @PathVariable tournamentId: Int): String { + val user = auth.getUser() + adminMenuService.addPartsForMenu(user, model) + if (editPermission.validate(user).not()) { + model.addAttribute("permission", editPermission.permissionString) + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "GET /$view/create/$tournamentId", showPermission.permissionString) + return "admin403" + } + + if (!editEnabled) + return "redirect:/admin/control/$view/" + + val entity = KnockoutStageEntity(tournamentId = tournamentId) + + val actualEntity = onPreEdit(entity) + model.addAttribute("data", actualEntity) + if (!editPermissionCheck(user, actualEntity, null)) { + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "GET /$view/create/$tournamentId", + "editPermissionCheck() validation") + return "admin403" + } + + model.addAttribute("title", titleSingular) + model.addAttribute("editMode", false) + model.addAttribute("duplicateMode", false) + model.addAttribute("view", view) + model.addAttribute("inputs", descriptor.getInputs()) + model.addAttribute("mappings", entitySourceMapping) + model.addAttribute("user", user) + model.addAttribute("readOnly", false) + model.addAttribute("entityMode", false) + + onDetailsView(user, model) + return "details" + } + + @Override + @PostMapping("/create", headers = ["Referer"]) + fun create(@ModelAttribute(binding = false) dto: KnockoutStageEntity, + @RequestParam(required = false) file0: MultipartFile?, + @RequestParam(required = false) file1: MultipartFile?, + model: Model, + auth: Authentication, + @RequestHeader("Referer") referer: String, + ): String { + val tournamentId = referer.substringAfterLast("/create/").toIntOrNull() + ?: return "redirect:/admin/control/$view" + val user = auth.getUser() + adminMenuService.addPartsForMenu(user, model) + if (createPermission.validate(user).not()) { + model.addAttribute("permission", createPermission.permissionString) + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "POST /$view/create", createPermission.permissionString) + return "admin403" + } + + if (!editPermissionCheck(user, null, dto)) { + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "POST /$view/create", "editPermissionCheck() validation") + return "admin403" + } + + val entity = KnockoutStageEntity() + dto.tournamentId = tournamentId + val newValues = StringBuilder("entity new value: ") + updateEntity(descriptor, user, entity, dto, newValues, false, file0, false, file1) + entity.id = 0 + if (onEntityPreSave(entity, auth)) { + auditLog.create(user, component.component, newValues.toString()) + transactionManager.transaction(readOnly = false, isolation = TransactionDefinition.ISOLATION_READ_COMMITTED) { + dataSource.save(entity) + } + } + onEntityChanged(entity) + return "redirect:/admin/control/$view" + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index a10fddc6..705468e0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -46,7 +46,7 @@ data class KnockoutStageEntity( var name: String = "", @Column(nullable = false) - @property:GenerateInput(type = InputType.NUMBER, min = 1, order = 2, label = "Verseny ID") + @property:GenerateInput(type = InputType.HIDDEN, min = 1, order = 2, label = "Verseny ID") @property:GenerateOverview(columnName = "Verseny ID", order = 2, centered = true) @property:ImportFormat var tournamentId: Int = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 486f62ef..8beecac5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -37,21 +37,21 @@ data class TournamentEntity( @Column(nullable = false, columnDefinition = "TEXT") @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(maxLength = 64, order = 3, label = "Verseny leírása") - @property:GenerateOverview(columnName = "Leírás", order = 3) + @property:GenerateOverview(columnName = "Leírás", order = 2) @property:ImportFormat var description: String = "", @Column(nullable = true) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(maxLength = 64, order = 4, label = "Verseny helyszíne") - @property:GenerateOverview(columnName = "Helyszín", order = 4) + @property:GenerateOverview(columnName = "Helyszín", order = 3) @property:ImportFormat var location: String = "", @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(type = InputType.SWITCH, visible = true, ignore = true) - @property:GenerateOverview(columnName = "Joinable", order = 2) + @property:GenerateInput(type = InputType.SWITCH, order = 5, label = "Lehet-e jelentkezni") + @property:GenerateOverview(columnName = "Joinable", order = 4) @property:ImportFormat var joinable: Boolean = false, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/OneDeepEntityPage.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/OneDeepEntityPage.kt index 0e47cae5..0c642543 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/OneDeepEntityPage.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/OneDeepEntityPage.kt @@ -577,11 +577,11 @@ open class OneDeepEntityPage( } @PostMapping("/create") - fun create(@ModelAttribute(binding = false) dto: T, - @RequestParam(required = false) file0: MultipartFile?, - @RequestParam(required = false) file1: MultipartFile?, - model: Model, - auth: Authentication, + open fun create(@ModelAttribute(binding = false) dto: T, + @RequestParam(required = false) file0: MultipartFile?, + @RequestParam(required = false) file1: MultipartFile?, + model: Model, + auth: Authentication, ): String { val user = auth.getUser() adminMenuService.addPartsForMenu(user, model) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/TwoDeepEntityPage.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/TwoDeepEntityPage.kt index f6320b73..a0bd35ed 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/TwoDeepEntityPage.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/controller/admin/TwoDeepEntityPage.kt @@ -159,7 +159,7 @@ abstract class TwoDeepEntityPage Date: Mon, 23 Jun 2025 21:18:36 +0200 Subject: [PATCH 35/89] WIP joining to tournaments --- .../tournament/components/Tournament.tsx | 97 ++++++++++++------- .../src/pages/tournament/tournament.page.tsx | 13 +-- frontend/src/util/views/tournament.view.ts | 18 ++++ 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 0ac3a3d6..7acabd76 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -1,15 +1,38 @@ -import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; -import {Tab, TabList, TabPanel, TabPanels, Tabs} from "@chakra-ui/react"; +import { + TournamentDetailsView, + TournamentResponseMessages, + TournamentResponses +} from '../../../util/views/tournament.view.ts' +import { Flex, Heading, HStack, Tab, TabList, TabPanel, TabPanels, Tabs, Text, useToast } from '@chakra-ui/react' import KnockoutStage from "./KnockoutStage.tsx"; import {useState} from "react"; +import { useConfigContext } from '../../../api/contexts/config/ConfigContext.tsx' +import { ComponentUnavailable } from '../../../common-components/ComponentUnavailable.tsx' +import { Helmet } from 'react-helmet-async' +import { CmschPage } from '../../../common-components/layout/CmschPage.tsx' interface TournamentProps { - tournament: TournamentDetailsView + tournament: TournamentDetailsView, + refetch?: () => void } -const Tournament = ({tournament}: TournamentProps) => { +const Tournament = ({tournament, refetch = () => {}}: TournamentProps) => { + const toast = useToast() + const { components } = useConfigContext() + const userRole = useConfigContext().role + const tournamentComponent = components.tournament + if (!tournamentComponent) return + + const actionResponseCallback = (response: TournamentResponses) => { + if (response == TournamentResponses.OK) { + toast({ status: 'success', title: TournamentResponseMessages[response] }) + refetch() + } else { + toast({ status: 'error', title: TournamentResponseMessages[response] }) + } + } const [tabIndex, setTabIndex] = useState(0) @@ -18,39 +41,45 @@ const Tournament = ({tournament}: TournamentProps) => { } return ( -
-

{tournament.tournament.title}

-

{tournament.tournament.description}

-

{tournament.tournament.location}

- - - Résztvevők - { - tournament.stages.map((stage) => ( - {stage.name} - )) - } - - - + + + + {tournament.tournament.title} + {tournament.tournament.description} + {tournament.tournament.location} + + + + + + Résztvevők + { + tournament.stages.map((stage) => ( + {stage.name} + )) + } + + + + { + tournament.tournament.participants.map((participant) => ( + + {participant.teamName} + + )) + } + { - tournament.tournament.participants.map((participant) => ( -
-

{participant.teamName}

-
+ tournament.stages.map((stage) => ( + + + )) } -
- { - tournament.stages.map((stage) => ( - - - - )) - } -
-
-
+ + +
+ ) } diff --git a/frontend/src/pages/tournament/tournament.page.tsx b/frontend/src/pages/tournament/tournament.page.tsx index a9f7c319..e484f1b0 100644 --- a/frontend/src/pages/tournament/tournament.page.tsx +++ b/frontend/src/pages/tournament/tournament.page.tsx @@ -2,23 +2,16 @@ import {useTournamentQuery} from "../../api/hooks/tournament/useTournamentQuery. import {useParams} from "react-router-dom"; import {toInteger} from "lodash"; import {PageStatus} from "../../common-components/PageStatus.tsx"; -import {CmschPage} from "../../common-components/layout/CmschPage.tsx"; import Tournament from "./components/Tournament.tsx"; -import {Helmet} from "react-helmet-async"; const TournamentPage = () => { const { id } = useParams() - const { isLoading, isError, data } = useTournamentQuery(toInteger(id) || 0) + const { data, isLoading, error, refetch } = useTournamentQuery(toInteger(id) || 0) - if (isError || isLoading || !data) return + if (error || isLoading || !data) return - return ( - - - - - ) + return } export default TournamentPage diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 4e626b8b..6a2fc198 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -19,6 +19,24 @@ export type ParticipantView = { teamName: string } +export enum TournamentResponses { + OK = 'OK', + JOINING_DISABLED = 'JOINING_DISABLED', + ALREADY_JOINED = 'ALREADY_JOINED', + NOT_JOINABLE = 'NOT_JOINABLE', + INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', + ERROR = 'ERROR' +} + +export const TournamentResponseMessages: Record = { + [TournamentResponses.OK]: 'Sikeresen csatlakoztál a versenyhez.', + [TournamentResponses.JOINING_DISABLED]: 'A versenyhez való csatlakozás jelenleg le van tiltva.', + [TournamentResponses.ALREADY_JOINED]: 'Már csatlakoztál ehhez a versenyhez.', + [TournamentResponses.NOT_JOINABLE]: 'A versenyhez való csatlakozás nem lehetséges.', + [TournamentResponses.INSUFFICIENT_PERMISSIONS]: 'Nincs elég jogosultságod ehhez a művelethez.', + [TournamentResponses.ERROR]: 'Hiba történt a művelet végrehajtása során.' +} + export enum StageStatus { CREATED= 'CREATED', DRAFT = 'DRAFT', From 1f2e17790b32b87ebcfe93f11cb2db96b559a2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 23 Jun 2025 22:37:45 +0200 Subject: [PATCH 36/89] WIP joining to tournaments - backend support for user/group ownership mode --- .../tournament/TournamentApiController.kt | 51 ++++---------- .../tournament/TournamentJoinStatus.kt | 11 +++ .../component/tournament/TournamentService.kt | 67 ++++++++++++------- .../sch/cmsch/config/StartupPropertyConfig.kt | 1 + .../config/application-env.properties | 1 + .../resources/config/application.properties | 1 + 6 files changed, 71 insertions(+), 61 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinStatus.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 4323ecbd..99feabb5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -3,14 +3,18 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.annotation.JsonView import hu.bme.sch.cmsch.component.login.CmschUser import hu.bme.sch.cmsch.component.team.TeamService +import hu.bme.sch.cmsch.config.OwnershipType +import hu.bme.sch.cmsch.config.StartupPropertyConfig import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.RoleType import hu.bme.sch.cmsch.repository.GroupRepository +import hu.bme.sch.cmsch.util.getUserOrNull import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.http.ResponseEntity +import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -23,6 +27,7 @@ import kotlin.jvm.optionals.getOrNull @ConditionalOnBean(TournamentComponent::class) class TournamentApiController( private val tournamentComponent: TournamentComponent, + private val startupPropertyConfig: StartupPropertyConfig, private val tournamentService: TournamentService, private val stageService: KnockoutStageService, private val groupRepository: GroupRepository, @@ -98,47 +103,17 @@ class TournamentApiController( ) })) } - @PostMapping("/tournament/{tournamentId}/register/{teamId}") - @Operation( - summary = "Register a team for a tournament.", - ) - @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Team registered successfully"), - ApiResponse(responseCode = "401", description = "Not authorized, user must be group admin of the team"), - ApiResponse(responseCode = "404", description = "Tournament or team not found"), - ApiResponse(responseCode = "400", description = "Bad request, team already registered") - ]) + @PostMapping("/tournament/{tournamentId}/register") fun registerTeam( @PathVariable tournamentId: Int, - @PathVariable teamId: Int, - user: CmschUser - ): ResponseEntity { - val team = groupRepository.findById(teamId).getOrNull() - if (team == null) { - return ResponseEntity.notFound().build() - } - val tournament = tournamentService.findById(tournamentId).getOrNull() - if (tournament == null) { - return ResponseEntity.notFound().build() - } - - if( tournamentService.isTeamRegistered(tournamentId, teamId)) { - return ResponseEntity.badRequest().build() - } - - if (user.groupId != team.id || user.role.value < RoleType.PRIVILEGED.value) { - return ResponseEntity.status(401).build() - } - - if (!tournament.joinable) { - return ResponseEntity.badRequest().body("Tournament is not joinable") - } + auth: Authentication? + ): TournamentJoinStatus { + val user = auth?.getUserOrNull() + ?: return TournamentJoinStatus.INSUFFICIENT_PERMISSIONS - val result = tournamentService.teamRegister(tournamentId, teamId, team.name) - return if (result) { - ResponseEntity.ok().build() - } else { - ResponseEntity.badRequest().body("Failed to register team") + return when (startupPropertyConfig.tournamentOwnershipMode){ + OwnershipType.GROUP -> tournamentService.teamRegister(tournamentId, user) + OwnershipType.USER -> tournamentService.userRegister(tournamentId, user) } } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinStatus.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinStatus.kt new file mode 100644 index 00000000..8973c292 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinStatus.kt @@ -0,0 +1,11 @@ +package hu.bme.sch.cmsch.component.tournament + +enum class TournamentJoinStatus { + OK, + JOINING_DISABLED, + ALREADY_JOINED, + TOURNAMENT_NOT_FOUND, + NOT_JOINABLE, + INSUFFICIENT_PERMISSIONS, + ERROR +} \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 832f94f2..0d13078b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -1,12 +1,18 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.databind.ObjectMapper +import hu.bme.sch.cmsch.component.login.CmschUser import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional +import java.sql.SQLException import java.util.* +import kotlin.jvm.optionals.getOrNull @Service @ConditionalOnBean(TournamentComponent::class) @@ -66,37 +72,52 @@ open class TournamentService( ) } - fun isTeamRegistered(tournamentId: Int, teamId: Int): Boolean { - val tournament = tournamentRepository.findById(tournamentId) - if (tournament.isEmpty) { - return false + @Retryable(value = [ SQLException::class ], maxAttempts = 5, backoff = Backoff(delay = 500L, multiplier = 1.5)) + @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) + fun teamRegister(tournamentId: Int, user: CmschUser): TournamentJoinStatus { + val tournament = tournamentRepository.findById(tournamentId).getOrNull() + ?: return TournamentJoinStatus.TOURNAMENT_NOT_FOUND + + val groupId = user.groupId + ?: return TournamentJoinStatus.INSUFFICIENT_PERMISSIONS + val team = groupRepository.findById(groupId).getOrNull() + ?: return TournamentJoinStatus.INSUFFICIENT_PERMISSIONS + + val participants = tournament.participants + val parsed = mutableListOf() + parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) + if (parsed.any { it.teamId == groupId }) { + return TournamentJoinStatus.ALREADY_JOINED } - val participants = tournament.get().participants - return participants.split("\n").any { it.contains("\"teamId\":$teamId") } + + parsed.add(ParticipantDto(groupId, team.name)) + + tournament.participants = parsed.joinToString("\n") { objectMapper.writeValueAsString(it) } + tournament.participantCount = parsed.size + + tournamentRepository.save(tournament) + return TournamentJoinStatus.OK } + @Retryable(value = [ SQLException::class ], maxAttempts = 5, backoff = Backoff(delay = 500L, multiplier = 1.5)) + @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) + fun userRegister(tournamentId: Int, user: CmschUser): TournamentJoinStatus { + val tournament = tournamentRepository.findById(tournamentId).getOrNull() + ?: return TournamentJoinStatus.TOURNAMENT_NOT_FOUND - @Transactional - fun teamRegister(tournamentId: Int, teamId: Int, teamName: String): Boolean { - val tournament = tournamentRepository.findById(tournamentId) - if (tournament.isEmpty) { - return false - } - val group = groupRepository.findById(teamId) - if (group.isEmpty) { - return false - } - val participants = tournament.get().participants + val participants = tournament.participants val parsed = mutableListOf() parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) + if (parsed.any { it.teamId == user.id }) { + return TournamentJoinStatus.ALREADY_JOINED + } + parsed.add(ParticipantDto(user.id, user.userName)) - parsed.add(ParticipantDto(teamId, teamName)) - - tournament.get().participants = parsed.joinToString("\n") { objectMapper.writeValueAsString(it) } - tournament.get().participantCount = parsed.size + tournament.participants = parsed.joinToString("\n") { objectMapper.writeValueAsString(it) } + tournament.participantCount = parsed.size - tournamentRepository.save(tournament.get()) - return true + tournamentRepository.save(tournament) + return TournamentJoinStatus.OK } companion object{ diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt index 10046bb7..9913a9ec 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/StartupPropertyConfig.kt @@ -29,6 +29,7 @@ data class StartupPropertyConfig @ConstructorBinding constructor( val tokenOwnershipMode: OwnershipType, val challengeOwnershipMode: OwnershipType, val raceOwnershipMode: OwnershipType, + val tournamentOwnershipMode: OwnershipType, // Increased session val increasedSessionTime: Int, diff --git a/backend/src/main/resources/config/application-env.properties b/backend/src/main/resources/config/application-env.properties index 4308cc1e..b68d77c9 100644 --- a/backend/src/main/resources/config/application-env.properties +++ b/backend/src/main/resources/config/application-env.properties @@ -93,6 +93,7 @@ hu.bme.sch.cmsch.startup.riddle-ownership-mode=${OWNER_RIDDLE:USER} hu.bme.sch.cmsch.startup.form-ownership-mode=${OWNER_FORM:USER} hu.bme.sch.cmsch.startup.challenge-ownership-mode=${OWNER_CHALLENGE:USER} hu.bme.sch.cmsch.startup.race-ownership-mode=${OWNER_RACE:USER} +hu.bme.sch.cmsch.startup.tournament-ownership-mode=${OWNER_TOURNAMENT:USER} hu.bme.sch.cmsch.startup.master-role=${MS_MASTER_ROLE:true} hu.bme.sch.cmsch.startup.riddle-microservice-enabled=${RIDDLE_MICROSERVICE:false} diff --git a/backend/src/main/resources/config/application.properties b/backend/src/main/resources/config/application.properties index e3261be9..2a87f7d9 100644 --- a/backend/src/main/resources/config/application.properties +++ b/backend/src/main/resources/config/application.properties @@ -167,6 +167,7 @@ hu.bme.sch.cmsch.startup.task-ownership-mode=USER hu.bme.sch.cmsch.startup.riddle-ownership-mode=USER hu.bme.sch.cmsch.startup.challenge-ownership-mode=GROUP hu.bme.sch.cmsch.startup.race-ownership-mode=USER +hu.bme.sch.cmsch.startup.tournament-ownership-mode=GROUP spring.jackson.serialization.indent-output=false From c7f2c8faefeaf4d6abe811503177a62ae5c61d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 23 Jun 2025 23:59:13 +0200 Subject: [PATCH 37/89] tournaments - access control --- .../tournament/TournamentApiController.kt | 47 ++++++----- .../tournament/TournamentComponent.kt | 5 ++ .../tournament/TournamentDetailedView.kt | 6 ++ .../component/tournament/TournamentEntity.kt | 9 ++- .../component/tournament/TournamentJoinDto.kt | 5 ++ .../component/tournament/TournamentService.kt | 78 ++++++++++++++++++- 6 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinDto.kt diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 99feabb5..4193a0eb 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -13,11 +13,13 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import kotlin.jvm.optionals.getOrNull @@ -40,17 +42,15 @@ class TournamentApiController( @ApiResponses(value = [ ApiResponse(responseCode = "200", description = "List of tournaments") ]) - fun tournaments(): ResponseEntity> { - val tournaments = tournamentService.findAll() - return ResponseEntity.ok(tournaments.map { - TournamentPreviewView( - it.id, - it.title, - it.description, - it.location, - it.status - ) - }) + fun listTournaments(auth: Authentication?): ResponseEntity> { + val user = auth?.getUserOrNull() + if (!tournamentComponent.minRole.isAvailableForRole(user?.role ?: RoleType.GUEST)) + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(listOf()) + if (!tournamentComponent.showTournamentsAtAll.isValueTrue()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build() + } + + return ResponseEntity.ok(tournamentService.listAllTournaments()) } @@ -63,9 +63,17 @@ class TournamentApiController( ApiResponse(responseCode = "404", description = "Tournament not found") ]) fun tournamentDetails( - @PathVariable tournamentId: Int - ): ResponseEntity{ - val tournament = tournamentService.findById(tournamentId) + @PathVariable tournamentId: Int, + auth: Authentication? + ): ResponseEntity{ + val user = auth?.getUserOrNull() + if (!tournamentComponent.minRole.isAvailableForRole(user?.role ?: RoleType.GUEST)) + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + + return tournamentService.showTournament(tournamentId, user)?.let { ResponseEntity.ok(it) } + ?: ResponseEntity.status(HttpStatus.NOT_FOUND).build() + + /*val tournament = tournamentService.findById(tournamentId) if (tournament.isEmpty) { return ResponseEntity.notFound().build() } @@ -76,6 +84,7 @@ class TournamentApiController( tournament.get().title, tournament.get().description, tournament.get().location, + tournament.get().joinable, tournament.get().participantCount, tournamentService.getParticipants(tournamentId), tournament.get().status @@ -100,20 +109,20 @@ class TournamentApiController( it.awayTeamScore, it.status ) } - ) })) + ) }))*/ } - @PostMapping("/tournament/{tournamentId}/register") + @PostMapping("/tournament/register") fun registerTeam( - @PathVariable tournamentId: Int, + @RequestBody tournamentJoinDto: TournamentJoinDto, auth: Authentication? ): TournamentJoinStatus { val user = auth?.getUserOrNull() ?: return TournamentJoinStatus.INSUFFICIENT_PERMISSIONS return when (startupPropertyConfig.tournamentOwnershipMode){ - OwnershipType.GROUP -> tournamentService.teamRegister(tournamentId, user) - OwnershipType.USER -> tournamentService.userRegister(tournamentId, user) + OwnershipType.GROUP -> tournamentService.teamRegister(tournamentJoinDto.id, user) + OwnershipType.USER -> tournamentService.userRegister(tournamentJoinDto.id, user) } } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt index bf69c6e8..dbe4a2a3 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentComponent.kt @@ -50,4 +50,9 @@ class TournamentComponent ( "minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.ADMIN, fieldName = "Minimum jogosultság", description = "A komponens eléréséhez szükséges minimum jogosultság" ) + + val showTournamentsAtAll = SettingProxy(componentSettingService, component, + "showTournamentsAtAll", "true", type = SettingType.BOOLEAN, serverSideOnly = true, + fieldName = "Versenylista megjelenítése", description = "Ha ki van kapcsolva, akkor a versenylista nincsen leküldve", + ) } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt index d551000e..e9d87652 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -5,6 +5,7 @@ data class TournamentWithParticipants( val title: String, val description: String, val location: String, + val joinable: Boolean, val participantCount: Int, val participants: List, val status: Int, @@ -39,4 +40,9 @@ data class KnockoutStageDetailedView( data class TournamentDetailedView( val tournament: TournamentWithParticipants, val stages: List, +) + +data class OptionalTournamentView ( + val visible: Boolean, + val tournament: TournamentDetailedView? ) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt index 8beecac5..814fd8a0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentEntity.kt @@ -55,10 +55,17 @@ data class TournamentEntity( @property:ImportFormat var joinable: Boolean = false, + @Column(nullable = false) + @field:JsonView(value = [ Edit::class ]) + @property:GenerateInput(type = InputType.SWITCH, order = 6, label = "Látható") + @property:GenerateOverview(columnName = "Visible", order = 5) + @property:ImportFormat + var visible: Boolean = false, + @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateInput(type = InputType.HIDDEN, visible = true, ignore = true) - @property:GenerateOverview(columnName = "Résztvevők száma", order = 5) + @property:GenerateOverview(columnName = "Résztvevők száma", order = 6) @property:ImportFormat var participantCount: Int = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinDto.kt new file mode 100644 index 00000000..119f0f60 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentJoinDto.kt @@ -0,0 +1,5 @@ +package hu.bme.sch.cmsch.component.tournament + +data class TournamentJoinDto ( + var id: Int = 0 +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 0d13078b..b8f145b0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -2,9 +2,11 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.component.login.CmschUser +import hu.bme.sch.cmsch.model.RoleType import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext +import org.springframework.http.ResponseEntity import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Retryable import org.springframework.stereotype.Service @@ -21,7 +23,8 @@ open class TournamentService( private val stageRepository: KnockoutStageRepository, private val groupRepository: GroupRepository, private val tournamentComponent: TournamentComponent, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, + private val stageService: KnockoutStageService ) { @Transactional(readOnly = true) open fun findAll(): List { @@ -33,6 +36,79 @@ open class TournamentService( return tournamentRepository.findById(tournamentId) } + + @Transactional(readOnly = true) + fun listAllTournaments(): List { + if (!tournamentComponent.showTournamentsAtAll.isValueTrue()) { + return listOf() + } + + var tournaments = tournamentRepository.findAll() + .filter { it.visible }.map { + TournamentPreviewView( + it.id, + it.title, + it.description, + it.location, + it.status + ) + } + return tournaments + } + + + @Transactional(readOnly = true) + fun showTournament(tournamentId: Int, user: CmschUser?): OptionalTournamentView? { + val tournament = tournamentRepository.findById(tournamentId).getOrNull() + ?: return null + + return if (tournament.visible && tournamentComponent.minRole.isAvailableForRole(user?.role ?: RoleType.GUEST)){ + OptionalTournamentView(true, mapTournament(tournament, user)) + } else { + OptionalTournamentView(false, null) + } + } + + private fun mapTournament(tournament: TournamentEntity, user: CmschUser?): TournamentDetailedView { + val joinEnabled = tournament.joinable && (user?.role ?: RoleType.GUEST) >= RoleType.PRIVILEGED + val participants = tournament.participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } + val stages = stageRepository.findAllByTournamentId(tournament.id) + .sortedBy { it.level } + + return TournamentDetailedView( + TournamentWithParticipants( + tournament.id, + tournament.title, + tournament.description, + tournament.location, + tournament.joinable, + tournament.participantCount, + getParticipants(tournament.id), + tournament.status + ), stages.map { KnockoutStageDetailedView( + it.id, + it.name, + it.level, + it.participantCount, + it.nextRound, + it.status, + stageService.findMatchesByStageId(it.id).map { MatchDto( + it.id, + it.gameId, + it.kickoffTime, + it.level, + it.location, + it.homeSeed, + it.awaySeed, + if(it.homeTeamId!=null) ParticipantDto(it.homeTeamId!!, it.homeTeamName) else null, + if(it.awayTeamId!=null) ParticipantDto(it.awayTeamId!!, it.awayTeamName) else null, + it.homeTeamScore, + it.awayTeamScore, + it.status + ) } + ) }) + } + @Transactional(readOnly = true) fun getParticipants(tournamentId: Int): List { val tournament = tournamentRepository.findById(tournamentId) From 4560314ba580989060851762564b07e2dae4e92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Tue, 24 Jun 2025 00:19:45 +0200 Subject: [PATCH 38/89] tournaments - access control --- .../tournament/TournamentApiController.kt | 38 ------------------- .../tournament/TournamentDetailedView.kt | 3 +- .../component/tournament/TournamentService.kt | 19 ++++++++-- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt index 4193a0eb..334b6f81 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentApiController.kt @@ -72,44 +72,6 @@ class TournamentApiController( return tournamentService.showTournament(tournamentId, user)?.let { ResponseEntity.ok(it) } ?: ResponseEntity.status(HttpStatus.NOT_FOUND).build() - - /*val tournament = tournamentService.findById(tournamentId) - if (tournament.isEmpty) { - return ResponseEntity.notFound().build() - } - val stages = stageService.findStagesByTournamentId(tournamentId) - return ResponseEntity.ok(TournamentDetailedView( - TournamentWithParticipants( - tournament.get().id, - tournament.get().title, - tournament.get().description, - tournament.get().location, - tournament.get().joinable, - tournament.get().participantCount, - tournamentService.getParticipants(tournamentId), - tournament.get().status - ), stages.map { KnockoutStageDetailedView( - it.id, - it.name, - it.level, - it.participantCount, - it.nextRound, - it.status, - stageService.findMatchesByStageId(it.id).map { MatchDto( - it.id, - it.gameId, - it.kickoffTime, - it.level, - it.location, - it.homeSeed, - it.awaySeed, - if(it.homeTeamId!=null) ParticipantDto(it.homeTeamId!!, it.homeTeamName) else null, - if(it.awayTeamId!=null) ParticipantDto(it.awayTeamId!!, it.awayTeamName) else null, - it.homeTeamScore, - it.awayTeamScore, - it.status - ) } - ) }))*/ } @PostMapping("/tournament/register") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt index e9d87652..9f07eae6 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentDetailedView.kt @@ -5,7 +5,8 @@ data class TournamentWithParticipants( val title: String, val description: String, val location: String, - val joinable: Boolean, + val joinEnabled: Boolean, + val isJoined: Boolean, val participantCount: Int, val participants: List, val status: Int, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index b8f145b0..e8a72f50 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -2,6 +2,8 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.component.login.CmschUser +import hu.bme.sch.cmsch.config.OwnershipType +import hu.bme.sch.cmsch.config.StartupPropertyConfig import hu.bme.sch.cmsch.model.RoleType import hu.bme.sch.cmsch.repository.GroupRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnBean @@ -24,7 +26,8 @@ open class TournamentService( private val groupRepository: GroupRepository, private val tournamentComponent: TournamentComponent, private val objectMapper: ObjectMapper, - private val stageService: KnockoutStageService + private val stageService: KnockoutStageService, + private val startupPropertyConfig: StartupPropertyConfig ) { @Transactional(readOnly = true) open fun findAll(): List { @@ -70,8 +73,17 @@ open class TournamentService( } private fun mapTournament(tournament: TournamentEntity, user: CmschUser?): TournamentDetailedView { - val joinEnabled = tournament.joinable && (user?.role ?: RoleType.GUEST) >= RoleType.PRIVILEGED val participants = tournament.participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } + + val playerId = when (startupPropertyConfig.tournamentOwnershipMode){ + OwnershipType.GROUP -> user?.groupId ?: null + OwnershipType.USER -> user?.id ?: null + } + val isJoined = participants.any { it.teamId == playerId } + val joinEnabled = tournament.joinable && !isJoined && + ((user?.role ?: RoleType.GUEST) >= RoleType.PRIVILEGED || + (startupPropertyConfig.tournamentOwnershipMode == OwnershipType.USER && (user?.role ?: RoleType.GUEST) >= RoleType.BASIC)) + val stages = stageRepository.findAllByTournamentId(tournament.id) .sortedBy { it.level } @@ -81,7 +93,8 @@ open class TournamentService( tournament.title, tournament.description, tournament.location, - tournament.joinable, + joinEnabled, + isJoined, tournament.participantCount, getParticipants(tournament.id), tournament.status From ab5b34009233241c4b8aa1351394d4106b432651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Tue, 24 Jun 2025 00:30:27 +0200 Subject: [PATCH 39/89] dependency cycles removed --- .../tournament/KnockoutStageEntity.kt | 1 - .../tournament/KnockoutStageService.kt | 43 +++++++++---------- .../tournament/TournamentMatchController.kt | 4 +- .../component/tournament/TournamentService.kt | 35 ++++++++------- 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index 705468e0..a1fa1dbf 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -89,7 +89,6 @@ data class KnockoutStageEntity( fun rounds() = ceil(log2(participantCount.toDouble())).toInt() fun matches() = participantCount - 1 fun getStageService() = KnockoutStageService.getBean() - fun tournament(): TournamentEntity = getStageService().getTournamentService().findById(tournamentId).orElse(null) override fun getEntityConfig(env: Environment) = EntityConfig( name = "KnockoutStage", diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 070201dc..60f01615 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -14,7 +14,6 @@ import kotlin.random.Random @Service @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( - private val tournamentService: TournamentService, private val stageRepository: KnockoutStageRepository, private val matchRepository: TournamentMatchRepository, private val objectMapper: ObjectMapper @@ -64,7 +63,7 @@ class KnockoutStageService( @Transactional fun getTeamsForStage(stage: KnockoutStageEntity){ val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() - var participants = tournamentService.getResultsFromLevel(stage.tournamentId, stage.level - 1) + var participants = getResultsFromLevel(stage.tournamentId, stage.level - 1) if (participants.size >= stage.participantCount) { participants = participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) } } @@ -75,6 +74,26 @@ class KnockoutStageService( stageRepository.save(stage) } + @Transactional(readOnly = true) + fun getResultsFromLevel(tournamentId: Int, level: Int): List { + if (level < 1) { + return getParticipants(tournamentId).map { StageResultDto(it.teamId, it.teamName) } + } + val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) + if (stages.isEmpty()) { + return emptyList() + } + return stages.flatMap { it.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } }.sortedWith( + compareBy( + { it.position }, + { it.points }, + { it.won }, + { it.goalDifference }, + { it.goalsFor } + ) + ) + } + @Transactional fun calculateTeamsFromSeeds(stage: KnockoutStageEntity) { val matches = matchRepository.findAllByStageId(stage.id) @@ -90,8 +109,6 @@ class KnockoutStageService( } } - fun getTournamentService(): TournamentService = tournamentService - fun findById(id: Int): KnockoutStageEntity? { //return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } return stageRepository.findById(id).getOrNull() @@ -115,24 +132,6 @@ class KnockoutStageService( return matches } - fun getAggregatedMatchesByTournamentId(): List { - val tournaments = tournamentService.findAll().associateBy { it.id } - val stages = stageRepository.findAll().associateBy { it.id } - val aggregatedByStageId = matchRepository.findAllAggregated() - val aggregated = mutableMapOf() - for(aggregatedStage in aggregatedByStageId) { - aggregated[stages[aggregatedStage.stageId]!!.tournamentId] = aggregated.getOrDefault(stages[aggregatedStage.stageId]!!.tournamentId, 0) + aggregatedStage.matchCount - } - return aggregated.map { - MatchGroupDto( - it.key, - tournaments[it.key]?.title ?:"", - tournaments[it.key]?.location ?:"", - it.value.toInt() - ) - }.sortedByDescending { it.matchCount } - } - @Transactional fun deleteMatchesForStage(stage: KnockoutStageEntity) { matchRepository.deleteAllByStageId(stage.id) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt index de1eda22..81cbf92b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentMatchController.kt @@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping @ConditionalOnBean(TournamentComponent::class) class TournamentMatchController( private val matchRepository: TournamentMatchRepository, - private val tournamentRepository: TournamentRepository, + private val tournamentService: TournamentService, private val stageService: KnockoutStageService, importService: ImportService, adminMenuService: AdminMenuService, @@ -38,7 +38,7 @@ class TournamentMatchController( transactionManager, object : ManualRepository() { override fun findAll(): Iterable { - return stageService.getAggregatedMatchesByTournamentId() + return tournamentService.getAggregatedMatchesByTournamentId() } }, matchRepository, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index e8a72f50..827b29b7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -27,7 +27,8 @@ open class TournamentService( private val tournamentComponent: TournamentComponent, private val objectMapper: ObjectMapper, private val stageService: KnockoutStageService, - private val startupPropertyConfig: StartupPropertyConfig + private val startupPropertyConfig: StartupPropertyConfig, + private val matchRepository: TournamentMatchRepository ) { @Transactional(readOnly = true) open fun findAll(): List { @@ -140,27 +141,25 @@ open class TournamentService( return stage.get().participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } } - - @Transactional(readOnly = true) - fun getResultsFromLevel(tournamentId: Int, level: Int): List { - if (level < 1) { - return getParticipants(tournamentId).map { StageResultDto(it.teamId, it.teamName) } + fun getAggregatedMatchesByTournamentId(): List { + val tournaments = findAll().associateBy { it.id } + val stages = stageRepository.findAll().associateBy { it.id } + val aggregatedByStageId = matchRepository.findAllAggregated() + val aggregated = mutableMapOf() + for(aggregatedStage in aggregatedByStageId) { + aggregated[stages[aggregatedStage.stageId]!!.tournamentId] = aggregated.getOrDefault(stages[aggregatedStage.stageId]!!.tournamentId, 0) + aggregatedStage.matchCount } - val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) - if (stages.isEmpty()) { - return emptyList() - } - return stages.flatMap { it.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } }.sortedWith( - compareBy( - { it.position }, - { it.points }, - { it.won }, - { it.goalDifference }, - { it.goalsFor } + return aggregated.map { + MatchGroupDto( + it.key, + tournaments[it.key]?.title ?:"", + tournaments[it.key]?.location ?:"", + it.value.toInt() ) - ) + }.sortedByDescending { it.matchCount } } + @Retryable(value = [ SQLException::class ], maxAttempts = 5, backoff = Backoff(delay = 500L, multiplier = 1.5)) @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) fun teamRegister(tournamentId: Int, user: CmschUser): TournamentJoinStatus { From 6cd66a51173edee2cc2bb89f8b74e3d8fa6f3084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Tue, 24 Jun 2025 00:49:51 +0200 Subject: [PATCH 40/89] frontend support for joining tournaments --- .../tournament/actions/useTournamentJoin.ts | 26 ++++++++++ .../{ => queries}/useTournamentListQuery.ts | 6 +-- .../tournament/queries/useTournamentQuery.ts | 17 ++++++ .../hooks/tournament/useTournamentQuery.ts | 17 ------ .../tournament/components/Tournament.tsx | 52 ++++++++++++++++--- .../src/pages/tournament/tournament.page.tsx | 5 +- .../pages/tournament/tournamentList.page.tsx | 2 +- frontend/src/util/views/tournament.view.ts | 10 ++++ 8 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 frontend/src/api/hooks/tournament/actions/useTournamentJoin.ts rename frontend/src/api/hooks/tournament/{ => queries}/useTournamentListQuery.ts (66%) create mode 100644 frontend/src/api/hooks/tournament/queries/useTournamentQuery.ts delete mode 100644 frontend/src/api/hooks/tournament/useTournamentQuery.ts diff --git a/frontend/src/api/hooks/tournament/actions/useTournamentJoin.ts b/frontend/src/api/hooks/tournament/actions/useTournamentJoin.ts new file mode 100644 index 00000000..4d24f54e --- /dev/null +++ b/frontend/src/api/hooks/tournament/actions/useTournamentJoin.ts @@ -0,0 +1,26 @@ +import axios from 'axios' +import { TournamentResponses } from '../../../../util/views/tournament.view' +import { useState } from 'react' + +export const useTournamentJoin = (onResponse: (response: TournamentResponses) => void) => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState() + + const joinTournament = (id: number) => { + setLoading(true) + axios + .post(`/api/tournament/register`, { id }) + .then((res) => { + onResponse(res.data) + }) + .catch((err) => { + setError(err) + onResponse(TournamentResponses.ERROR) + }) + .finally(() => { + setLoading(false) + }) + } + + return { joinTournamentLoading: loading, joinTournamentError: error, joinTournament } +} diff --git a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts b/frontend/src/api/hooks/tournament/queries/useTournamentListQuery.ts similarity index 66% rename from frontend/src/api/hooks/tournament/useTournamentListQuery.ts rename to frontend/src/api/hooks/tournament/queries/useTournamentListQuery.ts index 861bd23f..573fc6f1 100644 --- a/frontend/src/api/hooks/tournament/useTournamentListQuery.ts +++ b/frontend/src/api/hooks/tournament/queries/useTournamentListQuery.ts @@ -1,8 +1,8 @@ -import { TournamentPreview } from '../../../util/views/tournament.view.ts' +import { TournamentPreview } from '../../../../util/views/tournament.view.ts' import { useQuery } from '@tanstack/react-query' -import { QueryKeys } from '../queryKeys.ts' +import { QueryKeys } from '../../queryKeys.ts' import axios from 'axios' -import { ApiPaths } from '../../../util/paths.ts' +import { ApiPaths } from '../../../../util/paths.ts' export const useTournamentListQuery = () => { diff --git a/frontend/src/api/hooks/tournament/queries/useTournamentQuery.ts b/frontend/src/api/hooks/tournament/queries/useTournamentQuery.ts new file mode 100644 index 00000000..fc5908a3 --- /dev/null +++ b/frontend/src/api/hooks/tournament/queries/useTournamentQuery.ts @@ -0,0 +1,17 @@ +import {useQuery} from "@tanstack/react-query"; +import { OptionalTournamentView } from '../../../../util/views/tournament.view.ts' +import {QueryKeys} from "../../queryKeys.ts"; +import axios from "axios"; +import {joinPath} from "../../../../util/core-functions.util.ts"; +import {ApiPaths} from "../../../../util/paths.ts"; + + +export const useTournamentQuery = (id: number) => { + return useQuery({ + queryKey: [QueryKeys.TOURNAMENTS, id], + queryFn: async () => { + const response = await axios.get(joinPath(ApiPaths.TOURNAMENTS, id)) + return response.data + } + }) +} diff --git a/frontend/src/api/hooks/tournament/useTournamentQuery.ts b/frontend/src/api/hooks/tournament/useTournamentQuery.ts deleted file mode 100644 index 53ac297c..00000000 --- a/frontend/src/api/hooks/tournament/useTournamentQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {useQuery} from "@tanstack/react-query"; -import {TournamentDetailsView} from "../../../util/views/tournament.view.ts"; -import {QueryKeys} from "../queryKeys.ts"; -import axios from "axios"; -import {joinPath} from "../../../util/core-functions.util.ts"; -import {ApiPaths} from "../../../util/paths.ts"; - - -export const useTournamentQuery = (id: number) => { - return useQuery({ - queryKey: [QueryKeys.TOURNAMENTS, id], - queryFn: async () => { - const response = await axios.get(joinPath(ApiPaths.TOURNAMENTS, id)) - return response.data - } - }) -} diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 7acabd76..48269c0d 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -3,13 +3,28 @@ import { TournamentResponseMessages, TournamentResponses } from '../../../util/views/tournament.view.ts' -import { Flex, Heading, HStack, Tab, TabList, TabPanel, TabPanels, Tabs, Text, useToast } from '@chakra-ui/react' +import { + Box, + Button, + Flex, + Heading, + HStack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, + useToast +} from '@chakra-ui/react' import KnockoutStage from "./KnockoutStage.tsx"; import {useState} from "react"; import { useConfigContext } from '../../../api/contexts/config/ConfigContext.tsx' import { ComponentUnavailable } from '../../../common-components/ComponentUnavailable.tsx' import { Helmet } from 'react-helmet-async' import { CmschPage } from '../../../common-components/layout/CmschPage.tsx' +import { useTournamentJoin } from '../../../api/hooks/tournament/actions/useTournamentJoin.ts' +import { FaSignInAlt } from 'react-icons/fa' interface TournamentProps { @@ -20,7 +35,6 @@ interface TournamentProps { const Tournament = ({tournament, refetch = () => {}}: TournamentProps) => { const toast = useToast() const { components } = useConfigContext() - const userRole = useConfigContext().role const tournamentComponent = components.tournament if (!tournamentComponent) return @@ -34,6 +48,8 @@ const Tournament = ({tournament, refetch = () => {}}: TournamentProps) => { } } + const { joinTournament, joinTournamentLoading } = useTournamentJoin(actionResponseCallback) + const [tabIndex, setTabIndex] = useState(0) const onTabSelected = (i: number) => { @@ -43,12 +59,32 @@ const Tournament = ({tournament, refetch = () => {}}: TournamentProps) => { return ( - + {tournament.tournament.title} {tournament.tournament.description} {tournament.tournament.location} - + {tournament.tournament.joinEnabled && ( + + )} + {tournament.tournament.isJoined && ( + + )} @@ -63,9 +99,11 @@ const Tournament = ({tournament, refetch = () => {}}: TournamentProps) => { { tournament.tournament.participants.map((participant) => ( - - {participant.teamName} - + + + {participant.teamName} + + )) } diff --git a/frontend/src/pages/tournament/tournament.page.tsx b/frontend/src/pages/tournament/tournament.page.tsx index e484f1b0..e6d16be0 100644 --- a/frontend/src/pages/tournament/tournament.page.tsx +++ b/frontend/src/pages/tournament/tournament.page.tsx @@ -1,4 +1,4 @@ -import {useTournamentQuery} from "../../api/hooks/tournament/useTournamentQuery.ts"; +import {useTournamentQuery} from "../../api/hooks/tournament/queries/useTournamentQuery.ts"; import {useParams} from "react-router-dom"; import {toInteger} from "lodash"; import {PageStatus} from "../../common-components/PageStatus.tsx"; @@ -11,7 +11,8 @@ const TournamentPage = () => { if (error || isLoading || !data) return - return + if (!data.tournament) return + return } export default TournamentPage diff --git a/frontend/src/pages/tournament/tournamentList.page.tsx b/frontend/src/pages/tournament/tournamentList.page.tsx index 2fc0bfe5..aece4d58 100644 --- a/frontend/src/pages/tournament/tournamentList.page.tsx +++ b/frontend/src/pages/tournament/tournamentList.page.tsx @@ -1,4 +1,4 @@ -import { useTournamentListQuery } from '../../api/hooks/tournament/useTournamentListQuery.ts' +import { useTournamentListQuery } from '../../api/hooks/tournament/queries/useTournamentListQuery.ts' import { useConfigContext } from '../../api/contexts/config/ConfigContext.tsx' import {Box, Heading, LinkOverlay, VStack} from '@chakra-ui/react' import { TournamentPreview } from '../../util/views/tournament.view.ts' diff --git a/frontend/src/util/views/tournament.view.ts b/frontend/src/util/views/tournament.view.ts index 6a2fc198..6125ef1d 100644 --- a/frontend/src/util/views/tournament.view.ts +++ b/frontend/src/util/views/tournament.view.ts @@ -11,7 +11,10 @@ export type TournamentWithParticipantsView = { title: string description: string location: string + joinEnabled: boolean + isJoined: boolean participants: ParticipantView[] + status: number } export type ParticipantView = { @@ -23,6 +26,7 @@ export enum TournamentResponses { OK = 'OK', JOINING_DISABLED = 'JOINING_DISABLED', ALREADY_JOINED = 'ALREADY_JOINED', + TOURNAMENT_NOT_FOUND = 'TOURNAMENT_NOT_FOUND', NOT_JOINABLE = 'NOT_JOINABLE', INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', ERROR = 'ERROR' @@ -32,6 +36,7 @@ export const TournamentResponseMessages: Record = { [TournamentResponses.OK]: 'Sikeresen csatlakoztál a versenyhez.', [TournamentResponses.JOINING_DISABLED]: 'A versenyhez való csatlakozás jelenleg le van tiltva.', [TournamentResponses.ALREADY_JOINED]: 'Már csatlakoztál ehhez a versenyhez.', + [TournamentResponses.TOURNAMENT_NOT_FOUND]: 'A verseny nem található.', [TournamentResponses.NOT_JOINABLE]: 'A versenyhez való csatlakozás nem lehetséges.', [TournamentResponses.INSUFFICIENT_PERMISSIONS]: 'Nincs elég jogosultságod ehhez a művelethez.', [TournamentResponses.ERROR]: 'Hiba történt a művelet végrehajtása során.' @@ -83,3 +88,8 @@ export type TournamentDetailsView = { stages: TournamentStageView[] } +export type OptionalTournamentView = { + visible: boolean + tournament?: TournamentDetailsView +} + From 2f61ea5134a62ce2636b0719964c48c4126cc7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Fri, 27 Jun 2025 14:46:08 +0200 Subject: [PATCH 41/89] seeds (lots of stuff, wip) --- backend/build.gradle.kts | 1 + .../tournament/KnockoutStageController.kt | 46 +++++ .../tournament/KnockoutStageEntity.kt | 18 ++ .../tournament/KnockoutStageService.kt | 66 +++++--- .../component/tournament/ParticipantDto.kt | 6 + .../component/tournament/StageResultDto.kt | 48 ++++-- .../component/tournament/TournamentService.kt | 9 +- .../hu/bme/sch/cmsch/config/AppConfig.kt | 2 + .../sch/cmsch/service/PermissionsService.kt | 26 +-- .../resources/templates/seedSettings.html | 160 ++++++++++++++++++ .../tournament/components/Tournament.tsx | 1 - 11 files changed, 326 insertions(+), 57 deletions(-) create mode 100644 backend/src/main/resources/templates/seedSettings.html diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index b8225cef..927da1c2 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -73,6 +73,7 @@ dependencies { implementation("org.commonmark:commonmark:0.24.0") implementation("org.commonmark:commonmark-ext-gfm-tables:0.24.0") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") developmentOnly("org.springframework.boot:spring-boot-devtools") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") runtimeOnly("com.h2database:h2") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt index 2779bd8d..96491255 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageController.kt @@ -2,6 +2,7 @@ package hu.bme.sch.cmsch.component.tournament import com.fasterxml.jackson.databind.ObjectMapper import hu.bme.sch.cmsch.controller.admin.ButtonAction +import hu.bme.sch.cmsch.controller.admin.ControlAction import hu.bme.sch.cmsch.controller.admin.TwoDeepEntityPage import hu.bme.sch.cmsch.repository.ManualRepository import hu.bme.sch.cmsch.service.AdminMenuService @@ -33,6 +34,7 @@ import org.springframework.web.multipart.MultipartFile @ConditionalOnBean(TournamentComponent::class) class KnockoutStageController( private val stageRepository: KnockoutStageRepository, + private val stageService: KnockoutStageService, private val tournamentRepository: TournamentRepository, importService: ImportService, adminMenuService: AdminMenuService, @@ -85,6 +87,18 @@ class KnockoutStageController( exportEnabled = false, adminMenuIcon = "lan", + + innerControlActions = mutableListOf( + ControlAction( + name = "Seedek kezelése", + endpoint = "seed/{id}", + icon = "sort_by_alpha", + permission = StaffPermissions.PERMISSION_SHOW_BRACKETS, + order = 200, + newPage = false, + usageString = "A kiesési szakasz seedjeinek kezelése" + ) + ) ) { override fun fetchSublist(id: Int): Iterable { return stageRepository.findAllByTournamentId(id) @@ -212,4 +226,36 @@ class KnockoutStageController( onEntityChanged(entity) return "redirect:/admin/control/$view" } + + + @GetMapping("/seed/{id}") + fun seedPage(model: Model, auth: Authentication, @PathVariable id: Int): String { + val user = auth.getUser() + adminMenuService.addPartsForMenu(user, model) + if(!StaffPermissions.PERMISSION_SHOW_BRACKETS.validate(user) ) { + model.addAttribute("permission", StaffPermissions.PERMISSION_GENERATE_BRACKETS.permissionString) + model.addAttribute("user", user) + auditLog.admin403(user, component.component, "GET /$view/seed/$id", StaffPermissions.PERMISSION_GENERATE_BRACKETS.permissionString) + return "admin403" + } + val stage = stageRepository.findById(id) + ?: return "redirect:/admin/control/$view" + val readOnly = !StaffPermissions.PERMISSION_SET_SEEDS.validate(user) || stage.get().status >= StageStatus.SET + val teams = stageService.getParticipants(id) + val tournament = tournamentRepository.findById(stage.get().tournamentId) + ?: return "redirect:/admin/control/$view" + model.addAttribute("title", "Kiesési szakasz seedek") + model.addAttribute("view", view) + model.addAttribute("readOnly", readOnly) + model.addAttribute("entityMode", false) + model.addAttribute("tournamentTitle", tournament.get().title) + model.addAttribute("stageLevel", stage.get().level) + model.addAttribute("stageTitle", stage.get().name) + model.addAttribute("teams", teams) + + return "seedSettings" + } + + + } \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt index a1fa1dbf..543883fa 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageEntity.kt @@ -9,6 +9,7 @@ import hu.bme.sch.cmsch.dto.Preview import hu.bme.sch.cmsch.model.ManagedEntity import hu.bme.sch.cmsch.service.StaffPermissions import jakarta.persistence.* +import jakarta.transaction.Transactional import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.core.env.Environment import kotlin.math.ceil @@ -72,6 +73,14 @@ data class KnockoutStageEntity( @property:ImportFormat var participants: String = "", + @Column(nullable = false, columnDefinition = "TEXT") + @field:JsonView(value = [ FullDetails::class ]) + @property:GenerateInput(type = InputType.HIDDEN, visible = false, ignore = true) + @property:GenerateOverview(visible = false) + @property:ImportFormat + var seeds: String = "", + + @Column(nullable = false) @field:JsonView(value = [ Preview::class, FullDetails::class ]) @property:GenerateOverview(columnName = "Következő kör", order = 4, centered = true) @@ -115,6 +124,15 @@ data class KnockoutStageEntity( @PrePersist fun prePersist() { getStageService().createMatchesForStage(this) + val teamSeeds = (1..participantCount).asIterable().shuffled().toList() + var participants = getStageService().getResultsForStage(this) + if (participants.size >= participantCount) { + participants = participants.subList(0, participantCount).map { StageResultDto(it.teamId, it.teamName) } + } + for (i in 0 until participantCount) { + participants[i].initialSeed = teamSeeds[i] + } + this.participants = participants.joinToString("\n") { getStageService().objectMapper.writeValueAsString(it) } } @PreRemove diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt index 60f01615..bbe6fb87 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/KnockoutStageService.kt @@ -7,6 +7,7 @@ import org.springframework.context.ApplicationContextAware import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional +import java.util.Optional import kotlin.jvm.optionals.getOrNull import kotlin.math.pow import kotlin.random.Random @@ -15,8 +16,9 @@ import kotlin.random.Random @ConditionalOnBean(TournamentComponent::class) class KnockoutStageService( private val stageRepository: KnockoutStageRepository, + private val tournamentService: TournamentService, private val matchRepository: TournamentMatchRepository, - private val objectMapper: ObjectMapper + val objectMapper: ObjectMapper ): ApplicationContextAware { @Transactional @@ -61,46 +63,58 @@ class KnockoutStageService( } @Transactional - fun getTeamsForStage(stage: KnockoutStageEntity){ + fun updateSeedsForStage(stageId: Int, seeds: List) { + val stage = findById(stageId) ?: throw IllegalArgumentException("No stage found with id $stageId") + if (seeds.size != stage.participantCount) { + + } + + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun transferTeamsForStage(stage: KnockoutStageEntity){ val teamSeeds = (1..stage.participantCount).asIterable().shuffled().toList() - var participants = getResultsFromLevel(stage.tournamentId, stage.level - 1) + var participants = getResultsForStage(stage) if (participants.size >= stage.participantCount) { participants = participants.subList(0, stage.participantCount).map { StageResultDto(it.teamId, it.teamName) } } for (i in 0 until stage.participantCount) { - participants[i].seed = teamSeeds[i] + participants[i].initialSeed = teamSeeds[i] } stage.participants = participants.joinToString("\n") { objectMapper.writeValueAsString(it) } stageRepository.save(stage) } @Transactional(readOnly = true) - fun getResultsFromLevel(tournamentId: Int, level: Int): List { - if (level < 1) { - return getParticipants(tournamentId).map { StageResultDto(it.teamId, it.teamName) } + fun getResultsForStage(stage: KnockoutStageEntity): List { + if (stage.level <= 1) { + return tournamentService.getParticipants(stage.tournamentId) + .mapIndexed { index, participant -> + StageResultDto( + participant.teamId, + participant.teamName, + stage.id, + initialSeed = index + 1, + detailedStats = Optional.empty() + ) + } } - val stages = stageRepository.findAllByTournamentIdAndLevel(tournamentId, level) + val stages = stageRepository.findAllByTournamentIdAndLevel(stage.tournamentId, stage.level - 1) if (stages.isEmpty()) { return emptyList() } - return stages.flatMap { it.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } }.sortedWith( - compareBy( - { it.position }, - { it.points }, - { it.won }, - { it.goalDifference }, - { it.goalsFor } - ) - ) + return stages.flatMap { it.participants.split("\n") + .map { objectMapper.readValue(it, StageResultDto::class.java) } } + .sorted() } @Transactional fun calculateTeamsFromSeeds(stage: KnockoutStageEntity) { val matches = matchRepository.findAllByStageId(stage.id) - val teams = stage.participants.split("\n").map { objectMapper.readValue(it, StageResultDto::class.java) } + val seeds = getSeeds(stage) for (match in matches){ - val homeTeam = teams.find { it.seed == match.homeSeed } - val awayTeam = teams.find { it.seed == match.awaySeed } + val homeTeam = seeds[match.homeSeed] + val awayTeam = seeds[match.awaySeed] match.homeTeamId = homeTeam?.teamId match.homeTeamName = homeTeam?.teamName ?: "TBD" match.awayTeamId = awayTeam?.teamId @@ -109,6 +123,18 @@ class KnockoutStageService( } } + fun getSeeds(stage: KnockoutStageEntity): Map{ + val seeds = mutableMapOf() + stage.seeds.split("\n").forEach { + val participant = objectMapper.readValue(it, SeededParticipantDto::class.java) + seeds[participant.seed] = ParticipantDto( + teamId = participant.teamId, + teamName = participant.teamName + ) + } + return seeds + } + fun findById(id: Int): KnockoutStageEntity? { //return stageRepository.findById(id).orElseThrow { IllegalArgumentException("No stage found with id $id") } return stageRepository.findById(id).getOrNull() diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt index 5b7eb28a..155b0b0c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/ParticipantDto.kt @@ -4,3 +4,9 @@ data class ParticipantDto( var teamId: Int = 0, var teamName: String = "", ) + +data class SeededParticipantDto( + var teamId: Int = 0, + var teamName: String = "", + var seed: Int = 0, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt index 8a3cffbd..1c0c618e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/StageResultDto.kt @@ -1,15 +1,41 @@ package hu.bme.sch.cmsch.component.tournament +import com.fasterxml.jackson.annotation.JsonInclude +import java.util.Optional +import kotlin.jvm.optionals.getOrElse +import kotlin.jvm.optionals.getOrNull + +@JsonInclude(JsonInclude.Include.NON_EMPTY) data class StageResultDto( - var teamId: Int, - var teamName: String, - var seed: Int = 0, - var position: Int = 0, - var points: Int = 0, - var won: Int = 0, - var drawn: Int = 0, - var lost: Int = 0, - var goalsFor: Int = 0, - var goalsAgainst: Int = 0, + val teamId: Int, + val teamName: String, + val stageId: Int = 0, + var highlighted: Boolean = false, + var initialSeed: Int = 0, + var detailedStats: Optional = Optional.empty() +): Comparable { + override fun compareTo(other: StageResultDto): Int { + return compareValuesBy(this, other, + { it.detailedStats.getOrElse({ GroupStageResults() })}, {it.initialSeed}, {it.highlighted}) + } +} +data class GroupStageResults( + var group: String = "", + var position: UShort = UShort.MAX_VALUE, + var points: UInt = 0u, + var won: UShort = 0u, + var drawn: UShort = 0u, + var lost: UShort = 0u, + var goalsFor: UInt = 0u, + var goalsAgainst: UInt = 0u, var goalDifference: Int = 0 -) +): Comparable { + override fun compareTo(other: GroupStageResults): Int { + return compareValuesBy(this, other, + { it.position.inv() }, + { it.points }, + { it.won }, + { it.goalDifference }, + { it.goalsFor }) + } +} diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt index 827b29b7..3454ba3b 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/tournament/TournamentService.kt @@ -6,6 +6,7 @@ import hu.bme.sch.cmsch.config.OwnershipType import hu.bme.sch.cmsch.config.StartupPropertyConfig import hu.bme.sch.cmsch.model.RoleType import hu.bme.sch.cmsch.repository.GroupRepository +import org.hibernate.internal.util.collections.CollectionHelper.listOf import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.context.ApplicationContext import org.springframework.http.ResponseEntity @@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional import java.sql.SQLException import java.util.* +import kotlin.collections.listOf import kotlin.jvm.optionals.getOrNull @Service @@ -26,7 +28,6 @@ open class TournamentService( private val groupRepository: GroupRepository, private val tournamentComponent: TournamentComponent, private val objectMapper: ObjectMapper, - private val stageService: KnockoutStageService, private val startupPropertyConfig: StartupPropertyConfig, private val matchRepository: TournamentMatchRepository ) { @@ -74,7 +75,7 @@ open class TournamentService( } private fun mapTournament(tournament: TournamentEntity, user: CmschUser?): TournamentDetailedView { - val participants = tournament.participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } + val participants = if (tournament.participants != "") tournament.participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) } else listOf() val playerId = when (startupPropertyConfig.tournamentOwnershipMode){ OwnershipType.GROUP -> user?.groupId ?: null @@ -106,7 +107,7 @@ open class TournamentService( it.participantCount, it.nextRound, it.status, - stageService.findMatchesByStageId(it.id).map { MatchDto( + matchRepository.findAllByStageId(it.id).map { MatchDto( it.id, it.gameId, it.kickoffTime, @@ -173,7 +174,7 @@ open class TournamentService( val participants = tournament.participants val parsed = mutableListOf() - parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) + if(participants != "") parsed.addAll(participants.split("\n").map { objectMapper.readValue(it, ParticipantDto::class.java) }) if (parsed.any { it.teamId == groupId }) { return TournamentJoinStatus.ALREADY_JOINED } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt index 2472f078..d5811b44 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/config/AppConfig.kt @@ -2,6 +2,7 @@ package hu.bme.sch.cmsch.config import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.fasterxml.jackson.module.kotlin.registerKotlinModule import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -26,6 +27,7 @@ class AppConfig { fun objectMapper(): ObjectMapper { val objectMapper = ObjectMapper() objectMapper.registerKotlinModule() + objectMapper.registerModule(Jdk8Module()) objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) return objectMapper } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt index 41bc6e27..981bb30f 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/service/PermissionsService.kt @@ -1621,13 +1621,6 @@ object StaffPermissions : PermissionGroup { component = TournamentComponent::class ) - val PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS = PermissionValidator( - "TOURNAMENT_PARTICIPANTS_SHOW", - "Verseny résztvevők kezelése", - readOnly = true, - component = TournamentComponent::class - ) - val PERMISSION_SET_SEEDS = PermissionValidator( "TOURNAMENT_SET_SEEDS", "Versenyzők seedjeinek állítása", @@ -1635,10 +1628,10 @@ object StaffPermissions : PermissionGroup { component = TournamentComponent::class ) - val PERMISSION_GENERATE_GROUPS = PermissionValidator( - "TOURNAMENT_GENERATE_GROUPS", - "Verseny csoportok generálása", - readOnly = false, + val PERMISSION_SHOW_BRACKETS = PermissionValidator( + "TOURNAMENT_SHOW_BRACKETS", + "Verseny táblák megtekintése", + readOnly = true, component = TournamentComponent::class ) @@ -1649,13 +1642,6 @@ object StaffPermissions : PermissionGroup { component = TournamentComponent::class ) - val PERMISSION_GENERATE_MATCHES = PermissionValidator( - "TOURNAMENT_GENERATE_MATCHES", - "Verseny meccsek generálása", - readOnly = false, - component = TournamentComponent::class - ) - val PERMISSION_EDIT_RESULTS = PermissionValidator( "TOURNAMENT_EDIT_RESULTS", "Verseny eredmények szerkesztése", @@ -1842,11 +1828,9 @@ object StaffPermissions : PermissionGroup { PERMISSION_CREATE_TOURNAMENTS, PERMISSION_DELETE_TOURNAMENTS, PERMISSION_EDIT_TOURNAMENTS, - PERMISSION_EDIT_TOURNAMENT_PARTICIPANTS, PERMISSION_SET_SEEDS, - PERMISSION_GENERATE_GROUPS, + PERMISSION_SHOW_BRACKETS, PERMISSION_GENERATE_BRACKETS, - PERMISSION_GENERATE_MATCHES, PERMISSION_EDIT_RESULTS, ) diff --git a/backend/src/main/resources/templates/seedSettings.html b/backend/src/main/resources/templates/seedSettings.html new file mode 100644 index 00000000..ab14ead8 --- /dev/null +++ b/backend/src/main/resources/templates/seedSettings.html @@ -0,0 +1,160 @@ + + + + + +
+
+ + +
+
+
+ +
+

View edit

+

View view

+

View type

+ +
+ + +

+
+ + + +

+

+
+
+ +
+
+ + + +
+ + + + + + undo + VISSZA + + + + +
+ +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/frontend/src/pages/tournament/components/Tournament.tsx b/frontend/src/pages/tournament/components/Tournament.tsx index 48269c0d..f7144c8b 100644 --- a/frontend/src/pages/tournament/components/Tournament.tsx +++ b/frontend/src/pages/tournament/components/Tournament.tsx @@ -8,7 +8,6 @@ import { Button, Flex, Heading, - HStack, Tab, TabList, TabPanel, From 8ce0f77df230b38abcd49f0bc3f424b4a5d93fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20Benedek?= Date: Mon, 30 Jun 2025 12:38:13 +0200 Subject: [PATCH 42/89] seedsetter admin page --- .../main/resources/templates/seedSettings.html | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/src/main/resources/templates/seedSettings.html b/backend/src/main/resources/templates/seedSettings.html index ab14ead8..993e85de 100644 --- a/backend/src/main/resources/templates/seedSettings.html +++ b/backend/src/main/resources/templates/seedSettings.html @@ -30,7 +30,7 @@ } function flipWithPrevious(id) { - const currentRow = document.getElementById(`row_${id}`); + const currentRow = document.getElementById(`team_${id}`); const previousRow = currentRow.previousElementSibling; if (previousRow && previousRow.id !== 'header') { @@ -91,20 +91,18 @@

View type