Compare commits

...

35 Commits

Author SHA1 Message Date
87d9291bfb Use substitutions for disable button test
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 23:19:00 +01:00
c646ae15ee Optionally disable starting a match and show reason
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 23:00:00 +01:00
Michel ten Voorde
8128ba4744 Cleanup
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-06 16:42:48 +01:00
Michel ten Voorde
19e4372006 Show substitutions in standings 2025-11-06 16:42:22 +01:00
Michel ten Voorde
7fd84005f9 Show 100 players by default 2025-11-06 16:41:03 +01:00
Michel ten Voorde
a56317fecd Update counter 2025-11-06 16:40:20 +01:00
Michel ten Voorde
ecbb16776c Don't show teams inline when entering match result
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-11-03 14:32:41 +01:00
Michel ten Voorde
3008f45dfa Use autocompletion for partner search
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-31 16:02:15 +01:00
Michel ten Voorde
5baf1228f7 Use autocompletion for partner search, WIP 2025-10-31 15:13:22 +01:00
Michel ten Voorde
aacf06e203 Made tournament-players more standalone, now works from tournament screen 2025-10-29 17:27:32 +01:00
368aa53b9d match-result simplification
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-29 00:12:11 +01:00
335577fa4d match-result simplification 2025-10-29 00:09:14 +01:00
1f55acecd0 Use snackbars instead of alerts
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-28 09:26:26 +01:00
c1fedd728b Use inline on round overview 2025-10-28 09:22:36 +01:00
0e9e8d1e0f Put event on top on match sheets 2025-10-28 09:22:21 +01:00
Michel ten Voorde
0e1e1932a1 Use localStorage
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-27 14:25:26 +01:00
fb36ee1a05 Always show substitutes if applicable
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-09 16:24:55 +02:00
9cb3568770 Update route config
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 22:36:02 +02:00
d387452044 Cleanup
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 22:18:04 +02:00
2cdccc4dd8 Cleanup
Some checks failed
Gitea/swiss-client/pipeline/head There was a failure building this commit
2025-10-08 22:15:41 +02:00
8083e7fc5f Updated auth config
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-08 21:36:13 +02:00
0fd693aa42 Layout fixes
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-10-04 22:40:51 +02:00
edaffc82e4 Updated angular-material
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-30 23:21:53 +02:00
1dbc2f1ca9 Updated angular-core 2025-09-30 23:01:46 +02:00
21fa706b45 Updated angular-cli 2025-09-30 23:00:57 +02:00
f2ee2b467d Fixed authguard 2025-09-30 22:59:13 +02:00
cdf27f1948 Moved logic to backend
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-25 23:14:32 +02:00
2c33c9a68d Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-23 22:45:44 +02:00
181aebf16a Invallers 2025-09-23 20:47:11 +02:00
6a9ea43e23 Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-18 23:28:02 +02:00
02b884bbcf Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-17 08:59:02 +02:00
5c846a6351 Invallers
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-11 23:43:28 +02:00
929fd6b581 WIP: substitutions
Some checks failed
Gitea/swiss-client/pipeline/head There was a failure building this commit
2025-09-10 10:35:15 +02:00
33bfde27d4 Spelerslijst op elke pagina
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-09-01 22:59:33 +02:00
564340fec4 Finals round
All checks were successful
Gitea/swiss-client/pipeline/head This commit looks good
2025-08-28 23:22:09 +02:00
56 changed files with 2230 additions and 1036 deletions

View File

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

546
package-lock.json generated
View File

