Compare commits

...

15 Commits

Author SHA1 Message Date
87d9291bfb Use substitutions for disable button test
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 23:19:00 +01:00
c646ae15ee Optionally disable starting a match and show reason
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 23:00:00 +01:00
Michel ten Voorde
8128ba4744 Cleanup
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 16:42:48 +01:00
Michel ten Voorde
19e4372006 Show substitutions in standings 2025-11-06 16:42:22 +01:00
Michel ten Voorde
7fd84005f9 Show 100 players by default 2025-11-06 16:41:03 +01:00
Michel ten Voorde
a56317fecd Update counter 2025-11-06 16:40:20 +01:00
Michel ten Voorde
ecbb16776c Don't show teams inline when entering match result
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-03 14:32:41 +01:00
Michel ten Voorde
3008f45dfa Use autocompletion for partner search
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-31 16:02:15 +01:00
Michel ten Voorde
5baf1228f7 Use autocompletion for partner search, WIP 2025-10-31 15:13:22 +01:00
Michel ten Voorde
aacf06e203 Made tournament-players more standalone, now works from tournament screen 2025-10-29 17:27:32 +01:00
368aa53b9d match-result simplification
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-29 00:12:11 +01:00
335577fa4d match-result simplification 2025-10-29 00:09:14 +01:00
1f55acecd0 Use snackbars instead of alerts
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-28 09:26:26 +01:00
c1fedd728b Use inline on round overview 2025-10-28 09:22:36 +01:00
0e9e8d1e0f Put event on top on match sheets 2025-10-28 09:22:21 +01:00
19 changed files with 440 additions and 168 deletions

View File

