Compare commits

...

52 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
Michel ten Voorde
0e1e1932a1 Use localStorage
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-27 14:25:26 +01:00
fb36ee1a05 Always show substitutes if applicable
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-09 16:24:55 +02:00
9cb3568770 Update route config
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 22:36:02 +02:00
d387452044 Cleanup
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 22:18:04 +02:00
2cdccc4dd8 Cleanup
Some checks failed
Gitea/swiss-client/pipeline/head There was a failure building this commit
2025-10-08 22:15:41 +02:00
8083e7fc5f Updated auth config
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 21:36:13 +02:00
0fd693aa42 Layout fixes
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-04 22:40:51 +02:00
edaffc82e4 Updated angular-material
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-30 23:21:53 +02:00
1dbc2f1ca9 Updated angular-core 2025-09-30 23:01:46 +02:00
21fa706b45 Updated angular-cli 2025-09-30 23:00:57 +02:00
f2ee2b467d Fixed authguard 2025-09-30 22:59:13 +02:00
cdf27f1948 Moved logic to backend
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-25 23:14:32 +02:00
2c33c9a68d Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-23 22:45:44 +02:00
181aebf16a Invallers 2025-09-23 20:47:11 +02:00
6a9ea43e23 Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-18 23:28:02 +02:00
02b884bbcf Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-17 08:59:02 +02:00
5c846a6351 Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-11 23:43:28 +02:00
929fd6b581 WIP: substitutions
Some checks failed
Gitea/swiss-client/pipeline/head There was a failure building this commit
2025-09-10 10:35:15 +02:00
33bfde27d4 Spelerslijst op elke pagina
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-01 22:59:33 +02:00
564340fec4 Finals round
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-28 23:22:09 +02:00
Michel ten Voorde
d1338bf524 Disable positioning fix
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-26 16:06:45 +02:00
Michel ten Voorde
f858020330 Upgrade angular/material 2025-08-26 15:59:42 +02:00
Michel ten Voorde
cf86a449e2 Upgrade angular/material 2025-08-26 15:59:28 +02:00
Michel ten Voorde
3dd4746304 Upgrade angular/core 2025-08-26 15:58:27 +02:00
Michel ten Voorde
1e45a12392 Upgrade angular/cli 2025-08-26 15:56:50 +02:00
Michel ten Voorde
8082a72d4d Fix position for menu 2025-08-26 15:54:52 +02:00
Michel ten Voorde
27e90ceb17 Make components standalone 2025-08-26 15:54:40 +02:00
d6de1c43fb Various improvements 2025-08-25 23:38:30 +02:00
5a5c134002 Added match counter
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-18 23:28:15 +02:00
71e5b5c912 Meta
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-15 09:39:53 +02:00
76acf809bd Added tournament-players component 2025-08-15 09:39:40 +02:00
9bc0d5f362 Added TournamentRegistration.active 2025-08-15 09:39:15 +02:00
348a2ab2c5 Added default values for new tournament 2025-08-15 09:37:55 +02:00
1b2f123683 Cleanup 2025-08-15 09:36:30 +02:00
1da6246d9a Hide inactive tournaments by default 2025-08-15 09:35:18 +02:00
26c0c1a2da Refactored title service
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-13 23:40:36 +02:00
f88cd94316 Removed *ngFor
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-12 23:32:32 +02:00
79 changed files with 4869 additions and 2963 deletions

View File

@@ -32,11 +32,11 @@
Player-registrations: oude toernooien verbergen via knop Player-registrations: oude toernooien verbergen via knop
Onderdeel afsluiten Onderdeel afsluiten
Wedstrijd opgave Wedstrijd opgave
Blessures Invallers
Titels pagina's Titels pagina's
Authenticatie Authenticatie
Progress indicator tijdens communicatie backend Progress indicator tijdens communicatie backend
Bij tellers rekening houden met invallers
https://blog.shhdharmen.me/browser-storage-in-angular-ssr https://blog.shhdharmen.me/browser-storage-in-angular-ssr
Won't do / later: Won't do / later:

2634
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,23 +11,24 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^20.1.6", "@angular/animations": "^20.3.2",
"@angular/cdk": "^20.1.5", "@angular/cdk": "^20.2.5",
"@angular/common": "^20.1.6", "@angular/common": "^20.3.2",
"@angular/compiler": "^20.1.6", "@angular/compiler": "^20.3.2",
"@angular/core": "^20.1.6", "@angular/core": "^20.3.2",
"@angular/forms": "^20.1.6", "@angular/forms": "^20.3.2",
"@angular/material": "^20.1.5", "@angular/material": "^20.2.5",
"@angular/material-moment-adapter": "^20.1.5", "@angular/material-moment-adapter": "^20.2.5",
"@angular/platform-browser": "^20.1.6", "@angular/platform-browser": "^20.3.2",
"@angular/platform-browser-dynamic": "^20.1.6", "@angular/platform-browser-dynamic": "^20.3.2",
"@angular/platform-server": "^20.1.6", "@angular/platform-server": "^20.3.2",
"@angular/router": "^20.1.6", "@angular/router": "^20.3.2",
"@angular/ssr": "^20.1.5", "@angular/ssr": "^20.3.3",
"@ng-bootstrap/ng-bootstrap": "^19.0.1", "@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
"express": "^4.21.2", "express": "^4.21.2",
"jwt-decode": "^4.0.0",
"moment": "2.30.1", "moment": "2.30.1",
"ngx-cookie-service-ssr": "^20.1.0", "ngx-cookie-service-ssr": "^20.1.0",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
@@ -37,10 +38,10 @@
"zone.js": "~0.15.1" "zone.js": "~0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^20.1.5", "@angular/build": "^20.3.3",
"@angular/cli": "^20.1.5", "@angular/cli": "^20.3.3",
"@angular/compiler-cli": "^20.1.6", "@angular/compiler-cli": "^20.3.2",
"@angular/localize": "^20.1.6", "@angular/localize": "^20.3.2",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^24.2.1", "@types/node": "^24.2.1",

View File

@@ -7,7 +7,7 @@
<mat-icon class="menu-button-icon" style="color: white;">menu</mat-icon> <mat-icon class="menu-button-icon" style="color: white;">menu</mat-icon>
</button> </button>
--> -->
<h5 class="m-3">{{ title }}</h5> <h5 class="m-3">{{ header }}</h5>
<span class="spacer"></span> <span class="spacer"></span>
<a routerLink="/tournaments" mat-button> <a routerLink="/tournaments" mat-button>
<mat-icon>list</mat-icon> <mat-icon>list</mat-icon>
@@ -17,10 +17,10 @@
<mat-icon>group</mat-icon> <mat-icon>group</mat-icon>
Spelers Spelers
</a> </a>
@if (this.userService.isLoggedIn()) { @if (this.authService.isLoggedIn()) {
<button mat-flat-button [matMenuTriggerFor]="accountMenu"> <button mat-flat-button [matMenuTriggerFor]="accountMenu">
<mat-icon>person</mat-icon> <mat-icon>person</mat-icon>
{{ user }} {{ this.authService.getUsername() }}
</button> </button>
<mat-menu #accountMenu="matMenu"> <mat-menu #accountMenu="matMenu">
<button mat-menu-item (click)="logOut()"> <button mat-menu-item (click)="logOut()">

View File

@@ -1,50 +1,63 @@
import {Component, OnDestroy, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router'; import {ActivatedRoute, NavigationEnd, Router, RouterLink, RouterOutlet} from '@angular/router';
import {CommonModule, NgOptimizedImage} from "@angular/common"; import {CommonModule, NgOptimizedImage} from "@angular/common";
import {MatAnchor, MatButton} from "@angular/material/button"; import {MatAnchor, MatButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatToolbar} from "@angular/material/toolbar"; import {MatToolbar} from "@angular/material/toolbar";
import {TitleService} from "./service/title.service"; import {filter, map, Subscription} from "rxjs";
import {Subscription} from "rxjs";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {UserService} from "./authentication/user.service"; // import {UserService} from "./authentication/user.service";
import {TournamentService} from "./service/tournament.service"; import {TournamentService} from "./service/tournament.service";
import {HeaderService} from "./service/header.service";
import {AuthService} from "./auth/auth.service";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, CommonModule, RouterLink, MatAnchor, MatIcon, MatButton, MatToolbar, NgOptimizedImage, MatMenuTrigger, MatMenu, MatMenuItem], imports: [RouterOutlet, CommonModule, RouterLink, MatAnchor, MatIcon, MatButton, MatToolbar, NgOptimizedImage, MatMenuTrigger, MatMenu, MatMenuItem],
providers: [TitleService],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit {
title: string; header: string;
titleSubscription: Subscription; // user: string;
user: string; // userSubscription: Subscription;
userSubscription: Subscription;
constructor( constructor(
protected userService: UserService, protected authService: AuthService,
private router: Router, private router: Router,
protected activatedRoute: ActivatedRoute, protected activatedRoute: ActivatedRoute,
private titleService: TitleService, private tournamentService: TournamentService,
private tournamentService: TournamentService private headerService: HeaderService
) { ) {
} }
ngOnInit() { ngOnInit() {
this.titleSubscription = this.titleService.currentTitle.subscribe(newTitle => this.title = newTitle); // this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
}
ngOnDestroy(): void { this.router.events.pipe(
this.titleSubscription.unsubscribe(); filter(event => event instanceof NavigationEnd),
map(() => {
let currentRoute = this.activatedRoute;
while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild;
}
return currentRoute.snapshot.data['header'] || '';
})
).subscribe(header => {
this.header = header;
});
this.headerService.header$.subscribe(override => {
if (override) {
this.header = override;
}
});
} }
logOut() { logOut() {
this.userService.removeUser(); this.authService.logout();
this.router.navigate(['/auth/login']); this.router.navigate(['/login']);
} }
addTestData() { addTestData() {

View File

@@ -1,4 +1,4 @@
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core'; import {ApplicationConfig, inject, provideAppInitializer, provideZoneChangeDetection} from '@angular/core';
import {provideRouter} from '@angular/router'; import {provideRouter} from '@angular/router';
import {routes} from './app.routes'; import {routes} from './app.routes';
@@ -7,10 +7,9 @@ import {HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptorsFromDi}
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
import {provideMomentDateAdapter} from "@angular/material-moment-adapter"; import {provideMomentDateAdapter} from "@angular/material-moment-adapter";
import {MAT_SNACK_BAR_DEFAULT_OPTIONS} from "@angular/material/snack-bar"; import {MAT_SNACK_BAR_DEFAULT_OPTIONS} from "@angular/material/snack-bar";
import {AuthGuard} from "./authentication/authguard"; import {provideNgxMask} from "ngx-mask";
import {TokenInterceptor} from "./authentication/tokenInterceptor"; import {provideAnimations} from "@angular/platform-browser/animations";
import {ErrorInterceptor} from "./authentication/errorInterceptor"; import {AuthInterceptor} from "./auth/auth.interceptor";
import {provideEnvironmentNgxMask, provideNgxMask} from "ngx-mask";
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -18,12 +17,11 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes), provideRouter(routes),
provideClientHydration(), provideClientHydration(),
provideHttpClient(withFetch(), withInterceptorsFromDi()), provideHttpClient(withFetch(), withInterceptorsFromDi()),
provideAnimationsAsync(), provideAnimations(),
provideMomentDateAdapter(undefined, {useUtc: false}), provideMomentDateAdapter(undefined, {useUtc: false}),
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 2500}}, { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 2500}},
AuthGuard, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
provideNgxMask(), provideNgxMask(),
] ]
}; };

View File

