diff --git a/apps/scripts/src/app/app.module.ts b/apps/scripts/src/app/app.module.ts index b1e34c28b6..1c534d5c4c 100644 --- a/apps/scripts/src/app/app.module.ts +++ b/apps/scripts/src/app/app.module.ts @@ -1,11 +1,12 @@ import { DatabaseModule } from '@badman/backend-database'; +import { VisualModule } from '@badman/backend-visual'; import { configSchema, load } from '@badman/utils'; import { Logger, Module, OnModuleInit } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ExportPlayersWithRanking } from './scripts'; +import { PlayersWrongRankingRunner } from './scripts'; @Module({ - providers: [ExportPlayersWithRanking], + providers: [PlayersWrongRankingRunner], imports: [ ConfigModule.forRoot({ cache: true, @@ -13,17 +14,18 @@ import { ExportPlayersWithRanking } from './scripts'; load: [load], }), DatabaseModule, + VisualModule, ], }) export class ScriptModule implements OnModuleInit { private readonly logger = new Logger(ScriptModule.name); - constructor(private fixer: ExportPlayersWithRanking) {} + constructor(private fixer: PlayersWrongRankingRunner) {} async onModuleInit() { this.logger.log('Running script'); - await this.fixer.exportPlayersWithRanking(); + await this.fixer.process(); this.logger.log('Script finished'); } diff --git a/apps/scripts/src/app/scripts/export-players-with-ranking/export-players-with-ranking.ts b/apps/scripts/src/app/scripts/export-players-with-ranking/export-players-with-ranking.ts index d247fd6466..d47b079bec 100644 --- a/apps/scripts/src/app/scripts/export-players-with-ranking/export-players-with-ranking.ts +++ b/apps/scripts/src/app/scripts/export-players-with-ranking/export-players-with-ranking.ts @@ -9,7 +9,7 @@ import xlsx from 'xlsx'; export class ExportPlayersWithRanking { private readonly logger = new Logger(ExportPlayersWithRanking.name); - public async exportPlayersWithRanking() { + public async process() { const primarySystem = await RankingSystem.findOne({ where: { primary: true, @@ -39,8 +39,8 @@ export class ExportPlayersWithRanking { model: RankingPlace, where: { systemId: primarySystem.id, - rankingDate: { - [Op.in]: ['2024-08-12', '2024-07-01'], + rankingDate: { + [Op.in]: ['2024-09-02', '2024-08-12'], }, }, }, @@ -77,39 +77,39 @@ export class ExportPlayersWithRanking { this.logger.debug(`Mapping ${players.length} data for ${type}`); const data = []; for (const player of players) { - const rankingAug = player.rankingPlaces.find( - (rp) => rp.systemId === system.id && moment(rp.rankingDate).isSame('2024-08-12', 'day'), + const rankingSep = player.rankingPlaces.find( + (rp) => rp.systemId === system.id && moment(rp.rankingDate).isSame('2024-09-12', 'day'), ); - const rankingJul = player.rankingPlaces.find( - (rp) => rp.systemId === system.id && moment(rp.rankingDate).isSame('2024-07-01', 'day'), + const rankingAug = player.rankingPlaces.find( + (rp) => rp.systemId === system.id && moment(rp.rankingDate).isSame('2024-08-01', 'day'), ); const averages = await this.calcaulateAverages( player, system, type, - '2024-08-12', - rankingAug, + '2024-09-02', + rankingSep, ); const pointsNeededForPromotion = - system.pointsToGoUp[system.pointsToGoUp.length + 1 - (rankingJul?.[type] ?? 0)]; + system.pointsToGoUp[system.pointsToGoUp.length + 1 - (rankingAug?.[type] ?? 0)]; const pointsNeededForDowngrade = - system.pointsToGoDown[system.pointsToGoDown.length - (rankingJul?.[type] ?? 0)]; + system.pointsToGoDown[system.pointsToGoDown.length - (rankingAug?.[type] ?? 0)]; const shouldHaveGoneUp = - rankingAug?.[type + 'Points'] > pointsNeededForPromotion && - rankingJul?.[type] == rankingAug?.[type]; + rankingSep?.[type + 'Points'] > pointsNeededForPromotion && + rankingAug?.[type] == rankingSep?.[type]; let shouldHaveGoneDown = averages.avgDowngrade < pointsNeededForDowngrade && - rankingJul?.[type] == rankingAug?.[type]; + rankingAug?.[type] == rankingSep?.[type]; shouldHaveGoneDown = this.validateShouldHaveGoneDown( shouldHaveGoneDown, type, - rankingJul, + rankingAug, system, ); @@ -117,9 +117,9 @@ export class ExportPlayersWithRanking { Name: player.fullName, Number: player.memberId, Gender: player.gender, + ['Ranking sep']: rankingSep?.[type], ['Ranking aug']: rankingAug?.[type], - ['Ranking jul']: rankingJul?.[type], - ['Points upgrade']: rankingAug?.[type + 'Points'], + ['Points upgrade']: rankingSep?.[type + 'Points'], ['Points downgrade']: averages.avgDowngrade ? Math.round(averages.avgDowngrade) : '', ['Points needed']: pointsNeededForPromotion, ['Should have gone up']: shouldHaveGoneUp ? 'Yes' : 'No', diff --git a/apps/scripts/src/app/scripts/index.ts b/apps/scripts/src/app/scripts/index.ts index a59fc1c10c..6ea0f7e68c 100644 --- a/apps/scripts/src/app/scripts/index.ts +++ b/apps/scripts/src/app/scripts/index.ts @@ -2,3 +2,5 @@ export * from './add-locations-to-cp'; export * from './changed-encounters'; export * from './export-players-with-ranking'; export * from './incorrect-change-encounters'; +export * from './players-with-wrong-ranking'; +export * from './wrong-dates'; diff --git a/apps/scripts/src/app/scripts/players-with-wrong-ranking/index.ts b/apps/scripts/src/app/scripts/players-with-wrong-ranking/index.ts new file mode 100644 index 0000000000..e6b3ae5a3a --- /dev/null +++ b/apps/scripts/src/app/scripts/players-with-wrong-ranking/index.ts @@ -0,0 +1 @@ +export * from './players-with-wrong-ranking'; diff --git a/apps/scripts/src/app/scripts/wrong-dates/index.ts b/apps/scripts/src/app/scripts/wrong-dates/index.ts new file mode 100644 index 0000000000..98221e9bc6 --- /dev/null +++ b/apps/scripts/src/app/scripts/wrong-dates/index.ts @@ -0,0 +1 @@ +export * from './wrong-dates.service'; diff --git a/apps/scripts/src/app/scripts/wrong-dates/wrong-dates.service.ts b/apps/scripts/src/app/scripts/wrong-dates/wrong-dates.service.ts index dee4468d4b..2ed0e77639 100644 --- a/apps/scripts/src/app/scripts/wrong-dates/wrong-dates.service.ts +++ b/apps/scripts/src/app/scripts/wrong-dates/wrong-dates.service.ts @@ -1,8 +1,10 @@ import { EncounterCompetition } from '@badman/backend-database'; import { VisualService } from '@badman/backend-visual'; +import { getSeasonPeriod } from '@badman/utils'; import { Injectable, Logger } from '@nestjs/common'; import moment from 'moment-timezone'; import { Op } from 'sequelize'; +import xlsx from 'xlsx'; @Injectable() export class WrongDatesService { @@ -11,14 +13,13 @@ export class WrongDatesService { constructor(private readonly visualService: VisualService) {} - async fixWrongDates(year: number) { - const startDate = moment([year, 9, 1]).toDate(); - const endDate = moment([year + 1, 7, 1]).toDate(); + async process(season: number) { + const period = getSeasonPeriod(season) as [string, string]; const encounters = await EncounterCompetition.findAll({ where: { date: { - [Op.between]: [startDate, endDate], + [Op.between]: period, }, originalDate: { [Op.ne]: null, @@ -28,7 +29,7 @@ export class WrongDatesService { this.logger.log(`Found ${encounters.length} encounters`); - const wrongTimezone = []; + const toChange = []; const notChanged = []; for (const encounter of encounters) { @@ -48,7 +49,7 @@ export class WrongDatesService { return; } - const result = await this.visualService.getDate(event.visualCode, encounter.visualCode); + const result = await this.visualService.getDate(event.visualCode, encounter.visualCode, false); const dateBrussels = moment.tz(result, 'Europe/Brussels'); @@ -58,23 +59,6 @@ export class WrongDatesService { const home = await encounter.getHome(); const away = await encounter.getAway(); - const gmtWrong = moment.tz(result, 'Etc/GMT+0'); - // Check if it was submitted without timezone update - if (gmtWrong.isSame(moment(encounter.date))) { - wrongTimezone.push( - `${home.name},${away.name},${gmtWrong.format( - this.visualFormat, - )},${moment(encounter.date).format(this.visualFormat)}`, - ); - - // this.visualService.changeDate( - // event.visualCode, - // encounter.visualCode, - // encounter.date - // ); - - continue; - } if (dateBrussels.isSame(moment(encounter.originalDate))) { notChanged.push( @@ -83,6 +67,13 @@ export class WrongDatesService { )},${moment(encounter.date).format(this.visualFormat)}`, ); + toChange.push({ + home: home.name, + away: away.name, + visual: dateBrussels.format(this.visualFormat), + badman: moment(encounter.date).format(this.visualFormat), + }); + // this.visualService.changeDate( // event.visualCode, // encounter.visualCode, @@ -91,26 +82,21 @@ export class WrongDatesService { continue; } - // this.logger.log(`Changing date for encounter ${encounter.id}`); - // encounter.synced = new Date(); } catch (error) { this.logger.error(error); } } - this.logger.debug(`Wrong timezone: ${wrongTimezone.length}`); this.logger.debug(`Not changed: ${notChanged.length}`); - this.logger.log('Done'); + // wirte to xlsx + const wb = xlsx.utils.book_new(); + const ws = xlsx.utils.json_to_sheet(toChange); + xlsx.utils.book_append_sheet(wb, ws, 'To change'); + xlsx.writeFile(wb, `wrong-dates-${season}.xlsx`); + - // await writeFile( - // 'wrong-timezone.csv', - // `home,away,wrong,right\n${wrongTimezone.join('\n')}` - // ); - // await writeFile( - // 'wrong-date.csv', - // `home,away,wrong,right\n${notChanged.join('\n')}` - // ); + this.logger.log('Done'); } } diff --git a/badman.babel b/badman.babel index 48914259d4..23b5276ec8 100644 --- a/badman.babel +++ b/badman.babel @@ -946,6 +946,25 @@ + + potential + + + + + en-US + false + + + fr-BE + false + + + nl-BE + false + + + title diff --git a/libs/backend/competition/assembly/src/models/assembly.model.ts b/libs/backend/competition/assembly/src/models/assembly.model.ts index 39c23dbd30..961e361a2f 100644 --- a/libs/backend/competition/assembly/src/models/assembly.model.ts +++ b/libs/backend/competition/assembly/src/models/assembly.model.ts @@ -95,6 +95,7 @@ export class AssemblyValidationData { meta?: MetaEntry; otherMeta?: MetaEntry[]; team?: Team; + previousSeasonTeam?: Team | null; teamIndex?: number; teamPlayers?: Player[]; diff --git a/libs/backend/competition/assembly/src/services/validate/assembly.service.ts b/libs/backend/competition/assembly/src/services/validate/assembly.service.ts index 18020d5e6d..66722406f3 100644 --- a/libs/backend/competition/assembly/src/services/validate/assembly.service.ts +++ b/libs/backend/competition/assembly/src/services/validate/assembly.service.ts @@ -9,6 +9,7 @@ import { RankingLastPlace, RankingPlace, RankingSystem, + Standing, SubEventCompetition, Team, } from '@badman/backend-database'; @@ -84,13 +85,33 @@ export class AssemblyValidationService extends ValidationService< const idSubs = args.subtitudes?.filter((p) => p !== undefined && p !== null); const team = await Team.findByPk(args.teamId, { - attributes: ['id', 'name', 'type', 'teamNumber', 'clubId'], + attributes: ['id', 'name', 'type', 'teamNumber', 'clubId', 'link', 'season'], }); - if (!team) { + if (!team?.season) { throw new Error('Team not found'); } + const previousSeasonTeam = await Team.findOne({ + attributes: ['id'], + where: { + link: team.link, + season: team.season - 1, + }, + include: [ + { + attributes: ['id'], + model: EventEntry, + include: [ + { + attributes: ['id', 'faller'], + model: Standing, + }, + ], + }, + ], + }); + const encounter = (await EncounterCompetition.findByPk(args.encounterId)) || undefined; let draw: DrawCompetition | null = null; @@ -304,8 +325,6 @@ export class AssemblyValidationService extends ValidationService< }), ); - // const titularsTeam = Team.baseTeam(players, type); - return { type, meta, @@ -321,6 +340,7 @@ export class AssemblyValidationService extends ValidationService< event, team, + previousSeasonTeam, system, diff --git a/libs/backend/competition/assembly/src/services/validate/rules/team-subevent-index.rule.ts b/libs/backend/competition/assembly/src/services/validate/rules/team-subevent-index.rule.ts index b2294fad2a..3a74f27ad2 100644 --- a/libs/backend/competition/assembly/src/services/validate/rules/team-subevent-index.rule.ts +++ b/libs/backend/competition/assembly/src/services/validate/rules/team-subevent-index.rule.ts @@ -12,12 +12,19 @@ export type TeamSubeventIndexRuleParams = { export class TeamSubeventIndexRule extends Rule { static override readonly description = 'all.rules.team-assembly.team-subevent-index'; async validate(assembly: AssemblyValidationData): Promise { - const { teamIndex, subEvent } = assembly; + const { teamIndex, subEvent, previousSeasonTeam } = assembly; if (!subEvent?.minBaseIndex) { throw new Error('Subevent is not defined'); } + // if team is degraded, it can have a lower index + if (previousSeasonTeam?.entry?.standing?.faller) { + return { + valid: true, + }; + } + if ((teamIndex ?? 0) < subEvent.minBaseIndex) { return { valid: false, diff --git a/libs/backend/competition/change-encounter/src/services/validate/rules/location-free.rule.ts b/libs/backend/competition/change-encounter/src/services/validate/rules/location-free.rule.ts index a497c8ea6c..9f7c9827b8 100644 --- a/libs/backend/competition/change-encounter/src/services/validate/rules/location-free.rule.ts +++ b/libs/backend/competition/change-encounter/src/services/validate/rules/location-free.rule.ts @@ -47,15 +47,19 @@ export class LocationRule extends Rule { }; } - const warning = await this.checkifLocationIsFree( + const { err, warn } = await this.checkifLocationIsFree( encounter.date, encounter.locationId, encounter.id, locations, false, ); - if (warning.length) { - errors.push(...warning); + if (err.length) { + errors.push(...err); + } + + if (warn.length) { + warnings.push(...warn); } // if we have suggested dates for the working encounter, we need to check if that date would give a warning @@ -66,15 +70,18 @@ export class LocationRule extends Rule { continue; } - const warning = await this.checkifLocationIsFree( + const { err, warn } = await this.checkifLocationIsFree( suggestedDate.date, suggestedDate.locationId, encounter.id, locations, true, ); - if (warning.length) { - warnings.push(...warning); + if (err.length) { + warnings.push(...err); + } + if (warn.length) { + warnings.push(...warn); } } } @@ -93,14 +100,11 @@ export class LocationRule extends Rule { locations: Location[], isSuggested = false, ) { - const errors: EncounterValidationError[] = []; + const err: EncounterValidationError[] = []; + const warn: EncounterValidationError[] = []; - if (!locations) { - return errors; - } - - if (!locationId) { - return errors; + if (!locations || !locationId) { + return { err, warn }; } const encsOnDayAndLocation = await EncounterCompetition.findAll({ @@ -119,9 +123,11 @@ export class LocationRule extends Rule { // all encounters that are on this day and don't have a encounterChange.accepted == false let count = encsOnDayAndLocation.filter((r) => r.encounterChange?.accepted !== false).length; + let countWithouteRplaced = encsOnDayAndLocation.length; if (isSuggested) { count++; + countWithouteRplaced++; } const location = locations.find((r) => r.id === locationId); @@ -150,7 +156,15 @@ export class LocationRule extends Rule { if (slot) { if (count > (slot?.courts ?? 0) / 2) { - errors.push({ + err.push({ + message: 'all.competition.change-encounter.errors.location-not-free', + params: { + encounterId: encounterId, + date: encounterDate, + }, + }); + } else if (countWithouteRplaced > (slot?.courts ?? 0) / 2) { + warn.push({ message: 'all.competition.change-encounter.errors.location-not-free', params: { encounterId: encounterId, @@ -159,7 +173,7 @@ export class LocationRule extends Rule { }); } } else { - errors.push({ + err.push({ message: 'all.competition.change-encounter.errors.location-no-timeslot', params: { encounterId: encounterId, @@ -168,6 +182,9 @@ export class LocationRule extends Rule { }); } - return errors.flat(); + return { + err, + warn, + }; } } diff --git a/libs/backend/graphql/src/resolvers/event/competition/assembly.resolver.ts b/libs/backend/graphql/src/resolvers/event/competition/assembly.resolver.ts index eec11ded32..ee4347f4ad 100644 --- a/libs/backend/graphql/src/resolvers/event/competition/assembly.resolver.ts +++ b/libs/backend/graphql/src/resolvers/event/competition/assembly.resolver.ts @@ -26,6 +26,9 @@ export class AssemblyResolver { return this.assemblyService.validate(assembly, { playerId: user.id, teamId: assembly.teamId }); } + + + @ResolveField(() => [PlayerRankingType]) async titularsPlayers(@Parent() assembly: AssemblyOutput): Promise { if (!assembly.titularsPlayerData) return []; @@ -87,6 +90,7 @@ export class AssemblyResolver { if (!assembly) throw new Error('Assembly is required'); if (!assembly.encounterId) throw new Error('Encounter is required'); if (!assembly.teamId) throw new Error('Team is required'); + if (!user?.id) throw new Error('User is required'); this.logger.debug( `Saving assembly for encounter ${assembly.encounterId} and team ${assembly.teamId}, by player ${user.fullName}`, diff --git a/libs/backend/graphql/src/resolvers/event/competition/encounter.resolver.ts b/libs/backend/graphql/src/resolvers/event/competition/encounter.resolver.ts index 0051c4fd99..45231b2623 100644 --- a/libs/backend/graphql/src/resolvers/event/competition/encounter.resolver.ts +++ b/libs/backend/graphql/src/resolvers/event/competition/encounter.resolver.ts @@ -109,9 +109,13 @@ export class EncounterCompetitionResolver { @ResolveField(() => [Assembly]) async assemblies( + @User() user: Player, @Parent() encounter: EncounterCompetition, @Args() listArgs: ListArgs, ): Promise { + if (!user?.id){ + return []; + } return encounter.getAssemblies(ListArgs.toFindOptions(listArgs)); } diff --git a/libs/backend/translate/assets/i18n/en/all.json b/libs/backend/translate/assets/i18n/en/all.json index 6d38483b94..a2227173a8 100644 --- a/libs/backend/translate/assets/i18n/en/all.json +++ b/libs/backend/translate/assets/i18n/en/all.json @@ -59,6 +59,7 @@ "validation": { "all": "Everything", "invalid": "Contains errors", + "potential": "Potential problems", "title": "Validation", "valid": "Contains no errors" } diff --git a/libs/backend/translate/assets/i18n/fr_BE/all.json b/libs/backend/translate/assets/i18n/fr_BE/all.json index ab87a72af7..ce9d7510f7 100644 --- a/libs/backend/translate/assets/i18n/fr_BE/all.json +++ b/libs/backend/translate/assets/i18n/fr_BE/all.json @@ -59,6 +59,7 @@ "validation": { "all": "Tous", "invalid": "Contient des erreurs", + "potential": "Problèmes potentiels", "title": "Validation", "valid": "Ne contient pas d'erreurs" } diff --git a/libs/backend/translate/assets/i18n/nl_BE/all.json b/libs/backend/translate/assets/i18n/nl_BE/all.json index ed4b627454..51d740bd2a 100644 --- a/libs/backend/translate/assets/i18n/nl_BE/all.json +++ b/libs/backend/translate/assets/i18n/nl_BE/all.json @@ -59,6 +59,7 @@ "validation": { "all": "Alles", "invalid": "Bevat fouten", + "potential": "Potentiële problemen", "title": "Validatie", "valid": "Bevat geen fouten" } diff --git a/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.html b/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.html index de8051d07a..ed6bf44121 100644 --- a/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.html +++ b/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.html @@ -72,6 +72,9 @@ }} {{ 'all.club.encounters.validation.valid' | translate + }} + {{ + 'all.club.encounters.validation.potential' | translate }} {{ 'all.club.encounters.validation.invalid' | translate diff --git a/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.service.ts b/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.service.ts index c68598bb96..ed063f61d1 100644 --- a/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.service.ts +++ b/libs/frontend/pages/club/src/pages/detail/club-encounters/club-encounters.service.ts @@ -4,7 +4,6 @@ import { FormControl, FormGroup } from '@angular/forms'; import { EncounterCompetition } from '@badman/frontend-models'; import { getSeasonPeriod, sortTeams } from '@badman/utils'; import { Apollo, gql } from 'apollo-angular'; -import { valid } from 'joi'; import moment from 'moment'; import { signalSlice } from 'ngxtension/signal-slice'; import { EMPTY, Observable, Subject, merge } from 'rxjs'; @@ -30,7 +29,7 @@ export interface ClubEncounterState { filterValidGames: validationFilter; } -export type validationFilter = 'all' | 'valid' | 'invalid'; +export type validationFilter = 'all' | 'valid' | 'invalid' | 'potential'; export type openRequestFilter = 'all' | 'openRequests' | 'noRequests'; @Injectable({ @@ -89,11 +88,14 @@ export class ClubEncounterService { if (this.state().filterOpenRequests === 'noRequests') { filtered = filtered.filter((encounter) => encounter.encounterChange?.accepted ?? true); } + + console.log(this.state().filterValidGames); - if (this.state().filterValidGames !== 'all') { + if (this.state().filterValidGames == 'invalid') { + filtered = filtered.filter((encounter) => encounter.validateEncounter?.valid == false); + } else if (this.state().filterValidGames == 'potential') { filtered = filtered.filter( - (encounter) => - encounter.validateEncounter?.valid == (this.state().filterValidGames === 'valid'), + (encounter) => (encounter.validateEncounter?.warnings?.length ?? 0) > 0, ); } diff --git a/libs/frontend/pages/competition/team-assembly/src/pages/create/components/assembly/assembly.component.ts b/libs/frontend/pages/competition/team-assembly/src/pages/create/components/assembly/assembly.component.ts index 37ee772feb..3510ce8d6a 100644 --- a/libs/frontend/pages/competition/team-assembly/src/pages/create/components/assembly/assembly.component.ts +++ b/libs/frontend/pages/competition/team-assembly/src/pages/create/components/assembly/assembly.component.ts @@ -1053,7 +1053,7 @@ export class AssemblyComponent implements OnInit { }; private _loadSaved(encounterId?: string, captainId?: string) { - if (!this.authenticateService.loggedIn || !encounterId) { + if (!this.authenticateService.loggedIn() || !encounterId) { return of([]); } diff --git a/libs/utils/src/lib/i18n.generated.ts b/libs/utils/src/lib/i18n.generated.ts index 64f7513ea1..80e365134c 100644 --- a/libs/utils/src/lib/i18n.generated.ts +++ b/libs/utils/src/lib/i18n.generated.ts @@ -66,6 +66,7 @@ export type I18nTranslations = { "validation": { "all": string; "invalid": string; + "potential": string; "title": string; "valid": string; };