Move to new server

This commit is contained in:
2024-10-12 13:38:41 +02:00
commit e2e855e9b9
113 changed files with 11268 additions and 0 deletions

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules*
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM node:20.13.1-alpine3.19 as build
WORKDIR /app/src
COPY package*.json ./
RUN apk update
RUN apk add yarn
RUN yarn install --frozen-lockfile --no-progress
COPY . ./
RUN yarn build
FROM node:20.13.1-alpine3.19
RUN addgroup -S k8s-group && adduser -S k8s-user -G k8s-group
USER root
WORKDIR /usr/app
COPY --from=build /app/src/dist/swiss-client/ ./
CMD node server/server.mjs
EXPOSE 4000

58
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,58 @@
// This Jenkinsfile defines a declarative pipeline
pipeline {
tools {
nodejs 'node'
}
options {
disableConcurrentBuilds()
}
agent any
// Defines the sequence of stages that will be executed
stages {
// This stage checks out the source code from the SCM (Source Code Management) system
stage('Checkout') {
steps {
// This command checks out the source code from the SCM into the Jenkins workspace
checkout scm
}
}
stage('Build') {
steps {
sh 'yarn install --no-progress'
sh 'npm run ng build --prod'
}
}
stage('Docker Build') {
steps {
sh 'docker build -t upquark/swiss-client:latest -t upquark/swiss-client:${BUILD_NUMBER} .'
}
}
stage('Push the Docker Image to DockerHUb') {
steps {
script {
withCredentials([string(credentialsId: 'c7783e4f-2f79-482f-885f-dfb39f8c02d3', variable: 'docker_hub')]) {
sh 'docker login -u upquark -p ${docker_hub}'
}
sh 'docker push upquark/swiss-client:latest'
sh 'docker push upquark/swiss-client:${BUILD_NUMBER}'
}
}
}
stage('Deploy to Kubernetes') {
steps {
script {
kubeconfig(credentialsId: 'k3s-kubeconfig') {
sh 'cat k8s/deployment.yaml | sed "s/latest/$BUILD_NUMBER/g" | kubectl apply -n swiss -f -'
sh 'kubectl apply -f k8s/service.yaml -n swiss'
}
}
}
}
}
}

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# SwissClient
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.5.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

48
TODO.txt Normal file
View File

@@ -0,0 +1,48 @@
Knop Nieuw Toernooi naar Toernooienlijst
Geen Flat buttons gebruiken
Knop Nieuwe Speler naar Spelerslijst
Tabellen stijl gelijktrekken
Uitslag verbeteren
Gebruik table-hover
Ronde afsluiten indien wedstrijden klaar
Wedstrijd stoppen
Alle toernooi-initialisatie-acties onderbrengen in manage
Loting nieuwe ronde
Baanbeheer
Wedstrijdbriefjes
Bij alle lotingen zorgen dat speler maar 1 keer uitgeloot wordt
Rondemenu verbergen bij geen opties
Animatie uit bij klik op tab
Badges in tabs voor aantal actieve wedstrijden
Loting los onderdeel wissen
Menu per wedstrijd
Alles HTTP POST
Gemiddelden tussen haakjes in stand
Niet-deelnemers per ronde bijhouden
Rondestatus DRAWN nodig?
Icoontjes langslopen
Badge bij actieve wedstrijden voor aantal
Max en min voor invoer uitslag?
Spelerslijst
Presentiemelding
Spelerslijst uitbreiden met onderdelen en kosten
TournamentPlayers testen met nieuw toernooi
Printbare overzichten
Stand per ronde
Player-registrations: oude toernooien verbergen via knop
Onderdeel afsluiten
Wedstrijd opgave
Blessures
Titels pagina's
Authenticatie
Progress indicator tijdens communicatie backend
https://blog.shhdharmen.me/browser-storage-in-angular-ssr
Won't do / later:
Date-picker
DTO's short/full
team1 + team2 => team[]
Validaties in controllers op juiste status
Voorkom benodigde dubbelklik op knoppen bij switchen tab

119
angular.json Normal file
View File

@@ -0,0 +1,119 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"swiss-client": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/swiss-client",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js",
"@angular/localize/init"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2MB",
"maximumError": "3MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "swiss-client:build:production"
},
"development": {
"buildTarget": "swiss-client:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing",
"@angular/localize/init"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "95cdfcca-c4c3-4279-80f4-6ccad3383c37"
}
}

1
ideas.txt Normal file
View File

@@ -0,0 +1 @@
Gebruik carroussel voor toernooi-initialisatie

20
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: swiss-client
spec:
replicas: 1
selector:
matchLabels:
app: swiss-client
template:
metadata:
labels:
app: swiss-client
spec:
containers:
- name: swiss-client
image: upquark/swiss-client:latest
imagePullPolicy: Always
ports:
- containerPort: 4000

12
k8s/service.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: swiss-client
spec:
type: NodePort
selector:
app: swiss-client
ports:
- protocol: TCP
port: 4000
nodePort: 30042

55
package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "swiss-client",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:swiss-client": "node dist/swiss-client/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.5",
"@angular/cdk": "^18.2.5",
"@angular/common": "^18.2.5",
"@angular/compiler": "^18.2.5",
"@angular/core": "^18.2.5",
"@angular/forms": "^18.2.5",
"@angular/material": "^18.2.5",
"@angular/material-moment-adapter": "^18.2.5",
"@angular/platform-browser": "^18.2.5",
"@angular/platform-browser-dynamic": "^18.2.5",
"@angular/platform-server": "^18.2.5",
"@angular/router": "^18.2.5",
"@angular/ssr": "^18.2.5",
"@ng-bootstrap/ng-bootstrap": "^17.0.0",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"express": "^4.18.2",
"moment": "2.18.1",
"ngx-cookie-service-ssr": "^18.0.0",
"ngx-mask": "^18.0.0",
"rxjs": "~7.8.0",
"ts-enums": "^0.0.6",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.5",
"@angular/cli": "^18.2.5",
"@angular/compiler-cli": "^18.2.5",
"@angular/localize": "^18.2.5",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/shuttle.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

61
server.ts Normal file
View File

@@ -0,0 +1,61 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('**', express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html',
}));
// All regular routes use the Angular engine
server.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
{ provide: 'REQUEST', useValue: req },
{ provide: 'RESPONSE', useValue: res }
],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();

View File

@@ -0,0 +1,46 @@
<div class="container">
<div class="row toolbar">
<mat-toolbar>
<img ngSrc="/shuttle.jpg" width="50" height="50" priority alt=""/>
<!--
<button mat-mini-fab class="menu-button">
<mat-icon class="menu-button-icon" style="color: white;">menu</mat-icon>
</button>
-->
<h5 class="m-3">{{ title }}</h5>
<span class="spacer"></span>
<a routerLink="/tournaments" mat-button>
<mat-icon>list</mat-icon>
Toernooien
</a>
<a routerLink="/players" mat-button>
<mat-icon>group</mat-icon>
Spelers
</a>
@if (this.userService.isLoggedIn()) {
<button mat-flat-button [matMenuTriggerFor]="accountMenu">
<mat-icon>person</mat-icon>
{{ user }}
</button>
<mat-menu #accountMenu="matMenu">
<button mat-menu-item (click)="logOut()">
<mat-icon>exit_to_app</mat-icon>
Uitloggen
</button>
<button mat-menu-item (click)="addTestData()">
<mat-icon>science</mat-icon>
Testtoernooi toevoegen
</button>
</mat-menu>
} @else {
<button mat-flat-button disabled>
<mat-icon>person</mat-icon>
Niet ingelogd
</button>
}
</mat-toolbar>
</div>
<div class="row mt-3">
<router-outlet></router-outlet>
</div>
</div>

View File

@@ -0,0 +1,30 @@
a {
margin-right: 1em;
}
@media print {
div.toolbar {
display: none;
}
div.container {
margin-left: 0;
margin-right: 0;
max-width: 100%;
}
}
.spacer {
flex: 1 1 auto;
}
.menu-button {
background-color: rgb(0, 92, 187);
}
.menu-button-icon {
color: white;
}
mat-toolbar {
background-color: transparent;
}

56
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,56 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {CommonModule, NgOptimizedImage} from "@angular/common";
import {MatAnchor, MatButton, MatIconButton, MatMiniFabButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatToolbar} from "@angular/material/toolbar";
import {TitleService} from "./service/title.service";
import {Subscription} from "rxjs";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {UserService} from "./authentication/user.service";
import {TournamentService} from "./service/tournament.service";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule, RouterLink, RouterLinkActive, MatAnchor, MatIcon, MatButton, MatToolbar, NgOptimizedImage, MatIconButton, MatMiniFabButton, MatMenuTrigger, MatMenu, MatMenuItem],
providers: [TitleService],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {
title: string;
titleSubscription: Subscription;
user: string;
userSubscription: Subscription;
constructor(
protected userService: UserService,
private router: Router,
protected activatedRoute: ActivatedRoute,
private titleService: TitleService,
private tournamentService: TournamentService
) {
}
ngOnInit() {
this.titleSubscription = this.titleService.currentTitle.subscribe(newTitle => this.title = newTitle);
this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
}
ngOnDestroy(): void {
this.titleSubscription.unsubscribe();
}
logOut() {
this.userService.removeUser();
this.router.navigate(['/auth/login']);
}
addTestData() {
this.tournamentService.addTestData().subscribe(data => {
this.router.navigate(['/tournaments']);
})
}
}

View File

@@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