@@ -4,32 +4,28 @@ import {PlayerEditComponent} from "./components/player-edit/player-edit.componen
import {TournamentListComponent} from "./components/tournament-list/tournament-list.component"; import {TournamentListComponent} from "./components/tournament-list/tournament-list.component";
import {TournamentEditComponent} from "./components/tournament-edit/tournament-edit.component"; import {TournamentEditComponent} from "./components/tournament-edit/tournament-edit.component";
import {PlayerRegistrationsComponent} from "./components/player-registrations/player-registrations.component"; import {PlayerRegistrationsComponent} from "./components/player-registrations/player-registrations.component";
import {TournamentRegistrationsComponent} from "./components/tournament-registrations/tournament-registrations.component";
import {TournamentValidateComponent} from "./components/tournament-validate/tournament-validate.component";
import {TournamentDivideComponent} from "./components/tournament-divide/tournament-divide.component";
import {TournamentDrawComponent} from "./components/tournament-draw/tournament-draw.component"; import {TournamentDrawComponent} from "./components/tournament-draw/tournament-draw.component";
import {TournamentManageComponent} from "./components/tournament-manage/tournament-manage.component"; import {TournamentManageComponent} from "./components/tournament-manage/tournament-manage.component";
import {MatchSheetsComponent} from "./components/match-sheets/match-sheets.component"; import {MatchSheetsComponent} from "./components/match-sheets/match-sheets.component";
import {RoundOverviewComponent} from "./components/round-overview/round-overview.component"; import {RoundOverviewComponent} from "./components/round-overview/round-overview.component";
import {AuthGuard} from "./authentication/authguard";
import {LoginComponent} from "./components/login/login.component"; import {LoginComponent} from "./components/login/login.component";
import {TournamentPlayersComponent} from "./components/tournament-players/tournament-players.component";
import {AuthGuard} from "./auth/auth-guard.service";
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: TournamentListComponent, canActivate: [AuthGuard], title: 'Toernooien' }, { path: '', component: TournamentListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'tournaments', component: TournamentListComponent, canActivate: [AuthGuard], title: 'Toernooien' }, { path: 'tournaments', component: TournamentListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'tournaments/add', component: TournamentEditComponent, canActivate: [AuthGuard], title: 'Nieuw Toernooi' }, { path: 'tournaments/add', component: TournamentEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Nieuw Toernooi' }},
{ path: 'tournaments/:id/edit', component: TournamentEditComponent, canActivate: [AuthGuard], title: 'Bewerk Toernooi' }, { path: 'tournaments/:id/edit', component: TournamentEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Bewerk Toernooi' }},
{ path: 'tournaments/:id/registrations', component: TournamentRegistrationsComponent, canActivate: [AuthGuard], title: 'Inschrijvingen' }, { path: 'tournaments/:id/registrations', component: TournamentPlayersComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Inschrijvingen' }},
{ path: 'tournaments/:id/validate', component: TournamentValidateComponent, canActivate: [AuthGuard], title: 'Toernooi' }, { path: 'tournaments/:id/draw', component: TournamentDrawComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooi loten' }},
{ path: 'tournaments/:id/divide', component: TournamentDivideComponent, canActivate: [AuthGuard], title: 'Toernooi valideren' }, { path: 'tournaments/:id/manage', component: TournamentManageComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'tournaments/:id/draw', component: TournamentDrawComponent, canActivate: [AuthGuard], title: 'Toernooi loten' }, { path: 'tournaments/:id/manage/:tab', component: TournamentManageComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'tournaments/:id/manage', component: TournamentManageComponent, canActivate: [AuthGuard] }, { path: 'players', component: PlayerListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Spelers' }},
{ path: 'tournaments/:id/manage/:tab', component: TournamentManageComponent, canActivate: [AuthGuard], title: 'Toernooien' }, { path: 'players/add', component: PlayerEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Nieuwe Speler' }},
{ path: 'players', component: PlayerListComponent, canActivate: [AuthGuard], title: 'Spelers' }, { path: 'players/:id/edit', component: PlayerEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Bewerk Speler' }},
{ path: 'players/add', component: PlayerEditComponent, canActivate: [AuthGuard], title: 'Nieuwe Speler' }, { path: 'players/:id/registrations', component: PlayerRegistrationsComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'players/edit/:id', component: PlayerEditComponent, canActivate: [AuthGuard], title: 'Bewerk Speler' }, { path: 'tournaments/:id/rounds/:roundId/matchsheets', component: MatchSheetsComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'players/:id/registrations', component: PlayerRegistrationsComponent, canActivate: [AuthGuard], title: 'Inschrijvingen' }, { path: 'tournaments/:id/rounds/:roundId/overview', component: RoundOverviewComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Rondeoverzicht' }},
{ path: 'tournaments/:id/rounds/:roundId/matchsheets', component: MatchSheetsComponent, canActivate: [AuthGuard], title: 'Wedstrijdbriefjes' }, { path: 'login', component: LoginComponent, data: { header: 'Inloggen'}}
{ path: 'tournaments/:id/rounds/:roundId/overview', component: RoundOverviewComponent, canActivate: [AuthGuard], title: 'Rondeoverzicht' },
{ path: 'auth/login', component: LoginComponent }
]; ];

View File

@@ -0,0 +1,22 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {AuthService} from './auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {
}
public canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.authService.isLoggedIn() && this.authService.isUserInRole(next.routeConfig?.data?.['role'])) {
return true;
} else {
// this.router.navigateByUrl("/login");
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}
}

View File

@@ -0,0 +1,35 @@
import {Injectable} from '@angular/core';
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {catchError, Observable, throwError} from 'rxjs';
import {Router} from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {
constructor(private router: Router) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let token = localStorage.getItem("app.token");
if (token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
},
});
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => this.handleErrorRes(error))
);
}
private handleErrorRes(error: HttpErrorResponse): Observable<never> {
if (error.status === 401) {
this.router.navigateByUrl("/login", {replaceUrl: true});
}
return throwError(() => error);
}
}

View File

@@ -0,0 +1,85 @@
import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {environment} from "../../environments/environment";
import {isPlatformBrowser} from "@angular/common";
import {jwtDecode, JwtPayload} from "jwt-decode";
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly authUrl: string
constructor(private http: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object) {
this.authUrl = `${environment.backendUrl}/api/auth`;
}
private get isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
isLoggedIn(): boolean {
if (!this.isBrowser) return false;
return localStorage.getItem("app.token") != null;
}
login(username: string, password: string): Observable<string> {
if (!this.isBrowser) {
throw new Error('Login can only be performed in browser');
}
const credentials = btoa(`${username}:${password}`);
const httpOptions = {
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
responseType: 'text' as 'text',
};
return this.http.post(this.authUrl, null, httpOptions);
}
logout() {
if (!this.isBrowser) return;
localStorage.removeItem("app.token");
localStorage.removeItem("app.roles");
}
isUserInRole(roleFromRoute: string) {
if (!this.isBrowser) return false;
const roles = localStorage.getItem("app.roles");
if (roles!.includes(",")) {
if (roles === roleFromRoute) {
return true;
}
} else {
const roleArray = roles!.split(",");
for (let role of roleArray) {
if (role === roleFromRoute) {
return true;
}
}
}
return false;
}
getUsername(): string | null {
if (!this.isBrowser) return null;
const token = localStorage.getItem("app.token");
if (!token) return null;
try {
const decodedToken = jwtDecode<JwtPayload>(token);
return decodedToken.sub || null; // 'sub' is the standard JWT claim for subject/username
} catch (error) {
return null;
}
}
}

View File

@@ -1,35 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {environment} from "../../environments/environment";
import {LoginCredentials} from "./loginCredentials";
import {TokenModel} from "./tokenModel";
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
private readonly authUrl: string
constructor(private http: HttpClient) {
this.authUrl = `${environment.backendUrl}`
}
public login(loginCredentials: LoginCredentials) {
return this.http.post<any>(`${this.authUrl}/authenticate`, loginCredentials);
}
public logout(tokenModel: TokenModel) {
return this.http.post<string>(
`${this.authUrl}/logout`,
tokenModel
);
}
public logoutEverywhere() {
return this.http.post<string>(
`${this.authUrl}/logout-everywhere`,
undefined
);
}
}

View File

@@ -1,23 +0,0 @@
import {Injectable} from "@angular/core";
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from "@angular/router";
import {UserService} from "./user.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private userService: UserService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.userService.getUser()) {
return true;
}
this.router.navigate(['/auth/login'], {
queryParams: { returnUrl: state.url },
});
return false;
}
}

View File

@@ -1,31 +0,0 @@
import {Injectable} from "@angular/core";
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {catchError, Observable, throwError} from "rxjs";
import {UserService} from "./user.service";
@Injectable({
providedIn: 'root'
})
export class ErrorInterceptor implements HttpInterceptor {
constructor(private userService: UserService) { }
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(error=>{
if((error.status == 401 || error.status == 403) && !this.isLoginPage(request)){
this.userService.removeUser();
}
// const errMsg = error.error.message || error.statusText;
return throwError(() => error);
// return throwError(()=> errMsg);
}));
}
private isLoginPage(request: HttpRequest<any>){
return request.url.includes("/authenticate")
}
}

View File

@@ -1,14 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class IpService {
constructor(private http: HttpClient) { }
public getIPAddress() {
return this.http.get("http://api.ipify.org/?format=json");
}
}

View File

@@ -1,6 +0,0 @@
export class LoginCredentials {
username: string;
password: string;
// ipAddress: string;
// recaptcha: string;
}

View File

@@ -1,27 +0,0 @@
import {Injectable} from "@angular/core";
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {UserService} from "./user.service";
@Injectable({
providedIn: 'root'
})
export class TokenInterceptor implements HttpInterceptor {
constructor(private userService: UserService) { }
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (this.userService.isLoggedIn()) {
let newRequest = request.clone({
setHeaders: {
Authorization: `Bearer ${this.userService.getUser()?.accessToken}`,
},
});
return next.handle(newRequest);
}
return next.handle(request);
}
}

View File

@@ -1,11 +0,0 @@
export class TokenModel {
constructor(token: string, refreshToken: string, ipAddress: string) {
this.token = token;
this.refreshToken = refreshToken;
this.ipAddress = ipAddress;
}
token: string;
refreshToken: string;
ipAddress: string;
}

View File

@@ -1,54 +0,0 @@
import {EventEmitter, Injectable, Output} from "@angular/core";
import {SsrCookieService} from "ngx-cookie-service-ssr";
import {Router} from "@angular/router";
import {User} from "./user";
import {BehaviorSubject} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class UserService {
user?: User;
private userEmitter = new BehaviorSubject('');
currentUser = this.userEmitter.asObservable();
constructor(
private cookieService: SsrCookieService,
private router: Router,
) {}
public getUser(): User | undefined {
const user = this.cookieService.get('swissuser');
if (user) {
this.user = JSON.parse(user);
this.userEmitter.next(JSON.parse(user).username);
} else {
this.user = undefined;
}
return this.user;
}
public isLoggedIn(): boolean {
const user = this.cookieService.get('swissuser');
return !(user == undefined || false || user == "");
}
public setUser(user: User) {
this.cookieService.set('swissuser', JSON.stringify(user));
this.user = user;
this.userEmitter.next(this.user.username);
}
public removeUser() {
this.cookieService.delete('swissuser');
this.user = undefined;
this.router.navigate(['/tournaments']);
this.userEmitter.next('');
}
}

View File

@@ -1,8 +0,0 @@
export class User {
accessToken: string;
username: string;
// passwordHash: string;
// email: string;
// token: string;
// refreshToken: string;
}

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

@@ -1,12 +1,32 @@
<h2 mat-dialog-title>Kies een baan:</h2>
<mat-dialog-content> <mat-dialog-content>
<button type="button" class="btn {{ data.availableCourts.indexOf(i + 1) < 0 ? 'btn-secondary' : 'btn-primary' }} btn-lg m-3" <h3>Kies een baan:</h3>
*ngFor="let item of [].constructor(data.totalCourts); let i = index" @for (item of [].constructor(data.totalCourts); track $index) {
[disabled]="data.availableCourts.indexOf(i + 1) < 0" [mat-dialog-close]="i + 1"> <button type="button"
{{ i + 1 }} class="btn {{ this.court == ($index + 1) ? 'btn-primary' : (data.availableCourts.indexOf($index + 1) < 0 ? 'btn-outline-secondary' : 'btn-outline-primary') }} btn-lg m-3"
[disabled]="data.availableCourts.indexOf($index + 1) < 0"
(click)="selectCourt($index + 1)">
{{ $index + 1 }}
</button> </button>
}
<br> <br>
<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-content>
<mat-dialog-actions> <mat-dialog-actions>
@if (court && counter) {
<button mat-button [mat-dialog-close]="{court: court, counter: counter}">Wedstrijd starten</button>
}
<button mat-button (click)="onAnnulerenClick()">Annuleren</button> <button mat-button (click)="onAnnulerenClick()">Annuleren</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

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

View File

@@ -3,39 +3,53 @@ import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogRef, MatDialogContent,
MatDialogTitle MatDialogRef
} from "@angular/material/dialog"; } from "@angular/material/dialog";
import {Match} from "../../model/match"; import {Match} from "../../model/match";
import {NgForOf} from "@angular/common";
import {MatButton} from "@angular/material/button"; 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({ @Component({
selector: 'app-court-selection', selector: 'app-court-selection',
imports: [ imports: [
MatDialogTitle,
MatDialogContent, MatDialogContent,
NgForOf,
MatButton, MatButton,
MatDialogClose, MatDialogClose,
MatDialogActions MatDialogActions,
MatFormField,
MatLabel,
MatOption,
MatSelect,
FormsModule,
], ],
templateUrl: './court-selection.component.html', templateUrl: './court-selection.component.html',
standalone: true,
styleUrl: './court-selection.component.scss' styleUrl: './court-selection.component.scss'
}) })
export class CourtSelectionComponent { export class CourtSelectionComponent {
court: number; court: number;
counter: TournamentPlayer;
readonly dialogRef = inject(MatDialogRef<CourtSelectionComponent>); readonly dialogRef = inject(MatDialogRef<CourtSelectionComponent>);
constructor(@Inject(MAT_DIALOG_DATA) public data: { constructor(@Inject(MAT_DIALOG_DATA) public data: {
match: Match, match: Match,
availableCourts: number[], availableCourts: number[],
totalCourts: number totalCourts: number,
availableCounters: TournamentPlayer[]
}) {} }) {}
onAnnulerenClick() { onAnnulerenClick() {
this.dialogRef.close(); this.dialogRef.close();
} }
selectCourt(selectedCourt: number) {
this.court = selectedCourt;
}
} }

View File

