Compare commits
147 Commits
dev-comp
...
babbc5d585
| Author | SHA1 | Date | |
|---|---|---|---|
| babbc5d585 | |||
| 51c774a21e | |||
| 9d85b9ae45 | |||
| 0ab0731ebd | |||
| ec48f73d9f | |||
| f98f1bd1ac | |||
| b9d752ac55 | |||
| 13650b27c1 | |||
| 0757ae7198 | |||
| a871b52006 | |||
| 78959fc485 | |||
| 0a91e72c29 | |||
| e3a1d1c50b | |||
| 3933954e09 | |||
| 959e356fb9 | |||
| 3b5955ff31 | |||
| e8ee5811a0 | |||
| 12326fe122 | |||
| 9624ff93f0 | |||
| 5444235512 | |||
| b54eaa2549 | |||
| 6a7af8aea9 | |||
| bc4dbb532f | |||
| 0e269e0bf7 | |||
| 6d6324e1b4 | |||
| 309f2a2268 | |||
| 13bae3e817 | |||
| c6751b0428 | |||
| 237952f95a | |||
| 075f01a205 | |||
| 82231744af | |||
| 71a4d98434 | |||
| 29a1184994 | |||
| 3d07a5bc8e | |||
| a730384d37 | |||
| 412e661319 | |||
| bc72b7912c | |||
| 6359e7e027 | |||
| c7a9cc221f | |||
| 17c61be171 | |||
| 76f69c2154 | |||
| 639f07ccdc | |||
| c02e37862c | |||
| 2b6dc48880 | |||
| 6879d9195d | |||
| 1595fb57c3 | |||
| 8067d496f8 | |||
| aba8d8a202 | |||
| 38663d97c2 | |||
| 377ce70125 | |||
| 61cee85335 | |||
| 19949d1e35 | |||
| 03cdcf78c8 | |||
| 0b4f843084 | |||
| fa00273c87 | |||
| b2438e52fc | |||
| 31bbe5719f | |||
| bf377c9c5e | |||
| 145d5e7ca8 | |||
| 448e49b9fd | |||
| b7a1e58436 | |||
| a73a3562c3 | |||
| 0dace76fc3 | |||
| f76ca43ed6 | |||
| c6b24a1b20 | |||
| c47c04c29e | |||
| e06df24002 | |||
| 352c34064f | |||
| 2ccd2a9178 | |||
| 0ff3ffd66b | |||
| 847674d62c | |||
| 8896e2a28a | |||
| 68b4f8e476 | |||
| d3fb884ab4 | |||
| 81faafe2de | |||
| ee730be570 | |||
| 4856326b29 | |||
| b94f4b43ce | |||
| 795d044c0b | |||
| dc5ab9c3c4 | |||
| 870891a77c | |||
| 7e1356b621 | |||
| b689521e56 | |||
| e81f475402 | |||
| 095a5fcede | |||
| 3decc6d0e0 | |||
| 254fd7d73f | |||
| 12a75d003c | |||
| 7f957b98f2 | |||
| 8b2c9613fd | |||
| 50b49c40e3 | |||
| 47d3921e05 | |||
| ecc9753237 | |||
| 7c4addd525 | |||
| a8565534e6 | |||
| 43fcbb5ef6 | |||
| fefc5d651b | |||
| 163d9b23e9 | |||
| 53509820c6 | |||
| a8ad3a7558 | |||
| f46e268d39 | |||
| abad3194a4 | |||
| 6e7eec0587 | |||
| 8c83668364 | |||
| 771c06ccd8 | |||
| b479b992cf | |||
| f018e52afa | |||
| be2f01c070 | |||
| 9ab50238b9 | |||
| cf5d93630f | |||
| e5e17d3862 | |||
| 7410569ced | |||
| b107f443aa | |||
| 8e2d68ebd5 | |||
| f050127fd7 | |||
| d02fd63834 | |||
| b143cc759f | |||
| cc5534ef00 | |||
| 26f56006f6 | |||
| f8dacee3e7 | |||
| ed1f30f2b6 | |||
| 8517e9824c | |||
| 6cec8ff31d | |||
| e7deba52e9 | |||
| d95c173fa8 | |||
| 8a0e4423f2 | |||
| b956236934 | |||
| 7767c98304 | |||
| 7e380ccb69 | |||
| 94d1148eb1 | |||
| 8a14f58ce5 | |||
| 79dbbdaaec | |||
| bf5704db54 | |||
| dec98f9508 | |||
| d3a62e980d | |||
| b89ed62795 | |||
| 5ffc9fb495 | |||
| 5e48bc4623 | |||
| 81b953fb05 | |||
| c6659f8d85 | |||
| e86fe42b3d | |||
| ef528aa524 | |||
| e6cc4cbc96 | |||
| c58dedf80a | |||
| 1908de681e | |||
| 76381c75bd | |||
| 7d281196c1 |
@@ -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
1
.gitignore
vendored
@@ -50,3 +50,4 @@ nb-configuration.xml
|
||||
/media/
|
||||
/media-ext/
|
||||
/sign.jpg
|
||||
/.gitea/workflows/test.yml
|
||||
|
||||
53
README.md
53
README.md
@@ -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/ .
|
||||
L’intranet de la Fédération française de Soft Armored Fighting a pour but centralise la prise de licences des adhérents
|
||||
et l’affiliation des clubs. Il permet aussi de recueillir les résultats des compétitions organisées par les clubs et,
|
||||
plus récemment, d’en faciliter l’organisation 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 d’affiliation ;
|
||||
* 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 d’affiliation.
|
||||
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 d’une poule. Il supporte plusieurs lices, assure la synchronisation en temps réel des modifications
|
||||
entre toutes les instances de l’application web et publie automatiquement les résultats sur le site de l’organisateur.
|
||||
|
||||
## Running the application in dev mode
|
||||
|
||||
@@ -51,35 +65,4 @@ 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)
|
||||
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
|
||||
18
pom.xml
18
pom.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class LogModel {
|
||||
}
|
||||
|
||||
public enum ObjectType {
|
||||
Membre, Affiliation, Licence, Club, Competition, Register
|
||||
Membre, Affiliation, Licence, Club, Competition, Register, Selection
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -64,6 +64,9 @@ public class AffiliationService {
|
||||
@Inject
|
||||
LoggerService ls;
|
||||
|
||||
@Inject
|
||||
VirusScannerService scanner;
|
||||
|
||||
@RestClient
|
||||
StateIdService stateIdService;
|
||||
|
||||
@@ -174,15 +177,12 @@ public class AffiliationService {
|
||||
LOGGER.debug("Affiliation Request Created");
|
||||
LOGGER.debug(form.toString());
|
||||
|
||||
// noinspection ResultOfMethodCallIgnored,ReactiveStreamsUnusedPublisher
|
||||
// noinspection ReactiveStreamsUnusedPublisher
|
||||
return pre_save(form, true)
|
||||
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
|
||||
"aff_request/logo")))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
|
||||
"aff_request/status")))
|
||||
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
|
||||
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
|
||||
.call(model -> reactiveMailer.send(
|
||||
Mail.withText("no-reply@ffsaf.fr",
|
||||
"[NOTIF] FFSAF - Nouvelle demande d'affiliation",
|
||||
@@ -245,11 +245,8 @@ public class AffiliationService {
|
||||
})
|
||||
.chain(model -> Panache.withTransaction(() -> repositoryRequest.persist(model)))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
|
||||
"aff_request/logo")))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
|
||||
"aff_request/status")))
|
||||
.invoke(m -> Utils.uploadFile(scanner, form.getLogo(), m.getId(), media, "aff_request/logo"))
|
||||
.invoke(m -> Utils.uploadFile(scanner, form.getStatus(), m.getId(), media, "aff_request/status"))
|
||||
.map(__ -> "Ok");
|
||||
}
|
||||
|
||||
@@ -319,13 +316,11 @@ public class AffiliationService {
|
||||
.recoverWithNull()
|
||||
.call(___ -> setMembre(form.new Member(3), club, req.getSaison()))))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom()
|
||||
.future(Utils.replacePhoto(form.getId(), form.getLogo(), media,
|
||||
"aff_request/logo")))
|
||||
.invoke(model -> Utils.uploadFile(scanner, form.getLogo(), form.getId(), media,
|
||||
"aff_request/logo"))
|
||||
.invoke(model -> Utils.uploadFile(scanner, form.getStatus(), form.getId(), media,
|
||||
"aff_request/status"))
|
||||
.onItem()
|
||||
.invoke(model -> Uni.createFrom()
|
||||
.future(Utils.replacePhoto(form.getId(), form.getStatus(), media,
|
||||
"aff_request/status")))
|
||||
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/logo",
|
||||
"ppClub"))
|
||||
.call(model -> Utils.moveMedia(form.getId(), model.getId(), media, "aff_request/status",
|
||||
@@ -355,19 +350,25 @@ public class AffiliationService {
|
||||
.map(c -> club));
|
||||
})
|
||||
.call(club -> reactiveMailer.send(
|
||||
Mail.withText(form.getM1_email(),
|
||||
"FFSAF - Acceptation de votre demande d'affiliation",
|
||||
String.format(
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Votre demande d'affiliation pour le club %s a été acceptée.
|
||||
Le numéro d'affiliation de votre club est le %d.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", club.getName(), club.getNo_affiliation())
|
||||
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
|
||||
Mail.withHtml(form.getM1_email(), "FFSAF - Creation de votre compte sur l'intranet",
|
||||
String.format(Utils.HTML_HEADER, "FFSAF - Creation de votre compte sur l'intranet") +
|
||||
String.format("""
|
||||
<p>Votre demande d'affiliation pour le club <span class="highlight">%s</span> a été acceptée.</p>
|
||||
<p>LLe numéro d'affiliation de votre club est le <span class="highlight">%d</span>.</p>
|
||||
""",
|
||||
club.getName(), club.getNo_affiliation()) +
|
||||
Utils.HTML_FOOTER
|
||||
)
|
||||
.setText(String.format("""
|
||||
Bonjour,
|
||||
|
||||
Votre demande d'affiliation pour le club %s a été acceptée.
|
||||
Le numéro d'affiliation de votre club est le %d.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", club.getName(), club.getNo_affiliation()))
|
||||
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
|
||||
.addTo(form.getM2_email(), form.getM3_email())
|
||||
));
|
||||
}
|
||||
@@ -458,21 +459,27 @@ public class AffiliationService {
|
||||
public Uni<?> deleteReqAffiliation(long id, String reason, boolean federationAdmin) {
|
||||
return repositoryRequest.findById(id)
|
||||
.call(aff -> federationAdmin ? reactiveMailer.send(
|
||||
Mail.withText(aff.getM1_email(),
|
||||
"FFSAF - Votre demande d'affiliation a été rejetée.",
|
||||
String.format(
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
|
||||
%s
|
||||
|
||||
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", aff.getName(), reason)
|
||||
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
|
||||
Mail.withHtml(aff.getM1_email(), "FFSAF - Votre demande d'affiliation a été rejetée.",
|
||||
String.format(Utils.HTML_HEADER, "FFSAF - Votre demande d'affiliation a été rejetée.") +
|
||||
String.format("""
|
||||
<p>Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):<br/>%s</p>
|
||||
<p>Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@ffsaf.fr">contact@ffsaf.fr</a>.</p>
|
||||
""",
|
||||
aff.getName(), reason) +
|
||||
Utils.HTML_FOOTER
|
||||
)
|
||||
.setText(String.format("""
|
||||
Bonjour,
|
||||
|
||||
Votre demande d'affiliation pour le club %s a été rejetée pour la/les raison(s) suivante(s):
|
||||
%s
|
||||
|
||||
Si vous rencontrez un problème ou si vous avez des questions, n'hésitez pas à nous contacter à l'adresse contact@ffsaf.fr.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", aff.getName(), reason))
|
||||
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("contact@ffsaf.fr")
|
||||
.addTo(aff.getM2_email(), aff.getM3_email())
|
||||
) : Uni.createFrom().nullItem())
|
||||
.chain(aff -> Panache.withTransaction(() -> repositoryRequest.delete(aff)))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import fr.titionfire.ffsaf.data.model.AffiliationModel;
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.repository.AffiliationRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.ClubRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.net2.ServerCustom;
|
||||
@@ -44,6 +45,9 @@ public class ClubService {
|
||||
@Inject
|
||||
ClubRepository repository;
|
||||
|
||||
@Inject
|
||||
AffiliationRepository affiliationRepository;
|
||||
|
||||
@Inject
|
||||
ServerCustom serverCustom;
|
||||
|
||||
@@ -234,7 +238,8 @@ public class ClubService {
|
||||
}
|
||||
return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append());
|
||||
}))
|
||||
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(clubModel) // update group in keycloak
|
||||
.call(clubModel -> nameChange.get() ? keycloakService.updateGroupFromClub(
|
||||
clubModel) // update group in keycloak
|
||||
: Uni.createFrom().nullItem())
|
||||
.invoke(membreModel -> SReqClub.sendIfNeed(serverCustom.clients,
|
||||
SimpleClubModel.fromModel(membreModel)))
|
||||
@@ -282,11 +287,13 @@ public class ClubService {
|
||||
combModel.setClub(null);
|
||||
combModel.setRole(RoleAsso.MEMBRE);
|
||||
}).toList())
|
||||
.call(list -> (list.isEmpty()) ? Uni.createFrom().voidItem() :
|
||||
.call(list -> list.stream().noneMatch(m -> m.getUserId() != null) ?
|
||||
Uni.createFrom().voidItem() :
|
||||
Uni.join().all(list.stream().filter(m -> m.getUserId() != null)
|
||||
.map(m -> keycloakService.clearUser(m.getUserId())).toList())
|
||||
.andCollectFailures())
|
||||
.chain(list -> Panache.withTransaction(() -> combRepository.persist(list)))
|
||||
.chain(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
Panache.withTransaction(() -> combRepository.persist(list)))
|
||||
.map(o -> club)
|
||||
)
|
||||
.call(clubModel -> (clubModel.getClubId() == null) ? Uni.createFrom()
|
||||
@@ -323,9 +330,11 @@ public class ClubService {
|
||||
}
|
||||
|
||||
public Uni<List<ClubMapData>> getMapData() {
|
||||
return repository.list("international", false).toMulti().flatMap(list -> Multi.createFrom().iterable(list))
|
||||
.call(clubModel -> Mutiny.fetch(clubModel.getContact()))
|
||||
.map(clubModel -> {
|
||||
return affiliationRepository.list("saison >= ?1 AND club.international = ?2", Utils.getSaison() - 1, false)
|
||||
.toMulti().flatMap(list -> Multi.createFrom().iterable(list))
|
||||
.call(affiliationModel -> Mutiny.fetch(affiliationModel.getClub().getContact()))
|
||||
.map(affiliationModel -> {
|
||||
ClubModel clubModel = affiliationModel.getClub();
|
||||
ClubMapData data = new ClubMapData();
|
||||
|
||||
data.setName(clubModel.getName());
|
||||
|
||||
@@ -98,7 +98,13 @@ public class CompetitionService {
|
||||
Cache cacheNoneAccess;
|
||||
|
||||
public Uni<CompetitionData> getById(SecurityCtx securityCtx, Long id) {
|
||||
return permService.hasViewPerm(securityCtx, id).map(CompetitionData::fromModelLight);
|
||||
return permService.hasViewPerm(securityCtx, id).map(cm -> {
|
||||
CompetitionData out = CompetitionData.fromModelLight(cm);
|
||||
if (cm.getAdmin() != null) {
|
||||
out.setCanEditRegisters(cm.getAdmin().stream().anyMatch(u -> u.equals(securityCtx.getSubject())));
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
|
||||
@@ -106,7 +112,8 @@ public class CompetitionService {
|
||||
return Uni.createFrom()
|
||||
.item(new CompetitionData(null, "", "", "", "", new Date(), new Date(),
|
||||
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
|
||||
null, "", "", null, true, "", "", "", ""));
|
||||
null, "", "", null, true, true,
|
||||
"", "", "", "", "{}"));
|
||||
}
|
||||
return permService.hasAdminViewPerm(securityCtx, id)
|
||||
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
|
||||
@@ -129,6 +136,14 @@ public class CompetitionService {
|
||||
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
|
||||
out.forEach(competition -> competition.setCanEdit(true));
|
||||
}))
|
||||
.chain(ids -> repository.list("id NOT IN ?1 AND ?2 IN admin", ids, securityCtx.getSubject())
|
||||
.map(cm -> {
|
||||
out.addAll(cm.stream().map(CompetitionData::fromModelLight).toList());
|
||||
out.forEach(competition -> competition.setCanEditRegisters(true));
|
||||
List<Long> ids2 = new ArrayList<>(ids);
|
||||
ids2.addAll(cm.stream().map(CompetitionModel::getId).toList());
|
||||
return ids2;
|
||||
}))
|
||||
.call(ids ->
|
||||
repository.list("id NOT IN ?1 AND (publicVisible = TRUE OR registerMode IN ?2)", ids,
|
||||
securityCtx.isClubAdmin() ? List.of(RegisterMode.FREE, RegisterMode.HELLOASSO,
|
||||
@@ -165,8 +180,9 @@ public class CompetitionService {
|
||||
public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
|
||||
CompetitionSystem system) {
|
||||
return repository.list("system = ?1", system)
|
||||
.chain(l -> Uni.join().all(l.stream().map(cm -> permService.hasTablePerm(securityCtx, cm)).toList())
|
||||
.andCollectFailures())
|
||||
.chain(l -> Uni.join().all(l.stream().map(cm ->
|
||||
permService.hasTablePerm(securityCtx, cm).onFailure().recoverWithNull()
|
||||
).toList()).andCollectFailures())
|
||||
.map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
|
||||
}
|
||||
|
||||
@@ -281,7 +297,7 @@ public class CompetitionService {
|
||||
public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
|
||||
String source) {
|
||||
if ("admin".equals(source))
|
||||
if (data.getLicence() != -1) { // not a guest
|
||||
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
|
||||
return permService.hasEditPerm(securityCtx, id)
|
||||
.chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname())
|
||||
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
|
||||
@@ -513,6 +529,64 @@ public class CompetitionService {
|
||||
.call(__ -> cache.invalidate(id));
|
||||
}
|
||||
|
||||
public Uni<SimpleCompetData> getInternalData(SecurityCtx securityCtx, Long id) {
|
||||
return permService.hasEditPerm(securityCtx, id)
|
||||
.invoke(Unchecked.consumer(cm -> {
|
||||
if (cm.getSystem() != CompetitionSystem.INTERNAL)
|
||||
throw new DBadRequestException("Competition is not INTERNAL");
|
||||
}))
|
||||
.chain(competitionModel -> {
|
||||
SimpleCompetData data = SimpleCompetData.fromModel(competitionModel);
|
||||
return vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
if (competitionModel.getAdmin() != null)
|
||||
data.setAdmin(
|
||||
competitionModel.getAdmin().stream().map(uuid -> keycloakService.getUserById(uuid))
|
||||
.filter(Optional::isPresent)
|
||||
.map(user -> user.get().getUsername())
|
||||
.toList());
|
||||
if (competitionModel.getTable() != null)
|
||||
data.setTable(
|
||||
competitionModel.getTable().stream().map(uuid -> keycloakService.getUserById(uuid))
|
||||
.filter(Optional::isPresent)
|
||||
.map(user -> user.get().getUsername())
|
||||
.toList());
|
||||
return data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<Void> setInternalData(SecurityCtx securityCtx, SimpleCompetData data) {
|
||||
return permService.hasEditPerm(securityCtx, data.getId())
|
||||
.invoke(Unchecked.consumer(cm -> {
|
||||
if (cm.getSystem() != CompetitionSystem.INTERNAL)
|
||||
throw new DBadRequestException("Competition is not INTERNAL");
|
||||
}))
|
||||
.chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
ArrayList<String> admin = new ArrayList<>();
|
||||
ArrayList<String> table = new ArrayList<>();
|
||||
for (String username : data.getAdmin()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
admin.add(opt.get().getId());
|
||||
}
|
||||
for (String username : data.getTable()) {
|
||||
Optional<UserRepresentation> opt = keycloakService.getUser(username);
|
||||
if (opt.isEmpty())
|
||||
throw new DBadRequestException("User " + username + " not found");
|
||||
table.add(opt.get().getId());
|
||||
}
|
||||
|
||||
cm.setAdmin(admin);
|
||||
cm.setTable(table);
|
||||
cm.setConfig(data.getConfigForInternal());
|
||||
|
||||
return cm;
|
||||
}))
|
||||
.chain(cm -> Panache.withTransaction(() -> repository.persist(cm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
public Uni<SimpleCompetData> getSafcaData(SecurityCtx securityCtx, Long id) {
|
||||
return permService.getSafcaConfig(id)
|
||||
.call(Unchecked.function(o -> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -156,24 +156,32 @@ public class KeycloakService {
|
||||
return oldEmail;
|
||||
}).call(oldEmail -> oldEmail == null || !enabled_email ? Uni.createFrom().item("") :
|
||||
reactiveMailer.send(
|
||||
Mail.withText(oldEmail,
|
||||
"FFSAF - Changement de votre adresse email",
|
||||
String.format(
|
||||
"""
|
||||
Bonjour,
|
||||
|
||||
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
|
||||
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
|
||||
|
||||
Ancienne adresse email : %s
|
||||
Nouvelle adresse email : %s
|
||||
|
||||
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", oldEmail, email)
|
||||
).setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
|
||||
Mail.withHtml(oldEmail, "FFSAF - Changement de votre adresse email",
|
||||
String.format(Utils.HTML_HEADER, "FFSAF - Changement de votre adresse email") +
|
||||
String.format("""
|
||||
<p>Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,<br/>
|
||||
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.</p>
|
||||
<p>Ancienne adresse email : <span class="highlight">%s</span><br/>Nouvelle adresse email : <span class="highlight">%s</span></p>
|
||||
<p>Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse <a href="mailto:support@ffsaf.fr">support@ffsaf.fr</a>.</p>
|
||||
""",
|
||||
oldEmail, email) +
|
||||
Utils.HTML_FOOTER
|
||||
)
|
||||
.setText(String.format("""
|
||||
Bonjour,
|
||||
|
||||
Suite à la modification de votre adresse email fournie lors de votre (ré)inscription à la FFSAF,
|
||||
vous allez recevoir dans les prochaines minutes un email de vérification de votre nouvelle adresse sur celle-ci.
|
||||
|
||||
Ancienne adresse email : %s
|
||||
Nouvelle adresse email : %s
|
||||
|
||||
Si vous n'avez pas demandé cette modification, veuillez contacter le support à l'adresse support@ffsaf.fr.
|
||||
|
||||
Cordialement,
|
||||
L'équipe de la FFSAF
|
||||
""", oldEmail, email))
|
||||
.setFrom("FFSAF <no-reply@ffsaf.fr>").setReplyTo("support@ffsaf.fr")
|
||||
).onFailure().invoke(e -> LOGGER.error("Fail to send email", e)));
|
||||
}
|
||||
|
||||
@@ -270,13 +278,33 @@ public class KeycloakService {
|
||||
.invoke(user -> membreModel.setUserId(user.getId()))
|
||||
.call(user -> updateRole(user.getId(), List.of("safca_user"), List.of()))
|
||||
.call(user -> enabled_email ? reactiveMailer.send(
|
||||
Mail.withText(user.getEmail(),
|
||||
"FFSAF - Creation de votre compte sur l'intranet",
|
||||
String.format(
|
||||
Mail.withHtml(user.getEmail(), "FFSAF - Creation de votre compte sur l'intranet",
|
||||
String.format(Utils.HTML_HEADER, "Création de votre compte intranet FFSAF") +
|
||||
String.format(
|
||||
"""
|
||||
<p>Suite à votre première inscription <span class="highlight">%s</span> à la <strong>Fédération France Soft Armored Fighting (FFSAF)</strong>, votre compte intranet a été créé.</p>
|
||||
<p>Ce compte vous permettra de :</p>
|
||||
<ul>
|
||||
<li>Consulter vos informations personnelles,</li>
|
||||
<li>Vous inscrire aux compétitions (bientôt disponible),</li>
|
||||
<li>Consulter les résultats des compétitions.</li>
|
||||
</ul>
|
||||
<p>L’intranet est accessible à l’adresse suivante :</p>
|
||||
<p style="text-align: center;"><a href="https://intra.ffsaf.fr" class="button">Accéder à l'intranet</a></p>
|
||||
<p>Votre nom d’utilisateur est : <span class="highlight">%s</span></p>
|
||||
<p>Pour définir votre mot de passe, rendez-vous sur l’intranet : <strong>Connexion > Mot de passe oublié ?</strong></p>
|
||||
<p>Si vous n’avez pas demandé cette inscription, veuillez contacter le support à l’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
|
||||
|
||||
@@ -84,6 +84,26 @@ public class LicenceService {
|
||||
));
|
||||
}
|
||||
|
||||
public Uni<?> validePaymentLicences(List<Long> ids) {
|
||||
Uni<String> uni = Uni.createFrom().nullItem();
|
||||
|
||||
for (Long id : ids) {
|
||||
uni = uni.chain(__ -> repository.find("membre.id = ?1 AND saison = ?2", id, Utils.getSaison()).firstResult()
|
||||
.chain(model -> {
|
||||
if (!model.isPay())
|
||||
ls.logUpdate("payment (admin) de la licence", model);
|
||||
return validatePayLicences(model);
|
||||
}))
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
return uni.call(__ -> ls.append());
|
||||
}
|
||||
|
||||
protected Uni<LicenceModel> validatePayLicences(LicenceModel model) {
|
||||
model.setPay(true);
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
}
|
||||
|
||||
public Uni<LicenceModel> setLicence(long id, LicenceForm form) {
|
||||
if (form.getId() == -1) {
|
||||
return combRepository.findById(id).chain(membreModel -> {
|
||||
@@ -121,7 +141,7 @@ public class LicenceService {
|
||||
}
|
||||
|
||||
private Function<MembreModel, Uni<?>> genLicenceNumberAndAccountIfNeed() {
|
||||
return membreModel -> ((membreModel.getLicence() <= 0) ?
|
||||
return membreModel -> ((membreModel.getLicence() == null || membreModel.getLicence() <= 0) ?
|
||||
sequenceRepository.getNextValueInTransaction(SequenceType.Licence)
|
||||
.invoke(i -> membreModel.setLicence(Math.toIntExact(i)))
|
||||
.chain(() -> combRepository.persist(membreModel))
|
||||
@@ -157,7 +177,7 @@ public class LicenceService {
|
||||
"Impossible de supprimer une licence pour laquelle un paiement est en cours");
|
||||
})))
|
||||
.call(model -> ls.logADelete(model))
|
||||
.chain(model -> repository.delete(model));
|
||||
.chain(model -> Panache.withTransaction(() -> repository.delete(model)));
|
||||
}
|
||||
|
||||
public Uni<LicenceModel> askLicence(long id, LicenceForm form, Consumer<MembreModel> checkPerm) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.net2.ServerCustom;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
|
||||
import fr.titionfire.ffsaf.net2.request.SReqComb;
|
||||
import fr.titionfire.ffsaf.rest.data.MeData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleLicence;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleMembreInOutData;
|
||||
import fr.titionfire.ffsaf.rest.data.*;
|
||||
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DInternalError;
|
||||
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
|
||||
import fr.titionfire.ffsaf.rest.from.UserSettingForm;
|
||||
import fr.titionfire.ffsaf.utils.*;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheQuery;
|
||||
@@ -125,8 +123,21 @@ public class MembreService {
|
||||
return sort;
|
||||
}
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, String club,
|
||||
int licenceRequest, int payState, String order, String categorie) {
|
||||
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
|
||||
String order, String categorie, Boolean archive, String subject) {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (membreModel == null || membreModel.getClub() == null)
|
||||
throw new DForbiddenException();
|
||||
}))
|
||||
.chain(membreModel ->
|
||||
searchAdmin(limit, page, search, membreModel.getClub(), licenceRequest, payState, order,
|
||||
categorie, archive));
|
||||
}
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> searchAdmin(int limit, int page, String search, Object club,
|
||||
int licenceRequest, int payState, String order, String categorie,
|
||||
Boolean archive) {
|
||||
if (search == null)
|
||||
search = "";
|
||||
search = "%" + search.replaceAll(" ", "% %") + "%";
|
||||
@@ -137,77 +148,47 @@ public class MembreService {
|
||||
else
|
||||
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
|
||||
|
||||
String finalSearch = search;
|
||||
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
|
||||
String archiveFilter;
|
||||
if (archive == null || archive)
|
||||
archiveFilter = "True";
|
||||
else
|
||||
archiveFilter = String.format(
|
||||
"EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)",
|
||||
Utils.getSaison() - 1);
|
||||
|
||||
String clubFilter = "(TRUE OR ?3 = ?3)";
|
||||
if (club != null) {
|
||||
if (club instanceof String club_) {
|
||||
if (!club_.isBlank()) {
|
||||
if (club_.equals("null"))
|
||||
clubFilter = "club IS NULL AND ?3 = ?3";
|
||||
else
|
||||
clubFilter = "LOWER(club.name) LIKE LOWER(?3)";
|
||||
}
|
||||
} else if (club instanceof ClubModel)
|
||||
clubFilter = "club = ?3";
|
||||
}
|
||||
|
||||
String queryStr = String.format(
|
||||
"SELECT m FROM MembreModel m WHERE id %s ?2 AND %s AND (%s) AND %s AND %s",
|
||||
(licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN", clubFilter, FIND_NAME_REQUEST,
|
||||
categorieFilter,
|
||||
archiveFilter);
|
||||
|
||||
Sort sort = getSort(order);
|
||||
if (sort == null)
|
||||
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
|
||||
|
||||
return baseUni
|
||||
String finalSearch = search;
|
||||
return getLicenceListe(licenceRequest, payState)
|
||||
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
|
||||
.chain(ids -> {
|
||||
PanacheQuery<MembreModel> query;
|
||||
|
||||
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
|
||||
|
||||
if (club == null || club.isBlank()) {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
|
||||
sort, finalSearch, ids)
|
||||
.page(Page.ofSize(limit));
|
||||
} else {
|
||||
if (club.equals("null")) {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?2 AND club IS NULL AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
|
||||
sort, finalSearch, ids).page(Page.ofSize(limit));
|
||||
} else {
|
||||
query = repository.find(
|
||||
"id " + idf + " ?3 AND LOWER(club.name) LIKE LOWER(?2) AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
|
||||
sort, finalSearch, club, ids)
|
||||
.page(Page.ofSize(limit));
|
||||
}
|
||||
}
|
||||
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
|
||||
return getPageResult(query, limit, page);
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<PageResult<SimpleMembre>> search(int limit, int page, String search, int licenceRequest, int payState,
|
||||
String order, String categorie, String subject) {
|
||||
if (search == null)
|
||||
search = "";
|
||||
search = "%" + search.replaceAll(" ", "% %") + "%";
|
||||
|
||||
String finalSearch = search;
|
||||
|
||||
Uni<List<LicenceModel>> baseUni = getLicenceListe(licenceRequest, payState);
|
||||
|
||||
String categorieFilter;
|
||||
if (categorie == null || categorie.isBlank())
|
||||
categorieFilter = " True";
|
||||
else
|
||||
categorieFilter = "categorie = " + Categorie.valueOf(categorie).ordinal();
|
||||
|
||||
Sort sort = getSort(order);
|
||||
if (sort == null)
|
||||
return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie"));
|
||||
|
||||
return baseUni
|
||||
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
|
||||
.chain(ids -> {
|
||||
String idf = ((licenceRequest == 0 || licenceRequest == 4) ? "NOT IN" : "IN");
|
||||
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.chain(membreModel -> {
|
||||
PanacheQuery<MembreModel> query = repository.find(
|
||||
"id " + idf + " ?3 AND club = ?2 AND (" + FIND_NAME_REQUEST + ") AND " + categorieFilter,
|
||||
sort, finalSearch, membreModel.getClub(), ids)
|
||||
.page(Page.ofSize(limit));
|
||||
return getPageResult(query, limit, page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Uni<PageResult<SimpleMembre>> getPageResult(PanacheQuery<MembreModel> query, int limit, int page) {
|
||||
return Uni.createFrom().item(new PageResult<SimpleMembre>())
|
||||
.invoke(result -> result.setPage(page))
|
||||
@@ -287,14 +268,16 @@ public class MembreService {
|
||||
if (model.getEmail() != null && !model.getEmail().isBlank()) {
|
||||
if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
|
||||
throw new DBadRequestException(
|
||||
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
|
||||
}
|
||||
|
||||
if (StringSimilarity.similarity(model.getLname().toUpperCase(),
|
||||
dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity(
|
||||
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException("Email '" + model.getEmail() + "' déja utiliser");
|
||||
throw new DBadRequestException(
|
||||
"Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +288,8 @@ public class MembreService {
|
||||
model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) {
|
||||
LOGGER.info("Similar membres found: " + model);
|
||||
throw new DBadRequestException(
|
||||
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide.");
|
||||
"Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence "
|
||||
+ model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")");
|
||||
}
|
||||
|
||||
ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model);
|
||||
@@ -313,8 +297,7 @@ public class MembreService {
|
||||
dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model);
|
||||
|
||||
model.setLname(dataIn.getNom().toUpperCase());
|
||||
model.setFname(dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1));
|
||||
|
||||
model.setFname(Utils.formatPrenom(dataIn.getPrenom()));
|
||||
|
||||
if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) {
|
||||
ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model);
|
||||
@@ -380,7 +363,7 @@ public class MembreService {
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0 && !membre.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utiliser");
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
})))
|
||||
.chain(membreModel -> clubRepository.findById(membre.getClub())
|
||||
.map(club -> new Pair<>(membreModel, club)))
|
||||
@@ -402,7 +385,7 @@ public class MembreService {
|
||||
.call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id)
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0 && !membre.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utiliser");
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
})))
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (!securityCtx.isInClubGroup(membreModel.getClub().getId()))
|
||||
@@ -434,7 +417,7 @@ public class MembreService {
|
||||
private Uni<String> update(Uni<MembreModel> uni, FullMemberForm membre, boolean admin) {
|
||||
return uni.chain(target -> {
|
||||
ls.logChange("Prénom", target.getFname(), membre.getFname(), target);
|
||||
target.setFname(membre.getFname());
|
||||
target.setFname(Utils.formatPrenom(membre.getFname()));
|
||||
ls.logChange("Nom", target.getLname(), membre.getLname(), target);
|
||||
target.setLname(membre.getLname().toUpperCase());
|
||||
ls.logChange("Pays", target.getCountry(), membre.getCountry(), target);
|
||||
@@ -482,7 +465,8 @@ public class MembreService {
|
||||
.call(membreModel -> licenceRepository.update("club_id = ?1 where membre = ?2 AND saison = ?3",
|
||||
(membreModel.getClub() == null) ? null : membreModel.getClub().getId(), membreModel,
|
||||
Utils.getSaison()))
|
||||
.call(membreModel -> membre.getPhoto_data().length > 0 ? ls.logAUpdate("Photo",
|
||||
.call(membreModel -> (membre.getPhoto_data() != null && membre.getPhoto_data()
|
||||
.size() > 0) ? ls.logAUpdate("Photo",
|
||||
membreModel) : Uni.createFrom().nullItem())
|
||||
.map(__ -> "OK");
|
||||
}
|
||||
@@ -492,7 +476,8 @@ public class MembreService {
|
||||
return clubRepository.findById(input.getClub())
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
|
||||
if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank())
|
||||
throw new DBadRequestException("Email déjà utilisé");
|
||||
})))
|
||||
.chain(clubModel -> {
|
||||
MembreModel model = getMembreModel(input, clubModel);
|
||||
@@ -508,7 +493,7 @@ public class MembreService {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.call(__ -> repository.count("email LIKE ?1", input.getEmail())
|
||||
.invoke(Unchecked.consumer(c -> {
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utiliser");
|
||||
if (c > 0) throw new DBadRequestException("Email déjà utilisé");
|
||||
})))
|
||||
.call(membreModel ->
|
||||
repository.count(
|
||||
@@ -577,8 +562,8 @@ public class MembreService {
|
||||
|
||||
private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) {
|
||||
MembreModel model = new MembreModel();
|
||||
model.setFname(input.getFname());
|
||||
model.setLname(input.getLname());
|
||||
model.setFname(Utils.formatPrenom(input.getFname()));
|
||||
model.setLname(input.getLname().toUpperCase());
|
||||
model.setEmail(input.getEmail());
|
||||
model.setLicence(null);
|
||||
model.setGenre(input.getGenre());
|
||||
@@ -602,12 +587,29 @@ public class MembreService {
|
||||
MeData meData = new MeData();
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(meData::setMembre)
|
||||
.chain(membreModel -> Mutiny.fetch(membreModel.getLicences()))
|
||||
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
|
||||
.invoke(meData::setLicences)
|
||||
.call(membreModel -> Mutiny.fetch(membreModel.getLicences())
|
||||
.map(licences -> licences.stream().map(SimpleLicence::fromModel).toList())
|
||||
.invoke(meData::setLicences))
|
||||
.call(membreModel -> Mutiny.fetch(membreModel.getSelections())
|
||||
.map(licences -> licences.stream().map(SimpleSelection::fromModel).toList())
|
||||
.invoke(meData::setSelections))
|
||||
.map(__ -> meData);
|
||||
}
|
||||
|
||||
public Uni<Void> updateSettings(String subject, UserSettingForm settingForm) {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.invoke(Unchecked.consumer(membreModel -> {
|
||||
if (settingForm.getResultPrivacy() != null) {
|
||||
ls.logChange("Confidentialité des résultats", membreModel.getResultPrivacy(),
|
||||
settingForm.getResultPrivacy(), membreModel);
|
||||
membreModel.setResultPrivacy(settingForm.getResultPrivacy());
|
||||
}
|
||||
}))
|
||||
.call(membreModel -> Panache.withTransaction(() -> repository.persist(membreModel)))
|
||||
.call(membreModel -> ls.append())
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 1 1 9 ?")
|
||||
Uni<Void> everySeason() {
|
||||
return repository.list("birth_date IS NOT NULL")
|
||||
@@ -617,5 +619,4 @@ public class MembreService {
|
||||
}).toList()).andCollectFailures())
|
||||
.map(__ -> null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
|
||||
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.utils.*;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
@@ -35,6 +33,12 @@ public class ResultService {
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
@Inject
|
||||
ClubRepository clubRepository;
|
||||
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
CategoryRepository categoryRepository;
|
||||
|
||||
@@ -43,52 +47,110 @@ public class ResultService {
|
||||
|
||||
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String");
|
||||
|
||||
private static final HashMap<Long, String> combTempIds = new HashMap<>();
|
||||
|
||||
private static String getCombTempId(Long key) {
|
||||
synchronized (combTempIds) {
|
||||
if (!combTempIds.containsKey(key)) {
|
||||
combTempIds.put(key, UUID.randomUUID().toString());
|
||||
}
|
||||
return combTempIds.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static Long getCombTempId(String value) {
|
||||
synchronized (combTempIds) {
|
||||
for (Map.Entry<Long, String> entry : combTempIds.entrySet()) {
|
||||
if (entry.getValue().equals(value)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final HashMap<String, Long> clubTempIds = new HashMap<>();
|
||||
|
||||
private static Long getClubTempId(String key) {
|
||||
synchronized (clubTempIds) {
|
||||
if (!clubTempIds.containsKey(key)) {
|
||||
clubTempIds.put(key, (System.currentTimeMillis() + clubTempIds.size()) * -1);
|
||||
}
|
||||
return clubTempIds.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getClubTempId(Long value) {
|
||||
synchronized (clubTempIds) {
|
||||
for (Map.Entry<String, Long> entry : clubTempIds.entrySet()) {
|
||||
if (entry.getValue().equals(value)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Uni<List<Object[]>> getList(SecurityCtx securityCtx) {
|
||||
return membreService.getByAccountId(securityCtx.getSubject())
|
||||
.chain(m -> registerRepository.list("membre = ?1", m))
|
||||
.chain(m -> registerRepository.list(
|
||||
"membre = ?1 OR (TRUE = ?2 AND membre.club = ?3)",
|
||||
m, securityCtx.isClubAdmin(), m.getClub()))
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.onItem().call(r -> Mutiny.fetch(r.getCompetition()))
|
||||
.onItem().transform(r -> new Object[]{r.getCompetition().getUuid(), r.getCompetition().getName(),
|
||||
r.getCompetition().getDate()})
|
||||
.collect().asList();
|
||||
.onItem().transform(RegisterModel::getCompetition)
|
||||
.collect().asList()
|
||||
.chain(l -> compRepository.list("owner = ?1 OR ?1 IN admin", securityCtx.getSubject())
|
||||
.map(l2 -> Stream.concat(l.stream(), l2.stream()).distinct()
|
||||
.map(c -> new Object[]{c.getUuid(), c.getName(), c.getDate()}).toList())
|
||||
);
|
||||
}
|
||||
|
||||
public Uni<List<ResultCategoryData>> getCategory(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx)
|
||||
.chain(m -> categoryRepository.list("compet.uuid = ?1", uuid)
|
||||
.chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True
|
||||
m.getMembre(), cats)))
|
||||
.map(matchModels -> {
|
||||
HashMap<Long, List<MatchModel>> map = new HashMap<>();
|
||||
for (MatchModel matchModel : matchModels) {
|
||||
if (!map.containsKey(matchModel.getCategory().getId()))
|
||||
map.put(matchModel.getCategory().getId(), new ArrayList<>());
|
||||
map.get(matchModel.getCategory().getId()).add(matchModel);
|
||||
}
|
||||
|
||||
return map.values();
|
||||
})
|
||||
.onItem()
|
||||
.transformToMulti(Multi.createFrom()::iterable)
|
||||
.onItem().call(list -> Mutiny.fetch(list.get(0).getCategory().getTree()))
|
||||
.onItem().transform(this::getData)
|
||||
.collect().asList();
|
||||
|
||||
public Uni<HashMap<String, Long>> getCategoryList(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx).chain(__ -> getCategoryList(uuid));
|
||||
}
|
||||
|
||||
private ResultCategoryData getData(List<MatchModel> matchModels) {
|
||||
public Uni<HashMap<String, Long>> getCategoryList(String uuid) {
|
||||
return categoryRepository.list("compet.uuid = ?1", uuid)
|
||||
.map(categoryModels -> {
|
||||
HashMap<String, Long> map = new HashMap<>();
|
||||
categoryModels.stream()
|
||||
.sorted(Comparator.comparing(CategoryModel::getName))
|
||||
.forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId()));
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx).chain(membreModel ->
|
||||
matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
|
||||
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
Mutiny.fetch(list.get(0).getCategory().getTree()))
|
||||
.map(list -> getData(list, membreModel)));
|
||||
}
|
||||
|
||||
public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
|
||||
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
Mutiny.fetch(list.get(0).getCategory().getTree()))
|
||||
.map(list -> getData(list, null));
|
||||
}
|
||||
|
||||
private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) {
|
||||
ResultCategoryData out = new ResultCategoryData();
|
||||
|
||||
CategoryModel categoryModel = matchModels.get(0).getCategory();
|
||||
out.setName(categoryModel.getName());
|
||||
out.setType(categoryModel.getType());
|
||||
out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";"));
|
||||
out.setGenTime(System.currentTimeMillis());
|
||||
|
||||
getArray2(matchModels, out);
|
||||
getTree(categoryModel.getTree(), out);
|
||||
getArray2(matchModels, membreModel, out);
|
||||
getTree(categoryModel.getTree(), membreModel, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void getArray2(List<MatchModel> matchModels_, ResultCategoryData out) {
|
||||
private void getArray2(List<MatchModel> matchModels_, MembreModel membreModel, ResultCategoryData out) {
|
||||
List<MatchModel> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
|
||||
|
||||
HashMap<Character, List<MatchModel>> matchMap = new HashMap<>();
|
||||
@@ -102,31 +164,30 @@ public class ResultService {
|
||||
matchMap.forEach((c, matchEntities) -> {
|
||||
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
|
||||
.sorted(Comparator.comparing(MatchModel::getCategory_ord))
|
||||
.map(ResultCategoryData.PouleArrayData::fromModel)
|
||||
.map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
|
||||
ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
|
||||
.toList();
|
||||
|
||||
List<ResultCategoryData.RankArray> rankArray = matchEntities.stream()
|
||||
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
|
||||
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
|
||||
.distinct()
|
||||
.map(combName -> {
|
||||
.filter(Objects::nonNull)
|
||||
.map(comb -> {
|
||||
AtomicInteger w = new AtomicInteger(0);
|
||||
AtomicInteger pointMake = new AtomicInteger(0);
|
||||
AtomicInteger pointTake = new AtomicInteger(0);
|
||||
|
||||
matchEntities.stream()
|
||||
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName) || m.getC2Name()
|
||||
.equals(combName)))
|
||||
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
|
||||
.forEach(matchModel -> {
|
||||
int win = matchModel.win();
|
||||
if ((matchModel.getC1Name()
|
||||
.equals(combName) && win > 0) || matchModel.getC2Name()
|
||||
.equals(combName) && win < 0)
|
||||
if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0)
|
||||
w.getAndIncrement();
|
||||
|
||||
for (ScoreEmbeddable score : matchModel.getScores()) {
|
||||
if (score.getS1() <= -900 || score.getS2() <= -900)
|
||||
continue;
|
||||
if (matchModel.getC1Name().equals(combName)) {
|
||||
if (matchModel.isC1(comb)) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
} else {
|
||||
@@ -137,7 +198,8 @@ public class ResultService {
|
||||
});
|
||||
float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();
|
||||
|
||||
return new ResultCategoryData.RankArray(0, combName, w.get(),
|
||||
return new ResultCategoryData.RankArray(0,
|
||||
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), w.get(),
|
||||
pointMake.get(), pointTake.get(), pointRate);
|
||||
})
|
||||
.sorted(Comparator
|
||||
@@ -161,111 +223,259 @@ public class ResultService {
|
||||
});
|
||||
}
|
||||
|
||||
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst) {
|
||||
dst.setData(ResultCategoryData.TreeData.from(src.getMatch()));
|
||||
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel,
|
||||
ResultPrivacy privacy) {
|
||||
dst.setData(ResultCategoryData.TreeData.from(src.getMatch(), membreModel, privacy));
|
||||
if (src.getLeft() != null) {
|
||||
dst.setLeft(new TreeNode<>());
|
||||
convertTree(src.getLeft(), dst.getLeft());
|
||||
convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy);
|
||||
}
|
||||
if (src.getRight() != null) {
|
||||
dst.setRight(new TreeNode<>());
|
||||
convertTree(src.getRight(), dst.getRight());
|
||||
convertTree(src.getRight(), dst.getRight(), membreModel, privacy);
|
||||
}
|
||||
}
|
||||
|
||||
private void getTree(List<TreeModel> treeModels, ResultCategoryData out) {
|
||||
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, ResultCategoryData out) {
|
||||
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
|
||||
treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> {
|
||||
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
|
||||
convertTree(treeModel, root);
|
||||
trees.add(root);
|
||||
});
|
||||
treeModels.stream()
|
||||
.filter(t -> t.getLevel() != 0)
|
||||
.sorted(Comparator.comparing(TreeModel::getLevel))
|
||||
.forEach(treeModel -> {
|
||||
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>();
|
||||
convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS);
|
||||
trees.add(root);
|
||||
});
|
||||
out.setTrees(trees);
|
||||
}
|
||||
|
||||
public Uni<CombsArrayData> getAllCombArray(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx)
|
||||
.chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid)
|
||||
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
|
||||
.map(matchModels -> new Pair<>(registers, matchModels)))
|
||||
.map(pair -> {
|
||||
List<RegisterModel> registers = pair.getKey();
|
||||
List<MatchModel> matchModels = pair.getValue();
|
||||
.chain(membreModel -> getAllCombArray_(uuid, membreModel));
|
||||
}
|
||||
|
||||
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
|
||||
public Uni<CombsArrayData> getAllCombArrayPublic(String uuid) {
|
||||
return getAllCombArray_(uuid, null);
|
||||
}
|
||||
|
||||
List<CombsArrayData.CombsData> combs = matchModels.stream()
|
||||
.flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name()))
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(combName -> {
|
||||
var builder2 = CombsArrayData.CombsData.builder();
|
||||
AtomicInteger w = new AtomicInteger(0);
|
||||
AtomicInteger l = new AtomicInteger(0);
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) {
|
||||
return registerRepository.list("competition.uuid = ?1", uuid)
|
||||
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
|
||||
.map(matchModels -> new Pair<>(registers, matchModels)))
|
||||
.map(pair -> {
|
||||
List<RegisterModel> registers = pair.getKey();
|
||||
List<MatchModel> matchModels = pair.getValue();
|
||||
|
||||
matchModels.stream()
|
||||
.filter(m -> m.isEnd() && (m.getC1Name().equals(combName)
|
||||
|| m.getC2Name().equals(combName)))
|
||||
.forEach(matchModel -> {
|
||||
int win = matchModel.win();
|
||||
if ((combName.equals(matchModel.getC1Name()) && win > 0) ||
|
||||
combName.equals(matchModel.getC2Name()) && win < 0) {
|
||||
w.getAndIncrement();
|
||||
} else {
|
||||
l.getAndIncrement();
|
||||
}
|
||||
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
|
||||
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
.forEach(score -> {
|
||||
if (combName.equals(matchModel.getC1Name())) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
} else {
|
||||
pointMake.addAndGet(score.getS2());
|
||||
pointTake.addAndGet(score.getS1());
|
||||
}
|
||||
});
|
||||
});
|
||||
List<CombsArrayData.CombsData> combs = matchModels.stream()
|
||||
.flatMap(m -> Stream.of(m.getC1_id(), m.getC2_id(), m.getC1_guest(), m.getC2_guest()))
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(comb -> {
|
||||
var builder2 = CombsArrayData.CombsData.builder();
|
||||
AtomicInteger w = new AtomicInteger(0);
|
||||
AtomicInteger l = new AtomicInteger(0);
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
|
||||
Categorie categorie = null;
|
||||
ClubModel club = null;
|
||||
makeStat(matchModels, comb, w, l, pointMake, pointTake);
|
||||
|
||||
Optional<RegisterModel> register = registers.stream()
|
||||
.filter(r -> r.getName().equals(combName)).findFirst();
|
||||
if (register.isPresent()) {
|
||||
categorie = register.get().getCategorie();
|
||||
club = register.get().getClub2();
|
||||
}
|
||||
Categorie categorie = null;
|
||||
String clubName = null;
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.name(combName);
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName());
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
Optional<RegisterModel> register = registers.stream()
|
||||
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
|
||||
if (register.isPresent()) {
|
||||
categorie = register.get().getCategorie2();
|
||||
ClubModel club = register.get().getClub2();
|
||||
clubName = (club == null) ? BUNDLE.getString("no.licence") : club.getName();
|
||||
} else if (comb instanceof CompetitionGuestModel guestModel) {
|
||||
categorie = guestModel.getCategorie();
|
||||
clubName = guestModel.getClub();
|
||||
} else if (comb instanceof MembreModel model) {
|
||||
categorie = model.getCategorie();
|
||||
clubName = (model.getClub() == null) ? BUNDLE.getString(
|
||||
"no.licence") : model.getClub().getName();
|
||||
}
|
||||
|
||||
return builder2.build();
|
||||
})
|
||||
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
|
||||
.toList();
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.club(clubName);
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
|
||||
builder.nb_insc(combs.size());
|
||||
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
|
||||
builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
|
||||
builder.combs(combs);
|
||||
return builder2.build();
|
||||
})
|
||||
.sorted(Comparator.comparing(CombsArrayData.CombsData::name))
|
||||
.toList();
|
||||
|
||||
return builder.build();
|
||||
builder.nb_insc(combs.size());
|
||||
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count());
|
||||
builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum());
|
||||
builder.combs(combs);
|
||||
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<HashMap<String, String>> getCombList(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx)
|
||||
.chain(membreModel -> getCombList(uuid, ResultPrivacy.REGISTERED_ONLY));
|
||||
}
|
||||
|
||||
public Uni<HashMap<String, String>> getCombList(String uuid) {
|
||||
return getCombList(uuid, ResultPrivacy.PUBLIC);
|
||||
}
|
||||
|
||||
private Uni<HashMap<String, String>> getCombList(String uuid, ResultPrivacy privacy) {
|
||||
return registerRepository.list("competition.uuid = ?1 AND membre.resultPrivacy <= ?2", uuid, privacy)
|
||||
.map(models -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
models.forEach(
|
||||
r -> map.put(Utils.getFullName(r.getMembre()), getCombTempId(r.getMembre().getId())));
|
||||
return map;
|
||||
})
|
||||
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(models -> {
|
||||
models.forEach(guestModel -> map.put(Utils.getFullName(guestModel),
|
||||
getCombTempId(guestModel.getId() * -1)));
|
||||
return map;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public Uni<?> getCombArrayPublic(String uuid, String combTempId, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx)
|
||||
.chain(membreModel -> getCombArrayPublic(uuid, combTempId, ResultPrivacy.REGISTERED_ONLY));
|
||||
}
|
||||
|
||||
public Uni<?> getCombArrayPublic(String uuid, String combTempId) {
|
||||
return getCombArrayPublic(uuid, combTempId, ResultPrivacy.PUBLIC);
|
||||
}
|
||||
|
||||
private Uni<?> getCombArrayPublic(String uuid, String combTempId, ResultPrivacy privacy) {
|
||||
CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder();
|
||||
|
||||
Long id = getCombTempId(combTempId);
|
||||
if (id == null) {
|
||||
return Uni.createFrom().failure(new DForbiddenException("Comb not found"));
|
||||
}
|
||||
|
||||
Uni<List<MatchModel>> uni;
|
||||
if (id >= 0) {
|
||||
uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
|
||||
uuid, privacy).firstResult()
|
||||
.chain(Unchecked.function(registerModel -> {
|
||||
if (registerModel == null)
|
||||
throw new DBadRequestException("Combattant non inscrit");
|
||||
|
||||
builder.name(Utils.getFullName(registerModel.getMembre()));
|
||||
builder.club((registerModel.getClub2() == null) ? BUNDLE.getString(
|
||||
"no.licence") : registerModel.getClub2().getName());
|
||||
builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2()
|
||||
.getName(BUNDLE));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid,
|
||||
registerModel.getMembre());
|
||||
}));
|
||||
} else {
|
||||
uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
|
||||
.chain(guestModel -> {
|
||||
builder.name(Utils.getFullName(guestModel));
|
||||
builder.club(guestModel.getClub());
|
||||
builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie()
|
||||
.getName(BUNDLE));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)",
|
||||
uuid, guestModel);
|
||||
});
|
||||
}
|
||||
|
||||
return uni.invoke(matchModels -> {
|
||||
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct()
|
||||
.toList();
|
||||
List<CombArrayData.MatchsData> matchs = new ArrayList<>();
|
||||
|
||||
AtomicInteger sumW = new AtomicInteger();
|
||||
AtomicInteger sumPointMake = new AtomicInteger(0);
|
||||
AtomicInteger sumPointTake = new AtomicInteger(0);
|
||||
|
||||
for (MatchModel matchModel : matchModels) {
|
||||
if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
|
||||
(matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
|
||||
continue;
|
||||
|
||||
var builder2 = CombArrayData.MatchsData.builder();
|
||||
builder2.date(matchModel.getDate());
|
||||
builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory()))
|
||||
.map(CategoryModel::getName).findFirst().orElse(""));
|
||||
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
|
||||
if (matchModel.isC1(id)) {
|
||||
builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest()));
|
||||
if (matchModel.isEnd()) {
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
.forEach(scoreEntity -> {
|
||||
pointMake.addAndGet(scoreEntity.getS1());
|
||||
pointTake.addAndGet(scoreEntity.getS2());
|
||||
});
|
||||
builder2.score(matchModel.getScores().stream()
|
||||
.map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
|
||||
} else {
|
||||
builder2.score(new ArrayList<>());
|
||||
}
|
||||
builder2.win(matchModel.win() > 0);
|
||||
} else {
|
||||
builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest()));
|
||||
if (matchModel.isEnd()) {
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
.forEach(scoreEntity -> {
|
||||
pointMake.addAndGet(scoreEntity.getS2());
|
||||
pointTake.addAndGet(scoreEntity.getS1());
|
||||
});
|
||||
builder2.score(matchModel.getScores().stream()
|
||||
.map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
|
||||
} else {
|
||||
builder2.score(new ArrayList<>());
|
||||
}
|
||||
builder2.win(matchModel.win() < 0);
|
||||
}
|
||||
|
||||
builder2.ratio(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
|
||||
sumPointMake.addAndGet(pointMake.get());
|
||||
sumPointTake.addAndGet(pointTake.get());
|
||||
|
||||
matchs.add(builder2.build());
|
||||
if (builder2.win)
|
||||
sumW.getAndIncrement();
|
||||
}
|
||||
|
||||
builder.totalWin(sumW.get());
|
||||
builder.pointRatio(
|
||||
(sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get());
|
||||
builder.pointMake(sumPointMake.get());
|
||||
builder.pointTake(sumPointTake.get());
|
||||
|
||||
matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule)
|
||||
.thenComparing(CombArrayData.MatchsData::adv));
|
||||
|
||||
builder.matchs(matchs);
|
||||
})
|
||||
.map(__ -> builder.build());
|
||||
}
|
||||
|
||||
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
@@ -277,98 +487,159 @@ public class ResultService {
|
||||
}
|
||||
}
|
||||
|
||||
public Uni<ClubArrayData> getClubArray(String uuid, SecurityCtx securityCtx) {
|
||||
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
public static record CombArrayData(String name, String club, String cat, int totalWin,
|
||||
float pointRatio, int pointMake, int pointTake, List<MatchsData> matchs) {
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
|
||||
boolean win) {
|
||||
}
|
||||
}
|
||||
|
||||
return hasAccess(uuid, securityCtx)
|
||||
.invoke(cm_register -> builder.name(cm_register.getClub2().getName()))
|
||||
.chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid,
|
||||
cm_register.getClub2())
|
||||
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
|
||||
.map(matchModels -> new Pair<>(registers, matchModels)))
|
||||
.map(pair -> {
|
||||
List<RegisterModel> registers = pair.getKey();
|
||||
List<MatchModel> matchModels = pair.getValue();
|
||||
public Uni<HashMap<String, Long>> getClubList(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx).chain(__ -> getClubList(uuid));
|
||||
}
|
||||
|
||||
builder.nb_insc(registers.size());
|
||||
|
||||
AtomicInteger tt_win = new AtomicInteger(0);
|
||||
AtomicInteger tt_match = new AtomicInteger(0);
|
||||
|
||||
List<ClubArrayData.CombData> combData = registers.stream()
|
||||
.map(register -> {
|
||||
|
||||
var builder2 = ClubArrayData.CombData.builder();
|
||||
AtomicInteger w = new AtomicInteger(0);
|
||||
AtomicInteger l = new AtomicInteger(0);
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
|
||||
matchModels.stream()
|
||||
.filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id())
|
||||
|| register.getMembre().equals(m.getC2_id())))
|
||||
.forEach(matchModel -> {
|
||||
int win = matchModel.win();
|
||||
if ((register.getMembre()
|
||||
.equals(matchModel.getC1_id()) && win > 0) ||
|
||||
register.getMembre()
|
||||
.equals(matchModel.getC2_id()) && win < 0) {
|
||||
w.getAndIncrement();
|
||||
} else {
|
||||
l.getAndIncrement();
|
||||
}
|
||||
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
.forEach(score -> {
|
||||
if (register.getMembre()
|
||||
.equals(matchModel.getC1_id())) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
} else {
|
||||
pointMake.addAndGet(score.getS2());
|
||||
pointTake.addAndGet(score.getS1());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Categorie categorie = register.getCategorie();
|
||||
if (categorie == null)
|
||||
categorie = register.getMembre().getCategorie();
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.name(register.getName());
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
|
||||
tt_win.addAndGet(w.get());
|
||||
tt_match.addAndGet(w.get() + l.get());
|
||||
|
||||
return builder2.build();
|
||||
})
|
||||
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
|
||||
.toList();
|
||||
|
||||
builder.nb_match(tt_match.get());
|
||||
builder.match_w(tt_win.get());
|
||||
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
|
||||
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
|
||||
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
|
||||
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
|
||||
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
|
||||
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
|
||||
builder.combs(combData);
|
||||
|
||||
return builder.build();
|
||||
public Uni<HashMap<String, Long>> getClubList(String uuid) {
|
||||
return registerRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(registers -> {
|
||||
HashMap<String, Long> registerMap = new HashMap<>();
|
||||
registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull)
|
||||
.forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId()));
|
||||
return registerMap;
|
||||
})
|
||||
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(guests -> {
|
||||
guests.stream().map(CompetitionGuestModel::getClub).distinct()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(guestClub -> map.putIfAbsent(guestClub,
|
||||
getClubTempId(guestClub)));
|
||||
return map;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public Uni<ClubArrayData> getClubArray(String uuid, Long id, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx).chain(membreModel -> getClubArray2(uuid, id, membreModel));
|
||||
}
|
||||
|
||||
public Uni<ClubArrayData> getClubArray2(String uuid, Long id, MembreModel membreModel) {
|
||||
if (id < 0) {
|
||||
String clubName = getClubTempId(id);
|
||||
if (clubName == null) {
|
||||
return Uni.createFrom().failure(new DForbiddenException("Club not found"));
|
||||
}
|
||||
|
||||
return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName)
|
||||
.call(list -> {
|
||||
if (list.isEmpty())
|
||||
return Uni.createFrom().failure(new DBadRequestException("Club not found"));
|
||||
return Uni.createFrom().voidItem();
|
||||
})
|
||||
.chain(guests -> matchRepository.list(
|
||||
"category.compet.uuid = ?1 AND (c1_guest IN ?2 OR c2_guest IN ?2)", uuid, guests)
|
||||
.map(matchModels ->
|
||||
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
|
||||
matchModels, new ArrayList<>(), membreModel)));
|
||||
} else {
|
||||
return clubRepository.findById(id).chain(clubModel ->
|
||||
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
|
||||
.chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid)
|
||||
.map(matchModels ->
|
||||
getClubArray2(clubModel.getName(),
|
||||
registers.stream().map(o -> (CombModel) o.getMembre()).toList(),
|
||||
matchModels, registers, membreModel))));
|
||||
}
|
||||
}
|
||||
|
||||
private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModel> matchModels,
|
||||
List<RegisterModel> registers, MembreModel membreModel) {
|
||||
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
|
||||
builder.name(name);
|
||||
builder.nb_insc(combs.size());
|
||||
|
||||
AtomicInteger tt_win = new AtomicInteger(0);
|
||||
AtomicInteger tt_match = new AtomicInteger(0);
|
||||
|
||||
List<ClubArrayData.CombData> combData = combs.stream().map(comb -> {
|
||||
var builder2 = ClubArrayData.CombData.builder();
|
||||
AtomicInteger w = new AtomicInteger(0);
|
||||
AtomicInteger l = new AtomicInteger(0);
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
|
||||
makeStat(matchModels, comb, w, l, pointMake, pointTake);
|
||||
|
||||
Categorie categorie = null;
|
||||
|
||||
Optional<RegisterModel> register = registers.stream()
|
||||
.filter(r -> Objects.equals(r.getMembre(), comb)).findFirst();
|
||||
if (register.isPresent()) {
|
||||
categorie = register.get().getCategorie2();
|
||||
} else if (comb instanceof CompetitionGuestModel guestModel) {
|
||||
categorie = guestModel.getCategorie();
|
||||
} else if (comb instanceof MembreModel model) {
|
||||
categorie = model.getCategorie();
|
||||
}
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint((pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
|
||||
tt_win.addAndGet(w.get());
|
||||
tt_match.addAndGet(w.get() + l.get());
|
||||
|
||||
return builder2.build();
|
||||
})
|
||||
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
|
||||
.toList();
|
||||
|
||||
builder.nb_match(tt_match.get());
|
||||
builder.match_w(tt_win.get());
|
||||
builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0)
|
||||
.mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L));
|
||||
builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum());
|
||||
builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum());
|
||||
builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0)
|
||||
.mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L));
|
||||
builder.combs(combData);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void makeStat(List<MatchModel> matchModels, CombModel comb, AtomicInteger w, AtomicInteger l,
|
||||
AtomicInteger pointMake, AtomicInteger pointTake) {
|
||||
matchModels.stream()
|
||||
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
|
||||
.forEach(matchModel -> {
|
||||
int win = matchModel.win();
|
||||
if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
|
||||
w.getAndIncrement();
|
||||
} else {
|
||||
l.getAndIncrement();
|
||||
}
|
||||
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
.forEach(score -> {
|
||||
if (matchModel.isC1(comb)) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
} else {
|
||||
pointMake.addAndGet(score.getS2());
|
||||
pointTake.addAndGet(score.getS1());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
public static record ClubArrayData(String name, int nb_insc, int nb_match, int match_w, float ratioVictoire,
|
||||
@@ -380,21 +651,35 @@ public class ResultService {
|
||||
}
|
||||
}
|
||||
|
||||
private Uni<RegisterModel> hasAccess(String uuid, SecurityCtx securityCtx) {
|
||||
private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
|
||||
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
|
||||
.firstResult()
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DForbiddenException("Access denied");
|
||||
}));
|
||||
}
|
||||
.chain(Unchecked.function(o -> {
|
||||
if (o != null)
|
||||
return Uni.createFrom().item(o.getMembre());
|
||||
|
||||
private Uni<RegisterModel> hasAccess(Long compId, SecurityCtx securityCtx) {
|
||||
return registerRepository.find("membre.userId = ?1 AND competition.id = ?2", securityCtx.getSubject(), compId)
|
||||
.firstResult()
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DForbiddenException("Access denied");
|
||||
return membreService.getByAccountId(securityCtx.getSubject()).chain(m -> {
|
||||
if (securityCtx.isClubAdmin()) {
|
||||
return registerRepository.count("membre.club = ?2 AND competition.uuid = ?1",
|
||||
uuid, m.getClub()).chain(c -> {
|
||||
if (c > 0) return Uni.createFrom().item(m);
|
||||
|
||||
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)",
|
||||
uuid, securityCtx.getSubject())
|
||||
.chain(c2 -> {
|
||||
if (c2 > 0) return Uni.createFrom().item(m);
|
||||
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return compRepository.count("uuid = ?1 AND (owner = ?2 OR ?2 IN admin)", uuid,
|
||||
securityCtx.getSubject())
|
||||
.chain(c2 -> {
|
||||
if (c2 > 0) return Uni.createFrom().item(m);
|
||||
return Uni.createFrom().failure(new DForbiddenException("Access denied"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 " +
|
||||
|
||||
@@ -3,10 +3,10 @@ package fr.titionfire.ffsaf.rest;
|
||||
import fr.titionfire.ffsaf.data.model.ClubModel;
|
||||
import fr.titionfire.ffsaf.domain.service.ClubService;
|
||||
import fr.titionfire.ffsaf.domain.service.PDFService;
|
||||
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||
import fr.titionfire.ffsaf.rest.data.*;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DInternalError;
|
||||
import fr.titionfire.ffsaf.rest.from.FullClubForm;
|
||||
import fr.titionfire.ffsaf.rest.from.PartClubForm;
|
||||
import fr.titionfire.ffsaf.utils.Contact;
|
||||
@@ -47,6 +47,9 @@ public class ClubEndpoints {
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
|
||||
@Inject
|
||||
VirusScannerService scannerService;
|
||||
|
||||
@ConfigProperty(name = "upload_dir")
|
||||
String media;
|
||||
|
||||
@@ -109,7 +112,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Renvoie les détails d'un club en fonction de son identifiant", description = "Renvoie les " +
|
||||
"détails d'un club en fonction de son identifiant, y compris les informations sur les membres et les affiliations")
|
||||
@@ -121,9 +124,8 @@ public class ClubEndpoints {
|
||||
})
|
||||
public Uni<SimpleClub> getById(
|
||||
@Parameter(description = "Identifiant de club") @PathParam("id") long id) {
|
||||
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel).invoke(m -> {
|
||||
m.setContactMap(Contact.toSite());
|
||||
});
|
||||
return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel)
|
||||
.invoke(m -> m.setContactMap(Contact.toSite()));
|
||||
}
|
||||
|
||||
@PUT
|
||||
@@ -145,25 +147,9 @@ public class ClubEndpoints {
|
||||
return clubService.update(id, input)
|
||||
.invoke(Unchecked.consumer(out -> {
|
||||
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
|
||||
})).chain(() -> {
|
||||
if (input.getLogo().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub"
|
||||
)).invoke(Unchecked.consumer(out -> {
|
||||
if (!out.equals("OK"))
|
||||
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
|
||||
})); // TODO log
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
}).chain(() -> {
|
||||
if (input.getStatus().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus"
|
||||
)).invoke(Unchecked.consumer(out -> {
|
||||
if (!out.equals("OK"))
|
||||
throw new DInternalError("Impossible de reconnaitre le fichier: " + out);
|
||||
})); // TODO log
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
}))
|
||||
.chain(() -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
|
||||
.chain(() -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
|
||||
}
|
||||
|
||||
@PUT
|
||||
@@ -181,19 +167,9 @@ public class ClubEndpoints {
|
||||
return clubService.add(input)
|
||||
.invoke(Unchecked.consumer(id -> {
|
||||
if (id == null) throw new InternalError("Fail to create club data");
|
||||
})).call(id -> {
|
||||
if (input.getLogo().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getLogo(), media, "ppClub"
|
||||
)); // TODO log
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
}).call(id -> {
|
||||
if (input.getStatus().length > 0)
|
||||
return Uni.createFrom().future(Utils.replacePhoto(id, input.getStatus(), media, "clubStatus"
|
||||
)); // TODO log
|
||||
else
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
}))
|
||||
.call(id -> Utils.uploadFile(scannerService, input.getLogo(), id, media, "ppClub"))
|
||||
.call(id -> Utils.uploadFile(scannerService, input.getStatus(), id, media, "clubStatus"));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@@ -230,7 +206,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("/me")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Renvoie les informations du club de l'utilisateur connecté", description = "Renvoie les " +
|
||||
"informations du club de l'utilisateur connecté")
|
||||
@@ -247,7 +223,7 @@ public class ClubEndpoints {
|
||||
|
||||
@PUT
|
||||
@Path("/me")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(summary = "Met à jour les informations du club de l'utilisateur connecté", description = "Met à jour les" +
|
||||
@@ -264,7 +240,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("/me/affiliation")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Operation(summary = "Renvoie l'attestation d'affiliation du club de l'utilisateur connecté", description =
|
||||
"Renvoie l'attestation d'affiliation du club de l'utilisateur connecté")
|
||||
@APIResponses(value = {
|
||||
@@ -296,7 +272,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("/renew/{id}")
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(hidden = true)
|
||||
public Uni<RenewAffData> getRenew(@PathParam("id") long id, @QueryParam("m1") long m1_id,
|
||||
@@ -307,7 +283,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("/desk/{id}")
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Renvoie la liste des membres du bureau du club", description = "Renvoie la liste des membres " +
|
||||
"du bureau du club spécifié")
|
||||
@@ -345,7 +321,7 @@ public class ClubEndpoints {
|
||||
|
||||
@GET
|
||||
@Path("{id}/status")
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})
|
||||
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_tresorier", "club_respo_intra"})
|
||||
@Operation(summary = "Renvoie le statut du club", description = "Renvoie le statut du club spécifié")
|
||||
@APIResponses(value = {
|
||||
@APIResponse(responseCode = "200", description = "Le statut du club"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,17 +30,41 @@ 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
|
||||
@Path("{uuid}/comb")
|
||||
public Uni<ResultService.CombsArrayData> getComb(@PathParam("uuid") String uuid) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.utils.ResultPrivacy;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -15,38 +16,42 @@ import java.util.List;
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class MeData {
|
||||
@Schema(description = "L'identifiant du membre.", example = "1")
|
||||
@Schema(description = "L'identifiant du membre.", examples = "1")
|
||||
private long id;
|
||||
@Schema(description = "Le nom du membre.", example = "Dupont")
|
||||
@Schema(description = "Le nom du membre.", examples = "Dupont")
|
||||
private String lname = "";
|
||||
@Schema(description = "Le prénom du membre.", example = "Jean")
|
||||
@Schema(description = "Le prénom du membre.", examples = "Jean")
|
||||
private String fname = "";
|
||||
@Schema(description = "La catégorie du membre.", example = "SENIOR")
|
||||
@Schema(description = "La catégorie du membre.", examples = "SENIOR")
|
||||
private String categorie;
|
||||
@Schema(description = "Le nom du club du membre.", example = "Association sportive")
|
||||
@Schema(description = "Le nom du club du membre.", examples = "Association sportive")
|
||||
private String club;
|
||||
@Schema(description = "Le genre du membre.", example = "Homme")
|
||||
@Schema(description = "Le genre du membre.", examples = "Homme")
|
||||
private String genre;
|
||||
@Schema(description = "Le numéro de licence du membre.", example = "12345")
|
||||
@Schema(description = "Le numéro de licence du membre.", examples = "12345")
|
||||
private int licence;
|
||||
@Schema(description = "Le pays du membre.", example = "FR")
|
||||
@Schema(description = "Le pays du membre.", examples = "FR")
|
||||
private String country;
|
||||
@Schema(description = "La date de naissance du membre.")
|
||||
private Date birth_date;
|
||||
@Schema(description = "L'adresse e-mail du membre.", example = "jean.dupont@example.com")
|
||||
@Schema(description = "L'adresse e-mail du membre.", examples = "jean.dupont@examples.com")
|
||||
private String email;
|
||||
@Schema(description = "Le rôle du membre dans l'association.", example = "MEMBRE")
|
||||
@Schema(description = "Le rôle du membre dans l'association.", examples = "MEMBRE")
|
||||
private String role;
|
||||
@Schema(description = "Le grade d'arbitrage du membre.", example = "N/A")
|
||||
@Schema(description = "Le grade d'arbitrage du membre.", examples = "N/A")
|
||||
private String grade_arbitrage;
|
||||
@Schema(description = "La confidentialité des résultats", examples = "PUBLIC")
|
||||
private ResultPrivacy resultPrivacy;
|
||||
@Schema(description = "La liste des licences du membre.")
|
||||
private List<SimpleLicence> licences;
|
||||
@Schema(description = "La liste des séléctions du membre.")
|
||||
private List<SimpleSelection> selections;
|
||||
|
||||
public void setMembre(MembreModel membreModel) {
|
||||
this.id = membreModel.getId();
|
||||
this.lname = membreModel.getLname();
|
||||
this.fname = membreModel.getFname();
|
||||
this.categorie = membreModel.getCategorie().getName();
|
||||
this.categorie = membreModel.getCategorie() == null ? "catégorie inconnue" : membreModel.getCategorie().getName();
|
||||
this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName();
|
||||
this.genre = membreModel.getGenre().str;
|
||||
this.licence = membreModel.getLicence();
|
||||
@@ -55,5 +60,6 @@ public class MeData {
|
||||
this.email = membreModel.getEmail();
|
||||
this.role = membreModel.getRole().str;
|
||||
this.grade_arbitrage = membreModel.getGrade_arbitrage().str;
|
||||
this.resultPrivacy = membreModel.getResultPrivacy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 + '\'' +
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.titionfire.ffsaf.utils;
|
||||
|
||||
public enum ResultPrivacy {
|
||||
PUBLIC,
|
||||
REGISTERED_ONLY,
|
||||
REGISTERED_ONLY_NO_DETAILS,
|
||||
PRIVATE;
|
||||
}
|
||||
@@ -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édération France Soft Armored Fighting</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
""";
|
||||
public static String HTML_FOOTER = """
|
||||
<p>Cordialement,<br>L’équipe de la FFSAF</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
public static int getSaison() {
|
||||
return getSaison(new Date());
|
||||
}
|
||||
@@ -108,44 +189,72 @@ public class Utils {
|
||||
});
|
||||
}
|
||||
|
||||
public static Future<String> replacePhoto(long id, byte[] input, String media, String dir) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (input == null || input.length == 0)
|
||||
return "OK";
|
||||
public static Uni<String> uploadFile(VirusScannerService ss, FileUpload file, long id, String media, String dir) {
|
||||
if (file == null || file.size() == 0)
|
||||
return Uni.createFrom().item("Ok");
|
||||
|
||||
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
|
||||
String mimeType;
|
||||
try {
|
||||
Tika tika = new Tika();
|
||||
mimeType = tika.detect(is);// Magic.getMagicMatch(input, false).getMimeType();
|
||||
} catch (IOException e) {
|
||||
mimeType = URLConnection.guessContentTypeFromStream(is);
|
||||
}
|
||||
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
|
||||
if (detectedExtensions.length == 0)
|
||||
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
|
||||
|
||||
File dirFile = new File(media, dir);
|
||||
if (!dirFile.exists())
|
||||
if (!dirFile.mkdirs())
|
||||
throw new IOException("Fail to create directory " + dir);
|
||||
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
|
||||
File[] files = dirFile.listFiles(filter);
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
LOGGER.infof("Received file upload request for: %s (size: %d bytes)", file.fileName(), file.size());
|
||||
return Uni.createFrom().<InputStream>item(() -> {
|
||||
try {
|
||||
// Read the entire file into memory for virus scanning
|
||||
// ByteArrayInputStream fully supports mark/reset operations required by ClamAV
|
||||
// This ensures we can scan the file content before any filesystem storage
|
||||
InputStream fileStream = java.nio.file.Files.newInputStream(file.uploadedFile());
|
||||
byte[] fileBytes = fileStream.readAllBytes();
|
||||
fileStream.close(); // Close the file stream immediately
|
||||
return new ByteArrayInputStream(fileBytes);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read uploaded file: " + e.getMessage(), e);
|
||||
}
|
||||
}).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool()).onItem()
|
||||
.transformToUni(inputStream -> {
|
||||
// Perform virus scanning reactively using the input stream
|
||||
return ss.scanFileReactive(file.fileName(), inputStream).onItem().invoke(() -> {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Warning: Failed to close input stream: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
})
|
||||
.onItem().transformToUni(scanResults -> Uni.createFrom().item(Unchecked.supplier(() -> {
|
||||
// Check if any scanner found a threat
|
||||
for (AntivirusScanResult result : scanResults) {
|
||||
if (result.getStatus() != Response.Status.OK.getStatusCode()) {
|
||||
LOGGER.warnf("THREAT DETECTED in %s: %s", file.fileName(), result.getMessage());
|
||||
throw new DetailException(Response.Status.fromStatusCode(result.getStatus()),
|
||||
"THREAT_DETECTED on File " + file.fileName() + " is infected: " + result.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String extension = "." + detectedExtensions[0];
|
||||
Files.write(new File(dirFile, id + extension).toPath(), input);
|
||||
return "OK";
|
||||
} catch (IOException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
});
|
||||
// File is clean - now we can safely process it
|
||||
LOGGER.infof("File is clean: %s (size=%d) (contentType=%s)", file.fileName(), file.size(),
|
||||
file.contentType());
|
||||
|
||||
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(file.contentType(), false);
|
||||
if (detectedExtensions.length == 0)
|
||||
throw new DBadRequestException(
|
||||
"Fail to detect file extension for MIME type " + file.contentType());
|
||||
|
||||
File dirFile = new File(media, dir);
|
||||
if (!dirFile.exists())
|
||||
if (!dirFile.mkdirs())
|
||||
throw new DInternalError("Fail to create directory " + dir);
|
||||
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
|
||||
File[] files = dirFile.listFiles(filter);
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
|
||||
File f = file.filePath().toFile();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0]));
|
||||
return "ok";
|
||||
})));
|
||||
}
|
||||
|
||||
public static Uni<Response> getMediaFile(long id, String media, String dirname,
|
||||
@@ -262,4 +371,46 @@ public class Utils {
|
||||
|
||||
return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
public static String formatPrenom(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
String[] mots = input.split(" ");
|
||||
|
||||
for (String mot : mots) {
|
||||
if (!mot.isEmpty()) {
|
||||
String[] parties = mot.split("[-']");
|
||||
StringBuilder motFormate = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < parties.length; i++) {
|
||||
if (!parties[i].isEmpty()) {
|
||||
String premiereLettre = parties[i].substring(0, 1).toUpperCase();
|
||||
String reste = parties[i].substring(1).toLowerCase();
|
||||
motFormate.append(premiereLettre).append(reste);
|
||||
}
|
||||
|
||||
if (i < parties.length - 1) {
|
||||
motFormate.append(mot.charAt(mot.indexOf(parties[i]) + parties[i].length()));
|
||||
}
|
||||
}
|
||||
result.append(motFormate).append(" ");
|
||||
}
|
||||
}
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
public static String getFullName(Object... models) {
|
||||
for (Object model : models) {
|
||||
if (model == null)
|
||||
continue;
|
||||
if (model instanceof MembreModel membreModel)
|
||||
return membreModel.getFname() + " " + membreModel.getLname();
|
||||
if (model instanceof CompetitionGuestModel guestModel)
|
||||
return guestModel.getFname() + " " + guestModel.getLname();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.domain.service.CompetPermService;
|
||||
import fr.titionfire.ffsaf.net2.MessageType;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
|
||||
import fr.titionfire.ffsaf.utils.SecurityCtx;
|
||||
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
|
||||
import fr.titionfire.ffsaf.ws.recv.*;
|
||||
@@ -16,12 +17,15 @@ import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER;
|
||||
|
||||
@@ -39,7 +43,7 @@ public class CompetitionWS {
|
||||
RCategorie rCategorie;
|
||||
|
||||
@Inject
|
||||
RRegister rRegister;
|
||||
RRegister rRegister;
|
||||
|
||||
@Inject
|
||||
RCardboard rCardboard;
|
||||
@@ -50,6 +54,15 @@ public class CompetitionWS {
|
||||
@Inject
|
||||
CompetPermService competPermService;
|
||||
|
||||
@SuppressWarnings("CdiInjectionPointsInspection")
|
||||
@Inject
|
||||
OpenConnections connections;
|
||||
|
||||
@Inject
|
||||
@Named("notify-executor")
|
||||
Executor notifyExecutor;
|
||||
private static Executor executor;
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
@@ -78,6 +91,8 @@ public class CompetitionWS {
|
||||
getWSReceiverMethods(RCategorie.class, rCategorie);
|
||||
getWSReceiverMethods(RRegister.class, rRegister);
|
||||
getWSReceiverMethods(RCardboard.class, rCardboard);
|
||||
|
||||
executor = notifyExecutor;
|
||||
}
|
||||
|
||||
@OnOpen
|
||||
@@ -94,8 +109,8 @@ public class CompetitionWS {
|
||||
.call(cm -> competPermService.hasEditPerm(securityCtx, cm).map(__ -> PermLevel.ADMIN)
|
||||
.onFailure()
|
||||
.recoverWithUni(competPermService.hasTablePerm(securityCtx, cm).map(__ -> PermLevel.TABLE))
|
||||
.onFailure()
|
||||
.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW))
|
||||
//.onFailure()
|
||||
//.recoverWithUni(competPermService.hasViewPerm(securityCtx, cm).map(__ -> PermLevel.VIEW))
|
||||
.invoke(prem -> connection.userData().put(UserData.TypedKey.forString("prem"), prem.toString()))
|
||||
.invoke(prem -> LOGGER.infof("Connection permission: %s", prem))
|
||||
.onFailure().transform(t -> new ForbiddenException()))
|
||||
@@ -104,10 +119,13 @@ public class CompetitionWS {
|
||||
waitingResponse.put(connection, new HashMap<>());
|
||||
})
|
||||
.map(cm -> {
|
||||
SimpleCompetData data = SimpleCompetData.fromModel(cm);
|
||||
WelcomeInfo welcomeInfo = new WelcomeInfo();
|
||||
|
||||
welcomeInfo.setName(cm.getName());
|
||||
welcomeInfo.setPerm(connection.userData().get(UserData.TypedKey.forString("prem")));
|
||||
welcomeInfo.setShow_blason(data.isShow_blason());
|
||||
welcomeInfo.setShow_flag(data.isShow_flag());
|
||||
|
||||
return new MessageOut(UUID.randomUUID(), "welcomeInfo", MessageType.NOTIFY, welcomeInfo);
|
||||
});
|
||||
@@ -166,6 +184,7 @@ public class CompetitionWS {
|
||||
return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti();
|
||||
return ((Uni<?>) method.invoke(entry.getValue(), connection,
|
||||
MAPPER.treeToValue(message.data(), method.getParameterTypes()[1])))
|
||||
.ifNoItem().after(Duration.ofSeconds(5)).fail()
|
||||
.map(o -> makeReply(message, o))
|
||||
.onFailure()
|
||||
.recoverWithItem(t -> {
|
||||
@@ -184,7 +203,7 @@ public class CompetitionWS {
|
||||
// return Uni.createFrom().item(new Message<>(message.uuid(), message.code(), MessageType.REPLY, "ko"));
|
||||
}
|
||||
|
||||
public static Uni<Void> sendNotifyToOtherEditor(WebSocketConnection connection, String code, Object data) {
|
||||
public static void sendNotifyToOtherEditor(WebSocketConnection connection, String code, Object data) {
|
||||
String uuid = connection.pathParam("uuid");
|
||||
|
||||
List<Uni<Void>> queue = new ArrayList<>();
|
||||
@@ -196,7 +215,15 @@ public class CompetitionWS {
|
||||
}
|
||||
});
|
||||
|
||||
return Uni.join().all(queue).andCollectFailures().onFailure().recoverWithNull().replaceWithVoid();
|
||||
Uni.join().all(queue)
|
||||
.andCollectFailures()
|
||||
.runSubscriptionOn(executor)
|
||||
.subscribeAsCompletionStage()
|
||||
.whenComplete((v, t) -> {
|
||||
if (t != null) {
|
||||
LOGGER.error("Error sending ws_out message", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnError
|
||||
|
||||
19
src/main/java/fr/titionfire/ffsaf/ws/ExecutorProducer.java
Normal file
19
src/main/java/fr/titionfire/ffsaf/ws/ExecutorProducer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,6 @@ import lombok.Data;
|
||||
public class WelcomeInfo {
|
||||
private String name;
|
||||
private String perm;
|
||||
private boolean show_blason;
|
||||
private boolean show_flag;
|
||||
}
|
||||
|
||||
@@ -80,35 +80,32 @@ public class RCardboard {
|
||||
return Uni.createFrom().nullItem();
|
||||
return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel));
|
||||
}))
|
||||
.call(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model)))
|
||||
.invoke(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "getCardboardWithoutThis", permission = PermLevel.VIEW)
|
||||
public Uni<CardboardAllMatch> getCardboardWithoutThis(WebSocketConnection connection, Long matchId) {
|
||||
return getById(matchId, connection)
|
||||
.chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2", matchModel.getCategory().getCompet(), matchModel)
|
||||
.chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2",
|
||||
matchModel.getCategory().getCompet(), matchModel)
|
||||
.map(models -> {
|
||||
CardboardAllMatch out = new CardboardAllMatch();
|
||||
|
||||
models.stream().filter(c -> (matchModel.getC1_id() != null
|
||||
&& Objects.equals(c.getComb(), matchModel.getC1_id()))
|
||||
|| (matchModel.getC1_guest() != null
|
||||
&& Objects.equals(c.getGuestComb(), matchModel.getC1_guest())))
|
||||
.forEach(c -> {
|
||||
out.c1_yellow += c.getYellow();
|
||||
out.c1_red += c.getRed();
|
||||
});
|
||||
|
||||
models.stream().filter(c -> (matchModel.getC2_id() != null
|
||||
&& Objects.equals(c.getComb(), matchModel.getC2_id()))
|
||||
|| (matchModel.getC2_guest() != null
|
||||
&& Objects.equals(c.getGuestComb(), matchModel.getC2_guest())))
|
||||
.forEach(c -> {
|
||||
out.c2_yellow += c.getYellow();
|
||||
out.c2_red += c.getRed();
|
||||
});
|
||||
|
||||
for (CardboardModel c : models) {
|
||||
if ((matchModel.getC1_id() != null && Objects.equals(c.getComb(),
|
||||
matchModel.getC1_id())) || (matchModel.getC1_guest() != null && Objects.equals(
|
||||
c.getGuestComb(), matchModel.getC1_guest()))) {
|
||||
out.c1_yellow += c.getYellow();
|
||||
out.c1_red += c.getRed();
|
||||
}
|
||||
if ((matchModel.getC2_id() != null && Objects.equals(c.getComb(),
|
||||
matchModel.getC2_id())) || (matchModel.getC2_guest() != null && Objects.equals(
|
||||
c.getGuestComb(), matchModel.getC2_guest()))) {
|
||||
out.c2_yellow += c.getYellow();
|
||||
out.c2_red += c.getRed();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ package fr.titionfire.ffsaf.ws.recv;
|
||||
import fr.titionfire.ffsaf.data.model.CategoryModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.model.TreeModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CategoryRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.TreeRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
@@ -46,6 +43,9 @@ public class RCategorie {
|
||||
@Inject
|
||||
TreeRepository treeRepository;
|
||||
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
|
||||
return categoryRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
@@ -97,7 +97,7 @@ public class RCategorie {
|
||||
|
||||
return categoryRepository.create(categoryModel);
|
||||
})
|
||||
.call(cat -> SSCategorie.sendAddCategory(connection, cat))
|
||||
.invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
|
||||
.map(CategoryModel::getId);
|
||||
}
|
||||
|
||||
@@ -118,9 +118,10 @@ public class RCategorie {
|
||||
uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId()))
|
||||
.chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat));
|
||||
}
|
||||
return uni;
|
||||
Uni<Long> finalUni = uni;
|
||||
return Panache.withTransaction(() -> finalUni);
|
||||
})
|
||||
.call(cat -> SSCategorie.sendCategory(connection, cat))
|
||||
.invoke(cat -> SSCategorie.sendCategory(connection, cat))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -201,7 +202,18 @@ public class RCategorie {
|
||||
.call(__ -> treeRepository.flush())
|
||||
.call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
|
||||
.map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
|
||||
.chain(trees -> SSCategorie.sendTreeCategory(connection, trees)))
|
||||
.invoke(trees -> SSCategorie.sendTreeCategory(connection, trees)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "deleteCategory", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
|
||||
return getById(id, connection)
|
||||
.call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
|
||||
.call(__ -> cardboardRepository.delete("match.category = ?1", cat))
|
||||
.call(__ -> matchRepository.delete("category = ?1", cat))))
|
||||
.chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
|
||||
.invoke(__ -> SSCategorie.sendDelCategory(connection, id))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ public class RMatch {
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
@@ -84,7 +87,7 @@ public class RMatch {
|
||||
}))
|
||||
.chain(categoryModel -> creatMatch(categoryModel, m))
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
|
||||
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -121,7 +124,7 @@ public class RMatch {
|
||||
mm.setC2_guest(null);
|
||||
}))
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
|
||||
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -136,7 +139,7 @@ public class RMatch {
|
||||
m.getCategory_ord()))
|
||||
.invoke(m -> m.setCategory_ord(order.pos))
|
||||
.call(m -> Panache.withTransaction(() -> matchRepository.persist(m)))
|
||||
.call(mm -> SSMatch.sendMatchOrder(connection, order))
|
||||
.invoke(mm -> SSMatch.sendMatchOrder(connection, order))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -163,12 +166,12 @@ public class RMatch {
|
||||
.call(mm -> {
|
||||
if (mm.isEnd() && mm.win() != old_win && mm.getCategory_ord() == -42) {
|
||||
return updateEndAndTree(mm, new ArrayList<>())
|
||||
.call(l -> SSMatch.sendMatch(connection, l));
|
||||
.invoke(l -> SSMatch.sendMatch(connection, l));
|
||||
}
|
||||
return Uni.createFrom().nullItem();
|
||||
});
|
||||
})
|
||||
.call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.invoke(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -189,7 +192,7 @@ public class RMatch {
|
||||
})
|
||||
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
|
||||
.chain(mm -> updateEndAndTree(mm, toSend))
|
||||
.call(__ -> SSMatch.sendMatch(connection, toSend))
|
||||
.invoke(__ -> SSMatch.sendMatch(connection, toSend))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -277,8 +280,11 @@ public class RMatch {
|
||||
@WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
|
||||
return getById(idMatch, connection)
|
||||
.chain(matchModel -> Panache.withTransaction(() -> matchRepository.delete(matchModel)))
|
||||
.call(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
|
||||
.map(__ -> idMatch)
|
||||
.chain(l -> Panache.withTransaction(() ->
|
||||
cardboardRepository.delete("match.id = ?1", l)
|
||||
.chain(__ -> matchRepository.delete("id = ?1", l))))
|
||||
.invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@@ -292,9 +298,11 @@ public class RMatch {
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
}))
|
||||
.call(cm -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm)
|
||||
.call(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove)))
|
||||
.call(cm -> matchRepository.list("id IN ?1 AND category = ?2",
|
||||
.call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
(Panache.withTransaction(
|
||||
() -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm))
|
||||
.invoke(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove))))
|
||||
.call(cm -> Panache.withSession(() -> matchRepository.list("id IN ?1 AND category = ?2",
|
||||
Stream.concat(data.matchOrderToUpdate.keySet().stream(),
|
||||
data.matchPouleToUpdate.keySet().stream())
|
||||
.distinct().toList(), cm)
|
||||
@@ -304,19 +312,22 @@ public class RMatch {
|
||||
if (data.matchOrderToUpdate.containsKey(model.getId()))
|
||||
model.setCategory_ord(data.matchOrderToUpdate.get(model.getId()));
|
||||
}))
|
||||
.call(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
|
||||
.invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList()))
|
||||
.call(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
Panache.withTransaction(() -> matchRepository.persist(mm)))
|
||||
.invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
|
||||
)
|
||||
.chain(categoryModel -> {
|
||||
Uni<List<MatchModel>> uni = Uni.createFrom().item(new ArrayList<>());
|
||||
for (AddMatch match : data.newMatch)
|
||||
uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add));
|
||||
return uni;
|
||||
Uni<List<MatchModel>> finalUni = uni;
|
||||
return Panache.withSession(() -> finalUni);
|
||||
}
|
||||
)
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))
|
||||
.invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
|
||||
.call(__ -> SSMatch.sendMatch(connection, matches))
|
||||
.chain(mm -> mm.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
Panache.withTransaction(() -> matchRepository.create(mm))
|
||||
.invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())))
|
||||
.invoke(__ -> SSMatch.sendMatch(connection, matches))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
1
src/main/webapp/.gitignore
vendored
1
src/main/webapp/.gitignore
vendored
@@ -22,3 +22,4 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/public/result_test.html
|
||||
|
||||
164
src/main/webapp/package-lock.json
generated
164
src/main/webapp/package-lock.json
generated
@@ -18,7 +18,9 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"obs-websocket-js": "^5.0.7",
|
||||
"proj4": "^2.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -1058,6 +1060,14 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
|
||||
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1845,6 +1855,11 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@@ -1870,6 +1885,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
@@ -2007,7 +2027,6 @@
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -2980,6 +2999,11 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -3018,8 +3042,7 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.0.6",
|
||||
@@ -3360,6 +3383,14 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
||||
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
|
||||
@@ -3447,6 +3478,17 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3474,6 +3516,14 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -3559,8 +3609,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
@@ -3696,6 +3745,39 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obs-websocket-js": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.7.tgz",
|
||||
"integrity": "sha512-SdSNSyrLVR6F0ogInKr7qcadV1tYaTUse/vbabxjkUL8hU3P3dyifxkZ7pEkDDrtCp3TkQ53Enx23kgZO0Cjcw==",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^2.7.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"type-fest": "^3.11.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/obs-websocket-js/node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"node_modules/obs-websocket-js/node_modules/type-fest": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
|
||||
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -3752,6 +3834,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3855,6 +3942,11 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/proj4": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz",
|
||||
@@ -4055,6 +4147,25 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
|
||||
@@ -4256,6 +4367,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
|
||||
@@ -4319,6 +4435,11 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
@@ -4378,6 +4499,14 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string.prototype.matchall": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
|
||||
@@ -4723,6 +4852,11 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uzip": {
|
||||
"version": "0.20201231.0",
|
||||
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
|
||||
@@ -4922,6 +5056,26 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"obs-websocket-js": "^5.0.7",
|
||||
"proj4": "^2.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
BIN
src/main/webapp/public/Logo-FFSAF-2023.png
Normal file
BIN
src/main/webapp/public/Logo-FFSAF-2023.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
946
src/main/webapp/public/competition.js
Normal file
946
src/main/webapp/public/competition.js
Normal file
@@ -0,0 +1,946 @@
|
||||
let apiUrlRoot = "";
|
||||
const rootDiv = document.getElementById("safca_api_data");
|
||||
|
||||
const header = `<h4>Résultat de la compétition :</h4>`
|
||||
const backButton = `<a onclick="setSubPage('home')" href="javascript:void(0);">Retour</a>`
|
||||
const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
|
||||
style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png"
|
||||
alt="">`
|
||||
|
||||
const voidFunction = () => {
|
||||
}
|
||||
let lastRf = 0;
|
||||
let rfFonction = voidFunction;
|
||||
|
||||
setInterval(() => {
|
||||
rfFonction();
|
||||
}, 15000);
|
||||
|
||||
function setSubPage(name) {
|
||||
window.location.hash = name;
|
||||
const location = name.split('/');
|
||||
console.log(location);
|
||||
|
||||
switch (location[0]) {
|
||||
case 'home':
|
||||
homePage();
|
||||
break;
|
||||
case 'poule':
|
||||
poulePage(location);
|
||||
break;
|
||||
case 'comb':
|
||||
combPage(location);
|
||||
break;
|
||||
case 'club':
|
||||
clubPage(location);
|
||||
break;
|
||||
case 'all':
|
||||
combsPage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function homePage() {
|
||||
rootDiv.innerHTML = header;
|
||||
|
||||
let content = document.createElement('div');
|
||||
content.innerHTML = `
|
||||
<ul>
|
||||
<li><a onclick="setSubPage('poule')" href="javascript:void(0);">Par catégorie</a></li>
|
||||
<li><a onclick="setSubPage('comb')" href="javascript:void(0);">Par combattant</a></li>
|
||||
<li><a onclick="setSubPage('club')" href="javascript:void(0);">Par club</a></li>
|
||||
<li><a onclick="setSubPage('all')" href="javascript:void(0);">Tous les combattants</a></li>
|
||||
</ul>
|
||||
`
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
let loadingAnimationStep = 0;
|
||||
|
||||
function startLoading(root) {
|
||||
const id = "loading" + Math.random().toString(36);
|
||||
let element = document.createElement('h2');
|
||||
element.id = id;
|
||||
|
||||
const anim = setInterval(() => {
|
||||
let str = "Chargement";
|
||||
for (let i = 0; i < loadingAnimationStep; i++) {
|
||||
str += ".";
|
||||
}
|
||||
loadingAnimationStep = (loadingAnimationStep + 1) % 4;
|
||||
element.innerText = str;
|
||||
}, 500);
|
||||
|
||||
root.append(element)
|
||||
return {interval: anim, root: root, element: element};
|
||||
}
|
||||
|
||||
function stopLoading(loading) {
|
||||
clearInterval(loading['interval']);
|
||||
loading['root'].removeChild(loading['element']);
|
||||
}
|
||||
|
||||
function scoreToString(score) {
|
||||
const scorePrint = (s1) => {
|
||||
switch (s1) {
|
||||
case -997:
|
||||
return "disc.";
|
||||
case -998:
|
||||
return "abs.";
|
||||
case -999:
|
||||
return "for.";
|
||||
case -1000:
|
||||
return "";
|
||||
default:
|
||||
return String(s1);
|
||||
}
|
||||
}
|
||||
|
||||
return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ");
|
||||
}
|
||||
|
||||
function dateToString(date) {
|
||||
if (date === null || date === undefined)
|
||||
return "";
|
||||
|
||||
const date_ = new Date(date);
|
||||
const date_2 = new Date(date);
|
||||
const current = new Date();
|
||||
current.setHours(0, 0, 0, 0);
|
||||
date_2.setHours(0, 0, 0, 0);
|
||||
|
||||
let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24));
|
||||
if (d === 0)
|
||||
return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
else if (d === 1)
|
||||
return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
else if (d === 2)
|
||||
return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
||||
else
|
||||
return date_.toLocaleDateString();
|
||||
}
|
||||
|
||||
function buildPouleMenu(isPoule, change_view) {
|
||||
const menuDiv = document.createElement('div');
|
||||
menuDiv.id = 'menu';
|
||||
menuDiv.style.borderBottom = '1px solid #9EA0A1';
|
||||
menuDiv.style.paddingBottom = '25px';
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
ul.id = 'onglets';
|
||||
ul.style.position = 'absolute';
|
||||
ul.style.border = '1px solid transparent';
|
||||
ul.style.padding = '0';
|
||||
ul.style.font = 'bold 11px Batang, arial, serif';
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.left = '50%';
|
||||
ul.style.marginTop = '0';
|
||||
ul.style.width = '430px';
|
||||
ul.style.marginLeft = '-215px';
|
||||
|
||||
function createTab(text, isActive, onClickHandler) {
|
||||
const li = document.createElement('li');
|
||||
if (isActive) {
|
||||
li.className = 'active';
|
||||
li.style.borderBottom = '1px solid #fff';
|
||||
li.style.backgroundColor = '#fff';
|
||||
} else {
|
||||
li.style.backgroundColor = '#F4F9FD';
|
||||
}
|
||||
li.style.float = 'left';
|
||||
li.style.height = '21px';
|
||||
li.style.margin = '2px 2px 0 2px !important';
|
||||
li.style.margin = '1px 2px 0 2px';
|
||||
li.style.border = '1px solid #9EA0A1';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = 'javascript:void(0);';
|
||||
a.onclick = onClickHandler;
|
||||
a.textContent = text;
|
||||
a.style.display = 'block';
|
||||
a.style.color = '#666';
|
||||
a.style.textDecoration = 'none';
|
||||
a.style.padding = '4px';
|
||||
|
||||
a.addEventListener('mouseover', function () {
|
||||
a.style.background = '#fff';
|
||||
});
|
||||
|
||||
a.addEventListener('mouseout', function () {
|
||||
if (!isActive)
|
||||
a.style.background = '#F4F9FD';
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
return li;
|
||||
}
|
||||
|
||||
const li1 = createTab('Poule', isPoule, function () {
|
||||
change_view(true);
|
||||
});
|
||||
ul.appendChild(li1);
|
||||
const li2 = createTab('Tournois', !isPoule, function () {
|
||||
change_view(false);
|
||||
});
|
||||
ul.appendChild(li2);
|
||||
|
||||
menuDiv.appendChild(ul);
|
||||
|
||||
return menuDiv;
|
||||
}
|
||||
|
||||
function buildMatchArray(matchs) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 800px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Rouge</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
<th class="has-text-align-center" data-align="center">Scores</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
<th class="has-text-align-center" data-align="center">Bleu</th>
|
||||
<th class="has-text-align-center" data-align="center">Date</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const match of matchs) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-right" data-align="right">${match.red}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.red_w ? cupImg : ""}</td>
|
||||
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.blue_w ? cupImg : ""}</td>
|
||||
<td class="has-text-align-left" data-align="left">${match.blue}</td>
|
||||
<td class="has-text-align-center" data-align="center">${dateToString((match.red_w || match.blue_w) ? match.date : null)}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
pouleDiv.innerHTML = arrayContent;
|
||||
return pouleDiv;
|
||||
}
|
||||
|
||||
function buildRankArray(rankArray) {
|
||||
const arrayDiv = document.createElement('div');
|
||||
let arrayContent = `<figure class="wp-block-table is-style-stripes" style="font-size: 16px; margin-top: 2em">
|
||||
<table style="width: 600px;overflow: auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Place</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoire</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const row of rankArray) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-center" data-align="center">${row.rank}</td>
|
||||
<td class="has-text-align-left" data-align="left">${row.name}</td>
|
||||
<td class="has-text-align-center" data-align="center">${row.win}</td>
|
||||
<td class="has-text-align-center" data-align="center">${row.pointRate.toFixed(3)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${row.pointMake}</td>
|
||||
<td class="has-text-align-center" data-align="center">${row.pointTake}</td>
|
||||
</tr>`
|
||||
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
arrayDiv.innerHTML = arrayContent;
|
||||
return arrayDiv;
|
||||
}
|
||||
|
||||
function buildTree(treeData) {
|
||||
return drawGraph(initTree(treeData))
|
||||
}
|
||||
|
||||
function poulePage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par catégorie</h4>';
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
|
||||
let currentPoule = 0;
|
||||
const loadPoule = () => {
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/poule/data?poule=${currentPoule}&rf=${lastRf}`)
|
||||
.then(response => {
|
||||
if (response.status === 204) // No content => no update
|
||||
return;
|
||||
|
||||
response.json().then(poule => {
|
||||
lastRf = (poule.genTime !== undefined) ? poule.genTime : 0;
|
||||
// console.log(poule);
|
||||
|
||||
if (location.length === 3 && poule.type < 3) {
|
||||
location.pop();
|
||||
window.location.hash = location.join('/');
|
||||
}
|
||||
|
||||
dataContainer.replaceChildren();
|
||||
|
||||
if (poule.type === 1) {
|
||||
for (const g in poule.matchs) {
|
||||
if (Object.keys(poule.matchs).length > 1) {
|
||||
const text = document.createElement('h4');
|
||||
text.textContent = `Groupe ${g}`;
|
||||
text.style.marginTop = '2em';
|
||||
dataContainer.append(text);
|
||||
}
|
||||
dataContainer.append(buildMatchArray(poule.matchs[g]));
|
||||
dataContainer.append(buildRankArray(poule.rankArray[g]));
|
||||
}
|
||||
} else if (poule.type === 2) {
|
||||
dataContainer.append(buildTree(poule['trees']));
|
||||
} else {
|
||||
const change_view = (isPoule) => {
|
||||
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view));
|
||||
|
||||
if (isPoule) {
|
||||
for (const g in poule.matchs) {
|
||||
if (Object.keys(poule.matchs).length > 1) {
|
||||
const text = document.createElement('h4');
|
||||
text.textContent = `Groupe ${g}`;
|
||||
text.style.marginTop = '2em';
|
||||
dataContainer.append(text);
|
||||
}
|
||||
dataContainer.append(buildMatchArray(poule.matchs[g]));
|
||||
dataContainer.append(buildRankArray(poule.rankArray[g]));
|
||||
}
|
||||
} else {
|
||||
dataContainer.append(buildTree(poule['trees']));
|
||||
}
|
||||
|
||||
location[2] = isPoule ? 1 : 2;
|
||||
window.location.hash = location.join('/');
|
||||
}
|
||||
change_view((location.length === 3) ? location[2] === '1' : true);
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
dataContainer.replaceChildren(new Text("Erreur de chargement de la poule"));
|
||||
})
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/poule/list`)
|
||||
.then(response => response.json())
|
||||
.then(poule => {
|
||||
const select = document.createElement('select');
|
||||
select.setAttribute('id', poule.id);
|
||||
select.innerHTML = `<option value="0">--Sélectionner une catégorie--</option>`;
|
||||
for (const pouleKey of Object.keys(poule).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))) {
|
||||
select.innerHTML += `<option value="${poule[pouleKey]}">${pouleKey}</option>`;
|
||||
}
|
||||
select.addEventListener('change', e => {
|
||||
location[1] = Object.keys(poule).find(key => poule[key] === Number(e.target.value));
|
||||
location[1] = encodeURI(location[1]);
|
||||
window.location.hash = location.join('/');
|
||||
lastRf = 0;
|
||||
currentPoule = e.target.value;
|
||||
loadPoule();
|
||||
})
|
||||
content.append(select);
|
||||
content.appendChild(dataContainer);
|
||||
|
||||
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
|
||||
const tmp = poule[decodeURI(location[1])];
|
||||
select.value = tmp;
|
||||
currentPoule = tmp
|
||||
loadPoule();
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des catégories")))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rfFonction = () => {
|
||||
if (currentPoule !== 0)
|
||||
loadPoule();
|
||||
}
|
||||
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
function buildCombView(comb) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>Info :</h3>
|
||||
<ul>
|
||||
<li>Nom Prénom : ${comb.name}</li>
|
||||
<li>Club : ${comb.club}</li>
|
||||
<li>Catégorie : ${comb.cat}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<ul>
|
||||
<li>Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})</li>
|
||||
<li>Points marqués : ${comb.pointMake}</li>
|
||||
<li>Points reçus : ${comb.pointTake}</li>
|
||||
<li>Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des matchs:</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 700px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Date</th>
|
||||
<th class="has-text-align-center" data-align="center">Poule</th>
|
||||
<th class="has-text-align-center" data-align="center">Adversaire</th>
|
||||
<th class="has-text-align-center" data-align="center">Scores</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio</th>
|
||||
<th class="has-text-align-center" data-align="center"></th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const match of comb.matchs) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-center" data-align="center">${dateToString(match.date)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.poule}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.adv}</td>
|
||||
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.ratio.toFixed(3)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.win ? cupImg : ""}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
pouleDiv.innerHTML = arrayContent;
|
||||
return pouleDiv;
|
||||
}
|
||||
|
||||
function combPage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par combattant</h4>';
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
|
||||
const loadComb = (id) => {
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/comb/data?comb=${id}`)
|
||||
.then(response => response.json())
|
||||
.then(comb => {
|
||||
console.log(comb);
|
||||
dataContainer.replaceChildren(buildCombView(comb));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant")))
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/comb/list`)
|
||||
.then(response => response.json())
|
||||
.then(combs => {
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML = `<option value="0">--Sélectionner un combattant--</option>`;
|
||||
for (const comb of Object.keys(combs).sort()) {
|
||||
select.innerHTML += `<option value="${combs[comb]}">${comb}</option>`;
|
||||
}
|
||||
select.addEventListener('change', e => {
|
||||
location[1] = Object.keys(combs).find(key => combs[key] === e.target.value);
|
||||
location[1] = encodeURI(location[1]);
|
||||
window.location.hash = location.join('/');
|
||||
loadComb(e.target.value);
|
||||
})
|
||||
content.append(select);
|
||||
content.appendChild(dataContainer);
|
||||
|
||||
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
|
||||
const tmp = combs[decodeURI(location[1])];
|
||||
select.value = tmp;
|
||||
loadComb(tmp);
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des combattants")))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
function buildClubView(club) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>Info :</h3>
|
||||
<ul>
|
||||
<li>Nom : ${club.name}</li>
|
||||
<li>Nombre d'inscris : ${club.nb_insc}</li>
|
||||
</ul>
|
||||
<h3>Statistique :</h3>
|
||||
<ul>
|
||||
<li>Nombre de match disputé : ${club.nb_match}</li>
|
||||
<li>Nombre de victoires : ${club.match_w}</li>
|
||||
<li>Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}</li>
|
||||
<li>Points marqués : ${club.pointMake}</li>
|
||||
<li>Points reçus : ${club.pointTake}</li>
|
||||
<li>Ratio de points moyen : ${club.ratioPoint.toFixed(3)}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des menbres :</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 800px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Catégorie</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Défaites</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio points</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const comb of club.combs) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-center" data-align="center">${comb.cat}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.name}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.w}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.l}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.ratioVictoire.toFixed(3)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.pointMake}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.pointTake}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.ratioPoint.toFixed(3)}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
pouleDiv.innerHTML = arrayContent;
|
||||
return pouleDiv;
|
||||
}
|
||||
|
||||
function clubPage(location) {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
content.innerHTML = '<h4>Recherche par club</h4>';
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
|
||||
const loadComb = (id) => {
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/club/data?club=${id}`)
|
||||
.then(response => response.json())
|
||||
.then(club => {
|
||||
console.log(club);
|
||||
dataContainer.replaceChildren(buildClubView(club));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club")))
|
||||
.finally(() => stopLoading(loading));
|
||||
}
|
||||
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/club/list`)
|
||||
.then(response => response.json())
|
||||
.then(clubs => {
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML = `<option value="0">--Sélectionner un club--</option>`;
|
||||
for (const club of Object.keys(clubs).sort()) {
|
||||
select.innerHTML += `<option value="${clubs[club]}">${club}</option>`;
|
||||
}
|
||||
select.addEventListener('change', e => {
|
||||
if (e.target.value === '0')
|
||||
return;
|
||||
location[1] = Object.keys(clubs).find(key => clubs[key] === Number(e.target.value));
|
||||
location[1] = encodeURI(location[1]);
|
||||
window.location.hash = location.join('/');
|
||||
loadComb(e.target.value);
|
||||
})
|
||||
content.append(select);
|
||||
content.appendChild(dataContainer);
|
||||
|
||||
if (location.length > 1 && location[1] !== undefined && location[1] !== '') {
|
||||
const tmp = clubs[decodeURI(location[1])];
|
||||
select.value = tmp;
|
||||
loadComb(tmp);
|
||||
}
|
||||
})
|
||||
.catch(() => rootDiv.append(new Text("Erreur de chargement des clubs")))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
function buildCombsView(combs) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>Statistique :</h3>
|
||||
<ul>
|
||||
<li>Nombre d'inscris : ${combs.nb_insc}</li>
|
||||
<li>Nombre de match disputé : ${combs.tt_match}</li>
|
||||
<li>Points marqués : ${combs.point}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Liste des combattants :</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 1200px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">Catégorie</th>
|
||||
<th class="has-text-align-center" data-align="center">Club</th>
|
||||
<th class="has-text-align-center" data-align="center">Nom</th>
|
||||
<th class="has-text-align-center" data-align="center">Victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Défaites</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratio victoires</th>
|
||||
<th class="has-text-align-center" data-align="center">Points marqués</th>
|
||||
<th class="has-text-align-center" data-align="center">Points reçus</th>
|
||||
<th class="has-text-align-center" data-align="center">Ratios points</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const comb of combs.combs) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-center" data-align="center">${comb.cat}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.club}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.name}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.w}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.l}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.ratioVictoire.toFixed(3)}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.pointMake}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.pointTake}</td>
|
||||
<td class="has-text-align-center" data-align="center">${comb.ratioPoint.toFixed(3)}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
pouleDiv.innerHTML = arrayContent;
|
||||
return pouleDiv;
|
||||
}
|
||||
|
||||
function combsPage() {
|
||||
rootDiv.innerHTML = header + backButton;
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/comb/get_all`)
|
||||
.then(response => response.json())
|
||||
.then(combs => {
|
||||
console.log(combs);
|
||||
dataContainer.replaceChildren(buildCombsView(combs));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste")))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
content.append(dataContainer);
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
let path = document.getElementById('safca_api_script').src;
|
||||
const urlParams = new URLSearchParams(new URL(path).search);
|
||||
apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id");
|
||||
|
||||
console.log("apiUrlRoot:", apiUrlRoot)
|
||||
|
||||
let hash = window.location.hash.substring(1);
|
||||
if (hash.length === 0)
|
||||
setSubPage('home');
|
||||
else
|
||||
setSubPage(hash);
|
||||
});
|
||||
|
||||
class TreeNode {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.left = null;
|
||||
this.right = null;
|
||||
}
|
||||
|
||||
death() {
|
||||
let dg = 0;
|
||||
let dd = 0;
|
||||
|
||||
if (this.right != null)
|
||||
dg = this.right.death();
|
||||
|
||||
if (this.left != null)
|
||||
dg = this.left.death();
|
||||
|
||||
return 1 + Math.max(dg, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function initTree(data_in) {
|
||||
out = [];
|
||||
for (const din of data_in) {
|
||||
out.push(parseTree(din));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseTree(data_in) {
|
||||
if (data_in?.data == null)
|
||||
return null;
|
||||
|
||||
let node = new TreeNode(data_in.data);
|
||||
node.left = parseTree(data_in?.left);
|
||||
node.right = parseTree(data_in?.right);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
const max_x = 500;
|
||||
const size = 24;
|
||||
|
||||
function getBounds(root) {
|
||||
let px = max_x;
|
||||
let py;
|
||||
let maxx, minx, miny, maxy
|
||||
|
||||
function drawNode(tree, px, py) {
|
||||
let death = tree.death() - 1
|
||||
|
||||
if (death === 0) {
|
||||
if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0;
|
||||
if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0;
|
||||
} else {
|
||||
if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0))
|
||||
miny = py - size * 2 * death - ((size * 1.5 / 2) | 0);
|
||||
if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0))
|
||||
maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0);
|
||||
}
|
||||
if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8;
|
||||
|
||||
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death);
|
||||
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
|
||||
}
|
||||
|
||||
if (root != null) {
|
||||
py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2;
|
||||
|
||||
maxx = px;
|
||||
minx = px;
|
||||
miny = py - (size * 1.5 / 2) | 0;
|
||||
maxy = py + (size * 1.5 / 2) | 0;
|
||||
|
||||
for (const node of root) {
|
||||
px = px - size * 2 - size * 8;
|
||||
if (minx > px) minx = px;
|
||||
|
||||
drawNode(node, px, py);
|
||||
//graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny);
|
||||
py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
|
||||
px = maxx;
|
||||
}
|
||||
} else {
|
||||
minx = 0;
|
||||
maxx = 0;
|
||||
miny = 0;
|
||||
maxy = 0;
|
||||
}
|
||||
|
||||
return [minx, maxx, miny, maxy];
|
||||
}
|
||||
|
||||
function drawGraph(root = []) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = "myCanvas";
|
||||
canvas.style.border = "1px solid grey";
|
||||
canvas.style.marginTop = "10px";
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const [minx, maxx, miny, maxy] = getBounds(root);
|
||||
canvas.width = maxx - minx;
|
||||
canvas.height = maxy - miny;
|
||||
ctx.translate(-minx, -miny);
|
||||
|
||||
|
||||
ctx.fillStyle = "#000000"
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = "#000000"
|
||||
|
||||
function printText(s, x, y, width, height, lineG, lineD) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
let tSize = 17;
|
||||
let ratioX = height * 1. / 20.;
|
||||
|
||||
ctx.font = "100 " + tSize + "px Arial";
|
||||
|
||||
let mw = width - (ratioX * 2) | 0;
|
||||
if (ctx.measureText(s).width > mw) {
|
||||
let dTextSize = true;
|
||||
do {
|
||||
tSize--;
|
||||
ctx.font = tSize + "px Arial";
|
||||
} while (ctx.measureText(s).width > mw && tSize > 10)
|
||||
|
||||
if (!dTextSize || ctx.measureText(s).width > mw) {
|
||||
let s = "";
|
||||
for (const string2 in s.split(" ")) {
|
||||
if (ctx.measureText(s + string2).width >= mw) {
|
||||
s += "...";
|
||||
break;
|
||||
} else {
|
||||
s += string2 + " "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = ctx.measureText(s)
|
||||
let dx = (width - text.width) / 2;
|
||||
let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
|
||||
|
||||
ctx.fillText(s, dx, dy, width - dy);
|
||||
ctx.restore();
|
||||
|
||||
ctx.beginPath();
|
||||
if (lineD) {
|
||||
ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2);
|
||||
ctx.lineTo(x + width, y + height / 2)
|
||||
}
|
||||
if (lineG) {
|
||||
ctx.moveTo(x, y + height / 2);
|
||||
ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2)
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function printScores(scores, px, py, scale) {
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(px - size * 2, py - size * scale);
|
||||
ctx.font = "100 " + 14 + "px Arial";
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scores[i].s1+"-"+scores[i].s2;
|
||||
const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
|
||||
const text = ctx.measureText(score);
|
||||
let dx = (size * 2 - text.width) / 2;
|
||||
let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2);
|
||||
|
||||
ctx.fillStyle = '#ffffffdd';
|
||||
ctx.fillRect(dx, size * 2 * scale / div * (i) + dy, text.width, 14);
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(score, dx, size * 2 * scale / div * (i) + dy, size * 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawNode(tree, px, py) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(px, py)
|
||||
ctx.lineTo(px - size, py)
|
||||
ctx.stroke();
|
||||
|
||||
let death = tree.death() - 1
|
||||
let match = tree.data
|
||||
|
||||
if (death === 0) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(px - size, py + size)
|
||||
ctx.lineTo(px - size, py - size)
|
||||
|
||||
ctx.moveTo(px - size, py + size)
|
||||
ctx.lineTo(px - size * 2, py + size)
|
||||
ctx.moveTo(px - size, py - size)
|
||||
ctx.lineTo(px - size * 2, py - size)
|
||||
ctx.stroke();
|
||||
|
||||
printScores(match.scores, px, py, 1);
|
||||
|
||||
ctx.fillStyle = "#FF0000";
|
||||
printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size - (size * 1.5 / 2) | 0,
|
||||
size * 8, (size * 1.5) | 0, false, true)
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size - (size * 1.5 / 2) | 0,
|
||||
size * 8, (size * 1.5) | 0, false, true)
|
||||
|
||||
if (max_y < py + size + ((size * 1.5 / 2) | 0)) max_y = py + size + (size * 1.5 / 2) | 0;
|
||||
} else {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(px - size, py)
|
||||
ctx.lineTo(px - size, py + size * 2 * death)
|
||||
ctx.moveTo(px - size, py)
|
||||
ctx.lineTo(px - size, py - size * 2 * death)
|
||||
|
||||
ctx.moveTo(px - size, py + size * 2 * death)
|
||||
ctx.lineTo(px - size * 2, py + size * 2 * death)
|
||||
ctx.moveTo(px - size, py - size * 2 * death)
|
||||
ctx.lineTo(px - size * 2, py - size * 2 * death)
|
||||
ctx.stroke();
|
||||
|
||||
printScores(match.scores, px, py, 1.5);
|
||||
|
||||
ctx.fillStyle = "#FF0000";
|
||||
printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2) | 0,
|
||||
size * 8, (size * 1.5) | 0, true, true)
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2) | 0,
|
||||
size * 8, (size * 1.5) | 0, true, true)
|
||||
|
||||
if (max_y < py + size * 2 * death + ((size * 1.5 / 2) | 0))
|
||||
max_y = py + size * 2 * death + ((size * 1.5 / 2) | 0);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death);
|
||||
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
|
||||
}
|
||||
|
||||
function win(scores) {
|
||||
let sum = 0;
|
||||
for (const score of scores) {
|
||||
if (score.s1 === -1000 || score.s2 === -1000)
|
||||
continue;
|
||||
|
||||
if (score.s1 > score.s2)
|
||||
sum++;
|
||||
else if (score.s1 < score.s2)
|
||||
sum--;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
let px = max_x;
|
||||
let py;
|
||||
let max_y
|
||||
|
||||
if (root != null) {
|
||||
py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2;
|
||||
|
||||
max_y = py + (size * 1.5 / 2) | 0;
|
||||
|
||||
for (const node of root) {
|
||||
let win_name = "";
|
||||
if (node.data.end) {
|
||||
if (win(node.data.scores) > 0)
|
||||
win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName;
|
||||
else
|
||||
win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName;
|
||||
}
|
||||
|
||||
|
||||
ctx.fillStyle = "#18A918";
|
||||
printText(win_name, px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0),
|
||||
size * 8, (size * 1.5) | 0, true, false);
|
||||
|
||||
px = px - size * 2 - size * 8;
|
||||
|
||||
drawNode(node, px, py);
|
||||
|
||||
py = max_y + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
|
||||
px = max_x;
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
828
src/main/webapp/public/obs_template.json
Normal file
828
src/main/webapp/public/obs_template.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx";
|
||||
import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx";
|
||||
import {MePage} from "./pages/MePage.jsx";
|
||||
import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx";
|
||||
import {FallingLines} from "react-loader-spinner";
|
||||
import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
|
||||
26
src/main/webapp/src/assets/SimpleIconsOBS.ts
Normal file
26
src/main/webapp/src/assets/SimpleIconsOBS.ts
Normal 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
|
||||
};
|
||||
@@ -93,7 +93,7 @@ export function TextField({name, text, value, placeholder, type = "text", disabl
|
||||
<div className="input-group">
|
||||
<span className="input-group-text" id={name}>{text}</span>
|
||||
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
|
||||
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required}/>
|
||||
name={name} aria-describedby={name} defaultValue={value} disabled={disabled} required={required} autoComplete="false"/>
|
||||
</div>
|
||||
{ttip}
|
||||
</div>
|
||||
@@ -119,13 +119,13 @@ export function CheckField({name, text, value, row = false}) {
|
||||
</>
|
||||
}
|
||||
|
||||
export const Checkbox = ({label, value, onChange}) => {
|
||||
export const Checkbox = ({label, value, onChange, name = "checkbox1"}) => {
|
||||
const handleChange = () => {
|
||||
onChange(!value);
|
||||
};
|
||||
|
||||
return <div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" id="checkbox1" checked={value} onChange={handleChange}/>
|
||||
<label className="form-check-label" htmlFor="checkbox1">{label}</label>
|
||||
<input className="form-check-input" type="checkbox" id={name} checked={value} onChange={handleChange}/>
|
||||
<label className="form-check-label" htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import './Nav.css'
|
||||
import {NavLink} from "react-router-dom";
|
||||
import {useAuth} from "../hooks/useAuth.jsx";
|
||||
import {login, logout} from "../utils/auth.js";
|
||||
import {isClubAdmin} from "../utils/Tools.js";
|
||||
|
||||
export function Nav() {
|
||||
|
||||
@@ -43,9 +44,9 @@ function AffiliationMenu() {
|
||||
}
|
||||
|
||||
function CompMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
const {is_authenticated} = useAuth()
|
||||
|
||||
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
|
||||
if (!is_authenticated)
|
||||
return <></>
|
||||
|
||||
return <li className="nav-item dropdown">
|
||||
@@ -55,7 +56,8 @@ function CompMenu() {
|
||||
<ul className="dropdown-menu">
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions Manager</NavLink></li>
|
||||
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions
|
||||
Manager</NavLink></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
@@ -63,8 +65,7 @@ function CompMenu() {
|
||||
function ClubMenu() {
|
||||
const {is_authenticated, userinfo} = useAuth()
|
||||
|
||||
if (!is_authenticated || !(userinfo?.roles?.includes("club_president")
|
||||
|| userinfo?.roles?.includes("club_secretaire") || userinfo?.roles?.includes("club_respo_intra")))
|
||||
if (!is_authenticated || !(isClubAdmin(userinfo)))
|
||||
return <></>
|
||||
|
||||
return <li className="nav-item dropdown">
|
||||
|
||||
@@ -2,44 +2,21 @@ import {useEffect, useRef, useState, memo} from 'react';
|
||||
|
||||
const cache = {};
|
||||
|
||||
export function SmartLogoBackground({
|
||||
src,
|
||||
export function detectOptimalBackground(blob,
|
||||
darkBackground = '#333333',
|
||||
lightBackground = '#f0f0f0',
|
||||
defaultBackground = 'transparent',
|
||||
tolerance = 55,
|
||||
minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond
|
||||
alt = 'Logo',
|
||||
style = {},
|
||||
imgClassName = '',
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const [background, setBackground] = useState(defaultBackground);
|
||||
const [load, setLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (cache[src]) {
|
||||
setBackground(cache[src]);
|
||||
return;
|
||||
}
|
||||
if (!load)
|
||||
return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
minPixels = 10) {
|
||||
return new Promise((resolve) => {
|
||||
const imgUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = src;
|
||||
|
||||
// Prevent error logging
|
||||
img.onerror = function () {
|
||||
return true;
|
||||
}
|
||||
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
@@ -69,12 +46,9 @@ export function SmartLogoBackground({
|
||||
const i = Math.round(y * canvas.width + x) * 4;
|
||||
if (isTransparent(i)) {
|
||||
const neighbors = [
|
||||
i - 4, // Haut
|
||||
i + 4, // Bas
|
||||
i - 4 * canvas.width, // Gauche
|
||||
i + 4 * canvas.width, // Droite
|
||||
i - 4, i + 4, // Haut/Bas
|
||||
i - 4 * canvas.width, i + 4 * canvas.width, // Gauche/Droite
|
||||
];
|
||||
|
||||
for (const neighbor of neighbors) {
|
||||
if (neighbor >= 0 && neighbor < pixels.length) {
|
||||
if (isLightColor(neighbor)) lightBorderCount++;
|
||||
@@ -82,24 +56,64 @@ export function SmartLogoBackground({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (lightBorderCount >= 25 || darkBorderCount >= 25)
|
||||
break
|
||||
}
|
||||
|
||||
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
|
||||
cache[src] = darkBackground;
|
||||
setBackground(darkBackground)
|
||||
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
|
||||
cache[src] = lightBackground;
|
||||
setBackground(lightBackground)
|
||||
} else {
|
||||
cache[src] = defaultBackground;
|
||||
setBackground(defaultBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
|
||||
resolve(darkBackground); // Fond sombre
|
||||
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
|
||||
resolve(lightBackground); // Fond clair
|
||||
} else {
|
||||
resolve(defaultBackground); // Fond transparent
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
resolve(defaultBackground); // En cas d'erreur
|
||||
};
|
||||
img.src = imgUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export function SmartLogoBackground({
|
||||
src,
|
||||
darkBackground = '#333333',
|
||||
lightBackground = '#f0f0f0',
|
||||
defaultBackground = 'transparent',
|
||||
tolerance = 55,
|
||||
minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond
|
||||
alt = 'Logo',
|
||||
style = {},
|
||||
imgClassName = '',
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const [background, setBackground] = useState(defaultBackground);
|
||||
const [load, setLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!load) return;
|
||||
|
||||
const fetchAndDetect = async () => {
|
||||
if (cache[src]) {
|
||||
setBackground(cache[src]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
const blob = await response.blob();
|
||||
const detectedBackground = await detectOptimalBackground(blob, darkBackground, lightBackground, defaultBackground, tolerance, minPixels);
|
||||
cache[src] = detectedBackground;
|
||||
setBackground(detectedBackground);
|
||||
} catch (error) {
|
||||
console.error("Erreur de détection du fond:", error);
|
||||
cache[src] = defaultBackground;
|
||||
setBackground(defaultBackground);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndDetect();
|
||||
}, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]);
|
||||
|
||||
return <>
|
||||
|
||||
@@ -25,7 +25,7 @@ function reducer(state, action) {
|
||||
country: action.payload.data.country,
|
||||
})
|
||||
if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) {
|
||||
console.debug("Updating comb", comb);
|
||||
//console.debug("Updating comb", comb);
|
||||
return {
|
||||
...state,
|
||||
[comb.id]: comb
|
||||
@@ -49,7 +49,7 @@ function reducer(state, action) {
|
||||
for (const o of combs) {
|
||||
newCombs[o.id] = o;
|
||||
}
|
||||
console.debug("Updating combs", newCombs);
|
||||
//console.debug("Updating combs", newCombs);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import {createContext, useContext, useReducer} from "react";
|
||||
|
||||
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0});
|
||||
const PubAffContext = createContext({
|
||||
next: [],
|
||||
c1: undefined,
|
||||
c2: undefined,
|
||||
showScore: true,
|
||||
timeCb: undefined,
|
||||
timeCb2: undefined,
|
||||
scoreRouge: 0,
|
||||
scoreBleu: 0
|
||||
});
|
||||
const PubAffDispatchContext = createContext(() => {
|
||||
});
|
||||
|
||||
@@ -11,6 +20,8 @@ function reducer(state, action) {
|
||||
case 'CALL_TIME':
|
||||
if (state.timeCb)
|
||||
state.timeCb(action.payload)
|
||||
if (state.timeCb2)
|
||||
state.timeCb2(action.payload)
|
||||
return state
|
||||
case 'CLEAR_CB_TIME':
|
||||
return {...state, timeCb: undefined}
|
||||
|
||||
183
src/main/webapp/src/hooks/useOBS.jsx
Normal file
183
src/main/webapp/src/hooks/useOBS.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import {createContext, useContext, useEffect, useRef, useState} from "react";
|
||||
import OBSWebSocket from "obs-websocket-js";
|
||||
import {hex2rgb} from "../utils/Tools.js";
|
||||
|
||||
const OBSContext = createContext({connected: false, obs: null})
|
||||
|
||||
export function OBSProvider({children}) {
|
||||
const obs = useRef(null)
|
||||
const obsParm = useRef(null)
|
||||
const assets = useRef(null)
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [doReconnect, setDoReconnect] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!doReconnect)
|
||||
return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (obs.current && !connected && doReconnect) {
|
||||
console.log("Reconnecting to OBS WebSocket...")
|
||||
obs.current.connect(obsParm.current.adresse, obsParm.current.password)
|
||||
.then(data => {
|
||||
console.log("Reconnected to OBS WebSocket", data)
|
||||
setDoReconnect(false)
|
||||
})
|
||||
}
|
||||
}, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, [connected, doReconnect]);
|
||||
|
||||
const connect = (adresse, password = undefined, assets_dir = undefined) => {
|
||||
if (connected && obs.current)
|
||||
return;
|
||||
assets.current = assets_dir;
|
||||
|
||||
const obs_ = new OBSWebSocket();
|
||||
obs_.connect(adresse, password)
|
||||
.then(data => {
|
||||
console.log("Connected to OBS WebSocket", data)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to connect to OBS WebSocket", err)
|
||||
});
|
||||
obs_.on('ConnectionOpened', () => {
|
||||
setConnected(true)
|
||||
console.log("OBS WebSocket connection opened")
|
||||
});
|
||||
obs_.on('ConnectionClosed', err => {
|
||||
setConnected(false)
|
||||
console.log("OBS WebSocket connection closed", err.code, err.message)
|
||||
if (err.code === 1000 || err.code === 4009) // 1000 = Normal Closure, 4009 = Authentication Failure
|
||||
return;
|
||||
obsParm.current = {adresse, password}
|
||||
setDoReconnect(true)
|
||||
});
|
||||
obs_.on('error', err => {
|
||||
console.error("OBS WebSocket error", err)
|
||||
});
|
||||
|
||||
obs.current = obs_;
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (obs.current && connected) {
|
||||
obs.current.disconnect();
|
||||
obs.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = {connected, obs: obs.current, connect, disconnect, assets: assets.current}
|
||||
return <OBSContext.Provider value={ret}>
|
||||
{children}
|
||||
</OBSContext.Provider>
|
||||
}
|
||||
|
||||
function getElementName(element) {
|
||||
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}`
|
||||
}
|
||||
|
||||
export function useOBS() {
|
||||
const {connected, obs, connect, disconnect, assets} = useContext(OBSContext)
|
||||
const setTextAndColor = (element, text, color) => {
|
||||
if (!connected)
|
||||
return;
|
||||
if (color.startsWith("#")){
|
||||
const tmp = hex2rgb(color);
|
||||
color = (tmp.b << 16) + (tmp.g << 8) + tmp.r;
|
||||
}
|
||||
|
||||
obs.call('SetInputSettings', {
|
||||
inputName: getElementName(element),
|
||||
inputSettings: {
|
||||
text: text,
|
||||
color: color,
|
||||
overlay: true,
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Failed to set text and color for OBS element", element, err)
|
||||
});
|
||||
}
|
||||
const setText = (element, text) => {
|
||||
if (!connected)
|
||||
return;
|
||||
obs.call('SetInputSettings', {
|
||||
inputName: getElementName(element),
|
||||
inputSettings: {
|
||||
text: text,
|
||||
overlay: true,
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Failed to set text for OBS element", element, err)
|
||||
});
|
||||
}
|
||||
|
||||
const setDiapo = (element, files) => {
|
||||
if (!connected)
|
||||
return;
|
||||
const arr = [];
|
||||
for (const file of files) {
|
||||
arr.push({
|
||||
hidden: false,
|
||||
selected: false,
|
||||
value: (assets.endsWith('/') ? assets : assets + '/') + file,
|
||||
})
|
||||
}
|
||||
obs.call('SetInputSettings', {
|
||||
inputName: getElementName(element),
|
||||
inputSettings: {
|
||||
files: arr,
|
||||
overlay: true,
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Failed to set diapo for OBS element", element, err)
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
connected,
|
||||
obs,
|
||||
connect,
|
||||
disconnect,
|
||||
setText,
|
||||
setTextAndColor,
|
||||
setDiapo,
|
||||
}
|
||||
}
|
||||
|
||||
export function exportOBSConfiguration(adresse, password, assets_dir) {
|
||||
const config = {
|
||||
adresse: adresse,
|
||||
password: password,
|
||||
assets_dir: assets_dir,
|
||||
};
|
||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2));
|
||||
const downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute("href", dataStr);
|
||||
downloadAnchorNode.setAttribute("download", "obs_configuration.json");
|
||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
export async function importOBSConfiguration() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
input.onchange = e => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
try {
|
||||
const config = JSON.parse(event.target.result);
|
||||
resolve(config);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react";
|
||||
import {apiAxios} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useAuth} from "./useAuth.jsx";
|
||||
|
||||
function uuidv4() {
|
||||
@@ -45,6 +43,8 @@ export function WSProvider({url, onmessage, children}) {
|
||||
const id = useId();
|
||||
const {is_authenticated} = useAuth()
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [doReconnect, setDoReconnect] = useState(false)
|
||||
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
|
||||
const [state, dispatch] = useReducer(reducer, {listener: []})
|
||||
const ws = useRef(null)
|
||||
const listenersRef = useRef([])
|
||||
@@ -59,37 +59,51 @@ export function WSProvider({url, onmessage, children}) {
|
||||
listenersRef.current = state.listener
|
||||
}, [state.listener])
|
||||
|
||||
const welcomeListener = ({data}) => {
|
||||
setWelcomeData({
|
||||
name: data.name,
|
||||
perm: data.perm,
|
||||
show_blason: data.show_blason,
|
||||
show_flag: data.show_flag
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!doReconnect && !is_authenticated && isReady)
|
||||
return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (isReady || !doReconnect || !is_authenticated)
|
||||
return;
|
||||
|
||||
console.log("WSProvider: reconnecting to", url);
|
||||
try {
|
||||
const newSocket = new WebSocket(url)
|
||||
newSocket.onopen = ws.current.onopen
|
||||
newSocket.onclose = ws.current.onclose
|
||||
newSocket.onmessage = ws.current.onmessage
|
||||
ws.current = newSocket
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, [isReady, doReconnect, is_authenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mountCounter[id])
|
||||
mountCounter[id] = 0
|
||||
mountCounter[id] += 1
|
||||
setDoReconnect(true)
|
||||
console.log(`WSProvider ${id} mounted ${mountCounter[id]} time(s)`);
|
||||
|
||||
if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)){
|
||||
if (mountCounter[id] === 1 && (ws.current === null || ws.current.readyState >= WebSocket.CLOSING)) {
|
||||
console.log("WSProvider: connecting to", url);
|
||||
const socket = new WebSocket(url)
|
||||
|
||||
socket.onopen = () => setIsReady(true)
|
||||
socket.onclose = () => {
|
||||
setIsReady(false)
|
||||
if (mountCounter[id] > 0) {
|
||||
setTimeout(() => {
|
||||
//if (is_authenticated){
|
||||
console.log("WSProvider: reconnecting to", url);
|
||||
try {
|
||||
const newSocket = new WebSocket(url)
|
||||
ws.current = newSocket
|
||||
newSocket.onopen = socket.onopen
|
||||
newSocket.onclose = socket.onclose
|
||||
newSocket.onmessage = socket.onmessage
|
||||
}catch (e) {
|
||||
|
||||
}
|
||||
//}else{
|
||||
// console.log("WSProvider: not reconnecting, user is not authenticated");
|
||||
//}
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
socket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data)
|
||||
@@ -118,6 +132,12 @@ export function WSProvider({url, onmessage, children}) {
|
||||
console.error("Listener callback error:", err)
|
||||
}
|
||||
});
|
||||
|
||||
if (msg.code === 'welcomeInfo') {
|
||||
welcomeListener({...msg})
|
||||
isHandled = true;
|
||||
}
|
||||
|
||||
if (!isHandled && onmessage)
|
||||
onmessage(JSON.parse(event.data))
|
||||
}
|
||||
@@ -132,6 +152,7 @@ export function WSProvider({url, onmessage, children}) {
|
||||
setTimeout(() => {
|
||||
console.log(`WSProvider ${id} checking for close, ${mountCounter[id]} instance(s) remain`);
|
||||
if (mountCounter[id] === 0) {
|
||||
setDoReconnect(false)
|
||||
console.log("WSProvider: closing connection to", url);
|
||||
ws.current.close()
|
||||
}
|
||||
@@ -139,13 +160,14 @@ export function WSProvider({url, onmessage, children}) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const send = (uuid, code, type, data, resolve = () => {
|
||||
const send2 = (uuid, code, type, data, resolve = () => {
|
||||
}, reject = () => {
|
||||
}) => {
|
||||
if (!isReadyRef.current) {
|
||||
reject("WebSocket is not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "REQUEST") {
|
||||
const timeout = setTimeout(() => {
|
||||
reject("timeout");
|
||||
@@ -164,7 +186,7 @@ export function WSProvider({url, onmessage, children}) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("WSProvider: sending message", {uuid, code, type, data});
|
||||
//console.log("WSProvider: sending message", {uuid, code, type, data});
|
||||
ws.current?.send(JSON.stringify({
|
||||
uuid: uuid,
|
||||
code: code,
|
||||
@@ -172,20 +194,41 @@ export function WSProvider({url, onmessage, children}) {
|
||||
data: data
|
||||
}))
|
||||
}
|
||||
const send = (uuid, code, type, data, resolve = () => {
|
||||
}, reject = () => {
|
||||
}) => {
|
||||
if (isReadyRef.current) {
|
||||
send2(uuid, code, type, data, resolve, reject);
|
||||
} else {
|
||||
let counter = 0;
|
||||
const waitInterval = setInterval(() => {
|
||||
if (isReadyRef.current) {
|
||||
clearInterval(waitInterval);
|
||||
send2(uuid, code, type, data, resolve, reject);
|
||||
}
|
||||
counter += 1;
|
||||
if (counter >= 300) { // 30 seconds timeout
|
||||
clearInterval(waitInterval);
|
||||
reject("WebSocket is not connected");
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef}
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData}
|
||||
return <WebsocketContext.Provider value={ret}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>
|
||||
}
|
||||
|
||||
export function useWS() {
|
||||
const {isReady, dispatch, send, wait_length} = useContext(WebsocketContext)
|
||||
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext)
|
||||
return {
|
||||
dispatch,
|
||||
isReady,
|
||||
wait_length,
|
||||
welcomeData,
|
||||
sendRequest: (code, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
send(uuidv4(), code, "REQUEST", data, resolve, reject);
|
||||
@@ -229,7 +272,7 @@ export function useRequestWS(code, payload, setLoading = null, loadingLevel = 1)
|
||||
useEffect(() => {
|
||||
if (isReady)
|
||||
refresh(code, payload)
|
||||
else{
|
||||
else {
|
||||
if (setLoading)
|
||||
setLoading(loadingLevel)
|
||||
setTimeout(() => refresh(code, payload), 1000)
|
||||
|
||||
@@ -299,7 +299,7 @@ function AssoInfo({initData, needFile}) {
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="input-group">
|
||||
<label className="input-group-text" htmlFor="status">Statue{needFile && "*"}</label>
|
||||
<label className="input-group-text" htmlFor="status">Statuts{needFile && "*"}</label>
|
||||
<input type="file" className="form-control" id="status" name="status" accept=".pdf,.txt" required={needFile}/>
|
||||
</div>
|
||||
{!needFile && <div className="form-text" id="status">Laissez vide pour ne rien changer. (Si un statu a déjà été envoyé lors de cette
|
||||
@@ -313,7 +313,7 @@ function MembreInfo({role, initData}) {
|
||||
|
||||
return <>
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôles</label>
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Rôle</label>
|
||||
<select className="form-select" id="inputGroupSelect01" defaultValue={initData.role ? initData.role : (role === "m1" ? "PRESIDENT" : 0)}
|
||||
disabled={initData.role ? initData.role === "PRESIDENT" : role === "m1"} name={role + "_role"} required>
|
||||
<option value="0">Sélectionner...</option>
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
faUserGroup,
|
||||
faVenus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {CheckField} from "../components/MemberCustomFiels.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, getCatName} from "../utils/Tools.js";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -30,12 +34,13 @@ export function MePage() {
|
||||
</div>
|
||||
<div className="col-lg-8">
|
||||
<InformationForm data={data}/>
|
||||
<SettingsCard data={data}/>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,16 +93,62 @@ function PhotoCard({data}) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
function SelectCard() {
|
||||
function SelectCard({userData}) {
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header">Sélection en équipe de France</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{userData?.selections && userData.selections.sort((a, b) => b.saison - a.saison).map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
</div>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function SettingsCard({data}) {
|
||||
const [privacy, setPrivacy] = useState("PUBLIC");
|
||||
|
||||
export function InformationForm({data}) {
|
||||
useEffect(() => {
|
||||
if (data?.resultPrivacy) {
|
||||
setPrivacy(data.resultPrivacy);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const formData = new FormData();
|
||||
formData.append("resultPrivacy", e.target.value);
|
||||
toast.promise(apiAxios.put(`/member/me/setting`, formData),
|
||||
{
|
||||
pending: 'Mise à jours des paramètres en cours...',
|
||||
success: 'Paramètres mis à jours avec succès 🎉',
|
||||
error: 'Échec de la mise à jours des paramètres 😕'
|
||||
})
|
||||
.then(() => {
|
||||
setPrivacy(String(formData.get("resultPrivacy")));
|
||||
});
|
||||
}
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">Paramètres du compte</div>
|
||||
<div className="card-body">
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="email_notifications">Visibilité des résultats</label>
|
||||
<select className="form-select" id="result_visibility" name="result_visibility" required value={privacy} onChange={handleChange}>
|
||||
<option value="PUBLIC">Public (visible par tous)</option>
|
||||
<option value="REGISTERED_ONLY">Membres connectés (visibles par les membres de la fédération)</option>
|
||||
<option value="REGISTERED_ONLY_NO_DETAILS">Membres connectés - masquer les détails (visibles par les membres de la fédération)
|
||||
</option>
|
||||
<option value="PRIVATE">Privé (visible uniquement par moi)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function InformationForm({data}) {
|
||||
const style = {marginRight: '0.7em'}
|
||||
|
||||
return <div className="card mb-4">
|
||||
@@ -127,4 +178,4 @@ export function InformationForm({data}) {
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function MemberList({source}) {
|
||||
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
const [showLicenceState, setShowLicenceState] = useState(false);
|
||||
const [showLicenceState, setShowLicenceState] = useState((sessionStorage.getItem("showLicenceState") || "false") === "true");
|
||||
|
||||
const setFilter = (filter) => {
|
||||
navigate("#" + encodeURI(JSON.stringify(filter)))
|
||||
@@ -35,6 +35,7 @@ export function MemberList({source}) {
|
||||
payment: 2,
|
||||
order: "",
|
||||
categorie: "",
|
||||
archived: false,
|
||||
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
|
||||
}
|
||||
|
||||
@@ -43,10 +44,10 @@ export function MemberList({source}) {
|
||||
data,
|
||||
error,
|
||||
refresh
|
||||
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`, setLoading, 1)
|
||||
} = useFetch(`/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`, setLoading, 1)
|
||||
|
||||
useEffect(() => {
|
||||
const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}`;
|
||||
const tmp = `/member/find/${source}?page=${filter.page}&search=${filter.search}&club=${filter.club}&licenceRequest=${filter.licenceRequest}&payment=${filter.payment}&order=${filter.order}&categorie=${filter.categorie}&archive=${filter.archived}`;
|
||||
if (tmp === lastRefresh)
|
||||
return;
|
||||
lastRefresh = tmp
|
||||
@@ -76,6 +77,7 @@ export function MemberList({source}) {
|
||||
}, [data, licenceData]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("showLicenceState", showLicenceState);
|
||||
if (!showLicenceState)
|
||||
return;
|
||||
|
||||
@@ -137,6 +139,8 @@ export function MemberList({source}) {
|
||||
<div className="card-body">
|
||||
<FiltreBar showLicenceState={showLicenceState}
|
||||
setShowLicenceState={setShowLicenceState}
|
||||
showArchived={filter.archived}
|
||||
setShowArchived={e => setFilter({...filter, archived: e})}
|
||||
clubFilter={filter.club}
|
||||
setClubFilter={e => setFilter({...filter, club: e})}
|
||||
source={source}
|
||||
@@ -523,6 +527,8 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
|
||||
function FiltreBar({
|
||||
showLicenceState,
|
||||
setShowLicenceState,
|
||||
showArchived,
|
||||
setShowArchived,
|
||||
clubFilter,
|
||||
setClubFilter,
|
||||
source,
|
||||
@@ -538,6 +544,9 @@ function FiltreBar({
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label="Afficher l'état des licences"/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label="Afficher les combattants inactifs"/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">--- toute les catégories ---</option>
|
||||
|
||||
@@ -4,13 +4,14 @@ import {AxiosError} from "../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {apiAxios, errFormater} from "../utils/Tools.js";
|
||||
import {apiAxios, errFormater, getCatName} from "../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SearchBar} from "../components/SearchBar.jsx";
|
||||
import {ConfirmDialog} from "../components/ConfirmDialog.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./PayAndValidateList.css";
|
||||
import * as Tools from "../utils/Tools.js";
|
||||
|
||||
export function PayAndValidateList({source}) {
|
||||
|
||||
@@ -22,6 +23,7 @@ export function PayAndValidateList({source}) {
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
const [clubFilter, setClubFilter] = useState("");
|
||||
const [catFilter, setCatFilter] = useState("");
|
||||
const [stateFilter, setStateFilter] = useState((source === "club") ? 1 : 2)
|
||||
const [lastSearch, setLastSearch] = useState("");
|
||||
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
|
||||
@@ -34,15 +36,15 @@ export function PayAndValidateList({source}) {
|
||||
data,
|
||||
error,
|
||||
refresh
|
||||
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}`, setLoading, 1)
|
||||
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1)
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
|
||||
}, [selectedMembers]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
|
||||
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter]);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
|
||||
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
@@ -62,7 +64,7 @@ export function PayAndValidateList({source}) {
|
||||
setMemberData(data2);
|
||||
}, [data, licenceData]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLicenceData = () => {
|
||||
toast.promise(
|
||||
apiAxios.get(`/licence/current/${source}`),
|
||||
{
|
||||
@@ -77,6 +79,10 @@ export function PayAndValidateList({source}) {
|
||||
.then(data => {
|
||||
setLicenceData(data.data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLicenceData();
|
||||
}, []);
|
||||
|
||||
const search = (search) => {
|
||||
@@ -104,7 +110,31 @@ export function PayAndValidateList({source}) {
|
||||
}
|
||||
).then(() => {
|
||||
setSelectedMembers([]);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}`);
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
|
||||
});
|
||||
}
|
||||
|
||||
const handlePayment = () => {
|
||||
if (selectedMembers.length === 0) {
|
||||
toast.error("Aucun membre sélectionné");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/licence/validate-pay`, selectedMembers),
|
||||
{
|
||||
pending: "Validation des licences en cours...",
|
||||
success: "Licences validées avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la validation des licences")
|
||||
}
|
||||
}
|
||||
}
|
||||
).then(() => {
|
||||
setSelectedMembers([]);
|
||||
fetchLicenceData();
|
||||
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,13 +183,20 @@ export function PayAndValidateList({source}) {
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} source={source}
|
||||
stateFilter={stateFilter} setStateFilter={setStateFilter} paymentFilter={paymentFilter}
|
||||
setPaymentFilter={setPaymentFilter}/>
|
||||
setPaymentFilter={setPaymentFilter} setCatFilter={setCatFilter} catFilter={catFilter}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
{source === "admin" && <>
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation">Valider
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-pay">Valider le payement
|
||||
des {selectedMembers.length} licences sélectionnée
|
||||
</button>
|
||||
<ConfirmDialog title="Payment des licences"
|
||||
message={"Êtes-vous sûr de vouloir marquer comme payées les " + selectedMembers.length + " licences ?"}
|
||||
onConfirm={handlePayment} id="confirm-pay"/>
|
||||
|
||||
<button className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#confirm-validation" style={{marginTop: "0.5em"}}>Valider
|
||||
les {selectedMembers.length} licences sélectionnée
|
||||
</button>
|
||||
<ConfirmDialog title="Validation des licences"
|
||||
@@ -385,7 +422,7 @@ function MakeRow({member, source, isChecked, onCheckboxClick, onRowClick}) {
|
||||
|
||||
let allClub = []
|
||||
|
||||
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter}) {
|
||||
function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setStateFilter, paymentFilter, setPaymentFilter, setCatFilter, catFilter}) {
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
@@ -395,6 +432,14 @@ function FiltreBar({data, clubFilter, setClubFilter, source, stateFilter, setSta
|
||||
|
||||
return <div>
|
||||
{source !== "club" && <ClubSelectFilter clubFilter={clubFilter} setClubFilter={setClubFilter}/>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">--- toute les catégories ---</option>
|
||||
{Tools.CatList.map(cat => (
|
||||
<option key={cat} value={cat}>{getCatName(cat)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={stateFilter} onChange={event => setStateFilter(Number(event.target.value))}>
|
||||
<option value={1}>Avec demande ou licence validée</option>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function StatsLazy({data}) {
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "3em"}}>
|
||||
<h3>Nombre de licences par catégorie pour {getSaison()}</h3>
|
||||
<div className="col-lg-8" style={{margin: "auto"}}>
|
||||
<div className="col-lg-8" style={{margin: "auto"}}>
|
||||
<div style={{width: '100%', aspectRatio: 1.5}}>
|
||||
<CatGraph raw_data={data}/>
|
||||
</div>
|
||||
@@ -60,7 +60,8 @@ function NbGraph({raw_data}) {
|
||||
<Area type="monotone" dataKey="na" name="Non définie" stackId="1" stroke={colors[0]} fill={colors[0]}/>
|
||||
<Area type="monotone" dataKey="h" name="Homme" stackId="1" stroke={colors[1]} fill={colors[1]}/>
|
||||
<Area type="monotone" dataKey="f" name="Femme" stackId="1" stroke={colors[2]} fill={colors[2]}/>
|
||||
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5" fillOpacity="30%"/>
|
||||
<Area type="monotone" dataKey="not_valid" name="Non validée" stackId="2" stroke={colors[3]} fill={colors[3]} strokeDasharray="5 5"
|
||||
fillOpacity="30%"/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
}
|
||||
@@ -75,25 +76,25 @@ function CatGraph({raw_data}) {
|
||||
for (const k of raw_data.categories) {
|
||||
data.push({
|
||||
name: k.name,
|
||||
value: k.count,
|
||||
value: k.count + 1,
|
||||
})
|
||||
}
|
||||
setShowData(data)
|
||||
}, [raw_data])
|
||||
|
||||
const COLORS = ['#33FF8F', '#30E393', '#2DC697', '#2AAA9B'
|
||||
, '#278E9F', '#2471A3', '#2155A7', '#1E39AB'
|
||||
, '#1B1CAF', '#1800B3'];
|
||||
const COLORS = ['#ffff99', '#a6cee3', '#1f78b4', '#b2df8a'
|
||||
, '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f'
|
||||
, '#ff7f00', '#cab2d6', '#6a3d9a'];
|
||||
|
||||
const RADIAN = Math.PI / 180;
|
||||
const renderCustomizedLabel = ({cx, cy, midAngle, innerRadius, outerRadius, index}) => {
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.6;
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
return <> {
|
||||
showData[index]['value'] > 0 &&
|
||||
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||
<text x={x} y={y} fill={(index === 2 || index === 6 || index === 10) ? "white" : "black"} textAnchor="middle" dominantBaseline="central">
|
||||
{`${showData[index]['name']}`}
|
||||
</text>
|
||||
|
||||
@@ -159,9 +160,9 @@ function CatGraph({raw_data}) {
|
||||
|
||||
onMouseEnter={onPieEnter}>
|
||||
{showData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]}/>
|
||||
<Cell key={`cell-${index}`} fill={COLORS[(index) % COLORS.length]}/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ function InformationForm({data}) {
|
||||
}
|
||||
|
||||
return <>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id} readOnly hidden/>
|
||||
<input name="clubId" value={data.clubId} readOnly hidden/>
|
||||
|
||||
@@ -60,7 +60,7 @@ function InformationForm() {
|
||||
}
|
||||
|
||||
return <>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Nouveau club</div>
|
||||
<div className="card-body text-center">
|
||||
|
||||
@@ -67,10 +67,10 @@ export function InformationForm({data}) {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}).then(_ => {
|
||||
toast.success('Profile mis à jours avec succès 🎉');
|
||||
toast.success('Profil mis à jours avec succès 🎉');
|
||||
}).catch(e => {
|
||||
console.log(e.response)
|
||||
toast.error(errFormater(e,'Échec de la mise à jours du profile 😕'));
|
||||
toast.error(errFormater(e,'Échec de la mise à jours du profil 😕'));
|
||||
}).finally(() => {
|
||||
if (setLoading)
|
||||
setLoading(0)
|
||||
@@ -79,7 +79,7 @@ export function InformationForm({data}) {
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
return <form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-body">
|
||||
@@ -114,4 +114,4 @@ export function InformationForm({data}) {
|
||||
</div>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectCard} from "./SelectCard.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -58,7 +59,7 @@ export function MemberPage() {
|
||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
@@ -94,14 +95,3 @@ function PhotoCard({data}) {
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function SelectCard() {
|
||||
return <></>
|
||||
/*return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header">Sélection en équipe de France</div>
|
||||
<div className="card-body">
|
||||
<p className="mb-1">Web Design</p>
|
||||
|
||||
</div>
|
||||
</div>;*/
|
||||
}
|
||||
|
||||
208
src/main/webapp/src/pages/admin/member/SelectCard.jsx
Normal file
208
src/main/webapp/src/pages/admin/member/SelectCard.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, CatList, errFormater, getCatName, getSaison} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
function selectionReducer(selections, action) {
|
||||
switch (action.type) {
|
||||
case 'ADD':
|
||||
return [
|
||||
...selections,
|
||||
action.payload
|
||||
]
|
||||
case 'REMOVE':
|
||||
return selections.filter(selection => selection.id !== action.payload)
|
||||
case 'UPDATE_OR_ADD':
|
||||
const index = selections.findIndex(selection => selection.id === action.payload.id)
|
||||
if (index === -1) {
|
||||
return [
|
||||
...selections,
|
||||
action.payload
|
||||
]
|
||||
} else {
|
||||
selections[index] = action.payload
|
||||
return [...selections]
|
||||
}
|
||||
case 'SORT':
|
||||
return selections.sort((a, b) => b.saison - a.saison)
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
export function SelectCard({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
|
||||
|
||||
const [modalSelection, setModal] = useState({id: -1, membre: userData.id})
|
||||
const [selections, dispatch] = useReducer(selectionReducer, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
for (const dataKey of data) {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: dataKey})
|
||||
}
|
||||
dispatch({type: 'SORT'})
|
||||
}, [data]);
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Sélection en équipe de France</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#SelectionModal"
|
||||
onClick={() => setModal({id: -1, membre: userData.id, categorie: userData.categorie})}>Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{selections.map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
<button className="badge btn btn-primary rounded-pill" data-bs-toggle="modal"
|
||||
data-bs-target="#SelectionModal" onClick={_ => setModal(selection)}>
|
||||
<FontAwesomeIcon icon={faPen}/></button>
|
||||
</div>
|
||||
})}
|
||||
{error && <AxiosError error={error}/>}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="SelectionModal" tabIndex="-1" aria-labelledby="SelectionModalLabel"
|
||||
aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<ModalContent selection={modalSelection} dispatch={dispatch}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function sendSelection(event, dispatch) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
formData.set('selection', event.target.selection?.value?.length > 0 ? event.target.selection?.value : null)
|
||||
formData.set('categorie', event.target.categorie?.value)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/selection/${event.target.membre.value}`, {
|
||||
id: event.target.id.value,
|
||||
membre: event.target.membre.value,
|
||||
saison: event.target.saison.value,
|
||||
categorie: event.target.categorie.value,
|
||||
}),
|
||||
{
|
||||
pending: "Enregistrement de la séléction en cours",
|
||||
success: "Séléction enregistrée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de l'enregistrement de la séléction")
|
||||
}
|
||||
}
|
||||
}
|
||||
).then(data => {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: data.data})
|
||||
dispatch({type: 'SORT'})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function removeSelection(id, dispatch) {
|
||||
toast.promise(
|
||||
apiAxios.delete(`/selection/${id}`),
|
||||
{
|
||||
pending: "Suppression de la séléction en cours",
|
||||
success: "Séléction supprimée avec succès 🎉",
|
||||
error: {
|
||||
render({data}) {
|
||||
return errFormater(data, "Échec de la suppression de la séléction")
|
||||
}
|
||||
}
|
||||
}
|
||||
).then(_ => {
|
||||
dispatch({type: 'REMOVE', payload: id})
|
||||
})
|
||||
}
|
||||
|
||||
function ModalContent({selection, dispatch}) {
|
||||
const [saison, setSaison] = useState(0)
|
||||
const [cat, setCat] = useState("")
|
||||
const [isNew, setNew] = useState(true)
|
||||
|
||||
const setSeason = (event) => {
|
||||
setSaison(Number(event.target.value))
|
||||
}
|
||||
const handleCatChange = (event) => {
|
||||
setCat(event.target.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selection.id !== -1) {
|
||||
setNew(false)
|
||||
setSaison(selection.saison)
|
||||
} else {
|
||||
setNew(true)
|
||||
setSaison(getSaison())
|
||||
}
|
||||
setCat(selection.categorie)
|
||||
}, [selection]);
|
||||
|
||||
return <form onSubmit={e => sendSelection(e, dispatch)}>
|
||||
<input name="id" value={selection.id} readOnly hidden/>
|
||||
<input name="membre" value={selection.membre} readOnly hidden/>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="SelectionModalLabel">Edition de la séléction</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3 justify-content-md-center">
|
||||
{isNew
|
||||
? <input type="number" className="form-control" placeholder="Saison" name="saison"
|
||||
aria-label="Saison" aria-describedby="basic-addon2" value={saison} onChange={setSeason}/>
|
||||
: <><span className="input-group-text" id="basic-addon2">{saison}</span>
|
||||
<input name="saison" value={saison} readOnly hidden/></>}
|
||||
<span className="input-group-text" id="basic-addon2">-</span>
|
||||
<span className="input-group-text" id="basic-addon2">{saison + 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<label className="input-group-text" htmlFor="inputGroupSelect01">Catégorie</label>
|
||||
<select className="form-select" id="inputGroupSelect01" value={cat} onChange={handleCatChange} name="categorie" required>
|
||||
<option>Choisir...</option>
|
||||
{CatList.map((cat) => {
|
||||
return (<option key={cat} value={cat}>{getCatName(cat)}</option>)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
{isNew || <button type="button" className="btn btn-danger" data-bs-dismiss="modal"
|
||||
onClick={() => removeSelection(selection.id, dispatch)}>Supprimer</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
function RadioGroupeOnOff({value, onChange, name, text}) {
|
||||
return <div className="btn-group input-group mb-3 justify-content-md-center" role="group"
|
||||
aria-label="Basic radio toggle button group">
|
||||
<span className="input-group-text">{text}</span>
|
||||
<input type="radio" className="btn-check" id={"btnradio1" + name} autoComplete="off"
|
||||
value="false" checked={value === false} onChange={onChange}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio1" + name}>Non</label>
|
||||
<input type="radio" className="btn-check" name={name} id={"btnradio2" + name} autoComplete="off"
|
||||
value="true" checked={value === true} onChange={onChange}/>
|
||||
<label className="btn btn-outline-primary" htmlFor={"btnradio2" + name}>Oui</label>
|
||||
</div>;
|
||||
}
|
||||
@@ -155,8 +155,8 @@ function ModalContent2({clubData, data}) {
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Veuillez sélectionner 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
|
||||
vous pourrez toujours remplacer un des membres sélectionné à la prochaine étape)</p>
|
||||
<p>Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain,
|
||||
vous pourrez les renseigner à la prochaine étape)</p>
|
||||
{data && data.map((d, index) => {
|
||||
return <div key={index} className="input-group mb-1">
|
||||
<div className="input-group-text">
|
||||
|
||||
@@ -67,7 +67,7 @@ function InformationForm({data}) {
|
||||
}
|
||||
|
||||
return <>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<input name="id" value={data.id} readOnly hidden/>
|
||||
<div className="card-header">Affiliation n°{data.no_affiliation}</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function InformationForm({data}) {
|
||||
addPhoto(event, formData, send);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
return <form onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Information</div>
|
||||
<div className="card-body">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {apiAxios, errFormater} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectCard} from "./SelectCard.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -56,7 +57,7 @@ export function MemberPage() {
|
||||
<LoadingProvider><LicenceCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<LoadingProvider><SelectCard/></LoadingProvider>
|
||||
<LoadingProvider><SelectCard userData={data}/></LoadingProvider>
|
||||
</div>
|
||||
</div>
|
||||
{data.licence == null &&
|
||||
@@ -93,14 +94,3 @@ function PhotoCard({data}) {
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function SelectCard() {
|
||||
return <></>
|
||||
/*return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header">Sélection en équipe de France</div>
|
||||
<div className="card-body">
|
||||
<p className="mb-1">Soon</p>
|
||||
|
||||
</div>
|
||||
</div>;*/
|
||||
}
|
||||
|
||||
27
src/main/webapp/src/pages/club/member/SelectCard.jsx
Normal file
27
src/main/webapp/src/pages/club/member/SelectCard.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {getCatName} from "../../../utils/Tools.js";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
|
||||
export function SelectCard({userData}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/selection/${userData.id}`, setLoading, 1)
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">Sélection en équipe de France</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="list-group">
|
||||
{data && data.sort((a, b) => b.saison - a.saison).map((selection, index) => {
|
||||
return <div key={index} className="list-group-item d-flex justify-content-between align-items-start">
|
||||
<div className="me-auto">{selection?.saison}-{selection?.saison + 1} en {getCatName(selection?.categorie)}</div>
|
||||
</div>
|
||||
})}
|
||||
{error && <AxiosError error={error}/>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -51,9 +51,11 @@ export function CompetitionEdit() {
|
||||
<Content data={data} refresh={refresh}/>
|
||||
|
||||
{data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary"
|
||||
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier les participants</button>}
|
||||
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier
|
||||
les participants</button>}
|
||||
|
||||
{data.id !== null && data.system === "SAFCA" && <ContentSAFCA data2={data}/>}
|
||||
{data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
|
||||
<ContentSAFCAAndInternal data2={data} type={data.system}/>}
|
||||
|
||||
{data.id !== null && <>
|
||||
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
|
||||
@@ -72,9 +74,12 @@ export function CompetitionEdit() {
|
||||
</>
|
||||
}
|
||||
|
||||
function ContentSAFCA({data2}) {
|
||||
function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
const getDataPath = type === "SAFCA" ? `/competition/${data2.id}/safcaData` : `/competition/${data2.id}/internalData`
|
||||
const setDataPath = type === "SAFCA" ? "/competition/safcaData" : "/competition/internalData"
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/competition/${data2.id}/safcaData`, setLoading, 1)
|
||||
const {data, error} = useFetch(getDataPath, setLoading, 1)
|
||||
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [state2, dispatch2] = useReducer(SimpleReducer, [])
|
||||
@@ -109,7 +114,7 @@ function ContentSAFCA({data2}) {
|
||||
out['table'] = state2.map(d => d.data)
|
||||
|
||||
toast.promise(
|
||||
apiAxios.post(`/competition/safcaData`, out),
|
||||
apiAxios.post(setDataPath, out),
|
||||
{
|
||||
pending: "Enregistrement des paramètres en cours",
|
||||
success: "Paramètres enregistrée avec succès 🎉",
|
||||
@@ -402,7 +407,8 @@ function Content({data}) {
|
||||
<li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à
|
||||
chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour
|
||||
qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de
|
||||
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> > <strong>Paramètres</strong> >
|
||||
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.
|
||||
<img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
}, [data, clubFilter, catFilter]);
|
||||
|
||||
const sendRegister = (new_state) => {
|
||||
toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
|
||||
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
|
||||
pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: {
|
||||
render({data}) {
|
||||
return data.response.data || "Combattant non trouvé"
|
||||
@@ -75,11 +75,11 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
onClick={() => setModalState({id: 0})}>Ajouter un combattant
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
{source === "admin" && <div className="mb-4">
|
||||
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
|
||||
onClick={() => setModalState({id: -793548328091516928})}>Ajouter un invité
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
<QuickAdd sendRegister={sendRegister} source={source}/>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">Filtre</div>
|
||||
@@ -147,15 +147,19 @@ function SearchMember({sendRegister}) {
|
||||
|
||||
const handleAdd = (name) => {
|
||||
const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name);
|
||||
console.log("Quick add licence:", member)
|
||||
|
||||
if (!member) {
|
||||
toast.error("Combattant non trouvé");
|
||||
return;
|
||||
}
|
||||
|
||||
sendRegister({
|
||||
licence: member.licence, fname: member.fname, lname: member.lname, weight: "", overCategory: 0, lockEdit: false, id: null
|
||||
licence: member.licence,
|
||||
fname: member.fname,
|
||||
lname: member.lname,
|
||||
weight: "",
|
||||
overCategory: 0,
|
||||
lockEdit: false,
|
||||
id: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,7 +169,7 @@ function SearchMember({sendRegister}) {
|
||||
const names = data.map(member => `${member.fname} ${member.lname}`.trim());
|
||||
names.sort((a, b) => a.localeCompare(b));
|
||||
setSuggestions(names);
|
||||
}, []);
|
||||
}, [data]);
|
||||
|
||||
return <>
|
||||
{data ? <div className="row mb-3" style={{marginTop: "0.5em"}}>
|
||||
@@ -331,9 +335,9 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
const new_state = {
|
||||
licence: licence,
|
||||
fname: fname,
|
||||
lname: lname,
|
||||
licence: Number.isInteger(licence) ? licence : licence.trim(),
|
||||
fname: fname.trim(),
|
||||
lname: lname.trim(),
|
||||
weight: weight,
|
||||
overCategory: cat,
|
||||
lockEdit: lockEdit,
|
||||
@@ -346,8 +350,10 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
new_state.country = country_
|
||||
new_state.genre = genre
|
||||
}
|
||||
setModalState(new_state)
|
||||
sendRegister(new_state)
|
||||
.then(() => {
|
||||
setModalState(new_state)
|
||||
})
|
||||
}}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5"
|
||||
@@ -355,7 +361,9 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{modalState.id < 0 && <div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
|
||||
{modalState.id < 0 &&
|
||||
<div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via
|
||||
ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
|
||||
<div className="card" style={{marginBottom: "1em"}}>
|
||||
<div className="card-header">{modalState.id >= 0 ? "Recherche*" : "Information"}</div>
|
||||
<div className="card-body">
|
||||
|
||||
@@ -79,6 +79,13 @@ function MakeContent({data}) {
|
||||
href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank"
|
||||
rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p>
|
||||
}
|
||||
{data.canEditRegisters &&
|
||||
<div style={{marginTop: "0.5em"}}>
|
||||
<button type="button" className="btn btn-primary"
|
||||
onClick={_ => navigate("/competition/" + data.id + "/register")}>Inscription - mode administrateur
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js"
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {CategoryContent} from "./CategoryAdminContent.jsx";
|
||||
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
|
||||
import {createPortal} from "react-dom";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
||||
import JSZip from "jszip";
|
||||
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export function CMAdmin() {
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
export function CMAdmin({compUuid}) {
|
||||
const [catId, setCatId] = useState(null);
|
||||
const [cat, setCat] = useState(null);
|
||||
const menuActions = useRef({});
|
||||
const {dispatch} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
const categoryListener = ({data}) => {
|
||||
if (!cat || data.id !== cat.id)
|
||||
return
|
||||
setCat({
|
||||
...cat,
|
||||
setCat(cat_ => ({
|
||||
...cat_,
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type
|
||||
})
|
||||
}))
|
||||
}
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
||||
}, []);
|
||||
}, [cat]);
|
||||
|
||||
return <>
|
||||
<div className="card">
|
||||
@@ -35,10 +45,261 @@ export function CMAdmin() {
|
||||
<div className="card-body">
|
||||
<LoadingProvider>
|
||||
<div className="row">
|
||||
<CategoryContent cat={cat} catId={catId} setCat={setCat}/>
|
||||
<CategoryContent cat={cat} catId={catId} setCat={setCat} menuActions={menuActions}/>
|
||||
</div>
|
||||
</LoadingProvider>
|
||||
</div>
|
||||
<Menu menuActions={menuActions} compUuid={compUuid}/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
let tto = [];
|
||||
|
||||
function resizeImageWithOptimalBackground(blob) {
|
||||
return new Promise(async (resolve) => {
|
||||
const background = await detectOptimalBackground(blob);
|
||||
const imgUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1080;
|
||||
canvas.height = 1080;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Dessiner l'image centrée
|
||||
const scale = Math.min(1080 / img.width, 1080 / img.height);
|
||||
const newWidth = img.width * scale;
|
||||
const newHeight = img.height * scale;
|
||||
const x = (1080 - newWidth) / 2;
|
||||
const y = (1080 - newHeight) / 2;
|
||||
|
||||
ctx.filter = `drop-shadow(0 0 2rem ${background})`;
|
||||
ctx.drawImage(img, x, y, newWidth, newHeight);
|
||||
|
||||
// Exporter en PNG
|
||||
canvas.toBlob((newBlob) => {
|
||||
resolve(newBlob);
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
}, 'image/png', 1.0);
|
||||
};
|
||||
img.onerror = () => resolve(blob); // Retourne l'original en cas d'erreur
|
||||
img.src = imgUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadResourcesAsZip(resourceList) {
|
||||
const zip = new JSZip();
|
||||
const modal = new bootstrap.Modal(document.getElementById('progressModal'));
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
let completed = 0;
|
||||
|
||||
if (!resourceList.some(d => d.url === '/obs_template.json'))
|
||||
resourceList.push({url: '/obs_template.json', name: 'saf_obs_template.json'});
|
||||
|
||||
// Afficher la modale
|
||||
modal.show();
|
||||
|
||||
// Fonction pour télécharger une ressource et l'ajouter au ZIP
|
||||
const addResourceToZip = async (data) => {
|
||||
try {
|
||||
const response = await fetch(data.url);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return {success: false, filename: data.name || data.url.split('/').pop()};
|
||||
}
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(`Erreur HTTP: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const filename = data.name || data.url.split('/').pop();
|
||||
const format = filename.split('.').pop().toLowerCase();
|
||||
|
||||
if (['png', 'jpg', 'jpeg', 'svg'].includes(format)) {
|
||||
const resizedBlob = await resizeImageWithOptimalBackground(blob);
|
||||
const pngFilename = filename.replace(/\.[^/.]+$/, ".png");
|
||||
zip.file(pngFilename, resizedBlob);
|
||||
return {success: true, pngFilename};
|
||||
} else {
|
||||
zip.file(filename, blob);
|
||||
return {success: true, filename};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Impossible d'ajouter ${data.url} au ZIP:`, error);
|
||||
return {success: false, filename: data.name || data.url.split('/').pop()};
|
||||
}
|
||||
};
|
||||
|
||||
// Télécharger toutes les ressources et mettre à jour la progression
|
||||
await Promise.all(
|
||||
resourceList.map(async (data) => {
|
||||
const result = await addResourceToZip(data);
|
||||
completed++;
|
||||
const progress = Math.round((completed / resourceList.length) * 100);
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`;
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
// Générer le ZIP et déclencher le téléchargement
|
||||
const zipBlob = await zip.generateAsync({type: 'blob'});
|
||||
const zipUrl = URL.createObjectURL(zipBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = zipUrl;
|
||||
a.download = 'ressources.zip';
|
||||
a.click();
|
||||
URL.revokeObjectURL(zipUrl);
|
||||
|
||||
// Fermer la modale
|
||||
modal.hide();
|
||||
progressText.textContent = "Téléchargement terminé !";
|
||||
}
|
||||
|
||||
function Menu({menuActions, compUuid}) {
|
||||
const e = document.getElementById("actionMenu")
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
|
||||
for (const x of tto)
|
||||
x.dispose();
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
|
||||
tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
|
||||
const longTimeAction = (button) => {
|
||||
if (button === "obs") {
|
||||
obsModal.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
const longPressDown = (button) => {
|
||||
longPress.current.button = button;
|
||||
longPress.current.time = new Date();
|
||||
longPress.current.timer = setTimeout(() => {
|
||||
longTimeAction(button);
|
||||
|
||||
longPress.current.time = null;
|
||||
longPress.current.button = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const longPressUp = (button) => {
|
||||
clearTimeout(longPress.current.timer);
|
||||
|
||||
if (longPress.current.time) {
|
||||
const diff = new Date() - longPress.current.time;
|
||||
if (longPress.current.button === button) {
|
||||
if (diff >= 1000) {
|
||||
longTimeAction(button);
|
||||
} else {
|
||||
if (button === "obs") {
|
||||
downloadResourcesAsZip(menuActions.current.resourceList || [])
|
||||
.then(__ => console.log("Ressources téléchargées"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
longPress.current.time = null;
|
||||
longPress.current.button = null;
|
||||
}
|
||||
}
|
||||
|
||||
const handleOBSSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const adresse = form[0].value;
|
||||
const password = form[1].value;
|
||||
const assets_dir = form[2].value;
|
||||
|
||||
exportOBSConfiguration(adresse, password, assets_dir)
|
||||
}
|
||||
|
||||
const copyScriptToClipboard = () => {
|
||||
navigator.clipboard.writeText(`<div id='safca_api_data'></div>
|
||||
<script id="safca_api_script" type="text/javascript" src="${vite_url}/competition.js?id=${compUuid}"></script>`
|
||||
).then(() => {
|
||||
toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.");
|
||||
}).catch(err => {
|
||||
toast.error("Erreur lors de la copie dans le presse-papier : " + err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!e)
|
||||
return <></>;
|
||||
return <>
|
||||
{createPortal(
|
||||
<>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onMouseDown={() => longPressDown("obs")}
|
||||
onMouseUp={() => longPressUp("obs")}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/>
|
||||
<FontAwesomeIcon icon={faGlobe} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer"}}
|
||||
onClick={() => copyScriptToClipboard()}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title="Copier le scripte d'intégration"/>
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
</button>
|
||||
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Configuration OBS</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form onSubmit={handleOBSSubmit}>
|
||||
<div className="modal-body">
|
||||
<strong>/!\ Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en
|
||||
changer entre chaque compétition</strong>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Adresse du serveur</span>
|
||||
<span className="input-group-text">ws://</span>
|
||||
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
|
||||
defaultValue={"127.0.0.1:4455"}/>
|
||||
<span className="input-group-text">/</span>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Mot de passe du serveur</span>
|
||||
<input type="password" className="form-control" placeholder="12345" aria-label=""
|
||||
defaultValue={""}/>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">Dossier des resources</span>
|
||||
<input type="text" className="form-control" placeholder="" aria-label="" required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Exporter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="progressModal" tabIndex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Téléchargement en cours...</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="progress">
|
||||
<div id="progressBar" className="progress-bar" role="progressbar" style={{width: "0%"}}></div>
|
||||
</div>
|
||||
<div id="progressText" className="mt-2">Préparation...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -60,15 +321,34 @@ function CategoryHeader({cat, setCatId}) {
|
||||
data
|
||||
])
|
||||
}
|
||||
const sendAddCategory = ({data}) => {
|
||||
setCats([...cats, data])
|
||||
}
|
||||
const sendDelCategory = ({data}) => {
|
||||
setCatId(catId => {
|
||||
if (catId === data) return null;
|
||||
return catId;
|
||||
})
|
||||
setCats([...cats.filter(c => c.id !== data)])
|
||||
}
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
||||
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: categoryListener})
|
||||
dispatch({type: 'removeListener', payload: sendAddCategory})
|
||||
dispatch({type: 'removeListener', payload: sendDelCategory})
|
||||
}
|
||||
}, [cats]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cats && cats.length > 0 && !cat) {
|
||||
if (cats && cats.length > 0 && (!cat || (cats && !cats.find(c => c.id === cat.id)))) {
|
||||
setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id);
|
||||
} else if (cats && cats.length === 0) {
|
||||
setModal({});
|
||||
bthRef.current.click();
|
||||
}
|
||||
}, [cats]);
|
||||
}, [cats, cat]);
|
||||
|
||||
const handleCatChange = (e) => {
|
||||
const selectedCatId = e.target.value;
|
||||
@@ -77,7 +357,6 @@ function CategoryHeader({cat, setCatId}) {
|
||||
} else { // New category
|
||||
setModal({});
|
||||
bthRef.current.click();
|
||||
console.log(cat);
|
||||
e.target.value = cat?.id;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +365,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
<div className="col-auto">
|
||||
<div className="input-group">
|
||||
<h5 style={{margin: "auto 0.5em auto 0"}}>Edition de la catégorie</h5>
|
||||
<select className="form-select" onChange={handleCatChange}>
|
||||
<select className="form-select" onChange={handleCatChange} value={cat?.id || ""}>
|
||||
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>))}
|
||||
{cats && <option value={-1}>Nouvelle...</option>}
|
||||
@@ -96,7 +375,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
|
||||
{cat &&
|
||||
<div>Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} |
|
||||
Lice: {cat.liceName}</div>}
|
||||
Zone: {cat.liceName}</div>}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary float-end" onClick={() => {
|
||||
@@ -126,7 +405,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
|
||||
function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
const [name, setName] = useState("")
|
||||
const [lice, setLice] = useState("1")
|
||||
const [lice, setLice] = useState("A")
|
||||
const [poule, setPoule] = useState(true)
|
||||
const [tournoi, setTournoi] = useState(false)
|
||||
const [size, setSize] = useState(4)
|
||||
@@ -136,7 +415,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
|
||||
useEffect(() => {
|
||||
setName(state.name || "");
|
||||
setLice(state.liceName || "1");
|
||||
setLice(state.liceName || "A");
|
||||
setPoule(((state.type || 1) & 1) !== 0);
|
||||
setTournoi((state.type & 2) !== 0);
|
||||
|
||||
@@ -162,7 +441,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
|
||||
const regex = /^([^;]+;)*[^;]+$/;
|
||||
if (regex.test(lice.trim()) === false) {
|
||||
toast.error("Le format du nom des lices est invalide. Veuillez séparer les noms par des ';'.");
|
||||
toast.error("Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,8 +547,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
error: 'Erreur lors de la création de la catégorie'
|
||||
}
|
||||
).then(id => {
|
||||
setCatId(id);
|
||||
|
||||
if (tournoi) {
|
||||
const trees = build_tree(size, loserMatch)
|
||||
console.log("Creating trees for new category:", trees);
|
||||
@@ -280,7 +557,9 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
success: 'Arbres créés !',
|
||||
error: 'Erreur lors de la création des arbres'
|
||||
}
|
||||
)
|
||||
).finally(() => setCatId(id))
|
||||
} else {
|
||||
setCatId(id);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -297,7 +576,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">Ajouter une catégorie</h1>
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{state.id === undefined ? "Ajouter" : "Modifier"} une catégorie</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
@@ -309,8 +588,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="liceInput1" className="form-label">Nom des lices <small>(séparée par des ';')</small></label>
|
||||
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="lice" value={lice}
|
||||
<label htmlFor="liceInput1" className="form-label">Nom des zones de combat <small>(séparée par des ';')</small></label>
|
||||
<input type="text" className="form-control" id="liceInput1" placeholder="A;B" name="zone de combat" value={lice}
|
||||
onChange={e => setLice(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
@@ -371,6 +650,22 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Enregistrer</button>
|
||||
{state.id !== undefined && <button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={() => {
|
||||
setConfirm({
|
||||
title: "Suppression de la catégorie",
|
||||
message: `Voulez-vous vraiment supprimer la catégorie ${state.name}. Cela va supprimer tous les matchs associés !`,
|
||||
confirm: () => {
|
||||
toast.promise(sendRequest('deleteCategory', state.id),
|
||||
{
|
||||
pending: 'Suppression de la catégorie...',
|
||||
success: 'Catégorie supprimée !',
|
||||
error: 'Erreur lors de la suppression de la catégorie'
|
||||
}
|
||||
).then(() => setCatId(null));
|
||||
}
|
||||
})
|
||||
confirmRef.current.click();
|
||||
}}>Supprimer</button>}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function ChronoPanel() {
|
||||
})
|
||||
const [chrono, setChrono] = useState({time: 0, startTime: 0})
|
||||
const chronoText = useRef(null)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"})
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
|
||||
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
|
||||
@@ -34,12 +34,12 @@ export function ChronoPanel() {
|
||||
|
||||
const timer = setInterval(() => {
|
||||
let currentDuration = config.time
|
||||
let color = "black"
|
||||
let color = "#000000"
|
||||
if (state_.chronoState === 1) {
|
||||
color = (state_.countBlink < blinkRfDuration) ? "black" : "red"
|
||||
color = (state_.countBlink < blinkRfDuration) ? "#000000" : "#ff0000"
|
||||
} else if (state_.chronoState === 2) {
|
||||
currentDuration = (state_.chronoState === 0) ? 10000 : config.pause
|
||||
color = (state_.countBlink < blinkRfDuration) ? "green" : "red"
|
||||
color = (state_.countBlink < blinkRfDuration) ? "#008000" : "#ff0000"
|
||||
}
|
||||
const timeStr = timePrint(currentDuration - getTime())
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||
import {toast} from "react-toastify";
|
||||
import "./CMTMatchPanel.css"
|
||||
import {useOBS} from "../../../hooks/useOBS.jsx";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
@@ -22,17 +23,36 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
||||
const {dispatch} = useWS();
|
||||
const {connected, setText} = useOBS();
|
||||
|
||||
useEffect(() => {
|
||||
const categoryListener = ({data}) => {
|
||||
setCats([...cats.filter(c => c.id !== data.id), data])
|
||||
}
|
||||
const sendAddCategory = ({data}) => {
|
||||
setCats([...cats, data])
|
||||
}
|
||||
const sendDelCategory = ({data}) => {
|
||||
if (catId === data)
|
||||
setCatId(-1);
|
||||
setCats([...cats.filter(c => c.id !== data)])
|
||||
}
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
||||
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: categoryListener})
|
||||
dispatch({type: 'removeListener', payload: sendAddCategory})
|
||||
dispatch({type: 'removeListener', payload: sendDelCategory})
|
||||
}
|
||||
}, [cats]);
|
||||
|
||||
const cat = cats?.find(c => c.id === catId);
|
||||
|
||||
useEffect(() => {
|
||||
setText("poule",cat ? cat.name : "");
|
||||
}, [cat, connected]);
|
||||
|
||||
return <>
|
||||
<div className="input-group">
|
||||
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
|
||||
@@ -67,7 +87,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
setLoading(1);
|
||||
sendRequest('getFullCategory', catId)
|
||||
.then((data) => {
|
||||
setTrees(data.trees.map(d => from_sendTree(d, true)))
|
||||
setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
@@ -81,7 +101,7 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
const treeListener = ({data}) => {
|
||||
if (data.length < 1 || data[0].categorie !== catId)
|
||||
return
|
||||
setTrees(data.map(d => from_sendTree(d, true)))
|
||||
setTrees(data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
@@ -131,10 +151,15 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
const [type, setType] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cat)
|
||||
return;
|
||||
if ((cat.type & type) === 0)
|
||||
setType(cat.type);
|
||||
}, [cat]);
|
||||
|
||||
if (!cat)
|
||||
return <></>;
|
||||
|
||||
return <div style={{marginTop: "1em"}}>
|
||||
{cat && cat.type === 3 && <>
|
||||
<ul className="nav nav-tabs">
|
||||
@@ -173,6 +198,10 @@ function MatchList({matches, cat, menuActions}) {
|
||||
.map(m => ({...m, win: win(m.scores)}))
|
||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
||||
|
||||
const isActiveMatch = (index) => {
|
||||
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
|
||||
}
|
||||
|
||||
const match = matches.find(m => m.id === activeMatch)
|
||||
useEffect(() => {
|
||||
if (!match) {
|
||||
@@ -183,7 +212,7 @@ function MatchList({matches, cat, menuActions}) {
|
||||
payload: {
|
||||
c1: match.c1,
|
||||
c2: match.c2,
|
||||
next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({
|
||||
next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({
|
||||
c1: m.c1,
|
||||
c2: m.c2
|
||||
}))
|
||||
@@ -198,7 +227,7 @@ function MatchList({matches, cat, menuActions}) {
|
||||
|
||||
useEffect(() => {
|
||||
if (match && match.poule !== lice)
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id)
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
|
||||
}, [lice]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -207,12 +236,12 @@ function MatchList({matches, cat, menuActions}) {
|
||||
if (marches2.some(m => m.id === activeMatch))
|
||||
return;
|
||||
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id);
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
|
||||
}, [matches])
|
||||
return <>
|
||||
{liceName.length > 1 &&
|
||||
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
|
||||
<label className="input-group-text" htmlFor="selectLice">Lice</label>
|
||||
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
|
||||
<label className="input-group-text" htmlFor="selectLice">Zone de combat</label>
|
||||
<select className="form-select" id="selectLice" value={lice} onChange={e => {
|
||||
setLice(e.target.value);
|
||||
localStorage.setItem("cm_lice", e.target.value);
|
||||
@@ -224,11 +253,11 @@ function MatchList({matches, cat, menuActions}) {
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="table-responsive-xxl">
|
||||
<div className="table-responsive-xxl overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
<table className="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
@@ -240,7 +269,7 @@ function MatchList({matches, cat, menuActions}) {
|
||||
<tbody className="table-group-divider">
|
||||
{marches2.map((m, index) => (
|
||||
<tr key={m.id}
|
||||
className={m.id === activeMatch ? "table-info" : (liceName[(index - firstIndex) % liceName.length] === lice ? "" : "table-warning")}
|
||||
className={m.id === activeMatch ? "table-info" : (isActiveMatch(index) ? "" : "table-warning")}
|
||||
onClick={() => setActiveMatch(m.id)}>
|
||||
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
|
||||
{liceName[(index - firstIndex) % liceName.length]}</td>
|
||||
@@ -259,7 +288,7 @@ function MatchList({matches, cat, menuActions}) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -325,27 +354,29 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
|
||||
|
||||
return <div>
|
||||
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
|
||||
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
|
||||
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
|
||||
<div className="overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
|
||||
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
|
||||
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentMatch?.matchSelect &&
|
||||
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScorePanel({matchId, match, menuActions}) {
|
||||
function ScorePanel({matchId, matchs, match, menuActions}) {
|
||||
const onClickVoid = useRef(() => {
|
||||
});
|
||||
|
||||
return <div className="row" onClick={onClickVoid.current}>
|
||||
<ScorePanel_ matchId={matchId} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
|
||||
<ScorePanel_ matchId={matchId} matchs={matchs} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
|
||||
<CardPanel matchId={matchId} match={match}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
|
||||
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
const {sendRequest} = useWS()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
@@ -355,6 +386,11 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
|
||||
const tableRef = useRef(null)
|
||||
const scoreRef = useRef([])
|
||||
const lastScoreClick = useRef(null)
|
||||
const scoreInRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
scoreInRef.current = scoreIn;
|
||||
}, [scoreIn]);
|
||||
|
||||
useEffect(() => {
|
||||
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
|
||||
@@ -403,9 +439,12 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
|
||||
const {matchId, round, comb} = lastScoreClick.current;
|
||||
lastScoreClick.current = null;
|
||||
|
||||
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
|
||||
const scoreIn_ = String(scoreInRef.current).trim() === "" ? -1000 : Number(scoreInRef.current);
|
||||
|
||||
const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round);
|
||||
|
||||
console.log("Updating score", matchId, round, comb, scoreIn_, score);
|
||||
|
||||
const score = match.scores.find(s => s.n_round === round);
|
||||
let newScore;
|
||||
if (score) {
|
||||
if (comb === 1)
|
||||
@@ -474,6 +513,18 @@ function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
|
||||
setEnd(match.end);
|
||||
}, [match]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (inputRef.current && !inputRef.current.contains(event.target)) {
|
||||
onClickVoid();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const o = [...tooltipTriggerList]
|
||||
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user