143 Commits

Author SHA1 Message Date
ec48f73d9f Merge pull request 'feat: support ha partial config' (#96) from dev into master
Reviewed-on: #96
2026-01-07 15:24:44 +00:00
f98f1bd1ac feat: support ha partial config
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-01-07 16:24:19 +01:00
b9d752ac55 Merge pull request 'dev' (#95) from dev into master
Reviewed-on: #95
2026-01-05 16:57:01 +00:00
13650b27c1 feat: add selection
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m50s
2026-01-05 17:56:37 +01:00
0757ae7198 feat: allow admin and club admin to get comp result 2026-01-05 14:20:22 +01:00
a871b52006 feat: use blason setting 2026-01-05 11:09:22 +01:00
78959fc485 feat: cma select comb sort by name 2026-01-04 21:29:42 +01:00
0a91e72c29 feat: autorise null mail for admin add 2026-01-04 21:05:20 +01:00
e3a1d1c50b Merge pull request 'dev' (#94) from dev into master
Reviewed-on: #94
2026-01-03 20:50:18 +00:00
3933954e09 feat: add permission setting to admin page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m44s
2026-01-03 21:49:36 +01:00
959e356fb9 fix: quick add auto complet 2026-01-03 11:22:04 +01:00
3b5955ff31 fix: rm match with cardboard 2026-01-02 23:44:53 +01:00
e8ee5811a0 feat: avoid ws reconnect when user logged out 2026-01-02 23:12:22 +01:00
12326fe122 Merge pull request 'fix: ws timeout' (#93) from dev into master
Reviewed-on: #93
2026-01-02 21:33:40 +00:00
9624ff93f0 fix: ws timeout
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2026-01-02 22:33:11 +01:00
5444235512 Merge pull request 'dev' (#92) from dev into master
Reviewed-on: #92
2026-01-02 15:00:08 +00:00
b54eaa2549 fix: remove case sensitive sort result
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m44s
2026-01-02 15:58:59 +01:00
6a7af8aea9 fix: tree order on result api 2026-01-02 15:33:56 +01:00
bc4dbb532f fix: remove extra log on recalculateMatch 2026-01-02 15:13:41 +01:00
0e269e0bf7 Merge pull request 'dev' (#91) from dev into master
Reviewed-on: #91
2026-01-02 14:06:09 +00:00
6d6324e1b4 fix: result on empty category
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m45s
2026-01-02 15:05:30 +01:00
309f2a2268 feat: recalculateMatch add tmp log 2026-01-02 15:03:09 +01:00
13bae3e817 Merge pull request 'dev' (#90) from dev into master
Reviewed-on: #90
2026-01-02 13:42:36 +00:00
c6751b0428 fix: recalculateMatch 2026-01-02 14:42:02 +01:00
237952f95a fix: tree order on cmt 2026-01-02 14:29:11 +01:00
075f01a205 feat: add privacy setting in me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m53s
2026-01-02 14:09:55 +01:00
82231744af feat: improve result page design 2026-01-02 13:48:36 +01:00
71a4d98434 fix: club remove 2026-01-02 13:47:54 +01:00
29a1184994 feat: add Result Privacy membre setting 2026-01-01 19:43:25 +01:00
3d07a5bc8e Merge pull request 'fix: rm licence' (#89) from dev into master
Reviewed-on: #89
2025-12-31 15:47:55 +00:00
a730384d37 fix: rm licence
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m42s
2025-12-31 16:47:37 +01:00
412e661319 Merge pull request 'fix: cat graphe color and text alignement' (#88) from dev into master
Reviewed-on: #88
2025-12-31 13:50:57 +00:00
bc72b7912c fix: cat graphe color and text alignement
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m46s
2025-12-31 14:50:15 +01:00
6359e7e027 Merge pull request 'fix: ci cache' (#87) from dev into master
Reviewed-on: #87
2025-12-31 13:15:42 +00:00
c7a9cc221f fix: ci cache
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m31s
2025-12-31 14:15:25 +01:00
17c61be171 Merge pull request 'fix: ci cache' (#86) from dev into master
Reviewed-on: #86
2025-12-31 13:12:17 +00:00
76f69c2154 fix: ci cache
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 1m30s
2025-12-31 14:11:43 +01:00
639f07ccdc Merge pull request 'fix: ci cache' (#85) from dev into master
Reviewed-on: #85
2025-12-31 13:07:57 +00:00
c02e37862c fix: ci cache
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2s
2025-12-31 14:07:37 +01:00
2b6dc48880 Merge pull request 'dev' (#84) from dev into master
Reviewed-on: #84
2025-12-31 12:59:50 +00:00
6879d9195d fix: remove useless log
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 7s
2025-12-31 13:59:33 +01:00
1595fb57c3 update: config + add action cache 2025-12-31 13:50:28 +01:00
8067d496f8 Merge pull request 'Re-update to 3.30.5' (#83) from dev into master
Reviewed-on: #83
2025-12-31 12:20:08 +00:00
aba8d8a202 Re-update to 3.30.5
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 8m10s
2025-12-31 13:19:31 +01:00
38663d97c2 Merge pull request 'Restore pom.xml' (#82) from dev into master
Reviewed-on: #82
2025-12-29 22:47:27 +00:00
377ce70125 Restore pom.xml
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m36s
2025-12-29 23:47:06 +01:00
61cee85335 Merge pull request 'Restore Quarkus 3.16.4' (#81) from dev into master
Reviewed-on: #81
2025-12-29 22:36:34 +00:00
19949d1e35 Restore Quarkus 3.16.4
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m35s
2025-12-29 23:36:02 +01:00
03cdcf78c8 Merge pull request 'Move to Quarkus 3.20.4' (#80) from dev into master
Reviewed-on: #80
2025-12-29 22:30:24 +00:00
0b4f843084 Move to Quarkus 3.20.4
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m20s
2025-12-29 23:30:01 +01:00
fa00273c87 Merge pull request 'Move to Quarkus 3.27.1' (#79) from dev into master
Reviewed-on: #79
2025-12-29 22:24:03 +00:00
b2438e52fc Move to Quarkus 3.27.1
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 5m4s
2025-12-29 23:23:42 +01:00
31bbe5719f Merge pull request 'dev' (#78) from dev into master
Reviewed-on: #78
2025-12-29 20:54:30 +00:00
bf377c9c5e feat: external website result connector
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 2m31s
2025-12-29 21:03:05 +01:00
145d5e7ca8 feat: update quarkus 2025-12-29 15:41:13 +01:00
448e49b9fd feat: add obs connector 2025-12-28 21:22:28 +01:00
b7a1e58436 fix: cma comb dupli on poule list 2025-12-27 17:34:58 +01:00
a73a3562c3 feat: add scrolle to match list in cmt 2025-12-27 17:29:28 +01:00
0dace76fc3 feat: cat remove 2025-12-27 17:06:27 +01:00
f76ca43ed6 feat: add import fail log detail 2025-12-27 16:55:23 +01:00
c6b24a1b20 feat: add fname formatter 2025-12-27 16:43:14 +01:00
c47c04c29e fix: cat update/add refresh 2025-12-27 15:00:50 +01:00
e06df24002 ty-fix: ws timeout 2025-12-27 14:20:10 +01:00
352c34064f fix: score save update 2025-12-27 14:18:38 +01:00
2ccd2a9178 fix: disable match filter on 1 zone cat + rename lice to zone 2025-12-27 13:02:45 +01:00
0ff3ffd66b Merge pull request 'feat: add club_tresorier to club admin' (#77) from dev into master
Reviewed-on: #77
2025-12-26 21:19:10 +00:00
847674d62c feat: add club_tresorier to club admin
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 22:18:03 +01:00
8896e2a28a Merge pull request 'fix: delect club without user' (#76) from dev into master
Reviewed-on: #76
2025-12-26 13:37:05 +00:00
68b4f8e476 fix: delect club without user
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m40s
2025-12-26 14:35:19 +01:00
d3fb884ab4 Merge pull request 'fix: Me page' (#75) from dev into master
Reviewed-on: #75
2025-12-26 13:16:56 +00:00
81faafe2de fix: Me page
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m46s
2025-12-26 14:16:30 +01:00
ee730be570 Merge pull request 'dev' (#74) from dev into master
Reviewed-on: #74
2025-12-25 19:03:17 +00:00
4856326b29 feat: html mail for all public
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 11m4s
2025-12-25 19:59:47 +01:00
b94f4b43ce fix: reword message + fix missing comb un cat list (cma) 2025-12-25 19:27:15 +01:00
795d044c0b fix:typo 2025-12-25 19:25:56 +01:00
dc5ab9c3c4 feat: add inactif member filter 2025-12-20 18:22:24 +01:00
870891a77c Merge pull request 'dev' (#73) from dev into master
Reviewed-on: #73
2025-12-19 15:14:04 +00:00
7e1356b621 fix: cm auto ask new category on empty competition
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-12-19 16:13:37 +01:00
b689521e56 fix: comb register with null license 2025-12-19 16:02:28 +01:00
e81f475402 Merge pull request 'dev-comp' (#72) from dev into master
Reviewed-on: #72
2025-12-19 13:47:40 +00:00
095a5fcede Merge branch 'dev-comp' into dev
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m20s
2025-12-19 14:43:46 +01:00
3decc6d0e0 Merge pull request 'dev' (#71) from dev into master
Reviewed-on: #71
2025-12-18 15:37:19 +00:00
254fd7d73f feat: html email
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m5s
2025-12-18 16:36:30 +01:00
12a75d003c feat: mask inactive club on map 2025-12-18 15:33:45 +01:00
7f957b98f2 fix: deleteLicence Panache missing transaction context 2025-12-18 15:24:32 +01:00
8b2c9613fd Merge pull request 'dev' (#70) from dev into master
Reviewed-on: #70
2025-12-18 14:03:51 +00:00
50b49c40e3 fix: error 500 when license is null
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m6s
2025-12-18 15:03:22 +01:00
47d3921e05 fix: email typo 2025-12-18 14:57:26 +01:00
ecc9753237 Merge pull request 'feat: add cat filter on mass license validation' (#69) from dev into master
Reviewed-on: #69
2025-12-18 13:47:32 +00:00
7c4addd525 feat: add cat filter on mass license validation
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-12-18 14:46:52 +01:00
a8565534e6 update README.md 2025-12-05 15:28:03 +01:00
43fcbb5ef6 Merge pull request 'feat: re-add siret/rna api' (#68) from dev into master
Reviewed-on: #68
2025-11-19 14:31:46 +00:00
fefc5d651b feat: re-add siret/rna api
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m4s
2025-11-19 15:26:36 +01:00
163d9b23e9 Merge pull request 'fix: error on loading logo on maps' (#67) from dev into master
Reviewed-on: #67
2025-11-18 21:02:58 +00:00
53509820c6 fix: error on loading logo on maps
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m58s
2025-11-18 22:02:13 +01:00
a8ad3a7558 Merge pull request 'fix: remove siret/rna api' (#66) from dev into master
Reviewed-on: #66
2025-11-18 20:46:34 +00:00
f46e268d39 fix: remove siret/rna api
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 1m33s
2025-11-18 21:46:05 +01:00
abad3194a4 Merge pull request 'dev' (#65) from dev into master
Reviewed-on: #65
2025-11-18 19:32:14 +00:00
6e7eec0587 feat: membre list filter history
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-18 15:45:18 +01:00
8c83668364 feat: add categorie filter membre search 2025-11-18 14:02:02 +01:00
771c06ccd8 feat: make user-friendly cat name 2025-11-18 13:27:49 +01:00
b479b992cf feat: add ordering for member page 2025-11-17 21:48:24 +01:00
f018e52afa Merge pull request 'dev' (#64) from dev into master
Reviewed-on: #64
2025-11-14 15:55:53 +00:00
be2f01c070 feat: upgrade club groupe name in kc when club is rename
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m13s
2025-11-14 16:38:44 +01:00
9ab50238b9 feat: upgrade aff find to user all id (Rna, siret, siren) 2025-11-14 16:17:40 +01:00
cf5d93630f fix: certifDate NaN all membre export 2025-11-14 14:46:58 +01:00
e5e17d3862 Merge pull request 'fix: null licence on import' (#63) from dev into master
Reviewed-on: #63
2025-11-14 13:32:09 +00:00
7410569ced fix: null licence on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m3s
2025-11-14 14:31:37 +01:00
b107f443aa Merge pull request 'fix: add more log to import' (#62) from dev into master
Reviewed-on: #62
2025-11-14 13:15:27 +00:00
8e2d68ebd5 fix: add more log to import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m10s
2025-11-14 14:14:22 +01:00
f050127fd7 Merge pull request 'dev' (#61) from dev into master
Reviewed-on: #61
2025-11-14 12:45:53 +00:00
d02fd63834 fix: empty mail on import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m30s
2025-11-14 13:43:51 +01:00
b143cc759f feat: remove kc new account mail 2025-11-12 16:38:35 +01:00
cc5534ef00 feat: add cache to getAssoInfo 2025-11-12 16:15:40 +01:00
26f56006f6 feat: merge rna and siret fields 2025-11-12 14:56:28 +01:00
f8dacee3e7 Merge pull request 'fix: club delete on RegisterModel' (#60) from dev into master
Reviewed-on: #60
2025-11-08 18:09:04 +00:00
ed1f30f2b6 fix: club delete on RegisterModel
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m17s
2025-11-08 19:08:41 +01:00
8517e9824c Merge pull request 'feat: allow empty mail' (#59) from dev into master
Reviewed-on: #59
2025-11-08 17:45:02 +00:00
6cec8ff31d feat: allow empty mail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m16s
2025-11-08 18:44:35 +01:00
e7deba52e9 Merge pull request 'dev' (#58) from dev into master
Reviewed-on: #58
2025-11-07 15:31:18 +00:00
d95c173fa8 fix: aff renew select length
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m51s
2025-11-07 16:19:49 +01:00
8a0e4423f2 feat: remove saison selection on aff req 2025-11-07 16:13:37 +01:00
b956236934 fix: affiliation ok login msg 2025-11-07 15:57:36 +01:00
7767c98304 feat: add email tooltip 2025-11-07 15:55:10 +01:00
7e380ccb69 feat: add re-login message 2025-11-07 15:31:25 +01:00
94d1148eb1 fix: null email on import 2025-11-07 15:30:56 +01:00
8a14f58ce5 Merge pull request 'fix: log detail' (#57) from dev into master
Reviewed-on: #57
2025-11-05 21:06:54 +00:00
79dbbdaaec fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m27s
2025-11-05 22:06:36 +01:00
bf5704db54 Merge pull request 'fix: log detail' (#56) from dev into master
Reviewed-on: #56
2025-11-05 20:43:26 +00:00
dec98f9508 fix: log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m22s
2025-11-05 21:43:05 +01:00
d3a62e980d Merge pull request 'feat: add aff req log detail' (#55) from dev into master
Reviewed-on: #55
2025-11-05 20:19:10 +00:00
b89ed62795 feat: add aff req log detail
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m33s
fix: membre import null email filter
2025-11-05 21:18:29 +01:00
5ffc9fb495 Merge pull request 'fix: typo' (#54) from dev into master
Reviewed-on: #54
2025-09-05 18:31:45 +00:00
5e48bc4623 fix: typo
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 9m51s
2025-09-05 20:31:10 +02:00
81b953fb05 Merge pull request 'dev' (#53) from dev into master
Reviewed-on: #53
2025-09-03 19:47:13 +00:00
c6659f8d85 feat: keep log
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 10m19s
2025-09-03 21:46:26 +02:00
e86fe42b3d fix: club order 2025-09-03 21:35:36 +02:00
ef528aa524 fix: log message length 2025-09-03 19:53:19 +02:00
e6cc4cbc96 Merge pull request 'dev' (#52) from dev into master
Reviewed-on: #52
2025-08-21 09:43:54 +00:00
c58dedf80a Merge pull request 'competition rework' (#51) from dev into master
Reviewed-on: #51
2025-08-18 09:33:51 +00:00
1908de681e Merge pull request 'dev' (#50) from dev into master
Reviewed-on: #50
2025-08-15 19:49:13 +00:00
76381c75bd Merge pull request 'fix: Membre edition' (#49) from dev into master
Reviewed-on: #49
2025-08-15 18:34:43 +00:00
7d281196c1 Merge pull request 'dev' (#48) from dev into master
Reviewed-on: #48
2025-08-15 16:26:23 +00:00
107 changed files with 5622 additions and 1187 deletions

View File

@@ -17,12 +17,33 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache node modules
id: cache-npm
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17.0.12'
java-version: '21'
distribution: 'graalvm'
cache: 'maven'
- name: Set up Node.js
uses: actions/setup-node@v4

1
.gitignore vendored
View File

@@ -50,3 +50,4 @@ nb-configuration.xml
/media/
/media-ext/
/sign.jpg
/.gitea/workflows/test.yml

View File

@@ -1,8 +1,22 @@
# ffsaf-site
# FFSAF - Intranet
This project uses Quarkus, the Supersonic Subatomic Java Framework.
## Introduction et context
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
Lintranet de la Fédération française de Soft Armored Fighting a pour but centralise la prise de licences des adhérents
et laffiliation des clubs. Il permet aussi de recueillir les résultats des compétitions organisées par les clubs et,
plus récemment, den faciliter lorganisation grâce à un outil intégré.
Le système de prise de licence se divise en trois parties :
* Un formulaire public pour la première demande daffiliation ;
* Un espace club, accessible après validation, pour la saisie des informations relatives aux demandes de licence des
adhérents ;
* Un espace fédération pour accepter les demandes de licences et daffiliation.
Un espace membre permet enfin à chaque adhérent de télécharger son attestation de licence.
Pour les compétitions, le système permet la création de catégories, de poules et de matchs, avec génération automatique
des matchs au sein dune poule. Il supporte plusieurs lices, assure la synchronisation en temps réel des modifications
entre toutes les instances de lapplication web et publie automatiquement les résultats sur le site de lorganisateur.
## Running the application in dev mode
@@ -52,34 +66,3 @@ Or, if you don't have GraalVM installed, you can run the native executable build
You can then execute your native executable with: `./target/ffsaf-site-1.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
## Related Guides
- Reactive MySQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the MySQL database using the reactive pattern
- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A Jakarta REST implementation utilizing build time processing and Vert.x.
This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the
active record or the repository pattern
- Reactive PostgreSQL client ([guide](https://quarkus.io/guides/reactive-sql-clients)): Connect to the PostgreSQL database using the reactive pattern
## Provided Code
### Hibernate ORM
Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
### RESTEasy Reactive
Easily start your Reactive RESTful Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
### RESTEasy Reactive Qute
Create your web page using Quarkus RESTEasy Reactive & Qute
[Related guide section...](https://quarkus.io/guides/qute#type-safe-templates)

18
pom.xml
View File

@@ -11,7 +11,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.16.4</quarkus.platform.version>
<quarkus.platform.version>3.30.5</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.3</surefire-plugin.version>
</properties>
@@ -56,19 +56,15 @@
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mssql-client</artifactId>
<version>4.4.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.tika</groupId>
<artifactId>quarkus-tika</artifactId>
<version>2.0.4</version>
<groupId>io.quarkiverse.antivirus</groupId>
<artifactId>quarkus-antivirus</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
@@ -96,7 +92,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
@@ -127,7 +123,7 @@
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>fop</artifactId>
<version>2.6</version>
<version>2.11</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
public interface CombModel {
Long getCombId();
String getName();
String getName(MembreModel model, ResultPrivacy privacy);
}

View File

@@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
@@ -17,7 +18,7 @@ import lombok.Setter;
@Entity
@Table(name = "competition_guest")
public class CompetitionGuestModel {
public class CompetitionGuestModel implements CombModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@@ -44,7 +45,18 @@ public class CompetitionGuestModel {
this.lname = s.substring(s.indexOf(" ") + 1);
}
@Override
public Long getCombId() {
return this.id * -1;
}
@Override
public String getName() {
return fname + " " + lname;
return this.fname + " " + this.lname;
}
@Override
public String getName(MembreModel model, ResultPrivacy privacy) {
return getName();
}
}

View File

@@ -67,8 +67,15 @@ public class CompetitionModel {
@Column(name = "table_")
List<String> table = new ArrayList<>();
@Column(columnDefinition = "TEXT")
String data1;
@Column(columnDefinition = "TEXT")
String data2;
@Column(columnDefinition = "TEXT")
String data3;
@Column(columnDefinition = "TEXT")
String data4;
@Column(columnDefinition = "TEXT")
String config;
}

View File

@@ -41,7 +41,7 @@ public class LogModel {
}
public enum ObjectType {
Membre, Affiliation, Licence, Club, Competition, Register
Membre, Affiliation, Licence, Club, Competition, Register, Selection
}
}

View File

@@ -1,6 +1,7 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.CompetitionSystem;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
@@ -9,6 +10,7 @@ import lombok.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Getter
@Setter
@@ -65,19 +67,35 @@ public class MatchModel {
@JoinColumn(name = "match", referencedColumnName = "id")
List<CardboardModel> cardboard = new ArrayList<>();
public String getC1Name(MembreModel model, ResultPrivacy privacy) {
if (c1_id != null)
return c1_id.getName(model, privacy);
if (c1_guest != null)
return c1_guest.getName(model, privacy);
return "";
}
public String getC2Name(MembreModel model, ResultPrivacy privacy) {
if (c2_id != null)
return c2_id.getName(model, privacy);
if (c2_guest != null)
return c2_guest.getName(model, privacy);
return "";
}
public String getC1Name() {
if (c1_id != null)
return c1_id.fname + " " + c1_id.lname;
return c1_id.getName();
if (c1_guest != null)
return c1_guest.fname + " " + c1_guest.lname;
return c1_guest.getName();
return "";
}
public String getC2Name() {
if (c2_id != null)
return c2_id.fname + " " + c2_id.lname;
return c2_id.getName();
if (c2_guest != null)
return c2_guest.fname + " " + c2_guest.lname;
return c2_guest.getName();
return "";
}
@@ -94,4 +112,24 @@ public class MatchModel {
}
return sum;
}
public boolean isC1(Object comb) {
if (comb instanceof Long id_) {
if (id_ >= 0)
return Objects.equals(this.c1_id != null ? this.c1_id.getId() : null, id_);
else
return Objects.equals(this.c1_guest != null ? this.c1_guest.getId() : null, -id_);
}
return Objects.equals(this.c1_id, comb) || Objects.equals(this.c1_guest, comb);
}
public boolean isC2(Object comb) {
if (comb instanceof Long id_) {
if (id_ >= 0)
return Objects.equals(this.c2_id != null ? this.c2_id.getId() : null, id_);
else
return Objects.equals(this.c2_guest != null ? this.c2_guest.getId() : null, -id_);
}
return Objects.equals(this.c2_id, comb) || Objects.equals(this.c2_guest, comb);
}
}

View File

@@ -1,9 +1,6 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
@@ -11,6 +8,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Getter
@Setter
@@ -21,23 +19,23 @@ import java.util.List;
@Entity
@Table(name = "membre")
public class MembreModel implements LoggableModel {
public class MembreModel implements LoggableModel, CombModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Access(AccessType.PROPERTY)
@Schema(description = "L'identifiant du membre.", example = "1")
@Schema(description = "L'identifiant du membre.", examples = "1")
Long id;
@Schema(description = "L'identifiant long du membre (userID).", example = "e81d1d35-d897-421e-8086-6c5e74d13c6e")
@Schema(description = "L'identifiant long du membre (userID).", examples = "e81d1d35-d897-421e-8086-6c5e74d13c6e")
String userId;
@Schema(description = "Le nom du membre.", example = "Dupont")
@Schema(description = "Le nom du membre.", examples = "Dupont")
String lname;
@Schema(description = "Le prénom du membre.", example = "Jean")
@Schema(description = "Le prénom du membre.", examples = "Jean")
String fname;
@Schema(description = "La catégorie du membre.", example = "SENIOR")
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
Categorie categorie;
@ManyToOne(fetch = FetchType.EAGER)
@@ -45,34 +43,39 @@ public class MembreModel implements LoggableModel {
@Schema(description = "Le club du membre.")
ClubModel club;
@Schema(description = "Le genre du membre.", example = "H")
@Schema(description = "Le genre du membre.", examples = "H")
Genre genre;
@Schema(description = "Le numéro de licence du membre.", example = "12345")
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
Integer licence;
@Schema(description = "Le pays du membre.", example = "FR")
@Schema(description = "Le pays du membre.", examples = "FR")
String country;
@Schema(description = "La date de naissance du membre.")
Date birth_date;
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
String email;
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
RoleAsso role;
@Schema(description = "Le grade d'arbitrage du membre.", example = "NA")
@Schema(description = "Le grade d'arbitrage du membre.", examples = "NA")
GradeArbitrage grade_arbitrage;
@Schema(hidden = true)
String url_photo;
@Schema(description = "La confidentialité de ces résultats", examples = "PUBLIC")
@Column(nullable = false, columnDefinition = "smallint default 0")
ResultPrivacy resultPrivacy = ResultPrivacy.PUBLIC;
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Schema(description = "Les licences du membre. (optionnel)")
List<LicenceModel> licences;
@OneToMany(mappedBy = "membre", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Schema(description = "Les séléctions du membre. (optionnel)")
List<SelectionModel> selections;
@Override
public String getObjectName() {
return fname + " " + lname;
@@ -100,4 +103,25 @@ public class MembreModel implements LoggableModel {
", grade_arbitrage=" + grade_arbitrage +
'}';
}
@Override
public Long getCombId() {
return this.id;
}
@Override
public String getName() {
return this.fname + " " + this.lname;
}
@Override
public String getName(MembreModel model, ResultPrivacy privacy) {
if (model == null || !Objects.equals(this.getId(), model.getId())) {
if (model == null && this.getResultPrivacy() != ResultPrivacy.PUBLIC)
return "Anonyme";
if (this.getResultPrivacy().ordinal() > privacy.ordinal())
return "Anonyme";
}
return getName();
}
}

View File

@@ -66,4 +66,13 @@ public class RegisterModel {
return membre.club;
return club;
}
public Categorie getCategorie2() {
Categorie tmp = this.categorie;
if (tmp == null)
tmp = membre.getCategorie();
if (tmp == null)
return null;
return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)];
}
}

View File

@@ -0,0 +1,44 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "selection")
public class SelectionModel implements LoggableModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Schema(description = "L'identifiant de la séléction.")
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre", referencedColumnName = "id")
@Schema(description = "Le membre de la séléction. (optionnel)")
MembreModel membre;
@Schema(description = "La saison de la séléction.", examples = "2025")
int saison;
@Schema(description = "Catégorie de la séléction.")
Categorie categorie;
@Override
public String getObjectName() {
return "selection " + id.toString();
}
@Override
public LogModel.ObjectType getObjectType() {
return LogModel.ObjectType.Selection;
}
}

View File

@@ -2,8 +2,17 @@ package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CheckoutModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CheckoutRepository implements PanacheRepositoryBase<CheckoutModel, Long> {
public Uni<Long> countByLicenseId(long id) {
return getSession()
.chain(s -> s
.createNativeQuery("SELECT COUNT(*) FROM ffsaf__checkout WHERE ?1 = ANY(license_ids)", Long.class)
.setParameter(1, id)
.getSingleResult());
}
}

View File

@@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.SelectionModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class SelectionRepository implements PanacheRepositoryBase<SelectionModel, Long> {
}

View File

@@ -1,64 +0,0 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class MembreEntity {
private long id;
private String lname = "";
private String fname = "";
private Categorie categorie;
private ClubEntity club;
private Genre genre;
private Integer licence;
private String country;
private Date birth_date;
private String email;
private RoleAsso role;
private GradeArbitrage grade_arbitrage;
private String url_photo;
public static MembreEntity fromModel(MembreModel model) {
if (model == null)
return null;
return new MembreEntityBuilder()
.id(model.getId())
.lname(model.getLname())
.fname(model.getFname())
.categorie(model.getCategorie())
.club(ClubEntity.fromModel(model.getClub()))
.genre(model.getGenre())
.licence(model.getLicence())
.country(model.getCountry())
.birth_date(model.getBirth_date())
.email(model.getEmail())
.role(model.getRole())
.grade_arbitrage(model.getGrade_arbitrage())
.url_photo(model.getUrl_photo())
.build();
}
public static String getFullName(MembreModel model) {
return model.getFname() + " " + model.getLname();
}
public String getFullName() {
return this.fname + " " + this.lname;
}
}

View File

@@ -64,6 +64,9 @@ public class AffiliationService {
@Inject
LoggerService ls;
@Inject
VirusScannerService scanner;
@RestClient
StateIdService stateIdService;
@@ -174,15 +177,12 @@ public class AffiliationService {
LOGGER.debug("Affiliation Request Created");
LOGGER.debug(form.toString());
// noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher
// noinspection ReactiveStreamsUnusedPublisher
return pre_save(form, true)
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
.onItem()
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
"aff_request/logo")))
.onItem()
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
"aff_request/status")))
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
.call(model -> reactiveMailer.send(
Mail.withText("no-reply@ffsaf.fr",
"[NOTIF] FFSAF - Nouvelle demande d'affiliation",
@@ -245,11 +245,8 @@ public class AffiliationService {
})
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
.onItem()
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
"aff_request/logo")))
.onItem()
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
"aff_request/status")))
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
.map(__ -> "Ok");
}
@@ -319,13 +316,11 @@ public class AffiliationService {
.recoverWithNull()
.call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
.onItem()
.invoke(model -> Uni.createFrom()
.future(Utils.replacePhoto(form.getId(), form.getLogo(), media,
"aff_request/logo")))
.invoke(model -> Utils.uploadFile(scanner, form.getLogo(), form.getId(), media,
"aff_request/logo"))
.invoke(model -> Utils.uploadFile(scanner, form.getStatus(), form.getId(), media,
"aff_request/status"))
.onItem()
.invoke(model -> Uni.createFrom()
.future(Utils.replacePhoto(form.getId(), form.getStatus(), media,
"aff_request/status")))
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/logo",
"ppClub"))
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/status",
@@ -355,19 +350,25 @@ public class AffiliationService {
.map(c -> club));
})
.call(club -> reactiveMailer.send(
Mail.withText(form.getM1_email(),
"FFSAF - Acceptation de votre demande d'affiliation",
String.format(
"""
Bonjour,
Mail.withHtml(form.getM1_email(), "FFSAF - Creation de votre compte sur l'intranet",
String.format(Utils.HTML_HEADER, "FFSAF - Creation de votre compte sur l'intranet") +
String.format("""
<p>Votre demande d'affiliation pour le club <span class="highlight">%s</span> a été acceptée.</p>
<p>LLe numéro d'affiliation de votre club est le <span class="highlight">%d</span>.</p>
""",
club.getName(), club.getNo_affiliation()) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Votre demande d'affiliation pour le club %s a été acceptée.
Le numéro d'affiliation de votre club est le %d.
Votre demande d'affiliation pour le club %s a été acceptée.
Le numéro d'affiliation de votre club est le %d.
Cordialement,
L'équipe de la FFSAF
""", club.getName(), club.getNo_affiliation())
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
Cordialement,
L'équipe de la FFSAF
""", club.getName(), club.getNo_affiliation()))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(form.getM2_email(), form.getM3_email())
));
}
@@ -458,21 +459,27 @@ public class AffiliationService {
public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
return repositoryRequest.findById(id)
.call(aff -> federationAdmin ? reactiveMailer.send(
Mail.withText(aff.getM1_email(),
"FFSAF - Votre demande d'affiliation a été rejetée.",
String.format(
"""
Bonjour,
Mail.withHtml(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.",
String.format(Utils.HTML_HEADER, "FFSAF - Votre demande d'affiliation a été rejetée.") +
String.format("""
<p>Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):<br/>%s</p>
<p>Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@ffsaf.fr">contact@ffsaf.fr</a>.</p>
""",
aff.getName(), reason) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
%s
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
%s
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
Cordialement,
L'équipe de la FFSAF
""", aff.getName(), reason)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
Cordialement,
L'équipe de la FFSAF
""", aff.getName(), reason))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(aff.getM2_email(), aff.getM3_email())
) : Uni.createFrom().nullItem())
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))

View File

@@ -58,7 +58,7 @@ public class CheckoutService {
String organizationSlug;
public Uni<Boolean> canDeleteLicence(long id) {
return repository.find("?1 IN licenseIds", id).count().map(count -> count == 0);
return repository.countByLicenseId(id).invoke(c -> System.out.println(c)).map(count -> count == 0);
}
public Uni<String> create(List<Long> ids, SecurityCtx securityCtx) {

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import fr.titionfire.ffsaf.data.model.AffiliationModel;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.net2.ServerCustom;
@@ -44,6 +45,9 @@ public class ClubService {
@Inject
ClubRepository repository;
@Inject
AffiliationRepository affiliationRepository;
@Inject
ServerCustom serverCustom;
@@ -234,7 +238,8 @@ public class ClubService {
}
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
}))
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(clubModel) // update group in keycloak
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(
clubModel) // update group in keycloak
: Uni.createFrom().nullItem())
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
SimpleClubModel.fromModel(membreModel)))
@@ -282,11 +287,13 @@ public class ClubService {
combModel.setClub(null);
combModel.setRole(RoleAsso.MEMBRE);
}).toList())
.call(list -> (list.isEmpty()) ? Uni.createFrom().voidItem() :
.call(list -> list.stream().noneMatch(m -> m.getUserId() != null) ?
Uni.createFrom().voidItem() :
Uni.join().all(list.stream().filter(m -> m.getUserId() != null)
.map(m -> keycloakService.clearUser(m.getUserId())).toList())
.andCollectFailures())
.chain(list -> Panache.withTransaction(() -> combRepository.persist(list)))
.chain(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Panache.withTransaction(() -> combRepository.persist(list)))
.map(o -> club)
)
.call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom()
@@ -323,9 +330,11 @@ public class ClubService {
}
public Uni<List<ClubMapData>> getMapData() {
return repository.list("international", false).toMulti().flatMap(list -> Multi.createFrom().iterable(list))
.call(clubModel -> Mutiny.fetch(clubModel.getContact()))
.map(clubModel -> {
return affiliationRepository.list("saison >= ?1 AND club.international = ?2", Utils.getSaison() - 1, false)
.toMulti().flatMap(list -> Multi.createFrom().iterable(list))
.call(affiliationModel -> Mutiny.fetch(affiliationModel.getClub().getContact()))
.map(affiliationModel -> {
ClubModel clubModel = affiliationModel.getClub();
ClubMapData data = new ClubMapData();
data.setName(clubModel.getName());

View File

@@ -98,7 +98,13 @@ public class CompetitionService {
Cache cacheNoneAccess;
public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
return permService.hasViewPerm(securityCtx, id).map(CompetitionData::fromModelLight);
return permService.hasViewPerm(securityCtx, id).map(cm -> {
CompetitionData out = CompetitionData.fromModelLight(cm);
if (cm.getAdmin() != null) {
out.setCanEditRegisters(cm.getAdmin().stream().anyMatch(u -> u.equals(securityCtx.getSubject())));
}
return out;
});
}
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
@@ -106,7 +112,8 @@ public class CompetitionService {
return Uni.createFrom()
.item(new CompetitionData(null, "", "", "", "", new Date(), new Date(),
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
null, "", "", null, true, "", "", "", ""));
null, "", "", null, true, true,
"", "", "", "", "{}"));
}
return permService.hasAdminViewPerm(securityCtx, id)
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
@@ -129,6 +136,14 @@ public class CompetitionService {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEdit(true));
}))
.chain(ids -> repository.list("id NOT IN ?1 AND ?2 IN admin", ids, securityCtx.getSubject())
.map(cm -> {
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
out.forEach(competition -> competition.setCanEditRegisters(true));
List<Long> ids2 = new ArrayList<>(ids);
ids2.addAll(cm.stream().map(CompetitionModel::getId).toList());
return ids2;
}))
.call(ids ->
repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids,
securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO,
@@ -165,8 +180,9 @@ public class CompetitionService {
public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
CompetitionSystem system) {
return repository.list("system = ?1", system)
.chain(l -> Uni.join().all(l.stream().map(cm -> permService.hasTablePerm(securityCtx, cm)).toList())
.andCollectFailures())
.chain(l -> Uni.join().all(l.stream().map(cm ->
permService.hasTablePerm(securityCtx, cm).onFailure().recoverWithNull()
).toList()).andCollectFailures())
.map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
}
@@ -281,7 +297,7 @@ public class CompetitionService {
public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
String source) {
if ("admin".equals(source))
if (data.getLicence() != -1) { // not a guest
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
return permService.hasEditPerm(securityCtx, id)
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
@@ -513,6 +529,64 @@ public class CompetitionService {
.call(__ -> cache.invalidate(id));
}
public Uni<SimpleCompetData> getInternalData(SecurityCtx securityCtx, Long id) {
return permService.hasEditPerm(securityCtx, id)
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException("Competition is not INTERNAL");
}))
.chain(competitionModel -> {
SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
return vertx.getOrCreateContext().executeBlocking(() -> {
if (competitionModel.getAdmin() != null)
data.setAdmin(
competitionModel.getAdmin().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
if (competitionModel.getTable() != null)
data.setTable(
competitionModel.getTable().stream().map(uuid -> keycloakService.getUserById(uuid))
.filter(Optional::isPresent)
.map(user -> user.get().getUsername())
.toList());
return data;
});
});
}
public Uni<Void> setInternalData(SecurityCtx securityCtx, SimpleCompetData data) {
return permService.hasEditPerm(securityCtx, data.getId())
.invoke(Unchecked.consumer(cm -> {
if (cm.getSystem() != CompetitionSystem.INTERNAL)
throw new DBadRequestException("Competition is not INTERNAL");
}))
.chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
ArrayList<String> admin = new ArrayList<>();
ArrayList<String> table = new ArrayList<>();
for (String username : data.getAdmin()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
admin.add(opt.get().getId());
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
table.add(opt.get().getId());
}
cm.setAdmin(admin);
cm.setTable(table);
cm.setConfig(data.getConfigForInternal());
return cm;
}))
.chain(cm -> Panache.withTransaction(() -> repository.persist(cm)))
.replaceWithVoid();
}
public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) {
return permService.getSafcaConfig(id)
.call(Unchecked.function(o -> {

View File

@@ -4,7 +4,6 @@ import fr.titionfire.ffsaf.rest.client.HelloAssoAuthClient;
import fr.titionfire.ffsaf.rest.client.dto.TokenResponse;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
@@ -13,7 +12,6 @@ import org.jboss.logging.Logger;
public class HelloAssoTokenService {
private static final Logger LOG = Logger.getLogger(HelloAssoTokenService.class);
@Inject
@RestClient
HelloAssoAuthClient authClient;
@@ -27,6 +25,10 @@ public class HelloAssoTokenService {
// Récupère un token valide (en le rafraîchissant si nécessaire)
public Uni<String> getValidAccessToken() {
if (clientId == null || clientSecret == null || clientId.isEmpty() || clientSecret.isEmpty()) {
LOG.error("Client ID ou Client Secret non configuré pour HelloAsso");
return Uni.createFrom().failure(new IllegalStateException("HelloAsso client credentials not configured"));
}
if (currentToken == null || currentToken.isExpired()) {
return fetchNewToken(clientId, clientSecret);
}

View File

@@ -156,24 +156,32 @@ public class KeycloakService {
return oldEmail;
}).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
reactiveMailer.send(
Mail.withText(oldEmail,
"FFSAF - Changement de votre adresse email",
String.format(
"""
Bonjour,
Mail.withHtml(oldEmail, "FFSAF - Changement de votre adresse email",
String.format(Utils.HTML_HEADER, "FFSAF - Changement de votre adresse email") +
String.format("""
<p>Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,<br/>
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.</p>
<p>Ancienne adresse email : <span class="highlight">%s</span><br/>Nouvelle adresse email : <span class="highlight">%s</span></p>
<p>Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
""",
oldEmail, email) +
Utils.HTML_FOOTER
)
.setText(String.format("""
Bonjour,
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
Ancienne adresse email : %s
Nouvelle adresse email : %s
Ancienne adresse email : %s
Nouvelle adresse email : %s
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
Cordialement,
L'équipe de la FFSAF
""", oldEmail, email)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
Cordialement,
L'équipe de la FFSAF
""", oldEmail, email))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
}
@@ -270,13 +278,33 @@ public class KeycloakService {
.invoke(user -> membreModel.setUserId(user.getId()))
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
.call(user -> enabled_email ? reactiveMailer.send(
Mail.withText(user.getEmail(),
"FFSAF - Creation de votre compte sur l'intranet",
String.format(
Mail.withHtml(user.getEmail(), "FFSAF - Creation de votre compte sur l'intranet",
String.format(Utils.HTML_HEADER, "Création de votre compte intranet FFSAF") +
String.format(
"""
<p>Suite &agrave; votre premi&egrave;re inscription <span class="highlight">%s</span> &agrave; la <strong>F&eacute;d&eacute;ration France Soft Armored Fighting (FFSAF)</strong>, votre compte intranet a &eacute;t&eacute; cr&eacute;&eacute;.</p>
<p>Ce compte vous permettra de :</p>
<ul>
<li>Consulter vos informations personnelles,</li>
<li>Vous inscrire aux comp&eacute;titions (bient&ocirc;t disponible),</li>
<li>Consulter les r&eacute;sultats des comp&eacute;titions.</li>
</ul>
<p>L&rsquo;intranet est accessible &agrave; l&rsquo;adresse suivante :</p>
<p style="text-align: center;"><a href="https://intra.ffsaf.fr" class="button">Acc&eacute;der &agrave; l&apos;intranet</a></p>
<p>Votre nom d&rsquo;utilisateur est : <span class="highlight">%s</span></p>
<p>Pour d&eacute;finir votre mot de passe, rendez-vous sur l&rsquo;intranet : <strong>Connexion &gt; Mot de passe oubli&eacute; ?</strong></p>
<p>Si vous n&rsquo;avez pas demand&eacute; cette inscription, veuillez contacter le support &agrave; l&rsquo;adresse : <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
<p class="footer">(Pas de panique, nous ne vous enverrons pas de message autre que ceux concernant votre compte.)</p>
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername()) +
Utils.HTML_FOOTER
)
.setText(String.format(
"""
Bonjour,
Suite à votre première inscription %sà la Fédération Française de Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
Suite à votre première inscription %sà la Fédération France Soft Armored Fighting (FFSAF), votre compte intranet a été créé.
Ce compte vous permettra de consulter vos informations et, dans un futur proche, de vous inscrire aux compétitions ainsi que d'en consulter les résultats.
L'intranet est accessible à l'adresse suivante : https://intra.ffsaf.fr
@@ -291,8 +319,8 @@ public class KeycloakService {
L'équipe de la FFSAF
""",
membreModel.getRole() == RoleAsso.MEMBRE ? "par votre club (" + membreModel.getClub()
.getName() + ") " : "", user.getUsername())
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
.getName() + ") " : "", user.getUsername()))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
) : Uni.createFrom().nullItem())
.call(user -> membreService.setUserId(membreModel.getId(), user.getId()))
.call(user -> setClubGroupMembre(membreModel, membreModel.getClub()));
@@ -349,7 +377,11 @@ public class KeycloakService {
public Optional<UserRepresentation> getUser(UUID userId) {
UserResource user = keycloak.realm(realm).users().get(userId.toString());
return getUserById(userId.toString());
}
public Optional<UserRepresentation> getUserById(String userId) {
UserResource user = keycloak.realm(realm).users().get(userId);
if (user == null)
return Optional.empty();
else

View File

@@ -84,6 +84,26 @@ public class LicenceService {
));
}
public Uni<?> validePaymentLicences(List<Long> ids) {
Uni<String> uni = Uni.createFrom().nullItem();
for (Long id : ids) {
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
.chain(model -> {
if (!model.isPay())
ls.logUpdate("payment (admin) de la licence", model);
return validatePayLicences(model);
}))
.map(__ -> "OK");
}
return uni.call(__ -> ls.append());
}
protected Uni<LicenceModel> validatePayLicences(LicenceModel model) {
model.setPay(true);
return Panache.withTransaction(() -> repository.persist(model));
}
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
if (form.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> {
@@ -121,7 +141,7 @@ public class LicenceService {
}
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
return membreModel -> ((membreModel.getLicence() <= 0) ?
return membreModel -> ((membreModel.getLicence() == null || membreModel.getLicence() <= 0) ?
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
.chain(() -> combRepository.persist(membreModel))
@@ -157,7 +177,7 @@ public class LicenceService {
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
})))
.call(model -> ls.logADelete(model))
.chain(model -> repository.delete(model));
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
}
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {

View File

@@ -58,6 +58,8 @@ public class LoggerService {
}
public Uni<?> append() {
if (buffer.isEmpty())
return Uni.createFrom().voidItem();
return Panache.withTransaction(() -> repository.persist(buffer))
.invoke(__ -> buffer.clear());
}

View File

@@ -7,14 +7,12 @@ import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
import fr.titionfire.ffsaf.net2.request.SReqComb;
import fr.titionfire.ffsaf.rest.data.MeData;
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
@@ -125,8 +123,21 @@ public class MembreService {
return sort;
}
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club,
int licenceRequest, int payState, String order, String categorie) {
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
String order, String categorie, Boolean archive, String subject) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (membreModel == null || membreModel.getClub() == null)
throw new DForbiddenException();
}))
.chain(membreModel ->
searchAdmin(limit, page, search, membreModel.getClub(), licenceRequest, payState, order,
categorie, archive));
}
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, Object club,
int licenceRequest, int payState, String order, String categorie,
Boolean archive) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
@@ -137,77 +148,47 @@ public class MembreService {
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
String finalSearch = search;
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
String archiveFilter;
if (archive == null || archive)
archiveFilter = "True";
else
archiveFilter = String.format(
"EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)",
Utils.getSaison() - 1);
String clubFilter = "(TRUE OR ?3 = ?3)";
if (club != null) {
if (club instanceof String club_) {
if (!club_.isBlank()) {
if (club_.equals("null"))
clubFilter = "club IS NULL AND ?3 = ?3";
else
clubFilter = "LOWER(club.name) LIKE LOWER(?3)";
}
} else if (club instanceof ClubModel)
clubFilter = "club = ?3";
}
String queryStr = String.format(
"SELECT m FROM MembreModel m WHERE id %s ?2 AND %s AND (%s) AND %s AND %s",
(licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN", clubFilter, FIND_NAME_REQUEST,
categorieFilter,
archiveFilter);
Sort sort = getSort(order);
if (sort == null)
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
return baseUni
String finalSearch = search;
return getLicenceListe(licenceRequest, payState)
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
PanacheQuery<MembreModel> query;
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
if (club == null || club.isBlank()) {
query = repository.find(
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, ids)
.page(Page.ofSize(limit));
} else {
if (club.equals("null")) {
query = repository.find(
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, ids).page(Page.ofSize(limit));
} else {
query = repository.find(
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, club, ids)
.page(Page.ofSize(limit));
}
}
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
return getPageResult(query, limit, page);
});
}
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
String order, String categorie, String subject) {
if (search == null)
search = "";
search = "%" + search.replaceAll(" ", "% %") + "%";
String finalSearch = search;
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
String categorieFilter;
if (categorie == null || categorie.isBlank())
categorieFilter = " True";
else
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
Sort sort = getSort(order);
if (sort == null)
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
return baseUni
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
.chain(ids -> {
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> {
PanacheQuery<MembreModel> query = repository.find(
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
sort, finalSearch, membreModel.getClub(), ids)
.page(Page.ofSize(limit));
return getPageResult(query, limit, page);
});
});
}
private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) {
return Uni.createFrom().item(new PageResult<SimpleMembre>())
.invoke(result -> result.setPage(page))
@@ -287,14 +268,16 @@ public class MembreService {
if (model.getEmail() != null && !model.getEmail().isBlank()) {
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
throw new DBadRequestException(
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
}
if (StringSimilarity.similarity(model.getLname().toUpperCase(),
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
throw new DBadRequestException(
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
}
}
@@ -305,7 +288,8 @@ public class MembreService {
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
LOGGER.info("Similar membres found: " + model);
throw new DBadRequestException(
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide.");
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence "
+ model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")");
}
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
@@ -313,8 +297,7 @@ public class MembreService {
dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model);
model.setLname(dataIn.getNom().toUpperCase());
model.setFname(dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1));
model.setFname(Utils.formatPrenom(dataIn.getPrenom()));
if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) {
ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model);
@@ -380,7 +363,7 @@ public class MembreService {
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser");
throw new DBadRequestException("Email déjà utilisé");
})))
.chain(membreModel -> clubRepository.findById(membre.getClub())
.map(club -> new Pair<>(membreModel, club)))
@@ -402,7 +385,7 @@ public class MembreService {
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
.invoke(Unchecked.consumer(c -> {
if (c > 0 && !membre.getEmail().isBlank())
throw new DBadRequestException("Email déjà utiliser");
throw new DBadRequestException("Email déjà utilisé");
})))
.invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
@@ -434,7 +417,7 @@ public class MembreService {
private Uni<String> update(Uni<MembreModel> uni, FullMemberForm membre, boolean admin) {
return uni.chain(target -> {
ls.logChange("Prénom", target.getFname(), membre.getFname(), target);
target.setFname(membre.getFname());
target.setFname(Utils.formatPrenom(membre.getFname()));
ls.logChange("Nom", target.getLname(), membre.getLname(), target);
target.setLname(membre.getLname().toUpperCase());
ls.logChange("Pays", target.getCountry(), membre.getCountry(), target);
@@ -482,7 +465,8 @@ public class MembreService {
.call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3",
(membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel,
Utils.getSaison()))
.call(membreModel -> membre.getPhoto_data().length > 0 ? ls.logAUpdate("Photo",
.call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data()
.size() > 0) ? ls.logAUpdate("Photo",
membreModel) : Uni.createFrom().nullItem())
.map(__ -> "OK");
}
@@ -492,7 +476,8 @@ public class MembreService {
return clubRepository.findById(input.getClub())
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> {
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank())
throw new DBadRequestException("Email déjà utilisé");
})))
.chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel);
@@ -508,7 +493,7 @@ public class MembreService {
return repository.find("userId = ?1", subject).firstResult()
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
.invoke(Unchecked.consumer(c -> {
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
if (c > 0) throw new DBadRequestException("Email déjà utilisé");
})))
.call(membreModel ->
repository.count(
@@ -577,8 +562,8 @@ public class MembreService {
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
MembreModel model = new MembreModel();
model.setFname(input.getFname());
model.setLname(input.getLname());
model.setFname(Utils.formatPrenom(input.getFname()));
model.setLname(input.getLname().toUpperCase());
model.setEmail(input.getEmail());
model.setLicence(null);
model.setGenre(input.getGenre());
@@ -602,12 +587,29 @@ public class MembreService {
MeData meData = new MeData();
return repository.find("userId = ?1", subject).firstResult()
.invoke(meData::setMembre)
.chain(membreModel -> Mutiny.fetch(membreModel.getLicences()))
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
.invoke(meData::setLicences)
.call(membreModel -> Mutiny.fetch(membreModel.getLicences())
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
.invoke(meData::setLicences))
.call(membreModel -> Mutiny.fetch(membreModel.getSelections())
.map(licences -> licences.stream().map(SimpleSelection::fromModel).toList())
.invoke(meData::setSelections))
.map(__ -> meData);
}
public Uni<Void> updateSettings(String subject, UserSettingForm settingForm) {
return repository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(membreModel -> {
if (settingForm.getResultPrivacy() != null) {
ls.logChange("Confidentialité des résultats", membreModel.getResultPrivacy(),
settingForm.getResultPrivacy(), membreModel);
membreModel.setResultPrivacy(settingForm.getResultPrivacy());
}
}))
.call(membreModel -> Panache.withTransaction(() -> repository.persist(membreModel)))
.call(membreModel -> ls.append())
.replaceWithVoid();
}
@Scheduled(cron = "0 0 1 1 9 ?")
Uni<Void> everySeason() {
return repository.list("birth_date IS NOT NULL")
@@ -617,5 +619,4 @@ public class MembreService {
}).toList()).andCollectFailures())
.map(__ -> null);
}
}

View File

@@ -1,11 +1,9 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
@@ -35,6 +33,12 @@ public class ResultService {
@Inject
MembreService membreService;
@Inject
ClubRepository clubRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CategoryRepository categoryRepository;
@@ -43,52 +47,110 @@ public class ResultService {
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");
private static final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) {
synchronized (combTempIds) {
if (!combTempIds.containsKey(key)) {
combTempIds.put(key, UUID.randomUUID().toString());
}
return combTempIds.get(key);
}
}
private static Long getCombTempId(String value) {
synchronized (combTempIds) {
for (Map.Entry<Long, String> entry : combTempIds.entrySet()) {
if (entry.getValue().equals(value)) {
return entry.getKey();
}
}
return null;
}
}
private static final HashMap<String, Long> clubTempIds = new HashMap<>();
private static Long getClubTempId(String key) {
synchronized (clubTempIds) {
if (!clubTempIds.containsKey(key)) {
clubTempIds.put(key, (System.currentTimeMillis() + clubTempIds.size()) * -1);
}
return clubTempIds.get(key);
}
}
private static String getClubTempId(Long value) {
synchronized (clubTempIds) {
for (Map.Entry<String, Long> entry : clubTempIds.entrySet()) {
if (entry.getValue().equals(value)) {
return entry.getKey();
}
}
return null;
}
}
public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
return membreService.getByAccountId(securityCtx.getSubject())
.chain(m -> registerRepository.list("membre = ?1", m))
.chain(m -> registerRepository.list(
"membre = ?1 OR (TRUE = ?2 AND membre.club = ?3)",
m, securityCtx.isClubAdmin(), m.getClub()))
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().call(r -> Mutiny.fetch(r.getCompetition()))
.onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(),
r.getCompetition().getDate()})
.collect().asList();
.onItem().transform(RegisterModel::getCompetition)
.collect().asList()
.chain(l -> compRepository.list("owner = ?1 OR ?1 IN admin", securityCtx.getSubject())
.map(l2 -> Stream.concat(l.stream(), l2.stream()).distinct()
.map(c -> new Object[]{c.getUuid(), c.getName(), c.getDate()}).toList())
);
}
public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(m -> categoryRepository.list("compet.uuid = ?1", uuid)
.chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True
m.getMembre(), cats)))
.map(matchModels -> {
HashMap<Long, List<MatchModel>> map = new HashMap<>();
for (MatchModel matchModel : matchModels) {
if (!map.containsKey(matchModel.getCategory().getId()))
map.put(matchModel.getCategory().getId(), new ArrayList<>());
map.get(matchModel.getCategory().getId()).add(matchModel);
}
return map.values();
})
.onItem()
.transformToMulti(Multi.createFrom()::iterable)
.onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
.onItem().transform(this::getData)
.collect().asList();
public Uni<HashMap<String, Long>> getCategoryList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid));
}
private ResultCategoryData getData(List<MatchModel> matchModels) {
public Uni<HashMap<String, Long>> getCategoryList(String uuid) {
return categoryRepository.list("compet.uuid = ?1", uuid)
.map(categoryModels -> {
HashMap<String, Long> map = new HashMap<>();
categoryModels.stream()
.sorted(Comparator.comparing(CategoryModel::getName))
.forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId()));
return map;
});
}
public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel ->
matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Mutiny.fetch(list.get(0).getCategory().getTree()))
.map(list -> getData(list, membreModel)));
}
public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Mutiny.fetch(list.get(0).getCategory().getTree()))
.map(list -> getData(list, null));
}
private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) {
ResultCategoryData out = new ResultCategoryData();
CategoryModel categoryModel = matchModels.get(0).getCategory();
out.setName(categoryModel.getName());
out.setType(categoryModel.getType());
out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";"));
out.setGenTime(System.currentTimeMillis());
getArray2(matchModels, out);
getTree(categoryModel.getTree(), out);
getArray2(matchModels, membreModel, out);
getTree(categoryModel.getTree(), membreModel, out);
return out;
}
private void getArray2(List<MatchModel> matchModels_, ResultCategoryData out) {
private void getArray2(List<MatchModel> matchModels_, MembreModel membreModel, ResultCategoryData out) {
List<MatchModel> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
HashMap<Character, List<MatchModel>> matchMap = new HashMap<>();
@@ -102,31 +164,30 @@ public class ResultService {
matchMap.forEach((c, matchEntities) -> {
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
.sorted(Comparator.comparing(MatchModel::getCategory_ord))
.map(ResultCategoryData.PouleArrayData::fromModel)
.map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
.toList();
List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
.distinct()
.map(combName -> {
.filter(Objects::nonNull)
.map(comb -> {
AtomicInteger w = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger(0);
AtomicInteger pointTake = new AtomicInteger(0);
matchEntities.stream()
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName) || m.getC2Name()
.equals(combName)))
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((matchModel.getC1Name()
.equals(combName) && win > 0) || matchModel.getC2Name()
.equals(combName) && win < 0)
if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0)
w.getAndIncrement();
for (ScoreEmbeddable score : matchModel.getScores()) {
if (score.getS1() <= -900 || score.getS2() <= -900)
continue;
if (matchModel.getC1Name().equals(combName)) {
if (matchModel.isC1(comb)) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
@@ -137,7 +198,8 @@ public class ResultService {
});
float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();
return new ResultCategoryData.RankArray(0, combName, w.get(),
return new ResultCategoryData.RankArray(0,
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), w.get(),
pointMake.get(), pointTake.get(), pointRate);
})
.sorted(Comparator
@@ -161,111 +223,259 @@ public class ResultService {
});
}
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst) {
dst.setData(ResultCategoryData.TreeData.from(src.getMatch()));
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel,
ResultPrivacy privacy) {
dst.setData(ResultCategoryData.TreeData.from(src.getMatch(), membreModel, privacy));
if (src.getLeft() != null) {
dst.setLeft(new TreeNode<>());
convertTree(src.getLeft(), dst.getLeft());
convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy);
}
if (src.getRight() != null) {
dst.setRight(new TreeNode<>());
convertTree(src.getRight(), dst.getRight());
convertTree(src.getRight(), dst.getRight(), membreModel, privacy);
}
}
private void getTree(List<TreeModel> treeModels, ResultCategoryData out) {
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, ResultCategoryData out) {
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
convertTree(treeModel, root);
trees.add(root);
});
treeModels.stream()
.filter(t -> t.getLevel() != 0)
.sorted(Comparator.comparing(TreeModel::getLevel))
.forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS);
trees.add(root);
});
out.setTrees(trees);
}
public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid)
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
.chain(membreModel -> getAllCombArray_(uuid, membreModel));
}
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
public Uni<CombsArrayData> getAllCombArrayPublic(String uuid) {
return getAllCombArray_(uuid, null);
}
List<CombsArrayData.CombsData> combs = matchModels.stream()
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
.filter(Objects::nonNull)
.distinct()
.map(combName -> {
var builder2 = CombsArrayData.CombsData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) {
return registerRepository.list("competition.uuid = ?1", uuid)
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
matchModels.stream()
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName)
|| m.getC2Name().equals(combName)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((combName.equals(matchModel.getC1Name()) && win > 0) ||
combName.equals(matchModel.getC2Name()) && win < 0) {
w.getAndIncrement();
} else {
l.getAndIncrement();
}
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> {
if (combName.equals(matchModel.getC1Name())) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
});
});
List<CombsArrayData.CombsData> combs = matchModels.stream()
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
.filter(Objects::nonNull)
.distinct()
.map(comb -> {
var builder2 = CombsArrayData.CombsData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
Categorie categorie = null;
ClubModel club = null;
makeStat(matchModels, comb, w, l, pointMake, pointTake);
Optional<RegisterModel> register = registers.stream()
.filter(r -> r.getName().equals(combName)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie();
club = register.get().getClub2();
}
Categorie categorie = null;
String clubName = null;
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(combName);
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName());
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
Optional<RegisterModel> register = registers.stream()
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie2();
ClubModel club = register.get().getClub2();
clubName = (club == null) ? BUNDLE.getString("no.licence") : club.getName();
} else if (comb instanceof CompetitionGuestModel guestModel) {
categorie = guestModel.getCategorie();
clubName = guestModel.getClub();
} else if (comb instanceof MembreModel model) {
categorie = model.getCategorie();
clubName = (model.getClub() == null) ? BUNDLE.getString(
"no.licence") : model.getClub().getName();
}
return builder2.build();
})
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
.toList();
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.club(clubName);
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
builder.nb_insc(combs.size());
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
builder.combs(combs);
return builder2.build();
})
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
.toList();
return builder.build();
builder.nb_insc(combs.size());
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
builder.combs(combs);
return builder.build();
});
}
public Uni<HashMap<String, String>> getCombList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(membreModel -> getCombList(uuid, ResultPrivacy.REGISTERED_ONLY));
}
public Uni<HashMap<String, String>> getCombList(String uuid) {
return getCombList(uuid, ResultPrivacy.PUBLIC);
}
private Uni<HashMap<String, String>> getCombList(String uuid, ResultPrivacy privacy) {
return registerRepository.list("competition.uuid = ?1 AND membre.resultPrivacy <= ?2", uuid, privacy)
.map(models -> {
HashMap<String, String> map = new HashMap<>();
models.forEach(
r -> map.put(Utils.getFullName(r.getMembre()), getCombTempId(r.getMembre().getId())));
return map;
})
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
.map(models -> {
models.forEach(guestModel -> map.put(Utils.getFullName(guestModel),
getCombTempId(guestModel.getId() * -1)));
return map;
})
);
}
public Uni<?> getCombArrayPublic(String uuid, String combTempId, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(membreModel -> getCombArrayPublic(uuid, combTempId, ResultPrivacy.REGISTERED_ONLY));
}
public Uni<?> getCombArrayPublic(String uuid, String combTempId) {
return getCombArrayPublic(uuid, combTempId, ResultPrivacy.PUBLIC);
}
private Uni<?> getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) {
CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder();
Long id = getCombTempId(combTempId);
if (id == null) {
return Uni.createFrom().failure(new DForbiddenException("Comb not found"));
}
Uni<List<MatchModel>> uni;
if (id >= 0) {
uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
uuid, privacy).firstResult()
.chain(Unchecked.function(registerModel -> {
if (registerModel == null)
throw new DBadRequestException("Combattant non inscrit");
builder.name(Utils.getFullName(registerModel.getMembre()));
builder.club((registerModel.getClub2() == null) ? BUNDLE.getString(
"no.licence") : registerModel.getClub2().getName());
builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2()
.getName(BUNDLE));
return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid,
registerModel.getMembre());
}));
} else {
uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
.chain(guestModel -> {
builder.name(Utils.getFullName(guestModel));
builder.club(guestModel.getClub());
builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie()
.getName(BUNDLE));
return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)",
uuid, guestModel);
});
}
return uni.invoke(matchModels -> {
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct()
.toList();
List<CombArrayData.MatchsData> matchs = new ArrayList<>();
AtomicInteger sumW = new AtomicInteger();
AtomicInteger sumPointMake = new AtomicInteger(0);
AtomicInteger sumPointTake = new AtomicInteger(0);
for (MatchModel matchModel : matchModels) {
if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
(matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
continue;
var builder2 = CombArrayData.MatchsData.builder();
builder2.date(matchModel.getDate());
builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory()))
.map(CategoryModel::getName).findFirst().orElse(""));
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
if (matchModel.isC1(id)) {
builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest()));
if (matchModel.isEnd()) {
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS1());
pointTake.addAndGet(scoreEntity.getS2());
});
builder2.score(matchModel.getScores().stream()
.map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
} else {
builder2.score(new ArrayList<>());
}
builder2.win(matchModel.win() > 0);
} else {
builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest()));
if (matchModel.isEnd()) {
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS2());
pointTake.addAndGet(scoreEntity.getS1());
});
builder2.score(matchModel.getScores().stream()
.map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
} else {
builder2.score(new ArrayList<>());
}
builder2.win(matchModel.win() < 0);
}
builder2.ratio(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
sumPointMake.addAndGet(pointMake.get());
sumPointTake.addAndGet(pointTake.get());
matchs.add(builder2.build());
if (builder2.win)
sumW.getAndIncrement();
}
builder.totalWin(sumW.get());
builder.pointRatio(
(sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get());
builder.pointMake(sumPointMake.get());
builder.pointTake(sumPointTake.get());
matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule)
.thenComparing(CombArrayData.MatchsData::adv));
builder.matchs(matchs);
})
.map(__ -> builder.build());
}
@Builder
@RegisterForReflection
@@ -277,98 +487,159 @@ public class ResultService {
}
}
public Uni<ClubArrayData> getClubArray(String uuid, SecurityCtx securityCtx) {
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
@Builder
@RegisterForReflection
public static record CombArrayData(String name, String club, String cat, int totalWin,
float pointRatio, int pointMake, int pointTake, List<MatchsData> matchs) {
@Builder
@RegisterForReflection
public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
boolean win) {
}
}
return hasAccess(uuid, securityCtx)
.invoke(cm_register -> builder.name(cm_register.getClub2().getName()))
.chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid,
cm_register.getClub2())
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
public Uni<HashMap<String, Long>> getClubList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid));
}
builder.nb_insc(registers.size());
AtomicInteger tt_win = new AtomicInteger(0);
AtomicInteger tt_match = new AtomicInteger(0);
List<ClubArrayData.CombData> combData = registers.stream()
.map(register -> {
var builder2 = ClubArrayData.CombData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
matchModels.stream()
.filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id())
|| register.getMembre().equals(m.getC2_id())))
.forEach(matchModel -> {
int win = matchModel.win();
if ((register.getMembre()
.equals(matchModel.getC1_id()) && win > 0) ||
register.getMembre()
.equals(matchModel.getC2_id()) && win < 0) {
w.getAndIncrement();
} else {
l.getAndIncrement();
}
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> {
if (register.getMembre()
.equals(matchModel.getC1_id())) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
});
});
Categorie categorie = register.getCategorie();
if (categorie == null)
categorie = register.getMembre().getCategorie();
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(register.getName());
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
tt_win.addAndGet(w.get());
tt_match.addAndGet(w.get() + l.get());
return builder2.build();
})
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
.toList();
builder.nb_match(tt_match.get());
builder.match_w(tt_win.get());
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
builder.combs(combData);
return builder.build();
public Uni<HashMap<String, Long>> getClubList(String uuid) {
return registerRepository.list("competition.uuid = ?1", uuid)
.map(registers -> {
HashMap<String, Long> registerMap = new HashMap<>();
registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull)
.forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId()));
return registerMap;
})
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
.map(guests -> {
guests.stream().map(CompetitionGuestModel::getClub).distinct()
.filter(Objects::nonNull)
.forEach(guestClub -> map.putIfAbsent(guestClub,
getClubTempId(guestClub)));
return map;
})
);
}
public Uni<ClubArrayData> getClubArray(String uuid, Long id, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> getClubArray2(uuid, id, membreModel));
}
public Uni<ClubArrayData> getClubArray2(String uuid, Long id, MembreModel membreModel) {
if (id < 0) {
String clubName = getClubTempId(id);
if (clubName == null) {
return Uni.createFrom().failure(new DForbiddenException("Club not found"));
}
return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName)
.call(list -> {
if (list.isEmpty())
return Uni.createFrom().failure(new DBadRequestException("Club not found"));
return Uni.createFrom().voidItem();
})
.chain(guests -> matchRepository.list(
"category.compet.uuid = ?1 AND (c1_guest IN ?2 OR c2_guest IN ?2)", uuid, guests)
.map(matchModels ->
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
matchModels, new ArrayList<>(), membreModel)));
} else {
return clubRepository.findById(id).chain(clubModel ->
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
.map(matchModels ->
getClubArray2(clubModel.getName(),
registers.stream().map(o -> (CombModel) o.getMembre()).toList(),
matchModels, registers, membreModel))));
}
}
private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModel> matchModels,
List<RegisterModel> registers, MembreModel membreModel) {
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
builder.name(name);
builder.nb_insc(combs.size());
AtomicInteger tt_win = new AtomicInteger(0);
AtomicInteger tt_match = new AtomicInteger(0);
List<ClubArrayData.CombData> combData = combs.stream().map(comb -> {
var builder2 = ClubArrayData.CombData.builder();
AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
makeStat(matchModels, comb, w, l, pointMake, pointTake);
Categorie categorie = null;
Optional<RegisterModel> register = registers.stream()
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie2();
} else if (comb instanceof CompetitionGuestModel guestModel) {
categorie = guestModel.getCategorie();
} else if (comb instanceof MembreModel model) {
categorie = model.getCategorie();
}
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(w.get());
builder2.l(l.get());
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint((pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
tt_win.addAndGet(w.get());
tt_match.addAndGet(w.get() + l.get());
return builder2.build();
})
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
.toList();
builder.nb_match(tt_match.get());
builder.match_w(tt_win.get());
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
builder.combs(combData);
return builder.build();
}
private static void makeStat(List<MatchModel> matchModels, CombModel comb, AtomicInteger w, AtomicInteger l,
AtomicInteger pointMake, AtomicInteger pointTake) {
matchModels.stream()
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
w.getAndIncrement();
} else {
l.getAndIncrement();
}
matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> {
if (matchModel.isC1(comb)) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
});
});
}
@Builder
@RegisterForReflection
public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire,
@@ -380,21 +651,35 @@ public class ResultService {
}
}
private Uni<RegisterModel> hasAccess(String uuid, SecurityCtx securityCtx) {
private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
.firstResult()
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DForbiddenException("Access denied");
}));
}
.chain(Unchecked.function(o -> {
if (o != null)
return Uni.createFrom().item(o.getMembre());
private Uni<RegisterModel> hasAccess(Long compId, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId)
.firstResult()
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DForbiddenException("Access denied");
return membreService.getByAccountId(securityCtx.getSubject()).chain(m -> {
if (securityCtx.isClubAdmin()) {
return registerRepository.count("membre.club = ?2 AND competition.uuid = ?1",
uuid, m.getClub()).chain(c -> {
if (c > 0) return Uni.createFrom().item(m);
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)",
uuid, securityCtx.getSubject())
.chain(c2 -> {
if (c2 > 0) return Uni.createFrom().item(m);
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
});
});
} else {
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", uuid,
securityCtx.getSubject())
.chain(c2 -> {
if (c2 > 0) return Uni.createFrom().item(m);
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
});
}
});
}));
}
}

View File

@@ -0,0 +1,64 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.LogModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.model.SelectionModel;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.data.repository.SelectionRepository;
import fr.titionfire.ffsaf.rest.data.SimpleSelection;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.List;
import java.util.function.Consumer;
@WithSession
@ApplicationScoped
public class SelectionService {
@Inject
CombRepository combRepository;
@Inject
SelectionRepository repository;
@Inject
LoggerService ls;
public Uni<List<SelectionModel>> getSelection(long id, Consumer<MembreModel> checkPerm) {
return combRepository.findById(id).invoke(checkPerm)
.chain(combRepository -> Mutiny.fetch(combRepository.getSelections()));
}
public Uni<SelectionModel> setSelection(long id, SimpleSelection data) {
if (data.getId() == -1) {
return combRepository.findById(id).chain(membreModel -> {
SelectionModel model = new SelectionModel();
model.setMembre(membreModel);
model.setSaison(data.getSaison());
model.setCategorie(data.getCategorie());
return Panache.withTransaction(() -> repository.persist(model))
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
licenceModel));
});
} else {
return repository.findById(data.getId()).chain(model -> {
ls.logChange("Catégorie", model.getCategorie(), data.getCategorie(), model);
model.setCategorie(data.getCategorie());
return Panache.withTransaction(() -> repository.persist(model))
.call(__ -> ls.append());
});
}
}
public Uni<Void> deleteSelection(long id) {
return repository.findById(id)
.call(model -> ls.logADelete(model))
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
}
}

View File

@@ -36,7 +36,7 @@ public class StatsService {
LicenceStats.YearStats yearStats = stats.getLicences().get(licence.getSaison());
System.out.println("stats: " + licence.getMembre().getFname());
// System.out.println("stats: " + licence.getMembre().getFname());
if (licence.isValidate()) {
if (licence.getMembre().getGenre() == Genre.H)
yearStats.addH();

View File

@@ -0,0 +1,23 @@
package fr.titionfire.ffsaf.domain.service;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.HashMap;
@ApplicationScoped
public class UpdateService { // For public result page
static HashMap<Long, Long> lastUpdate = new HashMap<>();
public void setNewData(long id) {
lastUpdate.put(id, System.currentTimeMillis());
}
public boolean needUpdate(long id, long last_update) {
if (!lastUpdate.containsKey(id)) {
lastUpdate.put(id, System.currentTimeMillis() - 5000);
return true;
}
return lastUpdate.getOrDefault(id, 0L) > last_update;
}
}

View File

@@ -0,0 +1,28 @@
package fr.titionfire.ffsaf.domain.service;
import io.quarkiverse.antivirus.runtime.Antivirus;
import io.quarkiverse.antivirus.runtime.AntivirusScanResult;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.InputStream;
import java.util.List;
@ApplicationScoped
public class VirusScannerService {
@Inject
Antivirus antivirus;
public Uni<List<AntivirusScanResult>> scanFileReactive(String fileName, InputStream inputStream) {
System.out.println("Starting reactive virus scan for file: " + fileName);
// Wrap the blocking antivirus scan in a reactive context
// This moves the blocking operation to a worker thread
return Uni.createFrom().item(() -> {
System.out.println("Scanning file on worker thread: " + fileName);
return antivirus.scan(fileName, inputStream);
}).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool());
}
}

View File

@@ -49,7 +49,7 @@ public class AffiliationEndpoints {
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les affiliations pour un club", description = "Cette méthode renvoie les affiliations pour un club donné. Seuls les administrateurs de la fédération et les présidents, secrétaires et responsables intranet du club peuvent accéder à cette méthode.")
@APIResponses(value = {

View File

@@ -73,7 +73,7 @@ public class AffiliationRequestEndpoints {
@GET
@Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie une demande d'affiliation", description = "Cette méthode renvoie une demande d'affiliation " +
"pour l'identifiant spécifié.")
@@ -92,7 +92,7 @@ public class AffiliationRequestEndpoints {
@DELETE
@Path("/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Supprime une demande d'affiliation", description = "Cette méthode supprime une demande " +
"d'affiliation pour l'identifiant spécifié.")
@@ -128,7 +128,7 @@ public class AffiliationRequestEndpoints {
@PUT
@Path("/edit")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Modifie une demande d'affiliation", description = "Cette méthode modifie une demande " +

View File

@@ -3,10 +3,10 @@ package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.domain.service.ClubService;
import fr.titionfire.ffsaf.domain.service.PDFService;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.rest.data.*;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullClubForm;
import fr.titionfire.ffsaf.rest.from.PartClubForm;
import fr.titionfire.ffsaf.utils.Contact;
@@ -47,6 +47,9 @@ public class ClubEndpoints {
@Inject
SecurityCtx securityCtx;
@Inject
VirusScannerService scannerService;
@ConfigProperty(name = "upload_dir")
String media;
@@ -109,7 +112,7 @@ public class ClubEndpoints {
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " +
"détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations")
@@ -121,9 +124,8 @@ public class ClubEndpoints {
})
public Uni<SimpleClub> getById(
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel).invoke(m -> {
m.setContactMap(Contact.toSite());
});
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel)
.invoke(m -> m.setContactMap(Contact.toSite()));
}
@PUT
@@ -145,25 +147,9 @@ public class ClubEndpoints {
return clubService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
})).chain(() -> {
if (input.getLogo().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK"))
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
})); // TODO log
else
return Uni.createFrom().nullItem();
}).chain(() -> {
if (input.getStatus().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK"))
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
})); // TODO log
else
return Uni.createFrom().nullItem();
});
}))
.chain(() -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
.chain(() -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
}
@PUT
@@ -181,19 +167,9 @@ public class ClubEndpoints {
return clubService.add(input)
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to create club data");
})).call(id -> {
if (input.getLogo().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub"
)); // TODO log
else
return Uni.createFrom().nullItem();
}).call(id -> {
if (input.getStatus().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus"
)); // TODO log
else
return Uni.createFrom().nullItem();
});
}))
.call(id -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
.call(id -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
}
@DELETE
@@ -230,7 +206,7 @@ public class ClubEndpoints {
@GET
@Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " +
"informations du club de l'utilisateur connecté")
@@ -247,7 +223,7 @@ public class ClubEndpoints {
@PUT
@Path("/me")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" +
@@ -264,7 +240,7 @@ public class ClubEndpoints {
@GET
@Path("/me/affiliation")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté", description =
"Renvoie l'attestation d'affiliation du club de l'utilisateur connecté")
@APIResponses(value = {
@@ -296,7 +272,7 @@ public class ClubEndpoints {
@GET
@Path("/renew/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(hidden = true)
public Uni<RenewAffData> getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id,
@@ -307,7 +283,7 @@ public class ClubEndpoints {
@GET
@Path("/desk/{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " +
"du bureau du club spécifié")
@@ -345,7 +321,7 @@ public class ClubEndpoints {
@GET
@Path("{id}/status")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le statut du club"),

View File

@@ -71,6 +71,14 @@ public class CompetitionEndpoints {
return service.getSafcaData(securityCtx, id);
}
@GET
@Path("{id}/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleCompetData> getInternalData(@PathParam("id") Long id) {
return service.getInternalData(securityCtx, id);
}
@GET
@Path("all")
@@ -95,6 +103,14 @@ public class CompetitionEndpoints {
return service.setSafcaData(securityCtx, data);
}
@POST
@Path("/internalData")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> setInternalData(SimpleCompetData data) {
return service.setInternalData(securityCtx, data);
}
@DELETE
@Path("{id}")
@Authenticated

View File

@@ -37,7 +37,7 @@ public class CompteEndpoints {
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie les informations d'un compte utilisateur", description = "Renvoie les informations d'un" +
" compte utilisateur en fonction de son identifiant long (UUID)")
@APIResponses(value = {

View File

@@ -0,0 +1,84 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.ResultService;
import fr.titionfire.ffsaf.domain.service.UpdateService;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.HashMap;
@Path("api/public/result/{id}")
public class ExternalResultEndpoints {
@Inject
ResultService resultService;
@Inject
UpdateService updateService;
@PathParam("id")
private String id;
@GET
@Path("/poule/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, Long>> list() {
return resultService.getCategoryList(id);
}
@GET
@Path("/poule/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getArray(@QueryParam("poule") long poule, @DefaultValue("-1") @QueryParam("rf") long rf) {
if (poule == 0)
return Uni.createFrom().voidItem();
if (updateService.needUpdate(poule, rf)) {
return resultService.getCategory(id, poule);
} else {
return Uni.createFrom().voidItem();
}
}
@GET
@Path("/comb/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, String>> combList() {
return resultService.getCombList(id);
}
@GET
@Path("/comb/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getArray(@QueryParam("comb") String comb) {
if (comb.equals("0"))
return Uni.createFrom().item("");
return resultService.getCombArrayPublic(id, comb);
}
@GET
@Path("/comb/get_all")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getAll() {
return resultService.getAllCombArrayPublic(id);
}
@GET
@Path("/club/list")
@Produces(MediaType.APPLICATION_JSON)
public Uni<HashMap<String, Long>> clubList() {
return resultService.getClubList(id);
}
@GET
@Path("/club/data")
@Produces(MediaType.APPLICATION_JSON)
public Uni<?> getClubArray(@QueryParam("club") long club) {
if (club == 0)
return Uni.createFrom().item("");
return resultService.getClubArray2(id, club, null);
}
}

View File

@@ -36,7 +36,7 @@ public class LicenceEndpoints {
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les licences d'un membre", description = "Renvoie les licences d'un membre en fonction " +
"de son identifiant")
@@ -68,7 +68,7 @@ public class LicenceEndpoints {
@GET
@Path("current/club")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les licences de la saison en cours (pour les clubs)", description = "Renvoie les " +
"licences de la saison en cours (pour les clubs)")
@@ -83,7 +83,7 @@ public class LicenceEndpoints {
@POST
@Path("pay")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Paiement des licence", description = "Retourne le lien de paiement pour les licence des membre fournie")
@@ -101,7 +101,7 @@ public class LicenceEndpoints {
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Créer une licence", description = "Créer unr licence en fonction de son identifiant et des " +
@Operation(summary = "Créer une licence", description = "Créer une licence en fonction de son identifiant et des " +
"informations fournies dans le formulaire (pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La licence a été mise à jour avec succès"),
@@ -118,7 +118,7 @@ public class LicenceEndpoints {
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Validation licence", description = "Valide en masse les licence de l'année en cours (pour les administrateurs)")
@Operation(summary = "Validation licence", description = "Valide en masse les licences de l'année en cours (pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@@ -128,6 +128,21 @@ public class LicenceEndpoints {
return licenceService.valideLicences(ids);
}
@POST
@Path("validate-pay")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Validation du payment des licences", description = "Valide en masse le payment des licences de l'année en cours (pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les licences ont été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> validePaymentLicences(@Parameter(description = "Id des membres a valider") List<Long> ids) {
return licenceService.validePaymentLicences(ids);
}
@DELETE
@Path("{id}")
@RolesAllowed("federation_admin")
@@ -146,7 +161,7 @@ public class LicenceEndpoints {
@POST
@Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "Demande une nouvelle licence", description = "Demande une nouvelle licence en fonction de" +
@@ -163,7 +178,7 @@ public class LicenceEndpoints {
@DELETE
@Path("club/{id}")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Supprime une demande de licence", description = "Supprime une demande de licence en fonction " +
"de son identifiant (pour les clubs)")

View File

@@ -2,6 +2,7 @@ package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.service.MembreService;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
@@ -21,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.function.Consumer;
@@ -29,10 +31,14 @@ import java.util.function.Consumer;
@Path("api/member")
@RolesAllowed({"federation_admin"})
public class MembreAdminEndpoints {
private static final Logger LOGGER = Logger.getLogger(MembreAdminEndpoints.class);
@Inject
MembreService membreService;
@Inject
VirusScannerService scannerService;
@ConfigProperty(name = "upload_dir")
String media;
@@ -59,6 +65,7 @@ public class MembreAdminEndpoints {
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Club à filter") @QueryParam("club") String club,
@Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@Parameter(description = "Inclure les archive") @QueryParam("archive") Boolean archive,
@Parameter(description = "État de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
@Parameter(description = "État du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment,
@Parameter(description = "Ordre") @QueryParam("order") String order) {
@@ -66,7 +73,8 @@ public class MembreAdminEndpoints {
limit = 50;
if (page == null || page < 1)
page = 1;
return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order, categorie);
return membreService.searchAdmin(limit, page - 1, search, club, licenceRequest, payment, order,
categorie, archive);
}
@GET
@@ -95,16 +103,8 @@ public class MembreAdminEndpoints {
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK"))
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
})).chain(() -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK"))
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
}));
else
return Uni.createFrom().nullItem();
});
}))
.call(__ -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre"));
}
@POST
@@ -121,13 +121,8 @@ public class MembreAdminEndpoints {
return membreService.add(input)
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
));
else
return Uni.createFrom().nullItem();
});
}))
.call(id -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre"));
}
@DELETE

View File

@@ -1,9 +1,9 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.MembreService;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.SecurityCtx;
@@ -24,7 +24,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.util.List;
@Tag(name = "Membre club", description = "Gestion des membres (pour les clubs)")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Path("api/member")
public class MembreClubEndpoints {
@@ -37,6 +37,9 @@ public class MembreClubEndpoints {
@Inject
SecurityCtx securityCtx;
@Inject
VirusScannerService scannerService;
@GET
@Path("/find/club")
@Produces(MediaType.APPLICATION_JSON)
@@ -51,6 +54,7 @@ public class MembreClubEndpoints {
@Parameter(description = "Page à consulter") @QueryParam("page") Integer page,
@Parameter(description = "Text à rechercher") @QueryParam("search") String search,
@Parameter(description = "Catégorie à filter") @QueryParam("categorie") String categorie,
@Parameter(description = "Inclure les archive") @QueryParam("archive") Boolean archive,
@Parameter(description = "Etat de la demande de licence: 0 -> sans demande, 1 -> avec demande ou validée, 2 -> toute les demande non validée, 3 -> validée, 4 -> tout, 5 -> demande complete, 6 -> demande incomplete") @QueryParam("licenceRequest") int licenceRequest,
@Parameter(description = "Etat du payment: 0 -> non payer, 1 -> payer, 2 -> tout") @QueryParam("payment") int payment,
@Parameter(description = "Ordre") @QueryParam("order") String order) {
@@ -58,7 +62,8 @@ public class MembreClubEndpoints {
limit = 50;
if (page == null || page < 1)
page = 1;
return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, securityCtx.getSubject());
return membreService.search(limit, page - 1, search, licenceRequest, payment, order, categorie, archive,
securityCtx.getSubject());
}
@GET
@@ -106,16 +111,7 @@ public class MembreClubEndpoints {
return membreService.update(id, input, securityCtx)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
})).chain(() -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK"))
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
}));
else
return Uni.createFrom().nullItem();
});
})).chain(() -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre"));
}
@POST
@@ -133,13 +129,7 @@ public class MembreClubEndpoints {
return membreService.add(input, securityCtx.getSubject())
.invoke(Unchecked.consumer(id -> {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
));
else
return Uni.createFrom().nullItem();
});
})).call(id -> Utils.uploadFile(scannerService, input.getPhoto_data(), id, media, "ppMembre"));
}
@DELETE

View File

@@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.domain.service.PDFService;
import fr.titionfire.ffsaf.rest.data.MeData;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.security.Authenticated;
@@ -51,7 +52,7 @@ public class MembreEndpoints {
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les détails d'un membre en fonction de son identifiant", description = "Renvoie les " +
"détails d'un membre en fonction de son identifiant")
@@ -68,7 +69,7 @@ public class MembreEndpoints {
@GET
@Path("/find/licence")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie les détails d'un membre en fonction de son numéro de licence", description = "Renvoie " +
"les détails d'un membre en fonction de son numéro de licence")
@APIResponses(value = {
@@ -113,6 +114,21 @@ public class MembreEndpoints {
return pdfService.getLicencePdf(securityCtx.getSubject());
}
@PUT
@Path("me/setting")
@Authenticated
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Met à jour les paramètres du membre connecté", description = "Met à jour les paramètres du membre connecté")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Les paramètres ont été mis à jour"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<Void> updateMeSettings(UserSettingForm settingForm) {
return membreService.updateSettings(securityCtx.getSubject(), settingForm);
}
@GET
@Path("me/photo")
@Authenticated
@@ -130,7 +146,7 @@ public class MembreEndpoints {
@GET
@Path("{id}/photo")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie la photo d'un membre", description = "Renvoie la photo d'un membre en fonction de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La photo du membre"),
@@ -146,7 +162,7 @@ public class MembreEndpoints {
@GET
@Path("{id}/licence")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
@Operation(summary = "Renvoie le pdf de la licence d'un membre", description = "Renvoie le pdf de la licence d'un membre en fonction de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Le pdf de la licence"),

View File

@@ -10,6 +10,7 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import java.util.HashMap;
import java.util.List;
@Authenticated
@@ -29,15 +30,39 @@ public class ResultEndpoints {
}
@GET
@Path("{uuid}")
public Uni<List<ResultCategoryData>> getCategory(@PathParam("uuid") String uuid) {
return resultService.getCategory(uuid, securityCtx);
@Path("{uuid}/category/list")
public Uni<HashMap<String, Long>> getCategoryList(@PathParam("uuid") String uuid) {
return resultService.getCategoryList(uuid, securityCtx);
}
@GET
@Path("{uuid}/club")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid) {
return resultService.getClubArray(uuid, securityCtx);
@Path("{uuid}/category/{id}")
public Uni<ResultCategoryData> getCategory(@PathParam("uuid") String uuid, @PathParam("id") long id) {
return resultService.getCategory(uuid, id, securityCtx);
}
@GET
@Path("{uuid}/club/list")
public Uni<HashMap<String, Long>> getClubList(@PathParam("uuid") String uuid) {
return resultService.getClubList(uuid, securityCtx);
}
@GET
@Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {
return resultService.getClubArray(uuid, id, securityCtx);
}
@GET
@Path("{uuid}/comb/list")
public Uni<HashMap<String, String>> getCombList(@PathParam("uuid") String uuid) {
return resultService.getCombList(uuid, securityCtx);
}
@GET
@Path("{uuid}/comb/{id}")
public Uni<?> getCombList(@PathParam("uuid") String uuid, @PathParam("id") String id) {
return resultService.getCombArrayPublic(uuid, id, securityCtx);
}
@GET

View File

@@ -0,0 +1,85 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.service.SelectionService;
import fr.titionfire.ffsaf.rest.data.SimpleSelection;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import java.util.List;
import java.util.function.Consumer;
@Path("api/selection")
public class SelectionEndpoints {
@Inject
SelectionService selectionService;
@Inject
SecurityCtx securityCtx;
Consumer<MembreModel> checkPerm = Unchecked.consumer(membreModel -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(membreModel.getClub().getId()))
throw new DForbiddenException();
});
@GET
@Path("{id}")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra",
"ffsaf_selectionneur"})
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Renvoie les séléctions d'un membre", description = "Renvoie les séléctions d'un membre en fonction " +
"de son identifiant")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La liste des séléctions du membre"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "Le membre n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<List<SimpleSelection>> getSelection(@PathParam("id") long id) {
return selectionService.getSelection(id, checkPerm)
.map(selectionModels -> selectionModels.stream().map(SimpleSelection::fromModel).toList());
}
@POST
@Path("{id}")
@RolesAllowed({"federation_admin", "ffsaf_selectionneur"})
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Créer une séléction", description = "Créer une séléction en fonction de son identifiant et des " +
"informations fournies dans le formulaire (pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "La séléction a été mise à jour avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "La séléction n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<SimpleSelection> setSelection(@PathParam("id") long id, SimpleSelection data) {
return selectionService.setSelection(id, data).map(SimpleSelection::fromModel);
}
@DELETE
@Path("{id}")
@RolesAllowed({"federation_admin", "ffsaf_selectionneur"})
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Supprime une séléction", description = "Supprime une séléction en fonction de son identifiant " +
"(pour les administrateurs)")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "La séléction a été supprimée avec succès"),
@APIResponse(responseCode = "403", description = "Accès refusé"),
@APIResponse(responseCode = "404", description = "La séléction n'existe pas"),
@APIResponse(responseCode = "500", description = "Erreur interne du serveur")
})
public Uni<?> deleteSelection(@PathParam("id") long id) {
return selectionService.deleteSelection(id);
}
}

View File

@@ -35,10 +35,12 @@ public class CompetitionData {
private String owner;
private List<SimpleRegister> registers;
private boolean canEdit;
private boolean canEditRegisters;
private String data1;
private String data2;
private String data3;
private String data4;
private String config;
public static CompetitionData fromModel(CompetitionModel model) {
if (model == null)
@@ -47,8 +49,8 @@ public class CompetitionData {
return new CompetitionData(model.getId(), model.getName(), model.getDescription(), model.getAdresse(),
model.getUuid(), model.getDate(), model.getTodate(), model.getSystem(),
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false,
model.getData1(), model.getData2(), model.getData3(), model.getData4());
model.getClub().getId(), model.getClub().getName(), model.getOwner(), null, false, false,
model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig());
}
public static CompetitionData fromModelLight(CompetitionModel model) {
@@ -58,14 +60,15 @@ public class CompetitionData {
CompetitionData out = new CompetitionData(model.getId(), model.getName(), model.getDescription(),
model.getAdresse(), "", model.getDate(), model.getTodate(), null,
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
null, model.getClub().getName(), "", null, false,
"", "", "", "");
null, model.getClub().getName(), "", null, false, false,
"", "", "", "", "{}");
if (model.getRegisterMode() == RegisterMode.HELLOASSO) {
out.setData1(model.getData1());
out.setData2(model.getData2());
}
return out;
}

View File

@@ -1,6 +1,7 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -15,38 +16,42 @@ import java.util.List;
@NoArgsConstructor
@RegisterForReflection
public class MeData {
@Schema(description = "L'identifiant du membre.", example = "1")
@Schema(description = "L'identifiant du membre.", examples = "1")
private long id;
@Schema(description = "Le nom du membre.", example = "Dupont")
@Schema(description = "Le nom du membre.", examples = "Dupont")
private String lname = "";
@Schema(description = "Le prénom du membre.", example = "Jean")
@Schema(description = "Le prénom du membre.", examples = "Jean")
private String fname = "";
@Schema(description = "La catégorie du membre.", example = "SENIOR")
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
private String categorie;
@Schema(description = "Le nom du club du membre.", example = "Association sportive")
@Schema(description = "Le nom du club du membre.", examples = "Association sportive")
private String club;
@Schema(description = "Le genre du membre.", example = "Homme")
@Schema(description = "Le genre du membre.", examples = "Homme")
private String genre;
@Schema(description = "Le numéro de licence du membre.", example = "12345")
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
private int licence;
@Schema(description = "Le pays du membre.", example = "FR")
@Schema(description = "Le pays du membre.", examples = "FR")
private String country;
@Schema(description = "La date de naissance du membre.")
private Date birth_date;
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
private String email;
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
private String role;
@Schema(description = "Le grade d'arbitrage du membre.", example = "N/A")
@Schema(description = "Le grade d'arbitrage du membre.", examples = "N/A")
private String grade_arbitrage;
@Schema(description = "La confidentialité des résultats", examples = "PUBLIC")
private ResultPrivacy resultPrivacy;
@Schema(description = "La liste des licences du membre.")
private List<SimpleLicence> licences;
@Schema(description = "La liste des séléctions du membre.")
private List<SimpleSelection> selections;
public void setMembre(MembreModel membreModel) {
this.id = membreModel.getId();
this.lname = membreModel.getLname();
this.fname = membreModel.getFname();
this.categorie = membreModel.getCategorie().getName();
this.categorie = membreModel.getCategorie() == null ? "catégorie inconnue" : membreModel.getCategorie().getName();
this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName();
this.genre = membreModel.getGenre().str;
this.licence = membreModel.getLicence();
@@ -55,5 +60,6 @@ public class MeData {
this.email = membreModel.getEmail();
this.role = membreModel.getRole().str;
this.grade_arbitrage = membreModel.getGrade_arbitrage().str;
this.resultPrivacy = membreModel.getResultPrivacy();
}
}

View File

@@ -1,6 +1,8 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import fr.titionfire.ffsaf.utils.TreeNode;
import io.quarkus.runtime.annotations.RegisterForReflection;
@@ -21,6 +23,8 @@ public class ResultCategoryData {
HashMap<Character, List<PouleArrayData>> matchs = new HashMap<>();
HashMap<Character, List<RankArray>> rankArray = new HashMap<>();
ArrayList<TreeNode<TreeData>> trees;
String[] liceName;
long genTime;
@Data
@AllArgsConstructor
@@ -36,15 +40,15 @@ public class ResultCategoryData {
@RegisterForReflection
public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue, boolean end) {
public static PouleArrayData fromModel(MatchModel matchModel) {
public static PouleArrayData fromModel(MatchModel matchModel, MembreModel membreModel, ResultPrivacy privacy) {
return new PouleArrayData(
matchModel.getC1Name(),
matchModel.getC1Name(membreModel, privacy),
matchModel.isEnd() && matchModel.win() > 0,
matchModel.isEnd() ?
matchModel.getScores().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()
: new ArrayList<>(),
matchModel.isEnd() && matchModel.win() < 0,
matchModel.getC2Name(),
matchModel.getC2Name(membreModel, privacy),
matchModel.isEnd());
}
}
@@ -52,8 +56,8 @@ public class ResultCategoryData {
@RegisterForReflection
public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores,
boolean end) {
public static TreeData from(MatchModel match) {
return new TreeData(match.getId(), match.getC1Name(), match.getC2Name(), match.getScores(), match.isEnd());
public static TreeData from(MatchModel match, MembreModel membreModel, ResultPrivacy privacy) {
return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), match.getC2Name(membreModel, privacy), match.getScores(), match.isEnd());
}
}
}

View File

@@ -1,5 +1,9 @@
package fr.titionfire.ffsaf.rest.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.titionfire.ffsaf.data.model.CompetitionModel;
import fr.titionfire.ffsaf.net2.data.SimpleCompet;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
@@ -25,4 +29,41 @@ public class SimpleCompetData {
return new SimpleCompetData(compet.id(), compet.show_blason(), compet.show_flag(),
new ArrayList<>(), new ArrayList<>());
}
public static SimpleCompetData fromModel(CompetitionModel competitionModel) {
if (competitionModel == null)
return null;
boolean show_blason = true;
boolean show_flag = false;
if (competitionModel.getConfig() != null) {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(competitionModel.getConfig());
if (rootNode.has("show_blason"))
show_blason = rootNode.get("show_blason").asBoolean();
if (rootNode.has("show_flag"))
show_flag = rootNode.get("show_flag").asBoolean();
} catch (JsonProcessingException ignored) {
}
}
return new SimpleCompetData(competitionModel.getId(), show_blason, show_flag,
new ArrayList<>(), new ArrayList<>());
}
public String getConfigForInternal(){
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.createObjectNode()
.put("show_blason", this.show_blason)
.put("show_flag", this.show_flag);
try {
return objectMapper.writeValueAsString(rootNode);
} catch (JsonProcessingException e) {
return "{}";
}
}
}

View File

@@ -19,34 +19,32 @@ import java.util.Date;
@AllArgsConstructor
@RegisterForReflection
public class SimpleMembre {
@Schema(description = "L'identifiant du membre.", example = "1")
@Schema(description = "L'identifiant du membre.", examples = "1")
private long id;
@Schema(description = "L'identifiant long du membre (userID).", example = "e81d1d35-d897-421e-8086-6c5e74d13c6e")
@Schema(description = "L'identifiant long du membre (userID).", examples = "e81d1d35-d897-421e-8086-6c5e74d13c6e")
private String userId;
@Schema(description = "Le nom du membre.", example = "Dupont")
@Schema(description = "Le nom du membre.", examples = "Dupont")
private String lname = "";
@Schema(description = "Le prénom du membre.", example = "Jean")
@Schema(description = "Le prénom du membre.", examples = "Jean")
private String fname = "";
@Schema(description = "La catégorie du membre.", example = "SENIOR")
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
private Categorie categorie;
@Schema(description = "Le club du membre.")
private SimpleClubModel club;
@Schema(description = "Le genre du membre.", example = "H")
@Schema(description = "Le genre du membre.", examples = "H")
private Genre genre;
@Schema(description = "Le numéro de licence du membre.", example = "12345")
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
private Integer licence;
@Schema(description = "Le pays du membre.", example = "FR")
@Schema(description = "Le pays du membre.", examples = "FR")
private String country;
@Schema(description = "La date de naissance du membre.")
private Date birth_date;
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
private String email;
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
private RoleAsso role;
@Schema(description = "Le grade d'arbitrage du membre.", example = "N/A")
@Schema(description = "Le grade d'arbitrage du membre.", examples = "N/A")
private GradeArbitrage grade_arbitrage;
@Schema(hidden = true)
private String url_photo;
public static SimpleMembre fromModel(MembreModel model) {
if (model == null)
@@ -66,7 +64,6 @@ public class SimpleMembre {
.email(model.getEmail())
.role(model.getRole())
.grade_arbitrage(model.getGrade_arbitrage())
.url_photo(model.getUrl_photo())
.build();
}
}

View File

@@ -0,0 +1,36 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.SelectionModel;
import fr.titionfire.ffsaf.utils.Categorie;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class SimpleSelection {
@Schema(description = "ID de la séléction", examples = "1")
Long id;
@Schema(description = "ID du membre", examples = "1")
Long membre;
@Schema(description = "Saison de la séléction", examples = "2024")
int saison;
@Schema(description = "Catégorie de la séléction", examples = "JUNIOR")
Categorie categorie;
public static SimpleSelection fromModel(SelectionModel model) {
if (model == null)
return null;
return new SimpleSelection.SimpleSelectionBuilder()
.id(model.getId())
.membre(model.getMembre().getId())
.saison(model.getSaison())
.categorie(model.getCategorie())
.build();
}
}

View File

@@ -9,6 +9,7 @@ import lombok.ToString;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@Getter
@ToString(exclude = {"status", "logo"})
@@ -17,85 +18,85 @@ public class AffiliationRequestForm {
@FormParam("id")
private Long id = null;
@Schema(description = "Le nom de l'association.", example = "Association sportive", required = true)
@Schema(description = "Le nom de l'association.", examples = "Association sportive", required = true)
@FormParam("name")
private String name = null;
@Schema(description = "Le numéro SIRET/RNA de l'association.", example = "12345678901234", required = true)
@Schema(description = "Le numéro SIRET/RNA de l'association.", examples = "12345678901234", required = true)
@FormParam("state_id")
private String state_id = null;
@Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true)
@Schema(description = "L'adresse de l'association.", examples = "1 rue de l'exemple, 75000 Paris", required = true)
@FormParam("adresse")
private String adresse = null;
@Schema(description = "Email de contact de l'association", example = "test@test.fr")
@Schema(description = "Email de contact de l'association", examples = "test@test.fr")
@FormParam("contact")
private String contact = null;
@Schema(description = "La saison de l'affiliation.", example = "2025", required = true)
@Schema(description = "La saison de l'affiliation.", examples = "2025", required = true)
@FormParam("saison")
private int saison = -1;
@Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY, implementation = byte.class)
@Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY)
@FormParam("status")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] status = new byte[0];
private FileUpload status = null;
@Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY, implementation = byte.class)
@Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY)
@FormParam("logo")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] logo = new byte[0];
private FileUpload logo = null;
@Schema(description = "Le nom du premier membre de l'association.", example = "Doe", required = true)
@Schema(description = "Le nom du premier membre de l'association.", examples = "Doe", required = true)
@FormParam("m1_nom")
private String m1_lname = null;
@Schema(description = "Le prénom du premier membre de l'association.", example = "John", required = true)
@Schema(description = "Le prénom du premier membre de l'association.", examples = "John", required = true)
@FormParam("m1_prenom")
private String m1_fname = null;
@Schema(description = "L'adresse e-mail du premier membre de l'association.", example = "john.doe@test.com", required = true)
@Schema(description = "L'adresse e-mail du premier membre de l'association.", examples = "john.doe@test.com", required = true)
@FormParam("m1_mail")
private String m1_email = null;
@Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", example = "12345")
@Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", examples = "12345")
@FormParam("m1_licence")
private String m1_lincence = null;
@Schema(description = "Le rôle du premier membre de l'association. (doit être PRESIDENT)", example = "PRESIDENT", required = true)
@Schema(description = "Le rôle du premier membre de l'association. (doit être PRESIDENT)", examples = "PRESIDENT", required = true)
@FormParam("m1_role")
private RoleAsso m1_role = null;
@Schema(description = "Le nom du deuxième membre de l'association.", example = "Xavier", required = true)
@Schema(description = "Le nom du deuxième membre de l'association.", examples = "Xavier", required = true)
@FormParam("m2_nom")
private String m2_lname = null;
@Schema(description = "Le prénom du deuxième membre de l'association.", example = "Login", required = true)
@Schema(description = "Le prénom du deuxième membre de l'association.", examples = "Login", required = true)
@FormParam("m2_prenom")
private String m2_fname = null;
@Schema(description = "L'adresse e-mail du deuxième membre de l'association.", example = "xavier.login@test.com", required = true)
@Schema(description = "L'adresse e-mail du deuxième membre de l'association.", examples = "xavier.login@test.com", required = true)
@FormParam("m2_mail")
private String m2_email = null;
@Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", example = "04242")
@Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", examples = "04242")
@FormParam("m2_licence")
private String m2_lincence = null;
@Schema(description = "Le rôle du deuxième membre de l'association.", example = "SECRETAIRE", required = true)
@Schema(description = "Le rôle du deuxième membre de l'association.", examples = "SECRETAIRE", required = true)
@FormParam("m2_role")
private RoleAsso m2_role = null;
@Schema(description = "Le nom du troisième membre de l'association.", example = "Doe2", required = true)
@Schema(description = "Le nom du troisième membre de l'association.", examples = "Doe2", required = true)
@FormParam("m3_nom")
private String m3_lname = null;
@Schema(description = "Le prénom du troisième membre de l'association.", example = "John2", required = true)
@Schema(description = "Le prénom du troisième membre de l'association.", examples = "John2", required = true)
@FormParam("m3_prenom")
private String m3_fname = null;
@Schema(description = "L'adresse e-mail du troisième membre de l'association.", example = "john.doe22@test.com", required = true)
@Schema(description = "L'adresse e-mail du troisième membre de l'association.", examples = "john.doe22@test.com", required = true)
@FormParam("m3_mail")
private String m3_email = null;
@@ -103,7 +104,7 @@ public class AffiliationRequestForm {
@FormParam("m3_licence")
private String m3_lincence = null;
@Schema(description = "Le rôle du troisième membre de l'association.", example = "MEMBREBUREAU", required = true)
@Schema(description = "Le rôle du troisième membre de l'association.", examples = "MEMBREBUREAU", required = true)
@FormParam("m3_role")
private RoleAsso m3_role = null;

View File

@@ -6,123 +6,124 @@ import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@Getter
public class AffiliationRequestSaveForm {
@Schema(description = "L'identifiant de l'affiliation.", example = "1", required = true)
@Schema(description = "L'identifiant de l'affiliation.", examples = "1", required = true)
@FormParam("id")
private Long id = null;
@Schema(description = "Le nom de l'association.", example = "Association sportive", required = true)
@Schema(description = "Le nom de l'association.", examples = "Association sportive", required = true)
@FormParam("name")
private String name = null;
@Schema(description = "Le numéro SIRET ou RNA de l'association.", example = "12345678901234", required = true)
@Schema(description = "Le numéro SIRET ou RNA de l'association.", examples = "12345678901234", required = true)
@FormParam("state_id")
private String state_id = null;
@Schema(description = "L'adresse de l'association.", example = "1 rue de l'exemple, 75000 Paris", required = true)
@Schema(description = "L'adresse de l'association.", examples = "1 rue de l'exemple, 75000 Paris", required = true)
@FormParam("address")
private String address = null;
@Schema(description = "Email de contact de l'association", example = "test@test.fr")
@Schema(description = "Email de contact de l'association", examples = "test@test.fr")
@FormParam("contact")
private String contact = null;
@Schema(description = "Le statut de l'association.")
@FormParam("status")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] status = new byte[0];
private FileUpload status = null;
@Schema(description = "Le logo de l'association.")
@FormParam("logo")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] logo = new byte[0];
private FileUpload logo = null;
@Schema(description = "Mode utiliser pour la sauvegarde du membre 1 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true)
@Schema(description = "Mode utiliser pour la sauvegarde du membre 1 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true)
@FormParam("m1_mode")
private Integer m1_mode = null;
@Schema(description = "Le rôle du premier membre de l'association.", example = "PRÉSIDENT", required = true)
@Schema(description = "Le rôle du premier membre de l'association.", examples = "PRÉSIDENT", required = true)
@FormParam("m1_role")
private RoleAsso m1_role = null;
@Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", example = "1234567", required = true)
@Schema(description = "Le numéro de licence du premier membre de l'association. (null si non licencié)", examples = "1234567", required = true)
@FormParam("m1_licence")
private String m1_lincence = null;
@Schema(description = "Le nom du premier membre de l'association.", example = "Dupont", required = true)
@Schema(description = "Le nom du premier membre de l'association.", examples = "Dupont", required = true)
@FormParam("m1_lname")
private String m1_lname = null;
@Schema(description = "Le prénom du premier membre de l'association.", example = "Jean", required = true)
@Schema(description = "Le prénom du premier membre de l'association.", examples = "Jean", required = true)
@FormParam("m1_fname")
private String m1_fname = null;
@Schema(description = "L'adresse e-mail du premier membre de l'association.", example = "jean.dupont@example.com", required = true)
@Schema(description = "L'adresse e-mail du premier membre de l'association.", examples = "jean.dupont@examples.com", required = true)
@FormParam("m1_email")
private String m1_email = null;
@Schema(name = "keep_email",
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm1_email')", example = "1", required = true)
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm1_email')", examples = "1", required = true)
@FormParam("m1_email_mode")
private Integer m1_email_mode = null;
@Schema(description = "Mode utiliser pour la sauvegarde du membre 2 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true)
@Schema(description = "Mode utiliser pour la sauvegarde du membre 2 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true)
@FormParam("m2_mode")
private Integer m2_mode = null;
@Schema(description = "Le rôle du deuxième membre de l'association.", example = "TRÉSORIER", required = true)
@Schema(description = "Le rôle du deuxième membre de l'association.", examples = "TRÉSORIER", required = true)
@FormParam("m2_role")
private RoleAsso m2_role = null;
@Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", example = "2345678", required = true)
@Schema(description = "Le numéro de licence du deuxième membre de l'association. (null si non licencié)", examples = "2345678", required = true)
@FormParam("m2_licence")
private String m2_lincence = null;
@Schema(description = "Le nom du deuxième membre de l'association.", example = "Durand", required = true)
@Schema(description = "Le nom du deuxième membre de l'association.", examples = "Durand", required = true)
@FormParam("m2_lname")
private String m2_lname = null;
@Schema(description = "Le prénom du deuxième membre de l'association.", example = "Paul", required = true)
@Schema(description = "Le prénom du deuxième membre de l'association.", examples = "Paul", required = true)
@FormParam("m2_fname")
private String m2_fname = null;
@Schema(description = "L'adresse e-mail du deuxième membre de l'association.", example = "paul.durand@example.com", required = true)
@Schema(description = "L'adresse e-mail du deuxième membre de l'association.", examples = "paul.durand@examples.com", required = true)
@FormParam("m2_email")
private String m2_email = null;
@Schema(name = "keep_email",
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm2_email')", example = "1", required = true)
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm2_email')", examples = "1", required = true)
@FormParam("m2_email_mode")
private Integer m2_email_mode = null;
@Schema(description = "Mode utiliser pour la sauvegarde du membre 3 (0 = licence mode, 2 = nom, prénom)", example = "0", required = true)
@Schema(description = "Mode utiliser pour la sauvegarde du membre 3 (0 = licence mode, 2 = nom, prénom)", examples = "0", required = true)
@FormParam("m3_mode")
private Integer m3_mode = null;
@Schema(description = "Le rôle du troisième membre de l'association.", example = "SECRÉTAIRE", required = true)
@Schema(description = "Le rôle du troisième membre de l'association.", examples = "SECRÉTAIRE", required = true)
@FormParam("m3_role")
private RoleAsso m3_role = null;
@Schema(description = "Le numéro de licence du troisième membre de l'association. (null si non licencié)", example = "3456789", required = true)
@Schema(description = "Le numéro de licence du troisième membre de l'association. (null si non licencié)", examples = "3456789", required = true)
@FormParam("m3_licence")
private String m3_lincence = null;
@Schema(description = "Le nom du troisième membre de l'association.", example = "Martin", required = true)
@Schema(description = "Le nom du troisième membre de l'association.", examples = "Martin", required = true)
@FormParam("m3_lname")
private String m3_lname = null;
@Schema(description = "Le prénom du troisième membre de l'association.", example = "Pierre", required = true)
@Schema(description = "Le prénom du troisième membre de l'association.", examples = "Pierre", required = true)
@FormParam("m3_fname")
private String m3_fname = null;
@Schema(description = "L'adresse e-mail du troisième membre de l'association.", example = "pierre.martin@example.com", required = true)
@Schema(description = "L'adresse e-mail du troisième membre de l'association.", examples = "pierre.martin@examples.com", required = true)
@FormParam("m3_email")
private String m3_email = null;
@Schema(name = "keep_email",
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm3_email')", example = "1", required = true)
description = "Conserver l'email de la base de donner (1 = conserve, 0 = replacer par 'm3_email')", examples = "1", required = true)
@FormParam("m3_email_mode")
private Integer m3_email_mode = null;
@@ -174,8 +175,6 @@ public class AffiliationRequestSaveForm {
", state_id=" + state_id +
", address='" + address + '\'' +
", contact='" + contact + '\'' +
", status_len=" + status.length +
", logo_len=" + logo.length +
", m1_mode=" + m1_mode +
", m1_role=" + m1_role +
", m1_lincence='" + m1_lincence + '\'' +

View File

@@ -7,57 +7,58 @@ import lombok.ToString;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@ToString
@Getter
public class FullClubForm {
@FormParam("id")
@Schema(description = "Identifiant du club", example = "1", required = true)
@Schema(description = "Identifiant du club", examples = "1", required = true)
private String id = null;
@FormParam("name")
@Schema(description = "Nom du club", example = "Association sportive", required = true)
@Schema(description = "Nom du club", examples = "Association sportive", required = true)
private String name = null;
@FormParam("country")
@Schema(description = "Pays du club", example = "FR", required = true)
@Schema(description = "Pays du club", examples = "FR", required = true)
private String country = null;
@FormParam("contact")
@Schema(description = "Les contacts du club", example = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}", required = true)
@Schema(description = "Les contacts du club", examples = "{\"SITE\": \"www.test.com\", \"COURRIEL\": \"test@test.com\"}", required = true)
private String contact = null;
@FormParam("training_location")
@Schema(description = "Liste des lieux d'entraînement", example = "[{\"text\":\"addr 1\",\"lng\":2.24654,\"lat\":52.4868658},{\"text\":\"addr 2\",\"lng\":2.88654,\"lat\":52.7865456}]", required = true)
@Schema(description = "Liste des lieux d'entraînement", examples = "[{\"text\":\"addr 1\",\"lng\":2.24654,\"lat\":52.4868658},{\"text\":\"addr 2\",\"lng\":2.88654,\"lat\":52.7865456}]", required = true)
private String training_location = null;
@FormParam("training_day_time")
@Schema(description = "Liste des jours et horaires d'entraînement (jours 0-6, 0=>lundi) (temps en minute depuis 00:00, 122=>2h02)", example = "[{\"day\":0,\"time_start\":164,\"time_end\":240},{\"day\":3,\"time_start\":124,\"time_end\":250}]", required = true)
@Schema(description = "Liste des jours et horaires d'entraînement (jours 0-6, 0=>lundi) (temps en minute depuis 00:00, 122=>2h02)", examples = "[{\"day\":0,\"time_start\":164,\"time_end\":240},{\"day\":3,\"time_start\":124,\"time_end\":250}]", required = true)
private String training_day_time = null;
@FormParam("contact_intern")
@Schema(description = "Contact interne du club", example = "john.doe@test.com")
@Schema(description = "Contact interne du club", examples = "john.doe@test.com")
private String contact_intern = null;
@FormParam("address")
@Schema(description = "Adresse postale du club", example = "1 rue de l'exemple, 75000 Paris", required = true)
@Schema(description = "Adresse postale du club", examples = "1 rue de l'exemple, 75000 Paris", required = true)
private String address = null;
@FormParam("state_id")
@Schema(description = "Numéro SIRET ou RNA du club", example = "12345678901234", required = true)
@Schema(description = "Numéro SIRET ou RNA du club", examples = "12345678901234", required = true)
private String state_id = null;
@FormParam("international")
@Schema(description = "Club international", example = "false", required = true)
@Schema(description = "Club international", examples = "false", required = true)
private boolean international = false;
@FormParam("status")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY, implementation = byte.class)
private byte[] status = new byte[0];
@Schema(description = "Le statut de l'association.", type = SchemaType.ARRAY)
private FileUpload status = null;
@FormParam("logo")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY, implementation = byte.class)
private byte[] logo = new byte[0];
@Schema(description = "Le logo de l'association.", type = SchemaType.ARRAY)
private FileUpload logo = null;
}

View File

@@ -8,32 +8,33 @@ import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import java.util.Date;
@Getter
public class FullMemberForm {
@Schema(description = "L'identifiant du membre.", example = "1")
@Schema(description = "L'identifiant du membre.", examples = "1")
@FormParam("id")
private String id = null;
@Schema(description = "Le nom du membre.", example = "Dupont")
@Schema(description = "Le nom du membre.", examples = "Dupont")
@FormParam("lname")
private String lname = null;
@Schema(description = "Le prénom du membre.", example = "Jean")
@Schema(description = "Le prénom du membre.", examples = "Jean")
@FormParam("fname")
private String fname = null;
@Schema(description = "L'identifiant du club du membre.", example = "1")
@Schema(description = "L'identifiant du club du membre.", examples = "1")
@FormParam("club")
private Long club = null;
@Schema(description = "Le genre du membre.", example = "H")
@Schema(description = "Le genre du membre.", examples = "H")
@FormParam("genre")
private Genre genre;
@Schema(description = "Le pays du membre.", example = "FR")
@Schema(description = "Le pays du membre.", examples = "FR")
@FormParam("country")
private String country;
@@ -41,22 +42,22 @@ public class FullMemberForm {
@FormParam("birth_date")
private Date birth_date = null;
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@example.com")
@FormParam("email")
private String email;
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
@FormParam("role")
private RoleAsso role;
@Schema(description = "Le grade d'arbitrage du membre.", example = "ASSESSEUR")
@Schema(description = "Le grade d'arbitrage du membre.", examples = "ASSESSEUR")
@FormParam("grade_arbitrage")
private GradeArbitrage grade_arbitrage = GradeArbitrage.NA;
@Schema(description = "La photo du membre.")
@FormParam("photo_data")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] photo_data = new byte[0];
private FileUpload photo_data = null;
@Override
public String toString() {
@@ -70,7 +71,6 @@ public class FullMemberForm {
", email='" + email + '\'' +
", role=" + role +
", grade_arbitrage=" + grade_arbitrage +
", url_photo=" + photo_data.length +
'}';
}
}

View File

@@ -0,0 +1,15 @@
package fr.titionfire.ffsaf.rest.from;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import jakarta.ws.rs.FormParam;
import lombok.Getter;
import lombok.ToString;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@ToString
@Getter
public class UserSettingForm {
@FormParam("resultPrivacy")
@Schema(description = "La confidentialité des résultats", examples = "PUBLIC", defaultValue = "PUBLIC", required = true)
private ResultPrivacy resultPrivacy;
}

View File

@@ -0,0 +1,8 @@
package fr.titionfire.ffsaf.utils;
public enum ResultPrivacy {
PUBLIC,
REGISTERED_ONLY,
REGISTERED_ONLY_NO_DETAILS,
PRIVATE;
}

View File

@@ -1,12 +1,20 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.exception.DetailException;
import io.quarkiverse.antivirus.runtime.AntivirusScanResult;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jodd.net.MimeTypes;
import org.apache.tika.Tika;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import java.io.*;
import java.net.URISyntaxException;
@@ -21,6 +29,79 @@ import java.util.concurrent.Future;
public class Utils {
private static final Logger LOGGER = Logger.getLogger(Utils.class);
public static String HTML_HEADER = """
<!DOCTYPE html>
<html data-lt-installed="true">
<head>
<meta charset="UTF-8">
<title>%s</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #003366;
color: white;
padding: 20px;
text-align: center;
border-radius: 5px 5px 0 0;
}
.content {
padding: 20px;
background-color: #f9f9f9;
border-radius: 0 0 5px 5px;
border: 1px solid #ddd;
border-top: none;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #003366;
color: white !important;
text-decoration: none;
border-radius: 5px;
margin: 15px 0;
}
.footer {
margin-top: 20px;
font-size: 0.9em;
color: #666;
text-align: center;
}
.highlight {
font-weight: bold;
color: #003366;
}
</style>
</head>
<body data-gramm="false" data-lt-tmp-id="lt-957854">
<div class="header">
<div><img src="https://intra.ffsaf.fr/Logo-FFSAF-2023.png" alt="ffsaf" height="128">
<h1>F&eacute;d&eacute;ration France Soft Armored Fighting</h1>
</div>
</div>
<div class="content">
<p>Bonjour,</p>
""";
public static String HTML_FOOTER = """
<p>Cordialement,<br>L&rsquo;&eacute;quipe de la FFSAF</p>
</div>
</body>
</html>
""";
public static int getSaison() {
return getSaison(new Date());
}
@@ -108,44 +189,72 @@ public class Utils {
});
}
public static Future<String> replacePhoto(long id, byte[] input, String media, String dir) {
return CompletableFuture.supplyAsync(() -> {
if (input == null || input.length == 0)
return "OK";
public static Uni<String> uploadFile(VirusScannerService ss, FileUpload file, long id, String media, String dir) {
if (file == null || file.size() == 0)
return Uni.createFrom().item("Ok");
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
String mimeType;
try {
Tika tika = new Tika();
mimeType = tika.detect(is);// Magic.getMagicMatch(input, false).getMimeType();
} catch (IOException e) {
mimeType = URLConnection.guessContentTypeFromStream(is);
}
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
if (detectedExtensions.length == 0)
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
File dirFile = new File(media, dir);
if (!dirFile.exists())
if (!dirFile.mkdirs())
throw new IOException("Fail to create directory " + dir);
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
LOGGER.infof("Received file upload request for: %s (size: %d bytes)", file.fileName(), file.size());
return Uni.createFrom().<InputStream>item(() -> {
try {
// Read the entire file into memory for virus scanning
// ByteArrayInputStream fully supports mark/reset operations required by ClamAV
// This ensures we can scan the file content before any filesystem storage
InputStream fileStream = java.nio.file.Files.newInputStream(file.uploadedFile());
byte[] fileBytes = fileStream.readAllBytes();
fileStream.close(); // Close the file stream immediately
return new ByteArrayInputStream(fileBytes);
} catch (IOException e) {
throw new RuntimeException("Failed to read uploaded file: " + e.getMessage(), e);
}
}).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool()).onItem()
.transformToUni(inputStream -> {
// Perform virus scanning reactively using the input stream
return ss.scanFileReactive(file.fileName(), inputStream).onItem().invoke(() -> {
try {
inputStream.close();
} catch (IOException e) {
LOGGER.warn("Warning: Failed to close input stream: " + e.getMessage());
}
});
})
.onItem().transformToUni(scanResults -> Uni.createFrom().item(Unchecked.supplier(() -> {
// Check if any scanner found a threat
for (AntivirusScanResult result : scanResults) {
if (result.getStatus() != Response.Status.OK.getStatusCode()) {
LOGGER.warnf("THREAT DETECTED in %s: %s", file.fileName(), result.getMessage());
throw new DetailException(Response.Status.fromStatusCode(result.getStatus()),
"THREAT_DETECTED on File " + file.fileName() + " is infected: " + result.getMessage());
}
}
}
String extension = "." + detectedExtensions[0];
Files.write(new File(dirFile, id + extension).toPath(), input);
return "OK";
} catch (IOException e) {
return e.getMessage();
}
});
// File is clean - now we can safely process it
LOGGER.infof("File is clean: %s (size=%d) (contentType=%s)", file.fileName(), file.size(),
file.contentType());
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(file.contentType(), false);
if (detectedExtensions.length == 0)
throw new DBadRequestException(
"Fail to detect file extension for MIME type " + file.contentType());
File dirFile = new File(media, dir);
if (!dirFile.exists())
if (!dirFile.mkdirs())
throw new DInternalError("Fail to create directory " + dir);
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File f : files) {
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}
File f = file.filePath().toFile();
//noinspection ResultOfMethodCallIgnored
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0]));
return "ok";
})));
}
public static Uni<Response> getMediaFile(long id, String media, String dirname,
@@ -262,4 +371,46 @@ public class Utils {
return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24));
}
public static String formatPrenom(String input) {
if (input == null || input.isEmpty()) {
return input;
}
StringBuilder result = new StringBuilder();
String[] mots = input.split(" ");
for (String mot : mots) {
if (!mot.isEmpty()) {
String[] parties = mot.split("[-']");
StringBuilder motFormate = new StringBuilder();
for (int i = 0; i < parties.length; i++) {
if (!parties[i].isEmpty()) {
String premiereLettre = parties[i].substring(0, 1).toUpperCase();
String reste = parties[i].substring(1).toLowerCase();
motFormate.append(premiereLettre).append(reste);
}
if (i < parties.length - 1) {
motFormate.append(mot.charAt(mot.indexOf(parties[i]) + parties[i].length()));
}
}
result.append(motFormate).append(" ");
}
}
return result.toString().trim();
}
public static String getFullName(Object... models) {
for (Object model : models) {
if (model == null)
continue;
if (model instanceof MembreModel membreModel)
return membreModel.getFname() + " " + membreModel.getLname();
if (model instanceof CompetitionGuestModel guestModel)
return guestModel.getFname() + " " + guestModel.getLname();
}
return "";
}
}

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.domain.service.CompetPermService;
import fr.titionfire.ffsaf.net2.MessageType;
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
import fr.titionfire.ffsaf.ws.recv.*;
@@ -16,12 +17,15 @@ import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.ws.rs.ForbiddenException;
import org.jboss.logging.Logger;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Executor;
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
@@ -39,7 +43,7 @@ public class CompetitionWS {
RCategorie rCategorie;
@Inject
RRegister rRegister;
RRegister rRegister;
@Inject
RCardboard rCardboard;
@@ -50,6 +54,15 @@ public class CompetitionWS {
@Inject
CompetPermService competPermService;
@SuppressWarnings("CdiInjectionPointsInspection")
@Inject
OpenConnections connections;
@Inject
@Named("notify-executor")
Executor notifyExecutor;
private static Executor executor;
@Inject
CompetitionRepository competitionRepository;
@@ -78,6 +91,8 @@ public class CompetitionWS {
getWSReceiverMethods(RCategorie.class, rCategorie);
getWSReceiverMethods(RRegister.class, rRegister);
getWSReceiverMethods(RCardboard.class, rCardboard);
executor = notifyExecutor;
}
@OnOpen
@@ -94,8 +109,8 @@ public class CompetitionWS {
.call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> PermLevel.ADMIN)
.onFailure()
.recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> PermLevel.TABLE))
.onFailure()
.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW))
//.onFailure()
//.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW))
.invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem.toString()))
.invoke(prem -> LOGGER.infof("Connection permission: %s", prem))
.onFailure().transform(t -> new ForbiddenException()))
@@ -104,10 +119,13 @@ public class CompetitionWS {
waitingResponse.put(connection, new HashMap<>());
})
.map(cm -> {
SimpleCompetData data = SimpleCompetData.fromModel(cm);
WelcomeInfo welcomeInfo = new WelcomeInfo();
welcomeInfo.setName(cm.getName());
welcomeInfo.setPerm(connection.userData().get(UserData.TypedKey.forString("prem")));
welcomeInfo.setShow_blason(data.isShow_blason());
welcomeInfo.setShow_flag(data.isShow_flag());
return new MessageOut(UUID.randomUUID(), "welcomeInfo", MessageType.NOTIFY, welcomeInfo);
});
@@ -166,6 +184,7 @@ public class CompetitionWS {
return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti();
return ((Uni<?>) method.invoke(entry.getValue(), connection,
MAPPER.treeToValue(message.data(), method.getParameterTypes()[1])))
.ifNoItem().after(Duration.ofSeconds(5)).fail()
.map(o -> makeReply(message, o))
.onFailure()
.recoverWithItem(t -> {
@@ -184,7 +203,7 @@ public class CompetitionWS {
// return Uni.createFrom().item(new Message<>(message.uuid(), message.code(), MessageType.REPLY, "ko"));
}
public static Uni<Void> sendNotifyToOtherEditor(WebSocketConnection connection, String code, Object data) {
public static void sendNotifyToOtherEditor(WebSocketConnection connection, String code, Object data) {
String uuid = connection.pathParam("uuid");
List<Uni<Void>> queue = new ArrayList<>();
@@ -196,7 +215,15 @@ public class CompetitionWS {
}
});
return Uni.join().all(queue).andCollectFailures().onFailure().recoverWithNull().replaceWithVoid();
Uni.join().all(queue)
.andCollectFailures()
.runSubscriptionOn(executor)
.subscribeAsCompletionStage()
.whenComplete((v, t) -> {
if (t != null) {
LOGGER.error("Error sending ws_out message", t);
}
});
}
@OnError

View File

@@ -0,0 +1,19 @@
package fr.titionfire.ffsaf.ws;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Singleton
public class ExecutorProducer {
@Produces
@Named("notify-executor")
public Executor produceNotifyExecutor() {
// Créez un pool de threads avec une taille fixe (par exemple, 10 threads)
return Executors.newFixedThreadPool(10);
}
}

View File

@@ -8,4 +8,6 @@ import lombok.Data;
public class WelcomeInfo {
private String name;
private String perm;
private boolean show_blason;
private boolean show_flag;
}

View File

@@ -80,35 +80,32 @@ public class RCardboard {
return Uni.createFrom().nullItem();
return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel));
}))
.call(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model)))
.invoke(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model)))
.replaceWithVoid();
}
@WSReceiver(code = "getCardboardWithoutThis", permission = PermLevel.VIEW)
public Uni<CardboardAllMatch> getCardboardWithoutThis(WebSocketConnection connection, Long matchId) {
return getById(matchId, connection)
.chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2", matchModel.getCategory().getCompet(), matchModel)
.chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2",
matchModel.getCategory().getCompet(), matchModel)
.map(models -> {
CardboardAllMatch out = new CardboardAllMatch();
models.stream().filter(c -> (matchModel.getC1_id() != null
&& Objects.equals(c.getComb(), matchModel.getC1_id()))
|| (matchModel.getC1_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC1_guest())))
.forEach(c -> {
out.c1_yellow += c.getYellow();
out.c1_red += c.getRed();
});
models.stream().filter(c -> (matchModel.getC2_id() != null
&& Objects.equals(c.getComb(), matchModel.getC2_id()))
|| (matchModel.getC2_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC2_guest())))
.forEach(c -> {
out.c2_yellow += c.getYellow();
out.c2_red += c.getRed();
});
for (CardboardModel c : models) {
if ((matchModel.getC1_id() != null && Objects.equals(c.getComb(),
matchModel.getC1_id())) || (matchModel.getC1_guest() != null && Objects.equals(
c.getGuestComb(), matchModel.getC1_guest()))) {
out.c1_yellow += c.getYellow();
out.c1_red += c.getRed();
}
if ((matchModel.getC2_id() != null && Objects.equals(c.getComb(),
matchModel.getC2_id())) || (matchModel.getC2_guest() != null && Objects.equals(
c.getGuestComb(), matchModel.getC2_guest()))) {
out.c2_yellow += c.getYellow();
out.c2_red += c.getRed();
}
}
return out;
}));
}

View File

@@ -3,10 +3,7 @@ package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CategoryModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.model.TreeModel;
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.data.repository.TreeRepository;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
@@ -46,6 +43,9 @@ public class RCategorie {
@Inject
TreeRepository treeRepository;
@Inject
CardboardRepository cardboardRepository;
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
return categoryRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
@@ -97,7 +97,7 @@ public class RCategorie {
return categoryRepository.create(categoryModel);
})
.call(cat -> SSCategorie.sendAddCategory(connection, cat))
.invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
.map(CategoryModel::getId);
}
@@ -118,9 +118,10 @@ public class RCategorie {
uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId()))
.chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat));
}
return uni;
Uni<Long> finalUni = uni;
return Panache.withTransaction(() -> finalUni);
})
.call(cat -> SSCategorie.sendCategory(connection, cat))
.invoke(cat -> SSCategorie.sendCategory(connection, cat))
.replaceWithVoid();
}
@@ -201,7 +202,18 @@ public class RCategorie {
.call(__ -> treeRepository.flush())
.call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
.map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
.chain(trees -> SSCategorie.sendTreeCategory(connection, trees)))
.invoke(trees -> SSCategorie.sendTreeCategory(connection, trees)))
.replaceWithVoid();
}
@WSReceiver(code = "deleteCategory", permission = PermLevel.ADMIN)
public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
return getById(id, connection)
.call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
.call(__ -> cardboardRepository.delete("match.category = ?1", cat))
.call(__ -> matchRepository.delete("category = ?1", cat))))
.chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
.invoke(__ -> SSCategorie.sendDelCategory(connection, id))
.replaceWithVoid();
}

View File

@@ -44,6 +44,9 @@ public class RMatch {
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CardboardRepository cardboardRepository;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
@@ -84,7 +87,7 @@ public class RMatch {
}))
.chain(categoryModel -> creatMatch(categoryModel, m))
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.replaceWithVoid();
}
@@ -121,7 +124,7 @@ public class RMatch {
mm.setC2_guest(null);
}))
.chain(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.replaceWithVoid();
}
@@ -136,7 +139,7 @@ public class RMatch {
m.getCategory_ord()))
.invoke(m -> m.setCategory_ord(order.pos))
.call(m -> Panache.withTransaction(() -> matchRepository.persist(m)))
.call(mm -> SSMatch.sendMatchOrder(connection, order))
.invoke(mm -> SSMatch.sendMatchOrder(connection, order))
.replaceWithVoid();
}
@@ -163,12 +166,12 @@ public class RMatch {
.call(mm -> {
if (mm.isEnd() && mm.win() != old_win && mm.getCategory_ord() == -42) {
return updateEndAndTree(mm, new ArrayList<>())
.call(l -> SSMatch.sendMatch(connection, l));
.invoke(l -> SSMatch.sendMatch(connection, l));
}
return Uni.createFrom().nullItem();
});
})
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
.replaceWithVoid();
}
@@ -189,7 +192,7 @@ public class RMatch {
})
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
.chain(mm -> updateEndAndTree(mm, toSend))
.call(__ -> SSMatch.sendMatch(connection, toSend))
.invoke(__ -> SSMatch.sendMatch(connection, toSend))
.replaceWithVoid();
}
@@ -277,8 +280,11 @@ public class RMatch {
@WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN)
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
return getById(idMatch, connection)
.chain(matchModel -> Panache.withTransaction(() -> matchRepository.delete(matchModel)))
.call(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
.map(__ -> idMatch)
.chain(l -> Panache.withTransaction(() ->
cardboardRepository.delete("match.id = ?1", l)
.chain(__ -> matchRepository.delete("id = ?1", l))))
.invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
.replaceWithVoid();
}
@@ -292,9 +298,11 @@ public class RMatch {
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
}))
.call(cm -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm)
.call(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove)))
.call(cm -> matchRepository.list("id IN ?1 AND category = ?2",
.call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
(Panache.withTransaction(
() -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm))
.invoke(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove))))
.call(cm -> Panache.withSession(() -> matchRepository.list("id IN ?1 AND category = ?2",
Stream.concat(data.matchOrderToUpdate.keySet().stream(),
data.matchPouleToUpdate.keySet().stream())
.distinct().toList(), cm)
@@ -304,19 +312,22 @@ public class RMatch {
if (data.matchOrderToUpdate.containsKey(model.getId()))
model.setCategory_ord(data.matchOrderToUpdate.get(model.getId()));
}))
.call(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
.invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList()))
.call(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
Panache.withTransaction(() -> matchRepository.persist(mm)))
.invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
)
.chain(categoryModel -> {
Uni<List<MatchModel>> uni = Uni.createFrom().item(new ArrayList<>());
for (AddMatch match : data.newMatch)
uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add));
return uni;
Uni<List<MatchModel>> finalUni = uni;
return Panache.withSession(() -> finalUni);
}
)
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))
.invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
.call(__ -> SSMatch.sendMatch(connection, matches))
.chain(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
Panache.withTransaction(() -> matchRepository.create(mm))
.invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
.invoke(__ -> SSMatch.sendMatch(connection, matches))
.replaceWithVoid();
}

View File

@@ -3,11 +3,10 @@ package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
public class SSCardboard {
public static Uni<Void> sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity);
public static void sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity);
}
}

View File

@@ -5,29 +5,32 @@ import fr.titionfire.ffsaf.domain.entity.TreeEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import fr.titionfire.ffsaf.ws.recv.RCategorie;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import java.util.List;
public class SSCategorie {
public static Uni<Void> sendAddCategory(WebSocketConnection connection, CategoryModel category) {
return SSCategorie.sendAddCategory(connection, RCategorie.JustCategorie.from(category));
public static void sendAddCategory(WebSocketConnection connection, CategoryModel category) {
SSCategorie.sendAddCategory(connection, RCategorie.JustCategorie.from(category));
}
public static Uni<Void> sendAddCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendAddCategory", justCategorie);
public static void sendAddCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendAddCategory", justCategorie);
}
public static Uni<Void> sendCategory(WebSocketConnection connection, CategoryModel category) {
return SSCategorie.sendCategory(connection, RCategorie.JustCategorie.from(category));
public static void sendCategory(WebSocketConnection connection, CategoryModel category) {
SSCategorie.sendCategory(connection, RCategorie.JustCategorie.from(category));
}
public static Uni<Void> sendCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCategory", justCategorie);
public static void sendCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCategory", justCategorie);
}
public static Uni<Void> sendTreeCategory(WebSocketConnection connection, List<TreeEntity> treeEntities) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities);
public static void sendTreeCategory(WebSocketConnection connection, List<TreeEntity> treeEntities) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities);
}
public static void sendDelCategory(WebSocketConnection connection, Long id) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendDelCategory", id);
}
}

View File

@@ -4,29 +4,28 @@ import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import fr.titionfire.ffsaf.ws.recv.RMatch;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import java.util.List;
public class SSMatch {
public static Uni<Void> sendMatch(WebSocketConnection connection, MatchEntity matchEntity) {
return SSMatch.sendMatch(connection, List.of(matchEntity));
public static void sendMatch(WebSocketConnection connection, MatchEntity matchEntity) {
SSMatch.sendMatch(connection, List.of(matchEntity));
}
public static Uni<Void> sendMatch(WebSocketConnection connection, List<MatchEntity> matchEntities) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatch", matchEntities);
public static void sendMatch(WebSocketConnection connection, List<MatchEntity> matchEntities) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatch", matchEntities);
}
public static Uni<Void> sendMatchOrder(WebSocketConnection connection, RMatch.MatchOrder matchOrder) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatchOrder", matchOrder);
public static void sendMatchOrder(WebSocketConnection connection, RMatch.MatchOrder matchOrder) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatchOrder", matchOrder);
}
public static Uni<Void> sendDeleteMatch(WebSocketConnection connection, Long l) {
return SSMatch.sendDeleteMatch(connection, List.of(l));
public static void sendDeleteMatch(WebSocketConnection connection, Long l) {
SSMatch.sendDeleteMatch(connection, List.of(l));
}
public static Uni<Void> sendDeleteMatch(WebSocketConnection connection, List<Long> longs) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendDeleteMatch", longs);
public static void sendDeleteMatch(WebSocketConnection connection, List<Long> longs) {
CompetitionWS.sendNotifyToOtherEditor(connection, "sendDeleteMatch", longs);
}
}

View File

@@ -8,8 +8,9 @@ quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.physical-naming-strategy=fr.titionfire.ffsaf.data.SafcaNamingStrategy
quarkus.hibernate-orm.dialect=fr.titionfire.ffsaf.data.CustomPostgreSQLDialect
quarkus.datasource.jdbc.reactive.max-size=40
quarkus.http.cors=true
quarkus.http.cors.enabled=true
quarkus.quartz.start-mode=forced
%dev.pdf-maker.jar-path=src\\main\\pdf_gen\\target\\pdf_gen-1.0-SNAPSHOT-jar-with-dependencies.jar
@@ -75,6 +76,4 @@ helloasso.client-id=changeme
helloasso.client-secret=changeme
quarkus.rest-client.helloasso-auth.url=${helloasso.api}/oauth2
quarkus.rest-client.helloasso-auth.scope=javax.inject.Singleton
quarkus.rest-client.helloasso-api.url=${helloasso.api}/v5

View File

@@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/public/result_test.html

View File

@@ -18,7 +18,9 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7",
"proj4": "^2.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1058,6 +1060,14 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1845,6 +1855,11 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -1870,6 +1885,11 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -2007,7 +2027,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -2980,6 +2999,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3018,8 +3042,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/internal-slot": {
"version": "1.0.6",
@@ -3360,6 +3383,14 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
@@ -3447,6 +3478,17 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3474,6 +3516,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3559,8 +3609,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nanoid": {
"version": "3.3.7",
@@ -3696,6 +3745,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obs-websocket-js": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.7.tgz",
"integrity": "sha512-SdSNSyrLVR6F0ogInKr7qcadV1tYaTUse/vbabxjkUL8hU3P3dyifxkZ7pEkDDrtCp3TkQ53Enx23kgZO0Cjcw==",
"dependencies": {
"@msgpack/msgpack": "^2.7.1",
"crypto-js": "^4.1.1",
"debug": "^4.3.2",
"eventemitter3": "^5.0.1",
"isomorphic-ws": "^5.0.0",
"type-fest": "^3.11.0",
"ws": "^8.13.0"
},
"engines": {
"node": ">16.0"
}
},
"node_modules/obs-websocket-js/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/obs-websocket-js/node_modules/type-fest": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3752,6 +3834,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3855,6 +3942,11 @@
"node": ">=0.8"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proj4": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz",
@@ -4055,6 +4147,25 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/recharts": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
@@ -4256,6 +4367,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safe-regex-test": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
@@ -4319,6 +4435,11 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
@@ -4378,6 +4499,14 @@
"node": ">=0.8"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
@@ -4723,6 +4852,11 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
@@ -4922,6 +5056,26 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",

View File

@@ -20,7 +20,9 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"jszip": "^3.10.1",
"leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7",
"proj4": "^2.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -0,0 +1,946 @@
let apiUrlRoot = "";
const rootDiv = document.getElementById("safca_api_data");
const header = `<h4>Résultat de la compétition :</h4>`
const backButton = `<a onclick="setSubPage('home')" href="javascript:void(0);">Retour</a>`
const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png"
alt="">`
const voidFunction = () => {
}
let lastRf = 0;
let rfFonction = voidFunction;
setInterval(() => {
rfFonction();
}, 15000);
function setSubPage(name) {
window.location.hash = name;
const location = name.split('/');
console.log(location);
switch (location[0]) {
case 'home':
homePage();
break;
case 'poule':
poulePage(location);
break;
case 'comb':
combPage(location);
break;
case 'club':
clubPage(location);
break;
case 'all':
combsPage();
break;
}
}
function homePage() {
rootDiv.innerHTML = header;
let content = document.createElement('div');
content.innerHTML = `
<ul>
<li><a onclick="setSubPage('poule')" href="javascript:void(0);">Par catégorie</a></li>
<li><a onclick="setSubPage('comb')" href="javascript:void(0);">Par combattant</a></li>
<li><a onclick="setSubPage('club')" href="javascript:void(0);">Par club</a></li>
<li><a onclick="setSubPage('all')" href="javascript:void(0);">Tous les combattants</a></li>
</ul>
`
rootDiv.append(content)
}
let loadingAnimationStep = 0;
function startLoading(root) {
const id = "loading" + Math.random().toString(36);
let element = document.createElement('h2');
element.id = id;
const anim = setInterval(() => {
let str = "Chargement";
for (let i = 0; i < loadingAnimationStep; i++) {
str += ".";
}
loadingAnimationStep = (loadingAnimationStep + 1) % 4;
element.innerText = str;
}, 500);
root.append(element)
return {interval: anim, root: root, element: element};
}
function stopLoading(loading) {
clearInterval(loading['interval']);
loading['root'].removeChild(loading['element']);
}
function scoreToString(score) {
const scorePrint = (s1) => {
switch (s1) {
case -997:
return "disc.";
case -998:
return "abs.";
case -999:
return "for.";
case -1000:
return "";
default:
return String(s1);
}
}
return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ");
}
function dateToString(date) {
if (date === null || date === undefined)
return "";
const date_ = new Date(date);
const date_2 = new Date(date);
const current = new Date();
current.setHours(0, 0, 0, 0);
date_2.setHours(0, 0, 0, 0);
let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24));
if (d === 0)
return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
else if (d === 1)
return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
else if (d === 2)
return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
else
return date_.toLocaleDateString();
}
function buildPouleMenu(isPoule, change_view) {
const menuDiv = document.createElement('div');
menuDiv.id = 'menu';
menuDiv.style.borderBottom = '1px solid #9EA0A1';
menuDiv.style.paddingBottom = '25px';
const ul = document.createElement('ul');
ul.id = 'onglets';
ul.style.position = 'absolute';
ul.style.border = '1px solid transparent';
ul.style.padding = '0';
ul.style.font = 'bold 11px Batang, arial, serif';
ul.style.listStyleType = 'none';
ul.style.left = '50%';
ul.style.marginTop = '0';
ul.style.width = '430px';
ul.style.marginLeft = '-215px';
function createTab(text, isActive, onClickHandler) {
const li = document.createElement('li');
if (isActive) {
li.className = 'active';
li.style.borderBottom = '1px solid #fff';
li.style.backgroundColor = '#fff';
} else {
li.style.backgroundColor = '#F4F9FD';
}
li.style.float = 'left';
li.style.height = '21px';
li.style.margin = '2px 2px 0 2px !important';
li.style.margin = '1px 2px 0 2px';
li.style.border = '1px solid #9EA0A1';
const a = document.createElement('a');
a.href = 'javascript:void(0);';
a.onclick = onClickHandler;
a.textContent = text;
a.style.display = 'block';
a.style.color = '#666';
a.style.textDecoration = 'none';
a.style.padding = '4px';
a.addEventListener('mouseover', function () {
a.style.background = '#fff';
});
a.addEventListener('mouseout', function () {
if (!isActive)
a.style.background = '#F4F9FD';
});
li.appendChild(a);
return li;
}
const li1 = createTab('Poule', isPoule, function () {
change_view(true);
});
ul.appendChild(li1);
const li2 = createTab('Tournois', !isPoule, function () {
change_view(false);
});
ul.appendChild(li2);
menuDiv.appendChild(ul);
return menuDiv;
}
function buildMatchArray(matchs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 800px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">Rouge</th>
<th class="has-text-align-center" data-align="center"></th>
<th class="has-text-align-center" data-align="center">Scores</th>
<th class="has-text-align-center" data-align="center"></th>
<th class="has-text-align-center" data-align="center">Bleu</th>
<th class="has-text-align-center" data-align="center">Date</th>
</tr>
</thead><tbody>`
for (const match of matchs) {
arrayContent += `
<tr>
<td class="has-text-align-right" data-align="right">${match.red}</td>
<td class="has-text-align-center" data-align="center">${match.red_w ? cupImg : ""}</td>
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
<td class="has-text-align-center" data-align="center">${match.blue_w ? cupImg : ""}</td>
<td class="has-text-align-left" data-align="left">${match.blue}</td>
<td class="has-text-align-center" data-align="center">${dateToString((match.red_w || match.blue_w) ? match.date : null)}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function buildRankArray(rankArray) {
const arrayDiv = document.createElement('div');
let arrayContent = `<figure class="wp-block-table is-style-stripes" style="font-size: 16px; margin-top: 2em">
<table style="width: 600px;overflow: auto">
<thead>
<tr>
<th class="has-text-align-center" data-align="center">Place</th>
<th class="has-text-align-center" data-align="center">Nom</th>
<th class="has-text-align-center" data-align="center">Victoire</th>
<th class="has-text-align-center" data-align="center">Ratio</th>
<th class="has-text-align-center" data-align="center">Points marqués</th>
<th class="has-text-align-center" data-align="center">Points reçus</th>
</tr>
</thead><tbody>`
for (const row of rankArray) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${row.rank}</td>
<td class="has-text-align-left" data-align="left">${row.name}</td>
<td class="has-text-align-center" data-align="center">${row.win}</td>
<td class="has-text-align-center" data-align="center">${row.pointRate.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${row.pointMake}</td>
<td class="has-text-align-center" data-align="center">${row.pointTake}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
arrayDiv.innerHTML = arrayContent;
return arrayDiv;
}
function buildTree(treeData) {
return drawGraph(initTree(treeData))
}
function poulePage(location) {
rootDiv.innerHTML = header + backButton;
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = '<h4>Recherche par catégorie</h4>';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
let currentPoule = 0;
const loadPoule = () => {
const loading = startLoading(content);
fetch(`${apiUrlRoot}/poule/data?poule=${currentPoule}&rf=${lastRf}`)
.then(response => {
if (response.status === 204) // No content => no update
return;
response.json().then(poule => {
lastRf = (poule.genTime !== undefined) ? poule.genTime : 0;
// console.log(poule);
if (location.length === 3 && poule.type < 3) {
location.pop();
window.location.hash = location.join('/');
}
dataContainer.replaceChildren();
if (poule.type === 1) {
for (const g in poule.matchs) {
if (Object.keys(poule.matchs).length > 1) {
const text = document.createElement('h4');
text.textContent = `Groupe ${g}`;
text.style.marginTop = '2em';
dataContainer.append(text);
}
dataContainer.append(buildMatchArray(poule.matchs[g]));
dataContainer.append(buildRankArray(poule.rankArray[g]));
}
} else if (poule.type === 2) {
dataContainer.append(buildTree(poule['trees']));
} else {
const change_view = (isPoule) => {
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view));
if (isPoule) {
for (const g in poule.matchs) {
if (Object.keys(poule.matchs).length > 1) {
const text = document.createElement('h4');
text.textContent = `Groupe ${g}`;
text.style.marginTop = '2em';
dataContainer.append(text);
}
dataContainer.append(buildMatchArray(poule.matchs[g]));
dataContainer.append(buildRankArray(poule.rankArray[g]));
}
} else {
dataContainer.append(buildTree(poule['trees']));
}
location[2] = isPoule ? 1 : 2;
window.location.hash = location.join('/');
}
change_view((location.length === 3) ? location[2] === '1' : true);
}
})
})
.catch(e => {
console.error(e);
dataContainer.replaceChildren(new Text("Erreur de chargement de la poule"));
})
.finally(() => stopLoading(loading));
}
const loading = startLoading(content);
fetch(`${apiUrlRoot}/poule/list`)
.then(response => response.json())
.then(poule => {
const select = document.createElement('select');
select.setAttribute('id', poule.id);
select.innerHTML = `<option value="0">--Sélectionner une catégorie--</option>`;
for (const pouleKey of Object.keys(poule).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))) {
select.innerHTML += `<option value="${poule[pouleKey]}">${pouleKey}</option>`;
}
select.addEventListener('change', e => {
location[1] = Object.keys(poule).find(key => poule[key] === Number(e.target.value));
location[1] = encodeURI(location[1]);
window.location.hash = location.join('/');
lastRf = 0;
currentPoule = e.target.value;
loadPoule();
})
content.append(select);
content.appendChild(dataContainer);
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
const tmp = poule[decodeURI(location[1])];
select.value = tmp;
currentPoule = tmp
loadPoule();
}
})
.catch(() => rootDiv.append(new Text("Erreur de chargement des catégories")))
.finally(() => stopLoading(loading));
rfFonction = () => {
if (currentPoule !== 0)
loadPoule();
}
rootDiv.append(content)
}
function buildCombView(comb) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>Info :</h3>
<ul>
<li>Nom Pr&eacute;nom : ${comb.name}</li>
<li>Club : ${comb.club}</li>
<li>Cat&eacute;gorie : ${comb.cat}</li>
</ul>
<h3>Statistique :</h3>
<ul>
<li>Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})</li>
<li>Points marqués : ${comb.pointMake}</li>
<li>Points reçus : ${comb.pointTake}</li>
<li>Ratio du score (point marqu&eacute; / point re&ccedil;u): ${comb.pointRatio.toFixed(3)}</li>
</ul>
<h3>Liste des matchs:</h3>
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 700px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">Date</th>
<th class="has-text-align-center" data-align="center">Poule</th>
<th class="has-text-align-center" data-align="center">Adversaire</th>
<th class="has-text-align-center" data-align="center">Scores</th>
<th class="has-text-align-center" data-align="center">Ratio</th>
<th class="has-text-align-center" data-align="center"></th>
</tr>
</thead><tbody>`
for (const match of comb.matchs) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${dateToString(match.date)}</td>
<td class="has-text-align-center" data-align="center">${match.poule}</td>
<td class="has-text-align-center" data-align="center">${match.adv}</td>
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
<td class="has-text-align-center" data-align="center">${match.ratio.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${match.win ? cupImg : ""}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function combPage(location) {
rootDiv.innerHTML = header + backButton;
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = '<h4>Recherche par combattant</h4>';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
const loadComb = (id) => {
const loading = startLoading(content);
fetch(`${apiUrlRoot}/comb/data?comb=${id}`)
.then(response => response.json())
.then(comb => {
console.log(comb);
dataContainer.replaceChildren(buildCombView(comb));
})
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant")))
.finally(() => stopLoading(loading));
}
const loading = startLoading(content);
fetch(`${apiUrlRoot}/comb/list`)
.then(response => response.json())
.then(combs => {
const select = document.createElement('select');
select.innerHTML = `<option value="0">--Sélectionner un combattant--</option>`;
for (const comb of Object.keys(combs).sort()) {
select.innerHTML += `<option value="${combs[comb]}">${comb}</option>`;
}
select.addEventListener('change', e => {
location[1] = Object.keys(combs).find(key => combs[key] === e.target.value);
location[1] = encodeURI(location[1]);
window.location.hash = location.join('/');
loadComb(e.target.value);
})
content.append(select);
content.appendChild(dataContainer);
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
const tmp = combs[decodeURI(location[1])];
select.value = tmp;
loadComb(tmp);
}
})
.catch(() => rootDiv.append(new Text("Erreur de chargement des combattants")))
.finally(() => stopLoading(loading));
rootDiv.append(content)
}
function buildClubView(club) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>Info :</h3>
<ul>
<li>Nom : ${club.name}</li>
<li>Nombre d'inscris : ${club.nb_insc}</li>
</ul>
<h3>Statistique :</h3>
<ul>
<li>Nombre de match disput&eacute; : ${club.nb_match}</li>
<li>Nombre de victoires : ${club.match_w}</li>
<li>Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}</li>
<li>Points marqués : ${club.pointMake}</li>
<li>Points reçus : ${club.pointTake}</li>
<li>Ratio de points moyen : ${club.ratioPoint.toFixed(3)}</li>
</ul>
<h3>Liste des menbres :</h3>
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 800px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">Cat&eacute;gorie</th>
<th class="has-text-align-center" data-align="center">Nom</th>
<th class="has-text-align-center" data-align="center">Victoires</th>
<th class="has-text-align-center" data-align="center">D&eacute;faites</th>
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
<th class="has-text-align-center" data-align="center">Points marqués</th>
<th class="has-text-align-center" data-align="center">Points reçus</th>
<th class="has-text-align-center" data-align="center">Ratio points</th>
</tr>
</thead><tbody>`
for (const comb of club.combs) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${comb.cat}</td>
<td class="has-text-align-center" data-align="center">${comb.name}</td>
<td class="has-text-align-center" data-align="center">${comb.w}</td>
<td class="has-text-align-center" data-align="center">${comb.l}</td>
<td class="has-text-align-center" data-align="center">${comb.ratioVictoire.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${comb.pointMake}</td>
<td class="has-text-align-center" data-align="center">${comb.pointTake}</td>
<td class="has-text-align-center" data-align="center">${comb.ratioPoint.toFixed(3)}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function clubPage(location) {
rootDiv.innerHTML = header + backButton;
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = '<h4>Recherche par club</h4>';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
const loadComb = (id) => {
const loading = startLoading(content);
fetch(`${apiUrlRoot}/club/data?club=${id}`)
.then(response => response.json())
.then(club => {
console.log(club);
dataContainer.replaceChildren(buildClubView(club));
})
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club")))
.finally(() => stopLoading(loading));
}
const loading = startLoading(content);
fetch(`${apiUrlRoot}/club/list`)
.then(response => response.json())
.then(clubs => {
const select = document.createElement('select');
select.innerHTML = `<option value="0">--Sélectionner un club--</option>`;
for (const club of Object.keys(clubs).sort()) {
select.innerHTML += `<option value="${clubs[club]}">${club}</option>`;
}
select.addEventListener('change', e => {
if (e.target.value === '0')
return;
location[1] = Object.keys(clubs).find(key => clubs[key] === Number(e.target.value));
location[1] = encodeURI(location[1]);
window.location.hash = location.join('/');
loadComb(e.target.value);
})
content.append(select);
content.appendChild(dataContainer);
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
const tmp = clubs[decodeURI(location[1])];
select.value = tmp;
loadComb(tmp);
}
})
.catch(() => rootDiv.append(new Text("Erreur de chargement des clubs")))
.finally(() => stopLoading(loading));
rootDiv.append(content)
}
function buildCombsView(combs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>Statistique :</h3>
<ul>
<li>Nombre d'inscris : ${combs.nb_insc}</li>
<li>Nombre de match disput&eacute; : ${combs.tt_match}</li>
<li>Points marqués : ${combs.point}</li>
</ul>
<h3>Liste des combattants :</h3>
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
<table style="width: 1200px;overflow: auto"><thead>
<tr>
<th class="has-text-align-center" data-align="center">Cat&eacute;gorie</th>
<th class="has-text-align-center" data-align="center">Club</th>
<th class="has-text-align-center" data-align="center">Nom</th>
<th class="has-text-align-center" data-align="center">Victoires</th>
<th class="has-text-align-center" data-align="center">D&eacute;faites</th>
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
<th class="has-text-align-center" data-align="center">Points marqués</th>
<th class="has-text-align-center" data-align="center">Points reçus</th>
<th class="has-text-align-center" data-align="center">Ratios points</th>
</tr>
</thead><tbody>`
for (const comb of combs.combs) {
arrayContent += `
<tr>
<td class="has-text-align-center" data-align="center">${comb.cat}</td>
<td class="has-text-align-center" data-align="center">${comb.club}</td>
<td class="has-text-align-center" data-align="center">${comb.name}</td>
<td class="has-text-align-center" data-align="center">${comb.w}</td>
<td class="has-text-align-center" data-align="center">${comb.l}</td>
<td class="has-text-align-center" data-align="center">${comb.ratioVictoire.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${comb.pointMake}</td>
<td class="has-text-align-center" data-align="center">${comb.pointTake}</td>
<td class="has-text-align-center" data-align="center">${comb.ratioPoint.toFixed(3)}</td>
</tr>`
}
arrayContent += `</tbody></table></figure>`
pouleDiv.innerHTML = arrayContent;
return pouleDiv;
}
function combsPage() {
rootDiv.innerHTML = header + backButton;
const content = document.createElement('div');
content.style.marginTop = '1em';
const dataContainer = document.createElement('div');
dataContainer.id = 'data-container';
const loading = startLoading(content);
fetch(`${apiUrlRoot}/comb/get_all`)
.then(response => response.json())
.then(combs => {
console.log(combs);
dataContainer.replaceChildren(buildCombsView(combs));
})
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste")))
.finally(() => stopLoading(loading));
content.append(dataContainer);
rootDiv.append(content)
}
window.addEventListener("load", () => {
let path = document.getElementById('safca_api_script').src;
const urlParams = new URLSearchParams(new URL(path).search);
apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id");
console.log("apiUrlRoot:", apiUrlRoot)
let hash = window.location.hash.substring(1);
if (hash.length === 0)
setSubPage('home');
else
setSubPage(hash);
});
class TreeNode {
constructor(data) {
this.data = data;
this.left = null;
this.right = null;
}
death() {
let dg = 0;
let dd = 0;
if (this.right != null)
dg = this.right.death();
if (this.left != null)
dg = this.left.death();
return 1 + Math.max(dg, dd);
}
}
function initTree(data_in) {
out = [];
for (const din of data_in) {
out.push(parseTree(din));
}
return out;
}
function parseTree(data_in) {
if (data_in?.data == null)
return null;
let node = new TreeNode(data_in.data);
node.left = parseTree(data_in?.left);
node.right = parseTree(data_in?.right);
return node;
}
const max_x = 500;
const size = 24;
function getBounds(root) {
let px = max_x;
let py;
let maxx, minx, miny, maxy
function drawNode(tree, px, py) {
let death = tree.death() - 1
if (death === 0) {
if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0;
if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0;
} else {
if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0))
miny = py - size * 2 * death - ((size * 1.5 / 2) | 0);
if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0))
maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0);
}
if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8;
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death);
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
}
if (root != null) {
py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2;
maxx = px;
minx = px;
miny = py - (size * 1.5 / 2) | 0;
maxy = py + (size * 1.5 / 2) | 0;
for (const node of root) {
px = px - size * 2 - size * 8;
if (minx > px) minx = px;
drawNode(node, px, py);
//graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny);
py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
px = maxx;
}
} else {
minx = 0;
maxx = 0;
miny = 0;
maxy = 0;
}
return [minx, maxx, miny, maxy];
}
function drawGraph(root = []) {
const canvas = document.createElement('canvas');
canvas.id = "myCanvas";
canvas.style.border = "1px solid grey";
canvas.style.marginTop = "10px";
const ctx = canvas.getContext("2d");
const [minx, maxx, miny, maxy] = getBounds(root);
canvas.width = maxx - minx;
canvas.height = maxy - miny;
ctx.translate(-minx, -miny);
ctx.fillStyle = "#000000"
ctx.lineWidth = 2
ctx.strokeStyle = "#000000"
function printText(s, x, y, width, height, lineG, lineD) {
ctx.save();
ctx.translate(x, y);
let tSize = 17;
let ratioX = height * 1. / 20.;
ctx.font = "100 " + tSize + "px Arial";
let mw = width - (ratioX * 2) | 0;
if (ctx.measureText(s).width > mw) {
let dTextSize = true;
do {
tSize--;
ctx.font = tSize + "px Arial";
} while (ctx.measureText(s).width > mw && tSize > 10)
if (!dTextSize || ctx.measureText(s).width > mw) {
let s = "";
for (const string2 in s.split(" ")) {
if (ctx.measureText(s + string2).width >= mw) {
s += "...";
break;
} else {
s += string2 + " "
}
}
}
}
const text = ctx.measureText(s)
let dx = (width - text.width) / 2;
let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillText(s, dx, dy, width - dy);
ctx.restore();
ctx.beginPath();
if (lineD) {
ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2);
ctx.lineTo(x + width, y + height / 2)
}
if (lineG) {
ctx.moveTo(x, y + height / 2);
ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2)
}
ctx.stroke();
}
function printScores(scores, px, py, scale) {
ctx.save();
ctx.translate(px - size * 2, py - size * scale);
ctx.font = "100 " + 14 + "px Arial";
ctx.textBaseline = 'top';
for (let i = 0; i < scores.length; i++) {
const score = scores[i].s1+"-"+scores[i].s2;
const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
const text = ctx.measureText(score);
let dx = (size * 2 - text.width) / 2;
let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
ctx.fillStyle = '#ffffffdd';
ctx.fillRect(dx, size * 2 * scale / div * (i) + dy, text.width, 14);
ctx.fillStyle = "#000000";
ctx.fillText(score, dx, size * 2 * scale / div * (i) + dy, size * 2);
}
ctx.restore();
}
function drawNode(tree, px, py) {
ctx.beginPath()
ctx.moveTo(px, py)
ctx.lineTo(px - size, py)
ctx.stroke();
let death = tree.death() - 1
let match = tree.data
if (death === 0) {
ctx.beginPath()
ctx.moveTo(px - size, py + size)
ctx.lineTo(px - size, py - size)
ctx.moveTo(px - size, py + size)
ctx.lineTo(px - size * 2, py + size)
ctx.moveTo(px - size, py - size)
ctx.lineTo(px - size * 2, py - size)
ctx.stroke();
printScores(match.scores, px, py, 1);
ctx.fillStyle = "#FF0000";
printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size - (size * 1.5 / 2) | 0,
size * 8, (size * 1.5) | 0, false, true)
ctx.fillStyle = "#0000FF";
printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size - (size * 1.5 / 2) | 0,
size * 8, (size * 1.5) | 0, false, true)
if (max_y < py + size + ((size * 1.5 / 2) | 0)) max_y = py + size + (size * 1.5 / 2) | 0;
} else {
ctx.beginPath()
ctx.moveTo(px - size, py)
ctx.lineTo(px - size, py + size * 2 * death)
ctx.moveTo(px - size, py)
ctx.lineTo(px - size, py - size * 2 * death)
ctx.moveTo(px - size, py + size * 2 * death)
ctx.lineTo(px - size * 2, py + size * 2 * death)
ctx.moveTo(px - size, py - size * 2 * death)
ctx.lineTo(px - size * 2, py - size * 2 * death)
ctx.stroke();
printScores(match.scores, px, py, 1.5);
ctx.fillStyle = "#FF0000";
printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2) | 0,
size * 8, (size * 1.5) | 0, true, true)
ctx.fillStyle = "#0000FF";
printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2) | 0,
size * 8, (size * 1.5) | 0, true, true)
if (max_y < py + size * 2 * death + ((size * 1.5 / 2) | 0))
max_y = py + size * 2 * death + ((size * 1.5 / 2) | 0);
}
ctx.stroke();
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death);
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
}
function win(scores) {
let sum = 0;
for (const score of scores) {
if (score.s1 === -1000 || score.s2 === -1000)
continue;
if (score.s1 > score.s2)
sum++;
else if (score.s1 < score.s2)
sum--;
}
return sum;
}
let px = max_x;
let py;
let max_y
if (root != null) {
py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2;
max_y = py + (size * 1.5 / 2) | 0;
for (const node of root) {
let win_name = "";
if (node.data.end) {
if (win(node.data.scores) > 0)
win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName;
else
win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName;
}
ctx.fillStyle = "#18A918";
printText(win_name, px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0),
size * 8, (size * 1.5) | 0, true, false);
px = px - size * 2 - size * 8;
drawNode(node, px, py);
py = max_y + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
px = max_x;
}
}
return canvas;
}

View File

@@ -0,0 +1,828 @@
{
"current_scene": "Ovarlay",
"current_program_scene": "Ovarlay",
"scene_order": [
{
"name": "Ovarlay"
},
{
"name": "Scène 2"
}
],
"name": "saf",
"groups": [],
"quick_transitions": [
{
"name": "Coupure",
"duration": 300,
"hotkeys": [],
"id": 5,
"fade_to_black": false
},
{
"name": "Fondu",
"duration": 300,
"hotkeys": [],
"id": 6,
"fade_to_black": false
}
],
"transitions": [],
"saved_projectors": [],
"canvases": [],
"current_transition": "Fondu",
"transition_duration": 300,
"preview_locked": false,
"scaling_enabled": false,
"scaling_level": -9,
"scaling_off_x": 0.0,
"scaling_off_y": 0.0,
"virtual-camera": {
"type2": 3
},
"modules": {
"auto-scene-switcher": {
"interval": 300,
"non_matching_scene": "",
"switch_if_not_matching": false,
"active": false,
"switches": []
},
"captions": {
"source": "",
"enabled": false,
"lang_id": 1036,
"provider": "mssapi"
},
"output-timer": {
"streamTimerHours": 0,
"streamTimerMinutes": 0,
"streamTimerSeconds": 30,
"recordTimerHours": 0,
"recordTimerMinutes": 0,
"recordTimerSeconds": 30,
"autoStartStreamTimer": false,
"autoStartRecordTimer": false,
"pauseRecordTimer": false
},
"scripts-tool": []
},
"version": 1,
"sources": [
{
"prev_ver": 536870916,
"name": "Capture d'écran",
"uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b",
"id": "monitor_capture",
"versioned_id": "monitor_capture",
"settings": {
"monitor_id": "\\\\?\\DISPLAY#HWP3320#5&3974db33&0&UID143619#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "Ovarlay",
"uuid": "670a1c48-dac6-405a-959a-d40662bab4d8",
"id": "scene",
"versioned_id": "scene",
"settings": {
"custom_size": false,
"id_counter": 21,
"items": [
{
"name": "sub1.img.blue",
"source_uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8",
"visible": true,
"locked": false,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 19,
"group_item_backup": false,
"pos": {
"x": 1789.0,
"y": 950.0
},
"scale": {
"x": 0.11296296119689941,
"y": 0.11296296119689941
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.img.rouge",
"source_uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502",
"visible": true,
"locked": false,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 14,
"group_item_backup": false,
"pos": {
"x": 9.0,
"y": 950.0
},
"scale": {
"x": 0.11296296119689941,
"y": 0.11296296119689941
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.comb.blue",
"source_uuid": "4e75e437-20ed-4834-bfac-b668f5cac960",
"visible": true,
"locked": true,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 17,
"group_item_backup": false,
"pos": {
"x": 960.0,
"y": 945.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.comb.rouge",
"source_uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f",
"visible": true,
"locked": true,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 8,
"group_item_backup": false,
"pos": {
"x": 140.0,
"y": 945.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.score.blue",
"source_uuid": "264866ab-58e1-45ec-a32f-c440c32057cd",
"visible": true,
"locked": true,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 20,
"group_item_backup": false,
"pos": {
"x": 1108.0,
"y": 10.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 0.0,
"y": 0.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.score.rouge",
"source_uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12",
"visible": true,
"locked": true,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 16,
"group_item_backup": false,
"pos": {
"x": 686.0,
"y": 10.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "sub1.temps",
"source_uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418",
"visible": true,
"locked": true,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 21,
"group_item_backup": false,
"pos": {
"x": 584.0,
"y": 0.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 1.0,
"y": 1.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
}
]
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {
"OBSBasic.SelectScene": [],
"libobs.show_scene_item.19": [],
"libobs.hide_scene_item.19": [],
"libobs.show_scene_item.14": [],
"libobs.hide_scene_item.14": [],
"libobs.show_scene_item.17": [],
"libobs.hide_scene_item.17": [],
"libobs.show_scene_item.8": [],
"libobs.hide_scene_item.8": [],
"libobs.show_scene_item.20": [],
"libobs.hide_scene_item.20": [],
"libobs.show_scene_item.16": [],
"libobs.hide_scene_item.16": [],
"libobs.show_scene_item.21": [],
"libobs.hide_scene_item.21": []
},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e",
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "Scène 2",
"uuid": "d430fe13-0e4a-4d4b-a28e-7c833eb27bf6",
"id": "scene",
"versioned_id": "scene",
"settings": {
"custom_size": false,
"id_counter": 2,
"items": [
{
"name": "Capture d'écran",
"source_uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b",
"visible": false,
"locked": false,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 2,
"group_item_backup": false,
"pos": {
"x": 0.0,
"y": 0.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 0.0,
"y": 0.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
},
{
"name": "Ovarlay",
"source_uuid": "670a1c48-dac6-405a-959a-d40662bab4d8",
"visible": true,
"locked": false,
"rot": 0.0,
"align": 5,
"bounds_type": 0,
"bounds_align": 0,
"bounds_crop": false,
"crop_left": 0,
"crop_top": 0,
"crop_right": 0,
"crop_bottom": 0,
"id": 1,
"group_item_backup": false,
"pos": {
"x": 0.0,
"y": 0.0
},
"scale": {
"x": 1.0,
"y": 1.0
},
"bounds": {
"x": 0.0,
"y": 0.0
},
"scale_filter": "disable",
"blend_method": "default",
"blend_type": "normal",
"show_transition": {
"duration": 0
},
"hide_transition": {
"duration": 0
},
"private_settings": {}
}
]
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {
"OBSBasic.SelectScene": [],
"libobs.show_scene_item.2": [],
"libobs.hide_scene_item.2": [],
"libobs.show_scene_item.1": [],
"libobs.hide_scene_item.1": []
},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e",
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.comb.blue",
"uuid": "4e75e437-20ed-4834-bfac-b668f5cac960",
"id": "text_gdiplus",
"versioned_id": "text_gdiplus_v2",
"settings": {
"chatlog": false,
"extents": true,
"gradient": false,
"outline": false,
"undo_sname": "sub1.comb_rouge",
"vertical": false,
"font": {
"face": "Arial",
"flags": 0,
"size": 65,
"style": "Normal"
},
"align": "right",
"valign": "bottom",
"color": 4294917120,
"extents_cx": 835,
"extents_cy": 120,
"antialiasing": true,
"overlay": true,
"text": "Xavier Login"
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.comb.rouge",
"uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f",
"id": "text_gdiplus",
"versioned_id": "text_gdiplus_v2",
"settings": {
"chatlog": false,
"extents": true,
"gradient": false,
"outline": false,
"undo_sname": "sub1.comb_rouge",
"vertical": false,
"font": {
"face": "Arial",
"flags": 0,
"size": 65,
"style": "Normal"
},
"align": "left",
"valign": "bottom",
"color": 4278190335,
"extents_cx": 820,
"extents_cy": 120,
"antialiasing": true,
"overlay": true,
"text": "Xavier Login"
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.img.blue",
"uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8",
"id": "slideshow",
"versioned_id": "slideshow",
"settings": {
"files": [],
"transition": "slide",
"slide_time": 9000,
"transition_speed": 1300,
"use_custom_size": "1:1",
"playback_behavior": "stop_restart",
"slide_mode": "mode_auto",
"overlay": true
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {
"SlideShow.PlayPause": [],
"SlideShow.Restart": [],
"SlideShow.Stop": [],
"SlideShow.NextSlide": [],
"SlideShow.PreviousSlide": []
},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.img.rouge",
"uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502",
"id": "slideshow",
"versioned_id": "slideshow",
"settings": {
"files": [],
"transition": "slide",
"slide_time": 9000,
"transition_speed": 1300,
"use_custom_size": "1:1",
"playback_behavior": "stop_restart",
"slide_mode": "mode_auto",
"overlay": true
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {
"SlideShow.PlayPause": [],
"SlideShow.Restart": [],
"SlideShow.Stop": [],
"SlideShow.NextSlide": [],
"SlideShow.PreviousSlide": []
},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.score.blue",
"uuid": "264866ab-58e1-45ec-a32f-c440c32057cd",
"id": "text_gdiplus",
"versioned_id": "text_gdiplus_v2",
"settings": {
"chatlog": false,
"extents": true,
"gradient": false,
"outline": false,
"text": "0",
"undo_sname": "sub1.comb_rouge",
"vertical": false,
"font": {
"face": "Arial",
"flags": 0,
"size": 80,
"style": "Normal"
},
"align": "center",
"valign": "bottom",
"color": 4294917120,
"extents_wrap": true,
"extents_cx": 163,
"extents_cy": 80,
"antialiasing": true
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.score.rouge",
"uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12",
"id": "text_gdiplus",
"versioned_id": "text_gdiplus_v2",
"settings": {
"chatlog": false,
"extents": true,
"gradient": false,
"outline": false,
"text": "0",
"undo_sname": "sub1.comb_rouge",
"vertical": false,
"font": {
"face": "Arial",
"flags": 0,
"size": 80,
"style": "Normal"
},
"align": "center",
"valign": "bottom",
"color": 4278190335,
"extents_wrap": true,
"extents_cx": 163,
"extents_cy": 80,
"antialiasing": true
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
},
{
"prev_ver": 536870916,
"name": "sub1.temps",
"uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418",
"id": "text_gdiplus",
"versioned_id": "text_gdiplus_v2",
"settings": {
"extents": true,
"text": "00:10",
"font": {
"face": "Arial",
"flags": 0,
"size": 110,
"style": "Normal"
},
"align": "center",
"color": -16711936,
"extents_wrap": true,
"extents_cx": 800,
"extents_cy": 110
},
"mixers": 0,
"sync": 0,
"flags": 0,
"volume": 1.0,
"balance": 0.5,
"enabled": true,
"muted": false,
"push-to-mute": false,
"push-to-mute-delay": 0,
"push-to-talk": false,
"push-to-talk-delay": 0,
"hotkeys": {},
"deinterlace_mode": 0,
"deinterlace_field_order": 0,
"monitoring_type": 0,
"private_settings": {}
}
]
}

View File

@@ -14,6 +14,7 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {FallingLines} from "react-loader-spinner";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
const router = createBrowserRouter([

View File

@@ -0,0 +1,26 @@
import {
IconDefinition,
IconName,
IconPrefix
} from "@fortawesome/fontawesome-svg-core";
export const SimpleIconsOBS: IconDefinition = {
icon: [
// SVG viewbox width (in pixels)
20,
// SVG viewbox height (in pixels)
20,
// Aliases (not needed)
[],
// Unicode as hex value (not needed)
"",
// SVG path data
"M10 0C4.486 0 0 4.486 0 10s4.486 10 10 10 10-4.485 10-10S15.515 0 10 0m8.159 14.075c.55-1.617-.057-3.491-1.551-4.448-1.75-1.12-5.196 1.14-5.196 1.14s-.763 2.815-.026 4.007a5.4 5.4 0 0 1-1.77 1.709c-2.348 1.4-5.372.848-7.054-1.209a9 9 0 0 1-.235-.346c1.139 1.423 3.219 1.831 4.855.908 1.809-1.02 1.427-5.124 1.427-5.124S6.598 8.774 5.26 8.799a5.424 5.424 0 0 1 2.993-7.76c.157-.03.317-.05.476-.072a3.76 3.76 0 1 0 4.525 5.435 5.4 5.4 0 0 1 2.794.793c2.199 1.345 3.119 4.041 2.344 6.397-.072.166-.154.324-.234.483"
],
iconName: "simple-icons-obs" as IconName,
prefix: "simple-icons" as IconPrefix
};

View File

@@ -93,7 +93,7 @@ export function TextField({name, text, value, placeholder, type = "text", disabl
<div className="input-group">
<span className="input-group-text" id={name}>{text}</span>
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required}/>
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required} autoComplete="false"/>
</div>
{ttip}
</div>
@@ -119,13 +119,13 @@ export function CheckField({name, text, value, row = false}) {
</>
}
export const Checkbox = ({label, value, onChange}) => {
export const Checkbox = ({label, value, onChange, name = "checkbox1"}) => {
const handleChange = () => {
onChange(!value);
};
return <div className="form-check">
<input className="form-check-input" type="checkbox" id="checkbox1" checked={value} onChange={handleChange}/>
<label className="form-check-label" htmlFor="checkbox1">{label}</label>
<input className="form-check-input" type="checkbox" id={name} checked={value} onChange={handleChange}/>
<label className="form-check-label" htmlFor={name}>{label}</label>
</div>
};

View File

@@ -2,6 +2,7 @@ import './Nav.css'
import {NavLink} from "react-router-dom";
import {useAuth} from "../hooks/useAuth.jsx";
import {login, logout} from "../utils/auth.js";
import {isClubAdmin} from "../utils/Tools.js";
export function Nav() {
@@ -43,9 +44,9 @@ function AffiliationMenu() {
}
function CompMenu() {
const {is_authenticated, userinfo} = useAuth()
const {is_authenticated} = useAuth()
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
if (!is_authenticated)
return <></>
return <li className="nav-item dropdown">
@@ -55,7 +56,8 @@ function CompMenu() {
<ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions Manager</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions
Manager</NavLink></li>
</ul>
</li>
}
@@ -63,8 +65,7 @@ function CompMenu() {
function ClubMenu() {
const {is_authenticated, userinfo} = useAuth()
if (!is_authenticated || !(userinfo?.roles?.includes("club_president")
|| userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_respo_intra")))
if (!is_authenticated || !(isClubAdmin(userinfo)))
return <></>
return <li className="nav-item dropdown">

View File

@@ -2,44 +2,21 @@ import {useEffect, useRef, useState, memo} from 'react';
const cache = {};
export function SmartLogoBackground({
src,
export function detectOptimalBackground(blob,
darkBackground = '#333333',
lightBackground = '#f0f0f0',
defaultBackground = 'transparent',
tolerance = 55,
minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond
alt = 'Logo',
style = {},
imgClassName = '',
}) {
const canvasRef = useRef(null);
const [background, setBackground] = useState(defaultBackground);
const [load, setLoad] = useState(false)
useEffect(() => {
if (cache[src]) {
setBackground(cache[src]);
return;
}
if (!load)
return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
minPixels = 10) {
return new Promise((resolve) => {
const imgUrl = URL.createObjectURL(blob);
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = src;
// Prevent error logging
img.onerror = function () {
return true;
}
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -69,12 +46,9 @@ export function SmartLogoBackground({
const i = Math.round(y * canvas.width + x) * 4;
if (isTransparent(i)) {
const neighbors = [
i - 4, // Haut
i + 4, // Bas
i - 4 * canvas.width, // Gauche
i + 4 * canvas.width, // Droite
i - 4, i + 4, // Haut/Bas
i - 4 * canvas.width, i + 4 * canvas.width, // Gauche/Droite
];
for (const neighbor of neighbors) {
if (neighbor >= 0 && neighbor < pixels.length) {
if (isLightColor(neighbor)) lightBorderCount++;
@@ -82,24 +56,64 @@ export function SmartLogoBackground({
}
}
}
if (lightBorderCount >= 25 || darkBorderCount >= 25)
break
}
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
cache[src] = darkBackground;
setBackground(darkBackground)
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
cache[src] = lightBackground;
setBackground(lightBackground)
} else {
cache[src] = defaultBackground;
setBackground(defaultBackground)
}
}
}
URL.revokeObjectURL(imgUrl);
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
resolve(darkBackground); // Fond sombre
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
resolve(lightBackground); // Fond clair
} else {
resolve(defaultBackground); // Fond transparent
}
};
img.onerror = () => {
URL.revokeObjectURL(imgUrl);
resolve(defaultBackground); // En cas d'erreur
};
img.src = imgUrl;
});
}
export function SmartLogoBackground({
src,
darkBackground = '#333333',
lightBackground = '#f0f0f0',
defaultBackground = 'transparent',
tolerance = 55,
minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond
alt = 'Logo',
style = {},
imgClassName = '',
}) {
const canvasRef = useRef(null);
const [background, setBackground] = useState(defaultBackground);
const [load, setLoad] = useState(false)
useEffect(() => {
if (!load) return;
const fetchAndDetect = async () => {
if (cache[src]) {
setBackground(cache[src]);
return;
}
try {
const response = await fetch(src);
const blob = await response.blob();
const detectedBackground = await detectOptimalBackground(blob, darkBackground, lightBackground, defaultBackground, tolerance, minPixels);
cache[src] = detectedBackground;
setBackground(detectedBackground);
} catch (error) {
console.error("Erreur de détection du fond:", error);
cache[src] = defaultBackground;
setBackground(defaultBackground);
}
};
fetchAndDetect();
}, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]);
return <>

View File

@@ -25,7 +25,7 @@ function reducer(state, action) {
country: action.payload.data.country,
})
if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) {
console.debug("Updating comb", comb);
//console.debug("Updating comb", comb);
return {
...state,
[comb.id]: comb
@@ -49,7 +49,7 @@ function reducer(state, action) {
for (const o of combs) {
newCombs[o.id] = o;
}
console.debug("Updating combs", newCombs);
//console.debug("Updating combs", newCombs);
return {
...state,

View File

@@ -1,6 +1,15 @@
import {createContext, useContext, useReducer} from "react";
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0});
const PubAffContext = createContext({
next: [],
c1: undefined,
c2: undefined,
showScore: true,
timeCb: undefined,
timeCb2: undefined,
scoreRouge: 0,
scoreBleu: 0
});
const PubAffDispatchContext = createContext(() => {
});
@@ -11,6 +20,8 @@ function reducer(state, action) {
case 'CALL_TIME':
if (state.timeCb)
state.timeCb(action.payload)
if (state.timeCb2)
state.timeCb2(action.payload)
return state
case 'CLEAR_CB_TIME':
return {...state, timeCb: undefined}

View File

@@ -0,0 +1,183 @@
import {createContext, useContext, useEffect, useRef, useState} from "react";
import OBSWebSocket from "obs-websocket-js";
import {hex2rgb} from "../utils/Tools.js";
const OBSContext = createContext({connected: false, obs: null})
export function OBSProvider({children}) {
const obs = useRef(null)
const obsParm = useRef(null)
const assets = useRef(null)
const [connected, setConnected] = useState(false)
const [doReconnect, setDoReconnect] = useState(false)
useEffect(() => {
if (!doReconnect)
return;
const timer = setInterval(() => {
if (obs.current && !connected && doReconnect) {
console.log("Reconnecting to OBS WebSocket...")
obs.current.connect(obsParm.current.adresse, obsParm.current.password)
.then(data => {
console.log("Reconnected to OBS WebSocket", data)
setDoReconnect(false)
})
}
}, 5000);
return () => clearInterval(timer);
}, [connected, doReconnect]);
const connect = (adresse, password = undefined, assets_dir = undefined) => {
if (connected && obs.current)
return;
assets.current = assets_dir;
const obs_ = new OBSWebSocket();
obs_.connect(adresse, password)
.then(data => {
console.log("Connected to OBS WebSocket", data)
})
.catch(err => {
console.error("Failed to connect to OBS WebSocket", err)
});
obs_.on('ConnectionOpened', () => {
setConnected(true)
console.log("OBS WebSocket connection opened")
});
obs_.on('ConnectionClosed', err => {
setConnected(false)
console.log("OBS WebSocket connection closed", err.code, err.message)
if (err.code === 1000 || err.code === 4009) // 1000 = Normal Closure, 4009 = Authentication Failure
return;
obsParm.current = {adresse, password}
setDoReconnect(true)
});
obs_.on('error', err => {
console.error("OBS WebSocket error", err)
});
obs.current = obs_;
}
const disconnect = () => {
if (obs.current && connected) {
obs.current.disconnect();
obs.current = null;
}
}
const ret = {connected, obs: obs.current, connect, disconnect, assets: assets.current}
return <OBSContext.Provider value={ret}>
{children}
</OBSContext.Provider>
}
function getElementName(element) {
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}`
}
export function useOBS() {
const {connected, obs, connect, disconnect, assets} = useContext(OBSContext)
const setTextAndColor = (element, text, color) => {
if (!connected)
return;
if (color.startsWith("#")){
const tmp = hex2rgb(color);
color = (tmp.b << 16) + (tmp.g << 8) + tmp.r;
}
obs.call('SetInputSettings', {
inputName: getElementName(element),
inputSettings: {
text: text,
color: color,
overlay: true,
}
}).catch(err => {
console.error("Failed to set text and color for OBS element", element, err)
});
}
const setText = (element, text) => {
if (!connected)
return;
obs.call('SetInputSettings', {
inputName: getElementName(element),
inputSettings: {
text: text,
overlay: true,
}
}).catch(err => {
console.error("Failed to set text for OBS element", element, err)
});
}
const setDiapo = (element, files) => {
if (!connected)
return;
const arr = [];
for (const file of files) {
arr.push({
hidden: false,
selected: false,
value: (assets.endsWith('/') ? assets : assets + '/') + file,
})
}
obs.call('SetInputSettings', {
inputName: getElementName(element),
inputSettings: {
files: arr,
overlay: true,
}
}).catch(err => {
console.error("Failed to set diapo for OBS element", element, err)
});
}
return {
connected,
obs,
connect,
disconnect,
setText,
setTextAndColor,
setDiapo,
}
}
export function exportOBSConfiguration(adresse, password, assets_dir) {
const config = {
adresse: adresse,
password: password,
assets_dir: assets_dir,
};
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "obs_configuration.json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
export async function importOBSConfiguration() {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = event => {
try {
const config = JSON.parse(event.target.result);
resolve(config);
} catch (err) {
reject(err);
}
};
reader.readAsText(file);
};
input.click();
});
}

View File

@@ -1,6 +1,4 @@
import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react";
import {apiAxios} from "../utils/Tools.js";
import {toast} from "react-toastify";
import {useAuth} from "./useAuth.jsx";
function uuidv4() {
@@ -45,6 +43,8 @@ export function WSProvider({url, onmessage, children}) {
const id = useId();
const {is_authenticated} = useAuth()
const [isReady, setIsReady] = useState(false)
const [doReconnect, setDoReconnect] = useState(false)
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
const [state, dispatch] = useReducer(reducer, {listener: []})
const ws = useRef(null)
const listenersRef = useRef([])
@@ -59,37 +59,51 @@ export function WSProvider({url, onmessage, children}) {
listenersRef.current = state.listener
}, [state.listener])
const welcomeListener = ({data}) => {
setWelcomeData({
name: data.name,
perm: data.perm,
show_blason: data.show_blason,
show_flag: data.show_flag
})
}
useEffect(() => {
if (!doReconnect && !is_authenticated && isReady)
return;
const timer = setInterval(() => {
if (isReady || !doReconnect || !is_authenticated)
return;
console.log("WSProvider: reconnecting to", url);
try {
const newSocket = new WebSocket(url)
newSocket.onopen = ws.current.onopen
newSocket.onclose = ws.current.onclose
newSocket.onmessage = ws.current.onmessage
ws.current = newSocket
} catch (e) {
}
}, 5000);
return () => clearInterval(timer);
}, [isReady, doReconnect, is_authenticated]);
useEffect(() => {
if (!mountCounter[id])
mountCounter[id] = 0
mountCounter[id] += 1
setDoReconnect(true)
console.log(`WSProvider ${id} mounted ${mountCounter[id]} time(s)`);
if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)){
if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)) {
console.log("WSProvider: connecting to", url);
const socket = new WebSocket(url)
socket.onopen = () => setIsReady(true)
socket.onclose = () => {
setIsReady(false)
if (mountCounter[id] > 0) {
setTimeout(() => {
//if (is_authenticated){
console.log("WSProvider: reconnecting to", url);
try {
const newSocket = new WebSocket(url)
ws.current = newSocket
newSocket.onopen = socket.onopen
newSocket.onclose = socket.onclose
newSocket.onmessage = socket.onmessage
}catch (e) {
}
//}else{
// console.log("WSProvider: not reconnecting, user is not authenticated");
//}
}, 5000)
}
}
socket.onmessage = (event) => {
const msg = JSON.parse(event.data)
@@ -118,6 +132,12 @@ export function WSProvider({url, onmessage, children}) {
console.error("Listener callback error:", err)
}
});
if (msg.code === 'welcomeInfo') {
welcomeListener({...msg})
isHandled = true;
}
if (!isHandled && onmessage)
onmessage(JSON.parse(event.data))
}
@@ -132,6 +152,7 @@ export function WSProvider({url, onmessage, children}) {
setTimeout(() => {
console.log(`WSProvider ${id} checking for close, ${mountCounter[id]} instance(s) remain`);
if (mountCounter[id] === 0) {
setDoReconnect(false)
console.log("WSProvider: closing connection to", url);
ws.current.close()
}
@@ -139,13 +160,14 @@ export function WSProvider({url, onmessage, children}) {
}
}, [])
const send = (uuid, code, type, data, resolve = () => {
const send2 = (uuid, code, type, data, resolve = () => {
}, reject = () => {
}) => {
if (!isReadyRef.current) {
reject("WebSocket is not connected");
return;
}
if (type === "REQUEST") {
const timeout = setTimeout(() => {
reject("timeout");
@@ -164,7 +186,7 @@ export function WSProvider({url, onmessage, children}) {
}
}
console.log("WSProvider: sending message", {uuid, code, type, data});
//console.log("WSProvider: sending message", {uuid, code, type, data});
ws.current?.send(JSON.stringify({
uuid: uuid,
code: code,
@@ -172,20 +194,41 @@ export function WSProvider({url, onmessage, children}) {
data: data
}))
}
const send = (uuid, code, type, data, resolve = () => {
}, reject = () => {
}) => {
if (isReadyRef.current) {
send2(uuid, code, type, data, resolve, reject);
} else {
let counter = 0;
const waitInterval = setInterval(() => {
if (isReadyRef.current) {
clearInterval(waitInterval);
send2(uuid, code, type, data, resolve, reject);
}
counter += 1;
if (counter >= 300) { // 30 seconds timeout
clearInterval(waitInterval);
reject("WebSocket is not connected");
}
}, 100);
}
}
const ret = {isReady, dispatch, send, wait_length: callbackRef}
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData}
return <WebsocketContext.Provider value={ret}>
{children}
</WebsocketContext.Provider>
}
export function useWS() {
const {isReady, dispatch, send, wait_length} = useContext(WebsocketContext)
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext)
return {
dispatch,
isReady,
wait_length,
welcomeData,
sendRequest: (code, data) => {
return new Promise((resolve, reject) => {
send(uuidv4(), code, "REQUEST", data, resolve, reject);
@@ -229,7 +272,7 @@ export function useRequestWS(code, payload, setLoading = null, loadingLevel = 1)
useEffect(() => {
if (isReady)
refresh(code, payload)
else{
else {
if (setLoading)
setLoading(loadingLevel)
setTimeout(() => refresh(code, payload), 1000)

View File

@@ -299,7 +299,7 @@ function AssoInfo({initData, needFile}) {
<div className="mb-3">
<div className="input-group">
<label className="input-group-text" htmlFor="status">Statue{needFile && "*"}</label>
<label className="input-group-text" htmlFor="status">Statuts{needFile && "*"}</label>
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
</div>
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette
@@ -313,7 +313,7 @@ function MembreInfo({role, initData}) {
return <>
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôles</label>
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôle</label>
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
<option value="0">Sélectionner...</option>

View File

@@ -12,6 +12,10 @@ import {
faUserGroup,
faVenus
} from "@fortawesome/free-solid-svg-icons";
import {CheckField} from "../components/MemberCustomFiels.jsx";
import {toast} from "react-toastify";
import {apiAxios, getCatName} from "../utils/Tools.js";
import {useEffect, useState} from "react";
const vite_url = import.meta.env.VITE_URL;
@@ -30,12 +34,13 @@ export function MePage() {
</div>
<div className="col-lg-8">
<InformationForm data={data}/>
<SettingsCard data={data}/>
<div className="row">
<div className="col-md-6">
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
</div>
<div className="col-md-6">
<LoadingProvider><SelectCard/></LoadingProvider>
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
</div>
</div>
</div>
@@ -88,16 +93,62 @@ function PhotoCard({data}) {
</div>;
}
function SelectCard() {
function SelectCard({userData}) {
return <div className="card mb-4 mb-md-0">
<div className="card-header">Sélection en équipe de France</div>
<div className="card-body">
<ul className="list-group">
{userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
</div>
})}
</ul>
</div>
</div>;
}
function SettingsCard({data}) {
const [privacy, setPrivacy] = useState("PUBLIC");
export function InformationForm({data}) {
useEffect(() => {
if (data?.resultPrivacy) {
setPrivacy(data.resultPrivacy);
}
}, [data]);
const handleChange = (e) => {
const formData = new FormData();
formData.append("resultPrivacy", e.target.value);
toast.promise(apiAxios.put(`/member/me/setting`, formData),
{
pending: 'Mise à jours des paramètres en cours...',
success: 'Paramètres mis à jours avec succès 🎉',
error: 'Échec de la mise à jours des paramètres 😕'
})
.then(() => {
setPrivacy(String(formData.get("resultPrivacy")));
});
}
return <div className="card mb-4">
<div className="card-header">Paramètres du compte</div>
<div className="card-body">
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="email_notifications">Visibilité des résultats</label>
<select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}>
<option value="PUBLIC">Public (visible par tous)</option>
<option value="REGISTERED_ONLY">Membres connectés (visibles par les membres de la fédération)</option>
<option value="REGISTERED_ONLY_NO_DETAILS">Membres connectés - masquer les détails (visibles par les membres de la fédération)
</option>
<option value="PRIVATE">Privé (visible uniquement par moi)</option>
</select>
</div>
</div>
</div>;
}
function InformationForm({data}) {
const style = {marginRight: '0.7em'}
return <div className="card mb-4">

View File

@@ -35,6 +35,7 @@ export function MemberList({source}) {
payment: 2,
order: "",
categorie: "",
archived: false,
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
}
@@ -43,10 +44,10 @@ export function MemberList({source}) {
data,
error,
refresh
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`, setLoading, 1)
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`, setLoading, 1)
useEffect(() => {
const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`;
const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`;
if (tmp === lastRefresh)
return;
lastRefresh = tmp
@@ -137,6 +138,8 @@ export function MemberList({source}) {
<div className="card-body">
<FiltreBar showLicenceState={showLicenceState}
setShowLicenceState={setShowLicenceState}
showArchived={filter.archived}
setShowArchived={e => setFilter({...filter, archived: e})}
clubFilter={filter.club}
setClubFilter={e => setFilter({...filter, club: e})}
source={source}
@@ -523,6 +526,8 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
function FiltreBar({
showLicenceState,
setShowLicenceState,
showArchived,
setShowArchived,
clubFilter,
setClubFilter,
source,
@@ -538,6 +543,9 @@ function FiltreBar({
<div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
</div>
<div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/>
</div>
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option>

View File

@@ -4,13 +4,14 @@ import {AxiosError} from "../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useEffect, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import {apiAxios, errFormater} from "../utils/Tools.js";
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
import {toast} from "react-toastify";
import {SearchBar} from "../components/SearchBar.jsx";
import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
import "./PayAndValidateList.css";
import * as Tools from "../utils/Tools.js";
export function PayAndValidateList({source}) {
@@ -22,6 +23,7 @@ export function PayAndValidateList({source}) {
const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]);
const [clubFilter, setClubFilter] = useState("");
const [catFilter, setCatFilter] = useState("");
const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2)
const [lastSearch, setLastSearch] = useState("");
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
@@ -34,15 +36,15 @@ export function PayAndValidateList({source}) {
data,
error,
refresh
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1)
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1)
useEffect(() => {
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
}, [selectedMembers]);
useEffect(() => {
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]);
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);
useEffect(() => {
if (!data)
@@ -62,7 +64,7 @@ export function PayAndValidateList({source}) {
setMemberData(data2);
}, [data, licenceData]);
useEffect(() => {
const fetchLicenceData = () => {
toast.promise(
apiAxios.get(`/licence/current/${source}`),
{
@@ -77,6 +79,10 @@ export function PayAndValidateList({source}) {
.then(data => {
setLicenceData(data.data);
});
};
useEffect(() => {
fetchLicenceData();
}, []);
const search = (search) => {
@@ -104,7 +110,31 @@ export function PayAndValidateList({source}) {
}
).then(() => {
setSelectedMembers([]);
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
});
}
const handlePayment = () => {
if (selectedMembers.length === 0) {
toast.error("Aucun membre sélectionné");
return;
}
toast.promise(
apiAxios.post(`/licence/validate-pay`, selectedMembers),
{
pending: "Validation des licences en cours...",
success: "Licences validées avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la validation des licences")
}
}
}
).then(() => {
setSelectedMembers([]);
fetchLicenceData();
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
});
}
@@ -153,13 +183,20 @@ export function PayAndValidateList({source}) {
<div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
setPaymentFilter={setPaymentFilter}/>
setPaymentFilter={setPaymentFilter} setCatFilter={setCatFilter} catFilter={catFilter}/>
</div>
</div>
<div className="mb-4">
{source === "admin" && <>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement
des {selectedMembers.length} licences sélectionnée
</button>
<ConfirmDialog title="Payment des licences"
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"}
onConfirm={handlePayment} id="confirm-pay"/>
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider
les {selectedMembers.length} licences sélectionnée
</button>
<ConfirmDialog title="Validation des licences"
@@ -385,7 +422,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
let allClub = []
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) {
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter, setCatFilter, catFilter}) {
useEffect(() => {
if (!data)
return;
@@ -395,6 +432,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
return <div>
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option>
{Tools.CatList.map(cat => (
<option key={cat} value={cat}>{getCatName(cat)}</option>
))}
</select>
</div>
<div className="mb-3">
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
<option value={1}>Avec demande ou licence validée</option>

View File

@@ -14,7 +14,7 @@ export default function StatsLazy({data}) {
</div>
<div className="row" style={{marginTop: "3em"}}>
<h3>Nombre de licences par catégorie pour {getSaison()}</h3>
<div className="col-lg-8" style={{margin: "auto"}}>
<div className="col-lg-8" style={{margin: "auto"}}>
<div style={{width: '100%', aspectRatio: 1.5}}>
<CatGraph raw_data={data}/>
</div>
@@ -60,7 +60,8 @@ function NbGraph({raw_data}) {
<Area type="monotone" dataKey="na" name="Non définie" stackId="1" stroke={colors[0]} fill={colors[0]}/>
<Area type="monotone" dataKey="h" name="Homme" stackId="1" stroke={colors[1]} fill={colors[1]}/>
<Area type="monotone" dataKey="f" name="Femme" stackId="1" stroke={colors[2]} fill={colors[2]}/>
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5" fillOpacity="30%"/>
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5"
fillOpacity="30%"/>
</AreaChart>
</ResponsiveContainer>
}
@@ -75,25 +76,25 @@ function CatGraph({raw_data}) {
for (const k of raw_data.categories) {
data.push({
name: k.name,
value: k.count,
value: k.count + 1,
})
}
setShowData(data)
}, [raw_data])
const COLORS = ['#33FF8F', '#30E393', '#2DC697', '#2AAA9B'
, '#278E9F', '#2471A3', '#2155A7', '#1E39AB'
, '#1B1CAF', '#1800B3'];
const COLORS = ['#ffff99', '#a6cee3', '#1f78b4', '#b2df8a'
, '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f'
, '#ff7f00', '#cab2d6', '#6a3d9a'];
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({cx, cy, midAngle, innerRadius, outerRadius, index}) => {
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const radius = innerRadius + (outerRadius - innerRadius) * 0.6;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return <> {
showData[index]['value'] > 0 &&
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
<text x={x} y={y} fill={(index === 2 || index === 6 || index === 10) ? "white" : "black"} textAnchor="middle" dominantBaseline="central">
{`${showData[index]['name']}`}
</text>
@@ -159,7 +160,7 @@ function CatGraph({raw_data}) {
onMouseEnter={onPieEnter}>
{showData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]}/>
<Cell key={`cell-${index}`} fill={COLORS[(index) % COLORS.length]}/>
))}
</Pie>
</PieChart>

View File

@@ -100,7 +100,7 @@ function InformationForm({data}) {
}
return <>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/>
<input name="clubId" value={data.clubId} readOnly hidden/>

View File

@@ -60,7 +60,7 @@ function InformationForm() {
}
return <>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4">
<div className="card-header">Nouveau club</div>
<div className="card-body text-center">

View File

@@ -67,10 +67,10 @@ export function InformationForm({data}) {
'Content-Type': 'multipart/form-data',
}
}).then(_ => {
toast.success('Profile mis à jours avec succès 🎉');
toast.success('Profil mis à jours avec succès 🎉');
}).catch(e => {
console.log(e.response)
toast.error(errFormater(e,'Échec de la mise à jours du profile 😕'));
toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
}).finally(() => {
if (setLoading)
setLoading(0)
@@ -79,7 +79,7 @@ export function InformationForm({data}) {
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>
return <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4">
<div className="card-header">Information</div>
<div className="card-body">

View File

@@ -11,6 +11,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js";
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SelectCard} from "./SelectCard.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -58,7 +59,7 @@ export function MemberPage() {
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
</div>
<div className="col-md-6">
<LoadingProvider><SelectCard/></LoadingProvider>
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
</div>
</div>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
@@ -94,14 +95,3 @@ function PhotoCard({data}) {
</div>
</div>;
}
function SelectCard() {
return <></>
/*return <div className="card mb-4 mb-md-0">
<div className="card-header">Sélection en équipe de France</div>
<div className="card-body">
<p className="mb-1">Web Design</p>
</div>
</div>;*/
}

View File

@@ -0,0 +1,208 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {apiAxios, CatList, errFormater, getCatName, getSaison} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
function selectionReducer(selections, action) {
switch (action.type) {
case 'ADD':
return [
...selections,
action.payload
]
case 'REMOVE':
return selections.filter(selection => selection.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = selections.findIndex(selection => selection.id === action.payload.id)
if (index === -1) {
return [
...selections,
action.payload
]
} else {
selections[index] = action.payload
return [...selections]
}
case 'SORT':
return selections.sort((a, b) => b.saison - a.saison)
default:
throw new Error()
}
}
export function SelectCard({userData}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
const [modalSelection, setModal] = useState({id: -1, membre: userData.id})
const [selections, dispatch] = useReducer(selectionReducer, [])
useEffect(() => {
if (!data) return
for (const dataKey of data) {
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
}
dispatch({type: 'SORT'})
}, [data]);
return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid">
<div className="row">
<div className="col">Sélection en équipe de France</div>
<div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#SelectionModal"
onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>Ajouter
</button>
</div>
</div>
</div>
<div className="card-body">
<ul className="list-group">
{selections.map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#SelectionModal" onClick={_ => setModal(selection)}>
<FontAwesomeIcon icon={faPen}/></button>
</div>
})}
{error && <AxiosError error={error}/>}
</ul>
</div>
<div className="modal fade" id="SelectionModal" tabIndex="-1" aria-labelledby="SelectionModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<ModalContent selection={modalSelection} dispatch={dispatch}/>
</div>
</div>
</div>
</div>;
}
function sendSelection(event, dispatch) {
event.preventDefault();
const formData = new FormData(event.target);
formData.set('selection', event.target.selection?.value?.length > 0 ? event.target.selection?.value : null)
formData.set('categorie', event.target.categorie?.value)
toast.promise(
apiAxios.post(`/selection/${event.target.membre.value}`, {
id: event.target.id.value,
membre: event.target.membre.value,
saison: event.target.saison.value,
categorie: event.target.categorie.value,
}),
{
pending: "Enregistrement de la séléction en cours",
success: "Séléction enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la séléction")
}
}
}
).then(data => {
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
dispatch({type: 'SORT'})
})
}
function removeSelection(id, dispatch) {
toast.promise(
apiAxios.delete(`/selection/${id}`),
{
pending: "Suppression de la séléction en cours",
success: "Séléction supprimée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la séléction")
}
}
}
).then(_ => {
dispatch({type: 'REMOVE', payload: id})
})
}
function ModalContent({selection, dispatch}) {
const [saison, setSaison] = useState(0)
const [cat, setCat] = useState("")
const [isNew, setNew] = useState(true)
const setSeason = (event) => {
setSaison(Number(event.target.value))
}
const handleCatChange = (event) => {
setCat(event.target.value);
}
useEffect(() => {
if (selection.id !== -1) {
setNew(false)
setSaison(selection.saison)
} else {
setNew(true)
setSaison(getSaison())
}
setCat(selection.categorie)
}, [selection]);
return <form onSubmit={e => sendSelection(e, dispatch)}>
<input name="id" value={selection.id} readOnly hidden/>
<input name="membre" value={selection.membre} readOnly hidden/>
<div className="modal-header">
<h1 className="modal-title fs-5" id="SelectionModalLabel">Edition de la séléction</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="input-group mb-3 justify-content-md-center">
{isNew
? <input type="number" className="form-control" placeholder="Saison" name="saison"
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
: <><span className="input-group-text" id="basic-addon2">{saison}</span>
<input name="saison" value={saison} readOnly hidden/></>}
<span className="input-group-text" id="basic-addon2">-</span>
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
</div>
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="inputGroupSelect01">Catégorie</label>
<select className="form-select" id="inputGroupSelect01" value={cat} onChange={handleCatChange} name="categorie" required>
<option>Choisir...</option>
{CatList.map((cat) => {
return (<option key={cat} value={cat}>{getCatName(cat)}</option>)
})}
</select>
</div>
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
onClick={() => removeSelection(selection.id, dispatch)}>Supprimer</button>}
</div>
</form>
}
function RadioGroupeOnOff({value, onChange, name, text}) {
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
aria-label="Basic radio toggle button group">
<span className="input-group-text">{text}</span>
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
value="false" checked={value === false} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
value="true" checked={value === true} onChange={onChange}/>
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
</div>;
}

View File

@@ -155,8 +155,8 @@ function ModalContent2({clubData, data}) {
aria-label="Close"></button>
</div>
<div className="modal-body">
<p>Veuillez sélectionner 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
vous pourrez toujours remplacer un des membres sélectionné à la prochaine étape)</p>
<p>Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
vous pourrez les renseigner à la prochaine étape)</p>
{data && data.map((d, index) => {
return <div key={index} className="input-group mb-1">
<div className="input-group-text">

View File

@@ -67,7 +67,7 @@ function InformationForm({data}) {
}
return <>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4">
<input name="id" value={data.id} readOnly hidden/>
<div className="card-header">Affiliation n°{data.no_affiliation}</div>

View File

@@ -42,7 +42,7 @@ export function InformationForm({data}) {
addPhoto(event, formData, send);
}
return <form onSubmit={handleSubmit}>
return <form onSubmit={handleSubmit} autoComplete="off">
<div className="card mb-4">
<div className="card-header">Information</div>
<div className="card-body">

View File

@@ -10,6 +10,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
import {SelectCard} from "./SelectCard.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -56,7 +57,7 @@ export function MemberPage() {
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
</div>
<div className="col-md-6">
<LoadingProvider><SelectCard/></LoadingProvider>
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
</div>
</div>
{data.licence == null &&
@@ -93,14 +94,3 @@ function PhotoCard({data}) {
</div>
</div>;
}
function SelectCard() {
return <></>
/*return <div className="card mb-4 mb-md-0">
<div className="card-header">Sélection en équipe de France</div>
<div className="card-body">
<p className="mb-1">Soon</p>
</div>
</div>;*/
}

View File

@@ -0,0 +1,27 @@
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
import {getCatName} from "../../../utils/Tools.js";
import {AxiosError} from "../../../components/AxiosError.jsx";
export function SelectCard({userData}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid">
<div className="row">
<div className="col">Sélection en équipe de France</div>
</div>
</div>
<div className="card-body">
<ul className="list-group">
{data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => {
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
</div>
})}
{error && <AxiosError error={error}/>}
</ul>
</div>
</div>;
}

View File

@@ -51,9 +51,11 @@ export function CompetitionEdit() {
<Content data={data} refresh={refresh}/>
{data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary"
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier les participants</button>}
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier
les participants</button>}
{data.id !== null && data.system === "SAFCA" && <ContentSAFCA data2={data}/>}
{data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
<ContentSAFCAAndInternal data2={data} type={data.system}/>}
{data.id !== null && <>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
@@ -72,9 +74,12 @@ export function CompetitionEdit() {
</>
}
function ContentSAFCA({data2}) {
function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
const getDataPath = type === "SAFCA" ? `/competition/${data2.id}/safcaData` : `/competition/${data2.id}/internalData`
const setDataPath = type === "SAFCA" ? "/competition/safcaData" : "/competition/internalData"
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/competition/${data2.id}/safcaData`, setLoading, 1)
const {data, error} = useFetch(getDataPath, setLoading, 1)
const [state, dispatch] = useReducer(SimpleReducer, [])
const [state2, dispatch2] = useReducer(SimpleReducer, [])
@@ -109,7 +114,7 @@ function ContentSAFCA({data2}) {
out['table'] = state2.map(d => d.data)
toast.promise(
apiAxios.post(`/competition/safcaData`, out),
apiAxios.post(setDataPath, out),
{
pending: "Enregistrement des paramètres en cours",
success: "Paramètres enregistrée avec succès 🎉",
@@ -402,7 +407,8 @@ function Content({data}) {
<li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à
chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour
qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> &gt; <strong>Paramètres</strong> &gt;
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> &gt;
<strong>Paramètres</strong> &gt;
<strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong>
dans le champ <strong>Mon URL de callback</strong> et enregister.
<img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li>

View File

@@ -38,7 +38,7 @@ export function CompetitionRegisterAdmin({source}) {
}, [data, clubFilter, catFilter]);
const sendRegister = (new_state) => {
toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: {
render({data}) {
return data.response.data || "Combattant non trouvé"
@@ -75,11 +75,11 @@ export function CompetitionRegisterAdmin({source}) {
onClick={() => setModalState({id: 0})}>Ajouter un combattant
</button>
</div>
<div className="mb-4">
{source === "admin" && <div className="mb-4">
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
onClick={() => setModalState({id: -793548328091516928})}>Ajouter un invité
</button>
</div>
</div>}
<QuickAdd sendRegister={sendRegister} source={source}/>
<div className="card mb-4">
<div className="card-header">Filtre</div>
@@ -147,15 +147,19 @@ function SearchMember({sendRegister}) {
const handleAdd = (name) => {
const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name);
console.log("Quick add licence:", member)
if (!member) {
toast.error("Combattant non trouvé");
return;
}
sendRegister({
licence: member.licence, fname: member.fname, lname: member.lname, weight: "", overCategory: 0, lockEdit: false, id: null
licence: member.licence,
fname: member.fname,
lname: member.lname,
weight: "",
overCategory: 0,
lockEdit: false,
id: null
})
}
@@ -165,7 +169,7 @@ function SearchMember({sendRegister}) {
const names = data.map(member => `${member.fname} ${member.lname}`.trim());
names.sort((a, b) => a.localeCompare(b));
setSuggestions(names);
}, []);
}, [data]);
return <>
{data ? <div className="row mb-3" style={{marginTop: "0.5em"}}>
@@ -331,9 +335,9 @@ function Modal({sendRegister, modalState, setModalState, source}) {
<form onSubmit={e => {
e.preventDefault()
const new_state = {
licence: licence,
fname: fname,
lname: lname,
licence: Number.isInteger(licence) ? licence : licence.trim(),
fname: fname.trim(),
lname: lname.trim(),
weight: weight,
overCategory: cat,
lockEdit: lockEdit,
@@ -346,8 +350,10 @@ function Modal({sendRegister, modalState, setModalState, source}) {
new_state.country = country_
new_state.genre = genre
}
setModalState(new_state)
sendRegister(new_state)
.then(() => {
setModalState(new_state)
})
}}>
<div className="modal-header">
<h1 className="modal-title fs-5"
@@ -355,7 +361,9 @@ function Modal({sendRegister, modalState, setModalState, source}) {
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
{modalState.id < 0 && <div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
{modalState.id < 0 &&
<div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via
ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
<div className="card" style={{marginBottom: "1em"}}>
<div className="card-header">{modalState.id >= 0 ? "Recherche*" : "Information"}</div>
<div className="card-body">

View File

@@ -79,6 +79,13 @@ function MakeContent({data}) {
href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank"
rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p>
}
{data.canEditRegisters &&
<div style={{marginTop: "0.5em"}}>
<button type="button" className="btn btn-primary"
onClick={_ => navigate("/competition/" + data.id + "/register")}>Inscription - mode administrateur
</button>
</div>
}
</div>
</div>
}

View File

@@ -1,30 +1,40 @@
import {useEffect, useRef, useState} from "react";
import React, {useEffect, useRef, useState} from "react";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {toast} from "react-toastify";
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js"
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {CategoryContent} from "./CategoryAdminContent.jsx";
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
import {createPortal} from "react-dom";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
export function CMAdmin() {
const vite_url = import.meta.env.VITE_URL;
export function CMAdmin({compUuid}) {
const [catId, setCatId] = useState(null);
const [cat, setCat] = useState(null);
const menuActions = useRef({});
const {dispatch} = useWS();
useEffect(() => {
const categoryListener = ({data}) => {
if (!cat || data.id !== cat.id)
return
setCat({
...cat,
setCat(cat_ => ({
...cat_,
name: data.name,
liceName: data.liceName,
type: data.type
})
}))
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener})
}, []);
}, [cat]);
return <>
<div className="card">
@@ -35,10 +45,261 @@ export function CMAdmin() {
<div className="card-body">
<LoadingProvider>
<div className="row">
<CategoryContent cat={cat} catId={catId} setCat={setCat}/>
<CategoryContent cat={cat} catId={catId} setCat={setCat} menuActions={menuActions}/>
</div>
</LoadingProvider>
</div>
<Menu menuActions={menuActions} compUuid={compUuid}/>
</div>
</>
}
let tto = [];
function resizeImageWithOptimalBackground(blob) {
return new Promise(async (resolve) => {
const background = await detectOptimalBackground(blob);
const imgUrl = URL.createObjectURL(blob);
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = 1080;
canvas.height = 1080;
const ctx = canvas.getContext('2d');
// Dessiner l'image centrée
const scale = Math.min(1080 / img.width, 1080 / img.height);
const newWidth = img.width * scale;
const newHeight = img.height * scale;
const x = (1080 - newWidth) / 2;
const y = (1080 - newHeight) / 2;
ctx.filter = `drop-shadow(0 0 2rem ${background})`;
ctx.drawImage(img, x, y, newWidth, newHeight);
// Exporter en PNG
canvas.toBlob((newBlob) => {
resolve(newBlob);
URL.revokeObjectURL(imgUrl);
}, 'image/png', 1.0);
};
img.onerror = () => resolve(blob); // Retourne l'original en cas d'erreur
img.src = imgUrl;
});
}
async function downloadResourcesAsZip(resourceList) {
const zip = new JSZip();
const modal = new bootstrap.Modal(document.getElementById('progressModal'));
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
let completed = 0;
if (!resourceList.some(d => d.url === '/obs_template.json'))
resourceList.push({url: '/obs_template.json', name: 'saf_obs_template.json'});
// Afficher la modale
modal.show();
// Fonction pour télécharger une ressource et l'ajouter au ZIP
const addResourceToZip = async (data) => {
try {
const response = await fetch(data.url);
if (!response.ok) {
if (response.status === 404) {
return {success: false, filename: data.name || data.url.split('/').pop()};
}
// noinspection ExceptionCaughtLocallyJS
throw new Error(`Erreur HTTP: ${response.status}`);
}
const blob = await response.blob();
const filename = data.name || data.url.split('/').pop();
const format = filename.split('.').pop().toLowerCase();
if (['png', 'jpg', 'jpeg', 'svg'].includes(format)) {
const resizedBlob = await resizeImageWithOptimalBackground(blob);
const pngFilename = filename.replace(/\.[^/.]+$/, ".png");
zip.file(pngFilename, resizedBlob);
return {success: true, pngFilename};
} else {
zip.file(filename, blob);
return {success: true, filename};
}
} catch (error) {
console.error(`Impossible d'ajouter ${data.url} au ZIP:`, error);
return {success: false, filename: data.name || data.url.split('/').pop()};
}
};
// Télécharger toutes les ressources et mettre à jour la progression
await Promise.all(
resourceList.map(async (data) => {
const result = await addResourceToZip(data);
completed++;
const progress = Math.round((completed / resourceList.length) * 100);
progressBar.style.width = `${progress}%`;
progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`;
return result;
})
);
// Générer le ZIP et déclencher le téléchargement
const zipBlob = await zip.generateAsync({type: 'blob'});
const zipUrl = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = zipUrl;
a.download = 'ressources.zip';
a.click();
URL.revokeObjectURL(zipUrl);
// Fermer la modale
modal.hide();
progressText.textContent = "Téléchargement terminé !";
}
function Menu({menuActions, compUuid}) {
const e = document.getElementById("actionMenu")
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
for (const x of tto)
x.dispose();
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const longTimeAction = (button) => {
if (button === "obs") {
obsModal.current.click();
}
}
const longPressDown = (button) => {
longPress.current.button = button;
longPress.current.time = new Date();
longPress.current.timer = setTimeout(() => {
longTimeAction(button);
longPress.current.time = null;
longPress.current.button = null;
}, 1000);
}
const longPressUp = (button) => {
clearTimeout(longPress.current.timer);
if (longPress.current.time) {
const diff = new Date() - longPress.current.time;
if (longPress.current.button === button) {
if (diff >= 1000) {
longTimeAction(button);
} else {
if (button === "obs") {
downloadResourcesAsZip(menuActions.current.resourceList || [])
.then(__ => console.log("Ressources téléchargées"));
}
}
}
longPress.current.time = null;
longPress.current.button = null;
}
}
const handleOBSSubmit = (e) => {
e.preventDefault();
const form = e.target;
const adresse = form[0].value;
const password = form[1].value;
const assets_dir = form[2].value;
exportOBSConfiguration(adresse, password, assets_dir)
}
const copyScriptToClipboard = () => {
navigator.clipboard.writeText(`<div id='safca_api_data'></div>
<script id="safca_api_script" type="text/javascript" src="${vite_url}/competition.js?id=${compUuid}"></script>`
).then(() => {
toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.");
}).catch(err => {
toast.error("Erreur lors de la copie dans le presse-papier : " + err);
});
}
if (!e)
return <></>;
return <>
{createPortal(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/>
<FontAwesomeIcon icon={faGlobe} size="xl"
style={{color: "#6c757d", cursor: "pointer"}}
onClick={() => copyScriptToClipboard()}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Copier le scripte d'intégration"/>
</>, document.getElementById("actionMenu"))}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal
</button>
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Configuration OBS</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onSubmit={handleOBSSubmit}>
<div className="modal-body">
<strong>/!\ Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en
changer entre chaque compétition</strong>
<div className="input-group mb-3">
<span className="input-group-text">Adresse du serveur</span>
<span className="input-group-text">ws://</span>
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span>
</div>
<div className="input-group mb-3">
<span className="input-group-text">Mot de passe du serveur</span>
<input type="password" className="form-control" placeholder="12345" aria-label=""
defaultValue={""}/>
</div>
<div className="input-group mb-3">
<span className="input-group-text">Dossier des resources</span>
<input type="text" className="form-control" placeholder="" aria-label="" required/>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button>
</div>
</form>
</div>
</div>
</div>
<div className="modal fade" id="progressModal" tabIndex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Téléchargement en cours...</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="progress">
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
</div>
<div id="progressText" className="mt-2">Préparation...</div>
</div>
</div>
</div>
</div>
</>
}
@@ -60,15 +321,34 @@ function CategoryHeader({cat, setCatId}) {
data
])
}
const sendAddCategory = ({data}) => {
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
setCatId(catId => {
if (catId === data) return null;
return catId;
})
setCats([...cats.filter(c => c.id !== data)])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
return () => {
dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'removeListener', payload: sendAddCategory})
dispatch({type: 'removeListener', payload: sendDelCategory})
}
}, [cats]);
useEffect(() => {
if (cats && cats.length > 0 && !cat) {
if (cats && cats.length > 0 && (!cat || (cats && !cats.find(c => c.id === cat.id)))) {
setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id);
} else if (cats && cats.length === 0) {
setModal({});
bthRef.current.click();
}
}, [cats]);
}, [cats, cat]);
const handleCatChange = (e) => {
const selectedCatId = e.target.value;
@@ -77,7 +357,6 @@ function CategoryHeader({cat, setCatId}) {
} else { // New category
setModal({});
bthRef.current.click();
console.log(cat);
e.target.value = cat?.id;
}
}
@@ -86,7 +365,7 @@ function CategoryHeader({cat, setCatId}) {
<div className="col-auto">
<div className="input-group">
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
<select className="form-select" onChange={handleCatChange}>
<select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))}
{cats && <option value={-1}>Nouvelle...</option>}
@@ -96,7 +375,7 @@ function CategoryHeader({cat, setCatId}) {
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
{cat &&
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} |
Lice: {cat.liceName}</div>}
Zone: {cat.liceName}</div>}
</div>
<div className="col-auto">
<button className="btn btn-primary float-end" onClick={() => {
@@ -126,7 +405,7 @@ function CategoryHeader({cat, setCatId}) {
function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const [name, setName] = useState("")
const [lice, setLice] = useState("1")
const [lice, setLice] = useState("A")
const [poule, setPoule] = useState(true)
const [tournoi, setTournoi] = useState(false)
const [size, setSize] = useState(4)
@@ -136,7 +415,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
useEffect(() => {
setName(state.name || "");
setLice(state.liceName || "1");
setLice(state.liceName || "A");
setPoule(((state.type || 1) & 1) !== 0);
setTournoi((state.type & 2) !== 0);
@@ -162,7 +441,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const regex = /^([^;]+;)*[^;]+$/;
if (regex.test(lice.trim()) === false) {
toast.error("Le format du nom des lices est invalide. Veuillez séparer les noms par des ';'.");
toast.error("Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.");
return;
}
@@ -268,8 +547,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
error: 'Erreur lors de la création de la catégorie'
}
).then(id => {
setCatId(id);
if (tournoi) {
const trees = build_tree(size, loserMatch)
console.log("Creating trees for new category:", trees);
@@ -280,7 +557,9 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
success: 'Arbres créés !',
error: 'Erreur lors de la création des arbres'
}
)
).finally(() => setCatId(id))
} else {
setCatId(id);
}
})
}
@@ -297,7 +576,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
return <form onSubmit={handleSubmit}>
<div className="modal-header">
<h1 className="modal-title fs-5" id="CategorieModalLabel">Ajouter une catégorie</h1>
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
@@ -309,8 +588,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
</div>
<div className="mb-3">
<label htmlFor="liceInput1" className="form-label">Nom des lices <small>(séparée par des ';')</small></label>
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="lice" value={lice}
<label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label>
<input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice}
onChange={e => setLice(e.target.value)}/>
</div>
@@ -371,6 +650,22 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
setConfirm({
title: "Suppression de la catégorie",
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`,
confirm: () => {
toast.promise(sendRequest('deleteCategory', state.id),
{
pending: 'Suppression de la catégorie...',
success: 'Catégorie supprimée !',
error: 'Erreur lors de la suppression de la catégorie'
}
).then(() => setCatId(null));
}
})
confirmRef.current.click();
}}>Supprimer</button>}
</div>
</form>
}

View File

@@ -9,7 +9,7 @@ export function ChronoPanel() {
})
const [chrono, setChrono] = useState({time: 0, startTime: 0})
const chronoText = useRef(null)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"})
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch();
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
@@ -34,12 +34,12 @@ export function ChronoPanel() {
const timer = setInterval(() => {
let currentDuration = config.time
let color = "black"
let color = "#000000"
if (state_.chronoState === 1) {
color = (state_.countBlink < blinkRfDuration) ? "black" : "red"
color = (state_.countBlink < blinkRfDuration) ? "#000000" : "#ff0000"
} else if (state_.chronoState === 2) {
currentDuration = (state_.chronoState === 0) ? 10000 : config.pause
color = (state_.countBlink < blinkRfDuration) ? "green" : "red"
color = (state_.countBlink < blinkRfDuration) ? "#008000" : "#ff0000"
}
const timeStr = timePrint(currentDuration - getTime())

View File

@@ -11,6 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {toast} from "react-toastify";
import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx";
function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@@ -22,17 +23,36 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
const {connected, setText} = useOBS();
useEffect(() => {
const categoryListener = ({data}) => {
setCats([...cats.filter(c => c.id !== data.id), data])
}
const sendAddCategory = ({data}) => {
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
if (catId === data)
setCatId(-1);
setCats([...cats.filter(c => c.id !== data)])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
return () => {
dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'removeListener', payload: sendAddCategory})
dispatch({type: 'removeListener', payload: sendDelCategory})
}
}, [cats]);
const cat = cats?.find(c => c.id === catId);
useEffect(() => {
setText("poule",cat ? cat.name : "");
}, [cat, connected]);
return <>
<div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
@@ -67,7 +87,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
setLoading(1);
sendRequest('getFullCategory', catId)
.then((data) => {
setTrees(data.trees.map(d => from_sendTree(d, true)))
setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
@@ -81,7 +101,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId)
return
setTrees(data.map(d => from_sendTree(d, true)))
setTrees(data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
@@ -131,10 +151,15 @@ function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1);
useEffect(() => {
if (!cat)
return;
if ((cat.type & type) === 0)
setType(cat.type);
}, [cat]);
if (!cat)
return <></>;
return <div style={{marginTop: "1em"}}>
{cat && cat.type === 3 && <>
<ul className="nav nav-tabs">
@@ -173,6 +198,10 @@ function MatchList({matches, cat, menuActions}) {
.map(m => ({...m, win: win(m.scores)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
}
const match = matches.find(m => m.id === activeMatch)
useEffect(() => {
if (!match) {
@@ -183,7 +212,7 @@ function MatchList({matches, cat, menuActions}) {
payload: {
c1: match.c1,
c2: match.c2,
next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({
next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({
c1: m.c1,
c2: m.c2
}))
@@ -198,7 +227,7 @@ function MatchList({matches, cat, menuActions}) {
useEffect(() => {
if (match && match.poule !== lice)
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id)
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
}, [lice]);
useEffect(() => {
@@ -207,12 +236,12 @@ function MatchList({matches, cat, menuActions}) {
if (marches2.some(m => m.id === activeMatch))
return;
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id);
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}, [matches])
return <>
{liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">Lice</label>
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">Zone de combat</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value);
@@ -224,11 +253,11 @@ function MatchList({matches, cat, menuActions}) {
</div>
}
<div className="table-responsive-xxl">
<div className="table-responsive-xxl overflow-y-auto" style={{maxHeight: "50vh"}}>
<table className="table table-striped table-hover">
<thead>
<tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
<th style={{textAlign: "center"}} scope="col"></th>
@@ -240,7 +269,7 @@ function MatchList({matches, cat, menuActions}) {
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<tr key={m.id}
className={m.id === activeMatch ? "table-info" : (liceName[(index - firstIndex) % liceName.length] === lice ? "" : "table-warning")}
className={m.id === activeMatch ? "table-info" : (isActiveMatch(index) ? "" : "table-warning")}
onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>
@@ -259,7 +288,7 @@ function MatchList({matches, cat, menuActions}) {
</table>
</div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match} menuActions={menuActions}/></LoadingProvider>}
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</>
}
@@ -325,27 +354,29 @@ function BuildTree({treeData, matches, menuActions}) {
return <div>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
<div className="overflow-y-auto" style={{maxHeight: "50vh"}}>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
</div>
</div>
{currentMatch?.matchSelect &&
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match} menuActions={menuActions}/></LoadingProvider>}
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</div>
}
function ScorePanel({matchId, match, menuActions}) {
function ScorePanel({matchId, matchs, match, menuActions}) {
const onClickVoid = useRef(() => {
});
return <div className="row" onClick={onClickVoid.current}>
<ScorePanel_ matchId={matchId} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
<ScorePanel_ matchId={matchId} matchs={matchs} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
<CardPanel matchId={matchId} match={match}/>
</div>
}
function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher()
@@ -355,6 +386,11 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const tableRef = useRef(null)
const scoreRef = useRef([])
const lastScoreClick = useRef(null)
const scoreInRef = useRef(null)
useEffect(() => {
scoreInRef.current = scoreIn;
}, [scoreIn]);
useEffect(() => {
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
@@ -403,9 +439,12 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const {matchId, round, comb} = lastScoreClick.current;
lastScoreClick.current = null;
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
const scoreIn_ = String(scoreInRef.current).trim() === "" ? -1000 : Number(scoreInRef.current);
const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round);
console.log("Updating score", matchId, round, comb, scoreIn_, score);
const score = match.scores.find(s => s.n_round === round);
let newScore;
if (score) {
if (comb === 1)
@@ -474,6 +513,18 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
setEnd(match.end);
}, [match]);
useEffect(() => {
const handleClickOutside = (event) => {
if (inputRef.current && !inputRef.current.contains(event.target)) {
onClickVoid();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))

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