29
src/app/app.config.ts Normal file
View File

@@ -0,0 +1,29 @@
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';
import {provideClientHydration} from '@angular/platform-browser';
import {HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptorsFromDi} from "@angular/common/http";
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";
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({eventCoalescing: true}),
provideRouter(routes),
provideClientHydration(),
provideHttpClient(withFetch(), withInterceptorsFromDi()),
provideAnimationsAsync(),
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 },
provideNgxMask(),
]
};

35
src/app/app.routes.ts Normal file
View File

@@ -0,0 +1,35 @@
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";
export const routes: Routes = [
{ path: '', component: TournamentListComponent, canActivate: [AuthGuard], title: 'Toernooien' },
{ path: 'tournaments', component: TournamentListComponent, canActivate: [AuthGuard], title: 'Toernooien' },
{ path: 'tournaments/add', component: TournamentEditComponent, canActivate: [AuthGuard], title: 'Nieuw Toernooi' },
{ path: 'tournaments/:id/edit', component: TournamentEditComponent, canActivate: [AuthGuard], title: 'Bewerk Toernooi' },
{ path: 'tournaments/:id/registrations', component: TournamentRegistrationsComponent, canActivate: [AuthGuard], title: 'Inschrijvingen' },
{ path: 'tournaments/:id/validate', component: TournamentValidateComponent, canActivate: [AuthGuard], title: 'Toernooi' },
{ path: 'tournaments/:id/divide', component: TournamentDivideComponent, canActivate: [AuthGuard], title: 'Toernooi valideren' },
{ path: 'tournaments/:id/draw', component: TournamentDrawComponent, canActivate: [AuthGuard], title: 'Toernooi loten' },
{ path: 'tournaments/:id/manage', component: TournamentManageComponent, canActivate: [AuthGuard] },
{ path: 'tournaments/:id/manage/:tab', component: TournamentManageComponent, canActivate: [AuthGuard], title: 'Toernooien' },
{ path: 'players', component: PlayerListComponent, canActivate: [AuthGuard], title: 'Spelers' },
{ path: 'players/add', component: PlayerEditComponent, canActivate: [AuthGuard], title: 'Nieuwe Speler' },
{ path: 'players/edit/:id', component: PlayerEditComponent, canActivate: [AuthGuard], title: 'Bewerk Speler' },
{ path: 'players/:id/registrations', component: PlayerRegistrationsComponent, canActivate: [AuthGuard], title: 'Inschrijvingen' },
{ path: 'tournaments/:id/rounds/:roundId/matchsheets', component: MatchSheetsComponent, canActivate: [AuthGuard], title: 'Wedstrijdbriefjes' },
{ path: 'tournaments/:id/rounds/:roundId/overview', component: RoundOverviewComponent, canActivate: [AuthGuard], title: 'Rondeoverzicht' },
{ path: 'auth/login', component: LoginComponent }
];

View File

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,14 @@
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

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

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,54 @@
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

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

View File

@@ -0,0 +1,12 @@
<h2 mat-dialog-title>Kies een baan:</h2>
<mat-dialog-content>
<button type="button" class="btn {{ data.availableCourts.indexOf(i + 1) < 0 ? 'btn-secondary' : 'btn-primary' }} btn-lg m-3"
*ngFor="let item of [].constructor(data.totalCourts); let i = index"
[disabled]="data.availableCourts.indexOf(i + 1) < 0" [mat-dialog-close]="i + 1">
{{ i + 1 }}
</button>
<br>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onAnnulerenClick()">Annuleren</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,42 @@
import {Component, inject, Inject} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent, MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {Match} from "../../model/match";
import {NgForOf} from "@angular/common";
import {MatButton} from "@angular/material/button";
@Component({
selector: 'app-court-selection',
standalone: true,
imports: [
MatDialogTitle,
MatDialogContent,
NgForOf,
MatButton,
MatDialogClose,
MatDialogActions
],
templateUrl: './court-selection.component.html',
styleUrl: './court-selection.component.scss'
})
export class CourtSelectionComponent {
court: number;
readonly dialogRef = inject(MatDialogRef<CourtSelectionComponent>);
constructor(@Inject(MAT_DIALOG_DATA) public data: {
match: Match,
availableCourts: number[],
totalCourts: number
}) {}
onAnnulerenClick() {
this.dialogRef.close();
}
}

View File

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

View File

@@ -0,0 +1,87 @@
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} 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 {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 {TitleService} from "../../service/title.service";
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
standalone: true,
imports: [
MatCardTitle,
MatCardContent,
ReactiveFormsModule,
MatFormField,
MatButton,
RouterLink,
MatInput,
NgIf,
MatLabel,
MatError,
MatCard,
MatCardHeader
],
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,
private titleService: TitleService,
) {
this.initializeForm();
}
private initializeForm() {
this.form = this.fb.group({
username: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
ngOnInit() {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.titleService.setTitle("Inloggen");
}
login() {
if (this.form.invalid) {
return;
}
let loginCredentials = new LoginCredentials();
loginCredentials = {
...loginCredentials,
...this.form.value,
};
this.authenticationService
.login(loginCredentials)
.subscribe(
{
next: (user: User) => {
this.userPersistenceService.setUser(user);
this.router.navigate([this.returnUrl]);
}
}
);
}
}

View File

@@ -0,0 +1,82 @@
<h2 mat-dialog-title>Uitslag invoeren</h2>
<mat-dialog-content>
<mat-grid-list cols="5" rowHeight="12:7">
<mat-grid-tile colspan="2">
<div class="w-100" style="font-size: 16px;">
<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>
<mat-grid-tile colspan="2">
<div class="w-100" [ngClass]="{'winner': validateResult() == 1}">
{{ data.match.team1 | teamText }}
</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>
<mat-grid-tile colspan="2">
<div class="w-100" [ngClass]="{'winner': validateResult() == -1}">
{{ data.match.team2 | teamText }}
</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>
</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>
</mat-dialog-actions>

View File

@@ -0,0 +1,12 @@
mat-grid-tile {
font-size: 20px;
}
mat-form-field {
width: 80px !important;
}
.winner {
color: green;
font-weight: bold;
}

View File

@@ -0,0 +1,147 @@
import {Component, inject, Inject} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent, MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {MatButton, MatIconButton} from "@angular/material/button";
import {DatePipe, NgClass, NgForOf} from "@angular/common";
import {MatIcon} from "@angular/material/icon";
import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {Match} from "../../model/match";
import {MatFormField, MatInput} from "@angular/material/input";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {Result} from "../../model/result";
import {MatGridList, MatGridTile, MatGridTileText} from "@angular/material/grid-list";
import {Round} from "../../model/round";
import {Group} from "../../model/group";
import {Game} from "../../model/game";
@Component({
selector: 'app-match-result',
standalone: true,
imports: [
MatDialogContent,
MatDialogActions,
MatButton,
MatDialogClose,
MatDialogTitle,
DatePipe,
MatIcon,
NgForOf,
TeamPipe,
MatInput,
ReactiveFormsModule,
FormsModule,
MatFormField,
MatGridList,
MatGridTile,
MatGridTileText,
MatIconButton,
NgClass
],
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './match-result.component.html',
styleUrl: './match-result.component.scss'
})
export class MatchResultComponent {
result: Result = new Result();
constructor(@Inject(MAT_DIALOG_DATA) public data: {match: Match, group: Group, round: Round}) {
this.result.matchId = this.data.match.id;
if (data.match.games.length == 0) {
this.result.games.push(new Game());
this.result.games.push(new Game());
this.result.games.push(new Game());
} else {
for (let game of data.match.games) {
this.result.games.push(game);
}
if (data.match.games.length == 2) {
this.result.games.push(new Game());
}
}
}
readonly dialogRef = inject(MatDialogRef<MatchResultComponent>);
set21(team: number, game: number) {
if (team == 1) {
this.result.games[game - 1].score1 = 21;
}
if (team == 2) {
this.result.games[game - 1].score2 = 21;
}
}
onAnnulerenClick() {
this.dialogRef.close();
}
validateResult(): number {
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);
if (this.result.games[2].score1 != undefined && this.result.games[2].score2 != undefined) {
valid &&= this.gameValid(this.result.games[2].score1, this.result.games[2].score2);
}
return valid ? this.matchResult(this.result) : 0;
}
gameValid(score1: number, score2: number): boolean {
if (score1 == undefined) return false;
if (score2 == undefined) return false;
if (score1 < 0 || score1 > 30) return false;
if (score2 < 0 || score2 > 30) return false;
if (score1 == 21 && score2 <= 19) return true;
if (score1 <= 19 && score2 == 21) return true;
if (score1 == 30 && score2 == 29) return true;
if (score1 == 29 && score2 == 30) return true;
if (score1 >= 22 && score1 <= 30 && (score1 - score2) == 2) return true;
if (score2 >= 22 && score2 <= 30 && (score2 - score1) == 2) return true;
return false;
}
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++;
}
if (Math.abs(gameBalance) == 2 && (result.games[2].score1 != undefined || result.games[2].score2 != undefined)) {
return 0;
}
if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) {
if (result.games[2].score1 < result.games[2].score2) {
gameBalance--;
} else {
gameBalance++;
}
}
return Math.sign(gameBalance);
}
}

View File

@@ -0,0 +1,63 @@
@if (round) {
<ng-container *ngFor="let match of round.matches">
<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>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-6">
<b>{{ match.team2 | teamText }}</b>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
<div class="col-2">
<mat-form-field appearance="outline">
<input matInput>
</mat-form-field>
</div>
</div>
<div class="row">
<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>
</div>
<br>
</ng-container>
}