@@ -8,23 +8,24 @@
"name": "swiss-client", "name": "swiss-client",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/animations": "^20.2.1", "@angular/animations": "^20.3.2",
"@angular/cdk": "^20.2.0", "@angular/cdk": "^20.2.5",
"@angular/common": "^20.2.1", "@angular/common": "^20.3.2",
"@angular/compiler": "^20.2.1", "@angular/compiler": "^20.3.2",
"@angular/core": "^20.2.1", "@angular/core": "^20.3.2",
"@angular/forms": "^20.2.1", "@angular/forms": "^20.3.2",
"@angular/material": "^20.2.0", "@angular/material": "^20.2.5",
"@angular/material-moment-adapter": "^20.2.0", "@angular/material-moment-adapter": "^20.2.5",
"@angular/platform-browser": "^20.2.1", "@angular/platform-browser": "^20.3.2",
"@angular/platform-browser-dynamic": "^20.2.1", "@angular/platform-browser-dynamic": "^20.3.2",
"@angular/platform-server": "^20.2.1", "@angular/platform-server": "^20.3.2",
"@angular/router": "^20.2.1", "@angular/router": "^20.3.2",
"@angular/ssr": "^20.2.0", "@angular/ssr": "^20.3.3",
"@ng-bootstrap/ng-bootstrap": "^19.0.1", "@ng-bootstrap/ng-bootstrap": "^19.0.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
"express": "^4.21.2", "express": "^4.21.2",
"jwt-decode": "^4.0.0",
"moment": "2.30.1", "moment": "2.30.1",
"ngx-cookie-service-ssr": "^20.1.0", "ngx-cookie-service-ssr": "^20.1.0",
"ngx-mask": "^20.0.3", "ngx-mask": "^20.0.3",
@@ -34,10 +35,10 @@
"zone.js": "~0.15.1" "zone.js": "~0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^20.2.0", "@angular/build": "^20.3.3",
"@angular/cli": "^20.2.0", "@angular/cli": "^20.3.3",
"@angular/compiler-cli": "^20.2.1", "@angular/compiler-cli": "^20.3.2",
"@angular/localize": "^20.2.1", "@angular/localize": "^20.3.2",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
@@ -273,13 +274,13 @@
} }
}, },
"node_modules/@angular-devkit/architect": { "node_modules/@angular-devkit/architect": {
"version": "0.2002.0", "version": "0.2003.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2002.0.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.3.tgz",
"integrity": "sha512-PaBXFP1kdUuNtMie0lWnitlYbq8o1gz/s0YIa8oY1X3swOJ7bP6kBfxTb9opV5uXAOkXg2zCdnZ4Eu1aVkgPGw==", "integrity": "sha512-DOnGyv9g24vaDzf5koLOcVri1kYJIBD9UKiJWOWk4H5cFlcpTXQ+PilPmDq6A+X94Tt4MZHImmKsk6LLRPIwFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "20.2.0", "@angular-devkit/core": "20.3.3",
"rxjs": "7.8.2" "rxjs": "7.8.2"
}, },
"engines": { "engines": {
@@ -289,9 +290,9 @@
} }
}, },
"node_modules/@angular-devkit/core": { "node_modules/@angular-devkit/core": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.3.tgz",
"integrity": "sha512-3CM6Zsr09Kf92ItFkxijlnC4+ZOgkxdCk0vFYvuw9UuvTDNwyIqJi6693PRPRbcXgpdY2vs6u99elSvQVmoEEw==", "integrity": "sha512-2T5mX2duLapZYPYmXUSUe9VW8Dhu10nVBVvEp31jSE6xvjbPM5mlsv6+fks1E4RjhzvaamY9bm3WgwYwNiEV5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -317,13 +318,13 @@
} }
}, },
"node_modules/@angular-devkit/schematics": { "node_modules/@angular-devkit/schematics": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.3.tgz",
"integrity": "sha512-TCPIN6Bd04oGuNocETmsd9hzGYrjrivisbMKb0WOuDi3OnCkmWqsPR+QA2kYwTOGqG3HXkz/z3CA0g04M2fgrQ==", "integrity": "sha512-LDn39BjyQLAK/DaVamLElMtI0UoCZIs4jKcMEv8PJ/nnBmrYFHVavWPggeFWMycjeXsdX34Msiml88HZWlXypw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "20.2.0", "@angular-devkit/core": "20.3.3",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"magic-string": "0.30.17", "magic-string": "0.30.17",
"ora": "8.2.0", "ora": "8.2.0",
@@ -336,9 +337,9 @@
} }
}, },
"node_modules/@angular/animations": { "node_modules/@angular/animations": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.2.tgz",
"integrity": "sha512-g4yLXwXCF7OAahx1xI4FXRwG4dIXfBqHsvlpx2TappaMRpiPp7PfP2cW6l3ox+KRpTWhSvcRqbJyIOWad0f7Rw==", "integrity": "sha512-za7onSElEUbaI9iS8j7nKf8FjyvVng6wFsb2ZuHxr71dMgnYkqPfMu0KMP+mkZ3yUVc//7SllXcSkGBHShyCcw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -347,19 +348,18 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "20.2.1", "@angular/core": "20.3.2"
"@angular/core": "20.2.1"
} }
}, },
"node_modules/@angular/build": { "node_modules/@angular/build": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.3.tgz",
"integrity": "sha512-/Yhqhg01UvX0E+tx4WAeK3AnwpZLqcw+XKTmsPsH5rbqpLKNRR9XsC3PJ4qBFU1u9/Lh13mmmr1+pG2p8ixMug==", "integrity": "sha512-WhwAbovHAxDbNeR5jB2IS/SVs+yQg9NETFeJ5f7T3n/414ULkGOhXn+29i1rzwJhf1uqM9lsedcv2tKn1N24/A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "2.3.0", "@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.2002.0", "@angular-devkit/architect": "0.2003.3",
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-annotate-as-pure": "7.27.3",
"@babel/helper-split-export-declaration": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7",
@@ -377,12 +377,12 @@
"parse5-html-rewriting-stream": "8.0.0", "parse5-html-rewriting-stream": "8.0.0",
"picomatch": "4.0.3", "picomatch": "4.0.3",
"piscina": "5.1.3", "piscina": "5.1.3",
"rolldown": "1.0.0-beta.32", "rolldown": "1.0.0-beta.38",
"sass": "1.90.0", "sass": "1.90.0",
"semver": "7.7.2", "semver": "7.7.2",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"tinyglobby": "0.2.14", "tinyglobby": "0.2.14",
"vite": "7.1.2", "vite": "7.1.5",
"watchpack": "2.4.4" "watchpack": "2.4.4"
}, },
"engines": { "engines": {
@@ -401,7 +401,7 @@
"@angular/platform-browser": "^20.0.0", "@angular/platform-browser": "^20.0.0",
"@angular/platform-server": "^20.0.0", "@angular/platform-server": "^20.0.0",
"@angular/service-worker": "^20.0.0", "@angular/service-worker": "^20.0.0",
"@angular/ssr": "^20.2.0", "@angular/ssr": "^20.3.3",
"karma": "^6.4.0", "karma": "^6.4.0",
"less": "^4.2.0", "less": "^4.2.0",
"ng-packagr": "^20.0.0", "ng-packagr": "^20.0.0",
@@ -464,9 +464,9 @@
} }
}, },
"node_modules/@angular/cdk": { "node_modules/@angular/cdk": {
"version": "20.2.0", "version": "20.2.5",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.5.tgz",
"integrity": "sha512-BZkhRMr3nEOHHCzEgKZM537G4aq0VAwoejhYn7oIvY0UU+arHKz+U7Gc44KH5GaAgVLojtJtkFXsArifzYUwzw==", "integrity": "sha512-1cpR/5jeKXLR1D+PsEvRn0QhSWD3/AjtbugJF5nlx/7L90YXhNFCmNAxAkdFKSn4YIDoPwMHgvOpS7yb51wohQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"parse5": "^8.0.0", "parse5": "^8.0.0",
@@ -479,19 +479,19 @@
} }
}, },
"node_modules/@angular/cli": { "node_modules/@angular/cli": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.3.tgz",
"integrity": "sha512-p62hkuQOxf5kJsVq6AT7B1MHYo1uPGoZV4lf47qOrLjl0WANwfxEgLvyuVgL47ylnINbPnITeeUdoadVn4t1sw==", "integrity": "sha512-3c8xCklJ0C0T6ETSncAoXlOYNi3x7vLT3PS56rIaQ0jtlvD4Y+RQakd3+iffVAapvh/JB27WNor8pJRThLZ/jg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/architect": "0.2002.0", "@angular-devkit/architect": "0.2003.3",
"@angular-devkit/core": "20.2.0", "@angular-devkit/core": "20.3.3",
"@angular-devkit/schematics": "20.2.0", "@angular-devkit/schematics": "20.3.3",
"@inquirer/prompts": "7.8.2", "@inquirer/prompts": "7.8.2",
"@listr2/prompt-adapter-inquirer": "3.0.1", "@listr2/prompt-adapter-inquirer": "3.0.1",
"@modelcontextprotocol/sdk": "1.17.3", "@modelcontextprotocol/sdk": "1.17.3",
"@schematics/angular": "20.2.0", "@schematics/angular": "20.3.3",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.35.0", "algoliasearch": "5.35.0",
"ini": "5.0.0", "ini": "5.0.0",
@@ -553,9 +553,9 @@
} }
}, },
"node_modules/@angular/common": { "node_modules/@angular/common": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.2.tgz",
"integrity": "sha512-T6RYnDZA9TyYhj2hUz4set8p4RbBCg6IKUvy6qzdKTl4nn4xQ0XUV7aGBYN4LKiGrse9lzlVUAyXtkhmwuBbCQ==", "integrity": "sha512-5V9AzLhCA1dNhF+mvihmdHoZHbEhIb1jNYRA1/JMheR+G7NR8Mznu6RmWaKSWZ4AJeSJN8rizWN2wpVPWTKjSQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -564,14 +564,14 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "20.2.1", "@angular/core": "20.3.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/compiler": { "node_modules/@angular/compiler": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.2.tgz",
"integrity": "sha512-ghVt1E8xmwjMwqyGRwXYJkr7fz40VEreUSX1q+gEzbGTftVrK1foxPT8jcueIn0ztArDf7+zSMtu314FiJZyYA==", "integrity": "sha512-5fSzkPmRomZ9H43c82FJWLwdOi7MICMimP1y1oYJZcUh3jYRhXUrQvD0jifdRVkkgKNjaZYlMr0NkrYQFgFong==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -581,9 +581,9 @@
} }
}, },
"node_modules/@angular/compiler-cli": { "node_modules/@angular/compiler-cli": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.2.tgz",
"integrity": "sha512-VpbcRqNPJvy1L9RDtGGQsQiOrMzxodUWklphbtnh9MrrK6lLuy6Qj2ROiW7vKL9WfLTCXWA24gBAcMAR76dq3Q==", "integrity": "sha512-rLox2THiALVQqYGUaxZ6YD8qUoXIOGTw3s0tim9/U65GuXGRtYgG0ZQWYp3yjEBes0Ksx2/15eFPp1Ol4FdEKQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
@@ -603,7 +603,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "20.2.1", "@angular/compiler": "20.3.2",
"typescript": ">=5.8 <6.0" "typescript": ">=5.8 <6.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -613,9 +613,9 @@
} }
}, },
"node_modules/@angular/core": { "node_modules/@angular/core": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.2.tgz",
"integrity": "sha512-/hl3AkmdQ62P9ttmfULEDg9GIz7BkzhGv9bSH2ssiU3Y4ax6eM8uQXEbMxBA8OUKOvg1Q4POcNHIiJQgO5t28Q==", "integrity": "sha512-88uPgs5LjtnywnQaZE2ShBb1wa8IuD6jWs4nc4feo32QdBc55tjebTBFJSHbi3mUVAp0eS4wI6ITo0YIb01H4g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -624,7 +624,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "20.2.1", "@angular/compiler": "20.3.2",
"rxjs": "^6.5.3 || ^7.4.0", "rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
@@ -638,9 +638,9 @@
} }
}, },
"node_modules/@angular/forms": { "node_modules/@angular/forms": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.2.tgz",
"integrity": "sha512-SfkiHEIFPLtTKeaXUTpRfYnpJDxaeKiTi0YqfvzEjKE68qH0t+pQ4rL0Poch2/l4snP6JS1XzO/nDve1dk3vZw==", "integrity": "sha512-ECIbtwc7n9fPbiZXZVaoZpSiOksgcNbZ27oUN9BT7EmoXRzBw6yDL2UX6Ig7pEKhQGyBkKB+TMerRwTDVkkCWg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -649,16 +649,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "20.2.1", "@angular/common": "20.3.2",
"@angular/core": "20.2.1", "@angular/core": "20.3.2",
"@angular/platform-browser": "20.2.1", "@angular/platform-browser": "20.3.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/localize": { "node_modules/@angular/localize": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.2.tgz",
"integrity": "sha512-vemzYcHt6YX4FutpgNXiXTpKCMVaJdOG/m2+oJyvnr8KvdlrJKczXraPVY4ER+WJiHC5IQSg24otdSFc0UH2JA==", "integrity": "sha512-RZMHgLZV1Aka7rUKvQbg08Dn+dMyVBEGTlUS6/bTDoB1Xq2UE9L8YKmlnEDQyzveO5vTsPvZZQRL4iLc4IokzQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
@@ -675,20 +675,20 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "20.2.1", "@angular/compiler": "20.3.2",
"@angular/compiler-cli": "20.2.1" "@angular/compiler-cli": "20.3.2"
} }
}, },
"node_modules/@angular/material": { "node_modules/@angular/material": {
"version": "20.2.0", "version": "20.2.5",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.5.tgz",
"integrity": "sha512-lwkV1VP7PkC/dhPRXLeYaNtPaIAOjI8/zfpkPnmxJuGA7t7hkngtTxmY+6gElDAtfnle9ZJulW4KndKGr3ng/g==", "integrity": "sha512-zgmHqPykH3InEsVmNSpcVicXLWcYKHIt9Nv/J86K3NZDw4/IQgpfujnr7IotLwc9VpgI4Cl7Jbo95tFVFQAYmw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/cdk": "20.2.0", "@angular/cdk": "20.2.5",
"@angular/common": "^20.0.0 || ^21.0.0", "@angular/common": "^20.0.0 || ^21.0.0",
"@angular/core": "^20.0.0 || ^21.0.0", "@angular/core": "^20.0.0 || ^21.0.0",
"@angular/forms": "^20.0.0 || ^21.0.0", "@angular/forms": "^20.0.0 || ^21.0.0",
@@ -697,23 +697,23 @@
} }
}, },
"node_modules/@angular/material-moment-adapter": { "node_modules/@angular/material-moment-adapter": {
"version": "20.2.0", "version": "20.2.5",
"resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-20.2.5.tgz",
"integrity": "sha512-JzzUp3ZHhKkK8SNMPaDf+YUfFmR27WGzm5W4dKGwyLlMk9rXcVY1icHslzsO6ndion6QUJkKBgihpcG5jxvhvg==", "integrity": "sha512-d3unwWDlaleN82gf6EWTQxPrRp46OUIVr/5/lO1xscy5sINTZJcw2OxQikpKZvZiMb+obeKgjzUHiJuLqRJbmQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "^20.0.0 || ^21.0.0", "@angular/core": "^20.0.0 || ^21.0.0",
"@angular/material": "20.2.0", "@angular/material": "20.2.5",
"moment": "^2.18.1" "moment": "^2.18.1"
} }
}, },
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.2.tgz",
"integrity": "sha512-oxDih/A8G7W+I6oAip+sev+kebioYmzhB/NMzF8C8zx/ieVDzatJ+YeEZQt7eDaJLH94S4sIC25SPq3OFIabxg==", "integrity": "sha512-d9XcT2UuWZCc0UOtkCcPEnMcOFKNczahamT/Izg3H9jLS3IcT6l0ry23d/Xf0DRwhLYQdOZiG7l8HMZ1sWPMOg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -722,9 +722,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/animations": "20.2.1", "@angular/animations": "20.3.2",
"@angular/common": "20.2.1", "@angular/common": "20.3.2",
"@angular/core": "20.2.1" "@angular/core": "20.3.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@angular/animations": { "@angular/animations": {
@@ -733,9 +733,9 @@
} }
}, },
"node_modules/@angular/platform-browser-dynamic": { "node_modules/@angular/platform-browser-dynamic": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.2.tgz",
"integrity": "sha512-bzBeDnRZFzlA5w5q5GskuKhLgAeJ3pU0B3Ch7V2fhfaAZDOTEczBFvL7I1pcXhDg8Y/8aoz4/OwqnilKLO3FUg==", "integrity": "sha512-ehoV67Vxr3ZE8BJ3g7Q4ZLHo3qJVoDUDz/4UeCqmDeOnKxcdD53HTA/pgOO4QhKStUFbzgU19OQD4e6fkP8YoQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -744,16 +744,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "20.2.1", "@angular/common": "20.3.2",
"@angular/compiler": "20.2.1", "@angular/compiler": "20.3.2",
"@angular/core": "20.2.1", "@angular/core": "20.3.2",
"@angular/platform-browser": "20.2.1" "@angular/platform-browser": "20.3.2"
} }
}, },
"node_modules/@angular/platform-server": { "node_modules/@angular/platform-server": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.2.tgz",
"integrity": "sha512-yjos8jgHwcih9lF/CKjbKxzzc83NM+ZoIdm/XSVv9yg+QDnTsc6bLF3QZ+OChCoaCks/UtWUwyM7Ux2g/VvVFA==", "integrity": "sha512-D7tf5S5xxQQUDtw/dkMa2XePnxHwyZElN5FQP99ByiEy9PjT1iFjyKuP9jjHsI4Nmi+Juq0F1uo4azPfPaV/3w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0", "tslib": "^2.3.0",
@@ -763,17 +763,17 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "20.2.1", "@angular/common": "20.3.2",
"@angular/compiler": "20.2.1", "@angular/compiler": "20.3.2",
"@angular/core": "20.2.1", "@angular/core": "20.3.2",
"@angular/platform-browser": "20.2.1", "@angular/platform-browser": "20.3.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/router": { "node_modules/@angular/router": {
"version": "20.2.1", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.2.1.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.2.tgz",
"integrity": "sha512-f8KfG55EVnFDC9ud+MbxAP6voKi7hVQH4YaqPK0Lm6pyc1Xp0I5W25iRbg+Y1rO1csHKHauBPkUEESEuVGBGqg==", "integrity": "sha512-+Crx6QpK00juoNU3A1vbVf4DQ7fduLe3DUdAob6a9Uj+IoWj2Ijd8zUWF8E0cfNNFotJ4Gost0lJORDvqKcC7A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -782,16 +782,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "20.2.1", "@angular/common": "20.3.2",
"@angular/core": "20.2.1", "@angular/core": "20.3.2",
"@angular/platform-browser": "20.2.1", "@angular/platform-browser": "20.3.2",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/ssr": { "node_modules/@angular/ssr": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-20.3.3.tgz",
"integrity": "sha512-1IEojPGdXiqtn8ylQ1AZJVw4tgxP1Hn9pYmy9Uk8Qegof8HSbme4DHw5KOTFbnj09WGDfkETCMNVXi0MZRZAJQ==", "integrity": "sha512-DdwpwfNcoiaiaPvcm3aL+k24JWB0OOTq8/oM8HY4gAZbGNTnn8n1gTbTq3qjLt8zFtCWWqVU0+ejBgHIEvmDOw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -1093,21 +1093,21 @@
} }
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.4.5", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.0.4", "@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.4.5", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1116,9 +1116,9 @@
} }
}, },
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.0.4", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -2876,16 +2876,16 @@
} }
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.3", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz",
"integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==", "integrity": "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.4.5", "@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.4.5", "@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.0" "@tybys/wasm-util": "^0.10.1"
} }
}, },
"node_modules/@ng-bootstrap/ng-bootstrap": { "node_modules/@ng-bootstrap/ng-bootstrap": {
@@ -3181,20 +3181,10 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/@oxc-project/runtime": {
"version": "0.81.0",
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.81.0.tgz",
"integrity": "sha512-zm/LDVOq9FEmHiuM8zO4DWirv0VP2Tv2VsgaiHby9nvpq+FVrcqNYgv+TysLKOITQXWZj/roluTxFvpkHP0Iuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.81.0", "version": "0.89.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.81.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.89.0.tgz",
"integrity": "sha512-CnOqkybZK8z6Gx7Wb1qF7AEnSzbol1WwcIzxYOr8e91LytGOjo0wCpgoYWZo8sdbpqX+X+TJayIzo4Pv0R/KjA==", "integrity": "sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -3555,9 +3545,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.38.tgz",
"integrity": "sha512-Gs+313LfR4Ka3hvifdag9r44WrdKQaohya7ZXUXzARF7yx0atzFlVZjsvxtKAw1Vmtr4hB/RjUD1jf73SW7zDw==", "integrity": "sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3566,12 +3556,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.38.tgz",
"integrity": "sha512-W8oMqzGcI7wKPXUtS3WJNXzbghHfNiuM1UBAGpVb+XlUCgYRQJd2PRGP7D3WGql3rR3QEhUvSyAuCBAftPQw6Q==", "integrity": "sha512-RaoWOKc0rrFsVmKOjQpebMY6c6/I7GR1FBc25v7L/R7NlM0166mUotwGEv7vxu7ruXH4SJcFeVrfADFUUXUmmQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3580,12 +3573,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.38.tgz",
"integrity": "sha512-pM4c4sKUk37noJrnnDkJknLhCsfZu7aWyfe67bD0GQHfzAPjV16wPeD9CmQg4/0vv+5IfHYaa4VE536xbA+W0Q==", "integrity": "sha512-Ymojqc2U35iUc8NFU2XX1WQPfBRRHN6xHcrxAf9WS8BFFBn8pDrH5QPvH1tYs3lDkw6UGGbanr1RGzARqdUp1g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3594,12 +3590,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.38.tgz",
"integrity": "sha512-M8SUgFlYb5kJJWcFC8gUMRiX4WLFxPKMed3SJ2YrxontgIrEcpizPU8nLNVsRYEStoSfKHKExpQw3OP6fm+5bw==", "integrity": "sha512-0ermTQ//WzSI0nOL3z/LUWMNiE9xeM5cLGxjewPFEexqxV/0uM8/lNp9QageQ8jfc/VO1OURsGw34HYO5PaL8w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3608,12 +3607,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.38.tgz",
"integrity": "sha512-FuQpbNC/hE//bvv29PFnk0AtpJzdPdYl5CMhlWPovd9g3Kc3lw9TrEPIbL7gRPUdhKAiq6rVaaGvOnXxsa0eww==", "integrity": "sha512-GADxzVUTCTp6EWI52831A29Tt7PukFe94nhg/SUsfkI33oTiNQtPxyLIT/3oRegizGuPSZSlrdBurkjDwxyEUQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3622,12 +3624,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.38.tgz",
"integrity": "sha512-hRZygRlaGCjcNTNY9GV7dDI18sG1dK3cc7ujHq72LoDad23zFDUGMQjiSxHWK+/r92iMV+j2MiHbvzayxqynsg==", "integrity": "sha512-SKO7Exl5Yem/OSNoA5uLHzyrptUQ8Hg70kHDxuwEaH0+GUg+SQe9/7PWmc4hFKBMrJGdQtii8WZ0uIz9Dofg5Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3636,12 +3641,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.38.tgz",
"integrity": "sha512-HzgT6h+CXLs+GKAU0Wvkt3rvcv0CmDBsDjlPhh4GHysOKbG9NjpKYX2zvjx671E9pGbTvcPpwy7gGsy7xpu+8g==", "integrity": "sha512-SOo6+WqhXPBaShLxLT0eCgH17d3Yu1lMAe4mFP0M9Bvr/kfMSOPQXuLxBcbBU9IFM9w3N6qP9xWOHO+oUJvi8Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3650,12 +3658,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.38.tgz",
"integrity": "sha512-Ab/wbf6gdzphDbsg51UaxsC93foQ7wxhtg0SVCXd25BrV4MAJ1HoDtKN/f4h0maFmJobkqYub2DlmoasUzkvBg==", "integrity": "sha512-yvsQ3CyrodOX+lcoi+lejZGCOvJZa9xTsNB8OzpMDmHeZq3QzJfpYjXSAS6vie70fOkLVJb77UqYO193Cl8XBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3664,12 +3675,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.38.tgz",
"integrity": "sha512-VoxqGEfh5A1Yx+zBp/FR5QwAbtzbuvky2SVc+ii4g1gLD4zww6mt/hPi5zG+b88zYPFBKHpxMtsz9cWqXU5V5Q==", "integrity": "sha512-84qzKMwUwikfYeOuJ4Kxm/3z15rt0nFGGQArHYIQQNSTiQdxGHxOkqXtzPFqrVfBJUdxBAf+jYzR1pttFJuWyg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3678,12 +3692,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.38.tgz",
"integrity": "sha512-qZ1ViyOUDGbiZrSAJ/FIAhYUElDfVxxFW6DLT/w4KeoZN3HsF4jmRP95mXtl51/oGrqzU9l9Q2f7/P4O/o2ZZA==", "integrity": "sha512-QrNiWlce01DYH0rL8K3yUBu+lNzY+B0DyCbIc2Atan6/S6flxOL0ow5DLQvMamOI/oKhrJ4xG+9MkMb9dDHbLQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3692,12 +3709,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"openharmony" "openharmony"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.38.tgz",
"integrity": "sha512-hEkG3wD+f3wytV0lqwb/uCrXc4r4Ny/DWJFJPfQR3VeMWplhWGgSHNwZc2Q7k86Yi36f9NNzzWmrIuvHI9lCVw==", "integrity": "sha512-fnLtHyjwEsG4/aNV3Uv3Qd1ZbdH+CopwJNoV0RgBqrcQB8V6/Qdikd5JKvnO23kb3QvIpP+dAMGZMv1c2PJMzw==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -3705,16 +3725,16 @@
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@napi-rs/wasm-runtime": "^1.0.3" "@napi-rs/wasm-runtime": "^1.0.5"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.38.tgz",
"integrity": "sha512-k3MvDf8SiA7uP2ikP0unNouJ2YCrnwi7xcVW+RDgMp5YXVr3Xu6svmT3HGn0tkCKUuPmf+uy8I5uiHt5qWQbew==", "integrity": "sha512-19cTfnGedem+RY+znA9J6ARBOCEFD4YSjnx0p5jiTm9tR6pHafRfFIfKlTXhun+NL0WWM/M0eb2IfPPYUa8+wg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3723,12 +3743,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-win32-ia32-msvc": { "node_modules/@rolldown/binding-win32-ia32-msvc": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.38.tgz",
"integrity": "sha512-wAi/FxGh7arDOUG45UmnXE1sZUa0hY4cXAO2qWAjFa3f7bTgz/BqwJ7XN5SUezvAJPNkME4fEpInfnBvM25a0w==", "integrity": "sha512-HcICm4YzFJZV+fI0O0bFLVVlsWvRNo/AB9EfUXvNYbtAxakCnQZ15oq22deFdz6sfi9Y4/SagH2kPU723dhCFA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -3737,12 +3760,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.38.tgz",
"integrity": "sha512-Ej0i4PZk8ltblZtzVK8ouaGUacUtxRmTm5S9794mdyU/tYxXjAJNseOfxrnHpMWKjMDrOKbqkPqJ52T9NR4LQQ==", "integrity": "sha512-4Qx6cgEPXLb0XsCyLoQcUgYBpfL0sjugftob+zhUH0EOk/NVCAIT+h0NJhY+jn7pFpeKxhNMqhvTNx3AesxIAQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3751,12 +3777,15 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
"integrity": "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==", "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4041,14 +4070,14 @@
] ]
}, },
"node_modules/@schematics/angular": { "node_modules/@schematics/angular": {
"version": "20.2.0", "version": "20.3.3",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.3.tgz",
"integrity": "sha512-7sZVj7hOcytQrPE17ixjzul9ih81IfXGcEZvr7fT77qy7Hm5rbMjxqSYxCTf3kAyBFRSLq/E8GTapPAjk2coOg==", "integrity": "sha512-lqIP1pNKp8yaqd663R3graZWaTBjXH+Cl72BQl1Ghl7lFGReZJALr4GiSMiBR9r30Epklcw5TwOSi+Bs4UKmbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "20.2.0", "@angular-devkit/core": "20.3.3",
"@angular-devkit/schematics": "20.2.0", "@angular-devkit/schematics": "20.3.3",
"jsonc-parser": "3.3.1" "jsonc-parser": "3.3.1"
}, },
"engines": { "engines": {
@@ -4195,9 +4224,9 @@
} }
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.0", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -4519,9 +4548,9 @@
} }
}, },
"node_modules/ansis": { "node_modules/ansis": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
"integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
@@ -4957,9 +4986,9 @@
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.6.0", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5877,10 +5906,13 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.6", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@@ -6804,6 +6836,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/karma": { "node_modules/karma": {
"version": "6.4.4", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
@@ -8683,35 +8724,37 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-beta.32", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.32.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.38.tgz",
"integrity": "sha512-vxI2sPN07MMaoYKlFrVva5qZ1Y7DAZkgp7MQwTnyHt4FUMz9Sh+YeCzNFV9JYHI6ZNwoGWLCfCViE3XVsRC1cg==", "integrity": "sha512-58frPNX55Je1YsyrtPJv9rOSR3G5efUZpRqok94Efsj0EUa8dnqJV3BldShyI7A+bVPleucOtzXHwVpJRcR0kQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/runtime": "=0.81.0", "@oxc-project/types": "=0.89.0",
"@oxc-project/types": "=0.81.0", "@rolldown/pluginutils": "1.0.0-beta.38",
"@rolldown/pluginutils": "1.0.0-beta.32",
"ansis": "^4.0.0" "ansis": "^4.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
}, },
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-beta.32", "@rolldown/binding-android-arm64": "1.0.0-beta.38",
"@rolldown/binding-darwin-arm64": "1.0.0-beta.32", "@rolldown/binding-darwin-arm64": "1.0.0-beta.38",
"@rolldown/binding-darwin-x64": "1.0.0-beta.32", "@rolldown/binding-darwin-x64": "1.0.0-beta.38",
"@rolldown/binding-freebsd-x64": "1.0.0-beta.32", "@rolldown/binding-freebsd-x64": "1.0.0-beta.38",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.32", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.38",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.32", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.38",
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.32", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.38",
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.32", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.38",
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.32", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.38",
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.32", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.38",
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.32", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.38",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.32", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.38",
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.32", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.38",
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.32" "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.38"
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
@@ -9890,18 +9933,18 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.2", "version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.5.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.43.0", "rollup": "^4.43.0",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.15"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -9964,6 +10007,23 @@
} }
} }
}, },
"node_modules/vite/node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/void-elements": { "node_modules/void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",

View File

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

View File

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

View File

@@ -6,9 +6,10 @@ import {MatIcon} from "@angular/material/icon";
import {MatToolbar} from "@angular/material/toolbar"; import {MatToolbar} from "@angular/material/toolbar";
import {filter, map, Subscription} from "rxjs"; import {filter, map, Subscription} from "rxjs";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {UserService} from "./authentication/user.service"; // import {UserService} from "./authentication/user.service";
import {TournamentService} from "./service/tournament.service"; import {TournamentService} from "./service/tournament.service";
import {HeaderService} from "./service/header.service"; import {HeaderService} from "./service/header.service";
import {AuthService} from "./auth/auth.service";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -19,11 +20,11 @@ import {HeaderService} from "./service/header.service";
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
header: string; header: string;
user: string; // user: string;
userSubscription: Subscription; // userSubscription: Subscription;
constructor( constructor(
protected userService: UserService, protected authService: AuthService,
private router: Router, private router: Router,
protected activatedRoute: ActivatedRoute, protected activatedRoute: ActivatedRoute,
private tournamentService: TournamentService, private tournamentService: TournamentService,
@@ -32,7 +33,7 @@ export class AppComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser); // this.userSubscription = this.userService.currentUser.subscribe(newUser => this.user = newUser);
this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof NavigationEnd), filter(event => event instanceof NavigationEnd),
@@ -55,8 +56,8 @@ export class AppComponent implements OnInit {
} }
logOut() { logOut() {
this.userService.removeUser(); this.authService.logout();
this.router.navigate(['/auth/login']); this.router.navigate(['/login']);
} }
addTestData() { addTestData() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,9 @@ import {MatGridList, MatGridTile} from "@angular/material/grid-list";
import {Round} from "../../model/round"; import {Round} from "../../model/round";
import {Group} from "../../model/group"; import {Group} from "../../model/group";
import {Game} from "../../model/game"; import {Game} from "../../model/game";
import {TeamDisplayComponent} from "../team-display/team-display.component";
import {Event} from "../../model/event";
import {Tournament} from "../../model/tournament";
@Component({ @Component({
selector: 'app-match-result', selector: 'app-match-result',
@@ -28,14 +31,14 @@ import {Game} from "../../model/game";
MatButton, MatButton,
MatDialogClose, MatDialogClose,
MatDialogTitle, MatDialogTitle,
TeamPipe,
MatInput, MatInput,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
MatFormField, MatFormField,
MatGridList, MatGridList,
MatGridTile, MatGridTile,
NgClass NgClass,
TeamDisplayComponent
], ],
providers: [ providers: [
FullNamePipe, FullNamePipe,
@@ -48,8 +51,9 @@ import {Game} from "../../model/game";
export class MatchResultComponent { export class MatchResultComponent {
result: Result = new Result(); result: Result = new Result();
isValidResult: number = 0;
constructor(@Inject(MAT_DIALOG_DATA) public data: {match: Match, group: Group, round: Round}) { constructor(@Inject(MAT_DIALOG_DATA) public data: {match: Match, tournament: Tournament, event: Event, group: Group, round: Round}) {
this.result.matchId = this.data.match.id; this.result.matchId = this.data.match.id;
if (data.match.games.length == 0) { if (data.match.games.length == 0) {
@@ -64,6 +68,8 @@ export class MatchResultComponent {
this.result.games.push(new Game()); this.result.games.push(new Game());
} }
} }
this.validateResult();
} }
readonly dialogRef = inject(MatDialogRef<MatchResultComponent>); readonly dialogRef = inject(MatDialogRef<MatchResultComponent>);
@@ -76,13 +82,18 @@ export class MatchResultComponent {
this.result.games[game - 1].score2 = 21; this.result.games[game - 1].score2 = 21;
} }
this.validateResult();
} }
onAnnulerenClick() { onAnnulerenClick() {
this.dialogRef.close(); this.dialogRef.close();
} }
validateResult(): number { complementScores() {
// console.log("in complementScores");
}
validateResult(): void {
let valid : boolean = true; let valid : boolean = true;
valid &&= this.gameValid(this.result.games[0].score1, this.result.games[0].score2); valid &&= this.gameValid(this.result.games[0].score1, this.result.games[0].score2);
valid &&= this.gameValid(this.result.games[1].score1, this.result.games[1].score2); valid &&= this.gameValid(this.result.games[1].score1, this.result.games[1].score2);
@@ -91,7 +102,7 @@ export class MatchResultComponent {
valid &&= this.gameValid(this.result.games[2].score1, this.result.games[2].score2); valid &&= this.gameValid(this.result.games[2].score1, this.result.games[2].score2);
} }
return valid ? this.matchResult(this.result) : 0; this.isValidResult = valid ? this.matchResult(this.result) : 0;
} }
gameValid(score1: number, score2: number): boolean { gameValid(score1: number, score2: number): boolean {
@@ -114,29 +125,31 @@ export class MatchResultComponent {
} }
matchResult(result: Result): number { matchResult(result: Result): number {
let gameBalance = 0; let team1Wins = 0;
if (result.games[0].score1 < result.games[0].score2) { let team2Wins = 0;
gameBalance--;
} else { // Count wins for games 1 and 2
gameBalance++; if (result.games[0].score1 > result.games[0].score2) team1Wins++;
else team2Wins++;
if (result.games[1].score1 > result.games[1].score2) team1Wins++;
else team2Wins++;
// If match is already decided (2-0), game 3 shouldn't have scores
if (Math.max(team1Wins, team2Wins) == 2) {
if (result.games[2].score1 != undefined || result.games[2].score2 != undefined) {
return 0; // Invalid
} }
if (result.games[1].score1 < result.games[1].score2) { return team1Wins > team2Wins ? 1 : -1;
gameBalance--;
} else {
gameBalance++;
}
if (Math.abs(gameBalance) == 2 && (result.games[2].score1 != undefined || result.games[2].score2 != undefined)) {
return 0;
} }
// Match is 1-1, check game 3
if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) { if (result.games[2].score1 != undefined && result.games[2].score2 != undefined) {
if (result.games[2].score1 < result.games[2].score2) { if (result.games[2].score1 > result.games[2].score2) team1Wins++;
gameBalance--; else team2Wins++;
} else { return team1Wins > team2Wins ? 1 : -1;
gameBalance++; }
}
} return 0; // Incomplete
return Math.sign(gameBalance);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,6 @@ export class PlayerEditComponent implements OnInit {
player: Player; player: Player;
isEditMode: boolean = false; isEditMode: boolean = false;
constructor( constructor(
private playerService: PlayerService, private playerService: PlayerService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,13 +8,14 @@ import {ActivatedRoute, Router} from "@angular/router";
import {DecimalPipe} from "@angular/common"; import {DecimalPipe} from "@angular/common";
import {TeamPipe} from "../../pipes/team-pipe"; import {TeamPipe} from "../../pipes/team-pipe";
import {FullNamePipe} from "../../pipes/fullname-pipe"; import {FullNamePipe} from "../../pipes/fullname-pipe";
import {TeamDisplayComponent} from "../team-display/team-display.component";
@Component({ @Component({
selector: 'app-round-overview', selector: 'app-round-overview',
imports: [ imports: [
TeamPipe, TeamPipe,
DecimalPipe, DecimalPipe,
FullNamePipe TeamDisplayComponent
], ],
providers: [ providers: [
TeamPipe, TeamPipe,

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,13 @@
</ng-template> </ng-template>
<app-tournament-validate></app-tournament-validate> <app-tournament-validate></app-tournament-validate>
</mat-tab> </mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>settings</mat-icon>
&nbsp;Beheer
</ng-template>
<app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab>
} }
@if (tournament.status == 'DIVIDED') { @if (tournament.status == 'DIVIDED') {
@@ -97,6 +104,13 @@
} }
} }
</mat-tab> </mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>settings</mat-icon>
&nbsp;Beheer
</ng-template>
<app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab>
} }
@if (tournament.status == 'ONGOING') { @if (tournament.status == 'ONGOING') {
@@ -108,30 +122,49 @@
<span class="badge text-bg-success">{{ this.activeMatches().length }}</span> <span class="badge text-bg-success">{{ this.activeMatches().length }}</span>
} }
</ng-template> </ng-template>
<div class="tab-content-wrapper">
@if (this.activeMatches().length > 0) { @if (this.activeMatches().length > 0) {
<h6 class="mt-3">Actieve wedstrijden</h6> <h6 class="mt-3"></h6>
@for (activeMatch of this.activeMatches(); track activeMatch.match.id) { @for (activeMatch of this.activeMatches(); track activeMatch.match.id) {
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<div class="col-md-2">Baan {{ activeMatch.match.court }}</div> <div class="col-md-2 d-flex align-items-center">Baan {{ activeMatch.match.court }}</div>
<div class="col-md-3">{{ activeMatch.match.team1 | teamText }}</div> <div class="col-md-3">
<div class="col-md-1">-</div> <app-team-display
<div class="col-md-3">{{ activeMatch.match.team2 | teamText }}</div> [team]="activeMatch.match.team1"
<div class="col-md-3">{{ activeMatch.group.name }} {{ activeMatch.round.name }}</div> [event]="activeMatch.event"
[tournament]="this.tournament"
[inline]="false">
</app-team-display>
</div>
<div class="col-md-1 d-flex align-items-center">-</div>
<div class="col-md-3">
<app-team-display
[team]="activeMatch.match.team2"
[event]="activeMatch.event"
[tournament]="this.tournament"
[inline]="false">
</app-team-display>
</div>
<div class="col-md-3 d-flex align-items-center">{{ activeMatch.group.name }} {{ activeMatch.round.name }}</div>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="row"> <div class="row">
<hr/>
<div class="col-md-3">Teller: {{ activeMatch.match.counter | fullName }}</div> <div class="col-md-3">Teller: {{ activeMatch.match.counter | fullName }}</div>
<div class="col-md-5"></div> <div class="col-md-5"></div>
<div class="col-md-2">Starttijd: {{ activeMatch.match.startTime | date: 'HH:mm' }}</div> <div class="col-md-2">Starttijd: {{ activeMatch.match.startTime | date: 'HH:mm' }}</div>
<div class="col-md-2">Duur: {{ getDuration(activeMatch.match.startTime) | date: 'mm:ss' }}</div> <div class="col-md-2">Duur: {{ getDuration(activeMatch.match.startTime) | date: 'mm:ss' }}</div>
</div> </div>
<mat-action-row> <mat-action-row>
<button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.group, activeMatch.round)"> <button class="align-baseline" mat-button (click)="editResult(activeMatch.match, activeMatch.event, activeMatch.group, activeMatch.round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag invoeren Uitslag invoeren
</button> </button>
<button mat-button (click)="changeCounter(activeMatch.match)">
<mat-icon>person</mat-icon>
Teller wijzigen
</button>
<button mat-button (click)="stopMatch(activeMatch.match)"> <button mat-button (click)="stopMatch(activeMatch.match)">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
Wedstrijd stoppen Wedstrijd stoppen
@@ -143,6 +176,7 @@
} @else { } @else {
<h6 class="mt-3">Geen actieve wedstrijden</h6> <h6 class="mt-3">Geen actieve wedstrijden</h6>
} }
</div>
</mat-tab> </mat-tab>
} }
@if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') { @if (tournament.status == 'ONGOING' || tournament.status == 'DRAWN') {
@@ -212,17 +246,19 @@
<mat-icon>print</mat-icon> <mat-icon>print</mat-icon>
Wedstrijdbriefjes printen Wedstrijdbriefjes printen
</button> </button>
@if (!round.isFinalsRound) {
<button mat-menu-item (click)="printRoundOverview(round)"> <button mat-menu-item (click)="printRoundOverview(round)">
<mat-icon>print</mat-icon> <mat-icon>print</mat-icon>
Rondeoverzicht printen Rondeoverzicht printen
</button> </button>
}
@if (round.status == 'IN_PROGRESS' && checkRoundComplete(round)) { @if (round.status == 'IN_PROGRESS' && checkRoundComplete(round)) {
<button mat-menu-item (click)="finishRound(round)"> <button mat-menu-item (click)="finishRound(round)">
<mat-icon>check</mat-icon> <mat-icon>check</mat-icon>
Ronde afsluiten Ronde afsluiten
</button> </button>
} }
@if (group.status != 'FINISHED' && round.status == 'FINISHED' && (roundIndex + 1) == group.rounds.length) { @if (group.status != 'FINISHED' && round.status == 'FINISHED' && (roundIndex + 1) == group.rounds.length && !round.isFinalsRound) {
<button mat-menu-item (click)="newRound(group)"> <button mat-menu-item (click)="newRound(group)">
<mat-icon>playlist_add</mat-icon> <mat-icon>playlist_add</mat-icon>
Nieuwe ronde Nieuwe ronde
@@ -238,15 +274,29 @@
<tbody> <tbody>
@for (match of round.matches; track match.id) { @for (match of round.matches; track match.id) {
<tr> <tr>
<td class="align-middle w-team">{{ match.team1 | teamText }}</td> <td class="align-middle w-team">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team">{{ match.team2 | teamText }}</td> <td class="align-middle w-team">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-fill"></td> <td class="align-middle w-fill"></td>
</tr> </tr>
} }
@if (round.drawnOut) { @if (round.drawnOut) {
<tr> <tr>
<td class="align-middle w-100" colspan="4"><b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}</td> <td class="align-middle w-100" colspan="4">
<b>Deze ronde uitgeloot:</b> {{ round.drawnOut | teamText }}
</td>
</tr> </tr>
} }
</tbody> </tbody>
@@ -256,23 +306,48 @@
<tbody> <tbody>
@for (match of round.matches; track match.id) { @for (match of round.matches; track match.id) {
<tr> <tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">{{ match.team2 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-fill"> <td class="align-middle w-fill">
@if (match.status == 'NOT_STARTED') { @if (match.status == 'NOT_STARTED') {
@if (match.canStart) {
<button mat-button (click)="startMatch(match)"> <button mat-button (click)="startMatch(match)">
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
Wedstrijd starten Wedstrijd starten
</button> </button>
} @else {
<span [matTooltip]="match.cantStartReason" style="cursor: not-allowed;">
<button mat-button disabled>
<mat-icon>play_arrow</mat-icon>
Wedstrijd starten
</button>
</span>
}
} @else if (match.status == 'IN_PROGRESS') { } @else if (match.status == 'IN_PROGRESS') {
<button mat-button (click)="editResult(match, group, round)"> <button mat-button (click)="editResult(match, event, group, round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag invoeren Uitslag
</button>
<button mat-button (click)="changeCounter(match)">
<mat-icon>person</mat-icon>
Teller
</button> </button>
<button mat-button (click)="stopMatch(match)"> <button mat-button (click)="stopMatch(match)">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
Wedstrijd stoppen Stoppen
</button> </button>
} @else if (match.status == 'FINISHED') { } @else if (match.status == 'FINISHED') {
<div class="row result align-items-center"> <div class="row result align-items-center">
@@ -287,8 +362,8 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<mat-menu #finishedMatchMenu="matMenu"> <mat-menu #finishedMatchMenu="matMenu">
<button mat-menu-item (click)="editResult(match, group, round)"> <button mat-menu-item (click)="editResult(match, event, group, round)">
<mat-icon>edit</mat-icon> <mat-icon>leaderboard</mat-icon>
Uitslag bewerken Uitslag bewerken
</button> </button>
</mat-menu> </mat-menu>
@@ -309,9 +384,21 @@
<tbody> <tbody>
@for (match of round.matches; track match.id) { @for (match of round.matches; track match.id) {
<tr> <tr>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">{{ match.team1 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 1}">
<app-team-display
[team]="match.team1"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-sep">-</td> <td class="align-middle w-sep">-</td>
<td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">{{ match.team2 | teamText }}</td> <td class="align-middle w-team" [ngClass]="{'winner': checkWinner(match) == 2}">
<app-team-display
[team]="match.team2"
[event]="event"
[tournament]="tournament">
</app-team-display>
</td>
<td class="align-middle w-fill"> <td class="align-middle w-fill">
<div class="row result align-items-center"> <div class="row result align-items-center">
@for (game of match.games; track game.id) { @for (game of match.games; track game.id) {
@@ -358,7 +445,14 @@
@for (entry of getStandingsForRound(round, group).entries; track entry.position) { @for (entry of getStandingsForRound(round, group).entries; track entry.position) {
<tr> <tr>
<td class="align-middle">{{ entry.position }}</td> <td class="align-middle">{{ entry.position }}</td>
<td class="align-middle">{{ entry.team | teamText }}</td> <td class="align-middle">
<app-team-display
[team]="entry.team"
[event]="event"
[tournament]="this.tournament"
[inline]="true">
</app-team-display>
</td>
<td class="align-middle">{{ entry.played }}</td> <td class="align-middle">{{ entry.played }}</td>
<td class="align-middle"> <td class="align-middle">
@if (entry.played > 0 ) { @if (entry.played > 0 ) {
@@ -379,6 +473,25 @@
} }
</tbody> </tbody>
</table> </table>
} @else if (round.isFinalsRound && round.status == 'FINISHED') {
<h6 class="mt-3">Uitslag</h6>
<table class="table w-50 m-4">
<tbody>
<tr>
<td>1e Plaats</td>
<td>{{ (checkWinner(round.matches[0]) == 1 ? round.matches[0].team1 : round.matches[0].team2) | teamText }}</td>
</tr>
<tr>
<td>2e Plaats</td>
<td>{{ (checkWinner(round.matches[0]) == 1 ? round.matches[0].team2 : round.matches[0].team1) | teamText }}</td>
</tr>
<tr>
<td>3e Plaats</td>
<td>{{ (checkWinner(round.matches[1]) == 1 ? round.matches[1].team1 : round.matches[1].team2) | teamText }}</td>
</tr>
</tbody>
</table>
} }
</mat-tab> </mat-tab>
} }
@@ -394,16 +507,8 @@
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
&nbsp;Beheer &nbsp;Beheer
</ng-template> </ng-template>
<mat-tab-group animationDuration="0ms" disableRipple="true">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>group</mat-icon>
&nbsp;Spelerslijst
</ng-template>
<app-tournament-players [tournament]="tournament"></app-tournament-players> <app-tournament-players [tournament]="tournament"></app-tournament-players>
</mat-tab> </mat-tab>
</mat-tab-group>
</mat-tab>
} }
</mat-tab-group> </mat-tab-group>
</mat-card-content> </mat-card-content>

View File

@@ -35,7 +35,10 @@ td.w-fill {
width: 95% !important; width: 95% !important;
} }
.mat-menu-panel { .mat-menu-panel {
z-index: 1000 !important; z-index: 1000 !important;
} }
.tab-content-wrapper {
margin: 0 4px 0 4px;
}

View File

@@ -1,7 +1,8 @@
import {Component, inject, Input, OnDestroy, OnInit} from '@angular/core'; import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import { import {
MatAccordion, MatAccordion,
MatExpansionPanel, MatExpansionPanelActionRow, MatExpansionPanel,
MatExpansionPanelActionRow,
MatExpansionPanelHeader, MatExpansionPanelHeader,
MatExpansionPanelTitle MatExpansionPanelTitle
} from "@angular/material/expansion"; } from "@angular/material/expansion";
@@ -16,7 +17,7 @@ import {MatButton, MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {Group} from "../../model/group"; import {Group} from "../../model/group";
import {Round} from "../../model/round"; import {Round} from "../../model/round";
import {MatMenu, MatMenuContent, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {Match} from "../../model/match"; import {Match} from "../../model/match";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MatTab, MatTabChangeEvent, MatTabGroup, MatTabLabel} from "@angular/material/tabs"; import {MatTab, MatTabChangeEvent, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
@@ -26,14 +27,16 @@ import {MatchResultPipe} from "../../pipes/match-result-pipe";
import {Event} from "../../model/event"; import {Event} from "../../model/event";
import {TournamentValidateComponent} from "../tournament-validate/tournament-validate.component"; import {TournamentValidateComponent} from "../tournament-validate/tournament-validate.component";
import {Player, Strength} from "../../model/player"; import {Player, Strength} from "../../model/player";
import {MatSlideToggleChange} from "@angular/material/slide-toggle";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {CourtSelectionComponent} from "../court-selection/court-selection.component"; import {CourtSelectionComponent} from "../court-selection/court-selection.component";
import {Standings} from "../../model/standings"; import {Standings} from "../../model/standings";
import {HeaderService} from "../../service/header.service"; import {HeaderService} from "../../service/header.service";
import {TournamentPlayersComponent} from "../tournament-players/tournament-players.component"; import {TournamentPlayersComponent} from "../tournament-players/tournament-players.component";
import {TournamentPlayer} from "../../model/tournamentPlayer"; import {TournamentPlayer} from "../../model/tournamentPlayer";
import {MatTooltip} from "@angular/material/tooltip"; import {TeamDisplayComponent} from "../team-display/team-display.component";
import {CounterSelectionComponent} from "../counter-selection/counter-selection.component";
import {Observable, Subscription, tap} from "rxjs";
import {MatTooltip} from '@angular/material/tooltip';
@Component({ @Component({
selector: 'app-tournament-manage', selector: 'app-tournament-manage',
@@ -63,6 +66,8 @@ import {MatTooltip} from "@angular/material/tooltip";
TournamentValidateComponent, TournamentValidateComponent,
TournamentPlayersComponent, TournamentPlayersComponent,
MatExpansionPanelActionRow, MatExpansionPanelActionRow,
TeamDisplayComponent,
MatTooltip,
], ],
providers: [ providers: [
FullNamePipe, FullNamePipe,
@@ -95,10 +100,10 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
this.activeRoundTab = params['tab']; this.activeRoundTab = params['tab'];
} }
}) })
this.tournamentService.getById(Number(id)).subscribe(data => { this.loadTournament(
this.tournament = data; this.tournamentService.getById(Number(id)),
this.headerService.setTitle(this.tournament.name); (tournament) => this.headerService.setTitle(tournament.name)
}); );
} }
ngOnDestroy() { ngOnDestroy() {
@@ -149,56 +154,42 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
} }
startRound(round: Round) { startRound(round: Round) {
this.tournamentService.startRound(this.tournament.id, round.id).subscribe(data => { this.loadTournament(this.tournamentService.startRound(this.tournament.id, round.id));
this.tournament = data;
});
} }
finishRound(round: Round) { finishRound(round: Round) {
this.tournamentService.finishRound(this.tournament.id, round.id).subscribe(data => { this.loadTournament(this.tournamentService.finishRound(this.tournament.id, round.id));
this.tournament = data;
});
} }
finishGroup(group: Group) { finishGroup(group: Group) {
this.tournamentService.finishGroup(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.finishGroup(this.tournament.id, group.id));
this.tournament = data;
});
} }
reopenGroup(group: Group) { reopenGroup(group: Group) {
this.tournamentService.reopenGroup(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.reopenGroup(this.tournament.id, group.id));
this.tournament = data;
});
} }
divideTournament() { divideTournament() {
this.tournamentService.divide(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.divide(this.tournament.id));
this.tournament = data;
});
} }
clearDivision() { clearDivision() {
this.tournamentService.clearDivision(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.clearDivision(this.tournament.id));
this.tournament = data;
});
} }
drawTournament() { drawTournament() {
this.tournamentService.draw(this.tournament.id).subscribe(data => { this.loadTournament(this.tournamentService.draw(this.tournament.id));
this.tournament = data;
});
} }
startMatch(match: Match) { startMatch(match: Match) {
const availableCourts = this.getAvailableCourts(); const availableCourts = this.getAvailableCourts();
if (availableCourts.length == 0) { if (availableCourts.length == 0) {
alert('Geen banen beschikbaar!'); this._snackBar.open('Geen banen beschikbaar.')
} else if (this.matchContainsPlayersThatArePlaying(match)) { } else if (this.matchContainsPlayersThatArePlaying(match)) {
alert('Deze wedstrijd bevat spelers die al aan het spelen zijn!'); this._snackBar.open('Deze wedstrijd bevat spelers die al aan het spelen zijn.')
} else if (this.matchContainsPlayersThatAreCounting(match)) { } else if (this.matchContainsPlayersThatAreCounting(match)) {
alert('Deze wedstrijd bevat spelers die aan het tellen zijn!'); this._snackBar.open('Deze wedstrijd bevat spelers die aan het tellen zijn.')
} else { } else {
this.courtSelectionDialog.open(CourtSelectionComponent, { this.courtSelectionDialog.open(CourtSelectionComponent, {
data: { data: {
@@ -211,46 +202,72 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
minHeight: '250px' minHeight: '250px'
}).afterClosed().subscribe(result => { }).afterClosed().subscribe(result => {
if (result != undefined) { if (result != undefined) {
console.log('Start match on court ' + result.court + ' with counter ' + result.counter.name); this.loadTournament(this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId));
this.tournamentService.startMatch(this.tournament.id, match.id, result.court, result.counter.playerId).subscribe(data => {
this.tournament = data;
});
} }
}); });
} }
} }
matchContainsPlayersThatArePlaying(match: Match): boolean { matchContainsPlayersThatArePlaying(match: Match): boolean {
let activePlayers: number[] = [];
for (let activeMatch of this.activeMatches()) {
activePlayers.push(activeMatch.match.team1.player1.id);
if (activeMatch.match.team1.player2) activePlayers.push(activeMatch.match.team1.player2.id);
activePlayers.push(activeMatch.match.team2.player1.id);
if (activeMatch.match.team2.player2) activePlayers.push(activeMatch.match.team2.player2.id);
}
let matchPlayers: number[] = []; let matchPlayers: number[] = [];
matchPlayers.push(match.team1.player1.id); matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(match.team1.player2.id); if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(match.team2.player1.id); matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(match.team2.player2.id); if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
let playersThatArePlaying = activePlayers.filter(Set.prototype.has, new Set(matchPlayers));
return playersThatArePlaying.length > 0; let matchPlayersThatArePlaying = this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers));
return matchPlayersThatArePlaying.length > 0;
}
matchPlayersThatArePlaying(match: Match): number[] {
let matchPlayers: number[] = [];
matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
return this.tournament.playersPlaying.filter(Set.prototype.has, new Set(matchPlayers));
} }
matchContainsPlayersThatAreCounting(match: Match): boolean { matchContainsPlayersThatAreCounting(match: Match): boolean {
let currentCounters: number[] = []; let matchPlayers: number[] = [];
for (let activeMatch of this.activeMatches()) { matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
currentCounters.push(activeMatch.match.counter.id); if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
let matchPlayersThatAreCounting = this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers));
return matchPlayersThatAreCounting.length > 0;
} }
matchPlayersThatAreCounting(match: Match): number[] {
let matchPlayers: number[] = []; let matchPlayers: number[] = [];
matchPlayers.push(match.team1.player1.id);
if (match.team1.player2) matchPlayers.push(match.team1.player2.id);
matchPlayers.push(match.team2.player1.id);
if (match.team2.player2) matchPlayers.push(match.team2.player2.id);
let playersThatAreCounting = currentCounters.filter(Set.prototype.has, new Set(matchPlayers)); matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
return playersThatAreCounting.length > 0; if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
return this.tournament.playersCounting.filter(Set.prototype.has, new Set(matchPlayers));
}
getTournamentPlayerFromPlayer(player: Player): TournamentPlayer {
for (let tournamentPlayer of this.tournament.tournamentPlayers) {
if (tournamentPlayer.playerId == player.id) {
return tournamentPlayer;
}
}
return null!;
}
getTournamentPlayerFromId(tournamentPlayerId: number): TournamentPlayer {
for (let tournamentPlayer of this.tournament.tournamentPlayers) {
if (tournamentPlayer.id == tournamentPlayerId) {
return tournamentPlayer;
}
}
return null!;
} }
getAvailableCourts(): number[] { getAvailableCourts(): number[] {
@@ -264,40 +281,41 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
} }
getAvailableCounters(match: Match): TournamentPlayer[] { getAvailableCounters(match: Match): TournamentPlayer[] {
const activePlayerIds = new Set( const matchPlayers: number[] = [];
this.activeMatches().flatMap(activeMatch => [ matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player1, match.type).id);
activeMatch.match.team1.player1.id, if (match.team1.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team1.player2, match.type).id);
activeMatch.match.team1.player2?.id, matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player1, match.type).id);
activeMatch.match.team2.player1.id, if (match.team2.player2) matchPlayers.push(this.getPlayerOrSubstitute(match.team2.player2, match.type).id);
activeMatch.match.team2.player2?.id
].filter(id => id !== undefined))
);
const playerIdsInMatchToBeStarted = new Set([ const counterIds = this.tournament.playersAvailable.filter(player => !matchPlayers.includes(player));
match.team1.player1.id,
match.team1.player2?.id,
match.team2.player1.id,
match.team2.player2?.id
].filter(id => id !== undefined));
return this.tournament.tournamentPlayers.filter( return counterIds.map(id => this.getTournamentPlayerFromId(id));
player => }
!player.counting
&& !activePlayerIds.has(player.playerId) getPlayerOrSubstitute(player: Player, type: String): TournamentPlayer {
&& !playerIdsInMatchToBeStarted.has(player.playerId) return this.getSubstituteForEvent(player, type) || this.getTournamentPlayerFromPlayer(player);
}
getSubstituteForEvent(player: Player, type: String): TournamentPlayer | undefined {
const tournamentPlayer = this.tournament.tournamentPlayers.find(
tp => tp.playerId === player.id
); );
if (!tournamentPlayer) return undefined;
const substitution = tournamentPlayer.substitutions.find(
s => s.event === type
);
if (!substitution) return undefined;
return this.getTournamentPlayerFromId(substitution.substitute);
} }
stopMatch(match: Match) { stopMatch(match: Match) {
this.tournamentService.stopMatch(this.tournament.id, match.id).subscribe(data => { this.loadTournament(this.tournamentService.stopMatch(this.tournament.id, match.id));
this.tournament = data;
})
} }
newRound(group: Group) { newRound(group: Group) {
this.tournamentService.newRound(this.tournament.id, group.id).subscribe(data => { this.loadTournament(this.tournamentService.newRound(this.tournament.id, group.id));
this.tournament = data;
})
} }
getStrength(strength: string | undefined) { getStrength(strength: string | undefined) {
@@ -323,7 +341,7 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
for (const round of group.rounds) { for (const round of group.rounds) {
for (const match of round.matches) { for (const match of round.matches) {
if (match.status == 'IN_PROGRESS') { if (match.status == 'IN_PROGRESS') {
matches.push(new ActiveMatch(match, round, group)); matches.push(new ActiveMatch(match, round, group, event));
} }
} }
} }
@@ -354,16 +372,15 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
matchResultDialog = inject(MatDialog); matchResultDialog = inject(MatDialog);
courtSelectionDialog = inject(MatDialog); courtSelectionDialog = inject(MatDialog);
counterSelectionDialog = inject(MatDialog);
editResult(match: Match, group: Group, round: Round) { editResult(match: Match, event: Event, group: Group, round: Round) {
this.matchResultDialog.open(MatchResultComponent, { this.matchResultDialog.open(MatchResultComponent, {
data: {match: match, group: group, round: round}, data: {match: match, tournament: this.tournament, event: event, group: group, round: round},
minWidth: '800px' minWidth: '800px'
}).afterClosed().subscribe(result => { }).afterClosed().subscribe(result => {
if (result != undefined) { if (result != undefined) {
this.tournamentService.saveResult(this.tournament.id, result.matchId, result).subscribe(data => { this.loadTournament(this.tournamentService.saveResult(this.tournament.id, result.matchId, result));
this.tournament = data;
})
} }
}); });
} }
@@ -413,16 +430,97 @@ export class TournamentManageComponent implements OnInit, OnDestroy {
} }
return count; return count;
} }
changeCounter(match: Match) {
this.counterSelectionDialog.open(CounterSelectionComponent, {
data: {
match: match,
availableCounters: this.getAvailableCounters(match)
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.loadTournament(this.tournamentService.updateCounter(this.tournament.id, match.id, result.counter.playerId));
}
});
}
private loadTournament(
observable: Observable<Tournament>,
afterLoad?: (tournament: Tournament) => void
): Subscription {
return observable.subscribe(data => {
this.tournament = data;
this.updateMatchAvailability();
afterLoad?.(data); // Optional chaining
});
}
private updateMatchAvailability() {
for (const event of this.tournament.events) {
for (const group of event.groups) {
for (const round of group.rounds) {
for (const match of round.matches) {
let canStart = true;
let matchPlayersThatArePlaying = this.matchPlayersThatArePlaying(match);
let matchPlayersThatAreCounting = this.matchPlayersThatAreCounting(match);
let cantStartReason = "";
if (matchPlayersThatArePlaying.length > 0) {
canStart = false;
cantStartReason = this.joinWithEn(matchPlayersThatArePlaying.map(m => this.getTournamentPlayerFromId(m).name));
if (matchPlayersThatArePlaying.length == 1) {
cantStartReason += " is al aan het spelen";
} else {
cantStartReason += " zijn al aan het spelen";
}
}
if (matchPlayersThatAreCounting.length > 0) {
canStart = false;
if (cantStartReason.length > 0) {
cantStartReason += " en ";
}
cantStartReason += this.joinWithEn(matchPlayersThatAreCounting.map(m => this.getTournamentPlayerFromId(m).name));
if (matchPlayersThatAreCounting.length == 1) {
cantStartReason += " is aan het tellen";
} else {
cantStartReason += " zijn aan het tellen";
}
}
match.canStart = canStart;
match.cantStartReason = cantStartReason;
}
}
}
}
}
joinWithEn(items: string[], separator = ", "): string {
const len = items.length;
if (len === 0) return "";
if (len === 1) return items[0];
if (len === 2) return items.join(" en ");
return items.slice(0, -1).join(separator) + `${separator}and ${items[len - 1]}`;
}
} }
class ActiveMatch { class ActiveMatch {
constructor(match: Match, round: Round, group: Group) { constructor(match: Match, round: Round, group: Group, event: Event) {
this.match = match; this.match = match;
this.round = round; this.round = round;
this.group = group; this.group = group;
this.event = event;
} }
match: Match; match: Match;
round: Round; round: Round;
group: Group; group: Group;
event: Event;
} }