@@ -1,22 +1,23 @@
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<form class="form-horizontal" [formGroup]="form"> <form class="form-horizontal" (ngSubmit)="login()">
<div class="row"> <div class="row">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Gebruikersnaam</mat-label> <mat-label>Gebruikersnaam</mat-label>
<input matInput placeholder="Gebruikersnaam" formControlName="username" required> <input matInput name="username" placeholder="Gebruikersnaam" required [(ngModel)]="username">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Wachtwoord</mat-label> <mat-label>Wachtwoord</mat-label>
<input matInput placeholder="Wachtwoord" type="password" formControlName="password" required> <input matInput name="password" placeholder="Wachtwoord" type="password" required [(ngModel)]="password">
</mat-form-field> </mat-form-field>
</div> </div>
<button class="w-100 mt-2" mat-button mat-flat-button color="primary" <button class="w-100 mt-2" mat-button mat-flat-button color="primary"
(click)="login()" [disabled]="form.invalid"> (click)="login()">
Inloggen Inloggen
</button> </button>
</form> </form>
<p>{{message}}</p>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@@ -1,16 +1,13 @@
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {MatCard, MatCardContent} from "@angular/material/card";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field"; import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatButton} from "@angular/material/button"; import {MatButton} from "@angular/material/button";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {NgIf} from "@angular/common"; import {jwtDecode, JwtPayload} from "jwt-decode";
import {AuthenticationService} from "../../authentication/authentication.service"; import {AuthService} from "../../auth/auth.service";
import {UserService} from "../../authentication/user.service"; import {MatSnackBar} from "@angular/material/snack-bar";
import {LoginCredentials} from "../../authentication/loginCredentials";
import {User} from "../../authentication/user";
import {TitleService} from "../../service/title.service";
@Component({ @Component({
@@ -24,7 +21,9 @@ import {TitleService} from "../../service/title.service";
MatInput, MatInput,
MatLabel, MatLabel,
MatCard, MatCard,
FormsModule,
], ],
standalone: true,
styleUrls: ['./login.component.scss'] styleUrls: ['./login.component.scss']
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
@@ -32,50 +31,42 @@ export class LoginComponent implements OnInit {
private returnUrl: string; private returnUrl: string;
private ipAddress: string; private ipAddress: string;
constructor( username: string = "";
password: string = "";
message: string = "";
constructor(private authService: AuthService,
private route: ActivatedRoute, private route: ActivatedRoute,
private fb: FormBuilder,
private authenticationService: AuthenticationService,
private router: Router, private router: Router,
private userPersistenceService: UserService, private snackBar: MatSnackBar) {
private titleService: TitleService,
) {
this.initializeForm();
} }
private initializeForm() {
this.form = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
ngOnInit() { ngOnInit() {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.titleService.setTitle("Inloggen");
} }
login() { public login(): void {
if (this.form.invalid) { localStorage.removeItem("app.token");
return;
}
let loginCredentials = new LoginCredentials(); this.authService.login(this.username, this.password)
loginCredentials = { .subscribe({
...loginCredentials, next: (token) => {
...this.form.value, localStorage.setItem("app.token", token);
};
this.authenticationService const decodedToken = jwtDecode<JwtPayload>(token);
.login(loginCredentials) // @ts-ignore
.subscribe( localStorage.setItem("app.roles", decodedToken.scope);
{
next: (user: User) => { // this.router.navigateByUrl("/persons");
this.userPersistenceService.setUser(user);
this.router.navigate([this.returnUrl]); // this.router.navigate([this.returnUrl]);
this.router.navigate([this.route.snapshot.queryParams['returnUrl'] || '/']);
},
error: (error) => {
this.snackBar.open(`Login failed: ${error.status}`, "OK")
} }
} });
);
} }
} }

View File

@@ -7,76 +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}">
{{ data.match.team1 | teamText }} <app-team-display
[team]="data.match.team1"
[event]="data.event"
[tournament]="data.tournament"
[inline]="false">
</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}">
{{ data.match.team2 | teamText }} <app-team-display
[team]="data.match.team2"
[event]="data.event"
[tournament]="data.tournament"
[inline]="false">
</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

@@ -3,22 +3,25 @@ import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogRef, MatDialogContent,
MatDialogRef,
MatDialogTitle MatDialogTitle
} from "@angular/material/dialog"; } from "@angular/material/dialog";
import {MatButton, MatIconButton} from "@angular/material/button"; import {MatButton} from "@angular/material/button";
import {DatePipe, NgClass, NgForOf} from "@angular/common"; import {NgClass} from "@angular/common";
import {MatIcon} from "@angular/material/icon";
import {TeamPipe} from "../../pipes/team-pipe"; import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {Match} from "../../model/match"; import {Match} from "../../model/match";
import {MatFormField, MatInput} from "@angular/material/input"; import {MatFormField, MatInput} from "@angular/material/input";
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {Result} from "../../model/result"; import {Result} from "../../model/result";
import {MatGridList, MatGridTile, MatGridTileText} from "@angular/material/grid-list"; import {MatGridList, MatGridTile} from "@angular/material/grid-list";
import {Round} from "../../model/round"; import {Round} from "../../model/round";
import {Group} from "../../model/group"; import {Group} from "../../model/group";
import {Game} from "../../model/game"; import {Game} from "../../model/game";
import {TeamDisplayComponent} from "../team-display/team-display.component";
import {Event} from "../../model/event";
import {Tournament} from "../../model/tournament";
@Component({ @Component({
selector: 'app-match-result', selector: 'app-match-result',
@@ -28,27 +31,29 @@ import {Game} from "../../model/game";
MatButton, MatButton,
MatDialogClose, MatDialogClose,
MatDialogTitle, MatDialogTitle,
TeamPipe,
MatInput, MatInput,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
MatFormField, MatFormField,
MatGridList, MatGridList,
MatGridTile, MatGridTile,
NgClass NgClass,
TeamDisplayComponent
], ],
providers: [ providers: [
FullNamePipe, FullNamePipe,
TeamPipe TeamPipe
], ],
templateUrl: './match-result.component.html', templateUrl: './match-result.component.html',
standalone: true,
styleUrl: './match-result.component.scss' styleUrl: './match-result.component.scss'
}) })
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, 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;
if (data.match.games.length == 0) { if (data.match.games.length == 0) {
@@ -63,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>);
@@ -75,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);
@@ -90,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 {
@@ -113,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

@@ -1,14 +1,28 @@
@if (round) { @if (round) {
<ng-container *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<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"> <div class="col-6">
<b>{{ match.team1 | teamText }}</b> <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">
<b>
<app-team-display
[team]="match.team1"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</b>
</div> </div>
<div class="col-2"> <div class="col-2">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -28,7 +42,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<b>{{ match.team2 | teamText }}</b> <b>
<app-team-display
[team]="match.team2"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</b>
</div> </div>
<div class="col-2"> <div class="col-2">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -50,14 +71,10 @@
<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>
</div> </div>
<br> <br>
</ng-container> }
} }

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {MatCard, MatCardContent} from "@angular/material/card"; import {MatCard, MatCardContent} from "@angular/material/card";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
@@ -7,33 +7,35 @@ import {Round} from "../../model/round";
import {TeamPipe} from "../../pipes/team-pipe"; import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {Group} from "../../model/group"; import {Group} from "../../model/group";
import {NgForOf} from "@angular/common";
import {MatFormField} from "@angular/material/form-field"; import {MatFormField} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import {TitleService} from "../../service/title.service"; import {HeaderService} from "../../service/header.service";
import {TeamDisplayComponent} from "../team-display/team-display.component";
import {Event} from "../../model/event";
@Component({ @Component({
selector: 'app-match-sheets', selector: 'app-match-sheets',
imports: [ imports: [
MatCard, MatCard,
MatCardContent, MatCardContent,
TeamPipe,
NgForOf,
MatFormField, MatFormField,
MatInput, MatInput,
ReactiveFormsModule ReactiveFormsModule,
TeamDisplayComponent
], ],
providers: [ providers: [
TeamPipe, TeamPipe,
FullNamePipe FullNamePipe
], ],
templateUrl: './match-sheets.component.html', templateUrl: './match-sheets.component.html',
standalone: true,
styleUrl: './match-sheets.component.scss' styleUrl: './match-sheets.component.scss'
}) })
export class MatchSheetsComponent implements OnInit { export class MatchSheetsComponent implements OnInit, OnDestroy {
tournament: Tournament; tournament: Tournament;
event: Event;
group: Group; group: Group;
round: Round; round: Round;
@@ -41,7 +43,7 @@ export class MatchSheetsComponent implements OnInit {
private tournamentService: TournamentService, private tournamentService: TournamentService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService private headerService: HeaderService
) { ) {
} }
@@ -55,9 +57,10 @@ export class MatchSheetsComponent implements OnInit {
for (let group of event.groups) { for (let group of event.groups) {
for (let round of group.rounds) { for (let round of group.rounds) {
if (round.id == roundId) { if (round.id == roundId) {
this.event = event;
this.group = group; this.group = group;
this.round = round; this.round = round;
this.titleService.setTitle(`Wedstrijdbriefjes ${this.group.name} ${this.round.name}`); this.headerService.setTitle(`Wedstrijdbriefjes ${this.group.name} ${this.round.name}`);
return; return;
} }
} }
@@ -66,4 +69,8 @@ export class MatchSheetsComponent implements OnInit {
}); });
} }
ngOnDestroy() {
this.headerService.clearTitle()
}
} }

View File

@@ -0,0 +1,4 @@
.has-substitute {
text-decoration-line: underline;
text-decoration-style: dotted;
}

View File

@@ -0,0 +1,49 @@
import {Component, Input} from '@angular/core';
import {Player} from '../../model/player';
import {Event} from '../../model/event';
import {Tournament} from '../../model/tournament';
import {FullNamePipe} from '../../pipes/fullname-pipe';
import {MatTooltip} from '@angular/material/tooltip';
@Component({
selector: 'app-player-display',
standalone: true,
imports: [FullNamePipe, MatTooltip],
styleUrls: ['./player-display.component.scss'],
template: `
@let substitute = getSubstituteForEvent(player, event);
@if (exlicitSubstitute) {
@if (substitute) {
{{ substitute }} (valt in voor {{ player | fullName }})
} @else {
{{ player | fullName }}
}
} @else {
<span [class.has-substitute]="substitute"
[matTooltip]="substitute ? 'Valt in voor ' + (player | fullName) : ''"
matTooltipPosition="below">{{ substitute || (player | fullName) }}</span>
}
`
})
export class PlayerDisplayComponent {
@Input({ required: true }) player!: Player;
@Input({ required: true }) event!: Event;
@Input({ required: true }) tournament!: Tournament;
@Input({ required: false }) exlicitSubstitute: boolean = false;
getSubstituteForEvent(player: Player, event: Event): string | undefined {
const tournamentPlayer = this.tournament.tournamentPlayers.find(
tp => tp.playerId === player.id
);
if (!tournamentPlayer) return undefined;
const substitution = tournamentPlayer.substitutions.find(
s => s.event === event.type
);
if (!substitution) return undefined;
return this.tournament.tournamentPlayers.find(
p => p.id === substitution.substitute
)?.name;
}
}

View File