View File

@@ -0,0 +1,9 @@
mat-form-field {
width: 80px !important;
}
@media print {
div.nobreak {
page-break-inside: avoid;
}
}

View File

@@ -0,0 +1,71 @@
import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {Tournament} from "../../model/tournament";
import {Round} from "../../model/round";
import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {Group} from "../../model/group";
import {NgForOf} from "@angular/common";
import {MatFormField} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-match-sheets',
standalone: true,
imports: [
MatCard,
MatCardHeader,
MatCardContent,
TeamPipe,
NgForOf,
MatFormField,
MatInput,
ReactiveFormsModule
],
providers: [
TeamPipe,
FullNamePipe
],
templateUrl: './match-sheets.component.html',
styleUrl: './match-sheets.component.scss'
})
export class MatchSheetsComponent implements OnInit {
tournament: Tournament;
group: Group;
round: Round;
constructor(
private tournamentService: TournamentService,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService
) {
}
ngOnInit() {
const tournamentId = this.route.snapshot.paramMap.get('id');
let roundId = Number(this.route.snapshot.paramMap.get('roundId'));
this.tournamentService.getById(Number(tournamentId)).subscribe(data => {
this.tournament = data;
for (let event of this.tournament.events) {
for (let group of event.groups) {
for (let round of group.rounds) {
if (round.id == roundId) {
this.group = group;
this.round = round;
this.titleService.setTitle(`Wedstrijdbriefjes ${this.group.name} ${this.round.name}`);
return;
}
}
}
}
});
}
}

View File

@@ -0,0 +1,82 @@
<form (ngSubmit)="savePlayer()">
<mat-card appearance="outlined">
<mat-card-content>
<div class="row">
<div class="col-md-5">
<mat-form-field appearance="fill">
<mat-label>Achternaam</mat-label>
<input matInput [(ngModel)]="player.lastName" name="lastName" required>
</mat-form-field>
</div>
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Tussenvoegsel</mat-label>
<input matInput [(ngModel)]="player.middleName" name="middleName">
</mat-form-field>
</div>
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Voornaam</mat-label>
<input matInput [(ngModel)]="player.firstName" name="firstName" required>
</mat-form-field>
</div>
<div class="col-md-3">
<mat-radio-group [(ngModel)]="player.sex" name="sex" required>
<mat-label>Geslacht</mat-label>
<mat-radio-button value="M">M</mat-radio-button>
<mat-radio-button value="V">V</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="row">
<div class="col-md-3">
<mat-form-field appearance="fill">
<mat-label>Telefoon</mat-label>
<input matInput [(ngModel)]="player.phoneNumber" name="phoneNumber" required>
</mat-form-field>
</div>
<div class="col-md-9">
<mat-form-field appearance="fill">
<mat-label>Emailadres</mat-label>
<input matInput [(ngModel)]="player.email" name="email" required>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-3">
<mat-form-field appearance="fill">
<mat-label>Geboortedatum</mat-label>
<input matInput mask="00-00-0000" [showMaskTyped]="true" [dropSpecialCharacters]="false" [(ngModel)]="player.birthday" type="text" name="birthday" required>
<mat-hint>dd-mm-jjjj</mat-hint>
</mat-form-field>
</div>
<div class="col-md-6">
<mat-form-field appearance="fill">
<mat-label>Club</mat-label>
<input matInput [(ngModel)]="player.club" name="club">
</mat-form-field>
</div>
<div class="col-md-3">
<mat-form-field appearance="fill">
<mat-label>Speelsterkte</mat-label>
<mat-select [(ngModel)]="player.strength" name="strength" required>
<mat-option *ngFor="let strengthOption of Strength | keyvalue" [value]="strengthOption.key">
{{ strengthOption.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button>
<mat-icon>save</mat-icon>
{{ isEditMode ? 'Bijwerken' : 'Opslaan' }}
</button>
<a mat-button routerLink="/players">
<mat-icon>cancel</mat-icon>
Annuleren
</a>
</mat-card-actions>
</mat-card>
</form>

View File

@@ -0,0 +1,6 @@
mat-form-field {
width: 100%;
}
a, button, mat-radio-button {
margin-right: 1em;
}

View File

@@ -0,0 +1,95 @@
import {Component, OnInit} from '@angular/core';
import {Player, Strength} from "../../model/player";
import {PlayerService} from "../../service/player.service";
import {ActivatedRoute, Router, RouterLink} from "@angular/router";
import {FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatIcon} from "@angular/material/icon";
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from "@angular/material/datepicker";
import {MatOption, MatSelect} from "@angular/material/select";
import {KeyValuePipe, NgForOf} from "@angular/common";
import {MatAnchor, MatButton} from "@angular/material/button";
import {TitleService} from "../../service/title.service";
import {MatSnackBar} from "@angular/material/snack-bar";
import {NgxMaskDirective, NgxMaskPipe} from "ngx-mask";
@Component({
selector: 'app-player-edit',
standalone: true,
imports: [
FormsModule,
RouterLink,
MatFormField,
MatInput,
MatIcon,
MatHint,
MatLabel,
MatRadioGroup,
MatRadioButton,
MatCard,
MatCardHeader,
MatCardContent,
MatCardActions,
MatDatepickerInput,
MatDatepickerToggle,
MatDatepicker,
MatSelect,
MatOption,
KeyValuePipe,
NgForOf,
MatButton,
MatAnchor,
ReactiveFormsModule,
NgxMaskDirective,
NgxMaskPipe
],
templateUrl: './player-edit.component.html',
styleUrl: './player-edit.component.scss'
})
export class PlayerEditComponent implements OnInit {
player: Player;
isEditMode: boolean = false;
constructor(
private playerService: PlayerService,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService,
private _snackBar: MatSnackBar,
) {
this.player = new Player();
}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.titleService.setTitle("Bewerk Speler");
this.isEditMode = true;
this.playerService.getById(Number(id)).subscribe(data => {
this.player = data;
});
} else {
this.titleService.setTitle("Nieuwe Speler");
}
}
savePlayer() {
if (this.isEditMode) {
this.playerService.update(this.player.id, this.player).subscribe(() => {
this.router.navigate(['/players']);
});
} else {
this.playerService.save(this.player).subscribe({
next: () => this.router.navigate(['/players']),
error: () => this._snackBar.open('Niet alle velden zijn correct gevuld.')
});
}
}
protected readonly Object = Object;
protected readonly Strength = Strength;
}

View File

@@ -0,0 +1,42 @@
<mat-card appearance="outlined">
<!--
<mat-card-header>
<h5>Spelers</h5>
</mat-card-header>
-->
<mat-card-content>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Naam</th>
<th scope="col">M/V</th>
<th scope="col">Club</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let player of players">
<td class="align-middle">{{ player.id }}</td>
<td class="align-middle">{{ player | fullName }}</td>
<td class="align-middle">{{ player.sex }}</td>
<td class="align-middle">{{ player.club }}</td>
<td class="align-middle">
<a mat-button [routerLink]="['/players/edit', player.id]">
<mat-icon>edit</mat-icon>
Bewerk
</a>
<a mat-button [routerLink]="['/players', player.id, 'registrations']">
<mat-icon>app_registration</mat-icon>
Inschrijvingen
</a>
</td>
</tr>
</tbody>
</table>
<a mat-button routerLink="/players/add">
<mat-icon>person_add</mat-icon>
Nieuwe speler
</a>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,7 @@
a {
margin-right: 1em;
}
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,36 @@
import {Component, OnInit} from '@angular/core';
import {Player} from "../../model/player";
import {PlayerService} from "../../service/player.service";
import {NgFor} from "@angular/common";
import {RouterLink} from "@angular/router";
import {MatAnchor} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-player-list',
standalone: true,
imports: [NgFor, RouterLink, MatAnchor, MatIcon, MatCard, MatCardHeader, MatCardContent, FullNamePipe],
templateUrl: './player-list.component.html',
styleUrl: './player-list.component.scss'
})
export class PlayerListComponent implements OnInit {
players: Player[];
constructor(
private titleService: TitleService,
private playerService: PlayerService) {
}
ngOnInit() {
this.titleService.setTitle("Spelers");
this.playerService.getAll().subscribe(data => {
this.players = data;
});
}
protected readonly Player = Player;
}

View File