View File

@@ -1,20 +1,27 @@
@if (tournament) { @if (tournament) {
<mat-tab-group animationDuration="0ms" disableRipple="true">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>euro</mat-icon>
&nbsp;Administratie
</ng-template>
<table class="table table-hover w-75 m-4"> <table class="table table-hover w-75 m-4">
<thead> <thead>
<tr> <tr>
<th>Naam</th> <th>Naam</th>
<th>Wedstrijden geteld</th>
<th>Onderdelen</th> <th>Onderdelen</th>
<th>Kosten</th> <th>Kosten</th>
<th>Betaald</th> <th>Betaald</th>
@if (tournament.status == 'ONGOING') {
<th>Aanwezig</th> <th>Aanwezig</th>
}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (tournamentPlayer of tournament.tournamentPlayers; track tournamentPlayer.playerId) { @for (tournamentPlayer of tournament.tournamentPlayers; track tournamentPlayer.playerId) {
<tr> <tr>
<td>{{ tournamentPlayer.name }}</td> <td>{{ tournamentPlayer.name }}</td>
<td>{{ tournamentPlayer.counts }}</td>
<td> <td>
@for (event of tournamentPlayer.events; track event) { @for (event of tournamentPlayer.events; track event) {
{{ event }}&nbsp; {{ event }}&nbsp;
@@ -32,6 +39,7 @@
} }
</mat-slide-toggle> </mat-slide-toggle>
</td> </td>
@if (tournament.status == 'ONGOING') {
<td> <td>
<mat-slide-toggle [(ngModel)]="tournamentPlayer.present" (change)="playerPresent($event, tournamentPlayer.playerId)"> <mat-slide-toggle [(ngModel)]="tournamentPlayer.present" (change)="playerPresent($event, tournamentPlayer.playerId)">
@if (tournamentPlayer.present) { @if (tournamentPlayer.present) {
@@ -41,8 +49,60 @@
} }
</mat-slide-toggle> </mat-slide-toggle>
</td> </td>
}
</tr> </tr>
} }
</tbody> </tbody>
</table> </table>
</mat-tab>
@if (tournament.status == 'ONGOING') {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>group</mat-icon>
&nbsp;Tellers en invallers
</ng-template>
<table class="table table-hover w-75 m-4">
<thead>
<tr>
<th>Naam</th>
<th>Wedstrijden geteld</th>
<th>Onderdelen</th>
<th>Invallers</th>
<th></th>
</tr>
</thead>
<tbody>
@for (tournamentPlayer of tournament.tournamentPlayers; track tournamentPlayer.playerId) {
<tr>
<td>{{ tournamentPlayer.name }}</td>
<td>{{ tournamentPlayer.counts }}</td>
<td>
@for (event of tournamentPlayer.events; track event) {
{{ event }}&nbsp;
}
</td>
<td>{{ hasSubstitutes(tournamentPlayer) ? 'Ja' : 'Nee' }}</td>
<td>
<button mat-icon-button [matMenuTriggerFor]="dividedTournamentMenu" class="menu-button">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #dividedTournamentMenu="matMenu">
<button mat-menu-item (click)="findSubstitute(tournamentPlayer)">
<mat-icon>autorenew</mat-icon>
Invallers
</button>
<button mat-menu-item> <!--(click)="drawTournament()"-->
<mat-icon>do_not_disturb_on</mat-icon>
Deelname stoppen
</button>
</mat-menu>
</td>
</tr>
}
</tbody>
</table>
</mat-tab>
}
</mat-tab-group>
} }