@@ -0,0 +1,22 @@
<mat-dialog-content>
<h3>Kies een teller:</h3>
<div class="col-md-6">
<mat-form-field appearance="fill">
<mat-label>Teller</mat-label>
<mat-select [(ngModel)]="counter">
<mat-option>Geen</mat-option>
@for (player of data.availableCounters; track player.playerId) {
<mat-option [value]="player">
{{ player.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions>
@if (counter) {
<button mat-button [mat-dialog-close]="{counter: counter}">Opslaan</button>
}
<button mat-button (click)="onAnnulerenClick()">Annuleren</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,4 @@
button:disabled {
cursor: not-allowed;
pointer-events: all !important;
}

View File

@@ -0,0 +1,49 @@
import {Component, inject, Inject} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogRef
} from "@angular/material/dialog";
import {Match} from "../../model/match";
import {MatButton} from "@angular/material/button";
import {TournamentPlayer} from "../../model/tournamentPlayer";
import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatOption, MatSelect} from "@angular/material/select";
import {FormsModule} from "@angular/forms";
@Component({
selector: 'app-counter-selection',
imports: [
MatDialogContent,
MatButton,
MatDialogClose,
MatDialogActions,
MatFormField,
MatLabel,
MatOption,
MatSelect,
FormsModule,
],
templateUrl: './counter-selection.component.html',
standalone: true,
styleUrl: './counter-selection.component.scss'
})
export class CounterSelectionComponent {
counter: TournamentPlayer;
readonly dialogRef = inject(MatDialogRef<CounterSelectionComponent>);
constructor(@Inject(MAT_DIALOG_DATA) public data: {
match: Match,
availableCounters: TournamentPlayer[]
}) {}
onAnnulerenClick() {
this.dialogRef.close();
}
}

View File

@@ -7,86 +7,76 @@
<b>{{ data.group.name }} {{ data.round.name }}</b> <b>{{ data.group.name }} {{ data.round.name }}</b>
</div> </div>
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(1, 1)">21</button>
</mat-grid-tile>
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(1, 2)">21</button>
</mat-grid-tile>
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(1, 3)">21</button>
</mat-grid-tile>
@for (gameNum of [1, 2, 3]; track gameNum) {
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(1, gameNum)">21</button>
</mat-grid-tile>
}
<mat-grid-tile colspan="2"> <mat-grid-tile colspan="2">
<div class="w-100" [ngClass]="{'winner': validateResult() == 1}"> <div class="w-100" [ngClass]="{'winner': isValidResult == 1}">
<app-team-display <app-team-display
[team]="data.match.team1" [team]="data.match.team1"
[event]="data.event" [event]="data.event"
[tournament]="data.tournament" [tournament]="data.tournament"
[inline]="true"> [inline]="false">
</app-team-display> </app-team-display>
</div> </div>
</mat-grid-tile> </mat-grid-tile>
@for (game of result.games; track $index; let i = $index) {
<mat-grid-tile> <mat-grid-tile>
<mat-form-field appearance="outline" (change)="validateResult()"> <mat-form-field appearance="outline">
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[0].score1"> <input matInput
</mat-form-field> type="number"
</mat-grid-tile> min="0"
<mat-grid-tile> max="30"
<mat-form-field appearance="outline" (change)="validateResult()"> [(ngModel)]="game.score1"
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[1].score1"> (blur)="complementScores()"
</mat-form-field> (ngModelChange)="validateResult()"
</mat-grid-tile> [tabindex]="i * 2 + 1">
<mat-grid-tile>
<mat-form-field appearance="outline" (change)="validateResult()">
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[2].score1">
</mat-form-field> </mat-form-field>
</mat-grid-tile> </mat-grid-tile>
}
<mat-grid-tile colspan="2"> <mat-grid-tile colspan="2">
<div class="w-100" [ngClass]="{'winner': validateResult() == -1}"> <div class="w-100" [ngClass]="{'winner': isValidResult == -1}">
<app-team-display <app-team-display
[team]="data.match.team2" [team]="data.match.team2"
[event]="data.event" [event]="data.event"
[tournament]="data.tournament" [tournament]="data.tournament"
[inline]="true"> [inline]="false">
</app-team-display> </app-team-display>
</div> </div>
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile>
<mat-form-field appearance="outline" (change)="validateResult()">
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[0].score2">
</mat-form-field>
</mat-grid-tile>
<mat-grid-tile>
<mat-form-field appearance="outline" (change)="validateResult()">
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[1].score2">
</mat-form-field>
</mat-grid-tile>
<mat-grid-tile>
<mat-form-field appearance="outline" (change)="validateResult()">
<input matInput type="number" min="0" max="30" [(ngModel)]="result.games[2].score2">
</mat-form-field>
</mat-grid-tile>
@for (game of result.games; track $index; let i = $index) {
<mat-grid-tile> <mat-grid-tile>
<mat-form-field appearance="outline">
<input matInput
type="number"
min="0"
max="30"
[(ngModel)]="game.score2"
(blur)="complementScores()"
(ngModelChange)="validateResult()"
[tabindex]="i * 2 + 2">
</mat-form-field>
</mat-grid-tile>
}
<mat-grid-tile></mat-grid-tile>
<mat-grid-tile></mat-grid-tile>
</mat-grid-tile> @for (gameNum of [1, 2, 3]; track gameNum) {
<mat-grid-tile> <mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(2, gameNum)">21</button>
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile> }
<button type="button" class="btn btn-primary btn-lg" (click)="set21(2, 1)">21</button>
</mat-grid-tile>
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(2, 2)">21</button>
</mat-grid-tile>
<mat-grid-tile>
<button type="button" class="btn btn-primary btn-lg" (click)="set21(2, 3)">21</button>
</mat-grid-tile>
</mat-grid-list> </mat-grid-list>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button (click)="onAnnulerenClick()">Annuleren</button> <button mat-button (click)="onAnnulerenClick()">Annuleren</button>
<button mat-button [disabled]="validateResult() == 0" [mat-dialog-close]="result">Opslaan</button> <button mat-button [disabled]="isValidResult == 0" [mat-dialog-close]="result">Opslaan</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -51,6 +51,7 @@ import {Tournament} from "../../model/tournament";
export class MatchResultComponent { export class MatchResultComponent {
result: Result = new Result(); result: Result = new Result();
isValidResult: number = 0;
constructor(@Inject(MAT_DIALOG_DATA) public data: {match: Match, tournament: Tournament, event: Event, group: Group, round: Round}) { constructor(@Inject(MAT_DIALOG_DATA) public data: {match: Match, tournament: Tournament, event: Event, group: Group, round: Round}) {
this.result.matchId = this.data.match.id; this.result.matchId = this.data.match.id;
@@ -67,6 +68,8 @@ export class MatchResultComponent {
this.result.games.push(new Game()); this.result.games.push(new Game());
} }
} }
this.validateResult();
} }
readonly dialogRef = inject(MatDialogRef<MatchResultComponent>); readonly dialogRef = inject(MatDialogRef<MatchResultComponent>);
@@ -79,13 +82,18 @@ export class MatchResultComponent {
this.result.games[game - 1].score2 = 21; this.result.games[game - 1].score2 = 21;
} }
this.validateResult();
} }
onAnnulerenClick() { onAnnulerenClick() {
this.dialogRef.close(); this.dialogRef.close();
} }
validateResult(): number { complementScores() {
// console.log("in complementScores");
}
validateResult(): void {
let valid : boolean = true; let valid : boolean = true;
valid &&= this.gameValid(this.result.games[0].score1, this.result.games[0].score2); valid &&= this.gameValid(this.result.games[0].score1, this.result.games[0].score2);
valid &&= this.gameValid(this.result.games[1].score1, this.result.games[1].score2); valid &&= this.gameValid(this.result.games[1].score1, this.result.games[1].score2);
@@ -94,7 +102,7 @@ export class MatchResultComponent {
valid &&= this.gameValid(this.result.games[2].score1, this.result.games[2].score2); valid &&= this.gameValid(this.result.games[2].score1, this.result.games[2].score2);
} }
return valid ? this.matchResult(this.result) : 0; this.isValidResult = valid ? this.matchResult(this.result) : 0;
} }
gameValid(score1: number, score2: number): boolean { gameValid(score1: number, score2: number): boolean {
@@ -117,29 +125,31 @@ export class MatchResultComponent {
} }
matchResult(result: Result): number { matchResult(result: Result): number {
let gameBalance = 0; let team1Wins = 0;
if (result.games[0].score1 < result.games[0].score2) { let team2Wins = 0;
gameBalance--;
} else { // Count wins for games 1 and 2
gameBalance++; if (result.games[0].score1 > result.games[0].score2) team1Wins++;
else team2Wins++;
if (result.games[1].score1 > result.games[1].score2) team1Wins++;
else team2Wins++;
// If match is already decided (2-0), game 3 shouldn't have scores
if (Math.max(team1Wins, team2Wins) == 2) {
if (result.games[2].score1 != undefined || result.games[2].score2 != undefined) {
return 0; // Invalid
} }
if (result.games[1].score1 < result.games[1].score2) { return team1Wins > team2Wins ? 1 : -1;
gameBalance--;
} else {
gameBalance++;
}
if (Math.abs(gameBalance) == 2 && (result.games[2].score1 != undefined || result.games[2].score2 != undefined)) {
return 0;
} }
// Match is 1-1, check game 3
if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) { if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) {
if (result.games[2].score1 < result.games[2].score2) { if (result.games[2].score1 > result.games[2].score2) team1Wins++;
gameBalance--; else team2Wins++;
} else { return team1Wins > team2Wins ? 1 : -1;
gameBalance++; }
}
} return 0; // Incomplete
return Math.sign(gameBalance);
} }
} }