@@ -0,0 +1,50 @@
@if (player && tournamentRegistrations && allPlayers) {
<mat-card appearance="outlined">
<mat-card-content>
<mat-card *ngFor="let tournamentRegistration of getTournamentRegistrations()" appearance="outlined" class="mb-3">
<mat-card-header>
<h6>{{ tournamentRegistration.name }}</h6>
</mat-card-header>
<mat-card-content>
<ng-container *ngFor="let eventRegistration of tournamentRegistration.events">
<div class="row event-row">
<div class="col-md-2">
<mat-checkbox [disabled]="!tournamentRegistration.editable" [(ngModel)]="eventRegistration.registered" (change)="updateModelWhenEventChecked(eventRegistration, $event)" name="registered">
{{ EventRegistration.getType(eventRegistration.type) }}
</mat-checkbox>
</div>
<div class="col-md-4">
<ng-container *ngIf="eventRegistration.doublesEvent">
<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>
<mat-option *ngFor="let player of getRelevantPlayers(eventRegistration.type)" [value]="player.id">
{{ player | fullName }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
</div>
<div class="col-6"></div>
</div>
</ng-container>
</mat-card-content>
<mat-card-actions *ngIf="tournamentRegistration.editable">
<button mat-button (click)="saveRegistration(tournamentRegistration, $event)" [disabled]="waitingForBackend">
<mat-icon>save</mat-icon>
Opslaan
</button>
<a mat-button routerLink="/players">
<mat-icon>cancel</mat-icon>
Annuleren
</a>
</mat-card-actions>
</mat-card>
<button mat-button (click)="this.showAll = true" *ngIf="!this.showAll">
<mat-icon>search</mat-icon>
Toon oude toernooien
</button>
</mat-card-content>
</mat-card>
}

View File

@@ -0,0 +1,9 @@
.event-row {
height: 5em;
}
mat-form-field {
width: 100%;
}
a, button {
margin-right: 1em;
}

View File

@@ -0,0 +1,125 @@
import {Component, OnInit} from '@angular/core';
import {Player} from "../../model/player";
import {ActivatedRoute, Router, RouterLink} from "@angular/router";
import {PlayerService} from "../../service/player.service";
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {RegistrationService} from "../../service/registration.service";
import {KeyValuePipe, NgFor, NgIf} from "@angular/common";
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, MatFabButton} from "@angular/material/button";
import {MatSnackBar} from "@angular/material/snack-bar";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {Tournament} from "../../model/tournament";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-player-registrations',
standalone: true,
imports: [
MatCard,
MatCardContent,
MatCardHeader,
MatFormField,
MatInput,
MatLabel,
NgFor,
ReactiveFormsModule,
FormsModule,
MatCheckbox,
NgIf,
MatCardActions,
RouterLink,
KeyValuePipe,
MatOption,
MatSelect,
MatIcon,
MatFabButton,
MatButton,
MatAnchor,
FullNamePipe
],
providers: [
FullNamePipe
],
templateUrl: './player-registrations.component.html',
styleUrl: './player-registrations.component.scss'
})
export class PlayerRegistrationsComponent implements OnInit {
player: Player;
tournamentRegistrations: TournamentRegistration[];
allPlayers: Player[];
showAll: boolean = false;
waitingForBackend: boolean = false;
constructor(
private _snackBar: MatSnackBar,
private playerService: PlayerService,
private registrationService: RegistrationService,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService,
private fullNamePipe: FullNamePipe
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.playerService.getById(Number(id)).subscribe(data => {
this.player = data;
this.titleService.setTitle(`Inschrijvingen van ${this.fullNamePipe.transform(this.player)}`);
});
this.registrationService.getTournamentRegistrationsByPlayerId(Number(id)).subscribe(data => {
this.tournamentRegistrations = data;
})
this.playerService.getAll().subscribe(data => {
this.allPlayers = data;
});
}
saveRegistration(tournamentRegistration: TournamentRegistration, event: MouseEvent) {
this.waitingForBackend = true;
this.registrationService.saveTournamentRegistrations(tournamentRegistration, this.player.id).subscribe(data => {
this.waitingForBackend = false;
this._snackBar.open('Registratie opgeslagen.');
});
}
updateModelWhenEventChecked(eventRegistration: EventRegistration, event: MatCheckboxChange) {
if (event.checked) {
eventRegistration.player = this.player.id;
} else {
eventRegistration.player = undefined;
}
}
getRelevantPlayers(type: string): Player[] {
if ( (this.player.sex == 'M' && type == 'HD') ||
(this.player.sex == 'V' && type == 'GD')
) {
return this.allPlayers.filter(player => player.sex == 'M' && player.id != this.player.id);
} else {
return this.allPlayers.filter(player => player.sex == 'V' && player.id != this.player.id);
}
}
getTournamentRegistrations(): TournamentRegistration[] {
if (this.showAll) {
return this.tournamentRegistrations;
} else {
return this.tournamentRegistrations.filter(t => (t.status == 'UPCOMING' || t.status == 'ONGOING'));
}
}
protected readonly Player = Player;
protected readonly EventRegistration = EventRegistration;
protected readonly TournamentRegistration = TournamentRegistration;
}

View File

@@ -0,0 +1,92 @@
@if (round) {
<h6>{{ group.name }}, {{ round.name }}:</h6>
@if (round.status != 'FINISHED') {
<table class="table table-sm m-4 wide w-100">
<tbody>
<tr *ngFor="let match of round.matches">
<td class="align-middle" style="width: 45%;">{{ match.team1 | teamText }}</td>
<td class="align-middle w-sep">-</td>
<td class="align-middle" style="width: 45%;">{{ match.team2 | teamText }}</td>
<td class="align-middle w-sep"></td>
</tr>
@if (round.drawnOut) {
<tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
</tr>
}
</tbody>
</table>
} @else {
<table class="table table-sm m-4 wide w-100">
<tbody>
<tr *ngFor="let match of round.matches">
<td class="align-middle" style="width: 30%;">
@if (event.doublesEvent) {
{{ match.team1.player1 | fullName }} /<br>{{ match.team1.player2 | fullName }}
} @else {
{{ match.team1.player1 | fullName }}
}
<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 }}
}
</td>
<td class="align-middle" style="width: 35%;">
<div class="row result align-items-center">
<span *ngFor="let game of match.games" class="col-3">{{ game.score1 }}-{{ game.score2 }}</span>
@if (match.games.length == 2) {
<span class="col-3"></span>
}
</div>
</td>
</tr>
@if (round.drawnOut) {
<tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
</tr>
}
</tbody>
</table>
}
@if (round.status == 'FINISHED' || prevRound) {
@if (group.status == 'FINISHED') {
<h6>Eindstand:</h6>
} @else if (round.status == 'FINISHED') {
<h6>Stand na {{ round.name }}:</h6>
} @else {
<h6>Stand na {{ prevRound?.name }}:</h6>
}
<table class="table table-sm w-100 m-4">
<thead>
<tr>
<th>#</th>
<th>
@if (event.doublesEvent) {
Team
} @else {
Speler
}
</th>
<th>Gespeeld</th>
<th>Punten/W</th>
<th>Games/W</th>
<th>#/W</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr *ngFor="let entry of round.standings.entries">
<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">{{ entry.points / entry.played | number: '1.0-2' }}</td>
<td class="align-middle">{{ (entry.gamesWon - entry.gamesLost) / entry.played | number: '1.0-2' }}</td>
<td class="align-middle">{{ (entry.pointsWon - entry.pointsLost) / entry.played | number: '1.0-2' }}</td>
</tr>
</tbody>
</table>
}
}

View File

@@ -0,0 +1,11 @@
td.w-team {
width: 30%;
}
td.w-sep {
width: 5%;
}
td.w-fill {
width: 35%;
}

View File

@@ -0,0 +1,79 @@
import {Component, OnInit} from '@angular/core';
import {Tournament} from "../../model/tournament";
import {Event} from "../../model/event";
import {Group} from "../../model/group";
import {Round} from "../../model/round";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {DecimalPipe, NgForOf} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatMenu, MatMenuItem} from "@angular/material/menu";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-round-overview',
standalone: true,
imports: [
NgForOf,
TeamPipe,
DecimalPipe,
MatButton,
MatIcon,
MatIconButton,
MatMenu,
MatMenuItem,
FullNamePipe
],
providers: [
TeamPipe,
FullNamePipe
],
templateUrl: './round-overview.component.html',
styleUrl: './round-overview.component.scss'
})
export class RoundOverviewComponent implements OnInit {
tournament: Tournament;
event: Event;
group: Group;
round: Round;
prevRound: Round | undefined;
constructor(
private tournamentService: TournamentService,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService
) {
}
ngOnInit() {
this.titleService.setTitle("Rondeoverzicht");
const tournamentId = this.route.snapshot.paramMap.get('id');
let roundId = Number(this.route.snapshot.paramMap.get('roundId'));
this.tournamentService.getById(Number(tournamentId)).subscribe(data => {
this.tournament = data;
for (let event of this.tournament.events) {
for (let group of event.groups) {
let roundIndex = 0;
this.prevRound = undefined;
for (let round of group.rounds) {
roundIndex++;
if (round.id == roundId) {
this.event = event;
this.group = group;
this.round = round;
return;
}
this.prevRound = round;
}
}
}
});
}
}

View File

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

View File

@@ -0,0 +1,3 @@
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,65 @@
import {Component, OnInit} from '@angular/core';
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {TournamentDivision} from "../../model/tournamentDivision";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {KeyValuePipe, NgForOf, NgIf} from "@angular/common";
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {Event} from "../../model/event";
import {Player, Strength} from "../../model/player";
import {EventDivision} from "../../model/eventDivision";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-divide',
standalone: true,
imports: [
MatCard,
MatCardHeader,
NgIf,
MatCardContent,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
NgForOf,
KeyValuePipe,
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

@@ -0,0 +1,40 @@
<mat-card appearance="outlined" *ngIf="tournament">
<mat-card-header>
<h5>Loting voor {{ tournament.name }}</h5>
</mat-card-header>
<mat-card-content>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3">
<mat-card-header>
<h6>Loting {{ TournamentEvent.getType(event.type) }}</h6>
</mat-card-header>
<mat-card-content>
<mat-accordion multi="true">
<mat-expansion-panel *ngFor="let group of this.event.groups">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<table class="table {{ event.doublesEvent ? 'w-100' : 'w-50' }}">
<thead class="thead-dark">
<tr>
<th scope="col" class="w-25">Team 1</th>
<th scope="col" style="width: 5%">-</th>
<th scope="col" class="w-25">Team 2</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let match of group.rounds[0].matches">
<td class="align-middle">{{ match.team1 | teamText }}</td>
<td class="align-middle">-</td>
<td class="align-middle">{{ match.team2 | teamText }}</td>
</tr>
</tbody>
</table>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,3 @@
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,58 @@
import {Component, OnInit} from '@angular/core';
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {NgForOf, NgIf} from "@angular/common";
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {Event} from "../../model/event";
import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-draw',
standalone: true,
imports: [
FullNamePipe,
MatCard,
NgIf,
MatCardContent,
MatCardHeader,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
NgForOf,
TeamPipe,
MatAccordion
],
providers: [
FullNamePipe,
TeamPipe
],
templateUrl: './tournament-draw.component.html',
styleUrl: './tournament-draw.component.scss'
})
export class TournamentDrawComponent 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.draw(Number(id)).subscribe(data => {
this.tournament = data;
})
}
protected readonly TournamentEvent = Event;
}

