155 Commits

Author SHA1 Message Date
cad6d14ba8 Merge pull request 'fix: translate backend add log' (#100) from dev into master
Reviewed-on: #100
2026-01-16 12:59:33 +00:00
c456fdae8b fix: translate backend add log
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m48s
2026-01-16 13:59:12 +01:00
4bf07cc8f8 Merge pull request 'i18n' (#99) from i18n into master
Reviewed-on: #99
2026-01-16 12:18:53 +00:00
a8356c8663 feat: translate backend
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m23s
2026-01-15 16:32:43 +01:00
c7a2215103 feat: translation competition manager part 2026-01-15 14:08:57 +01:00
46f543abde feat: translation competition part 2026-01-14 18:24:26 +01:00
d8b6f1ad25 feat: translation result 2026-01-14 15:51:15 +01:00
9170157dbb feat: init translation 2026-01-13 21:31:05 +01:00
babbc5d585 Merge pull request 'feat: keep showLicenceState in sessionStorage' (#98) from dev into master
Reviewed-on: #98
2026-01-07 21:10:12 +00:00
51c774a21e feat: keep showLicenceState in sessionStorage
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m52s
2026-01-07 22:09:41 +01:00
9d85b9ae45 Merge pull request 'feat: support ha partial config' (#97) from dev into master
Reviewed-on: #97
2026-01-07 15:35:10 +00:00
0ab0731ebd feat: support ha partial config
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m52s
2026-01-07 16:34:57 +01:00
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
159 changed files with 8903 additions and 2722 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,52 @@
package fr.titionfire.ffsaf;
import fr.titionfire.ffsaf.domain.service.TradService;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.ext.Provider;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@Provider
@PreMatching
public class UserInfoProvider implements ContainerRequestFilter {
private static final List<Locale> SUPPORTED_LANGUAGES = Arrays.asList(
Locale.FRENCH,
Locale.ENGLISH
);
@Override
public void filter(ContainerRequestContext requestContext) {
System.out.println(requestContext.getHeaders());
List<Locale> acceptableLanguages = requestContext.getAcceptableLanguages();
System.out.println(acceptableLanguages);
Locale selectedLocale = findFirstSupportedLanguage(acceptableLanguages);
if (selectedLocale == null)
selectedLocale = TradService.fallbackLocale;
System.out.println(selectedLocale);
requestContext.setProperty("userLocale", selectedLocale);
}
private Locale findFirstSupportedLanguage(List<Locale> acceptableLanguages) {
for (Locale acceptableLanguage : acceptableLanguages) {
// Vérifie si la langue est dans la liste des langues supportées
if (SUPPORTED_LANGUAGES.contains(acceptableLanguage)) {
return acceptableLanguage;
}
// Vérifie aussi par tag de langue (ex: "fr-FR" -> "fr")
String languageTag = acceptableLanguage.getLanguage();
for (Locale supportedLanguage : SUPPORTED_LANGUAGES) {
if (supportedLanguage.getLanguage().equals(languageTag)) {
return supportedLanguage;
}
}
}
return null;
}
}

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;
@@ -76,6 +79,9 @@ public class AffiliationService {
@ConfigProperty(name = "notif.affRequest.mail")
List<String> mails;
@Inject
TradService trad;
public Uni<List<AffiliationRequestModel>> getAllReq() {
return repositoryRequest.listAll();
}
@@ -89,7 +95,7 @@ public class AffiliationService {
return Uni.createFrom().item(affModel)
.invoke(Unchecked.consumer(model -> {
if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) {
throw new DBadRequestException("Saison non valid");
throw new DBadRequestException(trad.t("saison.non.valid"));
}
}))
.chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna(
@@ -107,7 +113,7 @@ public class AffiliationService {
out, affModel.getSaison()))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0 && unique) {
throw new DBadRequestException("Demande d'affiliation déjà existante");
throw new DBadRequestException(trad.t("demande.d.affiliation.deja.existante"));
}
}))
)
@@ -115,28 +121,28 @@ public class AffiliationService {
repository.count("club = ?1 and saison = ?2", club, affModel.getSaison())))
.onItem().invoke(Unchecked.consumer(count -> {
if (count != 0) {
throw new DBadRequestException("Affiliation déjà existante");
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
}
}))
.map(o -> affModel)
.call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence",
model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException("Licence membre n°1 inconnue");
throw new DBadRequestException(trad.t("licence.membre.n.1.inconnue"));
}
})) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence",
model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException("Licence membre n°2 inconnue");
throw new DBadRequestException(trad.t("licence.membre.n.2.inconnue"));
}
})) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence",
model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> {
if (count == 0) {
throw new DBadRequestException("Licence membre n°3 inconnue");
throw new DBadRequestException(trad.t("licence.membre.n.3.inconnue"));
}
})) : Uni.createFrom().nullItem())
);
@@ -145,7 +151,7 @@ public class AffiliationService {
public Uni<?> saveEdit(AffiliationRequestForm form) {
return pre_save(form, false)
.chain(model -> repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.chain(origine -> {
origine.setName(model.getName());
origine.setAddress(model.getAddress());
@@ -174,15 +180,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",
@@ -201,7 +204,7 @@ public class AffiliationService {
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.map(model -> {
model.setName(form.getName());
model.setState_id(form.getState_id());
@@ -245,11 +248,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");
}
@@ -309,7 +309,7 @@ public class AffiliationService {
LOGGER.debug(form.toString());
return repositoryRequest.findById(form.getId())
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.chain(req ->
clubRepository.find("StateId = ?1", form.getState_id()).firstResult()
.chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model))
@@ -319,13 +319,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,10 +353,16 @@ public class AffiliationService {
.map(c -> club));
})
.call(club -> reactiveMailer.send(
Mail.withText(form.getM1_email(),
"FFSAF - Acceptation de votre demande d'affiliation",
String.format(
"""
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.
@@ -366,8 +370,8 @@ public class AffiliationService {
Cordialement,
L'équipe de la FFSAF
""", club.getName(), club.getNo_affiliation())
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
""", club.getName(), club.getNo_affiliation()))
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
.addTo(form.getM2_email(), form.getM3_email())
));
}
@@ -397,7 +401,7 @@ public class AffiliationService {
public Uni<SimpleReqAffiliation> getRequest(long id) {
return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel)
.onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve")))
.call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> {
if (c != null) {
out.setClub(c.getId());
@@ -421,7 +425,7 @@ public class AffiliationService {
public Uni<List<SimpleAffiliation>> getAffiliation(long id) {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
.call(model -> Mutiny.fetch(model.getAffiliations()))
.chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId())
.map(reqs -> reqs.stream().map(req ->
@@ -433,11 +437,11 @@ public class AffiliationService {
public Uni<SimpleAffiliation> setAffiliation(long id, int saison) {
return clubRepository.findById(id)
.onItem().ifNull().failWith(new DNotFoundException("Club non trouvé"))
.onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve")))
.call(model -> Mutiny.fetch(model.getAffiliations()))
.invoke(Unchecked.consumer(club -> {
if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) {
throw new DBadRequestException("Affiliation déjà existante");
throw new DBadRequestException(trad.t("affiliation.deja.existante"));
}
}))
.chain(club ->
@@ -458,10 +462,16 @@ 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(
"""
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):
@@ -471,8 +481,8 @@ public class AffiliationService {
Cordialement,
L'équipe de la FFSAF
""", aff.getName(), reason)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
""", 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

@@ -45,10 +45,13 @@ public class CategoryService {
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
TradService trad;
public Uni<CategoryData> getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system)
.firstResult()
.onItem().ifNull().failWith(() -> new RuntimeException("Category not found"))
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
.call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet()))
.map(CategoryData::fromModel);
}
@@ -64,7 +67,7 @@ public class CategoryService {
.chain(o -> {
if (o == null) {
return competRepository.findById(data.getCompet())
.onItem().ifNull().failWith(() -> new RuntimeException("Competition not found"))
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
.call(o2 -> permService.hasEditPerm(securityCtx, o2))
.chain(competitionModel -> {
CategoryModel model = new CategoryModel();
@@ -139,7 +142,7 @@ public class CategoryService {
.onItem().ifNotNull().call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet()))
.onItem().ifNull().switchTo(
() -> competRepository.findById(data.getCompet())
.onItem().ifNull().failWith(() -> new RuntimeException("Compet not found"))
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found")))
.call(o -> permService.hasEditPerm(securityCtx, o))
.map(o -> {
CategoryModel model = new CategoryModel();
@@ -256,7 +259,7 @@ public class CategoryService {
public Uni<?> delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) {
return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult()
.onItem().ifNull().failWith(() -> new RuntimeException("Category not found"))
.onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver")))
.call(o -> permService.hasEditPerm(securityCtx, o.getCompet()))
.call(o -> Mutiny.fetch(o.getTree())
.call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() :

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;
@@ -59,6 +63,9 @@ public class ClubService {
@Inject
LoggerService ls;
@Inject
TradService trad;
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(
() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
@@ -126,7 +133,7 @@ public class ClubService {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException("Club non trouvé");
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.map(MembreModel::getClub)
.call(club -> Mutiny.fetch(club.getContact()));
@@ -146,7 +153,7 @@ public class ClubService {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException("Club non trouvé");
throw new DNotFoundException(trad.t("club.non.trouve"));
if (!securityCtx.isInClubGroup(m.getClub().getId()))
throw new DForbiddenException();
}))
@@ -162,7 +169,7 @@ public class ClubService {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException("Club non trouvé");
throw new DNotFoundException(trad.t("club.non.trouve"));
if (!securityCtx.isInClubGroup(m.getClub().getId()))
throw new DForbiddenException();
}))
@@ -179,7 +186,7 @@ public class ClubService {
ls.logUpdate("Contact(s)...", club);
club.setContact(MAPPER.readValue(form.getContact(), typeRef));
} catch (JsonProcessingException e) {
throw new DBadRequestException("Erreur de format des contacts");
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
}
ls.logChange("Lieux d'entrainements", club.getTraining_location(), form.getTraining_location(),
@@ -229,12 +236,13 @@ public class ClubService {
ls.logUpdate("Contact(s)...", m);
m.setContact(MAPPER.readValue(input.getContact(), typeRef));
} catch (JsonProcessingException e) {
throw new DBadRequestException("Erreur de format des contacts");
throw new DBadRequestException(trad.t("erreur.de.format.des.contacts"));
}
}
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 +290,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 +333,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

@@ -97,8 +97,17 @@ public class CompetitionService {
@CacheName("have-access")
Cache cacheNoneAccess;
@Inject
TradService trad;
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 +115,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 +139,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 +183,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());
}
@@ -176,7 +195,7 @@ public class CompetitionService {
.invoke(Unchecked.consumer(combModel -> {
if (!securityCtx.getRoles().contains("create_compet") && !securityCtx.getRoles()
.contains("federation_admin"))
throw new DForbiddenException("Vous ne pouvez pas créer de compétition");
throw new DForbiddenException(trad.t("vous.ne.pouvez.pas.creer.de.competition"));
}))
.map(MembreModel::getClub)
.chain(clubModel -> {
@@ -207,7 +226,8 @@ public class CompetitionService {
keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null))
.invoke(Unchecked.consumer(newOwner -> {
if (newOwner == null)
throw new DBadRequestException("User " + data.getOwner() + " not found");
throw new DBadRequestException(
String.format(trad.t("user.not.found"), data.getOwner()));
if (!newOwner.equals(model.getOwner())) {
if (!securityCtx.roleHas("federation_admin")
&& !securityCtx.roleHas("safca_super_admin")
@@ -281,7 +301,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()))
@@ -326,7 +346,7 @@ public class CompetitionService {
|| !securityCtx.isClubAdmin())
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException("Inscription fermée");
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
@@ -334,8 +354,7 @@ public class CompetitionService {
if (!securityCtx.isInClubGroup(model.getClub().getId()))
throw new DForbiddenException();
if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(
"Vous n'avez pas le droit d'inscrire ce membre (par décision de l'administrateur de la compétition)");
throw new DForbiddenException(trad.t("insc.err1"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false)))
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()));
@@ -345,13 +364,12 @@ public class CompetitionService {
if (cm.getRegisterMode() != RegisterMode.FREE)
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException("Inscription fermée");
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.chain(c -> membreService.getByAccountId(securityCtx.getSubject())
.invoke(Unchecked.consumer(model -> {
if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(
"Vous n'avez pas le droit de vous inscrire (par décision de l'administrateur de la compétition)");
throw new DForbiddenException(trad.t("insc.err2"));
}))
.chain(combModel -> updateRegister(data, c, combModel, false)))
.map(r -> SimpleRegisterComb.fromModel(r, List.of()));
@@ -364,8 +382,7 @@ public class CompetitionService {
.map(Unchecked.function(r -> {
if (r != null) {
if (!admin && r.isLockEdit())
throw new DForbiddenException(
"Modification bloquée par l'administrateur de la compétition");
throw new DForbiddenException(trad.t("insc.err3"));
r.setWeight(data.getWeight());
r.setOverCategory(data.getOverCategory());
r.setCategorie(
@@ -408,17 +425,17 @@ public class CompetitionService {
return combRepository.find("licence = ?1", licence).firstResult()
.invoke(Unchecked.consumer(combModel -> {
if (combModel == null)
throw new DForbiddenException("Licence " + licence + " non trouvé");
throw new DForbiddenException(String.format(trad.t("licence.non.trouve"), licence));
}));
} else {
if (fname == null || lname == null)
return Uni.createFrom().failure(new DBadRequestException("Nom et prénom requis"));
return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
return combRepository.find("unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2)",
lname,
fname).firstResult()
.invoke(Unchecked.consumer(combModel -> {
if (combModel == null)
throw new DForbiddenException("Combattant " + fname + " " + lname + " non trouvé");
throw new DForbiddenException(String.format(trad.t("combattant.non.trouve"), fname, lname));
}));
}
}
@@ -445,12 +462,12 @@ public class CompetitionService {
|| !securityCtx.isClubAdmin())
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException("Inscription fermée");
throw new DBadRequestException(trad.t("inscription.fermee"));
}))
.call(cm -> membreService.getById(combId)
.invoke(Unchecked.consumer(model -> {
if (model == null)
throw new DNotFoundException("Membre " + combId + " n'existe pas");
throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId));
if (!securityCtx.isInClubGroup(model.getClub().getId()))
throw new DForbiddenException();
})))
@@ -462,7 +479,7 @@ public class CompetitionService {
if (cm.getRegisterMode() != RegisterMode.FREE || !Objects.equals(model.getId(), combId))
throw new DForbiddenException();
if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister()))
throw new DBadRequestException("Inscription fermée");
throw new DBadRequestException(trad.t("inscription.fermee"));
})))
.chain(c -> deleteRegister(combId, c, false));
}
@@ -470,7 +487,7 @@ public class CompetitionService {
private Uni<Void> deleteRegister(Long combId, CompetitionModel c, boolean admin) {
if (admin && combId < 0) {
return competitionGuestRepository.find("competition = ?1 AND id = ?2", c, combId * -1).firstResult()
.onFailure().transform(t -> new DBadRequestException("Combattant non inscrit"))
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
.call(Unchecked.function(
model -> Panache.withTransaction(() -> competitionGuestRepository.delete(model))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
@@ -479,10 +496,10 @@ public class CompetitionService {
.replaceWithVoid();
}
return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult()
.onFailure().transform(t -> new DBadRequestException("Combattant non inscrit"))
.onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit")))
.call(Unchecked.function(registerModel -> {
if (!admin && registerModel.isLockEdit())
throw new DForbiddenException("Modification bloquée par l'administrateur de la compétition");
throw new DForbiddenException(trad.t("insc.err3"));
return Panache.withTransaction(() -> registerRepository.delete(registerModel))
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom().voidItem());
@@ -513,6 +530,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(trad.t("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(trad.t("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(String.format(trad.t("user.not.found"), username));
admin.add(opt.get().getId());
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
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 -> {
@@ -549,13 +624,13 @@ public class CompetitionService {
for (String username : data.getAdmin()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
admin.add(UUID.fromString(opt.get().getId()));
}
for (String username : data.getTable()) {
Optional<UserRepresentation> opt = keycloakService.getUser(username);
if (opt.isEmpty())
throw new DBadRequestException("User " + username + " not found");
throw new DBadRequestException(String.format(trad.t("user.not.found"), username));
table.add(UUID.fromString(opt.get().getId()));
}

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,11 @@ 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() || clientId.equalsIgnoreCase(
"null") || clientSecret.equalsIgnoreCase("null")) {
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,10 +156,18 @@ 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(
"""
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,
@@ -172,8 +180,8 @@ public class KeycloakService {
Cordialement,
L'équipe de la FFSAF
""", oldEmail, email)
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
""", 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",
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

@@ -47,6 +47,9 @@ public class LicenceService {
@Inject
CheckoutService checkoutService;
@Inject
TradService trad;
public Uni<List<LicenceModel>> getLicence(long id, Consumer<MembreModel> checkPerm) {
return combRepository.findById(id).invoke(checkPerm)
.chain(combRepository -> Mutiny.fetch(combRepository.getLicences()));
@@ -84,6 +87,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 +144,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))
@@ -137,7 +160,7 @@ public class LicenceService {
return repository.list("membre.id IN ?1 AND saison = ?2 AND pay = FALSE", ids, Utils.getSaison())
.invoke(Unchecked.consumer(models -> {
if (models.size() != ids.size())
throw new DBadRequestException("Erreur lors de lalection des membres");
throw new DBadRequestException(trad.t("erreur.lors.de.la.selection.des.membres"));
}))
.call(models -> {
Uni<?> uni = Uni.createFrom().nullItem();
@@ -154,10 +177,10 @@ public class LicenceService {
.call(__ -> checkoutService.canDeleteLicence(id)
.invoke(Unchecked.consumer(b -> {
if (!b) throw new DBadRequestException(
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
trad.t("licence.rm.err1"));
})))
.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) {
@@ -166,7 +189,7 @@ public class LicenceService {
return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count()
.invoke(Unchecked.consumer(count -> {
if (count > 0)
throw new DBadRequestException("Licence déjà demandée");
throw new DBadRequestException(trad.t("licence.deja.demandee"));
})).chain(__ -> combRepository.findById(id).chain(membreModel2 -> {
LicenceModel model = new LicenceModel();
model.setClub_id((membreModel2.getClub() == null) ? null : membreModel2.getClub().getId());
@@ -195,13 +218,13 @@ public class LicenceService {
.call(__ -> checkoutService.canDeleteLicence(id)
.invoke(Unchecked.consumer(b -> {
if (!b) throw new DBadRequestException(
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
trad.t("licence.rm.err1"));
})))
.invoke(Unchecked.consumer(licenceModel -> {
if (licenceModel.isValidate())
throw new DBadRequestException("Impossible de supprimer une licence déjà validée");
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.validee"));
if (licenceModel.isPay())
throw new DBadRequestException("Impossible de supprimer une licence déjà payée");
throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.payee"));
}))
.call(model -> ls.logADelete(model))
.chain(__ -> Panache.withTransaction(() -> repository.deleteById(id)));

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;
@@ -64,6 +62,9 @@ public class MembreService {
@Inject
LoggerService ls;
@Inject
TradService trad;
public SimpleCombModel find(int licence, String np) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
repository.find(
@@ -125,8 +126,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 +151,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 Uni.createFrom().failure(new DInternalError(trad.t("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))
@@ -215,7 +199,7 @@ public class MembreService {
.call(result -> query.count().invoke(result::setResult_count))
.call(result -> query.pageCount()
.invoke(Unchecked.consumer(pages -> {
if (page > pages) throw new DBadRequestException("Page out of range");
if (page > pages) throw new DBadRequestException(trad.t("page.out.of.range"));
}))
.invoke(result::setPage_count))
.call(result -> query.page(Page.of(page, limit)).list()
@@ -260,8 +244,8 @@ public class MembreService {
for (MembreModel membreModel : membres) {
if (!Objects.equals(membreModel.getClub(), clubModel.get())) {
LOGGER.info("Similar membres found: " + membreModel);
throw new DForbiddenException(
"Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club");
throw new DForbiddenException(String.format(trad.t("le.membre.appartient.pas.a.votre.club"),
membreModel.getLicence()));
}
}
Uni<Void> uniResult = Uni.createFrom().voidItem();
@@ -287,14 +271,18 @@ 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(
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
model.getFname(), model.getLname()));
}
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(
String.format(trad.t("email.deja.utilise.par"), model.getEmail(),
model.getFname(), model.getLname()));
}
}
@@ -305,7 +293,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.");
String.format(trad.t("try.edit.licence"), model.getLicence(), model.getFname(),
model.getFname()));
}
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
@@ -313,8 +302,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 +368,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(trad.t("email.deja.utilise"));
})))
.chain(membreModel -> clubRepository.findById(membre.getClub())
.map(club -> new Pair<>(membreModel, club)))
@@ -402,7 +390,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(trad.t("email.deja.utilise"));
})))
.invoke(Unchecked.consumer(membreModel -> {
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
@@ -411,7 +399,7 @@ public class MembreService {
membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity(
membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) {
throw new DBadRequestException(
"Pour enregistrer un nouveau membre, veuillez utilisez le bouton prévue a cette effet.");
trad.t("regiter.new.membre"));
}
}))
.invoke(Unchecked.consumer(membreModel -> {
@@ -420,7 +408,7 @@ public class MembreService {
else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE;
else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU;
if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level)
throw new DForbiddenException("Permission insuffisante");
throw new DForbiddenException(trad.t("permission.insuffisante"));
}))
.onItem().transform(target -> {
if (!securityCtx.getSubject().equals(target.getUserId())) {
@@ -434,7 +422,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 +470,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 +481,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(trad.t("email.deja.utilise"));
})))
.chain(clubModel -> {
MembreModel model = getMembreModel(input, clubModel);
@@ -508,7 +498,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(trad.t("email.deja.utilise"));
})))
.call(membreModel ->
repository.count(
@@ -516,7 +506,7 @@ public class MembreService {
input.getLname(), input.getFname(), membreModel.getClub())
.invoke(Unchecked.consumer(c -> {
if (c > 0)
throw new DBadRequestException("Membre déjà existent");
throw new DBadRequestException(trad.t("membre.deja.existent"));
})))
.chain(membreModel -> {
MembreModel model = getMembreModel(input, membreModel.getClub());
@@ -549,13 +539,13 @@ public class MembreService {
.invoke(Unchecked.consumer(membreModel -> {
if (membreModel.getLicence() != null) {
throw new DBadRequestException(
"Impossible de supprimer un membre qui a déjà un numéro de licence");
trad.t("membre.rm.err1"));
}
}))
.call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count()
.invoke(Unchecked.consumer(l -> {
if (l > 0)
throw new DBadRequestException("Impossible de supprimer un membre avec des licences");
throw new DBadRequestException(trad.t("membre.rm.err2"));
})))
.call(membreModel -> (membreModel.getUserId() != null) ?
keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem())
@@ -577,8 +567,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());
@@ -601,13 +591,30 @@ public class MembreService {
public Uni<MeData> getMembre(String subject) {
MeData meData = new MeData();
return repository.find("userId = ?1", subject).firstResult()
.invoke(meData::setMembre)
.chain(membreModel -> Mutiny.fetch(membreModel.getLicences()))
.invoke(m -> meData.setMembre(m, trad))
.call(membreModel -> Mutiny.fetch(membreModel.getLicences())
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
.invoke(meData::setLicences)
.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 +624,4 @@ public class MembreService {
}).toList()).andCollectFailures())
.map(__ -> null);
}
}

View File

@@ -46,6 +46,9 @@ public class PDFService {
@ConfigProperty(name = "pdf-maker.sign-file")
String sign_file;
@Inject
TradService trad;
public Uni<Response> getLicencePdf(String subject) {
return getLicencePdf(combRepository.find("userId = ?1", subject).firstResult()
@@ -58,7 +61,7 @@ public class PDFService {
LicenceModel licence = m.getLicences().stream()
.filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate())
.findFirst()
.orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours"));
.orElseThrow(() -> new DNotFoundException(trad.t("pas.de.licence.pour.la.saison.en.cours")));
try {
byte[] buff = make_pdf(m, licence);
@@ -119,7 +122,7 @@ public class PDFService {
combRepository.find("userId = ?1", subject).firstResult()
.invoke(Unchecked.consumer(m -> {
if (m == null || m.getClub() == null)
throw new DNotFoundException("Club non trouvé");
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.map(MembreModel::getClub)
.call(m -> Mutiny.fetch(m.getAffiliations())));
@@ -130,7 +133,7 @@ public class PDFService {
clubRepository.findById(id)
.invoke(Unchecked.consumer(m -> {
if (m == null)
throw new DNotFoundException("Club non trouvé");
throw new DNotFoundException(trad.t("club.non.trouve"));
}))
.call(m -> Mutiny.fetch(m.getAffiliations())));
}
@@ -141,7 +144,7 @@ public class PDFService {
.map(Unchecked.function(m -> {
if (m.getAffiliations().stream()
.noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison()))
throw new DNotFoundException("Pas d'affiliation pour la saison en cours");
throw new DNotFoundException(trad.t("pas.d.affiliation.pour.la.saison.en.cours"));
try {
byte[] buff = make_pdf(m);

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,60 +33,125 @@ public class ResultService {
@Inject
MembreService membreService;
@Inject
ClubRepository clubRepository;
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CategoryRepository categoryRepository;
@Inject
MatchRepository matchRepository;
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");
@Inject
TradService trad;
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);
public Uni<HashMap<String, Long>> getCategoryList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid));
}
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) {
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;
});
}
private ResultCategoryData getData(List<MatchModel> matchModels) {
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 +165,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 +199,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,23 +224,27 @@ 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 -> {
treeModels.stream()
.filter(t -> t.getLevel() != 0)
.sorted(Comparator.comparing(TreeModel::getLevel))
.forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
convertTree(treeModel, root);
convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS);
trees.add(root);
});
out.setTrees(trees);
@@ -185,7 +252,15 @@ public class ResultService {
public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx)
.chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid)
.chain(membreModel -> getAllCombArray_(uuid, membreModel));
}
public Uni<CombsArrayData> getAllCombArrayPublic(String uuid) {
return getAllCombArray_(uuid, null);
}
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 -> {
@@ -195,57 +270,42 @@ public class ResultService {
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
List<CombsArrayData.CombsData> combs = matchModels.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()))
.filter(Objects::nonNull)
.distinct()
.map(combName -> {
.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();
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();
}
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());
}
});
});
makeStat(matchModels, comb, w, l, pointMake, pointTake);
Categorie categorie = null;
ClubModel club = null;
String clubName = null;
Optional<RegisterModel> register = registers.stream()
.filter(r -> r.getName().equals(combName)).findFirst();
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
if (register.isPresent()) {
categorie = register.get().getCategorie();
club = register.get().getClub2();
categorie = register.get().getCategorie2();
ClubModel club = register.get().getClub2();
clubName = (club == null) ? trad.t("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) ? trad.t("no.licence")
: model.getClub().getName();
}
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
builder2.name(combName);
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
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((club == null) ? BUNDLE.getString("no.licence") : club.getName());
builder2.club(clubName);
builder2.pointMake(pointMake.get());
builder2.pointTake(pointTake.get());
builder2.ratioPoint(
@@ -262,10 +322,161 @@ public class ResultService {
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(trad.t("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(trad.t("combattant.non.inscrit"));
builder.name(Utils.getFullName(registerModel.getMembre()));
builder.club((registerModel.getClub2() == null) ? trad.t("no.licence") :
registerModel.getClub2().getName());
builder.cat((registerModel.getCategorie2() == null) ? "---" :
registerModel.getCategorie2().getName(trad));
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(trad));
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,74 +488,111 @@ 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())
public Uni<HashMap<String, Long>> getClubList(String uuid, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid));
}
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(trad.t("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(trad.t("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 -> new Pair<>(registers, matchModels)))
.map(pair -> {
List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue();
.map(matchModels ->
getClubArray2(clubModel.getName(),
registers.stream().map(o -> (CombModel) o.getMembre()).toList(),
matchModels, registers, membreModel))));
}
}
builder.nb_insc(registers.size());
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 = registers.stream()
.map(register -> {
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();
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();
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();
}
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.cat((categorie == null) ? "---" : categorie.getName(trad));
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());
builder2.ratioPoint((pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
tt_win.addAndGet(w.get());
tt_match.addAndGet(w.get() + l.get());
@@ -365,8 +613,32 @@ public class ResultService {
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
@@ -380,21 +652,36 @@ 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(
trad.t("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(trad.t("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,41 @@
package fr.titionfire.ffsaf.domain.service;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
@RequestScoped
public class TradService {
@Inject
Instance<ContainerRequestContext> requestContextInstance;
public static final Locale fallbackLocale = Locale.FRANCE;
public String t(String key) {
return translate(key);
}
public String translate(String key) {
ContainerRequestContext requestContext = requestContextInstance.get();
Locale userLocale = (Locale) requestContext.getProperty("userLocale");
try {
ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale);
return messages.getString(key);
} catch (MissingResourceException e) {
try {
ResourceBundle fallbackMessages = ResourceBundle.getBundle("lang.messages", fallbackLocale);
return fallbackMessages.getString(key);
} catch (MissingResourceException ex) {
return key;
}
}
}
}

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

@@ -1,10 +1,12 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.client.StateIdService;
import fr.titionfire.ffsaf.rest.data.AssoData;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -19,6 +21,9 @@ public class AssoEndpoints {
@RestClient
SirenService sirenService;
@Inject
TradService trad;
@GET
@Path("state_id/{stateId}")
@Produces(MediaType.APPLICATION_JSON)
@@ -28,9 +33,9 @@ public class AssoEndpoints {
stateId).chain(stateIdService::getAssoDataFromUnit)).onFailure().transform(throwable -> {
if (throwable instanceof WebApplicationException exception) {
if (exception.getResponse().getStatus() == 404)
return new DNotFoundException("Service momentanément indisponible");
return new DNotFoundException(trad.t("service.momentanement.indisponible"));
if (exception.getResponse().getStatus() == 400)
return new DNotFoundException("Asso introuvable");
return new DNotFoundException(trad.t("asso.introuvable"));
}
return throwable;
});

View File

@@ -3,10 +3,11 @@ 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.TradService;
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,9 +48,15 @@ public class ClubEndpoints {
@Inject
SecurityCtx securityCtx;
@Inject
VirusScannerService scannerService;
@ConfigProperty(name = "upload_dir")
String media;
@Inject
TradService trad;
Consumer<ClubModel> checkPerm = Unchecked.consumer(clubModel -> {
if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(clubModel.getId()))
throw new DForbiddenException();
@@ -81,7 +88,7 @@ public class ClubEndpoints {
@Operation(summary = "Renvoie les types de contacts pour les clubs", description = "Renvoie la liste des types de " +
"contacts possibles pour les clubs")
public Uni<HashMap<String, String>> getConcatType() {
return Uni.createFrom().item(Contact.toSite());
return Uni.createFrom().item(Contact.toSite(trad));
}
@GET
@@ -109,7 +116,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 +128,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(trad)));
}
@PUT
@@ -145,25 +151,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 +171,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 +210,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é")
@@ -242,12 +222,12 @@ public class ClubEndpoints {
})
public Uni<SimpleClub> getOfUser() {
return clubService.getOfUser(securityCtx).map(SimpleClub::fromModel)
.invoke(m -> m.setContactMap(Contact.toSite()));
.invoke(m -> m.setContactMap(Contact.toSite(trad)));
}
@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 +244,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 +276,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 +287,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 +325,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,8 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.utils.ResultPrivacy;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -15,45 +17,50 @@ 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) {
public void setMembre(MembreModel membreModel, TradService trad) {
this.id = membreModel.getId();
this.lname = membreModel.getLname();
this.fname = membreModel.getFname();
this.categorie = membreModel.getCategorie().getName();
this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName();
this.genre = membreModel.getGenre().str;
this.categorie = membreModel.getCategorie() == null ? trad.t("categorie.inconnue") : membreModel.getCategorie().getName(trad);
this.club = membreModel.getClub() == null ? trad.t("sans.club") : membreModel.getClub().getName();
this.genre = membreModel.getGenre().getString(trad);
this.licence = membreModel.getLicence();
this.country = membreModel.getCountry();
this.birth_date = membreModel.getBirth_date();
this.email = membreModel.getEmail();
this.role = membreModel.getRole().str;
this.grade_arbitrage = membreModel.getGrade_arbitrage().str;
this.role = membreModel.getRole().getString(trad);
this.grade_arbitrage = membreModel.getGrade_arbitrage().getString(trad);
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

@@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.domain.service.TradService;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.ResourceBundle;
@@ -19,7 +20,7 @@ public enum Categorie {
VETERAN2;
public String getName(ResourceBundle BUNDLE) {
return switch (this){
return switch (this) {
case SUPER_MINI -> BUNDLE.getString("Cat.SUPER_MINI");
case MINI_POUSSIN -> BUNDLE.getString("Cat.MINI_POUSSIN");
case POUSSIN -> BUNDLE.getString("Cat.POUSSIN");
@@ -34,8 +35,24 @@ public enum Categorie {
};
}
public String getName(TradService tradService) {
return switch (this) {
case SUPER_MINI -> tradService.translate("Cat.SUPER_MINI");
case MINI_POUSSIN -> tradService.translate("Cat.MINI_POUSSIN");
case POUSSIN -> tradService.translate("Cat.POUSSIN");
case BENJAMIN -> tradService.translate("Cat.BENJAMIN");
case MINIME -> tradService.translate("Cat.MINIME");
case CADET -> tradService.translate("Cat.CADET");
case JUNIOR -> tradService.translate("Cat.JUNIOR");
case SENIOR1 -> tradService.translate("Cat.SENIOR1");
case SENIOR2 -> tradService.translate("Cat.SENIOR2");
case VETERAN1 -> tradService.translate("Cat.VETERAN1");
case VETERAN2 -> tradService.translate("Cat.VETERAN2");
};
}
public String getName() {
return switch (this){
return switch (this) {
case SUPER_MINI -> "Super Mini";
case MINI_POUSSIN -> "Mini Poussin";
case POUSSIN -> "Poussin";

View File

@@ -1,32 +1,34 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.domain.service.TradService;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.HashMap;
@RegisterForReflection
public enum Contact {
COURRIEL("Courriel"),
TELEPHONE("Téléphone"),
SITE("Site web"),
FACEBOOK("Facebook"),
INSTAGRAM("Instagram"),
AUTRE("Autre");
COURRIEL,
TELEPHONE,
SITE,
FACEBOOK,
INSTAGRAM,
AUTRE;
public String name;
Contact(String name) {
this.name = name;
public String getName(TradService trad) {
return switch (this) {
case COURRIEL -> trad.translate("Contact.COURRIEL");
case TELEPHONE -> trad.translate("Contact.TELEPHONE");
case SITE -> trad.translate("Contact.SITE");
case FACEBOOK -> trad.translate("Contact.FACEBOOK");
case INSTAGRAM -> trad.translate("Contact.INSTAGRAM");
case AUTRE -> trad.translate("Contact.AUTRE");
};
}
public void setName(String name) {
this.name = name;
}
public static HashMap<String, String> toSite() {
public static HashMap<String, String> toSite(TradService trad) {
HashMap<String, String> map = new HashMap<>();
for (Contact contact : Contact.values()) {
map.put(contact.toString(), contact.name);
map.put(contact.toString(), contact.getName(trad));
}
return map;
}

View File

@@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.domain.service.TradService;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
@@ -27,8 +28,17 @@ public enum Genre {
}
}
public String getString(TradService trad) {
return switch (this) {
case H -> trad.translate("Genre.Homme");
case F -> trad.translate("Genre.Femme");
case NA -> trad.translate("Genre.NA");
};
}
@Override
public String toString() {
return str;
}
}

View File

@@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.domain.service.TradService;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
@@ -14,6 +15,14 @@ public enum GradeArbitrage {
this.str = name;
}
public String getString(TradService trad) {
return switch (this) {
case NA -> trad.translate("GradeArbitrage.NA");
case ASSESSEUR -> trad.translate("GradeArbitrage.ASSESSEUR");
case ARBITRE -> trad.translate("GradeArbitrage.ARBITRE");
};
}
@Override
public String toString() {
return str;

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,5 +1,6 @@
package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.domain.service.TradService;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
@@ -21,6 +22,19 @@ public enum RoleAsso {
this.level = level;
}
public String getString(TradService trad) {
return switch (this){
case MEMBRE -> trad.translate("RoleAsso.MEMBRE");
case PRESIDENT -> trad.translate("RoleAsso.PRESIDENT");
case VPRESIDENT -> trad.translate("RoleAsso.VPRESIDENT");
case SECRETAIRE -> trad.translate("RoleAsso.SECRETAIRE");
case VSECRETAIRE -> trad.translate("RoleAsso.VSECRETAIRE");
case TRESORIER -> trad.translate("RoleAsso.TRESORIER");
case VTRESORIER -> trad.translate("RoleAsso.VTRESORIER");
case MEMBREBUREAU -> trad.translate("RoleAsso.MEMBREBUREAU");
};
}
@Override
public String toString() {
return str;

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;
LOGGER.infof("Received file upload request for: %s (size: %d bytes)", file.fileName(), file.size());
return Uni.createFrom().<InputStream>item(() -> {
try {
Tika tika = new Tika();
mimeType = tika.detect(is);// Magic.getMagicMatch(input, false).getMimeType();
// 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) {
mimeType = URLConnection.guessContentTypeFromStream(is);
throw new RuntimeException("Failed to read uploaded file: " + e.getMessage(), e);
}
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
}).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());
}
}
// 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 IOException("Fail to detect file extension for MIME type " + mimeType);
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 IOException("Fail to create directory " + dir);
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 file : files) {
for (File f : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
f.delete();
}
}
String extension = "." + detectedExtensions[0];
Files.write(new File(dirFile, id + extension).toPath(), input);
return "OK";
} catch (IOException e) {
return e.getMessage();
}
});
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;
@@ -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

@@ -5,6 +5,7 @@ import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CardboardRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.ws.PermLevel;
@@ -32,13 +33,16 @@ public class RCardboard {
@Inject
CardboardRepository cardboardRepository;
@Inject
TradService trad;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Matche non trouver");
throw new DNotFoundException(trad.t("matche.non.trouver"));
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
throw new DForbiddenException(trad.t("permission.denied"));
}));
}
@@ -80,35 +84,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 -> {
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();
});
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 -> {
}
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,12 +3,10 @@ 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.domain.service.TradService;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.TreeNode;
@@ -46,13 +44,19 @@ public class RCategorie {
@Inject
TreeRepository treeRepository;
@Inject
CardboardRepository cardboardRepository;
@Inject
TradService trad;
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
return categoryRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Catégorie non trouver");
throw new DNotFoundException(trad.t("categorie.non.trouver"));
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
throw new DForbiddenException(trad.t("permission.denied"));
}));
}
@@ -97,7 +101,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 +122,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 +206,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

@@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
import fr.titionfire.ffsaf.domain.service.TradService;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
@@ -44,13 +45,19 @@ public class RMatch {
@Inject
CompetitionGuestRepository competitionGuestRepository;
@Inject
CardboardRepository cardboardRepository;
@Inject
TradService trad;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Matche non trouver");
throw new DNotFoundException(trad.t("matche.non.trouver"));
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
throw new DForbiddenException(trad.t("permission.denied"));
}));
}
@@ -78,13 +85,13 @@ public class RMatch {
return categoryRepository.findById(m.categorie)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Catégorie non trouver");
throw new DNotFoundException(trad.t("categorie.non.trouver"));
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
throw new DForbiddenException(trad.t("permission.denied"));
}))
.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 +128,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 +143,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 +170,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 +196,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 +284,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();
}
@@ -288,13 +298,15 @@ public class RMatch {
return categoryRepository.findById(data.categorie)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Catégorie non trouver");
throw new DNotFoundException(trad.t("categorie.non.trouver"));
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
throw new DForbiddenException(trad.t("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 +316,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))
.chain(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
Panache.withTransaction(() -> matchRepository.create(mm))
.invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
.call(__ -> SSMatch.sendMatch(connection, matches))
.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

@@ -1,16 +0,0 @@
filtre.all=--tout--
no.licence=Non licenci\u00E9
# Categories
Cat.SUPER_MINI=Super Mini
Cat.MINI_POUSSIN=Mini Poussin
Cat.POUSSIN= Poussin
Cat.BENJAMIN=Benjamin
Cat.MINIME=Minime
Cat.CADET=Cadet
Cat.JUNIOR=Junior
Cat.SENIOR1=Senior 1
Cat.SENIOR2=Senior 2
Cat.VETERAN1=V\u00E9t\u00E9ran 1
Cat.VETERAN2=V\u00E9t\u00E9ran 2

View File

@@ -0,0 +1,87 @@
filtre.all=--all--
no.licence=Not licensed
Cat.SUPER_MINI=Super Mini
Cat.MINI_POUSSIN=Mini Chick
Cat.POUSSIN=Chick
Cat.BENJAMIN=Benjamin
Cat.MINIME=Minime
Cat.CADET=Cadet
Cat.JUNIOR=Junior
Cat.SENIOR1=Senior 1
Cat.SENIOR2=Senior 2
Cat.VETERAN1=Veteran 1
Cat.VETERAN2=Veteran 2
access.denied=Access denied
club.not.found=Club not found
combattant.non.inscrit=Fighter not registered
comb.not.found=Fighter not found
matche.non.trouver=Match not found
permission.denied=Permission denied
categorie.non.trouver=Category not found
RoleAsso.MEMBRE=Member
RoleAsso.PRESIDENT=President
RoleAsso.VPRESIDENT=Vice-President
RoleAsso.SECRETAIRE=Secretary
RoleAsso.VSECRETAIRE=Vice-Secretary
RoleAsso.TRESORIER=Treasurer
RoleAsso.VTRESORIER=Vice-Treasurer
RoleAsso.MEMBREBUREAU=Board Member
sans.club=No club
categorie.inconnue=Unknown category
GradeArbitrage.NA=N/A
GradeArbitrage.ASSESSEUR=Assessor
GradeArbitrage.ARBITRE=Referee
Genre.Homme=Male
Genre.Femme=Female
Genre.NA=Not defined
Contact.COURRIEL=Email
Contact.TELEPHONE=Phone
Contact.SITE=Website
Contact.FACEBOOK=Facebook
Contact.INSTAGRAM=Instagram
Contact.AUTRE=Other
service.momentanement.indisponible=Service temporarily unavailable
asso.introuvable=Association not found
erreur.lors.calcul.du.trie=Error during sorting calculation
page.out.of.range=Page out of range
le.membre.appartient.pas.a.votre.club=Member no. %d does not belong to your club
email.deja.utilise.par=Email '%s' already used by %s %s
try.edit.licence=To register a new member, please leave the license field blank. (Unauthorized attempt to change the name on license %d for %s %s)
email.deja.utilise=Email already in use
regiter.new.membre=To register a new member, please use the dedicated button.
permission.insuffisante=Insufficient permission
membre.deja.existent=Member already exists
membre.rm.err1=Cannot delete a member who already has a license number
membre.rm.err2=Cannot delete a member with licenses
pas.de.licence.pour.la.saison.en.cours=No license for the current season
club.non.trouve=Club not found
pas.d.affiliation.pour.la.saison.en.cours=No affiliation for the current season
erreur.lors.de.la.selection.des.membres=Error during member selection
licence.rm.err1=Cannot delete a license for which payment is in progress
licence.deja.demandee=License already requested
impossible.de.supprimer.une.licence.deja.validee=Cannot delete an already validated license
impossible.de.supprimer.une.licence.deja.payee=Cannot delete an already paid license
vous.ne.pouvez.pas.creer.de.competition=You cannot create a competition
user.not.found=User %s not found
inscription.fermee=Registration closed
insc.err1=You do not have the right to register this member (by decision of the competition administrator)
insc.err2=You do not have the right to register (by decision of the competition administrator)
insc.err3=Modification blocked by the competition administrator
licence.non.trouve=License %s not found
nom.et.prenom.requis=Last name and first name required
combattant.non.trouve=Fighter %s %s not found
le.membre.n.existe.pas=Member %d does not exist
competition.is.not.internal=Competition is not INTERNAL
erreur.de.format.des.contacts=Contact format error
competition.not.found=Competition not found
saison.non.valid=Invalid season
demande.d.affiliation.deja.existante=Affiliation request already exists
affiliation.deja.existante=Affiliation already exists
licence.membre.n.1.inconnue=License member no. 1 unknown
licence.membre.n.2.inconnue=License member no. 2 unknown
licence.membre.n.3.inconnue=License member no. 3 unknown
demande.d.affiliation.non.trouve=Affiliation request not found

View File

@@ -0,0 +1,83 @@
filtre.all=--Tous--
no.licence=Non licencié
Cat.SUPER_MINI=Super Mini
Cat.MINI_POUSSIN=Mini-Poussin
Cat.POUSSIN=Poussin
Cat.BENJAMIN=Benjamin
Cat.MINIME=Minime
Cat.CADET=Cadet
Cat.JUNIOR=Junior
Cat.SENIOR1=Senior 1
Cat.SENIOR2=Senior 2
Cat.VETERAN1=Vétéran 1
Cat.VETERAN2=Vétéran 2
access.denied=Accès refusé
club.not.found=Club introuvable
combattant.non.inscrit=Combattant non inscrit
comb.not.found=Combat introuvable
matche.non.trouver=Match introuvable
permission.denied=Permission refusée
categorie.non.trouver=Catégorie introuvable
RoleAsso.MEMBRE=Membre
RoleAsso.PRESIDENT=Président
RoleAsso.VPRESIDENT=Vice-Président
RoleAsso.SECRETAIRE=Secrétaire
RoleAsso.VSECRETAIRE=Vice-Secrétaire
RoleAsso.TRESORIER=Trésorier
RoleAsso.VTRESORIER=Vice-Trésorier
RoleAsso.MEMBREBUREAU=Membre bureau
sans.club=Sans club
categorie.inconnue=catégorie inconnue
GradeArbitrage.NA=N/A
GradeArbitrage.ASSESSEUR=Assesseur
GradeArbitrage.ARBITRE=Arbitre
Genre.Homme=Homme
Genre.Femme=Femme
Genre.NA=Non défini
Contact.COURRIEL=Courriel
Contact.TELEPHONE=Téléphone
Contact.SITE=Site web
Contact.FACEBOOK=Facebook
Contact.INSTAGRAM=Instagram
Contact.AUTRE=Autre
service.momentanement.indisponible=Service momentanément indisponible
asso.introuvable=Association introuvable
erreur.lors.calcul.du.trie=Erreur lors du calcul du tri
page.out.of.range=Page out of range
le.membre.appartient.pas.a.votre.club=Le membre n°%d n?appartient pas à votre club
email.deja.utilise.par=L?adresse e-mail '%s' est déjà utilisée par %s %s
try.edit.licence=Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (Tentative de modification non autorisée du nom sur la licence %d pour %s %s)
email.deja.utilise=Adresse e-mail déjà utilisée
regiter.new.membre=Pour enregistrer un nouveau membre, veuillez utiliser le bouton prévu à cet effet.
permission.insuffisante=Permissions insuffisantes
membre.deja.existent=Membre déjà existant
membre.rm.err1=Impossible de supprimer un membre qui possède déjà un numéro de licence
membre.rm.err2=Impossible de supprimer un membre ayant des licences actives
pas.de.licence.pour.la.saison.en.cours=Aucune licence pour la saison en cours
club.non.trouve=Club introuvable
pas.d.affiliation.pour.la.saison.en.cours=Aucune affiliation pour la saison en cours
erreur.lors.de.la.selection.des.membres=Erreur lors de la sélection des membres
licence.rm.err1=Impossible de supprimer une licence pour laquelle un paiement est en cours
licence.deja.demandee=Licence déjà demandée
impossible.de.supprimer.une.licence.deja.validee=Impossible de supprimer une licence déjà validée
impossible.de.supprimer.une.licence.deja.payee=Impossible de supprimer une licence déjà payée
vous.ne.pouvez.pas.creer.de.competition=Vous n?êtes pas autorisé à créer une compétition
user.not.found=Utilisateur %s introuvable
inscription.fermee=Inscription fermée
insc.err1=Vous n?êtes pas autorisé à inscrire ce membre (décision de l?administrateur de la compétition)
insc.err2=Vous n?êtes pas autorisé à vous inscrire (décision de l?administrateur de la compétition)
insc.err3=Modification bloquée par l?administrateur de la compétition
licence.non.trouve=Licence %s introuvable
nom.et.prenom.requis=Nom et prénom obligatoires
combattant.non.trouve=Combattant %s %s introuvable
le.membre.n.existe.pas=Le membre n°%d n?existe pas
competition.is.not.internal=Competition is not INTERNAL
erreur.de.format.des.contacts=Format des contacts invalide
competition.not.found=Compétition introuvable
saison.non.valid=Saison invalide
demande.d.affiliation.deja.existante=Une demande d?affiliation existe déjà
affiliation.deja.existante=Affiliation déjà existante
licence.membre.n.1.inconnue=Licence du membre n°1 inconnue
licence.membre.n.2.inconnue=Licence du membre n°2 inconnue
licence.membre.n.3.inconnue=Licence du membre n°3 inconnue
demande.d.affiliation.non.trouve=Demande d?affiliation introuvable

View File

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

View File

@@ -18,10 +18,16 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.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",
"react-i18next": "^16.5.2",
"react-is": "^19.0.0",
"react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6",
@@ -343,12 +349,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"engines": {
"node": ">=6.9.0"
}
@@ -1058,6 +1061,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 +1856,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",
@@ -1856,6 +1872,14 @@
"node": ">=0.8"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1870,6 +1894,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 +2036,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"
},
@@ -2971,6 +2999,60 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "25.7.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
"integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@@ -2980,6 +3062,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 +3105,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 +3446,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 +3541,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 +3579,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 +3672,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",
@@ -3585,6 +3697,25 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -3696,6 +3827,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 +3916,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 +4024,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",
@@ -3936,6 +4110,32 @@
"react": "^18.2.0"
}
},
"node_modules/react-i18next": {
"version": "16.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
"integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
"dependencies": {
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
@@ -4055,6 +4255,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",
@@ -4110,11 +4329,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -4256,6 +4470,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 +4538,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 +4602,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",
@@ -4575,6 +4807,11 @@
"node": ">=4"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
@@ -4723,6 +4960,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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",
@@ -4804,6 +5054,28 @@
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4922,6 +5194,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,10 +20,16 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.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",
"react-i18next": "^16.5.2",
"react-is": "^19.0.0",
"react-leaflet": "^4.2.1",
"react-loader-spinner": "^6.1.6",

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -0,0 +1,988 @@
import i18next from 'https://cdn.jsdelivr.net/npm/i18next@25.7.4/+esm';
import i18nextHttpBackend from 'https://cdn.jsdelivr.net/npm/i18next-http-backend@3.0.2/+esm'
import i18nextBrowserLanguagedetector from 'https://cdn.jsdelivr.net/npm/i18next-browser-languagedetector@8.2.0/+esm'
let apiUrlRoot = "";
const rootDiv = document.getElementById("safca_api_data");
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 = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4>`;
let content = document.createElement('div');
content.innerHTML = `
<ul>
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
</ul>
`
rootDiv.append(content)
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
}
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 = i18next.t('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 i18next.t('disc.');
case -998:
return i18next.t('abs.');
case -999:
return i18next.t('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 i18next.t('aujourdhuià', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})});
else if (d === 1)
return i18next.t('hierà', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})});
else if (d === 2)
return i18next.t('avanthierà', {time: 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(i18next.t('poule'), isPoule, function () {
change_view(true);
});
ul.appendChild(li1);
const li2 = createTab(i18next.t('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">${i18next.t('rouge')}</th>
<th class="has-text-align-center" data-align="center"></th>
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
<th class="has-text-align-center" data-align="center"></th>
<th class="has-text-align-center" data-align="center">${i18next.t('bleu')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('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">${i18next.t('place')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('victoire')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReç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 = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = `<h4>${i18next.t('rechercheParCaté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 = `${i18next.t('poule')} ${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 = `${i18next.t('poule')} ${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(i18next.t('erreurDeChargementDeLaPoule')));
})
.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">${i18next.t('--sélectionnerUneCaté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(i18next.t('erreurDeChargementDesCaté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>${i18next.t('nomPrénom')} : ${comb.name}</li>
<li>${i18next.t('club')} : ${comb.club}</li>
<li>${i18next.t('catégorie')} : ${comb.cat}</li>
</ul>
<h3>${i18next.t('statistique')} :</h3>
<ul>
<li>${i18next.t('tauxDeVictoire2', {
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0),
victoires: comb.totalWin,
matchs: comb.matchs.length
})}
</li>
<li>${i18next.t('pointsMarqués2', {nb: comb.pointMake})}</li>
<li>${i18next.t('pointsReçus2', {nb: comb.pointTake})}</li>
<li>${i18next.t('ratioDuScore2', {nb: comb.pointRatio.toFixed(3)})}</li>
</ul>
<h3>${i18next.t('listeDesMatchs')}:</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">${i18next.t('date')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('poule')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('adversaire')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('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 = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = `<h4>${i18next.t('rechercheParCombattant')}</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(i18next.t('erreurDeChargementDuCombattant'))))
.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">${i18next.t('--sélectionnerUnCombattant--')}</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(i18next.t('erreurDeChargementDesCombattants'))))
.finally(() => stopLoading(loading));
rootDiv.append(content)
}
function buildClubView(club) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>${i18next.t('info')} :</h3>
<ul>
<li>${i18next.t('nom')} : ${club.name}</li>
<li>${i18next.t('nombreDinscris')} : ${club.nb_insc}</li>
</ul>
<h3>${i18next.t('statistique')} :</h3>
<ul>
<li>${i18next.t('nombreDeMatchDisputé2', {nb: club.nb_match})}</li>
<li>${i18next.t('nombreDeVictoires2', {nb: club.match_w})}</li>
<li>${i18next.t('ratioDeVictoiresMoyen2', {nb: club.ratioVictoire.toFixed(3)})}</li>
<li>${i18next.t('pointsMarqués2', {nb: club.pointMake})}</li>
<li>${i18next.t('pointsReçus2', {nb: club.pointTake})}</li>
<li>${i18next.t('ratioDePointsMoyen2', {nb: club.ratioPoint.toFixed(3)})}</li>
</ul>
<h3>${i18next.t('listeDesMenbres')} :</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">${i18next.t('catégorie')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('victoires')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('défaites')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratioVictoires')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReçus')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratioPoints')}</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 = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
const content = document.createElement('div');
content.style.marginTop = '1em';
content.innerHTML = `<h4>${i18next.t('rechercheParClub')}</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(i18next.t('erreurDeChargementDuClub'))))
.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">${i18next.t('--sélectionnerUnClub--')}</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(i18next.t('erreurDeChargementDesClubs'))))
.finally(() => stopLoading(loading));
rootDiv.append(content)
}
function buildCombsView(combs) {
const pouleDiv = document.createElement('div');
let arrayContent = `
<h3>${i18next.t('statistique')} :</h3>
<ul>
<li>${i18next.t('nombreDinscris2', {nb: combs.nb_insc})}</li>
<li>${i18next.t('nombreDeMatchDisputé2', {nb: combs.tt_match})}</li>
<li>${i18next.t('pointsMarqués2', {nb: combs.point})}</li>
</ul>
<h3>${i18next.t('listeDesCombattants')} :</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">${i18next.t('catégorie')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('club')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('victoires')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('défaites')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratioVictoires')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsReçus')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratiosPoints')}</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 = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
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(i18next.t('erreurDeChargementDeLaListe'))))
.finally(() => stopLoading(loading));
content.append(dataContainer);
rootDiv.append(content)
}
export async function initCompetitionApi(apiUrlRoot_, host) {
apiUrlRoot = apiUrlRoot_;
const options = {
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
caches: [],
}
const backend = {
loadPath: `${host}/locales/{{lng}}/{{ns}}.json`,
}
await i18next
.use(i18nextHttpBackend)
.use(i18nextBrowserLanguagedetector)
.init({
fallbackLng: 'fr',
debug: true,
interpolation: {
escapeValue: true,
},
detection: options,
ns: ['result'],
defaultNS: 'result',
})
console.log("apiUrlRoot:", apiUrlRoot)
console.log("FFSAF Competition API initialized.")
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) {
let 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,143 @@
{
"--SélectionnerUnCombattant--": "-- Select a fighter --",
"--Tous--": "-- All --",
"actuel": "Current",
"administration": "Administration",
"adresseDuServeur": "Server address",
"ajouter": "Add",
"ajouterDesCombattants": "Add fighters",
"attention": "Warning",
"aucuneConfigurationObs": "No OBS configuration found, please import one",
"bleu": "Blue",
"blue": "Blue",
"catégorie": "Category",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
"chrono.-10S": "-10 s",
"chrono.-1S": "-1 s",
"chrono.arrêter": "Stop",
"chrono.définirLeTemps": "Set time",
"chrono.démarrer": "Start",
"chrono.editionTemps": "Time editing",
"chrono.entrezLeTempsEnS": "Enter time in seconds",
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
"chronomètre": "Stopwatch",
"club": "Club",
"compétition": "Competition",
"compétitionManager": "Competition manager",
"config.obs.dossierDesResources": "Resources folder",
"config.obs.motDePasseDuServeur": "Server password",
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
"config.obs.ws": "ws://",
"configurationObs": "OBS Configuration",
"confirm1": "This match already has results; are you sure you want to delete it?",
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
"confirm2.title": "Tournament tree change",
"confirm3.msg": "Do you really want to remove the {{typeStr}} part from the category? This will delete the matches contained in this part!",
"confirm3.title": "Change category type",
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
"confirm4.title": "Delete category",
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs",
"créerLesMatchs": "Create matches",
"demi-finalesEtFinales": "Semi-finals and finals",
"duréePause": "Pause duration",
"duréeRound": "Round duration",
"editionDeLaCatégorie": "Edit category",
"enregister": "Save",
"enregistrer": "Save",
"epéeBouclier": "Sword and shield",
"err1": "A fighter cannot fight themselves!",
"err2": "The combat zone name format is invalid. Please separate names with ';'.",
"err3": "At least one type (pool or tournament) must be selected.",
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
"exporter": "Export",
"fermer": "Close",
"finalesUniquement": "Finals only",
"genre": "Gender",
"genre.f": "F",
"genre.h": "M",
"genre.na": "NA",
"inscrit": "Registered",
"manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
"matches": "Matches",
"modifier": "Edit",
"msg1": "There are already matches in this pool; what do you want to do with them?",
"neRienConserver": "Keep nothing",
"no": "No.",
"nom": "Name",
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
"nouvelle...": "New...",
"obs.préfixDesSources": "Source prefix",
"pays": "Country",
"poids": "Weight",
"poule": "Pool",
"poulePour": "Pool for: ",
"préparation...": "Preparing...",
"rouge": "Red",
"réinitialiser": "Reset",
"résultat": "Result",
"sansPoule": "No pool",
"sauvegarde": "Backup",
"sauvegarder": "Save",
"score": "Score",
"score.err1": "Cannot end a tournament match in a draw.",
"score.spe": "Special scores: <br/>-997: disqualified <br/>-998: absent <br/>-999: forfeit",
"scores": "Scores",
"secrétariatsDeLice": "Ring secretariats",
"select.aucunCombattantDisponible": "No fighters available",
"select.aucunCombattantSélectionné": "No fighters selected",
"select.msg1": "(0 = disabled)",
"select.recherche": "Search",
"select.sélectionnerDesCombatants": "Select fighters",
"select.à": "to",
"serveur": "Server",
"suivant": "Next",
"supprimer": "Delete",
"sélectionneLesModesDaffichage": "Select display modes",
"sélectionner": "Select",
"terminé": "Finished",
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.",
"toast.createCategory.error": "Error while creating the category",
"toast.createCategory.pending": "Creating category...",
"toast.createCategory.success": "Category created!",
"toast.deleteCategory.error": "Error while deleting the category",
"toast.deleteCategory.pending": "Deleting category...",
"toast.deleteCategory.success": "Category deleted!",
"toast.matchs.create.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.",
"toast.updateCategory.error": "Error while updating the category",
"toast.updateCategory.pending": "Updating category...",
"toast.updateCategory.success": "Category updated!",
"toast.updateMatchScore.error": "Error while updating match score",
"toast.updateMatchScore.pending": "Updating match score...",
"toast.updateMatchScore.success": "Match score updated!",
"toast.updateTrees.error": "Error while updating trees",
"toast.updateTrees.init.error": "Error while creating trees",
"toast.updateTrees.init.pending": "Creating tournament trees...",
"toast.updateTrees.init.success": "Trees created!",
"toast.updateTrees.pending": "Updating tournament trees...",
"toast.updateTrees.success": "Trees updated!",
"tournoi": "Tournament",
"tournois": "Tournaments",
"tousLesMatchs": "All matches",
"toutConserver": "Keep all",
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
"ttm.table.pub_aff": "Open public display",
"ttm.table.pub_score": "Show scores on public display",
"type": "Type",
"téléchargement": "Download",
"téléchargementEnCours": "Downloading...",
"téléchargementTerminé!": "Download completed!",
"uneCatégorie": "a category",
"valider": "Validate",
"zone": "Zone",
"zoneDeCombat": "Combat zone"
}

View File

@@ -0,0 +1,543 @@
{
"(optionnelle)": "(optional)",
"---SansClub---": "--- no club ---",
"---ToutLesClubs---": "--- all clubs ---",
"---ToutLesPays---": "--- all countries ---",
"---TouteLesCatégories---": "--- all categories ---",
"--NonLicencier--": "-- Not licensed --",
"--SélectionnerCatégorie--": "-- Select category --",
"1Catégorie": "+1 category",
"2Catégorie": "+2 categories",
"activer": "Activate",
"admin": "Administration",
"administrateur": "Administrator",
"adresse": "Address",
"adresseAdministrative": "Administrative address",
"aff.ancienNom": "Former name: {{name}}",
"aff.byMembreSim": "By similar member",
"aff.byNewMenbre": "By new member",
"aff.byNoLicence": "By license number",
"aff.info1": "This club has already been affiliated (affiliation no. {{no}})",
"aff.membreNo": "Member no. {{no}}",
"aff.nomDuClub": "Club name",
"aff.raisonDuRefus": "Reason for refusal",
"aff.raisonDuRefus.msg": "Please indicate the reason for refusal",
"aff.refusConfirm": "Are you sure you want to refuse this request?",
"aff.refuserLaDemande": "Refuse the request",
"aff.refuserLaDemande.detail": "Are you sure you want to refuse this request?",
"aff.submit.error1": "Please enter a valid license number for member {{id}}",
"aff.submit.error2": "Please enter a valid license number for member {{id}}",
"aff.submit.error3": "Please enter a valid email for member {{id}}",
"aff.toast.accept.error": "Failed to accept affiliation",
"aff.toast.accept.pending": "Accepting affiliation in progress",
"aff.toast.accept.success": "Affiliation accepted successfully 🎉",
"aff.toast.del.error": "Failed to delete affiliation request",
"aff.toast.del.pending": "Deleting affiliation request in progress",
"aff.toast.del.success": "Affiliation request deleted successfully 🎉",
"aff.toast.del2.error": "Failed to delete affiliation",
"aff.toast.del2.pending": "Deleting affiliation in progress",
"aff.toast.del2.success": "Affiliation deleted successfully 🎉",
"aff.toast.save.error": "Failed to save affiliation request",
"aff.toast.save.pending": "Saving affiliation request in progress",
"aff.toast.save.success": "Affiliation request saved successfully 🎉",
"aff.toast.save2.error": "Failed to save affiliation",
"aff.toast.save2.pending": "Saving affiliation in progress",
"aff.toast.save2.success": "Affiliation saved successfully 🎉",
"aff_req.appuyerSurRechercher": "Press search to complete",
"aff_req.association": "The association",
"aff_req.button.cancel": "Cancel my request",
"aff_req.button.confirm": "Confirm my affiliation request",
"aff_req.button.save": "Save changes",
"aff_req.denomination": "Denomination",
"aff_req.disposeLicence": "Already has a license",
"aff_req.error1": "The role of member {{i}} is required",
"aff_req.error2": "The SIRET/RNA format is invalid",
"aff_req.nomDeLassociation": "Association name",
"aff_req.text1": "Affiliation is annual and valid for one sports season: from September 1st to August 31st of the following year.",
"aff_req.text2": "To affiliate, a sports association must meet the following conditions:",
"aff_req.text2.li": [
"Have its headquarters in France or the Principality of Monaco",
"Be constituted in accordance with Chapter 1 of Title II of Book 1 of the Sports Code",
"Pursue a purpose that falls within the definition of Article 1 of the Federation's statutes",
"Have statutes compatible with the principles of organization and operation of the Federation",
"Ensure freedom of opinion and respect for the rights of defense within it, and prohibit any discrimination",
"Comply with the rules of supervision, hygiene, and safety established by the Federation's regulations"
],
"aff_req.text3": "After validation of your request, you will receive a temporary identifier and password to access your FFSAF space",
"aff_req.text4": "Note that to finalize your affiliation, you will need to:",
"aff_req.text4.li1": "Have at least three licensed members, including the president",
"aff_req.text4.li2": "Have paid the fees provided for by the federal regulations",
"aff_req.text5": "You can later add publicly visible addresses for your training locations",
"aff_req.text6": "Leave blank to make no changes. (If a coat of arms has already been sent with this request, it will be used; otherwise, we will use the one from the previous affiliation)",
"aff_req.text7": "Affiliation request sent successfully",
"aff_req.text8": "Once your request is validated, you will receive a temporary identifier and password to access your FFSAF space",
"aff_req.toast.undo.error": "Failed to cancel affiliation request",
"aff_req.toast.undo.pending": "Cancelling affiliation request in progress",
"aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉",
"afficherLétatDesAffiliation": "Display affiliation status",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation no. {{no}}",
"ajout": "Addition",
"ajouterUnClub": "Add a club",
"ajouterUnMembre": "Add a member",
"all_season": "--- all seasons ---",
"au": "to",
"aucun": "None",
"aucunMembreSélectionné": "No member selected",
"back": "« back",
"blason": "Coat of arms",
"bureau": "Board",
"button.accepter": "Accept",
"button.ajouter": "Add",
"button.annuler": "Cancel",
"button.appliquer": "Apply",
"button.confirmer": "Confirm",
"button.créer": "Create",
"button.enregister": "Save",
"button.enregistrer": "Save",
"button.fermer": "Close",
"button.modifier": "Edit",
"button.refuser": "Refuse",
"button.seDésinscrire": "Unsubscribe",
"button.suivant": "Next",
"button.supprimer": "Delete",
"cat.benjamin": "Benjamin",
"cat.cadet": "Cadet",
"cat.catégorieInconnue": "Unknown category",
"cat.junior": "Junior",
"cat.miniPoussin": "Mini Chick",
"cat.minime": "Minime",
"cat.poussin": "Chick",
"cat.senior1": "Senior 1",
"cat.senior2": "Senior 2",
"cat.superMini": "Super Mini",
"cat.vétéran1": "Veteran 1",
"cat.vétéran2": "Veteran 2",
"categorie": "category",
"catégorie": "Category",
"certificatMédical": "Medical certificate",
"chargement...": "Loading...",
"chargerLexcel": "Load Excel",
"chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.",
"choisir...": "Choose...",
"club.aff_renew.msg": "Please select 0 to 3 board members to fill out the pre-request. (If a non-board member will become one next year, you can enter them in the next step)",
"club.change.status": "To modify the above information, please contact the FFSAF by email.",
"club.contact.tt": {
"AUTRE": "Other club contact",
"COURRIEL": "Club email address<br>Example: contact@ffsaf.fr",
"FACEBOOK": "Club Facebook page starting with 'https://www.facebook.com/'<br>Example: https://www.facebook.com/ffmsf",
"INSTAGRAM": "Club Instagram account starting with 'https://www.instagram.com/'<br>Example: https://www.instagram.com/ff_msf",
"SITE": "Club website with or without 'https://'<br>Example: ffsaf.fr<br>Or https://ffsaf.fr",
"TELEPHONE": "Club phone number<br>Example: 06 12 13 78 55"
},
"club.toast.aff.error": "Failed to load affiliations",
"club.toast.aff.pending": "Loading affiliations in progress",
"club.toast.aff.success": "Affiliations loaded successfully 🎉",
"club.toast.del.error": "Failed to delete club",
"club.toast.del.pending": "Deleting club in progress",
"club.toast.del.success": "Club deleted successfully 🎉",
"club.toast.new.error": "Failed to create club",
"club.toast.new.pending": "Creating club in progress",
"club.toast.new.success": "Club created successfully 🎉",
"club.toast.save.error": "Failed to save club",
"club.toast.save.pending": "Saving club in progress",
"club.toast.save.success": "Club saved successfully 🎉",
"clubExterne": "External club",
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "No club",
"combattant": "fighter",
"comp.aff.blason": "Display the club's coat of arms on screens",
"comp.aff.flag": "Display the fighter's country on screens",
"comp.ajoutRapide": "Quick add",
"comp.ajouterUnCombattant": "Add a fighter",
"comp.ajouterUnInvité": "Add a guest",
"comp.billetterie": "Ticketing",
"comp.billetterieHelloasso": "HelloAsso Ticketing",
"comp.catégorieNormalisée": "Standardized category",
"comp.combattantNonTrouvé": "Fighter not found",
"comp.combattantsInscrits": "Registered fighters",
"comp.compétitionFuture": "Future competition",
"comp.compétitionPassée": "Past competition",
"comp.créationCompétition": "Competition creation",
"comp.dateDinscription": "Registration date",
"comp.editionCompétition": "Competition edition",
"comp.error1": "The end date must be after the start date.",
"comp.error2": "Please enter the start and end dates of registration.",
"comp.error3": "The end date of registration must be after the start date of registration.",
"comp.exporterLesInscription": "Export registrations",
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email for receiving failed registrations",
"comp.ha.error1": "Please enter the HelloAsso ticketing URL and associated rates.",
"comp.ha.error2": "The HelloAsso ticketing URL is invalid. Please check the URL format.",
"comp.ha.error3": "Please enter the email for receiving failed registrations.",
"comp.ha.tarifsHelloasso": "HelloAsso rates",
"comp.ha.text1": "To ensure good interconnection with HelloAsso, please follow these instructions:",
"comp.ha.text2": "<strong>Configure the notification URL:</strong> In order for us to receive a notification for each registration, it is necessary to configure the notification URL of your HelloAsso account to redirect to \"https://intra.ffsaf.fr/api/webhook/ha\". To do this, from the home page of your association on HelloAsso, go to <strong>My account</strong> > <strong>Settings</strong> > <strong>Integrations and API</strong> section Notification and copy-paste <strong>https://intra.ffsaf.fr/api/webhook/ha</strong> in the <strong>My callback URL</strong> field and save.",
"comp.ha.text3": "<strong>Copy-paste the exact name of the rates</strong> <em>-separated by semicolons-</em> that will result in automatic registration. All these rates must imperatively require the license number as a mandatory field. To do this, during the configuration of your ticketing at step 3, click on <strong>+ Add information</strong>, enter the exact title <strong>License number</strong>, in <strong>Type of response desired</strong> enter Number, select the rates entered previously and make the information mandatory.",
"comp.ha.text4": "<strong>Copy-paste the URL of your ticketing</strong> in the field below. It should look like this: https://www.helloasso.com/associations/__asso-name-on-helloasso__/events/__ticketing-name__",
"comp.ha.text5": "HelloAsso ticketing URL",
"comp.ha.text6": "If for any reason the automatic registration fails, an email will be sent to this address to inform you",
"comp.informationsGénéralesSurLaCompétition": "General information about the competition",
"comp.informationsSurLeModeDinscription": "Information on the registration mode",
"comp.informationsTechniques": "Technical information",
"comp.inscription": "Registration",
"comp.inscriptionModeAdministrateur": "Registration - administrator mode",
"comp.inscriptionsLibres": "Free registrations",
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators",
"comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers",
"comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing",
"comp.modal.information": "Information",
"comp.modal.poids": "Weight (in kg)",
"comp.modal.recherche": "Search*",
"comp.modal.surclassement": "Overclassification",
"comp.modal.text1": "Guests are reserved for members not licensed by the federation. Fighters registered via this form will not be able to see their results from their profile.",
"comp.modal.text2": "Prevent members/clubs from modifying this registration",
"comp.modifierLesParticipants": "View/Edit participants",
"comp.monInscription": "My registration",
"comp.noDeLicence": "License number",
"comp.nouvelleCompétition": "New competition",
"comp.organisateur": "Organizer",
"comp.quiPeutInscrire": "Who can register",
"comp.reg.libres": "Free",
"comp.reg.parLesAdministrateursDeLaCompétition": "By competition administrators",
"comp.reg.parLesResponsablesDeClub": "By club managers",
"comp.reg.surLaBilletterieHelloasso": "On the HelloAsso ticketing",
"comp.responsablesEtBureauxDesAssociations": "Managers and boards of associations",
"comp.sinscrire": "Register",
"comp.supprimerLaCompétition": "Delete competition",
"comp.supprimerLaCompétition.msg": "Are you sure you want to delete this competition and all associated results?",
"comp.surclassement_one": "{{cat}} with 1 overclassification",
"comp.surclassement_other": "{{cat}} with {{count}} overclassifications",
"comp.surclassement_zero": "{{cat}}",
"comp.text2": "Visible to the public (appears in the list of competitions)",
"comp.text3": "If not checked, the competition will only be visible to people who can register participants.",
"comp.tips": "Tip 1: It is possible to ban a fighter, which will prevent them from being re-registered by any means other than by an administrator of this competition. To do this, click on the small <1/> next to their name.<br/>Tip 2: It is also possible to lock the modifications of their registration from their file, which will prevent it from being modified/deleted by themselves and/or a club manager.",
"comp.toast.del.error": "Failed to delete competition",
"comp.toast.del.pending": "Deleting competition in progress",
"comp.toast.del.success": "Competition deleted successfully 🎉",
"comp.toast.params.error": "Failed to update competition parameters",
"comp.toast.params.pending": "Updating competition parameters in progress...",
"comp.toast.params.success": "Competition parameters updated successfully 🎉",
"comp.toast.register.add.error": "Fighter not found",
"comp.toast.register.add.pending": "Search in progress",
"comp.toast.register.add.success": "Fighter found and added/updated",
"comp.toast.register.ban.error": "Error",
"comp.toast.register.ban.pending": "Unregistration in progress",
"comp.toast.register.ban.success": "Fighter unregistered and banned",
"comp.toast.register.del.error": "Error",
"comp.toast.register.del.pending": "Unregistration in progress",
"comp.toast.register.del.success": "Fighter unregistered",
"comp.toast.register.self.add.error": "Error during registration",
"comp.toast.register.self.add.pending": "Registration in progress",
"comp.toast.register.self.add.success": "Registration completed 🎉",
"comp.toast.register.self.del.error": "Error during unregistration",
"comp.toast.register.self.del.pending": "Unregistration in progress",
"comp.toast.register.self.del.success": "Unregistration completed",
"comp.toast.save.error": "Failed to save competition",
"comp.toast.save.pending": "Saving competition in progress",
"comp.toast.save.success": "Competition saved successfully 🎉",
"comp.tousLesMembresDeLaFfsaf": "All FFSAF members",
"comp.typeDinscription": "Registration type",
"comp.uniquementLesAdministrateursDeLaCompétition": "Only competition administrators",
"comp.warn1": "Are you sure you want to unregister and ban this fighter from the competition?\n(You can re-register them later)",
"comp.warn2": "Are you sure you want to unregister this fighter?\nThis will not unregister them from the HelloAsso ticketing and will not refund them.",
"comp.warn3": "Are you sure you want to unregister this fighter?",
"comp.warn4": "Are you sure you want to unregister yourself?",
"comp_manage": "Competitions Manager",
"competition_one": "Competition",
"competition_other": "Competitions",
"compte": "Account",
"compétition": "Competition",
"configuration": "Configuration",
"conserverLancienEmail": "Keep the old email",
"contactAdministratif": "Administrative contact",
"contactInterne": "Internal contact",
"contact_one": "Contact",
"contact_other": "Contacts",
"date": "Date",
"dateDeNaissance": "Date of birth",
"days": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"de": "of",
"demandeDaffiliationEnCours": "Affiliation request in progress",
"demandeDeLicence": "License request",
"demander": "Request",
"description": "Description",
"dlAff": "Download affiliation certificate",
"donnéesAdministratives": "Administrative data",
"du": "From",
"dun": "of a",
"définirLidDuCompte": "Define account ID",
"editionDeL'affiliation": "Editing affiliation",
"editionDeLaDemande": "Editing request",
"editionDeLaLicence": "Editing license",
"editionDeLaSéléction": "Editing selection",
"editionDeLadresse": "Editing address",
"email": "Email",
"en": "in",
"enAttente": "Pending",
"erreurDePaiement": "Payment error😕",
"erreurDePaiement.detail": "Error message:",
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
"espaceAdministration": "Administration space",
"f": "F",
"faitPar": "Done by",
"femme": "Female",
"filtre": "Filter",
"genre": "Gender",
"gestionGroupée": "Group management",
"gradeDarbitrage": "Refereeing grade",
"h": "M",
"home": {
"header1": "For licensed members",
"header2": "For clubs",
"text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, register for competitions, and consult your results, provided that the organizing club has entered them.<br/><br/>During your first registration, you will receive an email containing your login information; this email will be sent once your license is validated by the secretariat.",
"text2": "This is where you can take out federal licenses for your members, where you can request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.<br/>You will also have the possibility to publish registration forms for your competitions and to record the results.<br/><br/>Not yet affiliated with the federation? Click <1>here</1> to make your first request.",
"welcome_message": "Welcome to the intranet of the Fédération France Soft Armored Fighting"
},
"homme": "Male",
"horairesD'entraînements": "Training schedules",
"information": "Information",
"invité": "guest",
"keepEmpty": "Leave blank to make no changes.",
"le": "the",
"licence": "License",
"licenceNo": "License no. {{no}}",
"lieu": "Place",
"lieuxDentraînements": "Training locations",
"loading": "Loading...",
"me": {
"result": {
"PRIVATE": "Private (visible only to me)",
"PUBLIC": "Public (visible to all)",
"REGISTERED_ONLY": "Logged-in members (visible to federation members)",
"REGISTERED_ONLY_NO_DETAILS": "Logged-in members - hide details (visible to federation members)"
},
"toast.settings.error": "Failed to update settings 😕",
"toast.settings.pending": "Updating settings in progress...",
"toast.settings.success": "Settings updated successfully 🎉"
},
"me.changerMonMotDePasse": "Change my password",
"me.formationDarbitrage": "Refereeing training",
"me.paramètresDuCompte": "Account settings",
"me.rôleAuSienDuClub": "Role within the club",
"me.visibilitéDesRésultats": "Visibility of results",
"member_one": "Member",
"member_other": "Members",
"membre.emailVideàLaLigne": "Empty email on line {{no}}",
"membre.emailVérifié": "Email verified",
"membre.filtre.inactif": "Show inactive fighters",
"membre.filtre.licence": "Show license status",
"membre.filtre.licences": [
"No request or valid license",
"With request or valid license",
"Request in progress",
"License validated",
"All license statuses",
"Complete request",
"Incomplete request"
],
"membre.filtre.payement": [
"Without payment",
"With payment",
"All payment statuses"
],
"membre.identifiant": "Identifier",
"membre.import.err1": "Invalid medical certificate date format on line {{no}}",
"membre.import.err2": "Invalid medical certificate date format on line {{no}}",
"membre.import.err3": "Empty date of birth on line {{no}}",
"membre.import.err4": "Invalid date of birth format on line {{no}}",
"membre.import.err5": "Invalid email on line {{no}}",
"membre.import.errTT_one": "{{count}} error in the file, operation cancelled",
"membre.import.errTT_other": "{{count}} errors in the file, operation cancelled",
"membre.import.warn_one": "{{count}} medical certificate not filled",
"membre.import.warn_other": "{{count}} medical certificates not filled",
"membre.info.emailInfo": "The email is used to create an account to log in to the site and must be unique.<br/>For minors, the parents' email can be used multiple times using the following syntax: {'email.parent+<alphanumeric characters>@example.com'}.<br/>Examples: mail.parent+1@example.com, mail.parent+titouan@example.com, mail.parent+cedrique@example.com",
"membre.info.error1": "Please select a valid country 😕",
"membre.info.error2": "Please select a valid club 😕",
"membre.initaccount": "Initialize account",
"membre.initaccount.text1": "Enter the account UUID",
"membre.initaccount.text2": "Warning: only change a member's ID if you are sure of what you are doing...",
"membre.noAccount": "This member does not have an account...",
"membre.noAccount.clubMsg": "An account will be created by the federation when their first license is validated",
"membre.nomVideàLaLigne": "Empty last name on line {{no}}",
"membre.prénomVideàLaLigne": "Empty first name on line {{no}}",
"membre.toast.compte.created": "Account created successfully 🎉",
"membre.toast.compte.error": "Failed to create account",
"membre.toast.compte.pending": "Creating account in progress",
"membre.toast.del.error": "Failed to delete account",
"membre.toast.del.pending": "Deleting account in progress",
"membre.toast.del.success": "Account deleted successfully 🎉",
"membre.toast.id.error": "Failed to define identifier",
"membre.toast.id.pending": "Defining identifier in progress",
"membre.toast.id.success": "Identifier defined successfully 🎉",
"membre.toast.licence.ask.del.error": "Failed to delete license request",
"membre.toast.licence.ask.del.pending": "Deleting license request in progress",
"membre.toast.licence.ask.del.success": "License request deleted successfully 🎉",
"membre.toast.licence.ask.error": "Failed to request license",
"membre.toast.licence.ask.pending": "Saving license request in progress",
"membre.toast.licence.ask.success": "License request saved successfully 🎉",
"membre.toast.licence.del.error": "Failed to delete license",
"membre.toast.licence.del.pending": "Deleting license in progress",
"membre.toast.licence.del.success": "License deleted successfully 🎉",
"membre.toast.licence.save.error": "Failed to save license",
"membre.toast.licence.save.pending": "Saving license in progress",
"membre.toast.licence.save.success": "License saved successfully 🎉",
"membre.toast.licences.export.error": "Failed to export licenses",
"membre.toast.licences.export.pending": "Exporting licenses in progress",
"membre.toast.licences.export.success": "Licenses exported successfully 🎉",
"membre.toast.licences.import.error": "Failed to send changes",
"membre.toast.licences.import.pending": "Sending changes in progress",
"membre.toast.licences.import.success": "Changes sent successfully 🎉",
"membre.toast.licences.load.error": "Failed to load licenses",
"membre.toast.licences.load.pending": "Loading licenses in progress",
"membre.toast.licences.load.success": "Licenses loaded successfully 🎉",
"membre.toast.perm.error": "Failed to update permissions 😕",
"membre.toast.perm.pending": "Updating permissions in progress...",
"membre.toast.perm.success": "Permissions updated successfully 🎉",
"membre.toast.save.error": "Failed to update profile 😕",
"membre.toast.save.pending": "Updating profile in progress...",
"membre.toast.save.success": "Profile updated successfully 🎉",
"membre.toast.select.del.error": "Failed to delete selection",
"membre.toast.select.del.pending": "Deleting selection in progress",
"membre.toast.select.del.success": "Selection deleted successfully 🎉",
"membre.toast.select.save.error": "Failed to save selection",
"membre.toast.select.save.pending": "Saving selection in progress",
"membre.toast.select.save.success": "Selection saved successfully 🎉",
"mettreàJours": "Update",
"modification": "Modification",
"nationalité": "Nationality",
"nav": {
"account": "My account",
"aff_request": "Affiliation request",
"club": {
"my": "My club"
},
"competitions": {
"results": "My results"
},
"home": "Home",
"login": "Login",
"logout": "Logout",
"space": "My space",
"title": "FFSAF Intranet"
},
"noLicence": "License no.",
"noSiretOuRna": "SIRET or RNA no.",
"nom": "Last name",
"nombreDeLicences": "Number of licenses",
"nombreDeLicencesParCatégorie": "Number of licenses by category for {{saison}}",
"non": "No",
"nonDéfinie": "Not defined",
"nonValidée": "Not validated",
"nouveauClub": "New club",
"nouveauMembre": "New member",
"nouvelEmail": "New email",
"ou": "or",
"oui": "Yes",
"outdated_session": {
"login_button": "Log in again",
"message": "Your session has expired, please log in again to continue using the application.",
"title": "Session expired"
},
"pageClub": "Club page",
"pageMembre": "Member page",
"pageNouveauClub": "New club page",
"page_info_full": "Line {{line}} to {{tt_line}} (page {{page}} of {{tt_page}})",
"page_info_ligne": "{{show}} line(s) displayed out of {{total}}",
"paiementDeLaLicence": "License payment",
"paiementDesLicences": "License payments",
"par": "by",
"pasDeLicence": "No license",
"payment.ha.info": "HelloAsso's solidarity model guarantees that 100% of your payment will be transferred to the chosen association. You can support the help they provide to associations by leaving a voluntary contribution to HelloAsso at the time of your payment.",
"payment.info": "About HelloAsso",
"payment.paiementSécurisé": "Secure payment",
"payment.payerAvec": "Pay with",
"payment.recap": "{{count}} license(s) selected<br/>Total amount to pay: {{total}} €",
"paymentDesLicences": "License payments",
"paymentDesLicences.msg_one": "Are you sure you want to mark the license as paid?",
"paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licenses as paid?",
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
"paymentOk": "🎉Your payment has been processed successfully.🎉",
"paymentOk.msg": "Thank you for your payment. The licenses should be activated within the next hour, provided that the medical certificate is completed.",
"pays": "Country",
"perm.administrateurDeLaFédération": "Federation administrator",
"perm.créerDesCompétion": "Create competitions",
"perm.ffsafIntra": "FFSAF intra",
"permission": "Permission",
"photos": "Photos",
"prenom": "First name",
"prénomEtNom": "First and last name",
"rechercher": "Search",
"rechercher...": "Search...",
"registration_one": "Registration",
"registration_other": "Registrations",
"renouveler": "Renew",
"renouvellementDeLaffiliation": "Affiliation renewal",
"result_one": "Result",
"result_other": "Results",
"retouràLaListeDeMembres": "Back to member list",
"role": "Role",
"role.membre": "Member",
"role.membreDuBureau": "Board member",
"role.président": "President",
"role.secrétaire": "Secretary",
"role.trésorier": "Treasurer",
"role.vise-président": "Vice-President",
"role.vise-secrétaire": "Vice-Secretary",
"role.vise-trésorier": "Vice-Treasurer",
"saison": "Season",
"secrétariatsDeLice": "Ring secretariats",
"selectionner...": "Select...",
"siretOuRna": "SIRET or RNA",
"stats": "Statistics",
"statue": "Statue",
"status": "Status",
"statuts": "Statutes",
"supprimerLeClub": "Delete club",
"supprimerLeClub.msg": "Are you sure you want to delete this club?",
"supprimerLeCompte": "Delete account",
"supprimerLeCompte.msg": "Are you sure you want to delete this account?",
"sélectionEnéquipeDeFrance": "Selection in the French team",
"sélectionner...": "Select...",
"toast.edit.error": "Failed to save changes",
"toast.edit.pending": "Saving changes in progress",
"toast.edit.success": "Changes saved successfully 🎉",
"toast.licence.bulk.pay.error": "Failed to mark licenses as paid",
"toast.licence.bulk.pay.pending": "Marking licenses as paid in progress",
"toast.licence.bulk.pay.success": "Licenses marked as paid successfully 🎉",
"toast.licence.bulk.valid.error": "Failed to validate licenses",
"toast.licence.bulk.valid.pending": "Validating licenses in progress",
"toast.licence.bulk.valid.success": "Licenses validated successfully 🎉",
"toast.licence.order.error": "Failed to create order",
"toast.licence.order.pending": "Creating order in progress",
"toast.licence.order.success": "Order created successfully 🎉",
"trie": "Sort",
"téléchargerLexcelDesMembres": "Download members' Excel",
"téléchargerLexcelDesMembres.info": "To be used as a template to update information",
"téléchargéeLaLicence": "Download license",
"validationDeLaLicence": "License validation",
"validationDesLicences": "License validations",
"validerDesLicences": "Validate licenses",
"validerLePayement_one": "Validate payment for the selected license",
"validerLePayement_other": "Validate payment for the {{count}} selected licenses",
"validerLePayement_zero": "$t(validerLePayement_other)",
"validerLicence.msg_one": "Are you sure you want to validate the license?",
"validerLicence.msg_other": "Are you sure you want to validate the {{count}} licenses?",
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
"validerLicence_one": "Validate the selected license",
"validerLicence_other": "Validate the {{count}} selected licenses",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validated",
"voir/modifierLesParticipants": "View/Edit participants",
"voirLesStatues": "View statues",
"à": "at",
"étatDeLaDemande": "Request status"
}

View File

@@ -0,0 +1,67 @@
{
"--sélectionnerUnClub--": "--Select a club--",
"--sélectionnerUnCombattant--": "--Select a fighter--",
"--sélectionnerUneCatégorie--": "--Select a category--",
"abs.": "abs.",
"adversaire": "Opponent",
"aujourdhuià": "Today at {{time}}",
"avanthierà": "Day before yesterday at {{time}}",
"back": "« back",
"bleu": "Blue",
"catégorie": "Category",
"chargement": "Loading",
"club": "Club",
"combattant": "Fighter",
"combattants": "Fighters",
"date": "Date",
"disc.": "DQ",
"défaites": "Losses",
"erreurDeChargementDeLaListe": "Error loading the list",
"erreurDeChargementDeLaPoule": "Error loading the pool",
"erreurDeChargementDesCatégories": "Error loading categories",
"erreurDeChargementDesClubs": "Error loading clubs",
"erreurDeChargementDesCombattants": "Error loading fighters",
"erreurDeChargementDuClub": "Error loading club",
"erreurDeChargementDuCombattant": "Error loading fighter",
"for.": "forf.",
"hierà": "Yesterday at {{time}}",
"info": "Info",
"listeDesCombattants": "List of fighters",
"listeDesMatchs": "List of matches",
"listeDesMembres": "List of members",
"listeDesMenbres": "List of members",
"nom": "Last name",
"nomPrénom": "Last name First name",
"nombreDeMatchDisputé2": "Number of matches played: {{nb}}",
"nombreDeVictoires2": "Number of wins: {{nb}}",
"nombreDinscris": "Number of registered",
"nombreDinscris2": "Number of registered: {{nb}}",
"parCatégorie": "By category",
"parClub": "By club",
"parCombattant": "By fighter",
"place": "Place",
"pointsMarqués": "Points scored",
"pointsMarqués2": "Points scored: {{nb}}",
"pointsReçus": "Points received",
"pointsReçus2": "Points received: {{nb}}",
"poule": "Pool",
"ratio": "Ratio",
"ratioDePointsMoyen2": "Average points ratio: {{nb}}",
"ratioDeVictoiresMoyen2": "Average win ratio: {{nb}}",
"ratioDuScore2": "Score ratio (points scored / points received): {{nb}}",
"ratioPoints": "Points ratio",
"ratioVictoires": "Win ratio",
"ratiosPoints": "Points ratios",
"rechercheParCatégorie": "Search by category",
"rechercheParClub": "Search by club",
"rechercheParCombattant": "Search by fighter",
"rouge": "Red",
"résultatDeLaCompétition": "Competition result",
"scores": "Scores",
"statistique": "Statistics",
"tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})",
"tournois": "Tournaments",
"tousLesCombattants": "All fighters",
"victoire": "Win",
"victoires": "Wins"
}

View File

@@ -0,0 +1,143 @@
{
"--SélectionnerUnCombattant--": "-- Sélectionner un combattant --",
"--Tous--": "-- Tous --",
"actuel": "Actuel",
"administration": "Administration",
"adresseDuServeur": "Adresse du serveur",
"ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants",
"attention": "Attention",
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
"bleu": "Bleu",
"blue": "Blue",
"catégorie": "Catégorie",
"chrono.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s",
"chrono.+1S": "+1 s",
"chrono.-10S": "-10 s",
"chrono.-1S": "-1 s",
"chrono.arrêter": "Arrêter",
"chrono.définirLeTemps": "Définir le temps",
"chrono.démarrer": "Démarrer",
"chrono.editionTemps": "Edition temps",
"chrono.entrezLeTempsEnS": "Entrez le temps en s",
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
"chronomètre": "Chronomètre",
"club": "Club",
"compétition": "Compétition",
"compétitionManager": "Compétition manager",
"config.obs.dossierDesResources": "Dossier des resources",
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
"config.obs.warn1": "/! 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",
"config.obs.ws": "ws://",
"configurationObs": "Configuration OBS",
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!",
"confirm2.title": "Changement de l'arbre du tournoi",
"confirm3.msg": "Voulez-vous vraiment enlever la partie {{typeStr}} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !",
"confirm3.title": "Changement de type de catégorie",
"confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
"confirm4.title": "Suppression de la catégorie",
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre",
"créerLesMatchs": "Créer les matchs",
"demi-finalesEtFinales": "Demi-finales et finales",
"duréePause": "Durée pause",
"duréeRound": "Durée round",
"editionDeLaCatégorie": "Edition de la catégorie",
"enregister": "Enregister",
"enregistrer": "Enregistrer",
"epéeBouclier": "Epée bouclier",
"err1": "Un combattant ne peut pas s'affronter lui-même !",
"err2": "Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.",
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
"exporter": "Exporter",
"fermer": "Fermer",
"finalesUniquement": "Finales uniquement",
"genre": "Genre",
"genre.f": "F",
"genre.h": "H",
"genre.na": "NA",
"inscrit": "Inscrit",
"manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
"matches": "Matches",
"modifier": "Modifier",
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
"neRienConserver": "Ne rien conserver",
"no": "N°",
"nom": "Nom",
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
"nouvelle...": "Nouvelle...",
"obs.préfixDesSources": "Préfix des sources",
"pays": "Pays",
"poids": "Poids",
"poule": "Poule",
"poulePour": "Poule pour: ",
"préparation...": "Préparation...",
"rouge": "Rouge",
"réinitialiser": "Réinitialiser",
"résultat": "Résultat",
"sansPoule": "Sans poule",
"sauvegarde": "Sauvegarde",
"sauvegarder": "Sauvegarder",
"score": "Score",
"score.err1": "Impossible de terminer un match nul en tournois.",
"score.spe": "Score speciaux : <br/>-997 : disqualifié <br/>-998 : absent <br/>-999 : forfait",
"scores": "Scores",
"secrétariatsDeLice": "Secrétariats de lice",
"select.aucunCombattantDisponible": "Aucun combattant disponible",
"select.aucunCombattantSélectionné": "Aucun combattant sélectionné",
"select.msg1": "(0 = désactivé)",
"select.recherche": "Recherche",
"select.sélectionnerDesCombatants": "Sélectionner des combatants",
"select.à": "à",
"serveur": "Serveur",
"suivant": "Suivant",
"supprimer": "Supprimer",
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
"sélectionner": "Sélectionner",
"terminé": "Terminé",
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.",
"toast.createCategory.error": "Erreur lors de la création de la catégorie",
"toast.createCategory.pending": "Création de la catégorie...",
"toast.createCategory.success": "Catégorie créée !",
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
"toast.deleteCategory.pending": "Suppression de la catégorie...",
"toast.deleteCategory.success": "Catégorie supprimée !",
"toast.matchs.create.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.",
"toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie",
"toast.updateCategory.pending": "Mise à jour de la catégorie...",
"toast.updateCategory.success": "Catégorie mise à jour !",
"toast.updateMatchScore.error": "Erreur lors de la mise à jour du score du match",
"toast.updateMatchScore.pending": "Mise à jour du score du match...",
"toast.updateMatchScore.success": "Score du match mis à jour !",
"toast.updateTrees.error": "Erreur lors de la mise à jour des arbres",
"toast.updateTrees.init.error": "Erreur lors de la création des arbres",
"toast.updateTrees.init.pending": "Création des arbres du tournoi...",
"toast.updateTrees.init.success": "Arbres créés !",
"toast.updateTrees.pending": "Mise à jour des arbres du tournoi...",
"toast.updateTrees.success": "Arbres mis à jour !",
"tournoi": "Tournoi",
"tournois": "Tournois",
"tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver",
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
"ttm.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
"ttm.table.pub_aff": "Ouvrir l'affichage public",
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
"type": "Type",
"téléchargement": "Téléchargement",
"téléchargementEnCours": "Téléchargement en cours...",
"téléchargementTerminé!": "Téléchargement terminé !",
"uneCatégorie": "une catégorie",
"valider": "Valider",
"zone": "Zone",
"zoneDeCombat": "Zone de combat"
}

View File

@@ -0,0 +1,543 @@
{
"(optionnelle)": "(optionnelle)",
"---SansClub---": "--- sans club ---",
"---ToutLesClubs---": "--- tout les clubs ---",
"---ToutLesPays---": "--- tout les pays ---",
"---TouteLesCatégories---": "--- toute les catégories ---",
"--NonLicencier--": "-- Non licencier --",
"--SélectionnerCatégorie--": "-- Sélectionner catégorie --",
"1Catégorie": "+1 catégorie",
"2Catégorie": "+2 catégorie",
"activer": "Activer",
"admin": "Administration",
"administrateur": "Administrateur",
"adresse": "Adresse",
"adresseAdministrative": "Adresse administrative",
"aff.ancienNom": "Ancien nom: {{name}}",
"aff.byMembreSim": "Par Membre similaire",
"aff.byNewMenbre": "Par Nouveau membre",
"aff.byNoLicence": "Par n° de licence",
"aff.info1": "Ce club a déjà été affilié (affiliation n°{{no}})",
"aff.membreNo": "Membre n°{{no}}",
"aff.nomDuClub": "Nom du club",
"aff.raisonDuRefus": "Raison du refus",
"aff.raisonDuRefus.msg": "Veuillez indiquer la raison du refus",
"aff.refusConfirm": "Êtes-vous sûr de vouloir refuser cette demande ?",
"aff.refuserLaDemande": "Refuser la demande",
"aff.refuserLaDemande.detail": "Êtes-vous sûr de vouloir refuser cette demande ?",
"aff.submit.error1": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
"aff.submit.error2": "Veuillez saisir un numéro de licence valide pour le membre {{id}}",
"aff.submit.error3": "Veuillez saisir un email valide pour le membre {{id}}",
"aff.toast.accept.error": "Échec de l'acceptation de l'affiliation",
"aff.toast.accept.pending": "Acceptation de l'affiliation en cours",
"aff.toast.accept.success": "Affiliation acceptée avec succès 🎉",
"aff.toast.del.error": "Échec de la suppression de la demande d'affiliation",
"aff.toast.del.pending": "Suppression de la demande d'affiliation en cours",
"aff.toast.del.success": "Demande d'affiliation supprimée avec succès 🎉",
"aff.toast.del2.error": "Échec de la suppression de l'affiliation",
"aff.toast.del2.pending": "Suppression de l'affiliation en cours",
"aff.toast.del2.success": "Affiliation supprimée avec succès 🎉",
"aff.toast.save.error": "Échec de l'enregistrement de la demande d'affiliation",
"aff.toast.save.pending": "Enregistrement de la demande d'affiliation en cours",
"aff.toast.save.success": "Demande d'affiliation enregistrée avec succès 🎉",
"aff.toast.save2.error": "Échec de l'enregistrement de l'affiliation",
"aff.toast.save2.pending": "Enregistrement de l'affiliation en cours",
"aff.toast.save2.success": "Affiliation enregistrée avec succès 🎉",
"aff_req.appuyerSurRechercher": "Appuyer sur rechercher pour compléter",
"aff_req.association": "L'association",
"aff_req.button.cancel": "Annuler ma demande",
"aff_req.button.confirm": "Confirmer ma demande d'affiliation",
"aff_req.button.save": "Enregistrer les modifications",
"aff_req.denomination": "Dénomination",
"aff_req.disposeLicence": "Dispose déjà d'une licence",
"aff_req.error1": "Le rôle du membre {{i}} est obligatoire",
"aff_req.error2": "Le format du SIRET/RNA est invalide",
"aff_req.nomDeLassociation": "Nom de l'association",
"aff_req.text1": "L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée suivante.",
"aff_req.text2": "Pour saffilier, une association sportive doit réunir les conditions suivantes :",
"aff_req.text2.li": [
"Avoir son siège social en France ou Principauté de Monaco",
"Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport",
"Poursuivre un objet social entrant dans la définition de larticle 1 des statuts de la Fédération",
"Disposer de statuts compatibles avec les principes dorganisation et de fonctionnement de la Fédération",
"Assurer en son sein la liberté dopinion et le respect des droits de la défense, et sinterdire toute discrimination",
"Respecter les règles dencadrement, dhygiène et de sécurité établies par les règlements de la Fédération"
],
"aff_req.text3": "Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
"aff_req.text4": "Notez que pour finaliser votre affiliation, il vous faudra :",
"aff_req.text4.li1": "Disposer dau moins trois membres licenciés, dont le président",
"aff_req.text4.li2": "S'être acquitté des cotisations prévues par les règlements fédéraux",
"aff_req.text5": "Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux d'entrainement",
"aff_req.text6": "Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)",
"aff_req.text7": "Demande d'affiliation envoyée avec succès",
"aff_req.text8": "Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF",
"aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation",
"aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours",
"aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉",
"afficherLétatDesAffiliation": "Afficher l'état des affiliation",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation n°{{no}}",
"ajout": "Ajout",
"ajouterUnClub": "Ajouter un club",
"ajouterUnMembre": "Ajouter un membre",
"all_season": "--- tout les saisons ---",
"au": "au",
"aucun": "Aucun",
"aucunMembreSélectionné": "Aucun membre sélectionné",
"back": "« retour",
"blason": "Blason",
"bureau": "Bureau",
"button.accepter": "Accepter",
"button.ajouter": "Ajouter",
"button.annuler": "Annuler",
"button.appliquer": "Appliquer",
"button.confirmer": "Confirmer",
"button.créer": "Créer",
"button.enregister": "Enregister",
"button.enregistrer": "Enregistrer",
"button.fermer": "Fermer",
"button.modifier": "Modifier",
"button.refuser": "Refuser",
"button.seDésinscrire": "Se désinscrire",
"button.suivant": "Suivant",
"button.supprimer": "Supprimer",
"cat.benjamin": "Benjamin",
"cat.cadet": "Cadet",
"cat.catégorieInconnue": "Catégorie inconnue",
"cat.junior": "Junior",
"cat.miniPoussin": "Mini Poussin",
"cat.minime": "Minime",
"cat.poussin": "Poussin",
"cat.senior1": "Senior 1",
"cat.senior2": "Senior 2",
"cat.superMini": "Super Mini",
"cat.vétéran1": "Vétéran 1",
"cat.vétéran2": "Vétéran 2",
"categorie": "categorie",
"catégorie": "Catégorie",
"certificatMédical": "Certificat médical",
"chargement...": "Chargement...",
"chargerLexcel": "Charger l'Excel",
"chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.",
"choisir...": "Choisir...",
"club.aff_renew.msg": "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)",
"club.change.status": "Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.",
"club.contact.tt": {
"AUTRE": "Autre contact du club",
"COURRIEL": "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
"FACEBOOK": "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
"INSTAGRAM": "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
"SITE": "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
"TELEPHONE": "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55"
},
"club.toast.aff.error": "Impossible de charger les affiliations",
"club.toast.aff.pending": "Chargement des affiliations en cours",
"club.toast.aff.success": "Affiliations chargées avec succès 🎉",
"club.toast.del.error": "Échec de la suppression du club",
"club.toast.del.pending": "Suppression du club en cours",
"club.toast.del.success": "Club supprimé avec succès 🎉",
"club.toast.new.error": "Échec de la création du club",
"club.toast.new.pending": "Création du club en cours",
"club.toast.new.success": "Club créé avec succès 🎉",
"club.toast.save.error": "Échec de l'enregistrement du club",
"club.toast.save.pending": "Enregistrement du club en cours",
"club.toast.save.success": "Club enregistré avec succès 🎉",
"clubExterne": "Club externe",
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "Sans club",
"combattant": "combattant",
"comp.aff.blason": "Afficher le blason du club sur les écrans",
"comp.aff.flag": "Afficher le pays du combattant sur les écrans",
"comp.ajoutRapide": "Ajout rapide",
"comp.ajouterUnCombattant": "Ajouter un combattant",
"comp.ajouterUnInvité": "Ajouter un invité",
"comp.billetterie": "Billetterie",
"comp.billetterieHelloasso": "Billetterie HelloAsso",
"comp.catégorieNormalisée": "Catégorie normalisée",
"comp.combattantNonTrouvé": "Combattant non trouvé",
"comp.combattantsInscrits": "Combattants inscrits",
"comp.compétitionFuture": "Compétition future",
"comp.compétitionPassée": "Compétition passée",
"comp.créationCompétition": "Création compétition",
"comp.dateDinscription": "Date d'inscription",
"comp.editionCompétition": "Edition compétition",
"comp.error1": "La date de fin doit être postérieure à la date de début.",
"comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.",
"comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.",
"comp.exporterLesInscription": "Exporter les inscription",
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué",
"comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.",
"comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.",
"comp.ha.error3": "Veuillez renseigner l'email de réception des inscriptions échouées.",
"comp.ha.tarifsHelloasso": "Tarifs HelloAsso",
"comp.ha.text1": "Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :",
"comp.ha.text2": "<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> > <strong>Paramètres</strong> > <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.",
"comp.ha.text3": "<strong>Copier-coller le nom exacte des tarifs</strong> <em>-sépare par des point-virgules-</em> qui donneront lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3, cliquer sur <strong>+ Ajouter une information</strong>, saisissez l'intituler exact suivant <strong> Numéro de licence</strong>, dans <strong>Type de réponse souhaitée</strong> rentrer Nombre, sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.",
"comp.ha.text4": "<strong>Copier-coller l'url de votre billetterie</strong> dans le champs si dessous. Il devrais avoir la forme suivante: https://www.helloasso.com/associations/__nom-asso-sur-helloasso__/evenements/__nom-billetterie__",
"comp.ha.text5": "Url de la billetterie HelloAsso",
"comp.ha.text6": "Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour vous en informer",
"comp.informationsGénéralesSurLaCompétition": "Informations générales sur la compétition",
"comp.informationsSurLeModeDinscription": "Informations sur le mode d'inscription",
"comp.informationsTechniques": "Informations techniques",
"comp.inscription": "Inscription",
"comp.inscriptionModeAdministrateur": "Inscription - mode administrateur",
"comp.inscriptionsLibres": "Inscriptions libres",
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition",
"comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club",
"comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso",
"comp.modal.information": "Information",
"comp.modal.poids": "Poids (en kg)",
"comp.modal.recherche": "Recherche*",
"comp.modal.surclassement": "Surclassement",
"comp.modal.text1": "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.",
"comp.modal.text2": "Empêcher les membres/club de modifier cette inscription",
"comp.modifierLesParticipants": "Voir/Modifier les participants",
"comp.monInscription": "Mon inscription",
"comp.noDeLicence": "N° de licence",
"comp.nouvelleCompétition": "Nouvelle compétition",
"comp.organisateur": "Organisateur",
"comp.quiPeutInscrire": "Qui peut inscrire",
"comp.reg.libres": "Libres",
"comp.reg.parLesAdministrateursDeLaCompétition": "Par les administrateurs de la compétition",
"comp.reg.parLesResponsablesDeClub": "Par les responsables de club",
"comp.reg.surLaBilletterieHelloasso": "Sur la billetterie HelloAsso",
"comp.responsablesEtBureauxDesAssociations": "Responsables et bureaux des associations",
"comp.sinscrire": "S'inscrire",
"comp.supprimerLaCompétition": "Supprimer la compétition",
"comp.supprimerLaCompétition.msg": "Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?",
"comp.surclassement_one": "{{cat}} avec 1 de surclassement",
"comp.surclassement_other": "{{cat}} avec {{count}} de surclassements",
"comp.surclassement_zero": "{{cat}}",
"comp.text2": "Visible par le public (Apparaît dans la liste des compétitions)",
"comp.text3": "Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.",
"comp.tips": "Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition. Pour cela, cliquez sur la petite <1/> à côté de son nom.<br/>Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.",
"comp.toast.del.error": "Échec de la suppression de la compétition",
"comp.toast.del.pending": "Suppression de la compétition en cours",
"comp.toast.del.success": "Compétition supprimée avec succès 🎉",
"comp.toast.params.error": "Échec de la mise à jours des paramètres de la compétition",
"comp.toast.params.pending": "Mise à jours des paramètres de la compétition en cours...",
"comp.toast.params.success": "Paramètres de la compétition mis à jours avec succès 🎉",
"comp.toast.register.add.error": "Combattant non trouvé",
"comp.toast.register.add.pending": "Recherche en cours",
"comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour",
"comp.toast.register.ban.error": "Erreur",
"comp.toast.register.ban.pending": "Désinscription en cours",
"comp.toast.register.ban.success": "Combattant désinscrit et bannie",
"comp.toast.register.del.error": "Erreur",
"comp.toast.register.del.pending": "Désinscription en cours",
"comp.toast.register.del.success": "Combattant désinscrit",
"comp.toast.register.self.add.error": "Erreur lors de l'inscription",
"comp.toast.register.self.add.pending": "Inscription en cours",
"comp.toast.register.self.add.success": "Inscription réalisée 🎉",
"comp.toast.register.self.del.error": "Erreur lors de la désinscription",
"comp.toast.register.self.del.pending": "Désinscription en cours",
"comp.toast.register.self.del.success": "Désinscription réalisée",
"comp.toast.save.error": "Échec de l'enregistrement de la compétition",
"comp.toast.save.pending": "Enregistrement de la compétition en cours",
"comp.toast.save.success": "Compétition enregistrée avec succès 🎉",
"comp.tousLesMembresDeLaFfsaf": "Tous les membres de la FFSAF",
"comp.typeDinscription": "Type d'inscription",
"comp.uniquementLesAdministrateursDeLaCompétition": "Uniquement les administrateurs de la compétition",
"comp.warn1": "Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)\"",
"comp.warn2": "Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas.",
"comp.warn3": "Êtes-vous sûr de vouloir désinscrire ce combattant ?",
"comp.warn4": "Êtes-vous sûr de vouloir vous désinscrire ?",
"comp_manage": "Compétitions Manager",
"competition_one": "Compétition",
"competition_other": "Compétitions",
"compte": "Compte",
"compétition": "Compétition",
"configuration": "Configuration",
"conserverLancienEmail": "Conserver l'ancien email",
"contactAdministratif": "Contact administratif",
"contactInterne": "Contact interne",
"contact_one": "Contact",
"contact_other": "Contacts",
"date": "Date",
"dateDeNaissance": "Date de naissance",
"days": [
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi",
"Dimanche"
],
"de": "de",
"demandeDaffiliationEnCours": "Demande d'affiliation en cours",
"demandeDeLicence": "Demande de licence ",
"demander": "Demander",
"description": "Description",
"dlAff": "Téléchargée l'attestation d'affiliation",
"donnéesAdministratives": "Données administratives",
"du": "Du",
"dun": "d'un",
"définirLidDuCompte": "Définir l'id du compte",
"editionDeL'affiliation": "Edition de l'affiliation",
"editionDeLaDemande": "Edition de la demande ",
"editionDeLaLicence": "Edition de la licence",
"editionDeLaSéléction": "Edition de la séléction",
"editionDeLadresse": "Edition de l'adresse",
"email": "Email",
"en": "en",
"enAttente": "En attente",
"erreurDePaiement": "Erreur de paiement😕",
"erreurDePaiement.detail": "Message d'erreur :",
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.",
"espaceAdministration": "Espace administration",
"f": "F",
"faitPar": "Fait par",
"femme": "Femme",
"filtre": "Filtre",
"genre": "Genre",
"gestionGroupée": "Gestion groupée",
"gradeDarbitrage": "Grade d'arbitrage",
"h": "H",
"home": {
"header1": "Pour les licenciés",
"header2": "Pour les clubs",
"text1": "Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que le club organisateur les ait renseignés.<br/><br/>Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé une fois votre licence validée par le secrétariat.",
"text2": "C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le site ffsaf.fr.<br/>Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi que d'enregistrer les résultats.<br/><br/> Vous n'êtes pas encore affilié à la fédération ? Cliquez <1>içi</1> pour faire votre première demande.",
"welcome_message": "Bienvenue sur lintranet de la Fédération France Soft Armored Fighting"
},
"homme": "Homme",
"horairesD'entraînements": "Horaires d'entraînements",
"information": "Information",
"invité": "invité",
"keepEmpty": "Laissez vide pour ne rien changer.",
"le": "le",
"licence": "Licence",
"licenceNo": "Licence n°{{no}}",
"lieu": "Lieu",
"lieuxDentraînements": "Lieux d'entraînements",
"loading": "Chargement...",
"me": {
"result": {
"PRIVATE": "Privé (visible uniquement par moi)",
"PUBLIC": "Public (visible par tous)",
"REGISTERED_ONLY": "Membres connectés (visibles par les membres de la fédération)",
"REGISTERED_ONLY_NO_DETAILS": "Membres connectés - masquer les détails (visibles par les membres de la fédération)"
},
"toast.settings.error": "Échec de la mise à jours des paramètres 😕",
"toast.settings.pending": "Mise à jours des paramètres en cours...",
"toast.settings.success": "Paramètres mis à jours avec succès 🎉"
},
"me.changerMonMotDePasse": "Changer mon mot de passe",
"me.formationDarbitrage": "Formation d'arbitrage",
"me.paramètresDuCompte": "Paramètres du compte",
"me.rôleAuSienDuClub": "Rôle au sien du club",
"me.visibilitéDesRésultats": "Visibilité des résultats",
"member_one": "Membre",
"member_other": "Membres",
"membre.emailVideàLaLigne": "Email vide à la ligne {{no}}",
"membre.emailVérifié": "Email vérifié",
"membre.filtre.inactif": "Afficher les combattants inactifs",
"membre.filtre.licence": "Afficher l'état des licences",
"membre.filtre.licences": [
"Sans demande ni licence validée",
"Avec demande ou licence validée",
"Demande en cours",
"Licence validée",
"Tout les états de licences",
"Demande complet",
"Demande incomplet"
],
"membre.filtre.payement": [
"Sans paiement",
"Avec paiement",
"Tout les états de paiement"
],
"membre.identifiant": "Identifiant",
"membre.import.err1": "Format de la date de certificat invalide à la ligne {{no}}",
"membre.import.err2": "Format de la date de certificat invalide à la ligne {{no}}",
"membre.import.err3": "Date de naissance vide à la ligne {{no}}",
"membre.import.err4": "Format de la date de naissance invalide à la ligne {{no}}",
"membre.import.err5": "Email invalide à la ligne {{no}}",
"membre.import.errTT_one": "{{count}} erreur dans le fichier, opération annulée",
"membre.import.errTT_other": "{{count}} erreurs dans le fichier, opération annulée",
"membre.import.warn_one": "{{count}} certificat médical non rempli",
"membre.import.warn_other": "{{count}} certificats médicaux non remplis",
"membre.info.emailInfo": "L'email sert à la création de compte pour se connecter au site et doit être unique.<br/>Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+<caractères alphanumériques>@exemple.com'}.<br/>Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com",
"membre.info.error1": "Veuillez sélectionner un pays valide 😕",
"membre.info.error2": "Veuillez sélectionner un club valide 😕",
"membre.initaccount": "Initialiser le compte",
"membre.initaccount.text1": "Entré l'UUID du compte",
"membre.initaccount.text2": "Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...",
"membre.noAccount": "Ce membre ne dispose pas de compte...",
"membre.noAccount.clubMsg": "Un compte sera créé par la fédération lors de la validation de sa première licence",
"membre.nomVideàLaLigne": "Nom vide à la ligne {{no}}",
"membre.prénomVideàLaLigne": "Prénom vide à la ligne {{no}}",
"membre.toast.compte.created": "Compte créé avec succès 🎉",
"membre.toast.compte.error": "Échec de la création du compte",
"membre.toast.compte.pending": "Création du compte en cours",
"membre.toast.del.error": "Échec de la suppression du compte",
"membre.toast.del.pending": "Suppression du compte en cours",
"membre.toast.del.success": "Compte supprimé avec succès 🎉",
"membre.toast.id.error": "Échec de la définition de l'identifient",
"membre.toast.id.pending": "Définition de l'identifient en cours",
"membre.toast.id.success": "Identifient défini avec succès 🎉",
"membre.toast.licence.ask.del.error": "Échec de la suppression de la demande de licence",
"membre.toast.licence.ask.del.pending": "Suppression de la demande de licence en cours",
"membre.toast.licence.ask.del.success": "Demande de licence supprimée avec succès 🎉",
"membre.toast.licence.ask.error": "Échec de la demande de licence",
"membre.toast.licence.ask.pending": "Enregistrement de la demande de licence en cours",
"membre.toast.licence.ask.success": "Demande de licence enregistrée avec succès 🎉",
"membre.toast.licence.del.error": "Échec de la suppression de la licence",
"membre.toast.licence.del.pending": "Suppression de la licence en cours",
"membre.toast.licence.del.success": "Licence supprimée avec succès 🎉",
"membre.toast.licence.save.error": "Échec de l'enregistrement de la licence",
"membre.toast.licence.save.pending": "Enregistrement de la licence en cours",
"membre.toast.licence.save.success": "Licence enregistrée avec succès 🎉",
"membre.toast.licences.export.error": "Échec de l'export des licences",
"membre.toast.licences.export.pending": "Export des licences en cours",
"membre.toast.licences.export.success": "Licences exportées avec succès 🎉",
"membre.toast.licences.import.error": "Échec de l'envoie des changements",
"membre.toast.licences.import.pending": "Envoie des changements en cours",
"membre.toast.licences.import.success": "Changements envoyés avec succès 🎉",
"membre.toast.licences.load.error": "Impossible de charger les licences",
"membre.toast.licences.load.pending": "Chargement des licences en cours",
"membre.toast.licences.load.success": "Licences chargées avec succès 🎉",
"membre.toast.perm.error": "Échec de la mise à jours des permissions 😕",
"membre.toast.perm.pending": "Mise à jours des permissions en cours...",
"membre.toast.perm.success": "Permission mise à jours avec succès 🎉",
"membre.toast.save.error": "Échec de la mise à jours du profil 😕",
"membre.toast.save.pending": "Mise à jours du profil en cours...",
"membre.toast.save.success": "Profil mis à jours avec succès 🎉",
"membre.toast.select.del.error": "Échec de la suppression de la séléction",
"membre.toast.select.del.pending": "Suppression de la séléction en cours",
"membre.toast.select.del.success": "Séléction supprimée avec succès 🎉",
"membre.toast.select.save.error": "Échec de l'enregistrement de la séléction",
"membre.toast.select.save.pending": "Enregistrement de la séléction en cours",
"membre.toast.select.save.success": "Séléction enregistrée avec succès 🎉",
"mettreàJours": "Mettre à jours",
"modification": "Modification",
"nationalité": "Nationalité",
"nav": {
"account": "Mon compte",
"aff_request": "Demande d'affiliation",
"club": {
"my": "Mon club"
},
"competitions": {
"results": "Mes résultats"
},
"home": "Accueil",
"login": "Connexion",
"logout": "Déconnexion",
"space": "Mon espace",
"title": "FFSAF Intranet"
},
"noLicence": "N° Licence",
"noSiretOuRna": "N° $t(siretOuRna)",
"nom": "Nom",
"nombreDeLicences": "Nombre de licences",
"nombreDeLicencesParCatégorie": "Nombre de licences par catégorie pour {{saison}}",
"non": "Non",
"nonDéfinie": "Non définie",
"nonValidée": "Non validée",
"nouveauClub": "Nouveau club",
"nouveauMembre": "Nouveau membre",
"nouvelEmail": "Nouvel email",
"ou": "Ou",
"oui": "Oui",
"outdated_session": {
"login_button": "Se reconnecter",
"message": "Votre session a expirée, veuillez vous reconnecter pour continuer à utiliser l'application.",
"title": "Session expirée"
},
"pageClub": "Page club",
"pageMembre": "Page membre",
"pageNouveauClub": "Page nouveau club",
"page_info_full": "Ligne {{line}} à {{tt_line}} (page {{page}} sur {{tt_page}})",
"page_info_ligne": "{{show}} ligne(s) affichée(s) sur {{total}}",
"paiementDeLaLicence": "Paiement de la licence",
"paiementDesLicences": "Paiement des licences",
"par": "par",
"pasDeLicence": "Pas de licence",
"payment.ha.info": "Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à lassociation choisie. Vous pouvez soutenir laide quils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.",
"payment.info": "A propos de HelloAsso",
"payment.paiementSécurisé": "Paiement sécurisé",
"payment.payerAvec": "Payer avec",
"payment.recap": "{{count}} licence(s) sélectionnée<br/>Total à régler : {{total}} €",
"paymentDesLicences": "Payment des licences",
"paymentDesLicences.msg_one": "Êtes-vous sûr de vouloir marquer comme payées la licence ?",
"paymentDesLicences.msg_other": "Êtes-vous sûr de vouloir marquer comme payées les {{count}} licences ?",
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
"paymentOk": "🎉Votre paiement a été traité avec succès.🎉",
"paymentOk.msg": "Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.",
"pays": "Pays",
"perm.administrateurDeLaFédération": "Administrateur de la fédération",
"perm.créerDesCompétion": "Créer des compétion",
"perm.ffsafIntra": "FFSAF intra",
"permission": "Permission",
"photos": "Photos",
"prenom": "Prénom",
"prénomEtNom": "Prénom et nom",
"rechercher": "Rechercher",
"rechercher...": "Rechercher...",
"registration_one": "Inscription",
"registration_other": "Inscriptions",
"renouveler": "Renouveler",
"renouvellementDeLaffiliation": "Renouvellement de l'affiliation",
"result_one": "Résultat",
"result_other": "Résultats",
"retouràLaListeDeMembres": "Retour à la liste de membres",
"role": "Rôle",
"role.membre": "Membre",
"role.membreDuBureau": "Membre du bureau",
"role.président": "Président",
"role.secrétaire": "Secrétaire",
"role.trésorier": "Trésorier",
"role.vise-président": "Vise-Président",
"role.vise-secrétaire": "Vise-Secrétaire",
"role.vise-trésorier": "Vise-Trésorier",
"saison": "Saison",
"secrétariatsDeLice": "Secrétariats de lice",
"selectionner...": "Sélectionner...",
"siretOuRna": "SIRET ou RNA",
"stats": "Statistiques",
"statue": "Statue",
"status": "Status",
"statuts": "Statuts",
"supprimerLeClub": "Supprimer le club",
"supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?",
"supprimerLeCompte": "Supprimer le compte",
"supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?",
"sélectionEnéquipeDeFrance": "Sélection en équipe de France",
"sélectionner...": "Sélectionner...",
"toast.edit.error": "Échec de l'enregistrement des modifications",
"toast.edit.pending": "Enregistrement des modifications en cours",
"toast.edit.success": "Modifications enregistrées avec succès 🎉",
"toast.licence.bulk.pay.error": "Échec du marquage des licences comme payées",
"toast.licence.bulk.pay.pending": "Marquage des licences comme payées en cours",
"toast.licence.bulk.pay.success": "Licences marquées comme payées avec succès 🎉",
"toast.licence.bulk.valid.error": "Échec de la validation des licences",
"toast.licence.bulk.valid.pending": "Validation des licences en cours",
"toast.licence.bulk.valid.success": "Licences validées avec succès 🎉",
"toast.licence.order.error": "Échec de le création de la commande",
"toast.licence.order.pending": "Création de la commande en cours",
"toast.licence.order.success": "Commande créée avec succès 🎉",
"trie": "Trie",
"téléchargerLexcelDesMembres": "Télécharger l'Excel des membres",
"téléchargerLexcelDesMembres.info": "À utiliser comme template pour mettre à jour les informations",
"téléchargéeLaLicence": "Téléchargée la licence",
"validationDeLaLicence": "Validation de la licence",
"validationDesLicences": "Validation des licences",
"validerDesLicences": "Valider des licences",
"validerLePayement_one": "Valider le payement de la licence sélectionnée",
"validerLePayement_other": "Valider le payement des {{count}} licences sélectionnées",
"validerLePayement_zero": "$t(validerLePayement_other)",
"validerLicence.msg_one": "Êtes-vous sûr de vouloir valider la licence ?",
"validerLicence.msg_other": "Êtes-vous sûr de vouloir valider les {{count}} licences ?",
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
"validerLicence_one": "Valider la licence sélectionnée",
"validerLicence_other": "Valider les {{count}} licences sélectionnées",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validée",
"voir/modifierLesParticipants": "Voir/Modifier les participants",
"voirLesStatues": "Voir les statues",
"à": "à",
"étatDeLaDemande": "État de la demande"
}

View File

@@ -0,0 +1,67 @@
{
"--sélectionnerUnClub--": "--Sélectionner un club--",
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--",
"abs.": "abs.",
"adversaire": "Adversaire",
"aujourdhuià": "Aujourd'hui à {{time}}",
"avanthierà": "Avant-hier à {{time}}",
"back": "« retour",
"bleu": "Bleu",
"catégorie": "Catégorie",
"chargement": "Chargement",
"club": "Club",
"combattant": "Combattant",
"combattants": "Combattants",
"date": "Date",
"disc.": "disc.",
"défaites": "Défaites",
"erreurDeChargementDeLaListe": "Erreur de chargement de la liste",
"erreurDeChargementDeLaPoule": "Erreur de chargement de la poule",
"erreurDeChargementDesCatégories": "Erreur de chargement des catégories",
"erreurDeChargementDesClubs": "Erreur de chargement des clubs",
"erreurDeChargementDesCombattants": "Erreur de chargement des combattants",
"erreurDeChargementDuClub": "Erreur de chargement du club",
"erreurDeChargementDuCombattant": "Erreur de chargement du combattant",
"for.": "for.",
"hierà": "Hier à {{time}}",
"info": "Info",
"listeDesCombattants": "Liste des combattants",
"listeDesMatchs": "Liste des matchs",
"listeDesMembres": "Liste des membres",
"listeDesMenbres": "Liste des menbres",
"nom": "Nom",
"nomPrénom": "Nom Prénom",
"nombreDeMatchDisputé2": "Nombre de match disputé : {{nb}}",
"nombreDeVictoires2": "Nombre de victoires : {{nb}} ",
"nombreDinscris": "Nombre d'inscris",
"nombreDinscris2": "Nombre d'inscris : {{nb}}",
"parCatégorie": "Par catégorie",
"parClub": "Par club",
"parCombattant": "Par combattant",
"place": "Place",
"pointsMarqués": "Points marqués",
"pointsMarqués2": "Points marqués : {{nb}}",
"pointsReçus": "Points reçus",
"pointsReçus2": "Points reçus : {{nb}}",
"poule": "Poule",
"ratio": "Ratio",
"ratioDePointsMoyen2": "Ratio de points moyen : {{nb}}",
"ratioDeVictoiresMoyen2": "Ratio de victoires moyen : {{nb}}",
"ratioDuScore2": "Ratio du score (point marqué / point reçu): {{nb}}",
"ratioPoints": "Ratio points",
"ratioVictoires": "Ratio victoires",
"ratiosPoints": "Ratios points",
"rechercheParCatégorie": "Recherche par catégorie",
"rechercheParClub": "Recherche par club",
"rechercheParCombattant": "Recherche par combattant",
"rouge": "Rouge",
"résultatDeLaCompétition": "Résultat de la compétition",
"scores": "Scores",
"statistique": "Statistique",
"tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})",
"tournois": "Tournois",
"tousLesCombattants": "Tous les combattants",
"victoire": "Victoire",
"victoires": "Victoires"
}

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

@@ -15,6 +15,7 @@ import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
import {MePage} from "./pages/MePage.jsx";
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
import {useTranslation} from "react-i18next";
const router = createBrowserRouter([
{
@@ -78,10 +79,12 @@ const router = createBrowserRouter([
function GetCompetitionMangerLazy() {
const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx'))
const {t} = useTranslation();
return <Suspense
fallback={<div>
<h1>Compétition manager</h1>
<p>Chargement...</p>
<h1>{t("comp_manage")}</h1>
<p>{t("loading")}</p>
</div>}>
<CMLazy/>
</Suspense>
@@ -143,6 +146,7 @@ function Root() {
function ReAuthMsg() {
const {is_authenticated} = useAuth()
const location = useLocation()
const {t} = useTranslation();
const notAuthPaths = [
/^\/$/s,
@@ -160,15 +164,14 @@ function ReAuthMsg() {
}}>
<div className="card">
<div className="card-header">
<h5>Session expirée</h5>
<h5>{t("outdated_session.title")}</h5>
</div>
<div className="card-body">
<p className="card-text">Votre session a expirée, veuillez vous reconnecter pour continuer à
utiliser l'application.</p>
<p className="card-text">{t("outdated_session.message")}</p>
</div>
<div className="card-footer">
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>Se reconnecter</button>
<a className="btn btn-secondary" href="/">Accueil</a>
<button className="btn btn-primary" onClick={() => login()} style={{marginRight: "0.5em"}}>{t("outdated_session.login_button")}</button>
<a className="btn btn-secondary" href="/">{t("nav.home")}</a>
</div>
</div>
</div>

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

@@ -2,19 +2,12 @@ import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faCircleQuestion, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
export function ContactEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
const tooltipText = {
SITE: "Site web du club avec ou sans le 'https://'</br>Exemple: ffsaf.fr</br>Ou https://ffsaf.fr",
FACEBOOK: "Page Facebook du club débutant par 'https://www.facebook.com/'</br>Exemple: https://www.facebook.com/ffmsf",
TELEPHONE: "Numéro de téléphone du club<br>Exemple: 06 12 13 78 55",
INSTAGRAM: "Compte Instagram du club débutant par 'https://www.instagram.com/'</br>Exemple: https://www.instagram.com/ff_msf",
COURRIEL: "Adresse e-mail du club<br>Exemple: contact@ffsaf.fr",
AUTRE: "Autre contact du club",
}
const {t} = useTranslation();
useEffect(() => {
let i = 0;
@@ -38,7 +31,7 @@ export function ContactEditor({data}) {
return <div className="row mb-3">
<input name="contact" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Contacts</span>
<span className="input-group-text">{t('contact', {count : 2})}</span>
<ul className="list-group form-control">
{state.map((d, index) => {
if (d.data === undefined || d.data.value === undefined)
@@ -62,7 +55,7 @@ export function ContactEditor({data}) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: {type: d.data.type, value: e.target.value}}})
}}/>
<button type="button" className="btn btn-outline-info" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-title={tooltipText[d.data.type]} data-bs-html="true">
data-bs-title={t("club.contact.tt." + d.data.type)} data-bs-html="true">
<FontAwesomeIcon icon={faCircleQuestion}/>
</button>
<button className="btn btn-danger" type="button"

View File

@@ -2,6 +2,7 @@ import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
function timeNumberToSting(nbMin) {
return String(Math.floor(nbMin / 60)).padStart(2, '0') + ":" + String(nbMin % 60).padStart(2, '0')
@@ -15,6 +16,7 @@ function timeStringToNumber(time) {
export function HoraireEditor({data}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
const {t} = useTranslation();
useEffect(() => {
if (data.training_day_time === null)
@@ -38,7 +40,7 @@ export function HoraireEditor({data}) {
return <div className="row mb-3">
<input name="training_day_time" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Horaires d'entraînements</span>
<span className="input-group-text">{t("horairesD'entraînements")}</span>
<ul className="list-group form-control">
{state.map((d, index) => {
return <div key={index} className="input-group">
@@ -48,22 +50,22 @@ export function HoraireEditor({data}) {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire})
}}>
<option value="0">Lundi</option>
<option value="1">Mardi</option>
<option value="2">Mercredi</option>
<option value="3">Jeudi</option>
<option value="4">Vendredi</option>
<option value="5">Samedi</option>
<option value="6">Dimanche</option>
<option value="0">{t('days.0')}</option>
<option value="1">{t('days.1')}</option>
<option value="2">{t('days.2')}</option>
<option value="3">{t('days.3')}</option>
<option value="4">{t('days.4')}</option>
<option value="5">{t('days.5')}</option>
<option value="6">{t('days.6')}</option>
</select>
<span className="input-group-text">de</span>
<span className="input-group-text">{t('de')}</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_start)} required
onChange={(e) => {
d.data.time_start = timeStringToNumber(e.target.value)
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d.data}})
dispatch({type: 'SORT', payload: sortHoraire})
}}/>
<span className="input-group-text">à</span>
<span className="input-group-text">{t('à')}</span>
<input type="time" className="form-control" value={timeNumberToSting(d.data.time_end)} required
onChange={(e) => {
d.data.time_end = timeStringToNumber(e.target.value)

View File

@@ -5,10 +5,12 @@ import proj4 from "proj4";
import {useFetch} from "../../hooks/useFetch.js";
import {MapContainer, Marker, TileLayer} from "react-leaflet";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {useTranslation} from "react-i18next";
export function LocationEditor({data, setModal, sendData}) {
const [state, dispatch] = useReducer(SimpleReducer, [])
const [out_data, setOutData] = useState({})
const {t} = useTranslation();
useEffect(() => {
if (data.training_location === null)
@@ -42,7 +44,7 @@ export function LocationEditor({data, setModal, sendData}) {
return <div className="row mb-3">
<input name="training_location" value={JSON.stringify(out_data)} readOnly hidden/>
<span className="input-group-text">Lieux d'entraînements</span>
<span className="input-group-text">{t('lieuxDentraînements')}</span>
<ul className="list-group form-control">
{state.map((d, index) => {
return <div key={index} className="input-group">
@@ -83,8 +85,9 @@ export function LocationEditorModal({modal, sendData}) {
const [location, setLocation] = useState("")
const [locationObj, setLocationObj] = useState({text: "", lng: undefined, lat: undefined})
const [mapPosition, setMapPosition] = useState([46.652195, 2.430226])
const {data, error, refresh} = useFetch(null)
const {data, refresh} = useFetch(null)
const map = useRef(null)
const {t} = useTranslation();
useEffect(() => {
if (modal.data !== undefined) {
@@ -136,7 +139,7 @@ export function LocationEditorModal({modal, sendData}) {
<form onSubmit={e => sendData.current(e)}>
<div className="modal-content">
<div className="modal-header">
<h1 className="modal-title fs-5" id="EditModalLabel">Edition de l'adresse</h1>
<h1 className="modal-title fs-5" id="EditModalLabel">{t('editionDeLadresse')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
@@ -147,7 +150,7 @@ export function LocationEditorModal({modal, sendData}) {
<input name="loc_lng" value={locationObj.lng ? locationObj.lng : -142} readOnly hidden/>
<div className="row">
<div className="input-group mb-3">
<label className="input-group-text">Adresse</label>
<label className="input-group-text">{t('adresse')}</label>
<input className="form-control" aria-autocomplete="list" aria-expanded="true"
placeholder="Chercher une adresse..." aria-label="Recherche" list="addr" value={location}
onChange={e => setLocation(e.target.value)}/>
@@ -174,9 +177,9 @@ export function LocationEditorModal({modal, sendData}) {
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal"
disabled={locationObj.lng === undefined}>Enregistrer
disabled={locationObj.lng === undefined}>{t('button.enregistrer')}
</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
</div>
</div>
</form>

View File

@@ -1,6 +1,7 @@
import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
import {useFetch} from "../hooks/useFetch.js";
import {AxiosError} from "./AxiosError.jsx";
import {useTranslation} from "react-i18next";
export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
return <LoadingProvider>
@@ -11,6 +12,7 @@ export function ClubSelect({defaultValue, name, na = false, disabled = false}) {
function ClubSelect_({defaultValue, name, na, disabled}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/no_detail`, setLoading, 1)
const {t} = useTranslation();
return <>
{data
@@ -18,8 +20,8 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
<label className="input-group-text" id="inputGroupSelect02">Club</label>
<select className="form-select" id="inputGroupSelect02" disabled={disabled}
defaultValue={defaultValue ? defaultValue : -1} name={name}>
<option>Sélectionner...</option>
{na && <option value={-1}>-- Non licencier --</option>}
<option>{t('sélectionner...')}</option>
{na && <option value={-1}>{t('--NonLicencier--')}</option>}
{data.map(club => (<option key={club.id} value={club.id}>{club.name}</option>))}
</select>
</div>
@@ -31,11 +33,13 @@ function ClubSelect_({defaultValue, name, na, disabled}) {
}
function Def() {
const {t} = useTranslation();
return <div className="input-group mb-3">
<label className="input-group-text" id="inputGroupSelect02">Club</label>
<label className="input-group-text" id="inputGroupSelect02">{t("club", {count: 1})}</label>
<select className="form-select" id="inputGroupSelect02"
defaultValue="Chargement...">
<option>Chargement...</option>
defaultValue={t('chargement...')}>
<option>{t('chargement...')}</option>
</select>
</div>;
}

View File

@@ -1,5 +1,6 @@
import {Fragment} from "react";
import './ColoredCircle.css'
import i18n from "i18next";
export const ColoredCircle = ({color, boolean}) => {
const styles = {backgroundColor: '#F00'};
@@ -15,7 +16,7 @@ export const ColoredCircle = ({color, boolean}) => {
</Fragment>
};
export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => {
export const ColoredText = ({boolean, text={true: i18n.t('oui'), false: i18n.t('non')}}) => {
const styles = {color: '#F00'};
if (boolean !== undefined) {

View File

@@ -1,5 +1,7 @@
import {useTranslation} from "react-i18next";
export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) {
const {t} = useTranslation();
return <div className="modal fade" id={id} tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
@@ -10,8 +12,8 @@ export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel =
{message}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>Annuler</button>
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>Confirmer</a>
<button type="button" className="btn btn-default" data-dismiss="modal" data-bs-dismiss="modal" onClick={onCancel}>{t('button.annuler')}</button>
<a className="btn btn-danger btn-ok" data-bs-dismiss="modal" onClick={onConfirm}>{t('button.confirmer')}</a>
</div>
</div>
</div>

View File

@@ -1,11 +0,0 @@
export function Input({placeholder, value, onChange}) {
return <div>
<input
type="text"
className="form-control"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</div>
}

View File

@@ -1,83 +0,0 @@
import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "./AxiosError.jsx";
function SimpleReducer(datas, action) {
switch (action.type) {
case 'ADD':
return [
...datas,
action.payload
]
case 'REMOVE':
return datas.filter(data => data.id !== action.payload)
case 'UPDATE_OR_ADD':
const index = datas.findIndex(data => data.id === action.payload.id)
if (index === -1) {
return [
...datas,
action.payload
]
} else {
datas[index] = action.payload
return [...datas]
}
default:
throw new Error()
}
}
export function ListEditorTest() {
const [html, dispatch] = ListEditor(ListHTML)
useEffect(() => {
dispatch({type: 'UPDATE_OR_ADD', payload: {id: 1, content: "data in"}})
}, []);
return html
}
export function ListEditor(ListItem) {
const [modal, setModal] = useState({id: -1})
const [state, dispatch] = useReducer(SimpleReducer, [])
const sendAffiliation = (e) => {
dispatch({type: 'UPDATE_OR_ADD', payload: e})
}
return [<>
<ul className="list-group">
{state.map((d, index) => {
return <div key={index} className={"list-group-item d-flex justify-content-between align-items-start"}>
<ListItem data={d}/>
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
data-bs-target="#EditModal" onClick={_ => setModal(d)}>
<FontAwesomeIcon icon={faPen}/></button>
<button className="badge btn btn-danger rounded-pill"
onClick={() => dispatch({type: 'REMOVE', payload: d.id})}>
<FontAwesomeIcon icon={faTrashCan}/></button>
</div>
})}
</ul>
<div className="modal fade" id="EditModal" tabIndex="-1" aria-labelledby="EditModalLabel"
aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<form onSubmit={e => sendAffiliation(e, dispatch)}>
<input name="id" value={modal.id} readOnly hidden/>
</form>
<ModalContent affiliation={modalAffiliation} dispatch={dispatch}/>
</div>
</div>
</div>
</>
, dispatch]
}
function ListHTML({
data
}) {
return <div className="me-auto">{data.content}</div>
}

View File

@@ -1,11 +1,15 @@
import {useEffect, useState} from "react";
import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js";
import {useCountries} from "../hooks/useCountries.jsx";
import i18n from "../config/i18n.js";
import {useTranslation} from "react-i18next";
export function BirthDayField({inti_date, inti_category, required = true}) {
const [date, setDate] = useState(inti_date)
const [category, setCategory] = useState(inti_category)
const [canUpdate, setCanUpdate] = useState(false)
const {t} = useTranslation();
useEffect(() => {
const b = category !== getCategoryFormBirthDate(new Date(date))
if (b !== canUpdate)
@@ -18,19 +22,19 @@ export function BirthDayField({inti_date, inti_category, required = true}) {
return <>
<div className="input-group mb-3">
<span className="input-group-text" id="birth_date">Date de naissance</span>
<span className="input-group-text" id="birth_date">{t('dateDeNaissance')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
name="birth_date" aria-describedby="birth_date" defaultValue={date} required={required}
onChange={(e) => setDate(e.target.value)}/>
</div>
<div className="row">
<div className="input-group mb-3">
<span className="input-group-text" id="category">Catégorie</span>
<span className="input-group-text" id="category">{t('catégorie')}</span>
<input type="text" className="form-control" placeholder="" name="category"
aria-label="category" value={category ? getCatName(category) : ""} aria-describedby="category"
disabled/>
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={updateCat}>Mettre à jours</button>}
onClick={updateCat}>{t('mettreàJours')}</button>}
</div>
</div>
</>
@@ -52,14 +56,14 @@ export function OptionField({name, text, values, value, disabled = false}) {
export function RoleList({name, text, value, disabled = false}) {
return <OptionField name={name} text={text} value={value} disabled={disabled}
values={{
MEMBRE: 'Membre',
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire',
VPRESIDENT: 'Vise-Président',
VTRESORIER: 'Vise-Trésorier',
VSECRETAIRE: 'Vise-Secrétaire',
MEMBREBUREAU: 'Membre bureau'
MEMBRE: i18n.t('role.membre'),
PRESIDENT: i18n.t('role.président'),
TRESORIER: i18n.t('role.trésorier'),
SECRETAIRE: i18n.t('role.secrétaire'),
VPRESIDENT: i18n.t('role.vise-président'),
VTRESORIER: i18n.t('role.vise-trésorier'),
VSECRETAIRE: i18n.t('role.vise-secrétaire'),
MEMBREBUREAU: i18n.t('role.membreDuBureau')
}}/>
}
@@ -93,7 +97,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 +123,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>
};

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