Compare commits

...

50 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
72 changed files with 4487 additions and 2608 deletions

View File

@@ -32,11 +32,11 @@
Player-registrations: oude toernooien verbergen via knop
Onderdeel afsluiten
Wedstrijd opgave
Blessures
Invallers
Titels pagina's
Authenticatie
Progress indicator tijdens communicatie backend
Bij tellers rekening houden met invallers
https://blog.shhdharmen.me/browser-storage-in-angular-ssr
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,
"dependencies": {
"@angular/animations": "^20.1.6",
"@angular/cdk": "^20.1.5",
"@angular/common": "^20.1.6",
"@angular/compiler": "^20.1.6",
"@angular/core": "^20.1.6",
"@angular/forms": "^20.1.6",
"@angular/material": "^20.1.5",
"@angular/material-moment-adapter": "^20.1.5",
"@angular/platform-browser": "^20.1.6",
"@angular/platform-browser-dynamic": "^20.1.6",
"@angular/platform-server": "^20.1.6",
"@angular/router": "^20.1.6",
"@angular/ssr": "^20.1.5",
"@angular/animations": "^20.3.2",
"@angular/cdk": "^20.2.5",
"@angular/common": "^20.3.2",
"@angular/compiler": "^20.3.2",
"@angular/core": "^20.3.2",
"@angular/forms": "^20.3.2",
"@angular/material": "^20.2.5",
"@angular/material-moment-adapter": "^20.2.5",
"@angular/platform-browser": "^20.3.2",
"@angular/platform-browser-dynamic": "^20.3.2",
"@angular/platform-server": "^20.3.2",
"@angular/router": "^20.3.2",
"@angular/ssr": "^20.3.3",
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7",
"express": "^4.21.2",
"jwt-decode": "^4.0.0",
"moment": "2.30.1",
"ngx-cookie-service-ssr": "^20.1.0",
"ngx-mask": "^20.0.3",
@@ -37,10 +38,10 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/build": "^20.1.5",
"@angular/cli": "^20.1.5",
"@angular/compiler-cli": "^20.1.6",
"@angular/localize": "^20.1.6",
"@angular/build": "^20.3.3",
"@angular/cli": "^20.3.3",
"@angular/compiler-cli": "^20.3.2",
"@angular/localize": "^20.3.2",
"@types/express": "^5.0.3",
"@types/jasmine": "~5.1.0",
"@types/node": "^24.2.1",

View File

@@ -17,10 +17,10 @@
<mat-icon>group</mat-icon>
Spelers
</a>
@if (this.userService.isLoggedIn()) {
@if (this.authService.isLoggedIn()) {
<button mat-flat-button [matMenuTriggerFor]="accountMenu">
<mat-icon>person</mat-icon>
{{ user }}
{{ this.authService.getUsername() }}
</button>
<mat-menu #accountMenu="matMenu">
<button mat-menu-item (click)="logOut()">

View File

@@ -6,9 +6,10 @@ import {MatIcon} from "@angular/material/icon";
import {MatToolbar} from "@angular/material/toolbar";
import {filter, map, Subscription} from "rxjs";
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 {HeaderService} from "./service/header.service";
import {AuthService} from "./auth/auth.service";
@Component({
selector: 'app-root',
@@ -19,11 +20,11 @@ import {HeaderService} from "./service/header.service";
export class AppComponent implements OnInit {
header: string;
user: string;
userSubscription: Subscription;
// user: string;
// userSubscription: Subscription;
constructor(
protected userService: UserService,
protected authService: AuthService,
private router: Router,
protected activatedRoute: ActivatedRoute,
private tournamentService: TournamentService,
@@ -32,7 +33,7 @@ export class AppComponent implements OnInit {
}
ngOnInit() {
this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
// this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
@@ -55,8 +56,8 @@ export class AppComponent implements OnInit {
}
logOut() {
this.userService.removeUser();
this.router.navigate(['/auth/login']);
this.authService.logout();
this.router.navigate(['/login']);
}
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 {routes} from './app.routes';
@@ -7,10 +7,9 @@ import {HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptorsFromDi}
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
import {provideMomentDateAdapter} from "@angular/material-moment-adapter";
import {MAT_SNACK_BAR_DEFAULT_OPTIONS} from "@angular/material/snack-bar";
import {AuthGuard} from "./authentication/authguard";
import {TokenInterceptor} from "./authentication/tokenInterceptor";
import {ErrorInterceptor} from "./authentication/errorInterceptor";
import {provideEnvironmentNgxMask, provideNgxMask} from "ngx-mask";
import {provideNgxMask} from "ngx-mask";
import {provideAnimations} from "@angular/platform-browser/animations";
import {AuthInterceptor} from "./auth/auth.interceptor";
export const appConfig: ApplicationConfig = {
providers: [
@@ -18,12 +17,11 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes),
provideClientHydration(),
provideHttpClient(withFetch(), withInterceptorsFromDi()),
provideAnimationsAsync(),
provideAnimations(),
provideMomentDateAdapter(undefined, {useUtc: false}),
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 2500}},
AuthGuard,
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
provideNgxMask(),
]
};

View File

@@ -1,35 +1,31 @@
import { Routes } from '@angular/router';
import { PlayerListComponent} from "./components/player-list/player-list.component";
import {Routes} from '@angular/router';
import {PlayerListComponent} from "./components/player-list/player-list.component";
import {PlayerEditComponent} from "./components/player-edit/player-edit.component";
import {TournamentListComponent} from "./components/tournament-list/tournament-list.component";
import {TournamentEditComponent} from "./components/tournament-edit/tournament-edit.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 {TournamentManageComponent} from "./components/tournament-manage/tournament-manage.component";
import {MatchSheetsComponent} from "./components/match-sheets/match-sheets.component";
import {RoundOverviewComponent} from "./components/round-overview/round-overview.component";
import {AuthGuard} from "./authentication/authguard";
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 = [
{ path: '', component: TournamentListComponent, canActivate: [AuthGuard], data: { header: 'Toernooien' }},
{ path: 'tournaments', component: TournamentListComponent, canActivate: [AuthGuard], data: { header: 'Toernooien' }},
{ path: 'tournaments/add', component: TournamentEditComponent, canActivate: [AuthGuard], data: { header: 'Nieuw Toernooi' }},
{ path: 'tournaments/:id/edit', component: TournamentEditComponent, canActivate: [AuthGuard], data: { header: 'Bewerk Toernooi' }},
{ path: 'tournaments/:id/registrations', component: TournamentRegistrationsComponent, canActivate: [AuthGuard], data: { header: 'Inschrijvingen' }},
{ path: 'tournaments/:id/validate', component: TournamentValidateComponent, canActivate: [AuthGuard], data: { header: 'Toernooi' }},
{ path: 'tournaments/:id/divide', component: TournamentDivideComponent, canActivate: [AuthGuard], data: { header: 'Toernooi valideren' }},
{ path: 'tournaments/:id/draw', component: TournamentDrawComponent, canActivate: [AuthGuard], data: { header: 'Toernooi loten' }},
{ path: 'tournaments/:id/manage', component: TournamentManageComponent, canActivate: [AuthGuard]},
{ path: 'tournaments/:id/manage/:tab', component: TournamentManageComponent, canActivate: [AuthGuard], data: { header: 'Toernooien' }},
{ path: 'players', component: PlayerListComponent, canActivate: [AuthGuard], data: { header: 'Spelers' }},
{ path: 'players/add', component: PlayerEditComponent, canActivate: [AuthGuard], data: { header: 'Nieuwe Speler' }},
{ path: 'players/edit/:id', component: PlayerEditComponent, canActivate: [AuthGuard], data: { header: 'Bewerk Speler' }},
{ path: 'players/:id/registrations', component: PlayerRegistrationsComponent, canActivate: [AuthGuard]},
{ path: 'tournaments/:id/rounds/:roundId/matchsheets', component: MatchSheetsComponent, canActivate: [AuthGuard]},
{ path: 'tournaments/:id/rounds/:roundId/overview', component: RoundOverviewComponent, canActivate: [AuthGuard], data: { header: 'Rondeoverzicht' }},
{ path: 'auth/login', component: LoginComponent, data: { header: 'Inloggen'}}
{ path: '', component: TournamentListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'tournaments', component: TournamentListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'tournaments/add', component: TournamentEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Nieuw Toernooi' }},
{ path: 'tournaments/:id/edit', component: TournamentEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Bewerk Toernooi' }},
{ path: 'tournaments/:id/registrations', component: TournamentPlayersComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Inschrijvingen' }},
{ path: 'tournaments/:id/draw', component: TournamentDrawComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooi loten' }},
{ path: 'tournaments/:id/manage', component: TournamentManageComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'tournaments/:id/manage/:tab', component: TournamentManageComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Toernooien' }},
{ path: 'players', component: PlayerListComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Spelers' }},
{ path: 'players/add', component: PlayerEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Nieuwe Speler' }},
{ path: 'players/:id/edit', component: PlayerEditComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Bewerk Speler' }},
{ path: 'players/:id/registrations', component: PlayerRegistrationsComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'tournaments/:id/rounds/:roundId/matchsheets', component: MatchSheetsComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER' }},
{ path: 'tournaments/:id/rounds/:roundId/overview', component: RoundOverviewComponent, canActivate: [AuthGuard], data: { role: 'ROLE_USER', header: 'Rondeoverzicht' }},
{ path: 'login', component: LoginComponent, data: { header: 'Inloggen'}}
];

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

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

View File

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

View File

@@ -1,78 +1,72 @@
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {ActivatedRoute, Router} from '@angular/router';
import {MatCard, MatCardContent} from "@angular/material/card";
import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatButton} from "@angular/material/button";
import {MatInput} from "@angular/material/input";
import {NgIf} from "@angular/common";
import {AuthenticationService} from "../../authentication/authentication.service";
import {UserService} from "../../authentication/user.service";
import {LoginCredentials} from "../../authentication/loginCredentials";
import {User} from "../../authentication/user";
import {jwtDecode, JwtPayload} from "jwt-decode";
import {AuthService} from "../../auth/auth.service";
import {MatSnackBar} from "@angular/material/snack-bar";
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
imports: [
MatCardContent,
ReactiveFormsModule,
MatFormField,
MatButton,
MatInput,
MatLabel,
MatCard,
],
styleUrls: ['./login.component.scss']
selector: 'app-login',
templateUrl: './login.component.html',
imports: [
MatCardContent,
ReactiveFormsModule,
MatFormField,
MatButton,
MatInput,
MatLabel,
MatCard,
FormsModule,
],
standalone: true,
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
public form: FormGroup;
private returnUrl: string;
private ipAddress: string;
constructor(
private route: ActivatedRoute,
private fb: FormBuilder,
private authenticationService: AuthenticationService,
private router: Router,
private userPersistenceService: UserService,
) {
this.initializeForm();
username: string = "";
password: string = "";
message: string = "";
constructor(private authService: AuthService,
private route: ActivatedRoute,
private router: Router,
private snackBar: MatSnackBar) {
}
private initializeForm() {
this.form = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
ngOnInit() {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
login() {
if (this.form.invalid) {
return;
}
public login(): void {
localStorage.removeItem("app.token");
let loginCredentials = new LoginCredentials();
loginCredentials = {
...loginCredentials,
...this.form.value,
};
this.authService.login(this.username, this.password)
.subscribe({
next: (token) => {
localStorage.setItem("app.token", token);
this.authenticationService
.login(loginCredentials)
.subscribe(
{
next: (user: User) => {
this.userPersistenceService.setUser(user);
this.router.navigate([this.returnUrl]);
}
const decodedToken = jwtDecode<JwtPayload>(token);
// @ts-ignore
localStorage.setItem("app.roles", decodedToken.scope);
// this.router.navigateByUrl("/persons");
// 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>
</div>
</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">
<div class="w-100" [ngClass]="{'winner': validateResult() == 1}">
{{ data.match.team1 | teamText }}
<div class="w-100" [ngClass]="{'winner': isValidResult == 1}">
<app-team-display
[team]="data.match.team1"
[event]="data.event"
[tournament]="data.tournament"
[inline]="false">
</app-team-display>
</div>
</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].score1">
</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].score1">
</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].score1">
</mat-form-field>
</mat-grid-tile>
@for (game of result.games; track $index; let i = $index) {
<mat-grid-tile>
<mat-form-field appearance="outline">
<input matInput
type="number"
min="0"
max="30"
[(ngModel)]="game.score1"
(blur)="complementScores()"
(ngModelChange)="validateResult()"
[tabindex]="i * 2 + 1">
</mat-form-field>
</mat-grid-tile>
}
<mat-grid-tile colspan="2">
<div class="w-100" [ngClass]="{'winner': validateResult() == -1}">
{{ data.match.team2 | teamText }}
<div class="w-100" [ngClass]="{'winner': isValidResult == -1}">
<app-team-display
[team]="data.match.team2"
[event]="data.event"
[tournament]="data.tournament"
[inline]="false">
</app-team-display>
</div>
</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>
<mat-grid-tile>
@for (game of result.games; track $index; let i = $index) {
<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>
@for (gameNum of [1, 2, 3]; track gameNum) {
<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>
</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-dialog-content>
<mat-dialog-actions>
<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>

View File

@@ -19,36 +19,41 @@ import {MatGridList, MatGridTile} from "@angular/material/grid-list";
import {Round} from "../../model/round";
import {Group} from "../../model/group";
import {Game} from "../../model/game";
import {TeamDisplayComponent} from "../team-display/team-display.component";
import {Event} from "../../model/event";
import {Tournament} from "../../model/tournament";
@Component({
selector: 'app-match-result',
imports: [
MatDialogContent,
MatDialogActions,
MatButton,
MatDialogClose,
MatDialogTitle,
TeamPipe,
MatInput,
ReactiveFormsModule,
FormsModule,
MatFormField,
MatGridList,
MatGridTile,
NgClass
],
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './match-result.component.html',
styleUrl: './match-result.component.scss'
selector: 'app-match-result',
imports: [
MatDialogContent,
MatDialogActions,
MatButton,
MatDialogClose,
MatDialogTitle,
MatInput,
ReactiveFormsModule,
FormsModule,
MatFormField,
MatGridList,
MatGridTile,
NgClass,
TeamDisplayComponent
],
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './match-result.component.html',
standalone: true,
styleUrl: './match-result.component.scss'
})
export class MatchResultComponent {
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;
if (data.match.games.length == 0) {
@@ -63,6 +68,8 @@ export class MatchResultComponent {
this.result.games.push(new Game());
}
}
this.validateResult();
}
readonly dialogRef = inject(MatDialogRef<MatchResultComponent>);
@@ -75,13 +82,18 @@ export class MatchResultComponent {
this.result.games[game - 1].score2 = 21;
}
this.validateResult();
}
onAnnulerenClick() {
this.dialogRef.close();
}
validateResult(): number {
complementScores() {
// console.log("in complementScores");
}
validateResult(): void {
let valid : boolean = true;
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);
@@ -90,7 +102,7 @@ export class MatchResultComponent {
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 {
@@ -113,29 +125,31 @@ export class MatchResultComponent {
}
matchResult(result: Result): number {
let gameBalance = 0;
if (result.games[0].score1 < result.games[0].score2) {
gameBalance--;
} else {
gameBalance++;
}
if (result.games[1].score1 < result.games[1].score2) {
gameBalance--;
} else {
gameBalance++;
}
let team1Wins = 0;
let team2Wins = 0;
if (Math.abs(gameBalance) == 2 && (result.games[2].score1 != undefined || result.games[2].score2 != undefined)) {
return 0;
}
// Count wins for games 1 and 2
if (result.games[0].score1 > result.games[0].score2) team1Wins++;
else team2Wins++;
if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) {
if (result.games[2].score1 < result.games[2].score2) {
gameBalance--;
} else {
gameBalance++;
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
}
return team1Wins > team2Wins ? 1 : -1;
}
return Math.sign(gameBalance);
// Match is 1-1, check game 3
if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) {
if (result.games[2].score1 > result.games[2].score2) team1Wins++;
else team2Wins++;
return team1Wins > team2Wins ? 1 : -1;
}
return 0; // Incomplete
}
}

View File

@@ -3,12 +3,26 @@
<div class="nobreak">
<mat-card appearance="outlined">
<mat-card-content>
<h6>{{ tournament.name }}</h6>
<br>
<br>
<div class="row">
<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 class="col-2">
<mat-form-field appearance="outline">
@@ -28,7 +42,14 @@
</div>
<div class="row">
<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 class="col-2">
<mat-form-field appearance="outline">
@@ -50,9 +71,6 @@
<div class="col-6">
<u>Graag de winnaar omcirkelen</u>
</div>
<div class="col-6 text-end">
{{ group.name }} {{ round.name }}
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -11,27 +11,31 @@ import {MatFormField} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms";
import {HeaderService} from "../../service/header.service";
import {TeamDisplayComponent} from "../team-display/team-display.component";
import {Event} from "../../model/event";
@Component({
selector: 'app-match-sheets',
selector: 'app-match-sheets',
imports: [
MatCard,
MatCardContent,
TeamPipe,
MatFormField,
MatInput,
ReactiveFormsModule
ReactiveFormsModule,
TeamDisplayComponent
],
providers: [
TeamPipe,
FullNamePipe
],
templateUrl: './match-sheets.component.html',
styleUrl: './match-sheets.component.scss'
providers: [
TeamPipe,
FullNamePipe
],
templateUrl: './match-sheets.component.html',
standalone: true,
styleUrl: './match-sheets.component.scss'
})
export class MatchSheetsComponent implements OnInit, OnDestroy {
tournament: Tournament;
event: Event;
group: Group;
round: Round;
@@ -53,6 +57,7 @@ export class MatchSheetsComponent implements OnInit, OnDestroy {
for (let group of event.groups) {
for (let round of group.rounds) {
if (round.id == roundId) {
this.event = event;
this.group = group;
this.round = round;
this.headerService.setTitle(`Wedstrijdbriefjes ${this.group.name} ${this.round.name}`);

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

@@ -15,7 +15,7 @@ import {MatSnackBar} from "@angular/material/snack-bar";
import {NgxMaskDirective} from "ngx-mask";
@Component({
selector: 'app-player-edit',
selector: 'app-player-edit',
imports: [
FormsModule,
RouterLink,
@@ -37,14 +37,14 @@ import {NgxMaskDirective} from "ngx-mask";
ReactiveFormsModule,
NgxMaskDirective
],
templateUrl: './player-edit.component.html',
styleUrl: './player-edit.component.scss'
templateUrl: './player-edit.component.html',
standalone: true,
styleUrl: './player-edit.component.scss'
})
export class PlayerEditComponent implements OnInit {
player: Player;
isEditMode: boolean = false;
constructor(
private playerService: PlayerService,
private route: ActivatedRoute,

View File

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

View File

@@ -25,7 +25,7 @@
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef></th>
<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>
Bewerk
</a>
@@ -39,7 +39,8 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 20, 50]"
<mat-paginator [pageSizeOptions]="[10, 25, 100]"
[pageSize]="100"
showFirstLastButtons
aria-label="Select page of periodic elements">
</mat-paginator>

View File

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

View File

@@ -25,11 +25,12 @@ import {MatPaginator} from "@angular/material/paginator";
import {MatSort, MatSortHeader} from "@angular/material/sort";
@Component({
selector: 'app-player-list',
selector: 'app-player-list',
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],
templateUrl: './player-list.component.html',
styleUrl: './player-list.component.scss'
providers: [FullNamePipe],
templateUrl: './player-list.component.html',
standalone: true,
styleUrl: './player-list.component.scss'
})
export class PlayerListComponent implements AfterViewInit {

View File

@@ -16,19 +16,26 @@
</div>
<div class="col-md-4">
@if (eventRegistration.doublesEvent) {
<ng-container>
<mat-form-field appearance="fill">
<mat-label>Partner</mat-label>
<mat-select [value]="eventRegistration.partner" [disabled]="!tournamentRegistration.editable || !eventRegistration.registered" [(ngModel)]="eventRegistration.partner">
<mat-option>Geen</mat-option>
@for (player of getRelevantPlayers(eventRegistration.type); track player.id) {
<mat-option [value]="player.id">
{{ player | fullName }}
</mat-option>
}
</mat-select>
</mat-form-field>
</ng-container>
<mat-form-field appearance="fill">
<mat-label>Partner</mat-label>
<input type="text"
matInput
[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 }}
</mat-option>
}
</mat-autocomplete>
</mat-form-field>
}
</div>
<div class="col-6"></div>
@@ -51,8 +58,8 @@
}
@if (!this.showAll) {
<button mat-button (click)="this.showAll = true">
<mat-icon>search</mat-icon>
Toon oude toernooien
<mat-icon>expand</mat-icon>
Toon inactieve toernooien
</button>
}
</mat-card-content>

View File

@@ -9,15 +9,16 @@ import {RegistrationService} from "../../service/registration.service";
import {MatCheckbox, MatCheckboxChange} from "@angular/material/checkbox";
import {EventRegistration, TournamentRegistration} from "../../model/tournamentRegistration";
import {MatOption} from "@angular/material/core";
import {MatSelect} from "@angular/material/select";
import {MatIcon} from "@angular/material/icon";
import {MatAnchor, MatButton} from "@angular/material/button";
import {MatSnackBar} from "@angular/material/snack-bar";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {HeaderService} from "../../service/header.service";
import {MatAutocomplete, MatAutocompleteTrigger} from "@angular/material/autocomplete";
import {MatInput} from "@angular/material/input";
@Component({
selector: 'app-player-registrations',
selector: 'app-player-registrations',
imports: [
MatCard,
MatCardContent,
@@ -30,17 +31,21 @@ import {HeaderService} from "../../service/header.service";
MatCardActions,
RouterLink,
MatOption,
MatSelect,
// MatSelect,
MatIcon,
MatButton,
MatAnchor,
FullNamePipe,
MatAutocomplete,
MatAutocompleteTrigger,
MatInput,
],
providers: [
FullNamePipe
],
providers: [
FullNamePipe
],
templateUrl: './player-registrations.component.html',
styleUrl: './player-registrations.component.scss'
templateUrl: './player-registrations.component.html',
standalone: true,
styleUrl: './player-registrations.component.scss'
})
export class PlayerRegistrationsComponent implements OnInit {
@@ -51,6 +56,8 @@ export class PlayerRegistrationsComponent implements OnInit {
waitingForBackend: boolean = false;
private partnerSearchTerms: Map<number, string> = new Map();
constructor(
private _snackBar: MatSnackBar,
private playerService: PlayerService,
@@ -75,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) {
this.waitingForBackend = true;
this.registrationService.saveTournamentRegistrations(tournamentRegistration, this.player.id).subscribe(data => {
@@ -112,7 +153,7 @@ export class PlayerRegistrationsComponent implements OnInit {
if (this.showAll) {
return this.tournamentRegistrations;
} else {
return this.tournamentRegistrations.filter(t => (t.status == 'UPCOMING' || t.status == 'ONGOING'));
return this.tournamentRegistrations.filter(t => t.active);
}
}

View File

@@ -5,9 +5,23 @@
<tbody>
@for (match of round.matches; track match.id) {
<tr>
<td class="align-middle" style="width: 45%;">{{ match.team1 | teamText }}</td>
<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" 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>
</tr>
}
@@ -24,18 +38,20 @@
@for (match of round.matches; track match.id) {
<tr>
<td class="align-middle" style="width: 30%;">
@if (event.doublesEvent) {
{{ match.team1.player1 | fullName }} /<br>{{ match.team1.player2 | fullName }}
} @else {
{{ match.team1.player1 | fullName }}
}
<app-team-display
[team]="match.team1"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
<td class="align-middle w-sep">-</td>
<td class="align-middle" style="width: 30%;">
@if (event.doublesEvent) {
{{ match.team2.player1 | fullName }} /<br>{{ match.team2.player2 | fullName }}
} @else {
{{ match.team2.player1 | fullName }}
}
<app-team-display
[team]="match.team2"
[event]="this.event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</td>
<td class="align-middle" style="width: 35%;">
<div class="row result align-items-center">
@@ -87,7 +103,15 @@
@for (entry of round.standings.entries; track entry.position) {
<tr>
<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.points / entry.played | number: '1.0-2' }}</td>
<td class="align-middle">{{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }}</td>

View File

@@ -8,20 +8,22 @@ import {ActivatedRoute, Router} from "@angular/router";
import {DecimalPipe} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TeamDisplayComponent} from "../team-display/team-display.component";
@Component({
selector: 'app-round-overview',
selector: 'app-round-overview',
imports: [
TeamPipe,
DecimalPipe,
TeamDisplayComponent
],
providers: [
TeamPipe,
FullNamePipe
],
providers: [
TeamPipe,
FullNamePipe
],
templateUrl: './round-overview.component.html',
styleUrl: './round-overview.component.scss'
templateUrl: './round-overview.component.html',
standalone: true,
styleUrl: './round-overview.component.scss'
})
export class RoundOverviewComponent implements OnInit {

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,57 +0,0 @@
@if (tournament) {
<mat-card appearance="outlined">
<mat-card-header>
<h5>Indeling voor {{ tournament.name }}</h5>
</mat-card-header>
<mat-card-content>
@for (event of tournament.events; track event.id) {
<mat-card 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">
@for (group of event.groups; track group.id) {
<mat-expansion-panel>
<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>
@for (team of group.teams; track team.id) {
<tr>
<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">{{ team.player2 ? 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,58 +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 {
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,
MatCardContent,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
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

@@ -14,7 +14,7 @@ import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-draw',
selector: 'app-tournament-draw',
imports: [
MatCard,
MatCardContent,
@@ -25,12 +25,13 @@ import {FullNamePipe} from "../../pipes/fullname-pipe";
TeamPipe,
MatAccordion
],
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './tournament-draw.component.html',
styleUrl: './tournament-draw.component.scss'
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './tournament-draw.component.html',
standalone: true,
styleUrl: './tournament-draw.component.scss'
})
export class TournamentDrawComponent implements OnInit {

View File

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

View File

@@ -18,7 +18,7 @@ import {MatCheckbox} from "@angular/material/checkbox";
registerLocaleData(nl);
@Component({
selector: 'app-tournament-edit',
selector: 'app-tournament-edit',
imports: [
FormsModule,
RouterLink,
@@ -37,11 +37,12 @@ registerLocaleData(nl);
NgxMaskDirective,
MatCheckbox
],
providers: [
CurrencyPipe
],
templateUrl: './tournament-edit.component.html',
styleUrl: './tournament-edit.component.scss'
providers: [
CurrencyPipe
],
templateUrl: './tournament-edit.component.html',
standalone: true,
styleUrl: './tournament-edit.component.scss'
})
export class TournamentEditComponent implements OnInit {
@@ -65,7 +66,6 @@ export class TournamentEditComponent implements OnInit {
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
})
} else {
}
}

View File

@@ -9,12 +9,13 @@ import {MatTableModule} from "@angular/material/table";
import {HeaderService} from "../../service/header.service";
@Component({
selector: 'app-tournament-list',
selector: 'app-tournament-list',
imports: [
RouterLink, MatAnchor, MatIcon, MatCard, MatCardContent, MatButton, MatTableModule
],
templateUrl: './tournament-list.component.html',
styleUrl: './tournament-list.component.scss'
templateUrl: './tournament-list.component.html',
standalone: true,
styleUrl: './tournament-list.component.scss'
})
export class TournamentListComponent implements OnInit {

View File

@@ -1,10 +1,5 @@
@if (tournament) {
<mat-card appearance="outlined">
<!--
<mat-card-header>
<h5>{{ tournament.name }}</h5>
</mat-card-header>
-->
<mat-card-content>
<mat-tab-group animationDuration="0ms" disableRipple="true">
@@ -25,6 +20,13 @@
</ng-template>
<app-tournament-validate></app-tournament-validate>
</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') {
@@ -102,65 +104,79 @@
}
}
</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') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>play_arrow</mat-icon>
&nbsp;Actieve wedstrijden
&nbsp;
&nbsp;Actieve wedstrijden&nbsp;&nbsp;
@if (this.activeMatches().length > 0) {
<span class="badge text-bg-success">{{ this.activeMatches().length }}</span>
}
</ng-template>
<div class="tab-content-wrapper">
@if (this.activeMatches().length > 0) {
<h6 class="mt-3"></h6>
@if (this.activeMatches().length > 0) {
<table class="table table-hover w-100 m-4">
<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) {
<tr>
<td class="align-middle">{{ activeMatch.match.team1 | teamText }}</td>
<td class="align-middle">-</td>
<td class="align-middle">{{ activeMatch.match.team2 | teamText }}</td>
<td class="align-middle">{{ activeMatch.group.name }} {{ activeMatch.round.name }}</td>
<td class="align-middle">{{ activeMatch.match.startTime | date: 'HH:mm' }}</td>
<td class="align-middle">{{ activeMatch.match.court }}</td>
<td nowrap class="align-middle">
<button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.group, activeMatch.round)">
<mat-icon>edit</mat-icon>
Uitslag invoeren
</button>
<button mat-icon-button [matMenuTriggerFor]="activeMatchMenu" [matMenuTriggerData]="{ match: activeMatch.match }" class="menu-button">
<mat-icon>more_vert</mat-icon>
</button>
</td>
</tr>
}
</tbody>
</table>
<mat-menu #activeMatchMenu="matMenu">
<ng-template matMenuContent let-match="match">
<button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon>
Wedstrijd stoppen
</button>
</ng-template>
</mat-menu>
} @else {
<h6 class="mt-3">Geen actieve wedstrijden</h6>
}
@for (activeMatch of this.activeMatches(); track activeMatch.match.id) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<div class="col-md-2 d-flex align-items-center">Baan {{ activeMatch.match.court }}</div>
<div class="col-md-3">
<app-team-display
[team]="activeMatch.match.team1"
[event]="activeMatch.event"
[tournament]="this.tournament"
[inline]="false">
</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
</button>
<button mat-button (click)="changeCounter(activeMatch.match)">
<mat-icon>person</mat-icon>
Teller wijzigen
</button>
<button mat-button (click)="stopMatch(activeMatch.match)">
<mat-icon>stop</mat-icon>
Wedstrijd stoppen
</button>
</mat-action-row>
</mat-expansion-panel>
<br>
}
} @else {
<h6 class="mt-3">Geen actieve wedstrijden</h6>
}
</div>
</mat-tab>
}
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') {
@@ -175,8 +191,9 @@
@for (group of event.groups; track group.id) {
<mat-tab label="{{group.id}}">
<ng-template mat-tab-label>
<!--<mat-icon>list</mat-icon>&nbsp;-->
{{ group.name }}
@if (group.status == "FINISHED") {
<mat-icon>check</mat-icon>
&nbsp;}{{ group.name }}
&nbsp;
@if (getActiveMatchCountForGroup(group) > 0) {
<span class="badge text-bg-success">{{ getActiveMatchCountForGroup(group) }}</span>
@@ -224,22 +241,24 @@
<mat-icon>play_arrow</mat-icon>
Ronde starten
</button>
<button mat-menu-item (click)="printMatchSheets(round)">
}
<button mat-menu-item (click)="printMatchSheets(round)">
<mat-icon>print</mat-icon>
Wedstrijdbriefjes printen
</button>
@if (!round.isFinalsRound) {
<button mat-menu-item (click)="printRoundOverview(round)">
<mat-icon>print</mat-icon>
Wedstrijdbriefjes printen
Rondeoverzicht printen
</button>
}
<button mat-menu-item (click)="printRoundOverview(round)">
<mat-icon>print</mat-icon>
Rondeoverzicht printen
</button>
@if (round.status == 'IN_PROGRESS' && checkRoundComplete(round)) {
<button mat-menu-item (click)="finishRound(round)">
<mat-icon>check</mat-icon>
Ronde afsluiten
</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)">
<mat-icon>playlist_add</mat-icon>
Nieuwe ronde
@@ -251,45 +270,84 @@
<h6 class="mt-3">Wedstrijden</h6>
@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>
@for (match of round.matches; track match.id) {
<tr>
<td class="align-middle w-team">{{ match.team1 | teamText }}</td>
<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-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>
</tr>
}
@if (round.drawnOut) {
<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>
}
</tbody>
</table>
} @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>
@for (match of round.matches; track match.id) {
<tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td>
<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-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">
@if (match.status == 'NOT_STARTED') {
<button mat-button (click)="startMatch(match)">
<mat-icon>play_arrow</mat-icon>
Wedstrijd starten
</button>
@if (match.canStart) {
<button mat-button (click)="startMatch(match)">
<mat-icon>play_arrow</mat-icon>
Wedstrijd starten
</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') {
<button mat-button (click)="editResult(match, group, round)">
<mat-icon>edit</mat-icon>
Uitslag invoeren
<button mat-button (click)="editResult(match, event, group, round)">
<mat-icon>leaderboard</mat-icon>
Uitslag
</button>
<button mat-button (click)="changeCounter(match)">
<mat-icon>person</mat-icon>
Teller
</button>
<button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon>
Wedstrijd stoppen
Stoppen
</button>
} @else if (match.status == 'FINISHED') {
<div class="row result align-items-center">
@@ -304,8 +362,8 @@
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #finishedMatchMenu="matMenu">
<button mat-menu-item (click)="editResult(match, group, round)">
<mat-icon>edit</mat-icon>
<button mat-menu-item (click)="editResult(match, event, group, round)">
<mat-icon>leaderboard</mat-icon>
Uitslag bewerken
</button>
</mat-menu>
@@ -322,13 +380,25 @@
</tbody>
</table>
} @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>
@for (match of round.matches; track match.id) {
<tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td>
<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-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">
<div class="row result align-items-center">
@for (game of match.games; track game.id) {
@@ -350,51 +420,79 @@
</table>
}
<h6 class="mt-3">Stand</h6>
@if (!round.isFinalsRound) {
<h6 class="mt-3">Stand</h6>
<table class="table w-75 m-4">
<caption>Tussen haakjes de gemiddelden per gespeelde wedstrijd</caption>
<thead>
<tr>
<th>#</th>
<th>
@if (event.doublesEvent) {
Team
} @else {
Speler
<table class="table w-75 m-4">
<caption>Tussen haakjes de gemiddelden per gespeelde wedstrijd</caption>
<thead>
<tr>
<th>#</th>
<th>
@if (event.doublesEvent) {
Team
} @else {
Speler
}
</th>
<th>Gespeeld</th>
<th>Punten</th>
<th>Games</th>
<th>Wedstrijdpunten</th>
</tr>
</thead>
<tbody class="table-group-divider">
@for (entry of getStandingsForRound(round, group).entries; track entry.position) {
<tr>
<td class="align-middle">{{ entry.position }}</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">
@if (entry.played > 0 ) {
{{ entry.points }} ({{ entry.points / entry.played | number: '1.0-2' }})
}
</td>
<td class="align-middle">
@if (entry.played > 0 ) {
{{ entry.gamesWon }}-{{ entry.gamesLost}} ({{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }})
}
</td>
<td class="align-middle">
@if (entry.played > 0 ) {
{{ entry.pointsWon }}-{{ entry.pointsLost }} ({{ (entry.pointsWon - entry.pointsLost) / entry.played | number: '1.0-2' }})
}
</td>
</tr>
}
</th>
<th>Gespeeld</th>
<th>Punten</th>
<th>Games</th>
<th>Wedstrijdpunten</th>
</tr>
</thead>
<tbody class="table-group-divider">
@for (entry of getStandingsForRound(round, group).entries; track entry.position) {
</tbody>
</table>
} @else if (round.isFinalsRound && round.status == 'FINISHED') {
<h6 class="mt-3">Uitslag</h6>
<table class="table w-50 m-4">
<tbody>
<tr>
<td class="align-middle">{{ entry.position }}</td>
<td class="align-middle">{{ entry.team | teamText }}</td>
<td class="align-middle">{{ entry.played }}</td>
<td class="align-middle">
@if (entry.played > 0 ) {
{{ entry.points }} ({{ entry.points / entry.played | number: '1.0-2' }})
}
</td>
<td class="align-middle">
@if (entry.played > 0 ) {
{{ entry.gamesWon }}-{{ entry.gamesLost}} ({{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }})
}
</td>
<td class="align-middle">
@if (entry.played > 0 ) {
{{ entry.pointsWon }}-{{ entry.pointsLost }} ({{ (entry.pointsWon - entry.pointsLost) / entry.played | number: '1.0-2' }})
}
</td>
<td>1e Plaats</td>
<td>{{ (checkWinner(round.matches[0]) == 1 ? round.matches[0].team1 : round.matches[0].team2) | teamText }}</td>
</tr>
}
</tbody>
</table>
<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-group>
@@ -403,65 +501,13 @@
}
</mat-tab-group>
</mat-tab>
}
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>settings</mat-icon>
&nbsp;Beheer
</ng-template>
<mat-tab-group animationDuration="0ms" disableRipple="true">
<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>
@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>
<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>
<app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab>
}
</mat-tab-group>

View File

@@ -1,6 +1,7 @@
td {
vertical-align: middle;
}
td, th {
background-color: transparent !important;
}
@@ -8,6 +9,7 @@ td, th {
table.wide td, table.wide th {
height: 4em;
}
.winner {
color: green;
font-weight: bold;
@@ -24,3 +26,19 @@ td.w-sep {
td.w-fill {
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, OnDestroy, OnInit} from '@angular/core';
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelActionRow,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {CurrencyPipe, DatePipe, DecimalPipe, NgClass} from "@angular/common";
import {DatePipe, DecimalPipe, NgClass} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
@@ -16,7 +17,7 @@ import {MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {Group} from "../../model/group";
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 {FormsModule} from "@angular/forms";
import {MatTab, MatTabChangeEvent, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
@@ -25,55 +26,61 @@ import {MatDialog} from "@angular/material/dialog";
import {MatchResultPipe} from "../../pipes/match-result-pipe";
import {Event} from "../../model/event";
import {TournamentValidateComponent} from "../tournament-validate/tournament-validate.component";
import {Strength} from "../../model/player";
import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle";
import {Player, Strength} from "../../model/player";
import {MatSnackBar} from "@angular/material/snack-bar";
import {CourtSelectionComponent} from "../court-selection/court-selection.component";
import {Standings} from "../../model/standings";
import {Title} from '@angular/platform-browser';
import {HeaderService} from "../../service/header.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({
selector: 'app-tournament-manage',
imports: [
FullNamePipe,
MatAccordion,
MatCard,
MatCardContent,
MatCardHeader,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
TeamPipe,
MatIcon,
NgClass,
MatMenu,
MatMenuItem,
MatMenuTrigger,
FormsModule,
DatePipe,
MatTabGroup,
MatTab,
MatTabLabel,
MatButton,
MatIconButton,
DecimalPipe,
TournamentValidateComponent,
MatSlideToggle,
CurrencyPipe,
MatMenuContent
],
providers: [
FullNamePipe,
TeamPipe,
MatchResultPipe
],
templateUrl: './tournament-manage.component.html',
styleUrl: './tournament-manage.component.scss'
selector: 'app-tournament-manage',
imports: [
FullNamePipe,
MatAccordion,
MatCard,
MatCardContent,
MatCardHeader,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
TeamPipe,
MatIcon,
NgClass,
MatMenu,
MatMenuItem,
MatMenuTrigger,
FormsModule,
DatePipe,
MatTabGroup,
MatTab,
MatTabLabel,
MatButton,
MatIconButton,
DecimalPipe,
TournamentValidateComponent,
TournamentPlayersComponent,
MatExpansionPanelActionRow,
TeamDisplayComponent,
MatTooltip,
],
providers: [
FullNamePipe,
TeamPipe,
MatchResultPipe
],
templateUrl: './tournament-manage.component.html',
standalone: true,
styleUrl: './tournament-manage.component.scss'
})
export class TournamentManageComponent implements OnInit, OnDestroy {
@Input() tournament: Tournament;
tournament: Tournament;
activeRoundTab: number = 0;
@@ -93,16 +100,26 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
this.activeRoundTab = params['tab'];
}
})
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
this.headerService.setTitle(this.tournament.name);
});
this.loadTournament(
this.tournamentService.getById(Number(id)),
(tournament) => this.headerService.setTitle(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) {
const index = event.index;
this.router.navigate(
@@ -112,7 +129,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
});
}
getRoundIcon(status: String) {
getRoundIcon(status: string) {
if (status == "FINISHED") {
return "check";
} else if (status == "IN_PROGRESS") {
@@ -137,87 +154,120 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
}
startRound(round: Round) {
this.tournamentService.startRound(this.tournament.id, round.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.startRound(this.tournament.id, round.id));
}
finishRound(round: Round) {
this.tournamentService.finishRound(this.tournament.id, round.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.finishRound(this.tournament.id, round.id));
}
finishGroup(group: Group) {
this.tournamentService.finishGroup(this.tournament.id, group.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.finishGroup(this.tournament.id, group.id));
}
reopenGroup(group: Group) {
this.tournamentService.reopenGroup(this.tournament.id, group.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.reopenGroup(this.tournament.id, group.id));
}
divideTournament() {
this.tournamentService.divide(this.tournament.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.divide(this.tournament.id));
}
clearDivision() {
this.tournamentService.clearDivision(this.tournament.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.clearDivision(this.tournament.id));
}
drawTournament() {
this.tournamentService.draw(this.tournament.id).subscribe(data => {
this.tournament = data;
});
this.loadTournament(this.tournamentService.draw(this.tournament.id));
}
startMatch(match: Match) {
const availableCourts = this.getAvailableCourts();
if (availableCourts.length == 0) {
alert('Geen banen beschikbaar!');
this._snackBar.open('Geen banen beschikbaar.')
} 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 {
this.courtSelectionDialog.open(CourtSelectionComponent, {
data: {
match: match,
availableCourts: this.getAvailableCourts(),
totalCourts: this.tournament.courts
totalCourts: this.tournament.courts,
availableCounters: this.getAvailableCounters(match)
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.tournamentService.startMatch(this.tournament.id, match.id, result).subscribe(data => {
this.tournament = data;
})
this.loadTournament(this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId));
}
});
}
}
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[] = [];
matchPlayers.push(match.team1.player1.id);
if (match.team1.player2) matchPlayers.push(match.team1.player2.id);
matchPlayers.push(match.team2.player1.id);
if (match.team2.player2) matchPlayers.push(match.team2.player2.id);
let playersThatArePlaying = activePlayers.filter(Set.prototype.has, new Set(matchPlayers));
return playersThatArePlaying.length > 0;
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 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[] {
@@ -230,28 +280,42 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
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) {
this.tournamentService.stopMatch(this.tournament.id, match.id).subscribe(data => {
this.tournament = data;
})
this.loadTournament(this.tournamentService.stopMatch(this.tournament.id, match.id));
}
newRound(group: Group) {
this.tournamentService.newRound(this.tournament.id, group.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.');
});
this.loadTournament(this.tournamentService.newRound(this.tournament.id, group.id));
}
getStrength(strength: string | undefined) {
@@ -277,7 +341,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
for (const round of group.rounds) {
for (const match of round.matches) {
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, OnDestroy {
matchResultDialog = 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, {
data: {match: match, group: group, round: round},
data: {match: match, tournament: this.tournament, event: event, group: group, round: round},
minWidth: '800px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.tournamentService.saveResult(this.tournament.id, result.matchId, result).subscribe(data => {
this.tournament = data;
})
this.loadTournament(this.tournamentService.saveResult(this.tournament.id, result.matchId, result));
}
});
}
@@ -367,16 +430,97 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
}
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 {
constructor(match: Match, round: Round, group: Group) {
constructor(match: Match, round: Round, group: Group, event: Event) {
this.match = match;
this.round = round;
this.group = group;
this.event = event;
}
match: Match;
round: Round;
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 {
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

@@ -7,15 +7,16 @@ import {Event} from "../../model/event";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-registrations',
imports: [
MatCard,
MatCardHeader,
MatCardContent,
FullNamePipe
],
templateUrl: './tournament-registrations.component.html',
styleUrl: './tournament-registrations.component.scss'
selector: 'app-tournament-registrations',
imports: [
MatCard,
MatCardHeader,
MatCardContent,
FullNamePipe
],
templateUrl: './tournament-registrations.component.html',
standalone: true,
styleUrl: './tournament-registrations.component.scss'
})
export class TournamentRegistrationsComponent implements OnInit {

View File

@@ -1,7 +1,7 @@
@if (tournamentValidation && tournament) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header>
<h6>Toernooi</h6>
<h6>Toernooi ({{ getTournamentMatchCount(tournament)}} wedstrijden)</h6>
</mat-card-header>
<mat-card-content>
<mat-expansion-panel [disabled]="tournamentValidation.validations.length == 0">
@@ -24,7 +24,7 @@
@for (event of tournament.events; track event.id) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header>
<h6>{{ TournamentEvent.getType(event.type) }}</h6>
<h6>{{ TournamentEvent.getType(event.type) }} ({{ getEventMatchCount(event)}} wedstrijden)</h6>
</mat-card-header>
<mat-card-content>
<mat-accordion multi="true">

View File

@@ -18,21 +18,22 @@ import {PlayerService} from "../../service/player.service";
import {PlayerLinkComponent} from "../player-link/player-link.component";
@Component({
selector: 'app-tournament-validate',
imports: [
MatCard,
MatCardHeader,
MatCardContent,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
MatAccordion,
MatIcon,
FullNamePipe,
PlayerLinkComponent
],
templateUrl: './tournament-validate.component.html',
styleUrl: './tournament-validate.component.scss'
selector: 'app-tournament-validate',
imports: [
MatCard,
MatCardHeader,
MatCardContent,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
MatAccordion,
MatIcon,
FullNamePipe,
PlayerLinkComponent
],
templateUrl: './tournament-validate.component.html',
standalone: true,
styleUrl: './tournament-validate.component.scss'
})
export class TournamentValidateComponent implements OnInit {
tournament: Tournament;
@@ -98,4 +99,24 @@ export class TournamentValidateComponent implements OnInit {
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,5 +1,6 @@
import { Team } from "./team";
import {Game} from "./game";
import {Player} from "./player";
export class Match {
id: number;
@@ -11,4 +12,7 @@ export class Match {
endTime: Date;
games: Game[];
court: number;
counter: Player;
canStart?: boolean;
cantStartReason?: string;
}

View File

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

View File

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

View File

@@ -1,8 +1,18 @@
export class TournamentPlayer {
id: number;
playerId: number;
name: string;
events: string[];
paid: 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;
date: string;
status: string;
active: boolean;
events: EventRegistration[];
static getStatus(tournamentRegistration: TournamentRegistration): string {

View File

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

View File

@@ -1,4 +1,3 @@
// header.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

View File

@@ -5,6 +5,7 @@ import {Tournament} from "../model/tournament";
import {TournamentValidation} from "../model/tournamentValidation";
import {Result} from "../model/result";
import { environment } from "../../environments/environment"
import {TournamentPlayerSubstitution} from "../model/tournamentPlayer";
@Injectable({
providedIn: 'root'
@@ -72,8 +73,8 @@ export class TournamentService {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/groups/${groupId}/new`, null);
}
public startMatch(tournamentId: number, matchId: number, court: number): Observable<Tournament> {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/start/${court}`, null);
public startMatch(tournamentId: number, matchId: number, court: number, counter: number): Observable<Tournament> {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/start?court=${court}&counter=${counter}`, null);
}
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);
}
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> {
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 { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
export default bootstrap;

View File

@@ -2,3 +2,11 @@
html, body { height: 100%; }
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