View File

@@ -0,0 +1,85 @@
@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">
<mat-form-field appearance="fill">
<mat-label>Naam</mat-label>
<input matInput [(ngModel)]="tournament.name" name="name" required>
</mat-form-field>
</div>
<div class="col-md-3"></div>
</div>
<div class="row">
<div class="col-md-3">
<mat-form-field appearance="fill">
<mat-label>Datum</mat-label>
<input matInput mask="00-00-0000" [showMaskTyped]="true" [dropSpecialCharacters]="false" [(ngModel)]="tournament.date" name="date" required>
<mat-hint>dd-mm-jjjj</mat-hint>
</mat-form-field>
</div>
<div class="col-md-9"></div>
</div>
<div class="row">
<div class="col-md-4">
<label id="max-events-label">Max. aantal onderdelen per inschrijving</label>
<mat-radio-group [(ngModel)]="tournament.maxEvents" aria-labelledby="max-events-label" name="maxEvents" required>
<mat-radio-button [value]="2">2</mat-radio-button>
<mat-radio-button [value]="3">3</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="row">
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Kosten 1 onderdeel</mat-label>
<input matInput name="costsPerEvent1" type="number" min="0" step="0.5" [(ngModel)]="tournament.costsPerEvent[0]" required>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Kosten 2 onderdelen</mat-label>
<input matInput name="costsPerEvent2" type="number" min="0" step="0.5" [(ngModel)]="tournament.costsPerEvent[1]" required>
</mat-form-field>
</div>
</div>
@if (tournament.maxEvents == 3) {
<div class="row">
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Kosten 3 onderdelen</mat-label>
<input matInput name="costsPerEvent3" type="number" min="0" step="0.5" [(ngModel)]="tournament.costsPerEvent[2]" required>
</mat-form-field>
</div>
</div>
}
<div class="row">
<div class="col-md-2">
<mat-form-field appearance="fill">
<mat-label>Aantal banen</mat-label>
<input matInput name="courts" type="number" min="1" [(ngModel)]="tournament.courts" required>
</mat-form-field>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button>
<mat-icon>save</mat-icon>
{{ isEditMode ? 'Bijwerken' : 'Opslaan' }}
</button>
<a mat-button routerLink="/tournaments">
<mat-icon>cancel</mat-icon>
Annuleren
</a>
</mat-card-actions>
</mat-card>
</form>
}

View File

@@ -0,0 +1,6 @@
mat-form-field {
width: 100%;
}
a, button {
margin-right: 1em;
}

View File

@@ -0,0 +1,97 @@
import {Component, OnInit} from '@angular/core';
import {FormsModule} from "@angular/forms";
import {ActivatedRoute, Router, RouterLink} from "@angular/router";
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {MatAnchor, MatButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
import {MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
import {CurrencyPipe, NgForOf, registerLocaleData} from "@angular/common";
import nl from "@angular/common/locales/nl";
import {TitleService} from "../../service/title.service";
import {NgxMaskDirective, NgxMaskPipe} from "ngx-mask";
registerLocaleData(nl);
@Component({
selector: 'app-tournament-edit',
standalone: true,
imports: [
FormsModule,
RouterLink,
MatAnchor,
MatButton,
MatIcon,
MatCard,
MatCardHeader,
MatCardContent,
MatFormField,
MatInput,
MatLabel,
MatCardActions,
MatRadioButton,
MatRadioGroup,
NgForOf,
CurrencyPipe,
MatHint,
NgxMaskDirective,
NgxMaskPipe
],
providers: [
CurrencyPipe
],
templateUrl: './tournament-edit.component.html',
styleUrl: './tournament-edit.component.scss'
})
export class TournamentEditComponent implements OnInit {
tournament: Tournament;
isEditMode: boolean = false;
constructor(
private tournamentService: TournamentService,
private currencyPipe: CurrencyPipe,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService
) {
this.tournament = new Tournament();
}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.titleService.setTitle("Bewerk Toernooi");
this.isEditMode = true;
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
})
} else {
this.titleService.setTitle("Nieuw Toernooi");
}
}
saveTournament() {
if (this.isEditMode) {
this.tournamentService.update(this.tournament.id, this.tournament).subscribe(() => {
this.router.navigate(['/tournaments']);
});
} else {
this.tournamentService.save(this.tournament).subscribe(() => {
this.router.navigate(['/tournaments']);
});
}
}
protected readonly Array = Array;
setCostsPerEvent(number: number, $event: any) {
let value : string = String($event);
value = value.replace( /^\D+/g, ''); // replace all leading non-digits with nothing
this.tournament.costsPerEvent[number] = Number(value);
}
}

View File

@@ -0,0 +1,47 @@
<mat-card appearance="outlined">
<!--
<mat-card-header>
<h5>Toernooien</h5>
</mat-card-header>
-->
<mat-card-content>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Naam</th>
<th scope="col">Datum</th>
<th scope="col">Status</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let tournament of tournaments">
<td class="align-middle">{{ tournament.id }}</td>
<td class="align-middle"><a [routerLink]="['/tournaments', tournament.id, 'manage']">{{ tournament.name }}</a></td>
<td class="align-middle">{{ tournament.date }}</td>
<td class="align-middle">{{ Tournament.getStatus(tournament) }}</td>
<td class="align-middle">
<a mat-button [routerLink]="['/tournaments', tournament.id, 'edit']" *ngIf="Tournament.getStatus(tournament) != 'Afgerond'">
<mat-icon>edit</mat-icon>
Bewerk
</a>
<a mat-button [routerLink]="['/tournaments', tournament.id, 'registrations']">
<mat-icon>group</mat-icon>
Inschrijvingen
</a>
<a mat-button (click)="clearDraw(tournament)" *ngIf="Tournament.getStatus(tournament) == 'Geloot'">
<mat-icon>safety_divider</mat-icon>
Loting wissen
</a>
</td>
</tr>
</tbody>
</table>
<a mat-button routerLink="/tournaments/add">
<mat-icon>add</mat-icon>
Nieuw toernooi
</a>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,7 @@
a {
margin-right: 1em;
}
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,59 @@
import {AfterContentChecked, AfterContentInit, Component, EventEmitter, OnInit, Output} from '@angular/core';
import {NgFor, NgIf} from "@angular/common";
import {RouterLink} from "@angular/router";
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {MatAnchor, MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatHeaderRow, MatRow,
MatTable, MatTableModule
} from "@angular/material/table";
import {MatMenuTrigger} from "@angular/material/menu";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-tournament-list',
standalone: true,
imports: [
NgFor, RouterLink, NgIf, MatAnchor, MatIcon, MatCard, MatCardHeader, MatCardContent, MatButton, MatTable, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatCell, MatCellDef, MatHeaderRow, MatRow, MatTableModule, MatIconButton, MatMenuTrigger
],
templateUrl: './tournament-list.component.html',
styleUrl: './tournament-list.component.scss'
})
export class TournamentListComponent implements OnInit, AfterContentChecked {
tournaments: Tournament[];
constructor(
private tournamentService: TournamentService,
private titleService: TitleService
) {}
ngOnInit() {
this.tournamentService.getAll().subscribe(data => {
this.tournaments = data;
});
}
ngAfterContentChecked() {
this.titleService.setTitle("Toernooien");
}
protected readonly Tournament = Tournament;
clearDraw(tournament: Tournament) {
this.tournamentService.clearDraw(tournament.id).subscribe(data => {
this.tournamentService.getAll().subscribe(data => {
this.tournaments = data;
});
})
}
}

View File