View File

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

View File

@@ -1,18 +1,33 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, inject, Input, OnInit} from '@angular/core';
import {CurrencyPipe} from "@angular/common"; import {CurrencyPipe} from "@angular/common";
import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle"; import {MatSlideToggle, MatSlideToggleChange} from "@angular/material/slide-toggle";
import {TournamentService} from "../../service/tournament.service"; import {TournamentService} from "../../service/tournament.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute} from "@angular/router";
import {Tournament} from "../../model/tournament"; import {Tournament} from "../../model/tournament";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {MatIcon} from "@angular/material/icon";
import {MatIconButton} from "@angular/material/button";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {MatDialog} from "@angular/material/dialog";
import {SubstituteSelectionComponent} from "../substitute-selection/substitute-selection.component";
import {TournamentPlayer} from "../../model/tournamentPlayer";
import {MatTab, MatTabGroup, MatTabLabel} from "@angular/material/tabs";
@Component({ @Component({
selector: 'app-tournament-players', selector: 'app-tournament-players',
imports: [ imports: [
CurrencyPipe, CurrencyPipe,
MatSlideToggle, MatSlideToggle,
FormsModule FormsModule,
MatIcon,
MatIconButton,
MatMenu,
MatMenuItem,
MatMenuTrigger,
MatTab,
MatTabGroup,
MatTabLabel
], ],
templateUrl: './tournament-players.component.html', templateUrl: './tournament-players.component.html',
standalone: true, standalone: true,
@@ -24,17 +39,19 @@ export class TournamentPlayersComponent implements OnInit {
constructor( constructor(
private tournamentService: TournamentService, private tournamentService: TournamentService,
private _snackBar: MatSnackBar,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private _snackBar: MatSnackBar,
) {} ) {}
ngOnInit() { ngOnInit() {
console.log('Tournament received from parent:', this.tournament); if (!this.tournament) {
// const id = this.route.snapshot.paramMap.get('id'); const id = Number(this.route.snapshot.paramMap.get('id'));
// this.tournamentService.getById(Number(id)).subscribe(data => { if (id) {
// this.tournament = data; this.tournamentService.getById(Number(id)).subscribe(data => {
// }); this.tournament = data;
});
}
}
} }
playerPaid($event: MatSlideToggleChange, playerId: number) { playerPaid($event: MatSlideToggleChange, playerId: number) {
@@ -49,4 +66,27 @@ export class TournamentPlayersComponent implements OnInit {
}); });
} }
substituteSelectionDialog = inject(MatDialog);
findSubstitute(player: TournamentPlayer) {
this.substituteSelectionDialog.open(SubstituteSelectionComponent, {
data: {
player: player,
availablePlayers: this.tournament.tournamentPlayers
},
minWidth: '800px',
minHeight: '250px'
}).afterClosed().subscribe(result => {
if (result != undefined) {
this.tournamentService.playerSubstitute(this.tournament.id, player.playerId, result.substitutions).subscribe(data => {
this.tournament = data;
});
}
});
}
hasSubstitutes(tournamentPlayer: TournamentPlayer) {
return tournamentPlayer.substitutions.filter(s => s.substitute != null && s.substitute >= 0).length > 0;
}
} }