@@ -60,9 +60,11 @@
<mat-form-field appearance="fill"> <mat-form-field appearance="fill">
<mat-label>Speelsterkte</mat-label> <mat-label>Speelsterkte</mat-label>
<mat-select [(ngModel)]="player.strength" name="strength" required> <mat-select [(ngModel)]="player.strength" name="strength" required>
<mat-option *ngFor="let strengthOption of Strength | keyvalue" [value]="strengthOption.key"> @for (strengthOption of Strength | keyvalue; track strengthOption) {
<mat-option [value]="strengthOption.key">
{{ strengthOption.value }} {{ strengthOption.value }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

View File

@@ -9,9 +9,8 @@ import {MatIcon} from "@angular/material/icon";
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio"; import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
import {MatCard, MatCardActions, MatCardContent} from "@angular/material/card"; import {MatCard, MatCardActions, MatCardContent} from "@angular/material/card";
import {MatOption, MatSelect} from "@angular/material/select"; import {MatOption, MatSelect} from "@angular/material/select";
import {KeyValuePipe, NgForOf} from "@angular/common"; import {KeyValuePipe} from "@angular/common";
import {MatAnchor, MatButton} from "@angular/material/button"; import {MatAnchor, MatButton} from "@angular/material/button";
import {TitleService} from "../../service/title.service";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {NgxMaskDirective} from "ngx-mask"; import {NgxMaskDirective} from "ngx-mask";
@@ -33,25 +32,23 @@ import {NgxMaskDirective} from "ngx-mask";
MatSelect, MatSelect,
MatOption, MatOption,
KeyValuePipe, KeyValuePipe,
NgForOf,
MatButton, MatButton,
MatAnchor, MatAnchor,
ReactiveFormsModule, ReactiveFormsModule,
NgxMaskDirective NgxMaskDirective
], ],
templateUrl: './player-edit.component.html', templateUrl: './player-edit.component.html',
standalone: true,
styleUrl: './player-edit.component.scss' styleUrl: './player-edit.component.scss'
}) })
export class PlayerEditComponent implements OnInit { export class PlayerEditComponent implements OnInit {
player: Player; player: Player;
isEditMode: boolean = false; isEditMode: boolean = false;
constructor( constructor(
private playerService: PlayerService, private playerService: PlayerService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService,
private _snackBar: MatSnackBar, private _snackBar: MatSnackBar,
) { ) {
this.player = new Player(); this.player = new Player();
@@ -60,13 +57,11 @@ export class PlayerEditComponent implements OnInit {
ngOnInit() { ngOnInit() {
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
if (id) { if (id) {
this.titleService.setTitle("Bewerk Speler");
this.isEditMode = true; this.isEditMode = true;
this.playerService.getById(Number(id)).subscribe(data => { this.playerService.getById(Number(id)).subscribe(data => {
this.player = data; this.player = data;
}); });
} else { } else {
this.titleService.setTitle("Nieuwe Speler");
} }
} }

View File

@@ -1,14 +1,14 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {MatAnchor} from "@angular/material/button"; // import {MatAnchor} from "@angular/material/button";
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
@Component({ @Component({
selector: 'player-link', selector: 'player-link',
imports: [ imports: [
MatAnchor,
RouterLink RouterLink
], ],
templateUrl: './player-link.component.html', templateUrl: './player-link.component.html',
standalone: true,
styleUrl: './player-link.component.scss' styleUrl: './player-link.component.scss'
}) })
export class PlayerLinkComponent implements OnInit { export class PlayerLinkComponent implements OnInit {

View File

@@ -25,7 +25,7 @@
<ng-container matColumnDef="action"> <ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let player"> <td mat-cell *matCellDef="let player">
<a mat-button [routerLink]="['/players/edit', player.id]"> <a mat-button [routerLink]="['/players', player.id, 'edit']">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
Bewerk Bewerk
</a> </a>
@@ -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

@@ -6,7 +6,6 @@ import {MatAnchor} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatCard, MatCardContent} from "@angular/material/card"; import {MatCard, MatCardContent} from "@angular/material/card";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TitleService} from "../../service/title.service";
import { import {
MatCell, MatCell,
MatCellDef, MatCellDef,
@@ -30,6 +29,7 @@ import {MatSort, MatSortHeader} from "@angular/material/sort";
imports: [RouterLink, MatAnchor, MatIcon, MatCard, MatCardContent, FullNamePipe, MatTable, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatFormField, MatInput, MatFormFieldModule, MatPaginator, MatSortHeader, MatSort], imports: [RouterLink, MatAnchor, MatIcon, MatCard, MatCardContent, FullNamePipe, MatTable, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatFormField, MatInput, MatFormFieldModule, MatPaginator, MatSortHeader, MatSort],
providers: [FullNamePipe], providers: [FullNamePipe],
templateUrl: './player-list.component.html', templateUrl: './player-list.component.html',
standalone: true,
styleUrl: './player-list.component.scss' styleUrl: './player-list.component.scss'
}) })
export class PlayerListComponent implements AfterViewInit { export class PlayerListComponent implements AfterViewInit {
@@ -42,13 +42,11 @@ export class PlayerListComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor( constructor(
private titleService: TitleService,
private playerService: PlayerService, private playerService: PlayerService,
private fullNamePipe: FullNamePipe) { private fullNamePipe: FullNamePipe) {
} }
ngAfterViewInit() { ngAfterViewInit() {
this.titleService.setTitle("Spelers");
this.playerService.getAll().subscribe(data => { this.playerService.getAll().subscribe(data => {
this.players = data; this.players = data;
this.dataSource = new MatTableDataSource(this.players); this.dataSource = new MatTableDataSource(this.players);

View File

@@ -1,12 +1,13 @@
@if (player && tournamentRegistrations && allPlayers) { @if (player && tournamentRegistrations && allPlayers) {
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content>
<mat-card *ngFor="let tournamentRegistration of getTournamentRegistrations()" appearance="outlined" class="mb-3"> @for (tournamentRegistration of getTournamentRegistrations(); track tournamentRegistration.id) {
<mat-card appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<h6>{{ tournamentRegistration.name }}</h6> <h6>{{ tournamentRegistration.name }}</h6>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ng-container *ngFor="let eventRegistration of tournamentRegistration.events"> @for (eventRegistration of tournamentRegistration.events; track eventRegistration.id) {
<div class="row event-row"> <div class="row event-row">
<div class="col-md-2"> <div class="col-md-2">
<mat-checkbox [disabled]="!tournamentRegistration.editable" [(ngModel)]="eventRegistration.registered" (change)="updateModelWhenEventChecked(eventRegistration, $event)" name="registered"> <mat-checkbox [disabled]="!tournamentRegistration.editable" [(ngModel)]="eventRegistration.registered" (change)="updateModelWhenEventChecked(eventRegistration, $event)" name="registered">
@@ -15,22 +16,31 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@if (eventRegistration.doublesEvent) { @if (eventRegistration.doublesEvent) {
<ng-container>
<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
<mat-option *ngFor="let player of getRelevantPlayers(eventRegistration.type)" [value]="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">
{{ player | fullName }} {{ player | fullName }}
</mat-option> </mat-option>
</mat-select> }
</mat-autocomplete>
</mat-form-field> </mat-form-field>
</ng-container>
} }
</div> </div>
<div class="col-6"></div> <div class="col-6"></div>
</div> </div>
</ng-container> }
</mat-card-content> </mat-card-content>
@if (tournamentRegistration.editable) { @if (tournamentRegistration.editable) {
<mat-card-actions> <mat-card-actions>
@@ -45,10 +55,11 @@
</mat-card-actions> </mat-card-actions>
} }
</mat-card> </mat-card>
}
@if (!this.showAll) { @if (!this.showAll) {
<button mat-button (click)="this.showAll = true"> <button mat-button (click)="this.showAll = true">
<mat-icon>search</mat-icon> <mat-icon>expand</mat-icon>
Toon oude toernooien Toon inactieve toernooien
</button> </button>
} }
</mat-card-content> </mat-card-content>

View File

@@ -6,16 +6,16 @@ import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/m
import {MatFormField, MatLabel} from "@angular/material/form-field"; import {MatFormField, MatLabel} from "@angular/material/form-field";
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {RegistrationService} from "../../service/registration.service"; import {RegistrationService} from "../../service/registration.service";
import {NgFor, NgIf} from "@angular/common";
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 {TitleService} from "../../service/title.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',
@@ -25,24 +25,26 @@ import {TitleService} from "../../service/title.service";
MatCardHeader, MatCardHeader,
MatFormField, MatFormField,
MatLabel, MatLabel,
NgFor,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
MatCheckbox, MatCheckbox,
NgIf,
MatCardActions, MatCardActions,
RouterLink, RouterLink,
MatOption, MatOption,
MatSelect, // MatSelect,
MatIcon, MatIcon,
MatButton, MatButton,
MatAnchor, MatAnchor,
FullNamePipe FullNamePipe,
MatAutocomplete,
MatAutocompleteTrigger,
MatInput,
], ],
providers: [ providers: [
FullNamePipe FullNamePipe
], ],
templateUrl: './player-registrations.component.html', templateUrl: './player-registrations.component.html',
standalone: true,
styleUrl: './player-registrations.component.scss' styleUrl: './player-registrations.component.scss'
}) })
export class PlayerRegistrationsComponent implements OnInit { export class PlayerRegistrationsComponent implements OnInit {
@@ -54,21 +56,23 @@ 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,
private registrationService: RegistrationService, private registrationService: RegistrationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService, private fullNamePipe: FullNamePipe,
private fullNamePipe: FullNamePipe private headerService: HeaderService,
) {} ) {}
ngOnInit() { ngOnInit() {
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
this.playerService.getById(Number(id)).subscribe(data => { this.playerService.getById(Number(id)).subscribe(data => {
this.player = data; this.player = data;
this.titleService.setTitle(`Inschrijvingen van ${this.fullNamePipe.transform(this.player)}`); this.headerService.setTitle(`Inschrijvingen van ${this.fullNamePipe.transform(this.player)}`);
}); });
this.registrationService.getTournamentRegistrationsByPlayerId(Number(id)).subscribe(data => { this.registrationService.getTournamentRegistrationsByPlayerId(Number(id)).subscribe(data => {
this.tournamentRegistrations = data; this.tournamentRegistrations = data;
@@ -78,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 => {
@@ -115,7 +153,7 @@ export class PlayerRegistrationsComponent implements OnInit {
if (this.showAll) { if (this.showAll) {
return this.tournamentRegistrations; return this.tournamentRegistrations;
} else { } else {
return this.tournamentRegistrations.filter(t => (t.status == 'UPCOMING' || t.status == 'ONGOING')); return this.tournamentRegistrations.filter(t => t.active);
} }
} }

View File

@@ -3,12 +3,28 @@
@if (round.status != 'FINISHED') { @if (round.status != 'FINISHED') {
<table class="table table-sm m-4 wide w-100"> <table class="table table-sm m-4 wide w-100">
<tbody> <tbody>
<tr *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<td class="align-middle" style="width: 45%;">{{ match.team1 | teamText }}</td> <tr>
<td class="align-middle" style="width: 45%;">
<app-team-display
[team]="match.team1"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle" style="width: 45%;">{{ match.team2 | teamText }}</td> <td class="align-middle" style="width: 45%;">
<app-team-display
[team]="match.team2"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</td>
<td class="align-middle w-sep"></td> <td class="align-middle w-sep"></td>
</tr> </tr>
}
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
@@ -19,30 +35,36 @@
} @else { } @else {
<table class="table table-sm m-4 wide w-100"> <table class="table table-sm m-4 wide w-100">
<tbody> <tbody>
<tr *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<tr>
<td class="align-middle" style="width: 30%;"> <td class="align-middle" style="width: 30%;">
@if (event.doublesEvent) { <app-team-display
{{ match.team1.player1 | fullName }} /<br>{{ match.team1.player2 | fullName }} [team]="match.team1"
} @else { [event]="this.event"
{{ match.team1.player1 | fullName }} [tournament]="this.tournament"
} [inline]="true">
</app-team-display>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle" style="width: 30%;"> <td class="align-middle" style="width: 30%;">
@if (event.doublesEvent) { <app-team-display
{{ match.team2.player1 | fullName }} /<br>{{ match.team2.player2 | fullName }} [team]="match.team2"
} @else { [event]="this.event"
{{ match.team2.player1 | fullName }} [tournament]="this.tournament"
} [inline]="true">
</app-team-display>
</td> </td>
<td class="align-middle" style="width: 35%;"> <td class="align-middle" style="width: 35%;">
<div class="row result align-items-center"> <div class="row result align-items-center">
<span *ngFor="let game of match.games" class="col-3">{{ game.score1 }}-{{ game.score2 }}</span> @for (game of match.games; track game.id) {
<span class="col-3">{{ game.score1 }}-{{ game.score2 }}</span>
}
@if (match.games.length == 2) { @if (match.games.length == 2) {
<span class="col-3"></span> <span class="col-3"></span>
} }
</div> </div>
</td> </td>
</tr> </tr>
}
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
@@ -78,14 +100,24 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr *ngFor="let entry of round.standings.entries"> @for (entry of round.standings.entries; track entry.position) {
<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]="this.event"
[tournament]="this.tournament"
[inline]="true"
[explicitSubstitute]="true">
</app-team-display>
</td>
<td class="align-middle">{{ entry.played }}</td> <td class="align-middle">{{ entry.played }}</td>
<td class="align-middle">{{ entry.points / entry.played | number: '1.0-2' }}</td> <td class="align-middle">{{ entry.points / entry.played | number: '1.0-2' }}</td>
<td class="align-middle">{{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }}</td> <td class="align-middle">{{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }}</td>
<td class="align-middle">{{ (entry.pointsWon - entry.pointsLost) / entry.played | number: '1.0-2' }}</td> <td class="align-middle">{{ (entry.pointsWon - entry.pointsLost) / entry.played | number: '1.0-2' }}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
} }

View File

@@ -5,24 +5,24 @@ import {Group} from "../../model/group";
import {Round} from "../../model/round"; import {Round} from "../../model/round";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {DecimalPipe, NgForOf} from "@angular/common"; import {DecimalPipe} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe"; import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TitleService} from "../../service/title.service"; import {TeamDisplayComponent} from "../team-display/team-display.component";
@Component({ @Component({
selector: 'app-round-overview', selector: 'app-round-overview',
imports: [ imports: [
NgForOf,
TeamPipe, TeamPipe,
DecimalPipe, DecimalPipe,
FullNamePipe TeamDisplayComponent
], ],
providers: [ providers: [
TeamPipe, TeamPipe,
FullNamePipe FullNamePipe
], ],
templateUrl: './round-overview.component.html', templateUrl: './round-overview.component.html',
standalone: true,
styleUrl: './round-overview.component.scss' styleUrl: './round-overview.component.scss'
}) })
export class RoundOverviewComponent implements OnInit { export class RoundOverviewComponent implements OnInit {
@@ -37,12 +37,10 @@ export class RoundOverviewComponent implements OnInit {
private tournamentService: TournamentService, private tournamentService: TournamentService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService
) { ) {
} }
ngOnInit() { ngOnInit() {
this.titleService.setTitle("Rondeoverzicht");
const tournamentId = this.route.snapshot.paramMap.get('id'); const tournamentId = this.route.snapshot.paramMap.get('id');
let roundId = Number(this.route.snapshot.paramMap.get('roundId')); let roundId = Number(this.route.snapshot.paramMap.get('roundId'));

View File

@@ -0,0 +1,29 @@
<h2 mat-dialog-title>Kies een invaller voor {{ data.player.name }}:</h2>
<mat-dialog-content>
@for (substitution of data.player.substitutions; track $index) {
<div class="row mt-3">
<div class="col-md-3">
<h5>{{ Event.getType(substitution.event) }}</h5>
</div>
<div class="col-md-6">
<mat-form-field appearance="fill">
<mat-label>Invaller</mat-label>
<mat-select [(ngModel)]="substitution.substitute">
<mat-option [value]="-1">Geen</mat-option>
@for (player of data.availablePlayers; track player.id) {
<mat-option [value]="player.id">
{{ player.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<!-- @if (substitute) {-->
<button mat-button [mat-dialog-close]="{substitutions: data.player.substitutions}">Opslaan</button>
<!-- }-->
<button mat-button (click)="onAnnulerenClick()">Annuleren</button>
</mat-dialog-actions>

View File

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

View File

@@ -0,0 +1,39 @@
import {Component, Input} from '@angular/core';
import {Event} from '../../model/event';
import {Tournament} from '../../model/tournament';
import {PlayerDisplayComponent} from '../player-display/player-display.component';
import {Team} from "../../model/team";
@Component({
selector: 'app-team-display',
standalone: true,
imports: [PlayerDisplayComponent],
template: `
<app-player-display
[player]="team.player1"
[event]="event"
[tournament]="tournament"
[exlicitSubstitute]="explicitSubstitute">
</app-player-display>
@if (event.doublesEvent && team.player2) {
@if (this.inline) {
/
} @else {
<br />
}
<app-player-display
[player]="team.player2"
[event]="event"
[tournament]="tournament">
</app-player-display>
}
`
})
export class TeamDisplayComponent {
@Input({ required: true }) team!: Team;
@Input({ required: true }) event!: Event;
@Input({ required: true }) tournament!: Tournament;
@Input({ required: false }) inline: boolean = true;
@Input({ required: false }) explicitSubstitute: boolean = false;
}

View File

@@ -1,51 +0,0 @@
@if (tournament) {
<mat-card appearance="outlined">
<mat-card-header>
<h5>Indeling voor {{ tournament.name }}</h5>
</mat-card-header>
<mat-card-content>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3">
<mat-card-header>
<h6>Indeling {{ TournamentEvent.getType(event.type) }}</h6>
</mat-card-header>
<mat-card-content>
<mat-accordion multi="true">
<mat-expansion-panel *ngFor="let group of event.groups">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<table class="table {{ event.doublesEvent ? 'w-100' : 'w-50' }}">
<thead class="thead-dark">
<tr>
<th scope="col" class="w-20">Naam</th>
<th scope="col" class="w-20">Club</th>
<th scope="col" class="w-10">Speelsterkte</th>
@if (event.doublesEvent) {
<th scope="col" class="w-20">Partner</th>
<th scope="col" class="w-20">Club</th>
<th scope="col" class="w-10">Speelsterkte</th>
}
</tr>
</thead>
<tbody>
<tr *ngFor="let team of group.teams">
<td class="align-middle">{{ team.player1 | fullName }}</td>
<td class="align-middle">{{ team.player1.club }}</td>
<td class="align-middle">{{ getStrength(team.player1.strength.valueOf()) }}</td>
@if (event.doublesEvent) {
<td class="align-middle">{{ team.player2 | fullName }}</td>
<td class="align-middle">{{ team.player2.club }}</td>
<td class="align-middle">{{ getStrength(team.player2.strength.valueOf()) }}</td>
}
</tr>
</tbody>
</table>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>
}

View File

@@ -1,61 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {NgForOf, NgIf} from "@angular/common";
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {Event} from "../../model/event";
import {Strength} from "../../model/player";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-divide',
imports: [
MatCard,
MatCardHeader,
NgIf,
MatCardContent,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
NgForOf,
FullNamePipe,
MatAccordion
],
templateUrl: './tournament-divide.component.html',
styleUrl: './tournament-divide.component.scss'
})
export class TournamentDivideComponent implements OnInit {
tournament?: Tournament;
constructor(
private tournamentService: TournamentService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
});
// this.tournamentService.getDivision(Number(id)).subscribe(data => {
// this.tournamentDivision = data;
// });
}
protected readonly TournamentEvent = Event;
getStrength(strength: string) {
for (let [key, value] of Object.entries(Strength)) {
if (key == strength) return value;
}
return "";
}
}

View File

@@ -4,13 +4,15 @@
<h5>Loting voor {{ tournament.name }}</h5> <h5>Loting voor {{ tournament.name }}</h5>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3"> @for (event of tournament.events; track event.id) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header> <mat-card-header>
<h6>Loting {{ TournamentEvent.getType(event.type) }}</h6> <h6>Loting {{ TournamentEvent.getType(event.type) }}</h6>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-accordion multi="true"> <mat-accordion multi="true">
<mat-expansion-panel *ngFor="let group of event.groups"> @for (group of event.groups; track group.id) {
<mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span> {{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span>
@@ -25,17 +27,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let match of group.rounds[0].matches"> @for (match of group.rounds[0].matches; track match.id) {
<tr>
<td class="align-middle">{{ match.team1 | teamText }}</td> <td class="align-middle">{{ match.team1 | teamText }}</td>
<td class="align-middle">-</td> <td class="align-middle">-</td>
<td class="align-middle">{{ match.team2 | teamText }}</td> <td class="align-middle">{{ match.team2 | teamText }}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
}
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }

View File

@@ -3,7 +3,6 @@ import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {NgForOf, NgIf} from "@angular/common";
import { import {
MatAccordion, MatAccordion,
MatExpansionPanel, MatExpansionPanel,
@@ -18,13 +17,11 @@ import {FullNamePipe} from "../../pipes/fullname-pipe";
selector: 'app-tournament-draw', selector: 'app-tournament-draw',
imports: [ imports: [
MatCard, MatCard,
NgIf,
MatCardContent, MatCardContent,
MatCardHeader, MatCardHeader,
MatExpansionPanel, MatExpansionPanel,
MatExpansionPanelHeader, MatExpansionPanelHeader,
MatExpansionPanelTitle, MatExpansionPanelTitle,
NgForOf,
TeamPipe, TeamPipe,
MatAccordion MatAccordion
], ],
@@ -33,6 +30,7 @@ import {FullNamePipe} from "../../pipes/fullname-pipe";
TeamPipe TeamPipe
], ],
templateUrl: './tournament-draw.component.html', templateUrl: './tournament-draw.component.html',
standalone: true,
styleUrl: './tournament-draw.component.scss' styleUrl: './tournament-draw.component.scss'
}) })
export class TournamentDrawComponent implements OnInit { export class TournamentDrawComponent implements OnInit {

View File

@@ -1,11 +1,6 @@
@if (tournament) { @if (tournament) {
<form (ngSubmit)="saveTournament()"> <form (ngSubmit)="saveTournament()">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<!--
<mat-card-header>
<h5>{{ isEditMode ? 'Bewerk toernooi' : 'Toevoegen toernooi' }}</h5>
</mat-card-header>
-->
<mat-card-content> <mat-card-content>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">

View File

@@ -11,7 +11,6 @@ import {MatInput} from "@angular/material/input";
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio"; import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
import {CurrencyPipe, registerLocaleData} from "@angular/common"; import {CurrencyPipe, registerLocaleData} from "@angular/common";
import nl from "@angular/common/locales/nl"; import nl from "@angular/common/locales/nl";
import {TitleService} from "../../service/title.service";
import {NgxMaskDirective} from "ngx-mask"; import {NgxMaskDirective} from "ngx-mask";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {MatCheckbox} from "@angular/material/checkbox"; import {MatCheckbox} from "@angular/material/checkbox";
@@ -42,6 +41,7 @@ registerLocaleData(nl);
CurrencyPipe CurrencyPipe
], ],
templateUrl: './tournament-edit.component.html', templateUrl: './tournament-edit.component.html',
standalone: true,
styleUrl: './tournament-edit.component.scss' styleUrl: './tournament-edit.component.scss'
}) })
export class TournamentEditComponent implements OnInit { export class TournamentEditComponent implements OnInit {
@@ -54,7 +54,6 @@ export class TournamentEditComponent implements OnInit {
private currencyPipe: CurrencyPipe, private currencyPipe: CurrencyPipe,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService,
private _snackBar: MatSnackBar, private _snackBar: MatSnackBar,
) { ) {
this.tournament = new Tournament(); this.tournament = new Tournament();
@@ -63,13 +62,10 @@ export class TournamentEditComponent implements OnInit {
ngOnInit() { ngOnInit() {
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
if (id) { if (id) {
this.titleService.setTitle("Bewerk Toernooi");
this.isEditMode = true; this.isEditMode = true;
this.tournamentService.getById(Number(id)).subscribe(data => { this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data; this.tournament = data;
}) })
} else {
this.titleService.setTitle("Nieuw Toernooi");
} }
} }

View File

@@ -11,7 +11,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (tournament of getActiveTournaments(); track tournament) { @for (tournament of getActiveTournaments(); track tournament.id) {
<tr> <tr>
<td class="align-middle">{{ tournament.id }}</td> <td class="align-middle">{{ tournament.id }}</td>
<td class="align-middle"><a [routerLink]="['/tournaments', tournament.id, 'manage']">{{ tournament.name }}</a></td> <td class="align-middle"><a [routerLink]="['/tournaments', tournament.id, 'manage']">{{ tournament.name }}</a></td>
@@ -38,7 +38,7 @@
</tr> </tr>
} }
@if (showInactive) { @if (showInactive) {
@for (tournament of getInactiveTournaments(); track tournament) { @for (tournament of getInactiveTournaments(); track tournament.id) {
<tr> <tr>
<td class="align-middle">{{ tournament.id }}</td> <td class="align-middle">{{ tournament.id }}</td>
<td class="align-middle"><a [routerLink]="['/tournaments', tournament.id, 'manage']">{{ tournament.name }}</a></td> <td class="align-middle"><a [routerLink]="['/tournaments', tournament.id, 'manage']">{{ tournament.name }}</a></td>

View File

@@ -1,5 +1,4 @@
import {AfterContentChecked, Component, OnInit} from '@angular/core'; import {AfterContentChecked, Component, OnInit} from '@angular/core';
import {NgFor, NgIf} from "@angular/common";
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
import {Tournament} from "../../model/tournament"; import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
@@ -7,24 +6,24 @@ import {MatAnchor, MatButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatCard, MatCardContent} from "@angular/material/card"; import {MatCard, MatCardContent} from "@angular/material/card";
import {MatTableModule} from "@angular/material/table"; import {MatTableModule} from "@angular/material/table";
import {TitleService} from "../../service/title.service"; import {HeaderService} from "../../service/header.service";
@Component({ @Component({
selector: 'app-tournament-list', selector: 'app-tournament-list',
imports: [ imports: [
NgFor, RouterLink, NgIf, MatAnchor, MatIcon, MatCard, MatCardContent, MatButton, MatTableModule RouterLink, MatAnchor, MatIcon, MatCard, MatCardContent, MatButton, MatTableModule
], ],
templateUrl: './tournament-list.component.html', templateUrl: './tournament-list.component.html',
standalone: true,
styleUrl: './tournament-list.component.scss' styleUrl: './tournament-list.component.scss'
}) })
export class TournamentListComponent implements OnInit, AfterContentChecked { export class TournamentListComponent implements OnInit {
tournaments: Tournament[]; tournaments: Tournament[];
showInactive: boolean = false; showInactive: boolean = false;
constructor( constructor(
private tournamentService: TournamentService, private tournamentService: TournamentService
private titleService: TitleService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -33,10 +32,6 @@ export class TournamentListComponent implements OnInit, AfterContentChecked {
}); });
} }
ngAfterContentChecked() {
this.titleService.setTitle("Toernooien");
}
protected readonly Tournament = Tournament; protected readonly Tournament = Tournament;
clearDraw(tournament: Tournament) { clearDraw(tournament: Tournament) {

View File

@@ -1,10 +1,5 @@
@if (tournament) { @if (tournament) {
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<!--
<mat-card-header>
<h5>{{ tournament.name }}</h5>
</mat-card-header>
-->
<mat-card-content> <mat-card-content>
<mat-tab-group animationDuration="0ms" disableRipple="true"> <mat-tab-group animationDuration="0ms" disableRipple="true">
@@ -25,6 +20,13 @@
</ng-template> </ng-template>
<app-tournament-validate></app-tournament-validate> <app-tournament-validate></app-tournament-validate>
</mat-tab> </mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>settings</mat-icon>
&nbsp;Beheer
</ng-template>
<app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab>
} }
@if (tournament.status == 'DIVIDED') { @if (tournament.status == 'DIVIDED') {
@@ -51,7 +53,7 @@
<h6>Totaal: {{ getTournamentMatchCount(tournament)}} wedstrijden</h6> <h6>Totaal: {{ getTournamentMatchCount(tournament)}} wedstrijden</h6>
</mat-card-header> </mat-card-header>
</mat-card> </mat-card>
@for (event of tournament.events; track event) { @for (event of tournament.events; track event.id) {
@if (event.groups.length > 0) { @if (event.groups.length > 0) {
<mat-card appearance="outlined" class="m-3"> <mat-card appearance="outlined" class="m-3">
<mat-card-header> <mat-card-header>
@@ -59,7 +61,8 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-accordion multi="true"> <mat-accordion multi="true">
<mat-expansion-panel *ngFor="let group of event.groups"> @for (group of event.groups; track group.id) {
<mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span> {{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span>
@@ -79,84 +82,101 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let team of group.teams"> @for (team of group.teams; track team.id) {
<tr>
<td class="align-middle">{{ team.player1 | fullName }}</td> <td class="align-middle">{{ team.player1 | fullName }}</td>
<td class="align-middle">{{ team.player1.club }}</td> <td class="align-middle">{{ team.player1.club }}</td>
<td class="align-middle">{{ getStrength(team.player1.strength.valueOf()) }}</td> <td class="align-middle">{{ getStrength(team.player1.strength.valueOf()) }}</td>
@if (event.doublesEvent) { @if (event.doublesEvent && team.player2) {
<td class="align-middle">{{ team.player2 | fullName }}</td> <td class="align-middle">{{ team.player2 | fullName }}</td>
<td class="align-middle">{{ team.player2?.club }}</td> <td class="align-middle">{{ team.player2.club }}</td>
<td class="align-middle">{{ getStrength(team.player2?.strength?.valueOf()) }}</td> <td class="align-middle">{{ getStrength(team.player2.strength.valueOf()) }}</td>
} }
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }
} }
</mat-tab> </mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>settings</mat-icon>
&nbsp;Beheer
</ng-template>
<app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab>
} }
@if (tournament.status == 'ONGOING') { @if (tournament.status == 'ONGOING') {
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
&nbsp;Actieve wedstrijden &nbsp;Actieve wedstrijden&nbsp;&nbsp;
&nbsp;
@if (this.activeMatches().length > 0) { @if (this.activeMatches().length > 0) {
<span class="badge text-bg-success">{{ this.activeMatches().length }}</span> <span class="badge text-bg-success">{{ this.activeMatches().length }}</span>
} }
</ng-template> </ng-template>
<div class="tab-content-wrapper">
@if (this.activeMatches().length > 0) { @if (this.activeMatches().length > 0) {
<table class="table table-hover w-100 m-4"> <h6 class="mt-3"></h6>
<thead>
<tr>
<th colspan="3">Wedstrijd</th>
<th>Onderdeel/Ronde</th>
<th>Start</th>
<th>Baan</th>
<th></th>
</tr>
</thead>
<tbody>
@for (activeMatch of this.activeMatches(); track activeMatch.match.id) { @for (activeMatch of this.activeMatches(); track activeMatch.match.id) {
<tr> <mat-expansion-panel>
<td class="align-middle">{{ activeMatch.match.team1 | teamText }}</td> <mat-expansion-panel-header>
<td class="align-middle">-</td> <div class="col-md-2 d-flex align-items-center">Baan {{ activeMatch.match.court }}</div>
<td class="align-middle">{{ activeMatch.match.team2 | teamText }}</td> <div class="col-md-3">
<td class="align-middle">{{ activeMatch.group.name }} {{ activeMatch.round.name }}</td> <app-team-display
<td class="align-middle">{{ activeMatch.match.startTime | date: 'HH:mm' }}</td> [team]="activeMatch.match.team1"
<td class="align-middle">{{ activeMatch.match.court }}</td> [event]="activeMatch.event"
<td nowrap class="align-middle"> [tournament]="this.tournament"
<button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.group, activeMatch.round)"> [inline]="false">
<mat-icon>edit</mat-icon> </app-team-display>
</div>
<div class="col-md-1 d-flex align-items-center">-</div>
<div class="col-md-3">
<app-team-display
[team]="activeMatch.match.team2"
[event]="activeMatch.event"
[tournament]="this.tournament"
[inline]="false">
</app-team-display>
</div>
<div class="col-md-3 d-flex align-items-center">{{ activeMatch.group.name }} {{ activeMatch.round.name }}</div>
</mat-expansion-panel-header>
<div class="row">
<hr/>
<div class="col-md-3">Teller: {{ activeMatch.match.counter | fullName }}</div>
<div class="col-md-5"></div>
<div class="col-md-2">Starttijd: {{ activeMatch.match.startTime | date: 'HH:mm' }}</div>
<div class="col-md-2">Duur: {{ getDuration(activeMatch.match.startTime) | date: 'mm:ss' }}</div>
</div>
<mat-action-row>
<button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.event, activeMatch.group, activeMatch.round)">
<mat-icon>leaderboard</mat-icon>
Uitslag invoeren Uitslag invoeren
</button> </button>
<button mat-icon-button [matMenuTriggerFor]="activeMatchMenu" [matMenuTriggerData]="{ match: activeMatch.match }" class="menu-button"> <button mat-button (click)="changeCounter(activeMatch.match)">
<mat-icon>more_vert</mat-icon> <mat-icon>person</mat-icon>
Teller wijzigen
</button> </button>
</td> <button mat-button (click)="stopMatch(activeMatch.match)">
</tr>
}
</tbody>
</table>
<mat-menu #activeMatchMenu="matMenu">
<ng-template matMenuContent let-match="match">
<button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
Wedstrijd stoppen Wedstrijd stoppen
</button> </button>
</ng-template> </mat-action-row>
</mat-menu> </mat-expansion-panel>
<br>
}
} @else { } @else {
<h6 class="mt-3">Geen actieve wedstrijden</h6> <h6 class="mt-3">Geen actieve wedstrijden</h6>
} }
</div>
</mat-tab> </mat-tab>
} }
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') { @if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') {
@@ -167,12 +187,13 @@
</ng-template> </ng-template>
<mat-tab-group animationDuration="0ms" disableRipple="true"> <mat-tab-group animationDuration="0ms" disableRipple="true">
<ng-container *ngFor="let event of tournament.events"> @for (event of tournament.events; track event.id) {
<ng-container *ngFor="let group of event.groups"> @for (group of event.groups; track group.id) {
<mat-tab label="{{group.id}}"> <mat-tab label="{{group.id}}">
<ng-template mat-tab-label> <ng-template mat-tab-label>
<!--<mat-icon>list</mat-icon>&nbsp;--> @if (group.status == "FINISHED") {
{{ group.name }} <mat-icon>check</mat-icon>
&nbsp;}{{ group.name }}
&nbsp; &nbsp;
@if (getActiveMatchCountForGroup(group) > 0) { @if (getActiveMatchCountForGroup(group) > 0) {
<span class="badge text-bg-success">{{ getActiveMatchCountForGroup(group) }}</span> <span class="badge text-bg-success">{{ getActiveMatchCountForGroup(group) }}</span>
@@ -205,7 +226,7 @@
disableRipple="true" disableRipple="true"
[(selectedIndex)]="activeRoundTab" [(selectedIndex)]="activeRoundTab"
(selectedTabChange)="onRoundTabChange($event)"> (selectedTabChange)="onRoundTabChange($event)">
<ng-container *ngFor="let round of group.rounds; index as roundIndex"> @for (round of group.rounds; track round.id; let roundIndex = $index) {
<mat-tab label="{{round.id}}"> <mat-tab label="{{round.id}}">
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon>{{ getRoundIcon(round.status) }}</mat-icon> <mat-icon>{{ getRoundIcon(round.status) }}</mat-icon>
@@ -220,22 +241,24 @@
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
Ronde starten Ronde starten
</button> </button>
}
<button mat-menu-item (click)="printMatchSheets(round)"> <button mat-menu-item (click)="printMatchSheets(round)">
<mat-icon>print</mat-icon> <mat-icon>print</mat-icon>
Wedstrijdbriefjes printen Wedstrijdbriefjes printen
</button> </button>
} @if (!round.isFinalsRound) {
<button mat-menu-item (click)="printRoundOverview(round)"> <button mat-menu-item (click)="printRoundOverview(round)">
<mat-icon>print</mat-icon> <mat-icon>print</mat-icon>
Rondeoverzicht printen Rondeoverzicht printen
</button> </button>
}
@if (round.status == 'IN_PROGRESS' && checkRoundComplete(round)) { @if (round.status == 'IN_PROGRESS' && checkRoundComplete(round)) {
<button mat-menu-item (click)="finishRound(round)"> <button mat-menu-item (click)="finishRound(round)">
<mat-icon>check</mat-icon> <mat-icon>check</mat-icon>
Ronde afsluiten Ronde afsluiten
</button> </button>
} }
@if (group.status != 'FINISHED' && round.status == 'FINISHED' && (roundIndex + 1) == group.rounds.length) { @if (group.status != 'FINISHED' && round.status == 'FINISHED' && (roundIndex + 1) == group.rounds.length && !round.isFinalsRound) {
<button mat-menu-item (click)="newRound(group)"> <button mat-menu-item (click)="newRound(group)">
<mat-icon>playlist_add</mat-icon> <mat-icon>playlist_add</mat-icon>
Nieuwe ronde Nieuwe ronde
@@ -247,46 +270,90 @@
<h6 class="mt-3">Wedstrijden</h6> <h6 class="mt-3">Wedstrijden</h6>
@if (round.status == 'NOT_STARTED') { @if (round.status == 'NOT_STARTED') {
<table class="table table-hover m-4 wide w-100"> <table class="table table-hover m-4 wide w-95">
<tbody> <tbody>
<tr *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<td class="align-middle w-team">{{ match.team1 | teamText }}</td> <tr>
<td class="align-middle w-team">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team">{{ match.team2 | teamText }}</td> <td class="align-middle w-team">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-fill"></td> <td class="align-middle w-fill"></td>
</tr> </tr>
}
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4">
<b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}
</td>
</tr> </tr>
} }
</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-100"> <table class="table table-hover m-4 wide w-95">
<tbody> <tbody>
<tr *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td> <tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">{{ match.team2 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</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, 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">
<span *ngFor="let game of match.games" class="col-2">{{ game.score1 }}-{{ game.score2 }}</span> @for (game of match.games; track game.id) {
<span class="col-2">{{ game.score1 }}-{{ game.score2 }}</span>
}
@if (match.games.length == 2) { @if (match.games.length == 2) {
<span class="col-2"></span> <span class="col-2"></span>
} }
@@ -295,8 +362,8 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<mat-menu #finishedMatchMenu="matMenu"> <mat-menu #finishedMatchMenu="matMenu">
<button mat-menu-item (click)="editResult(match, 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>
@@ -304,6 +371,7 @@
} }
</td> </td>
</tr> </tr>
}
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
@@ -312,21 +380,37 @@
</tbody> </tbody>
</table> </table>
} @else if (round.status == 'FINISHED') { } @else if (round.status == 'FINISHED') {
<table class="table table-hover m-4 wide {{ this.groupIsDoublesType(group) ? 'w-100' : 'w-100' }}"> <table class="table table-hover m-4 wide {{ this.groupIsDoublesType(group) ? 'w-95' : 'w-95' }}">
<tbody> <tbody>
<tr *ngFor="let match of round.matches"> @for (match of round.matches; track match.id) {
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td> <tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">{{ match.team2 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-fill"> <td class="align-middle w-fill">
<div class="row result align-items-center"> <div class="row result align-items-center">
<span *ngFor="let game of match.games" class="col-2">{{ game.score1 }}-{{ game.score2 }}</span> @for (game of match.games; track game.id) {
<span class="col-2">{{ game.score1 }}-{{ game.score2 }}</span>
}
@if (match.games.length == 2) { @if (match.games.length == 2) {
<span class="col-2"></span> <span class="col-2"></span>
} }
</div> </div>
</td> </td>
</tr> </tr>
}
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
@@ -336,6 +420,7 @@
</table> </table>
} }
@if (!round.isFinalsRound) {
<h6 class="mt-3">Stand</h6> <h6 class="mt-3">Stand</h6>
<table class="table w-75 m-4"> <table class="table w-75 m-4">
@@ -357,9 +442,17 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr *ngFor="let entry of getStandingsForRound(round, group).entries"> @for (entry of getStandingsForRound(round, group).entries; track entry.position) {
<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 ) {
@@ -377,74 +470,44 @@
} }
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
} @else if (round.isFinalsRound && round.status == 'FINISHED') {
<h6 class="mt-3">Uitslag</h6>
<table class="table w-50 m-4">
<tbody>
<tr>
<td>1e Plaats</td>
<td>{{ (checkWinner(round.matches[0]) == 1 ? round.matches[0].team1 : round.matches[0].team2) | teamText }}</td>
</tr>
<tr>
<td>2e Plaats</td>
<td>{{ (checkWinner(round.matches[0]) == 1 ? round.matches[0].team2 : round.matches[0].team1) | teamText }}</td>
</tr>
<tr>
<td>3e Plaats</td>
<td>{{ (checkWinner(round.matches[1]) == 1 ? round.matches[1].team1 : round.matches[1].team2) | teamText }}</td>
</tr>
</tbody>
</table>
}
</mat-tab> </mat-tab>
</ng-container> }
</mat-tab-group>
</mat-tab>
</ng-container>
</ng-container>
</mat-tab-group> </mat-tab-group>
</mat-tab> </mat-tab>
} }
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') { }
</mat-tab-group>
</mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
&nbsp;Beheer &nbsp;Beheer
</ng-template> </ng-template>
<mat-tab-group animationDuration="0ms" disableRipple="true"> <app-tournament-players [tournament]="tournament"></app-tournament-players>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>group</mat-icon>
&nbsp;Spelerslijst
</ng-template>
<table class="table table-hover w-75 m-4">
<thead>
<tr>
<th>Naam</th>
<th>Onderdelen</th>
<th>Kosten</th>
<th>Betaald</th>
<th>Aanwezig</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let tournamentPlayer of tournament.tournamentPlayers">
<td>{{ tournamentPlayer.name }}</td>
<td>
<ng-container *ngFor="let event of tournamentPlayer.events">
{{ event }}&nbsp;
</ng-container>
</td>
<td>
{{ tournament.costsPerEvent[tournamentPlayer.events.length - 1] | currency:'EUR':'symbol':'1.2-2':'nl' }}
</td>
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.paid" (change)="playerPaid($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.paid) {
Betaald
} @else {
Nog niet betaald
}
</mat-slide-toggle>
</td>
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.present" (change)="playerPresent($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.present) {
Aanwezig
} @else {
Nog niet aanwezig
}
</mat-slide-toggle>
</td>
</tr>
</tbody>
</table>
</mat-tab>
</mat-tab-group>
</mat-tab> </mat-tab>
} }
</mat-tab-group> </mat-tab-group>

View File

@@ -1,6 +1,7 @@
td { td {
vertical-align: middle; vertical-align: middle;
} }
td, th { td, th {
background-color: transparent !important; background-color: transparent !important;
} }
@@ -8,6 +9,7 @@ td, th {
table.wide td, table.wide th { table.wide td, table.wide th {
height: 4em; height: 4em;
} }
.winner { .winner {
color: green; color: green;
font-weight: bold; font-weight: bold;
@@ -24,3 +26,19 @@ td.w-sep {
td.w-fill { td.w-fill {
width: 35%; width: 35%;
} }
.w-90 {
width: 90% !important;
}
.w-95 {
width: 95% !important;
}
.mat-menu-panel {
z-index: 1000 !important;
}
.tab-content-wrapper {
margin: 0 4px 0 4px;
}

View File

@@ -1,12 +1,13 @@
import {Component, inject, Input, OnInit} from '@angular/core'; import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import { import {
MatAccordion, MatAccordion,
MatExpansionPanel, MatExpansionPanel,
MatExpansionPanelActionRow,
MatExpansionPanelHeader, MatExpansionPanelHeader,
MatExpansionPanelTitle MatExpansionPanelTitle
} from "@angular/material/expansion"; } from "@angular/material/expansion";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {CurrencyPipe, DatePipe, DecimalPipe, NgClass, NgForOf, NgIf} from "@angular/common"; import {DatePipe, DecimalPipe, NgClass} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe"; import {TeamPipe} from "../../pipes/team-pipe";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
@@ -16,7 +17,7 @@ import {MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {Group} from "../../model/group"; import {Group} from "../../model/group";
import {Round} from "../../model/round"; import {Round} from "../../model/round";
import {MatMenu, MatMenuContent, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {Match} from "../../model/match"; import {Match} from "../../model/match";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MatTab, MatTabChangeEvent, MatTabGroup, MatTabLabel} from "@angular/material/tabs"; import {MatTab, MatTabChangeEvent, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
@@ -25,13 +26,17 @@ import {MatDialog} from "@angular/material/dialog";
import {MatchResultPipe} from "../../pipes/match-result-pipe"; import {MatchResultPipe} from "../../pipes/match-result-pipe";
import {Event} from "../../model/event"; import {Event} from "../../model/event";
import {TournamentValidateComponent} from "../tournament-validate/tournament-validate.component"; import {TournamentValidateComponent} from "../tournament-validate/tournament-validate.component";
import {Strength} from "../../model/player"; import {Player, Strength} from "../../model/player";
import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {CourtSelectionComponent} from "../court-selection/court-selection.component"; import {CourtSelectionComponent} from "../court-selection/court-selection.component";
import {Standings} from "../../model/standings"; import {Standings} from "../../model/standings";
import {Title} from '@angular/platform-browser'; import {HeaderService} from "../../service/header.service";
import {TitleService} from "../../service/title.service"; import {TournamentPlayersComponent} from "../tournament-players/tournament-players.component";
import {TournamentPlayer} from "../../model/tournamentPlayer";
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',
@@ -44,8 +49,6 @@ import {TitleService} from "../../service/title.service";
MatExpansionPanel, MatExpansionPanel,
MatExpansionPanelHeader, MatExpansionPanelHeader,
MatExpansionPanelTitle, MatExpansionPanelTitle,
NgForOf,
NgIf,
TeamPipe, TeamPipe,
MatIcon, MatIcon,
NgClass, NgClass,
@@ -61,9 +64,10 @@ import {TitleService} from "../../service/title.service";
MatIconButton, MatIconButton,
DecimalPipe, DecimalPipe,
TournamentValidateComponent, TournamentValidateComponent,
MatSlideToggle, TournamentPlayersComponent,
CurrencyPipe, MatExpansionPanelActionRow,
MatMenuContent TeamDisplayComponent,
MatTooltip,
], ],
providers: [ providers: [
FullNamePipe, FullNamePipe,
@@ -71,11 +75,12 @@ import {TitleService} from "../../service/title.service";
MatchResultPipe MatchResultPipe
], ],
templateUrl: './tournament-manage.component.html', templateUrl: './tournament-manage.component.html',
standalone: true,
styleUrl: './tournament-manage.component.scss' styleUrl: './tournament-manage.component.scss'
}) })
export class TournamentManageComponent implements OnInit { export class TournamentManageComponent implements OnInit, OnDestroy {
@Input() tournament: Tournament; tournament: Tournament;
activeRoundTab: number = 0; activeRoundTab: number = 0;
@@ -84,8 +89,7 @@ export class TournamentManageComponent implements OnInit {
private _snackBar: MatSnackBar, private _snackBar: MatSnackBar,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService, private headerService: HeaderService,
private title: Title
) { ) {
} }
@@ -96,11 +100,24 @@ export class TournamentManageComponent implements OnInit {
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.titleService.setTitle(this.tournament.name); (tournament) => this.headerService.setTitle(tournament.name)
this.title.setTitle(this.tournament.name); );
}); }
ngOnDestroy() {
this.headerService.clearTitle();
}
getDuration(startTime: string | Date): Date {
const start = new Date(startTime);
const now = new Date();
const diffInMs = now.getTime() - start.getTime();
// Convert milliseconds to a Date object starting from epoch
// This allows the date pipe to format it as mm:ss
return new Date(diffInMs);
} }
onRoundTabChange(event: MatTabChangeEvent) { onRoundTabChange(event: MatTabChangeEvent) {
@@ -112,7 +129,7 @@ export class TournamentManageComponent implements OnInit {
}); });
} }
getRoundIcon(status: String) { getRoundIcon(status: string) {
if (status == "FINISHED") { if (status == "FINISHED") {
return "check"; return "check";
} else if (status == "IN_PROGRESS") { } else if (status == "IN_PROGRESS") {
@@ -137,87 +154,120 @@ export class TournamentManageComponent implements OnInit {
} }
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)) {
this._snackBar.open('Deze wedstrijd bevat spelers die aan het tellen zijn.')
} else { } else {
this.courtSelectionDialog.open(CourtSelectionComponent, { this.courtSelectionDialog.open(CourtSelectionComponent, {
data: { data: {
match: match, match: match,
availableCourts: this.getAvailableCourts(), availableCourts: this.getAvailableCourts(),
totalCourts: this.tournament.courts totalCourts: this.tournament.courts,
availableCounters: this.getAvailableCounters(match)
}, },
minWidth: '800px', minWidth: '800px',
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).subscribe(data => { this.loadTournament(this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId));
this.tournament = data;
})
} }
}); });
} }
} }
matchContainsPlayersThatArePlaying(match: Match): boolean { matchContainsPlayersThatArePlaying(match: Match): boolean {
let activePlayers: number[] = [];
for (let activeMatch of this.activeMatches()) {
activePlayers.push(activeMatch.match.team1.player1.id);
if (activeMatch.match.team1.player2) activePlayers.push(activeMatch.match.team1.player2.id);
activePlayers.push(activeMatch.match.team2.player1.id);
if (activeMatch.match.team2.player2) activePlayers.push(activeMatch.match.team2.player2.id);
}
let matchPlayers: number[] = []; let matchPlayers: number[] = [];
matchPlayers.push(match.team1.player1.id); matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(match.team1.player2.id); if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(match.team2.player1.id); matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(match.team2.player2.id); if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
let playersThatArePlaying = activePlayers.filter(Set.prototype.has, new Set(matchPlayers));
return playersThatArePlaying.length > 0; let matchPlayersThatArePlaying = this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers));
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 {
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);
let matchPlayersThatAreCounting = this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers));
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 {
for (let tournamentPlayer of this.tournament.tournamentPlayers) {
if (tournamentPlayer.playerId == player.id) {
return tournamentPlayer;
}
}
return null!;
}
getTournamentPlayerFromId(tournamentPlayerId: number): TournamentPlayer {
for (let tournamentPlayer of this.tournament.tournamentPlayers) {
if (tournamentPlayer.id == tournamentPlayerId) {
return tournamentPlayer;
}
}
return null!;
} }
getAvailableCourts(): number[] { getAvailableCourts(): number[] {
@@ -230,28 +280,42 @@ export class TournamentManageComponent implements OnInit {
return courts.filter(court => activeCourts.indexOf(court) < 0); return courts.filter(court => activeCourts.indexOf(court) < 0);
} }
getAvailableCounters(match: Match): TournamentPlayer[] {
const 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);
const counterIds = this.tournament.playersAvailable.filter(player => !matchPlayers.includes(player));
return counterIds.map(id => this.getTournamentPlayerFromId(id));
}
getPlayerOrSubstitute(player: Player, type: String): TournamentPlayer {
return this.getSubstituteForEvent(player, type) || this.getTournamentPlayerFromPlayer(player);
}
getSubstituteForEvent(player: Player, type: String): TournamentPlayer | undefined {
const tournamentPlayer = this.tournament.tournamentPlayers.find(
tp => tp.playerId === player.id
);
if (!tournamentPlayer) return undefined;
const substitution = tournamentPlayer.substitutions.find(
s => s.event === type
);
if (!substitution) return undefined;
return this.getTournamentPlayerFromId(substitution.substitute);
}
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;
})
}
playerPaid($event: MatSlideToggleChange, playerId: number) {
this.tournamentService.playerPaid(this.tournament.id, playerId, $event.checked).subscribe(() => {
this._snackBar.open('Opgeslagen.');
});
}
playerPresent($event: MatSlideToggleChange, playerId: number) {
this.tournamentService.playerPresent(this.tournament.id, playerId, $event.checked).subscribe(() => {
this._snackBar.open('Opgeslagen.');
});
} }
getStrength(strength: string | undefined) { getStrength(strength: string | undefined) {
@@ -277,7 +341,7 @@ export class TournamentManageComponent implements OnInit {
for (const round of group.rounds) { for (const round of group.rounds) {
for (const match of round.matches) { for (const match of round.matches) {
if (match.status == 'IN_PROGRESS') { if (match.status == 'IN_PROGRESS') {
matches.push(new ActiveMatch(match, round, group)); matches.push(new ActiveMatch(match, round, group, event));
} }
} }
} }
@@ -308,16 +372,15 @@ export class TournamentManageComponent implements OnInit {
matchResultDialog = inject(MatDialog); matchResultDialog = inject(MatDialog);
courtSelectionDialog = inject(MatDialog); courtSelectionDialog = inject(MatDialog);
counterSelectionDialog = inject(MatDialog);
editResult(match: Match, group: Group, round: Round) { editResult(match: Match, event: Event, group: Group, round: Round) {
this.matchResultDialog.open(MatchResultComponent, { this.matchResultDialog.open(MatchResultComponent, {
data: {match: match, group: group, round: round}, data: {match: match, tournament: this.tournament, event: event, group: group, round: round},
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;
})
} }
}); });
} }
@@ -367,16 +430,97 @@ export class TournamentManageComponent implements OnInit {
} }
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 {
constructor(match: Match, round: Round, group: Group) { constructor(match: Match, round: Round, group: Group, event: Event) {
this.match = match; this.match = match;
this.round = round; this.round = round;
this.group = group; this.group = group;
this.event = event;
} }
match: Match; match: Match;
round: Round; round: Round;
group: Group; group: Group;
event: Event;
} }