@@ -0,0 +1,429 @@
@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">
@if (tournament.status == 'UPCOMING') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>playlist_add_check</mat-icon>
&nbsp;Validaties
<button mat-icon-button [matMenuTriggerFor]="upcomingTournamentMenu" class="menu-button m-3">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #upcomingTournamentMenu="matMenu">
<button mat-menu-item (click)="divideTournament()">
<mat-icon>play_arrow</mat-icon>
Deel toernooi in
</button>
</mat-menu>
</ng-template>
<app-tournament-validate></app-tournament-validate>
</mat-tab>
}
@if (tournament.status == 'DIVIDED') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>safety_divider</mat-icon>
&nbsp;Indeling
<button mat-icon-button [matMenuTriggerFor]="dividedTournamentMenu" class="menu-button m-3">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #dividedTournamentMenu="matMenu">
<button mat-menu-item (click)="clearDivision()">
<mat-icon>highlight_remove</mat-icon>
Indeling wissen
</button>
<button mat-menu-item (click)="drawTournament()">
<mat-icon>safety_divider</mat-icon>
1e ronde loten
</button>
</mat-menu>
</ng-template>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3">
<mat-card-header>
<h6>Indeling {{ TournamentEvent.getType(event.type) }}</h6>
</mat-card-header>
<mat-card-content>
<mat-accordion multi="true">
<mat-expansion-panel *ngFor="let group of event.groups">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.name }}&nbsp;<span class="badge text-bg-success">{{ group.teams.length }}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<table class="table {{ event.doublesEvent ? 'w-100' : 'w-50' }}">
<thead class="thead-dark">
<tr>
<th scope="col" class="w-20">Naam</th>
<th scope="col" class="w-20">Club</th>
<th scope="col" class="w-10">Speelsterkte</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-20">Partner</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-20">Club</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-10">Speelsterkte</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let team of group.teams">
<td class="align-middle">{{ team.player1 | fullName }}</td>
<td class="align-middle">{{ team.player1.club }}</td>
<td class="align-middle">{{ getStrength(team.player1.strength.valueOf()) }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ team.player2 | fullName }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ team.player2?.club }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ getStrength(team.player2?.strength?.valueOf()) }}</td>
</tr>
</tbody>
</table>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>
</mat-tab>
}
@if (tournament.status == 'ONGOING') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>play_arrow</mat-icon>
&nbsp;Actieve wedstrijden
&nbsp;
@if (this.activeMatches().length > 0) {
<span class="badge text-bg-success">{{ this.activeMatches().length }}</span>
}
</ng-template>
@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>
}
</mat-tab>
}
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>list</mat-icon>
&nbsp;Onderdelen
</ng-template>
<mat-tab-group animationDuration="0ms" disableRipple="true">
<ng-container *ngFor="let event of tournament.events">
<ng-container *ngFor="let group of event.groups">
<mat-tab label="{{group.id}}">
<ng-template mat-tab-label>
<!--<mat-icon>list</mat-icon>&nbsp;-->
{{ group.name }}
&nbsp;
@if (getActiveMatchCountForGroup(group) > 0) {
<span class="badge text-bg-success">{{ getActiveMatchCountForGroup(group) }}</span>
}
@if (group.status != 'FINISHED' && groupOnlyHasFinishedRounds(group)) {
<button mat-icon-button [matMenuTriggerFor]="groupMenu" class="menu-button m-3">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #groupMenu="matMenu">
<button mat-menu-item (click)="finishGroup(group)">
<mat-icon>check</mat-icon>
Onderdeel afsluiten
</button>
</mat-menu>
}
</ng-template>
<mat-tab-group
animationDuration="0ms"
disableRipple="true"
[(selectedIndex)]="activeRoundTab"
(selectedTabChange)="onRoundTabChange($event)">
<ng-container *ngFor="let round of group.rounds; index as roundIndex">
<mat-tab label="{{round.id}}">
<ng-template mat-tab-label>
<mat-icon>{{ getRoundIcon(round.status) }}</mat-icon>
&nbsp;{{ round.name }}
<button mat-icon-button [matMenuTriggerFor]="activeRoundMenu" class="menu-button m-3">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #activeRoundMenu="matMenu">
@if (round.status == 'NOT_STARTED') {
<button mat-menu-item (click)="startRound(round)">
<mat-icon>play_arrow</mat-icon>
Ronde starten
</button>
<button mat-menu-item (click)="printMatchSheets(round)">
<mat-icon>print</mat-icon>
Wedstrijdbriefjes 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) {
<button mat-menu-item (click)="newRound(group)">
<mat-icon>playlist_add</mat-icon>
Nieuwe ronde
</button>
}
</mat-menu>
</ng-template>
<h6 class="mt-3">Wedstrijden</h6>
@if (round.status == 'NOT_STARTED') {
<table class="table table-hover m-4 wide w-100">
<tbody>
<tr *ngFor="let match of round.matches">
<td class="align-middle w-team">{{ match.team1 | teamText }}</td>
<td class="align-middle w-sep">-</td>
<td class="align-middle w-team">{{ match.team2 | teamText }}</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>
</tr>
}
</tbody>
</table>
} @else if (round.status == 'IN_PROGRESS') {
<table class="table table-hover m-4 wide w-100">
<tbody>
<tr *ngFor="let match of round.matches">
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</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-fill">
@if (match.status == 'NOT_STARTED') {
<button mat-button (click)="startMatch(match)">
<mat-icon>play_arrow</mat-icon>
Wedstrijd starten
</button>
} @else if (match.status == 'IN_PROGRESS') {
<button mat-button (click)="editResult(match, group, round)">
<mat-icon>edit</mat-icon>
Uitslag invoeren
</button>
<button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon>
Wedstrijd stoppen
</button>
} @else if (match.status == 'FINISHED') {
<div class="row result align-items-center">
<span *ngFor="let game of match.games" class="col-2">{{ game.score1 }}-{{ game.score2 }}</span>
@if (match.games.length == 2) {
<span class="col-2"></span>
}
<button mat-icon-button [matMenuTriggerFor]="finishedMatchMenu" class="menu-button m-3">
<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>
Uitslag bewerken
</button>
</mat-menu>
</div>
}
</td>
</tr>
@if (round.drawnOut) {
<tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
</tr>
}
</tbody>
</table>
} @else if (round.status == 'FINISHED') {
<table class="table table-hover m-4 wide {{ this.groupIsDoublesType(group) ? 'w-100' : 'w-100' }}">
<tbody>
<tr *ngFor="let match of round.matches">
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</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-fill">
<div class="row result align-items-center">
<span *ngFor="let game of match.games" class="col-2">{{ game.score1 }}-{{ game.score2 }}</span>
@if (match.games.length == 2) {
<span class="col-2"></span>
}
</div>
</td>
</tr>
@if (round.drawnOut) {
<tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td>
</tr>
}
</tbody>
</table>
}
<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
}
</th>
<th>Gespeeld</th>
<th>Punten</th>
<th>Games</th>
<th>Wedstrijdpunten</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr *ngFor="let entry of getStandingsForRound(round, group).entries">
<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>
</tr>
</tbody>
</table>
</mat-tab>
</ng-container>
</mat-tab-group>
</mat-tab>
</ng-container>
</ng-container>
</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>
<tr *ngFor="let tournamentPlayer of tournament.tournamentPlayers">
<td>{{ tournamentPlayer.name }}</td>
<td>
<ng-container *ngFor="let event of tournamentPlayer.events">
{{ event }}&nbsp;
</ng-container>
</td>
<td>
{{ tournament.costsPerEvent[tournamentPlayer.events.length - 1] | currency:'EUR':'symbol':'1.2-2':'nl' }}
</td>
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.paid" (change)="playerPaid($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.paid) {
Betaald
} @else {
Nog niet betaald
}
</mat-slide-toggle>
</td>
<td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.present" (change)="playerPresent($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.present) {
Aanwezig
} @else {
Nog niet aanwezig
}
</mat-slide-toggle>
</td>
</tr>
</tbody>
</table>
</mat-tab>
</mat-tab-group>
</mat-tab>
}
</mat-tab-group>
</mat-card-content>
</mat-card>
}

View File

@@ -0,0 +1,26 @@
td {
vertical-align: middle;
}
td, th {
background-color: transparent !important;
}
table.wide td, table.wide th {
height: 4em;
}
.winner {
color: green;
font-weight: bold;
}
td.w-team {
width: 30%;
}
td.w-sep {
width: 5%;
}
td.w-fill {
width: 35%;
}

View File