View File

@@ -3,10 +3,17 @@
<div class="nobreak"> <div class="nobreak">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content>
<h6>{{ tournament.name }}</h6>
<br>
<br>
<div class="row"> <div class="row">
<div class="col-6">
<h5>{{ tournament.name }}</h5>
</div>
<div class="col-6 text-end">
{{ group.name }} {{ round.name }}
</div>
</div>
<br>
<br>
<div class="row align-middle">
<div class="col-6"> <div class="col-6">
<b> <b>
<app-team-display <app-team-display
@@ -35,12 +42,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<b>
<app-team-display <app-team-display
[team]="match.team2" [team]="match.team2"
[event]="this.event" [event]="this.event"
[tournament]="this.tournament" [tournament]="this.tournament"
[inline]="true"> [inline]="true">
</app-team-display> </app-team-display>
</b>
</div> </div>
<div class="col-2"> <div class="col-2">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -62,9 +71,6 @@
<div class="col-6"> <div class="col-6">
<u>Graag de winnaar omcirkelen</u> <u>Graag de winnaar omcirkelen</u>
</div> </div>
<div class="col-6 text-end">
{{ group.name }} {{ round.name }}
</div>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@@ -12,15 +12,24 @@ import {MatTooltip} from '@angular/material/tooltip';
styleUrls: ['./player-display.component.scss'], styleUrls: ['./player-display.component.scss'],
template: ` template: `
@let substitute = getSubstituteForEvent(player, event); @let substitute = getSubstituteForEvent(player, event);
@if (exlicitSubstitute) {
@if (substitute) {
{{ substitute }} (valt in voor {{ player | fullName }})
} @else {
{{ player | fullName }}
}
} @else {
<span [class.has-substitute]="substitute" <span [class.has-substitute]="substitute"
[matTooltip]="substitute ? 'Valt in voor ' + (player | fullName) : ''" [matTooltip]="substitute ? 'Valt in voor ' + (player | fullName) : ''"
matTooltipPosition="below">{{ substitute || (player | fullName) }}</span> matTooltipPosition="below">{{ substitute || (player | fullName) }}</span>
}
` `
}) })
export class PlayerDisplayComponent { export class PlayerDisplayComponent {
@Input({ required: true }) player!: Player; @Input({ required: true }) player!: Player;
@Input({ required: true }) event!: Event; @Input({ required: true }) event!: Event;
@Input({ required: true }) tournament!: Tournament; @Input({ required: true }) tournament!: Tournament;
@Input({ required: false }) exlicitSubstitute: boolean = false;
getSubstituteForEvent(player: Player, event: Event): string | undefined { getSubstituteForEvent(player: Player, event: Event): string | undefined {
const tournamentPlayer = this.tournament.tournamentPlayers.find( const tournamentPlayer = this.tournament.tournamentPlayers.find(

View File

@@ -39,7 +39,8 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
<mat-paginator [pageSizeOptions]="[10, 20, 50]" <mat-paginator [pageSizeOptions]="[10, 25, 100]"
[pageSize]="100"
showFirstLastButtons showFirstLastButtons
aria-label="Select page of periodic elements"> aria-label="Select page of periodic elements">
</mat-paginator> </mat-paginator>

View File

@@ -5,3 +5,7 @@ a {
td, th { td, th {
background-color: transparent !important; background-color: transparent !important;
} }
.mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.075);
}

View File

@@ -18,14 +18,23 @@
@if (eventRegistration.doublesEvent) { @if (eventRegistration.doublesEvent) {
<mat-form-field appearance="fill"> <mat-form-field appearance="fill">
<mat-label>Partner</mat-label> <mat-label>Partner</mat-label>
<mat-select [value]="eventRegistration.partner" [disabled]="!tournamentRegistration.editable || !eventRegistration.registered" [(ngModel)]="eventRegistration.partner"> <input type="text"
<mat-option>Geen</mat-option> matInput
@for (player of getRelevantPlayers(eventRegistration.type); track player.id) { [matAutocomplete]="auto"
[disabled]="!tournamentRegistration.editable || !eventRegistration.registered"
[(ngModel)]="eventRegistration.partner"
(input)="onPartnerSearch($any($event.target).value, eventRegistration)"
[name]="'partner-' + eventRegistration.id">
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayPartnerName.bind(this)"
(optionSelected)="onPartnerSelected($event, eventRegistration)">
<mat-option [value]="null">Geen</mat-option>
@for (player of getFilteredPlayers(eventRegistration); track player.id) {
<mat-option [value]="player.id"> <mat-option [value]="player.id">
{{ player | fullName }} {{ player | fullName }}
</mat-option> </mat-option>
} }
</mat-select> </mat-autocomplete>
</mat-form-field> </mat-form-field>
} }
</div> </div>

View File

@@ -9,12 +9,13 @@ import {RegistrationService} from "../../service/registration.service";
import {MatCheckbox, MatCheckboxChange} from "@angular/material/checkbox"; import {MatCheckbox, MatCheckboxChange} from "@angular/material/checkbox";
import {EventRegistration, TournamentRegistration} from "../../model/tournamentRegistration"; import {EventRegistration, TournamentRegistration} from "../../model/tournamentRegistration";
import {MatOption} from "@angular/material/core"; import {MatOption} from "@angular/material/core";
import {MatSelect} from "@angular/material/select";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatAnchor, MatButton} from "@angular/material/button"; import {MatAnchor, MatButton} from "@angular/material/button";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {HeaderService} from "../../service/header.service"; import {HeaderService} from "../../service/header.service";
import {MatAutocomplete, MatAutocompleteTrigger} from "@angular/material/autocomplete";
import {MatInput} from "@angular/material/input";
@Component({ @Component({
selector: 'app-player-registrations', selector: 'app-player-registrations',
@@ -30,11 +31,14 @@ import {HeaderService} from "../../service/header.service";
MatCardActions, MatCardActions,
RouterLink, RouterLink,
MatOption, MatOption,
MatSelect, // MatSelect,
MatIcon, MatIcon,
MatButton, MatButton,
MatAnchor, MatAnchor,
FullNamePipe FullNamePipe,
MatAutocomplete,
MatAutocompleteTrigger,
MatInput,
], ],
providers: [ providers: [
FullNamePipe FullNamePipe
@@ -52,6 +56,8 @@ export class PlayerRegistrationsComponent implements OnInit {
waitingForBackend: boolean = false; waitingForBackend: boolean = false;
private partnerSearchTerms: Map<number, string> = new Map();
constructor( constructor(
private _snackBar: MatSnackBar, private _snackBar: MatSnackBar,
private playerService: PlayerService, private playerService: PlayerService,
@@ -76,6 +82,40 @@ export class PlayerRegistrationsComponent implements OnInit {
}); });
} }
onPartnerSearch(searchTerm: any, eventRegistration: EventRegistration) {
// Only treat as search if it's a string (typed input)
if (typeof searchTerm === 'string') {
this.partnerSearchTerms.set(eventRegistration.id, searchTerm?.toLowerCase() || '');
}
}
getFilteredPlayers(eventRegistration: EventRegistration): Player[] {
const allRelevant = this.getRelevantPlayers(eventRegistration.type);
const searchTerm = this.partnerSearchTerms.get(eventRegistration.id);
if (!searchTerm) {
return allRelevant;
}
return allRelevant.filter(player => {
const fullName = this.fullNamePipe.transform(player).toLowerCase();
return fullName.includes(searchTerm);
});
}
displayPartnerName(playerId: number | null): string {
if (!playerId || !this.allPlayers) return '';
const player = this.allPlayers.find(p => p.id === playerId);
return player ? this.fullNamePipe.transform(player) : '';
}
onPartnerSelected(event: any, eventRegistration: EventRegistration) {
eventRegistration.partner = event.option.value;
// Clear the search term when a partner is selected
this.partnerSearchTerms.delete(eventRegistration.id);
}
saveRegistration(tournamentRegistration: TournamentRegistration, event: MouseEvent) { saveRegistration(tournamentRegistration: TournamentRegistration, event: MouseEvent) {
this.waitingForBackend = true; this.waitingForBackend = true;
this.registrationService.saveTournamentRegistrations(tournamentRegistration, this.player.id).subscribe(data => { this.registrationService.saveTournamentRegistrations(tournamentRegistration, this.player.id).subscribe(data => {

View File

@@ -10,7 +10,7 @@
[team]="match.team1" [team]="match.team1"
[event]="this.event" [event]="this.event"
[tournament]="this.tournament" [tournament]="this.tournament"
[inline]="false"> [inline]="true">
</app-team-display> </app-team-display>
</td> </td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
@@ -108,7 +108,8 @@
[team]="entry.team" [team]="entry.team"
[event]="this.event" [event]="this.event"
[tournament]="this.tournament" [tournament]="this.tournament"
[inline]="true"> [inline]="true"
[explicitSubstitute]="true">
</app-team-display> </app-team-display>
</td> </td>
<td class="align-middle">{{ entry.played }}</td> <td class="align-middle">{{ entry.played }}</td>

View File

@@ -12,7 +12,8 @@ import {Team} from "../../model/team";
<app-player-display <app-player-display
[player]="team.player1" [player]="team.player1"
[event]="event" [event]="event"
[tournament]="tournament"> [tournament]="tournament"
[exlicitSubstitute]="explicitSubstitute">
</app-player-display> </app-player-display>
@if (event.doublesEvent && team.player2) { @if (event.doublesEvent && team.player2) {
@@ -34,4 +35,5 @@ export class TeamDisplayComponent {
@Input({ required: true }) event!: Event; @Input({ required: true }) event!: Event;
@Input({ required: true }) tournament!: Tournament; @Input({ required: true }) tournament!: Tournament;
@Input({ required: false }) inline: boolean = true; @Input({ required: false }) inline: boolean = true;
@Input({ required: false }) explicitSubstitute: boolean = false;
} }

View File

@@ -158,9 +158,13 @@
</div> </div>
<mat-action-row> <mat-action-row>
<button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.event, activeMatch.group, activeMatch.round)"> <button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.event, activeMatch.group, activeMatch.round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag invoeren Uitslag invoeren
</button> </button>
<button mat-button (click)="changeCounter(activeMatch.match)">
<mat-icon>person</mat-icon>
Teller wijzigen
</button>
<button mat-button (click)="stopMatch(activeMatch.match)"> <button mat-button (click)="stopMatch(activeMatch.match)">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
Wedstrijd stoppen Wedstrijd stoppen
@@ -297,7 +301,6 @@
} }
</tbody> </tbody>
</table> </table>
<!-- }-->
} @else if (round.status == 'IN_PROGRESS') { } @else if (round.status == 'IN_PROGRESS') {
<table class="table table-hover m-4 wide w-95"> <table class="table table-hover m-4 wide w-95">
<tbody> <tbody>
@@ -320,18 +323,31 @@
</td> </td>
<td class="align-middle w-fill"> <td class="align-middle w-fill">
@if (match.status == 'NOT_STARTED') { @if (match.status == 'NOT_STARTED') {
@if (match.canStart) {
<button mat-button (click)="startMatch(match)"> <button mat-button (click)="startMatch(match)">
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
Wedstrijd starten Wedstrijd starten
</button> </button>
} @else {
<span [matTooltip]="match.cantStartReason" style="cursor: not-allowed;">
<button mat-button disabled>
<mat-icon>play_arrow</mat-icon>
Wedstrijd starten
</button>
</span>
}
} @else if (match.status == 'IN_PROGRESS') { } @else if (match.status == 'IN_PROGRESS') {
<button mat-button (click)="editResult(match, event, group, round)"> <button mat-button (click)="editResult(match, event, group, round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag invoeren Uitslag
</button>
<button mat-button (click)="changeCounter(match)">
<mat-icon>person</mat-icon>
Teller
</button> </button>
<button mat-button (click)="stopMatch(match)"> <button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
Wedstrijd stoppen Stoppen
</button> </button>
} @else if (match.status == 'FINISHED') { } @else if (match.status == 'FINISHED') {
<div class="row result align-items-center"> <div class="row result align-items-center">
@@ -347,7 +363,7 @@
</button> </button>
<mat-menu #finishedMatchMenu="matMenu"> <mat-menu #finishedMatchMenu="matMenu">
<button mat-menu-item (click)="editResult(match, event, group, round)"> <button mat-menu-item (click)="editResult(match, event, group, round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag bewerken Uitslag bewerken
</button> </button>
</mat-menu> </mat-menu>
@@ -429,7 +445,14 @@
@for (entry of getStandingsForRound(round, group).entries; track entry.position) { @for (entry of getStandingsForRound(round, group).entries; track entry.position) {
<tr> <tr>
<td class="align-middle">{{ entry.position }}</td> <td class="align-middle">{{ entry.position }}</td>
<td class="align-middle">{{ entry.team | teamText }}</td> <td class="align-middle">
<app-team-display
[team]="entry.team"
[event]="event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</td>
<td class="align-middle">{{ entry.played }}</td> <td class="align-middle">{{ entry.played }}</td>
<td class="align-middle"> <td class="align-middle">
@if (entry.played > 0 ) { @if (entry.played > 0 ) {

View File

@@ -33,8 +33,10 @@ import {Standings} from "../../model/standings";
import {HeaderService} from "../../service/header.service"; import {HeaderService} from "../../service/header.service";
import {TournamentPlayersComponent} from "../tournament-players/tournament-players.component"; import {TournamentPlayersComponent} from "../tournament-players/tournament-players.component";
import {TournamentPlayer} from "../../model/tournamentPlayer"; import {TournamentPlayer} from "../../model/tournamentPlayer";
import {MatTooltip} from "@angular/material/tooltip";
import {TeamDisplayComponent} from "../team-display/team-display.component"; import {TeamDisplayComponent} from "../team-display/team-display.component";
import {CounterSelectionComponent} from "../counter-selection/counter-selection.component";
import {Observable, Subscription, tap} from "rxjs";
import {MatTooltip} from '@angular/material/tooltip';
@Component({ @Component({
selector: 'app-tournament-manage', selector: 'app-tournament-manage',
@@ -65,6 +67,7 @@ import {TeamDisplayComponent} from "../team-display/team-display.component";
TournamentPlayersComponent, TournamentPlayersComponent,
MatExpansionPanelActionRow, MatExpansionPanelActionRow,
TeamDisplayComponent, TeamDisplayComponent,
MatTooltip,
], ],
providers: [ providers: [
FullNamePipe, FullNamePipe,
@@ -97,10 +100,10 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
this.activeRoundTab = params['tab']; this.activeRoundTab = params['tab'];
} }
}) })
this.tournamentService.getById(Number(id)).subscribe(data => { this.loadTournament(
this.tournament = data; this.tournamentService.getById(Number(id)),
this.headerService.setTitle(this.tournament.name); (tournament) => this.headerService.setTitle(tournament.name)
}); );
} }
ngOnDestroy() { ngOnDestroy() {
@@ -151,56 +154,42 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
} }
startRound(round: Round) { startRound(round: Round) {
this.tournamentService.startRound(this.tournament.id, round.id).subscribe(data => { this.loadTournament(this.tournamentService.startRound(this.tournament.id, round.id));
this.tournament = data;
});
} }
finishRound(round: Round) { finishRound(round: Round) {
this.tournamentService.finishRound(this.tournament.id, round.id).subscribe(data => { this.loadTournament(this.tournamentService.finishRound(this.tournament.id, round.id));
this.tournament = data;
});
} }
finishGroup(group: Group) { finishGroup(group: Group) {
this.tournamentService.finishGroup(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.finishGroup(this.tournament.id, group.id));
this.tournament = data;
});
} }
reopenGroup(group: Group) { reopenGroup(group: Group) {
this.tournamentService.reopenGroup(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.reopenGroup(this.tournament.id, group.id));
this.tournament = data;
});
} }
divideTournament() { divideTournament() {
this.tournamentService.divide(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.divide(this.tournament.id));
this.tournament = data;
});
} }
clearDivision() { clearDivision() {
this.tournamentService.clearDivision(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.clearDivision(this.tournament.id));
this.tournament = data;
});
} }
drawTournament() { drawTournament() {
this.tournamentService.draw(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.draw(this.tournament.id));
this.tournament = data;
});
} }
startMatch(match: Match) { startMatch(match: Match) {
const availableCourts = this.getAvailableCourts(); const availableCourts = this.getAvailableCourts();
if (availableCourts.length == 0) { if (availableCourts.length == 0) {
alert('Geen banen beschikbaar!'); this._snackBar.open('Geen banen beschikbaar.')
} else if (this.matchContainsPlayersThatArePlaying(match)) { } else if (this.matchContainsPlayersThatArePlaying(match)) {
alert('Deze wedstrijd bevat spelers die al aan het spelen zijn!'); this._snackBar.open('Deze wedstrijd bevat spelers die al aan het spelen zijn.')
} else if (this.matchContainsPlayersThatAreCounting(match)) { } else if (this.matchContainsPlayersThatAreCounting(match)) {
alert('Deze wedstrijd bevat spelers die aan het tellen zijn!'); this._snackBar.open('Deze wedstrijd bevat spelers die aan het tellen zijn.')
} else { } else {
this.courtSelectionDialog.open(CourtSelectionComponent, { this.courtSelectionDialog.open(CourtSelectionComponent, {
data: { data: {
@@ -213,9 +202,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
minHeight: '250px' minHeight: '250px'
}).afterClosed().subscribe(result => { }).afterClosed().subscribe(result => {
if (result != undefined) { if (result != undefined) {
this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId).subscribe(data => { this.loadTournament(this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId));
this.tournament = data;
});
} }
}); });
} }
@@ -223,26 +210,48 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
matchContainsPlayersThatArePlaying(match: Match): boolean { matchContainsPlayersThatArePlaying(match: Match): boolean {
let matchPlayers: number[] = []; let matchPlayers: number[] = [];
matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team1.player1).id); matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team1.player2).id); if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team2.player1).id); matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team2.player2).id); if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
let matchPlayersThatArePlaying = this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers)); let matchPlayersThatArePlaying = this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers));
return matchPlayersThatArePlaying.length > 0; return matchPlayersThatArePlaying.length > 0;
} }
matchPlayersThatArePlaying(match: Match): number[] {
let matchPlayers: number[] = [];
matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
return this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers));
}
matchContainsPlayersThatAreCounting(match: Match): boolean { matchContainsPlayersThatAreCounting(match: Match): boolean {
let matchPlayers: number[] = []; let matchPlayers: number[] = [];
matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team1.player1).id); matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team1.player2).id); if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team2.player1).id); matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getTournamentPlayerFromPlayer(match.team2.player2).id); if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
let matchPlayersThatAreCounting = this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers)); let matchPlayersThatAreCounting = this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers));
return matchPlayersThatAreCounting.length > 0; return matchPlayersThatAreCounting.length > 0;
} }
matchPlayersThatAreCounting(match: Match): number[] {
let matchPlayers: number[] = [];
matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
return this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers));
}
getTournamentPlayerFromPlayer(player: Player): TournamentPlayer { getTournamentPlayerFromPlayer(player: Player): TournamentPlayer {
for (let tournamentPlayer of this.tournament.tournamentPlayers) { for (let tournamentPlayer of this.tournament.tournamentPlayers) {
if (tournamentPlayer.playerId == player.id) { if (tournamentPlayer.playerId == player.id) {
@@ -302,15 +311,11 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
} }
stopMatch(match: Match) { stopMatch(match: Match) {
this.tournamentService.stopMatch(this.tournament.id, match.id).subscribe(data => { this.loadTournament(this.tournamentService.stopMatch(this.tournament.id, match.id));
this.tournament = data;
})
} }
newRound(group: Group) { newRound(group: Group) {
this.tournamentService.newRound(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.newRound(this.tournament.id, group.id));
this.tournament = data;
})
} }
getStrength(strength: string | undefined) { getStrength(strength: string | undefined) {
@@ -367,6 +372,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
matchResultDialog = inject(MatDialog); matchResultDialog = inject(MatDialog);
courtSelectionDialog = inject(MatDialog); courtSelectionDialog = inject(MatDialog);
counterSelectionDialog = inject(MatDialog);
editResult(match: Match, event: Event, group: Group, round: Round) { editResult(match: Match, event: Event, group: Group, round: Round) {
this.matchResultDialog.open(MatchResultComponent, { this.matchResultDialog.open(MatchResultComponent, {
@@ -374,9 +380,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
minWidth: '800px' minWidth: '800px'
}).afterClosed().subscribe(result => { }).afterClosed().subscribe(result => {
if (result != undefined) { if (result != undefined) {
this.tournamentService.saveResult(this.tournament.id, result.matchId, result).subscribe(data => { this.loadTournament(this.tournamentService.saveResult(this.tournament.id, result.matchId, result));
this.tournament = data;
})
} }
}); });
} }
@@ -427,6 +431,84 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
return count; return count;
} }
changeCounter(match: Match) {
this.counterSelectionDialog.open(CounterSelectionComponent, {
data: {
match: match,
availableCounters: this.getAvailableCounters(match)
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.loadTournament(this.tournamentService.updateCounter(this.tournament.id, match.id, result.counter.playerId));
}
});
}
private loadTournament(
observable: Observable<Tournament>,
afterLoad?: (tournament: Tournament) => void
): Subscription {
return observable.subscribe(data => {
this.tournament = data;
this.updateMatchAvailability();
afterLoad?.(data); // Optional chaining
});
}
private updateMatchAvailability() {
for (const event of this.tournament.events) {
for (const group of event.groups) {
for (const round of group.rounds) {
for (const match of round.matches) {
let canStart = true;
let matchPlayersThatArePlaying = this.matchPlayersThatArePlaying(match);
let matchPlayersThatAreCounting = this.matchPlayersThatAreCounting(match);
let cantStartReason = "";
if (matchPlayersThatArePlaying.length > 0) {
canStart = false;
cantStartReason = this.joinWithEn(matchPlayersThatArePlaying.map(m => this.getTournamentPlayerFromId(m).name));
if (matchPlayersThatArePlaying.length == 1) {
cantStartReason += " is al aan het spelen";
} else {
cantStartReason += " zijn al aan het spelen";
}
}
if (matchPlayersThatAreCounting.length > 0) {
canStart = false;
if (cantStartReason.length > 0) {
cantStartReason += " en ";
}
cantStartReason += this.joinWithEn(matchPlayersThatAreCounting.map(m => this.getTournamentPlayerFromId(m).name));
if (matchPlayersThatAreCounting.length == 1) {
cantStartReason += " is aan het tellen";
} else {
cantStartReason += " zijn aan het tellen";
}
}
match.canStart = canStart;
match.cantStartReason = cantStartReason;
}
}
}
}
}
joinWithEn(items: string[], separator = ", "): string {
const len = items.length;
if (len === 0) return "";
if (len === 1) return items[0];
if (len === 2) return items.join(" en ");
return items.slice(0, -1).join(separator) + `${separator}and ${items[len - 1]}`;
}
} }
class ActiveMatch { class ActiveMatch {

View File

@@ -2,14 +2,13 @@ import {Component, inject, Input, OnInit} from '@angular/core';
import {CurrencyPipe} from "@angular/common"; import {CurrencyPipe} from "@angular/common";
import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle"; import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute} from "@angular/router";
import {Tournament} from "../../model/tournament"; import {Tournament} from "../../model/tournament";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatIconButton} from "@angular/material/button"; import {MatIconButton} from "@angular/material/button";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {CourtSelectionComponent} from "../court-selection/court-selection.component";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {SubstituteSelectionComponent} from "../substitute-selection/substitute-selection.component"; import {SubstituteSelectionComponent} from "../substitute-selection/substitute-selection.component";
import {TournamentPlayer} from "../../model/tournamentPlayer"; import {TournamentPlayer} from "../../model/tournamentPlayer";
@@ -34,15 +33,27 @@ import {MatTab, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
standalone: true, standalone: true,
styleUrl: './tournament-players.component.scss' styleUrl: './tournament-players.component.scss'
}) })
export class TournamentPlayersComponent { export class TournamentPlayersComponent implements OnInit {
@Input() tournament: Tournament; @Input() tournament: Tournament;
constructor( constructor(
private tournamentService: TournamentService, private tournamentService: TournamentService,
private route: ActivatedRoute,
private _snackBar: MatSnackBar, private _snackBar: MatSnackBar,
) {} ) {}
ngOnInit() {
if (!this.tournament) {
const id = Number(this.route.snapshot.paramMap.get('id'));
if (id) {
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
});
}
}
}
playerPaid($event: MatSlideToggleChange, playerId: number) { playerPaid($event: MatSlideToggleChange, playerId: number) {
this.tournamentService.playerPaid(this.tournament.id, playerId, $event.checked).subscribe(() => { this.tournamentService.playerPaid(this.tournament.id, playerId, $event.checked).subscribe(() => {
this._snackBar.open('Opgeslagen.'); this._snackBar.open('Opgeslagen.');

View File

@@ -13,4 +13,6 @@ export class Match {
games: Game[]; games: Game[];
court: number; court: number;
counter: Player; counter: Player;
canStart?: boolean;
cantStartReason?: string;
} }

View File

@@ -97,6 +97,10 @@ export class TournamentService {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/substitutions`, substitutions) return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/substitutions`, substitutions)
} }
public updateCounter(tournamentId: number, matchId: number, counter: number): Observable<Tournament> {
return this.http.patch<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/update?counter=${counter}`, null);
}
public addTestData(): Observable<void> { public addTestData(): Observable<void> {
return this.http.get<void>(`${environment.backendUrl}/testdata`); return this.http.get<void>(`${environment.backendUrl}/testdata`);
} }

View File

@@ -7,3 +7,6 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
z-index: 2000 !important; z-index: 2000 !important;
} }
*/ */
.mat-tooltip {
white-space: pre-line !important;
}