View File

@@ -0,0 +1,108 @@
@if (tournament) {
<mat-tab-group animationDuration="0ms" disableRipple="true">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>euro</mat-icon>
&nbsp;Administratie
</ng-template>
<table class="table table-hover w-75 m-4">
<thead>
<tr>
<th>Naam</th>
<th>Onderdelen</th>
<th>Kosten</th>
<th>Betaald</th>
@if (tournament.status == 'ONGOING') {
<th>Aanwezig</th>
}
</tr>
</thead>
<tbody>
@for (tournamentPlayer of tournament.tournamentPlayers; track tournamentPlayer.playerId) {
<tr>
<td>{{ tournamentPlayer.name }}</td>
<td>
@for (event of tournamentPlayer.events; track event) {
{{ event }}&nbsp;
}
</td>
<td>
{{ tournament.costsPerEvent[tournamentPlayer.events.length - 1] | currency:'EUR':'symbol':'1.2-2':'nl' }}
</td>
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.paid" (change)="playerPaid($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.paid) {
Betaald
} @else {
Nog niet betaald
}
</mat-slide-toggle>
</td>
@if (tournament.status == 'ONGOING') {
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.present" (change)="playerPresent($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.present) {
Aanwezig
} @else {
Nog niet aanwezig
}
</mat-slide-toggle>
</td>
}
</tr>
}
</tbody>
</table>
</mat-tab>
@if (tournament.status == 'ONGOING') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>group</mat-icon>
&nbsp;Tellers en invallers
</ng-template>
<table class="table table-hover w-75 m-4">
<thead>
<tr>
<th>Naam</th>
<th>Wedstrijden geteld</th>
<th>Onderdelen</th>
<th>Invallers</th>
<th></th>
</tr>
</thead>
<tbody>
@for (tournamentPlayer of tournament.tournamentPlayers; track tournamentPlayer.playerId) {
<tr>
<td>{{ tournamentPlayer.name }}</td>
<td>{{ tournamentPlayer.counts }}</td>
<td>
@for (event of tournamentPlayer.events; track event) {
{{ event }}&nbsp;
}
</td>
<td>{{ hasSubstitutes(tournamentPlayer) ? 'Ja' : 'Nee' }}</td>
<td>
<button mat-icon-button [matMenuTriggerFor]="dividedTournamentMenu" class="menu-button">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #dividedTournamentMenu="matMenu">
<button mat-menu-item (click)="findSubstitute(tournamentPlayer)">
<mat-icon>autorenew</mat-icon>
Invallers
</button>
<button mat-menu-item> <!--(click)="drawTournament()"-->
<mat-icon>do_not_disturb_on</mat-icon>
Deelname stoppen
</button>
</mat-menu>
</td>
</tr>
}
</tbody>
</table>
</mat-tab>
}
</mat-tab-group>
}