@@ -0,0 +1,355 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelDescription,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {CurrencyPipe, DatePipe, DecimalPipe, NgClass, NgForOf, NgIf} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {Tournament} from "../../model/tournament";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {MatList, MatListItem} from "@angular/material/list";
import {MatDivider} from "@angular/material/divider";
import {MatAnchor, MatButton, MatIconAnchor, MatIconButton, MatMiniFabAnchor} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {Group} from "../../model/group";
import {Round} from "../../model/round";
import {MatTable} from "@angular/material/table";
import {StatusPipe} from "../../pipes/status-pipe";
import {MatMenu, MatMenuContent, 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";
import {MatchResultComponent} from "../match-result/match-result.component";
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 {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 {TitleService} from "../../service/title.service";
@Component({
selector: 'app-tournament-manage',
standalone: true,
imports: [
FullNamePipe,
MatAccordion,
MatCard,
MatCardContent,
MatCardHeader,
MatExpansionPanel,
MatExpansionPanelDescription,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
NgForOf,
NgIf,
TeamPipe,
MatListItem,
MatDivider,
MatList,
MatAnchor,
MatIcon,
MatTable,
StatusPipe,
NgClass,
MatMenu,
MatMenuItem,
MatMenuTrigger,
FormsModule,
DatePipe,
MatTabGroup,
MatTab,
MatTabLabel,
MatButton,
MatchResultPipe,
MatIconAnchor,
MatMiniFabAnchor,
MatIconButton,
DecimalPipe,
TournamentValidateComponent,
MatSlideToggle,
CurrencyPipe,
MatMenuContent
],
providers: [
FullNamePipe,
TeamPipe,
MatchResultPipe
],
templateUrl: './tournament-manage.component.html',
styleUrl: './tournament-manage.component.scss'
})
export class TournamentManageComponent implements OnInit {
@Input() tournament: Tournament;
activeRoundTab: number = 0;
constructor(
private tournamentService: TournamentService,
private _snackBar: MatSnackBar,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService,
private title: Title
) {
}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.route.queryParams.subscribe(params => {
if (params['tab']) {
this.activeRoundTab = params['tab'];
}
})
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
this.titleService.setTitle(this.tournament.name);
this.title.setTitle(this.tournament.name);
});
}
onRoundTabChange(event: MatTabChangeEvent) {
const index = event.index;
this.router.navigate(
['tournaments/' + this.tournament.id + '/manage'],
{
relativeTo: null, queryParams: {tab: index}
});
}
getRoundIcon(status: String) {
if (status == "FINISHED") {
return "check";
} else if (status == "IN_PROGRESS") {
return "play_arrow";
} else if (status == "NOT_STARTED") {
return "hourglass_top";
} else {
return "warning";
}
}
getStandingsForRound(round: Round, group: Group): Standings {
if (round.status == 'FINISHED') {
return round.standings;
} else {
return group.standings;
}
}
groupIsDoublesType(group: Group) {
return group.type == 'HD' || group.type == 'DD' || group.type == 'GD';
}
startRound(round: Round) {
this.tournamentService.startRound(this.tournament.id, round.id).subscribe(data => {
this.tournament = data;
});
}
finishRound(round: Round) {
this.tournamentService.finishRound(this.tournament.id, round.id).subscribe(data => {
this.tournament = data;
});
}
finishGroup(group: Group) {
this.tournamentService.finishGroup(this.tournament.id, group.id).subscribe(data => {
this.tournament = data;
});
}
divideTournament() {
this.tournamentService.divide(this.tournament.id).subscribe(data => {
this.tournament = data;
});
}
clearDivision() {
this.tournamentService.clearDivision(this.tournament.id).subscribe(data => {
this.tournament = data;
});
}
drawTournament() {
this.tournamentService.draw(this.tournament.id).subscribe(data => {
this.tournament = data;
});
}
startMatch(match: Match) {
const availableCourts = this.getAvailableCourts();
if (availableCourts.length == 0) {
alert('Geen banen beschikbaar!');
} else {
this.courtSelectionDialog.open(CourtSelectionComponent, {
data: {
match: match,
availableCourts: this.getAvailableCourts(),
totalCourts: this.tournament.courts
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.tournamentService.startMatch(this.tournament.id, match.id, result).subscribe(data => {
this.tournament = data;
})
}
});
}
}
getAvailableCourts(): number[] {
const maxCourts = this.tournament.courts;
const activeCourts = this.activeMatches().map(activeMatch => activeMatch.match.court);
let i = 0, courts = Array(maxCourts);
while (i < maxCourts) courts[i++] = i;
return courts.filter(court => activeCourts.indexOf(court) < 0);
}
stopMatch(match: Match) {
this.tournamentService.stopMatch(this.tournament.id, match.id).subscribe(data => {
this.tournament = data;
})
}
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.');
});
}
getStrength(strength: string | undefined) {
if (strength == undefined) return "";
for (let [key, value] of Object.entries(Strength)) {
if (key == strength) return value;
}
return "";
}
printMatchSheets(round: Round) {
window.open(`tournaments/${this.tournament.id}/rounds/${round.id}/matchsheets`, "_blank");
}
printRoundOverview(round: Round) {
window.open(`tournaments/${this.tournament.id}/rounds/${round.id}/overview`, "_blank");
}
activeMatches(): ActiveMatch[] {
let matches: ActiveMatch[] = [];
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) {
if (match.status == 'IN_PROGRESS') {
matches.push(new ActiveMatch(match, round, group));
}
}
}
}
}
return matches;
}
getActiveMatchCountForGroup(group: Group): number {
let active = 0;
for (const round of group.rounds) {
for (const match of round.matches) {
if (match.status == 'IN_PROGRESS') {
active++;
}
}
}
return active;
}
groupOnlyHasFinishedRounds(group: Group): boolean {
let allFinished = true;
for (const round of group.rounds) {
allFinished &&= round.status == 'FINISHED';
}
return allFinished;
}
matchResultDialog = inject(MatDialog);
courtSelectionDialog = inject(MatDialog);
editResult(match: Match, group: Group, round: Round) {
this.matchResultDialog.open(MatchResultComponent, {
data: {match: match, 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;
})
}
});
}
checkWinner(match: Match): number {
if (match.games.length == 0) return 0;
if (match.games.length == 3) {
if (match.games[2].score1 > match.games[2].score2) {
return 1;
} else {
return 2;
}
}
if (match.games[1].score1 > match.games[1].score2) {
return 1;
} else {
return 2;
}
}
checkRoundComplete(round: Round) {
let complete: boolean = true;
for (let match of round.matches) {
complete &&= match.status == 'FINISHED';
}
return complete;
}
protected readonly TournamentEvent = Event;
}
class ActiveMatch {
constructor(match: Match, round: Round, group: Group) {
this.match = match;
this.round = round;
this.group = group;
}
match: Match;
round: Round;
group: Group;
}

View File

@@ -0,0 +1,32 @@
<mat-card appearance="outlined" *ngIf="tournament">
<mat-card-header>
<h5>Inschrijvingen voor {{ tournament.name }}ab</h5>
</mat-card-header>
<mat-card-content>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3">
<mat-card-header>
<h6>{{ TournamentEvent.getType(event.type) }} ({{ event.registrations.length}} inschrijvingen)</h6>
</mat-card-header>
<mat-card-content>
<table class="table {{ event.doublesEvent ? 'w-100' : 'w-50' }}">
<thead class="thead-dark">
<tr>
<th scope="col" class="w-25">Naam</th>
<th scope="col" class="w-25">Club</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-25">Partner</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-25">Club</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let registration of event.registrations">
<td class="align-middle">{{ registration.player | fullName }}</td>
<td class="align-middle">{{ registration.player.club }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ registration.partner | fullName }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ registration.partner?.club }}</td>
</tr>
</tbody>
</table>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,3 @@
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,46 @@
import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {Tournament} from "../../model/tournament";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {NgForOf, NgIf} from "@angular/common";
import {Player} from "../../model/player";
import {Event} from "../../model/event";
import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TitleService} from "../../service/title.service";
@Component({
selector: 'app-tournament-registrations',
standalone: true,
imports: [
MatCard,
MatCardHeader,
MatCardContent,
NgForOf,
NgIf,
FullNamePipe
],
templateUrl: './tournament-registrations.component.html',
styleUrl: './tournament-registrations.component.scss'
})
export class TournamentRegistrationsComponent implements OnInit {
tournament?: Tournament;
constructor(
private tournamentService: TournamentService,
private route: ActivatedRoute,
private router: Router,
private titleService: TitleService
) {}
ngOnInit() {
this.titleService.setTitle("Inschrijvingen");
const id = this.route.snapshot.paramMap.get('id');
this.tournamentService.getById(Number(id)).subscribe(data => {
this.tournament = data;
})
}
protected readonly TournamentEvent = Event;
}

View File

@@ -0,0 +1,69 @@
@if (tournamentValidation && tournament) {
<mat-card appearance="outlined" class="m-3">
<mat-card-header>
<h6>Toernooi</h6>
</mat-card-header>
<mat-card-content>
<mat-expansion-panel [disabled]="tournamentValidation.validations.length == 0">
<mat-expansion-panel-header>
<mat-panel-title>
Validaties&nbsp;<span class="badge {{ Validation.hasErrors(tournamentValidation.validations) ? 'text-bg-danger' : 'text-bg-success'}}">{{ tournamentValidation.validations.length }}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<ul>
<li *ngFor="let validation of tournamentValidation.validations">
<mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon>
{{ validation.message }}
</li>
</ul>
</mat-expansion-panel>
</mat-card-content>
</mat-card>
<mat-card *ngFor="let event of tournament.events" appearance="outlined" class="m-3">
<mat-card-header>
<h6>{{ TournamentEvent.getType(event.type) }}</h6>
</mat-card-header>
<mat-card-content>
<mat-accordion multi="true">
<mat-expansion-panel [disabled]="event.registrations.length == 0">
<mat-expansion-panel-header>
<mat-panel-title>
Inschrijvingen&nbsp;<span class="badge {{ event.registrations.length == 0 ? 'text-bg-danger' : 'text-bg-success'}}">{{ event.registrations.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-25">Naam</th>
<th scope="col" class="w-25">Club</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-25">Partner</th>
<th *ngIf="event.doublesEvent" scope="col" class="w-25">Club</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let registration of event.registrations">
<td class="align-middle">{{ registration.player | fullName }}</td>
<td class="align-middle">{{ registration.player.club }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ registration.partner | fullName }}</td>
<td *ngIf="event.doublesEvent" class="align-middle">{{ registration.partner?.club }}</td>
</tr>
</tbody>
</table>
</mat-expansion-panel>
<mat-expansion-panel [disabled]="getEventValidation(event.id)?.validations?.length == 0">
<mat-expansion-panel-header>
<mat-panel-title>
Validaties&nbsp;<span class="badge {{ Validation.hasErrors(getEventValidation(event.id)?.validations) ? 'text-bg-danger' : 'text-bg-success'}}">{{ getEventValidation(event.id)?.validations?.length }}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<ul>
<li *ngFor="let validation of getEventValidation(event.id)?.validations">
<mat-icon class="text-{{ getColorForSeverity(validation.severity) }}">{{ getIconForSeverity(validation.severity) }}</mat-icon>
{{ validation.message }}
</li>
</ul>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>
}

View File

@@ -0,0 +1,3 @@
td, th {
background-color: transparent !important;
}

View File

@@ -0,0 +1,98 @@
import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card";
import {Tournament} from "../../model/tournament";
import {AsyncPipe, NgForOf, NgIf} from "@angular/common";
import {Event} from "../../model/event";
import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router";
import {
MatAccordion,
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelTitle
} from "@angular/material/expansion";
import {Player} from "../../model/player";
import {EventValidation, TournamentValidation, Validation} from "../../model/tournamentValidation";
import {MatIcon} from "@angular/material/icon";
import {FullNamePipe} from "../../pipes/fullname-pipe";
@Component({
selector: 'app-tournament-validate',
standalone: true,
imports: [
MatCard,
MatCardHeader,
MatCardContent,
NgForOf,
MatExpansionPanel,
MatExpansionPanelTitle,
MatExpansionPanelHeader,
NgIf,
MatAccordion,
AsyncPipe,
MatIcon,
FullNamePipe
],
templateUrl: './tournament-validate.component.html',
styleUrl: './tournament-validate.component.scss'
})
export class TournamentValidateComponent implements OnInit {
tournament: Tournament;
tournamentValidation: TournamentValidation;
protected readonly TournamentEvent = Event;
protected readonly Player = Player;
protected readonly Validation = Validation;
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.getValidation(Number(id)).subscribe(data => {
this.tournamentValidation = data;
});
}
getEventValidation(id: number): EventValidation | null {
if (!this.tournamentValidation) {
return null;
} else {}
return this.tournamentValidation.eventValidations.filter(eventValidation => eventValidation.eventId == id)[0];
}
getIconForSeverity(severity: string) {
if (severity == 'INFO') {
return "info";
} else if (severity == 'WARN') {
return "warning";
} else {
return "dangerous";
}
}
getColorForSeverity(severity: string) {
if (severity == 'INFO') {
return "success";
} else if (severity == 'WARN') {
return "warning";
} else {
return "danger";
}
}
tournamentHasErrors(): boolean {
let hasErrors: boolean = Validation.hasErrors(this.tournamentValidation.validations);
for (let eventValidation of this.tournamentValidation.eventValidations) {
hasErrors &&= Validation.hasErrors(eventValidation.validations);
}
return hasErrors;
}
}

