diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/challenge/ChallengeController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/challenge/ChallengeController.kt index 25a77736..5e16a00c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/challenge/ChallengeController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/challenge/ChallengeController.kt @@ -113,9 +113,10 @@ class ChallengeController( return true } + private fun processUserSubmission(entity: ChallengeSubmissionEntity): Boolean { if (entity.userName.isNotBlank() && entity.userName != "-") { - val id = entity.userName.split("|")[0].trim().toIntOrNull() ?: 0 + val id = entity.userName.split("|")[1].trim().toIntOrNull() ?: 0 val user = transactionManager.transaction(readOnly = true) { users.findById(id) } if (user.isPresent) { @@ -141,4 +142,4 @@ class ChallengeController( } private fun mapUsername(it: UserEntity) = - "${it.id}| ${it.fullNameWithAlias} [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" + "${it.fullNameWithAlias} | ${it.id} | [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/debt/ProductGroupVirtualEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/debt/ProductGroupVirtualEntity.kt index 5a187a43..d9ceacf7 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/debt/ProductGroupVirtualEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/debt/ProductGroupVirtualEntity.kt @@ -12,7 +12,7 @@ data class ProductGroupVirtualEntity ( @property:GenerateOverview(columnName = "Termék", order = 1) var name: String = "", - @property:GenerateOverview(columnName = "Eladott mennyiség", order = 2, centered = true) + @property:GenerateOverview(columnName = "Eladott mennyiség", order = 2, centered = true, renderer = OverviewType.NUMBER) var soldCount: Int = 0, ) : IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/ResponsesController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/ResponsesController.kt index 7f9c87f1..245ccc81 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/ResponsesController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/ResponsesController.kt @@ -128,14 +128,19 @@ class ResponsesController( } val objReader = objectMapper.readerFor(object : TypeReference>() {}) - val entries = formService.getResponsesById(id) - .map { objReader.readValue>(it.submission) } + val responses = formService.getResponsesById(id) + val submissions = responses.map { objReader.readValue>(it.submission) } .map { it.values } .toList() + val submitters = responses.map { listOf(it.submitterGroupId?:"", it.submitterGroupName, it.submitterUserId?:"", it.submitterUserName) } + + val entries = submitters.zip(submissions) { a, b -> a + b } - val headers = objReader.readValue>(formService.getResponsesById(id).firstOrNull()?.submission ?: "{}") + val header = objReader.readValue>(formService.getResponsesById(id).firstOrNull()?.submission ?: "{}") .keys .joinToString(",") + val headers = "submitterGroupId,submitterGroupName,submitterUserId,submitterUserName,$header" + val result = CsvMapper().writeValueAsString(entries) response.setHeader("Content-Disposition", "attachment; filename=\"form-${id}-responses.csv\"") return headers + "\n" + result @@ -149,7 +154,9 @@ class ResponsesController( return "403" } - val entries = formService.getResponsesById(id).joinToString(",") { it.submission } + val entries = formService.getResponsesById(id) + .map { "{submitterGroupId:${it.submitterGroupId},submitterGroupName:${it.submitterGroupName},submitterUserId:${it.submitterUserId},submitterUserName:${it.submitterUserName},submission:${it.submission}}" } + .joinToString(",") { it } return "[${entries}]" } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/VoteListDashboard.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/VoteListDashboard.kt index 5e4556a5..cc9fe673 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/VoteListDashboard.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/form/VoteListDashboard.kt @@ -25,7 +25,7 @@ data class FormVotesDto( @property:GenerateOverview(columnName = "Űrlap neve", order = 1, useForSearch = true) var name: String = "", - @property:GenerateOverview(columnName = "Kitöltések", order = 2, useForSearch = true) + @property:GenerateOverview(columnName = "Kitöltések", order = 2, useForSearch = true, renderer = OverviewType.NUMBER) var submissions: Long = 0, ) : IdentifiableEntity diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileService.kt index fff5e680..9fba327c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileService.kt @@ -9,9 +9,8 @@ import hu.bme.sch.cmsch.component.groupselection.GroupSelectionComponent import hu.bme.sch.cmsch.component.location.LocationService import hu.bme.sch.cmsch.component.login.CmschUser import hu.bme.sch.cmsch.component.login.LoginComponent -import hu.bme.sch.cmsch.component.race.FreestyleRaceEntryDto import hu.bme.sch.cmsch.component.race.RaceService -import hu.bme.sch.cmsch.component.race.RaceView +import hu.bme.sch.cmsch.component.race.RaceStatsView import hu.bme.sch.cmsch.component.riddle.RiddleBusinessLogicService import hu.bme.sch.cmsch.component.task.TasksService import hu.bme.sch.cmsch.component.token.ALL_TOKEN_TYPE @@ -64,8 +63,7 @@ class ProfileService( val tokenCategoryToDisplay = tokenComponent.map { it.collectRequiredType }.orElse(ALL_TOKEN_TYPE) val incompleteTasks = tasksService.map { it.getTasksThatNeedsToBeCompleted(user) }.orElse(null) - val raceView: RaceView? = raceService.map { it.getViewForUsers(user) }.orElse(null) - val freestyleRaceView: FreestyleRaceEntryDto? = raceService.map { it.getFreestyleEntryOfUser(user.id) }.orElse(null) + val raceStats: RaceStatsView? = raceService.map { it.getRaceStats(user) }.orElse(null) return ProfileView( loggedIn = true, @@ -114,10 +112,7 @@ class ProfileService( }.orElse(null), // Race component - racePlacement = raceView?.place, - raceStat = raceView?.bestTime, - freestyleRaceDescription = freestyleRaceView?.description, - freestyleRaceStat = freestyleRaceView?.time, + raceStats = raceStats, // Locations component locations = profileComponent.showGroupLeadersLocations.mapIfTrue { fetchLocations(group).orElse(null) }, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileView.kt index d4e4c8db..e40fff41 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileView.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/profile/ProfileView.kt @@ -3,6 +3,7 @@ package hu.bme.sch.cmsch.component.profile import com.fasterxml.jackson.annotation.JsonView import hu.bme.sch.cmsch.component.debt.DebtDto import hu.bme.sch.cmsch.component.leaderboard.LeaderBoardEntry +import hu.bme.sch.cmsch.component.race.RaceStatsView import hu.bme.sch.cmsch.component.token.TokenDto import hu.bme.sch.cmsch.dto.FullDetails import hu.bme.sch.cmsch.model.GuildType @@ -79,21 +80,9 @@ data class ProfileView( @field:JsonView(FullDetails::class) val completedTaskCount: Int? = null, - // Race placement + // Race stats @field:JsonView(FullDetails::class) - val racePlacement: Int? = null, - - // Race stat given in seconds - @field:JsonView(FullDetails::class) - val raceStat: Float? = null, - - // Freestyle (funky) race description - @field:JsonView(FullDetails::class) - val freestyleRaceDescription: String? = null, - - // Freestyle (funky) race stat given in seconds - @field:JsonView(FullDetails::class) - val freestyleRaceStat: Float? = null, + val raceStats: RaceStatsView? = null, @field:JsonView(FullDetails::class) val profileIsComplete: Boolean? = null, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrFightService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrFightService.kt index d19e7ed0..a6c6ded6 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrFightService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrFightService.kt @@ -27,6 +27,14 @@ const val TIMER_OCCURRENCE = 10 private const val NOBODY = "senki" +data class TowerHistoryEntry( + val userName: String = NOBODY, + val userId: Int = 0, + val ownerGroup: String = NOBODY, + val ownerGroupId: Int = 0, + val timestamp: Long = 0 +) + @Service @ConditionalOnBean(QrFightComponent::class) class QrFightService( @@ -309,22 +317,12 @@ class QrFightService( if (dailyLimit != -1L){ val history = towerEntity.history .split("\n") - .map { it.split(";") } - .map { - TokenPropertyRawView( - ownerUserName = it[0], - ownerUserId = it[1].toIntOrNull() ?: 0, - ownerGroupName = it[2], - ownerGroupId = it[3].toIntOrNull() ?: 0, - timestamp = it[4].toLongOrNull() ?: 0, - token = "", - score = 0 - ) - } + .filter { it.isNotBlank() } // Only non-empty lines + .map { objectMapper.readValue(it, TowerHistoryEntry::class.java) } .filter { it.timestamp > clock.getTimeInSeconds() - 24 * 3600 } - .filter { it.ownerUserId == user.id } + .filter { it.userId == user.id } - if (history.size > dailyLimit){ + if (history.size >= dailyLimit){ log.info("Tower '{}' daily limit exceeded for user:{} (group:{})", towerEntity.selector, user.userName, groupName) return TokenSubmittedView(QR_TOWER_DAILY_LIMIT_EXCEEDED, token.title, null, null) } @@ -337,7 +335,7 @@ class QrFightService( towerEntity.ownerGroupId = groupId towerEntity.ownerGroupName = groupName - towerEntity.history += "${user.userName};${user.id};${groupName};${groupId};${clock.getTimeInSeconds()};\n" + towerEntity.history += objectMapper.writeValueAsString(TowerHistoryEntry(userName = user.userName, userId = user.id, ownerGroup = groupName, ownerGroupId = groupId, timestamp = clock.getTimeInSeconds())) + "\n" qrTowerRepository.save(towerEntity) log.info("Tower '{}' captured by group:{} (user:{})", token.title, groupName, user.userName) return TokenSubmittedView( diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrLevelEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrLevelEntity.kt index bddf4084..7a26c0dd 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrLevelEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/qrfight/QrLevelEntity.kt @@ -121,7 +121,7 @@ data class QrLevelEntity( @property:ImportFormat var extraLevel: Boolean = false, - @Column(nullable = false, columnDefinition="BOOLEAN DEFAULT false") + @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = InputType.SWITCH, order = 14, label = "Treasure hunt szint", note = "Olyan tokeneket tartalmazó szint, ahol az addig megszerzett tokenek adják a hintet a további tokenekhez") diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceEntryDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceEntryDto.kt index d05675e3..fe8eb9d1 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceEntryDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceEntryDto.kt @@ -41,11 +41,6 @@ data class FreestyleRaceEntryDto( @property:ImportFormat var email: String = "", - @field:JsonView(FullDetails::class) - @property:GenerateOverview(columnName = "Címke", order = 6) - @property:ImportFormat - var label: String = "", - ) : ManagedEntity { override fun getEntityConfig(env: Environment) = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordController.kt index b92e2666..b9c21d6e 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordController.kt @@ -118,7 +118,7 @@ class FreestyleRaceRecordController( private fun processUserSubmission(entity: FreestyleRaceRecordEntity): Boolean { if (entity.userName.isNotBlank() && entity.userName != "-") { - val id = entity.userName.split("|")[0].trim().toIntOrNull() ?: 0 + val id = entity.userName.split("|")[1].trim().toIntOrNull() ?: 0 val user = transactionManager.transaction(readOnly = true) { users.findById(id) } if (user.isPresent) { @@ -146,7 +146,7 @@ class FreestyleRaceRecordController( } private fun mapUsername(it: UserEntity) = - "${it.id}| ${it.fullNameWithAlias} [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" + "${it.fullNameWithAlias} | ${it.id} | [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" private fun mapUsername(it: UserSelectorView) = - "${it.id}| ${it.fullNameWithAlias} [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" + "${it.fullNameWithAlias} | ${it.id} | [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordEntity.kt index 4bfb244e..26fe51e8 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/FreestyleRaceRecordEntity.kt @@ -41,7 +41,7 @@ data class FreestyleRaceRecordEntity( @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @Column(nullable = false) @property:GenerateInput(type = InputType.ENTITY_SELECT, order = 2, label = "Felhasználó", entitySource = "UserEntity", - note = "Csak akkor kell kijelölni ha felhasználók kapnak pontot. Formátum: `id| Teljes Név [a/g] email` ahol az: a = authsch, g = google", + note = "Csak akkor kell kijelölni ha felhasználók kapnak pontot. Formátum: `Teljes Név | id | [a/g] email` ahol az: a = authsch, g = google", interpreter = InputInterpreter.SEARCH) @property:GenerateOverview(columnName = "Felhasználó", order = 2, centered = true) @property:ImportFormat @@ -75,13 +75,6 @@ data class FreestyleRaceRecordEntity( @property:ImportFormat var timestamp: Long = 0, - @Column(nullable = false) - @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) - @property:GenerateInput(order = 7, label = "Címke", note = "Név melletti címke, pl: Gólya, Lány etc.") - @property:GenerateOverview(columnName = "Címke", order = 6) - @property:ImportFormat - var label: String = "", - ) : ManagedEntity, Duplicatable { override fun getEntityConfig(env: Environment) = EntityConfig( diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceEntryDto.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceEntryDto.kt index 1c0c99e3..e451823c 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceEntryDto.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceEntryDto.kt @@ -36,7 +36,17 @@ data class RaceEntryDto( @property:ImportFormat var email: String = "", -) : ManagedEntity { + @field:JsonView(FullDetails::class) + @property:GenerateOverview(columnName = "Címke", order = 5) + @property:ImportFormat + var label: String = "", + + @field:JsonView(FullDetails::class) + @property:GenerateOverview(columnName = "Címke színe", order = 6) + @property:ImportFormat + var labelColor: String? = "", + + ) : ManagedEntity { override fun getEntityConfig(env: Environment) = null diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordController.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordController.kt index 4f309738..f7550e04 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordController.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordController.kt @@ -127,7 +127,7 @@ class RaceRecordController( private fun processUserSubmission(entity: RaceRecordEntity): Boolean { if (entity.userName.isNotBlank() && entity.userName != "-") { - val id = entity.userName.split("|")[0].trim().toIntOrNull() ?: 0 + val id = entity.userName.split("|")[1].trim().toIntOrNull() ?: 0 val user = transactionManager.transaction(readOnly = true) { users.findById(id) } if (user.isPresent) { @@ -156,7 +156,7 @@ class RaceRecordController( private fun mapUsername(it: UserEntity) = - "${it.id}| ${it.fullNameWithAlias} [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" + "${it.fullNameWithAlias} | ${it.id} | [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" private fun mapUsername(it: UserSelectorView) = - "${it.id}| ${it.fullNameWithAlias} [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" + "${it.fullNameWithAlias} | ${it.id} | [${it.provider.firstOrNull() ?: 'n'}] ${it.email}" diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordEntity.kt index e89947d1..9bbc91f5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceRecordEntity.kt @@ -28,7 +28,7 @@ data class RaceRecordEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class ]) - @property:GenerateInput(order = 2, label = "Kategória", type = InputType.ENTITY_SELECT, + @property:GenerateInput(order = 1, label = "Kategória", type = InputType.ENTITY_SELECT, entitySource = "RaceCategoryEntity", note = "Az üres az alapértelmezett kategória") @property:GenerateOverview(columnName = "Kategória", order = 1) @property:ImportFormat @@ -42,7 +42,7 @@ data class RaceRecordEntity( @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @Column(nullable = false) @property:GenerateInput(type = InputType.ENTITY_SELECT, order = 2, label = "Felhasználó", entitySource = "UserEntity", - note = "Csak akkor kell kijelölni ha felhasználók kapnak pontot. Formátum: `id| Teljes Név [a/g] email` ahol az: a = authsch, g = google", + note = "Csak akkor kell kijelölni ha felhasználók kapnak pontot. Formátum: `Teljes Név | id | [a/g] email` ahol az: a = authsch, g = google", interpreter = InputInterpreter.SEARCH) @property:GenerateOverview(columnName = "Felhasználó", order = 2, centered = true) @property:ImportFormat @@ -76,6 +76,24 @@ data class RaceRecordEntity( @property:ImportFormat var timestamp: Long = 0, + @Column(nullable = false) + @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) + @property:GenerateInput(order = 6, label = "Címke", note = "Név melletti címke, pl: Gólya, Lány etc.") + @property:GenerateOverview(columnName = "Címke", order = 6) + @property:ImportFormat + var label: String = "", + + @Column(nullable = true, columnDefinition = "TEXT") + @field:JsonView(value = [Edit::class, Preview::class, FullDetails::class]) + @property:GenerateInput( + order = 7, + type = InputType.COLOR, + label = "Szín", + note = "A címke színe hex kódban megadva. Ha üresen hagyod, akkor az oldal színét fogja használni.", + ) + @property:ImportFormat + var labelColor: String? = "", + ) : ManagedEntity, Duplicatable { override fun getEntityConfig(env: Environment) = EntityConfig( diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceService.kt index d98ac50d..9c275df0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceService.kt @@ -8,6 +8,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import kotlin.jvm.optionals.getOrNull +import kotlin.math.pow +import kotlin.math.sqrt const val DEFAULT_CATEGORY = "" @@ -108,7 +110,9 @@ open class RaceService( submission.value.first().groupName, submission.value.first().groupName, submission.value.minOf { it.time }, - email = "" + email = "", + submission.value.first().label, + submission.value.first().labelColor ) } .sortedBy { it.time } @@ -121,7 +125,9 @@ open class RaceService( submission.value.first().groupName, submission.value.first().groupName, submission.value.maxOf { it.time }, - email = "" + email = "", + submission.value.first().label, + submission.value.first().labelColor ) } .sortedByDescending { it.time } @@ -136,7 +142,6 @@ open class RaceService( submission.groupName, submission.groupName, submission.time, - label = submission.label ) } .sortedBy { it.time } @@ -148,7 +153,6 @@ open class RaceService( submission.groupName, submission.groupName, submission.time, - label = submission.label, ) } .sortedByDescending { it.time } @@ -219,7 +223,9 @@ open class RaceService( submission.value.first().userName, submission.value.first().groupName, submission.value.minOf { it.time }, - email + email, + submission.value.first().label, + submission.value.first().labelColor ) } .sortedBy { it.time } @@ -235,7 +241,9 @@ open class RaceService( submission.value.first().userName, submission.value.first().groupName, submission.value.maxOf { it.time }, - email + email, + submission.value.first().label, + submission.value.first().labelColor ) } .sortedByDescending { it.time } @@ -251,7 +259,6 @@ open class RaceService( submission.groupName, submission.time, submission.description, - label = submission.label ) } .sortedBy { it.time } @@ -264,29 +271,48 @@ open class RaceService( submission.groupName, submission.time, submission.description, - label = submission.label ) } .sortedByDescending { it.time } } - /** - * Shorthand query of freestyle record of the user with the given ID. - * This does not query all the users, just the user requested. - * @return FreestyleRaceEntryDto of the user with the given id, or null if not present. + /*** + * Gets relevant statistics about race entries of the given user + * @param CmschUser whose stats we want to get + * @return RaceStatsView of the user, or null if the user has no submissions */ @Transactional(readOnly = true) - open fun getFreestyleEntryOfUser(userId: Int): FreestyleRaceEntryDto? { - val entity = freestyleRaceRecordRepository.findByUserId(userId) + open fun getRaceStats(user: CmschUser): RaceStatsView? { - return entity?.let { - FreestyleRaceEntryDto( - id = it.id, - name = it.userName, - groupName = it.groupName, - time = it.time, - description = it.description, - ) - } + // All records + val records: List = raceRecordRepository.findAll().sortedBy { it.time } + + // All the records of this user + var userRecords = records.filter { it.userId == user.id } + if( userRecords.isEmpty() ) return null + val bestTime = userRecords.minBy { it.time }.time + + // Best records of all users. + val bestTimes = records.groupBy { it.userId }.map { it.value.first() }.sortedBy { it.time } // Sort might be unnecessary, but just to be safe + val placement = bestTimes.indexOfFirst { it.userId == user.id } + 1 + + val timesParticipated = userRecords.count() + + val averageTime = userRecords.map { it.time }.average().toFloat() + + val deviation = sqrt(userRecords.map { it.time }.map { (it - averageTime).pow(2) }.average()).toFloat() + + // kcal of 0.5l of Soproni Lager + val kcalPerPortion = 164 + val kCaloriesPerSecond = kcalPerPortion / bestTime + + return RaceStatsView( + bestTime = bestTime, + placement = placement, + timesParticipated = timesParticipated, + averageTime = averageTime, + deviation = deviation, + kCaloriesPerSecond = kCaloriesPerSecond + ) } } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceStatsView.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceStatsView.kt new file mode 100644 index 00000000..335245f5 --- /dev/null +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/race/RaceStatsView.kt @@ -0,0 +1,31 @@ +package hu.bme.sch.cmsch.component.race + +import com.fasterxml.jackson.annotation.JsonView +import hu.bme.sch.cmsch.dto.FullDetails + + +data class RaceStatsView( + // The best time in seconds + @field:JsonView(FullDetails::class) + val bestTime: Float, + + // The placement on the uncategorized leaderboard + @field:JsonView(FullDetails::class) + val placement: Int, + + // The amount of submissions made + @field:JsonView(FullDetails::class) + val timesParticipated: Int, + + // Average of times + @field:JsonView(FullDetails::class) + val averageTime: Float, + + // Deviation of times + @field:JsonView(FullDetails::class) + val deviation: Float, + + // kcal consumed per second, using the best time as reference + @field:JsonView(FullDetails::class) + val kCaloriesPerSecond: Float, +) diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt index dd2da2d5..dca297d5 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt @@ -448,7 +448,7 @@ class RiddleBusinessLogicService( .toMap() return categories.associate { category -> - category.title to submissions.getOrDefault(category.id, listOf()) + category.title to submissions.getOrDefault(category.categoryId, listOf()) .map { riddle -> riddle to riddleCacheManager.getRiddleById(riddle.riddleId) } .sortedBy { it.second?.order } .mapNotNull { it.second?.let { riddle -> mapRiddle(it.first, riddle) } } @@ -467,7 +467,7 @@ class RiddleBusinessLogicService( .toMap() return categories.associate { category -> - category.title to submissions.getOrDefault(category.id, listOf()) + category.title to submissions.getOrDefault(category.categoryId, listOf()) .map { riddle -> riddle to riddleCacheManager.getRiddleById(riddle.riddleId) } .sortedBy { it.second?.order } .mapNotNull { it.second?.let { riddle -> mapRiddle(it.first, riddle) } } diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleEntity.kt index dcec9ab6..b45d1533 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleEntity.kt @@ -59,21 +59,21 @@ data class RiddleEntity( @Column(nullable = false) @field:JsonView(value = [ Edit::class ]) @property:GenerateInput(type = InputType.NUMBER, order = 5, label = "Pont") - @property:GenerateOverview(columnName = "Pont", order = 2, centered = true) + @property:GenerateOverview(columnName = "Pont", order = 2, centered = true, renderer = OverviewType.NUMBER) @property:ImportFormat var score: Int = 0, @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @Column(nullable = false, name = "`order`") @property:GenerateInput(type = InputType.NUMBER, order = 6, label = "Sorrend") - @property:GenerateOverview(columnName = "Sorrend", order = 3, centered = true) + @property:GenerateOverview(columnName = "Sorrend", order = 3, centered = true, renderer = OverviewType.NUMBER) @property:ImportFormat var order: Int = 0, @Column(nullable = false) @field:JsonView(value = [ Edit::class, Preview::class, FullDetails::class ]) @property:GenerateInput(type = InputType.NUMBER, min = 0, order = 7, label = "Kategória id-je") - @property:GenerateOverview(columnName = "Kategória", order = 4, centered = true) + @property:GenerateOverview(columnName = "Kategória", order = 4, centered = true, renderer = OverviewType.NUMBER) @property:ImportFormat var categoryId: Int = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/token/TokenEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/token/TokenEntity.kt index bdfaaa6b..b7a4d9ea 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/component/token/TokenEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/component/token/TokenEntity.kt @@ -75,7 +75,7 @@ data class TokenEntity( @Column(nullable = false) @property:GenerateInput(order = 6, label = "Pont", type = InputType.NUMBER, defaultValue = "0", note = "Egész szám, hány pontot ér a megszerzése") - @property:GenerateOverview(columnName = "Pont", order = 5, centered = true) + @property:GenerateOverview(columnName = "Pont", renderer = OverviewType.NUMBER, order = 5, centered = true) @property:ImportFormat var score: Int? = 0, diff --git a/backend/src/main/kotlin/hu/bme/sch/cmsch/dto/virtual/CheckRatingVirtualEntity.kt b/backend/src/main/kotlin/hu/bme/sch/cmsch/dto/virtual/CheckRatingVirtualEntity.kt index a2ea9997..807cccf0 100644 --- a/backend/src/main/kotlin/hu/bme/sch/cmsch/dto/virtual/CheckRatingVirtualEntity.kt +++ b/backend/src/main/kotlin/hu/bme/sch/cmsch/dto/virtual/CheckRatingVirtualEntity.kt @@ -12,10 +12,10 @@ data class CheckRatingVirtualEntity( @property:GenerateOverview(columnName = "Csoport", order = 1) var groupName: String = "", - @property:GenerateOverview(columnName = "Pont", order = 2) + @property:GenerateOverview(columnName = "Pont", order = 2, renderer = OverviewType.NUMBER) var score: Int = 0, - @property:GenerateOverview(columnName = "Kapható max", order = 3) + @property:GenerateOverview(columnName = "Kapható max", order = 3, renderer = OverviewType.NUMBER) var maxScore: Int = 0, ) : IdentifiableEntity diff --git a/backend/src/main/resources/templates/component/dashboard/form/searchable-select.html b/backend/src/main/resources/templates/component/dashboard/form/searchable-select.html index a8b16647..cbcd0359 100644 --- a/backend/src/main/resources/templates/component/dashboard/form/searchable-select.html +++ b/backend/src/main/resources/templates/component/dashboard/form/searchable-select.html @@ -44,8 +44,7 @@ const matchArray = findMatches(this.value, options); select[0].append(...matchArray); } - input.addEventListener('change', filterOptions); - input.addEventListener('keyup', filterOptions); + input.addEventListener('input', filterOptions); })(); diff --git a/backend/src/main/resources/templates/component/details/entity-select.html b/backend/src/main/resources/templates/component/details/entity-select.html index bb2526d6..1025ce4b 100644 --- a/backend/src/main/resources/templates/component/details/entity-select.html +++ b/backend/src/main/resources/templates/component/details/entity-select.html @@ -26,38 +26,36 @@ th:selected="${data != null && #strings.equals(property.get(data), opt)}"> - - - - + + + Típus: Kapcsolat diff --git a/frontend/src/common-components/CollapsableTableRow.tsx b/frontend/src/common-components/CollapsableTableRow.tsx index ce6ed1d6..fcc6a09c 100644 --- a/frontend/src/common-components/CollapsableTableRow.tsx +++ b/frontend/src/common-components/CollapsableTableRow.tsx @@ -1,5 +1,5 @@ import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' -import { Box, Link as ChakraLink, Grid, GridItem, HStack, useDisclosure } from '@chakra-ui/react' +import { Box, Link as ChakraLink, Grid, GridItem, Stack, useDisclosure } from '@chakra-ui/react' import { Fragment } from 'react' import { Link } from 'react-router' import TeamLabel from '../pages/teams/components/TeamLabel.tsx' @@ -37,14 +37,12 @@ export const CollapsableTableRow = ({ const outerColTemplate: string[] = [] if (!categorized) outerColTemplate.push('[place] auto') outerColTemplate.push('[name] 1fr') - if (data.label) outerColTemplate.push('[label] 1fr') if (showGroup) outerColTemplate.push('[group] 1fr') outerColTemplate.push('[score] auto [chevron] 20px') const innerColTemplate: string[] = [] if (categorized) innerColTemplate.push('[place] auto') innerColTemplate.push('[name] 1fr [score] auto') - if (data.label) innerColTemplate.push('[label] 1fr') if (!categorized) innerColTemplate.push('[chevron] 20px') innerColTemplate.push('[end]') @@ -63,9 +61,9 @@ export const CollapsableTableRow = ({ > {!categorized && {data.position}.} - - {data.name} {data.label && } - + + {data.name} {data.label && } + {showGroup && data.groupName && ( diff --git a/frontend/src/common-components/LeaderboardTable.tsx b/frontend/src/common-components/LeaderboardTable.tsx index 02d0cb06..247282f3 100644 --- a/frontend/src/common-components/LeaderboardTable.tsx +++ b/frontend/src/common-components/LeaderboardTable.tsx @@ -1,5 +1,5 @@ import { Box, Text } from '@chakra-ui/react' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useSearch } from '../util/useSearch' import { LeaderBoardItemView } from '../util/views/leaderBoardView' import { CollapsableTableRow } from './CollapsableTableRow' @@ -26,14 +26,17 @@ export const LeaderBoardTable = ({ }: LeaderboardTableProps) => { const dataWithPosition = useMemo(() => data.map((item, index) => ({ ...item, position: index + 1 })), [data]) - const searchArgs = useSearch( - dataWithPosition, - (item, searchWord) => + const searchFunc = useCallback((item: LeaderBoardItemView, searchWord: string) => { + return ( (item.name.toLowerCase().includes(searchWord) || item.groupName?.toLowerCase().includes(searchWord) || - item.description?.toLowerCase().includes(searchWord)) ?? + item.description?.toLowerCase().includes(searchWord) || + item.label?.toLowerCase().includes(searchWord)) ?? false - ) + ) + }, []) + + const searchArgs = useSearch(dataWithPosition, searchFunc) return ( <> diff --git a/frontend/src/common-components/cmsch-ui-renderer.tsx b/frontend/src/common-components/cmsch-ui-renderer.tsx index b2dec656..ef0ee422 100644 --- a/frontend/src/common-components/cmsch-ui-renderer.tsx +++ b/frontend/src/common-components/cmsch-ui-renderer.tsx @@ -1,4 +1,4 @@ -import { Divider, ListItem, Table, TableContainer, UnorderedList } from '@chakra-ui/react' +import { Divider, ListItem, OrderedList, Table, TableContainer, UnorderedList } from '@chakra-ui/react' import { PropsWithChildren, ReactNode } from 'react' import { Components } from 'react-markdown' import { CLIENT_BASE_URL } from '../util/configs/environment.config' @@ -34,6 +34,13 @@ const cmschTheme: Components = { ) }, + ol: ({ children }: PropsWithChildren) => { + return ( + + {children} + + ) + }, li: ({ children }: PropsWithChildren) => { return {children} }, diff --git a/frontend/src/pages/profile/profile.page.tsx b/frontend/src/pages/profile/profile.page.tsx index 3f0d9193..04708b34 100644 --- a/frontend/src/pages/profile/profile.page.tsx +++ b/frontend/src/pages/profile/profile.page.tsx @@ -68,6 +68,8 @@ const ProfilePage = () => { return } + const raceStats = profile?.raceStats + return ( @@ -150,6 +152,39 @@ const ProfilePage = () => { )} + {component.showRaceStats && raceStats && ( + <> + + Sörmérés + + + {`Legjobb idő: ${raceStats.bestTime}s`} + {`${raceStats.placement}. helyezett`} + + + + Mérések száma: + + {` ${raceStats.timesParticipated}`} + + + Átlag idő: + + {`${raceStats.averageTime}s`} + + + Szórás: + + {`${raceStats.deviation}s`} + + + Kalória/másodperc: + + {`${raceStats.kCaloriesPerSecond} kcal/s`} + + + )} + {component.showQr && profile.cmschId && ( <> @@ -158,29 +193,6 @@ const ProfilePage = () => { )} {(component.showTasks || component.showRiddles || component.showTokens) && } - {component.showRaceStats && profile?.raceStat && ( - - Mérés eredmény: - - {profile?.raceStat}s - {profile.racePlacement && {` (${profile.racePlacement}. helyezett)`}} - - - )} - {component.showRaceStats && profile?.freestyleRaceStat && ( - - - Funky Mérés:{' '} - - - - {profile.freestyleRaceStat}s - - {profile.freestyleRaceDescription && {` (${profile.freestyleRaceDescription})`}} - - - )} - {component.showTasks && (
diff --git a/frontend/src/pages/teams/components/TeamLabel.tsx b/frontend/src/pages/teams/components/TeamLabel.tsx index 4aa6a26e..08da1a4f 100644 --- a/frontend/src/pages/teams/components/TeamLabel.tsx +++ b/frontend/src/pages/teams/components/TeamLabel.tsx @@ -27,6 +27,8 @@ const LabelComponent = ({ name, color, darkColor }: { name: string; color: strin fontWeight="bold" fontSize={12} letterSpacing={1.2} + width="fit-content" + height="fit-content" > {name} diff --git a/frontend/src/pages/teams/components/TeamListItem.tsx b/frontend/src/pages/teams/components/TeamListItem.tsx index 60b83d36..0a335ec9 100644 --- a/frontend/src/pages/teams/components/TeamListItem.tsx +++ b/frontend/src/pages/teams/components/TeamListItem.tsx @@ -1,5 +1,5 @@ import { ChevronRightIcon } from '@chakra-ui/icons' -import { Box, Heading, HStack, Image, Spacer, VStack } from '@chakra-ui/react' +import { Box, Heading, HStack, Image, Spacer, Stack, VStack } from '@chakra-ui/react' import { Link } from 'react-router' import { useOpaqueBackground } from '../../../util/core-functions.util' import { AbsolutePaths } from '../../../util/paths' @@ -26,13 +26,13 @@ export const TeamListItem = ({ team, detailEnabled = false }: TeamListItemProps) > - + {team.name} {team.labels && team.labels.map((label, index) => )} - + {team.introduction && {team.introduction}} diff --git a/frontend/src/pages/teams/teamList.page.tsx b/frontend/src/pages/teams/teamList.page.tsx index dc915924..a9da572b 100644 --- a/frontend/src/pages/teams/teamList.page.tsx +++ b/frontend/src/pages/teams/teamList.page.tsx @@ -12,7 +12,15 @@ import { TeamListItemView } from '../../util/views/team.view' import { TeamListItem } from './components/TeamListItem' const EmptyData: TeamListItemView[] = [] -const searchFn = (item: TeamListItemView, search: string) => item.name.toLowerCase().includes(search) +const searchFn = (item: TeamListItemView, search: string) => { + return ( + (item.name.toLowerCase().includes(search) || + item.labels?.some((label) => { + return label.name.toLowerCase().includes(search) + })) ?? + false + ) +} export default function TeamListPage() { const config = useConfigContext() diff --git a/frontend/src/pages/token/components/QRScanResultComponent.tsx b/frontend/src/pages/token/components/QRScanResultComponent.tsx index ab2e2db3..9624d72c 100644 --- a/frontend/src/pages/token/components/QRScanResultComponent.tsx +++ b/frontend/src/pages/token/components/QRScanResultComponent.tsx @@ -46,6 +46,8 @@ export const QRScanResultComponent = ({ response, isError }: QrScanResultProps) return case ScanStatus.QR_FIGHT_TOTEM_LOCKED: return + case ScanStatus.QR_TOWER_DAILY_LIMIT_EXCEEDED: + return default: return } diff --git a/frontend/src/util/views/leaderBoardView.ts b/frontend/src/util/views/leaderBoardView.ts index 1cc234fc..2681ceb2 100644 --- a/frontend/src/util/views/leaderBoardView.ts +++ b/frontend/src/util/views/leaderBoardView.ts @@ -13,6 +13,7 @@ export type LeaderBoardItemView = { description?: string score?: number label?: string + labelColor?: string items?: LeaderBoardDetail[] total?: number tokenRarities?: { [key: string]: number } diff --git a/frontend/src/util/views/profile.view.ts b/frontend/src/util/views/profile.view.ts index 7e3136c4..fc70a9cc 100644 --- a/frontend/src/util/views/profile.view.ts +++ b/frontend/src/util/views/profile.view.ts @@ -34,10 +34,7 @@ export interface ProfileView { submittedTaskCount?: number completedTaskCount?: number - racePlacement?: number - raceStat?: number - freestyleRaceDescription?: string - freestyleRaceStat?: number + raceStats?: RaceStatsView locations?: GroupMemberLocationView[] debts?: DebtView[] @@ -50,6 +47,15 @@ export interface ProfileView { userMessage?: string } +export interface RaceStatsView { + bestTime: number + placement: number + timesParticipated: number + averageTime: number + deviation: number + kCaloriesPerSecond: number +} + //cannot compare roles if the enums values are strings use the RoleType[role] syntax export enum RoleType { GUEST = 0, // anyone without login diff --git a/frontend/src/util/views/token.view.ts b/frontend/src/util/views/token.view.ts index 3c936651..1b2e6d25 100644 --- a/frontend/src/util/views/token.view.ts +++ b/frontend/src/util/views/token.view.ts @@ -28,7 +28,8 @@ export enum ScanStatus { QR_TOTEM_LOGGED = 'QR_TOTEM_LOGGED', QR_TOTEM_ENSLAVED = 'QR_TOTEM_ENSLAVED', QR_TOTEM_ALREADY_ENSLAVED = 'QR_TOTEM_ALREADY_ENSLAVED', - QR_FIGHT_TOTEM_LOCKED = 'QR_FIGHT_TOTEM_LOCKED' + QR_FIGHT_TOTEM_LOCKED = 'QR_FIGHT_TOTEM_LOCKED', + QR_TOWER_DAILY_LIMIT_EXCEEDED = 'QR_TOWER_DAILY_LIMIT_EXCEEDED' } export const ScanMessages: Record = { @@ -48,7 +49,8 @@ export const ScanMessages: Record = { [ScanStatus.QR_TOWER_ALREADY_ENSLAVED]: 'Ez a QR torony már le van igázva', [ScanStatus.QR_TOTEM_ENSLAVED]: 'QR totem elfoglalva', [ScanStatus.QR_TOTEM_ALREADY_ENSLAVED]: 'Ez a QR totem már el van foglalva', - [ScanStatus.QR_FIGHT_TOTEM_LOCKED]: 'QR totem zárva' + [ScanStatus.QR_FIGHT_TOTEM_LOCKED]: 'QR totem zárva', + [ScanStatus.QR_TOWER_DAILY_LIMIT_EXCEEDED]: 'Ezt a tornyot az elmúlt 24 órában már elfoglaltad elégszer, próbáld meg holnap újra' } export interface ScanResponseView {