View File

@@ -1,3 +1,7 @@
td, th { td, th {
background-color: transparent !important; background-color: transparent !important;
} }
td {
vertical-align: middle;
}

View File

@@ -0,0 +1,92 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {CurrencyPipe} from "@angular/common";
import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute} from "@angular/router";
import {Tournament} from "../../model/tournament";
import {FormsModule} from "@angular/forms";
import {MatSnackBar} from "@angular/material/snack-bar";
import {MatIcon} from "@angular/material/icon";
import {MatIconButton} from "@angular/material/button";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {MatDialog} from "@angular/material/dialog";
import {SubstituteSelectionComponent} from "../substitute-selection/substitute-selection.component";
import {TournamentPlayer} from "../../model/tournamentPlayer";
import {MatTab, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
@Component({
selector: 'app-tournament-players',
imports: [
CurrencyPipe,
MatSlideToggle,
FormsModule,
MatIcon,
MatIconButton,
MatMenu,
MatMenuItem,
MatMenuTrigger,
MatTab,
MatTabGroup,
MatTabLabel
],
templateUrl: './tournament-players.component.html',
standalone: true,
styleUrl: './tournament-players.component.scss'
})
export class TournamentPlayersComponent implements OnInit {
@Input() tournament: Tournament;
constructor(
private tournamentService: TournamentService,
private route: ActivatedRoute,
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) {
this.tournamentService.playerPaid(this.tournament.id, playerId, $event.checked).subscribe(() => {
this._snackBar.open('Opgeslagen.');
});
}
playerPresent($event: MatSlideToggleChange, playerId: number) {
this.tournamentService.playerPresent(this.tournament.id, playerId, $event.checked).subscribe(() => {
this._snackBar.open('Opgeslagen.');
});
}
substituteSelectionDialog = inject(MatDialog);
findSubstitute(player: TournamentPlayer) {
this.substituteSelectionDialog.open(SubstituteSelectionComponent, {
data: {
player: player,
availablePlayers: this.tournament.tournamentPlayers
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.tournamentService.playerSubstitute(this.tournament.id, player.playerId, result.substitutions).subscribe(data => {
this.tournament = data;
});
}
});
}
hasSubstitutes(tournamentPlayer: TournamentPlayer) {
return tournamentPlayer.substitutions.filter(s => s.substitute != null && s.substitute >= 0).length > 0;
}
}

View File

@@ -4,7 +4,8 @@
<h5>Inschrijvingen voor {{ tournament.name }}</h5> <h5>Inschrijvingen voor {{ tournament.name }}</h5>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3"> @for (event of tournament.events; track event.id) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header> <mat-card-header>
<h6>{{ TournamentEvent.getType(event.type) }} ({{ event.registrations.length}} inschrijvingen)</h6> <h6>{{ TournamentEvent.getType(event.type) }} ({{ event.registrations.length}} inschrijvingen)</h6>
</mat-card-header> </mat-card-header>
@@ -21,7 +22,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let registration of event.registrations"> @for (registration of event.registrations; track registration.id) {
<tr>
<td class="align-middle">{{ registration.player | fullName }}</td> <td class="align-middle">{{ registration.player | fullName }}</td>
<td class="align-middle">{{ registration.player.club }}</td> <td class="align-middle">{{ registration.player.club }}</td>
@if (event.doublesEvent) { @if (event.doublesEvent) {
@@ -29,10 +31,12 @@
<td class="align-middle">{{ registration.partner | fullName }}</td> <td class="align-middle">{{ registration.partner | fullName }}</td>
} }
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
}
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }

View File

@@ -3,10 +3,8 @@ import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {Tournament} from "../../model/tournament"; import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {NgForOf, NgIf} from "@angular/common";
import {Event} from "../../model/event"; import {Event} from "../../model/event";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TitleService} from "../../service/title.service";
@Component({ @Component({
selector: 'app-tournament-registrations', selector: 'app-tournament-registrations',
@@ -14,11 +12,10 @@ import {TitleService} from "../../service/title.service";
MatCard, MatCard,
MatCardHeader, MatCardHeader,
MatCardContent, MatCardContent,
NgForOf,
NgIf,
FullNamePipe FullNamePipe
], ],
templateUrl: './tournament-registrations.component.html', templateUrl: './tournament-registrations.component.html',
standalone: true,
styleUrl: './tournament-registrations.component.scss' styleUrl: './tournament-registrations.component.scss'
}) })
export class TournamentRegistrationsComponent implements OnInit { export class TournamentRegistrationsComponent implements OnInit {
@@ -29,11 +26,9 @@ export class TournamentRegistrationsComponent implements OnInit {
private tournamentService: TournamentService, private tournamentService: TournamentService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: TitleService
) {} ) {}
ngOnInit() { ngOnInit() {
this.titleService.setTitle("Inschrijvingen");
const id = this.route.snapshot.paramMap.get('id'); const id = this.route.snapshot.paramMap.get('id');
this.tournamentService.getById(Number(id)).subscribe(data => { this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data; this.tournament = data;

View File

@@ -1,7 +1,7 @@
@if (tournamentValidation && tournament) { @if (tournamentValidation && tournament) {
<mat-card appearance="outlined" class="m-3"> <mat-card appearance="outlined" class="m-3">
<mat-card-header> <mat-card-header>
<h6>Toernooi</h6> <h6>Toernooi ({{ getTournamentMatchCount(tournament)}} wedstrijden)</h6>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-expansion-panel [disabled]="tournamentValidation.validations.length == 0"> <mat-expansion-panel [disabled]="tournamentValidation.validations.length == 0">
@@ -11,17 +11,20 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ul> <ul>
<li *ngFor="let validation of tournamentValidation.validations"> @for (validation of tournamentValidation.validations; track validation) {
<li>
<mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon> <mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon>
{{ validation.message }} {{ validation.message }}
</li> </li>
}
</ul> </ul>
</mat-expansion-panel> </mat-expansion-panel>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3"> @for (event of tournament.events; track event.id) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header> <mat-card-header>
<h6>{{ TournamentEvent.getType(event.type) }}</h6> <h6>{{ TournamentEvent.getType(event.type) }} ({{ getEventMatchCount(event)}} wedstrijden)</h6>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-accordion multi="true"> <mat-accordion multi="true">
@@ -43,7 +46,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let registration of event.registrations"> @for (registration of event.registrations; track registration.id) {
<tr>
<td class="align-middle">{{ registration.player | fullName }}</td> <td class="align-middle">{{ registration.player | fullName }}</td>
<td class="align-middle">{{ registration.player.club }}</td> <td class="align-middle">{{ registration.player.club }}</td>
@if (event.doublesEvent) { @if (event.doublesEvent) {
@@ -51,6 +55,7 @@
<td class="align-middle">{{ registration.partner?.club }}</td> <td class="align-middle">{{ registration.partner?.club }}</td>
} }
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</mat-expansion-panel> </mat-expansion-panel>
@@ -61,13 +66,16 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ul> <ul>
<li *ngFor="let validation of getEventValidation(event.id)?.validations"> @for (validation of getEventValidation(event.id)?.validations; track validation) {
<li>
<mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon> <mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon>
<player-link [validationMessage]="validation.message"></player-link> <player-link [validationMessage]="validation.message"></player-link>
</li> </li>
}
</ul> </ul>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }
}

View File

@@ -1,7 +1,6 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {Tournament} from "../../model/tournament"; import {Tournament} from "../../model/tournament";
import {NgForOf, NgIf} from "@angular/common";
import {Event} from "../../model/event"; import {Event} from "../../model/event";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
@@ -24,17 +23,16 @@ import {PlayerLinkComponent} from "../player-link/player-link.component";
MatCard, MatCard,
MatCardHeader, MatCardHeader,
MatCardContent, MatCardContent,
NgForOf,
MatExpansionPanel, MatExpansionPanel,
MatExpansionPanelTitle, MatExpansionPanelTitle,
MatExpansionPanelHeader, MatExpansionPanelHeader,
NgIf,
MatAccordion, MatAccordion,
MatIcon, MatIcon,
FullNamePipe, FullNamePipe,
PlayerLinkComponent PlayerLinkComponent
], ],
templateUrl: './tournament-validate.component.html', templateUrl: './tournament-validate.component.html',
standalone: true,
styleUrl: './tournament-validate.component.scss' styleUrl: './tournament-validate.component.scss'
}) })
export class TournamentValidateComponent implements OnInit { export class TournamentValidateComponent implements OnInit {
@@ -101,4 +99,24 @@ export class TournamentValidateComponent implements OnInit {
return hasErrors; return hasErrors;
} }
getEventMatchCount(event: Event) {
let numTeams = 0;
if (event.doublesEvent) {
numTeams = event.registrations.length / 2;
} else {
numTeams = event.registrations.length;
}
let rounds = numTeams <= 4 ? 3 : 4;
let matchesPerRound = Math.trunc(numTeams / 2);
return rounds * matchesPerRound;
}
getTournamentMatchCount(tournament: Tournament) {
let count = 0;
for (let event of tournament.events) {
count += this.getEventMatchCount(event);
}
return count;
}
} }