19
src/app/model/event.ts Normal file
View File

@@ -0,0 +1,19 @@
import {Registration} from "./registration";
import {Group} from "./group";
export class Event {
id: number;
type: string;
status: string;
doublesEvent: boolean;
registrations: Registration[];
groups: Group[];
static getType(type: string): string {
if (type == "HE") return "Herenenkel";
if (type == "HD") return "Herendubbel";
if (type == "DD") return "Damesdubbel";
if (type == "DE") return "Damesenkel";
return "Gemengd dubbel";
}
}

View File

@@ -0,0 +1,6 @@
import {Group} from "./group";
export class EventDivision {
eventId: number;
groups: Group[];
}

4
src/app/model/game.ts Normal file
View File

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

13
src/app/model/group.ts Normal file
View File

@@ -0,0 +1,13 @@
import {Team} from "./team";
import {Round} from "./round";
import {Standings} from "./standings";
export class Group {
id: number;
name: string;
type: string;
status: string;
rounds: Round[];
teams: Team[];
standings: Standings;
}

14
src/app/model/match.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Team } from "./team";
import {Game} from "./game";
export class Match {
id: number;
type: string;
status: string;
team1: Team;
team2: Team;
startTime: Date;
endTime: Date;
games: Game[];
court: number;
}

24
src/app/model/player.ts Normal file
View File

@@ -0,0 +1,24 @@
export enum Strength {
D5 = "5e divisie",
D6 = "6e divisie",
D7 = "7e divisie",
D8 = "8e divisie",
D9 = "9e divisie",
DR = "recreatief"
}
export class Player {
id: number;
firstName: string;
middleName: string;
lastName: string;
sex: string;
birthday: string;
phoneNumber: string
email: string;
club: string;
strength: Strength;
}

View File

@@ -0,0 +1,9 @@
import {Player} from "./player";
export class Registration {
id: number;
tournament: number;
event: number;
player: Player;
partner?: Player;
}

6
src/app/model/result.ts Normal file
View File

@@ -0,0 +1,6 @@
import {Game} from "./game";
export class Result {
matchId: number;
games: Game[] = [];
}

13
src/app/model/round.ts Normal file
View File

@@ -0,0 +1,13 @@
import {Match} from "./match";
import {Team} from "./team";
import {Standings} from "./standings";
export class Round {
id: number;
name: string;
matches: Match[];
status: string;
quit: Team[];
drawnOut: Team;
standings: Standings;
}

View File

@@ -0,0 +1,18 @@
import {Team} from "./team";
export class Standings {
entries: StandingEntry[];
}
export class StandingEntry {
position: number;
team: Team;
played: number;
won: number;
lost: number;
points: number;
gamesWon: number;
gamesLost: number;
pointsWon: number;
pointsLost: number;
}

7
src/app/model/team.ts Normal file
View File

@@ -0,0 +1,7 @@
import {Player} from "./player";
import {FullNamePipe} from "../pipes/fullname-pipe";
export class Team {
player1: Player;
player2: Player;
}

View File

@@ -0,0 +1,23 @@
import {Event} from "./event";
import {TournamentPlayer} from "./tournamentPlayer";
export class Tournament {
id: number;
name: string;
date: string;
status: string;
events: Event[];
tournamentPlayers: TournamentPlayer[];
maxEvents: number;
costsPerEvent: number[] = [0, 0, 0];
courts: number;
static getStatus(tournament: Tournament): string {
if (tournament.status == "CLOSED") return "Afgerond";
if (tournament.status == "DIVIDED") return "Ingedeeld";
if (tournament.status == "DRAWN") return "Geloot";
if (tournament.status == "ONGOING") return "Bezig";
return "Nieuw";
}
}

View File

@@ -0,0 +1,9 @@
import {EventDivision} from "./eventDivision";
export class TournamentDivision {
tournamentId: number;
divided: boolean;
eventDivisions: EventDivision[];
}

View File

@@ -0,0 +1,8 @@
export class TournamentPlayer {
playerId: number;
name: string;
events: string[];
paid: boolean;
present: boolean;
}

View File

@@ -0,0 +1,34 @@
export class TournamentRegistration {
id: number;
name: string;
editable: boolean;
date: string;
status: string;
events: EventRegistration[];
static getStatus(tournamentRegistration: TournamentRegistration): string {
if (tournamentRegistration.status == "CLOSED") return "Afgerond";
if (tournamentRegistration.status == "DIVIDED") return "Ingedeeld";
if (tournamentRegistration.status == "DRAWN") return "Geloot";
if (tournamentRegistration.status == "IN_PROGRESS") return "Bezig";
return "Nieuw";
}
}
export class EventRegistration {
id: number;
type: string;
doublesEvent: boolean;
registered: boolean;
player?: number;
partner?: number;
static getType(type: string): string {
if (type == "HE") return "Herenenkel";
if (type == "HD") return "Herendubbel";
if (type == "DD") return "Damesdubbel";
if (type == "DE") return "Damesenkel";
return "Gemengd dubbel";
}
}

View File

@@ -0,0 +1,24 @@
export class TournamentValidation {
id: number;
validations : Validation[];
eventValidations: EventValidation[];
}
export class EventValidation {
eventId: number;
validations: Validation[];
}
export class Validation {
severity: string;
message: string;
static hasErrors(validations: Validation[] | undefined): boolean {
if (validations == null) {
return false;
} else {
return validations.filter(v => v.severity == 'ERROR').length > 0;
}
}
}

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fullName',
standalone: true
})
export class FullNamePipe implements PipeTransform {
transform(person: any, args?: any): any {
if (person) {
return [person.firstName, person.middleName, person.lastName].join(" ");
} else {
return "";
}
}
}

View File

@@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: 'matchResult',
standalone: true
})
export class MatchResultPipe implements PipeTransform {
transform(match: any, args?: any): any {
let result = `${match.games[0].score1} ${match.games[0].score2}`;
result += `${match.games[1].score1} ${match.games[1].score2}`;
if (match.games[2] != null) {
result += `${match.games[2].score1} - ${match.games[2].score2}`;
}
return result;
}
}

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: 'status',
standalone: true
})
export class StatusPipe implements PipeTransform {
transform(objectWithStatus: any, args?: any): any {
if (objectWithStatus.status == 'NOT_STARTED') return 'Nog niet gestart';
return "";
}
}

View File

@@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
import {FullNamePipe} from "../pipes/fullname-pipe";
@Pipe({
name: 'teamText',
standalone: true
})
export class TeamPipe implements PipeTransform {
constructor(private fullNamePipe: FullNamePipe) {}
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

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable} from "rxjs";
import { Player } from "../model/player";
import {environment} from "../../environments/environment";
@Injectable({
providedIn: 'root'
})
export class PlayerService {
private readonly playersUrl: string;
constructor(private http: HttpClient) {
this.playersUrl = `${environment.backendUrl}/players`;
}
public getAll(): Observable<Player[]> {
return this.http.get<Player[]>(this.playersUrl);
}
public getById(id: number): Observable<Player> {
return this.http.get<Player>(`${this.playersUrl}/${id}`);
}
public save(player: Player) {
return this.http.post<Player>(this.playersUrl, player);
}
public update(id: number, player: Player) {
return this.http.put<Player>(`${this.playersUrl}/${id}`, player);
}
}

Some files were not shown because too many files have changed in this diff Show More