View File

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

View File

@@ -12,6 +12,9 @@ export class Tournament {
costsPerEvent: number[] = [10, 20, 0]; costsPerEvent: number[] = [10, 20, 0];
courts: number; courts: number;
active: boolean; active: boolean;
playersPlaying: number[];
playersCounting: number[];
playersAvailable: number[];
static getStatus(tournament: Tournament): string { static getStatus(tournament: Tournament): string {
if (tournament.status == "CLOSED") return "Afgerond"; if (tournament.status == "CLOSED") return "Afgerond";

View File

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

View File

@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import {FullNamePipe} from "../pipes/fullname-pipe"; import {FullNamePipe} from "./fullname-pipe";
@Pipe({ @Pipe({
name: 'teamText', name: 'teamText',
standalone: true standalone: true

View File

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

View File

@@ -5,6 +5,7 @@ import {Tournament} from "../model/tournament";
import {TournamentValidation} from "../model/tournamentValidation"; import {TournamentValidation} from "../model/tournamentValidation";
import {Result} from "../model/result"; import {Result} from "../model/result";
import { environment } from "../../environments/environment" import { environment } from "../../environments/environment"
import {TournamentPlayerSubstitution} from "../model/tournamentPlayer";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -92,6 +93,14 @@ export class TournamentService {
return this.http.patch<void>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/present/${paid}`, null); return this.http.patch<void>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/present/${paid}`, null);
} }
public playerSubstitute(tournamentId: number, playerId: number, substitutions: TournamentPlayerSubstitution[]): Observable<Tournament> {
return this.http.post<Tournament>(`${this.tournamentsUrl}/${tournamentId}/players/${playerId}/substitutions`, substitutions)
}
public updateCounter(tournamentId: number, matchId: number, counter: number): Observable<Tournament> {
return this.http.patch<Tournament>(`${this.tournamentsUrl}/${tournamentId}/matches/${matchId}/update?counter=${counter}`, null);
}
public addTestData(): Observable<void> { public addTestData(): Observable<void> {
return this.http.get<void>(`${environment.backendUrl}/testdata`); return this.http.get<void>(`${environment.backendUrl}/testdata`);
} }

View File

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

View File

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

820
yarn.lock

File diff suppressed because it is too large Load Diff