View File

@@ -1,4 +1,5 @@
export class Game { export class Game {
id: number;
score1: number; score1: number;
score2: number; score2: number;
} }

View File

@@ -1,5 +1,6 @@
import { Team } from "./team"; import { Team } from "./team";
import {Game} from "./game"; import {Game} from "./game";
import {Player} from "./player";
export class Match { export class Match {
id: number; id: number;
@@ -11,4 +12,7 @@ export class Match {
endTime: Date; endTime: Date;
games: Game[]; games: Game[];
court: number; court: number;
counter: Player;
canStart?: boolean;
cantStartReason?: string;
} }

View File

@@ -10,4 +10,5 @@ export class Round {
quit: Team[]; quit: Team[];
drawnOut: Team; drawnOut: Team;
standings: Standings; standings: Standings;
isFinalsRound: boolean;
} }

View File

@@ -2,6 +2,7 @@ import {Player} from "./player";
import {FullNamePipe} from "../pipes/fullname-pipe"; import {FullNamePipe} from "../pipes/fullname-pipe";
export class Team { export class Team {
id: number;
player1: Player; player1: Player;
player2: Player; player2: Player | null;
} }

View File

@@ -8,10 +8,13 @@ export class Tournament {
status: string; status: string;
events: Event[]; events: Event[];
tournamentPlayers: TournamentPlayer[]; tournamentPlayers: TournamentPlayer[];
maxEvents: number; maxEvents: number = 2;
costsPerEvent: number[] = [0, 0, 0]; costsPerEvent: number[] = [10, 20, 0];
courts: number; courts: number;
active: boolean; active: boolean;
playersPlaying: number[];
playersCounting: number[];
playersAvailable: number[];
static getStatus(tournament: Tournament): string { static getStatus(tournament: Tournament): string {
if (tournament.status == "CLOSED") return "Afgerond"; if (tournament.status == "CLOSED") return "Afgerond";

View File

@@ -1,8 +1,18 @@
export class TournamentPlayer { export class TournamentPlayer {
id: number;
playerId: number; playerId: number;
name: string; name: string;
events: string[]; events: string[];
paid: boolean; paid: boolean;
present: boolean; present: boolean;
counting: boolean;
counts: number;
substitutions: TournamentPlayerSubstitution[];
}
export class TournamentPlayerSubstitution {
substitutionId: number;
event: string;
substitute: number = -1;
} }

View File

@@ -4,6 +4,7 @@ export class TournamentRegistration {
editable: boolean; editable: boolean;
date: string; date: string;
status: string; status: string;
active: boolean;
events: EventRegistration[]; events: EventRegistration[];
static getStatus(tournamentRegistration: TournamentRegistration): string { static getStatus(tournamentRegistration: TournamentRegistration): string {

View File

@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import {FullNamePipe} from "../pipes/fullname-pipe"; import {FullNamePipe} from "./fullname-pipe";
@Pipe({ @Pipe({
name: 'teamText', name: 'teamText',
standalone: true standalone: true
@@ -10,10 +11,8 @@ export class TeamPipe implements PipeTransform {
transform(team: any, args?: any): any { transform(team: any, args?: any): any {
if (team.player2 != null) { if (team.player2 != null) {
// return this.player1.getFullName() + " / " + this.player2.getFullName();
return this.fullNamePipe.transform(team.player1) + " / " + this.fullNamePipe.transform(team.player2); return this.fullNamePipe.transform(team.player1) + " / " + this.fullNamePipe.transform(team.player2);
} }
// return this.player1.getFullName();
return this.fullNamePipe.transform(team.player1); return this.fullNamePipe.transform(team.player1);
} }
} }

View File

@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class HeaderService {
private headerSource = new BehaviorSubject<string | null>(null);
header$ = this.headerSource.asObservable();
setTitle(header: string) {
this.headerSource.next(header);
}
clearTitle() {
this.headerSource.next(null);
}
}

View File

@@ -1,16 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class TitleService {
private title = new BehaviorSubject('');
currentTitle = this.title.asObservable();
constructor() { }
setTitle(message: string) {
this.title.next(message);
}
}

View File

@@ -5,6 +5,7 @@ import {Tournament} from "../model/tournament";
import {TournamentValidation} from "../model/tournamentValidation"; import {TournamentValidation} from "../model/tournamentValidation";
import {Result} from "../model/result"; import {Result} from "../model/result";
import { environment } from "../../environments/environment" import { environment } from "../../environments/environment"
import {TournamentPlayerSubstitution} from "../model/tournamentPlayer";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -72,8 +73,8 @@ export class TournamentService {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/groups/${groupId}/new`, null); return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/groups/${groupId}/new`, null);
} }
public startMatch(tournamentId: number, matchId: number, court: number): Observable<Tournament> { public startMatch(tournamentId: number, matchId: number, court: number, counter: number): Observable<Tournament> {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/start/${court}`, null); return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/start?court=${court}&counter=${counter}`, null);
} }
public stopMatch(tournamentId: number, matchId: number): Observable<Tournament> { public stopMatch(tournamentId: number, matchId: number): Observable<Tournament> {
@@ -92,6 +93,14 @@ export class TournamentService {
return this.http.patch<void>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/present/${paid}`, null); return this.http.patch<void>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/present/${paid}`, null);
} }
public playerSubstitute(tournamentId: number, playerId: number, substitutions: TournamentPlayerSubstitution[]): Observable<Tournament> {
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

@@ -1,7 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server'; import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config); const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
export default bootstrap; export default bootstrap;

View File

@@ -2,3 +2,11 @@
html, body { height: 100%; } html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
/*
.cdk-overlay-container {
z-index: 2000 !important;
}
*/
.mat-tooltip {
white-space: pre-line !important;
}

1812
yarn.lock

File diff suppressed because it is too large Load Diff