Compare commits
62 Commits
cad6d14ba8
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 2772e44456 | |||
| 8f0e3c0738 | |||
| 81f6169d41 | |||
| 931c5076b7 | |||
| a328e71035 | |||
| d3805ea0e7 | |||
| 2d883ca1da | |||
| 2501b998fa | |||
| a7c89dbd73 | |||
| b2ee06d3b9 | |||
| 3ed44bc044 | |||
| d091503f19 | |||
| ea17989a82 | |||
| e8555f7992 | |||
| fdd42c6a63 | |||
| ddf530ce4c | |||
| 286911cf89 | |||
| 4bfa6e845d | |||
| 806ead5b98 | |||
| ee0a7d87e9 | |||
| 812d873d5d | |||
| d6d9f86254 | |||
| a22225ee7a | |||
| 2f390b03e2 | |||
| ed5d73c25f | |||
| 0a368454c4 | |||
| 752f03cba5 | |||
| d857fce71f | |||
| 2fd09af0ea | |||
| d43cdc1a4e | |||
| 8663aa61cf | |||
| 758e02dc5b | |||
| 31315c951a | |||
| ab4d57985c | |||
| 2e846bf801 | |||
| 9954dd002c | |||
| 7dab5b8880 | |||
| b37902466c | |||
| 89d9e04a6f | |||
| e2197d0712 | |||
| 172dcdaa67 | |||
| cdd7221e86 | |||
| 952300d063 | |||
| 541d3824f3 | |||
| 4c260b86b9 | |||
| 9018ecef12 | |||
| d749dea6f4 | |||
| 189eb135bb | |||
| 197ee0d5b1 | |||
| 3d8597869d | |||
| 4a07eb4ed9 | |||
| d2a7e6cbac | |||
| 78d22b466d | |||
| b419e2f58d | |||
| a5d3973394 | |||
| 4dff0940c1 | |||
| eed7a10cfb | |||
| 1cbbde6506 | |||
| e8d5d0fa0c | |||
| a5bbc41dfd | |||
| 01436ac220 | |||
| 0150c4fac2 |
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: src/main/webapp/package-lock.json
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ services:
|
||||
- default
|
||||
- intra
|
||||
- nginx
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "https://intra.ffsaf.fr/api" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
ffsaf-db:
|
||||
image: public.ecr.aws/docker/library/postgres:17.2
|
||||
|
||||
@@ -44,7 +44,9 @@ public class FrontendForwardingFilter implements ContainerResponseFilter {
|
||||
final String path = info.getPath();
|
||||
final String address = request.remoteAddress().toString();
|
||||
|
||||
LOG.infof("Request %s %s from IP %s", method, path, address);
|
||||
if (!path.equals("/api")) {
|
||||
LOG.infof("Request %s %s from IP %s", method, path, address);
|
||||
}
|
||||
|
||||
int status = responseContext.getStatus();
|
||||
if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) {
|
||||
|
||||
@@ -21,15 +21,11 @@ public class UserInfoProvider implements ContainerRequestFilter {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
System.out.println(requestContext.getHeaders());
|
||||
|
||||
List<Locale> acceptableLanguages = requestContext.getAcceptableLanguages();
|
||||
System.out.println(acceptableLanguages);
|
||||
Locale selectedLocale = findFirstSupportedLanguage(acceptableLanguages);
|
||||
|
||||
if (selectedLocale == null)
|
||||
selectedLocale = TradService.fallbackLocale;
|
||||
System.out.println(selectedLocale);
|
||||
requestContext.setProperty("userLocale", selectedLocale);
|
||||
}
|
||||
|
||||
|
||||
64
src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java
Normal file
64
src/main/java/fr/titionfire/ffsaf/data/model/CardModel.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "card")
|
||||
public class CardModel {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
Long comb;
|
||||
Long match;
|
||||
Long category;
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "competition", referencedColumnName = "id")
|
||||
CompetitionModel competition;
|
||||
|
||||
@JsonProperty("competition")
|
||||
Long competitionId;
|
||||
|
||||
CardType type;
|
||||
String reason;
|
||||
@CreationTimestamp
|
||||
Date date;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
boolean teamCard = false;
|
||||
|
||||
public boolean hasEffect(MatchModel match) {
|
||||
return switch (this.type) {
|
||||
case BLUE -> false;
|
||||
case YELLOW -> Objects.equals(this.match, match.getId());
|
||||
case RED -> Objects.equals(this.category, match.getCategory().getId())
|
||||
|| Objects.equals(this.match, match.getId());
|
||||
case BLACK -> true;
|
||||
};
|
||||
}
|
||||
|
||||
public enum CardType {
|
||||
BLUE,
|
||||
YELLOW,
|
||||
RED,
|
||||
BLACK
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "cardboard")
|
||||
public class CardboardModel {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "comb", referencedColumnName = "id")
|
||||
MembreModel comb;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "guest_comb", referencedColumnName = "id")
|
||||
CompetitionGuestModel guestComb;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "match", referencedColumnName = "id")
|
||||
MatchModel match;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "compet", referencedColumnName = "id")
|
||||
CompetitionModel compet;
|
||||
|
||||
int red;
|
||||
int yellow;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "category_preset")
|
||||
public class CatPresetModel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "competition", referencedColumnName = "id")
|
||||
CompetitionModel competition;
|
||||
|
||||
String name = "";
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "category_preset_catconfig", joinColumns = @JoinColumn(name = "id_preset"))
|
||||
List<CategorieEmbeddable> categories;
|
||||
|
||||
SwordType swordType = SwordType.NONE;
|
||||
ShieldType shieldType = ShieldType.NONE;
|
||||
|
||||
/*
|
||||
* 1 - 1 - Casque
|
||||
* 2 - 2 - Gorgerin
|
||||
* 3 - 4 - Coquille et Protection pelvienne
|
||||
* 4 - 8 - Gant main(s) armée(s)
|
||||
* 5 - 16 - Gant main bouclier
|
||||
* 6 - 32 - Plastron
|
||||
* 7 - 64 - Protection de bras armé(s)
|
||||
* 8 - 128 - Protection de bras de bouclier
|
||||
* 9 - 256 - Protection de jambes
|
||||
* 10 - 512 - Protection de genoux
|
||||
* 11 - 1024 - Protection de coudes
|
||||
* 12 - 2048 - Protection dorsale
|
||||
* 13 - 4096 - Protection de pieds
|
||||
*/
|
||||
int mandatoryProtection1 = 0;
|
||||
int mandatoryProtection2 = 0;
|
||||
|
||||
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
|
||||
private List<RegisterModel> registers = new ArrayList<>();
|
||||
|
||||
@ManyToMany(mappedBy = "categoriesInscrites", fetch = FetchType.LAZY)
|
||||
private List<CompetitionGuestModel> guest = new ArrayList<>();
|
||||
|
||||
public enum SwordType {
|
||||
NONE,
|
||||
ONE_HAND,
|
||||
TWO_HAND,
|
||||
SABER
|
||||
}
|
||||
|
||||
public enum ShieldType {
|
||||
NONE,
|
||||
STANDARD,
|
||||
ROUND,
|
||||
TEARDROP,
|
||||
BUCKLER
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Embeddable
|
||||
public static class CategorieEmbeddable {
|
||||
Categorie categorie;
|
||||
long roundDuration;
|
||||
long pauseDuration;
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,14 @@ public class CategoryModel {
|
||||
Integer type;
|
||||
|
||||
String liceName = "1";
|
||||
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
boolean treeAreClassement = false;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
boolean fullClassement = false;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "id_preset", referencedColumnName = "id")
|
||||
CatPresetModel preset;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "card_team")
|
||||
public class ClubCardModel {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long id;
|
||||
|
||||
Long competition;
|
||||
String teamUuid;
|
||||
String teamName;
|
||||
|
||||
CardModel.CardType type;
|
||||
String reason;
|
||||
@CreationTimestamp
|
||||
Date date;
|
||||
|
||||
List<Long> cardIds;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.ResultPrivacy;
|
||||
|
||||
public interface CombModel {
|
||||
Long getCombId();
|
||||
String getName();
|
||||
String getName(MembreModel model, ResultPrivacy privacy);
|
||||
Categorie getCategorie();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@@ -38,7 +43,32 @@ public class CompetitionGuestModel implements CombModel {
|
||||
|
||||
String country = "fr";
|
||||
|
||||
Integer weight = null;
|
||||
Float weight = null;
|
||||
Float weightReal = null;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
|
||||
@JoinTable(
|
||||
name = "groupe_membre",
|
||||
joinColumns = @JoinColumn(name = "groupe_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "membre_id")
|
||||
)
|
||||
List<MembreModel> comb = new ArrayList<>();
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
|
||||
@JoinTable(
|
||||
name = "groupe_guest",
|
||||
joinColumns = @JoinColumn(name = "groupe_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "guest_id")
|
||||
)
|
||||
List<CompetitionGuestModel> guest = new ArrayList<>();
|
||||
|
||||
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
|
||||
@JoinTable(
|
||||
name = "categories_insc_guest",
|
||||
joinColumns = @JoinColumn(name = "guest_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "category_id")
|
||||
)
|
||||
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
|
||||
|
||||
public CompetitionGuestModel(String s) {
|
||||
this.fname = s.substring(0, s.indexOf(" "));
|
||||
@@ -52,6 +82,8 @@ public class CompetitionGuestModel implements CombModel {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
if (this.isTeam())
|
||||
return this.fname;
|
||||
return this.fname + " " + this.lname;
|
||||
}
|
||||
|
||||
@@ -59,4 +91,25 @@ public class CompetitionGuestModel implements CombModel {
|
||||
public String getName(MembreModel model, ResultPrivacy privacy) {
|
||||
return getName();
|
||||
}
|
||||
|
||||
public boolean isTeam() {
|
||||
return "__team".equals(this.lname);
|
||||
}
|
||||
|
||||
public boolean isInTeam(Object comb_) {
|
||||
if (!this.isTeam())
|
||||
return false;
|
||||
|
||||
if (comb_ instanceof Long id_) {
|
||||
if (id_ >= 0)
|
||||
return comb.stream().anyMatch(membre -> Objects.equals(membre.getId(), id_));
|
||||
else
|
||||
return guest.stream().anyMatch(guestModel -> Objects.equals(guestModel.getId(), -id_));
|
||||
}
|
||||
return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_));
|
||||
}
|
||||
|
||||
public Float getWeight2() {
|
||||
return (this.weightReal != null) ? this.weightReal : this.weight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.data.model;
|
||||
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.CompetitionSystem;
|
||||
import fr.titionfire.ffsaf.utils.RegisterMode;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
@@ -58,6 +59,10 @@ public class CompetitionModel {
|
||||
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
|
||||
List<CompetitionGuestModel> guests = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "competition", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
|
||||
List<CatPresetModel> catPreset = new ArrayList<>();
|
||||
|
||||
List<Categorie> requiredWeight = new ArrayList<>();
|
||||
|
||||
List<Long> banMembre = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ public class MatchModel {
|
||||
|
||||
char poule = 'A';
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
@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);
|
||||
@@ -113,7 +109,26 @@ public class MatchModel {
|
||||
return sum;
|
||||
}
|
||||
|
||||
public CombModel getC1() {
|
||||
if (this.c1_id != null)
|
||||
return this.c1_id;
|
||||
if (this.c1_guest != null)
|
||||
return this.c1_guest;
|
||||
return null;
|
||||
}
|
||||
|
||||
public CombModel getC2() {
|
||||
if (this.c2_id != null)
|
||||
return this.c2_id;
|
||||
if (this.c2_guest != null)
|
||||
return this.c2_guest;
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isC1(Object comb) {
|
||||
if (this.c1_guest != null && this.c1_guest.isInTeam(comb))
|
||||
return true;
|
||||
|
||||
if (comb instanceof Long id_) {
|
||||
if (id_ >= 0)
|
||||
return Objects.equals(this.c1_id != null ? this.c1_id.getId() : null, id_);
|
||||
@@ -124,6 +139,9 @@ public class MatchModel {
|
||||
}
|
||||
|
||||
public boolean isC2(Object comb) {
|
||||
if (this.c2_guest != null && this.c2_guest.isInTeam(comb))
|
||||
return true;
|
||||
|
||||
if (comb instanceof Long id_) {
|
||||
if (id_ >= 0)
|
||||
return Objects.equals(this.c2_id != null ? this.c2_id.getId() : null, id_);
|
||||
|
||||
@@ -11,6 +11,9 @@ import lombok.Setter;
|
||||
import org.hibernate.annotations.OnDelete;
|
||||
import org.hibernate.annotations.OnDeleteAction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@@ -34,7 +37,8 @@ public class RegisterModel {
|
||||
@JoinColumn(name = "id_membre")
|
||||
MembreModel membre;
|
||||
|
||||
Integer weight;
|
||||
Float weight;
|
||||
Float weightReal;
|
||||
int overCategory = 0;
|
||||
Categorie categorie;
|
||||
|
||||
@@ -46,7 +50,18 @@ public class RegisterModel {
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
boolean lockEdit = false;
|
||||
|
||||
public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory,
|
||||
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
|
||||
@JoinTable(
|
||||
name = "categories_insc_comb",
|
||||
joinColumns = {
|
||||
@JoinColumn(name = "id_competition", referencedColumnName = "id_competition"),
|
||||
@JoinColumn(name = "id_membre", referencedColumnName = "id_membre")
|
||||
},
|
||||
inverseJoinColumns = @JoinColumn(name = "category_id")
|
||||
)
|
||||
List<CatPresetModel> categoriesInscrites = new ArrayList<>();
|
||||
|
||||
public RegisterModel(CompetitionModel competition, MembreModel membre, Float weight, int overCategory,
|
||||
Categorie categorie, ClubModel club) {
|
||||
this.id = new RegisterId(competition.getId(), membre.getId());
|
||||
this.competition = competition;
|
||||
@@ -75,4 +90,10 @@ public class RegisterModel {
|
||||
return null;
|
||||
return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)];
|
||||
}
|
||||
|
||||
public Float getWeight2() {
|
||||
if (weightReal != null)
|
||||
return weightReal;
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@ public class TreeModel {
|
||||
@JoinColumn(referencedColumnName = "id")
|
||||
TreeModel right;
|
||||
|
||||
public TreeModel(Long category, Integer level, MatchModel match) {
|
||||
this.category = category;
|
||||
this.level = level;
|
||||
this.match = match;
|
||||
this.left = null;
|
||||
this.right = null;
|
||||
}
|
||||
|
||||
public List<TreeModel> flat() {
|
||||
List<TreeModel> out = new ArrayList<>();
|
||||
this.flat(out);
|
||||
@@ -55,4 +63,44 @@ public class TreeModel {
|
||||
if (this.left != null)
|
||||
this.left.flat(out);
|
||||
}
|
||||
|
||||
public int death() {
|
||||
int dg = 0;
|
||||
int dd = 0;
|
||||
|
||||
if (this.right != null)
|
||||
dg = this.right.death();
|
||||
|
||||
if (this.left != null)
|
||||
dg = this.left.death();
|
||||
|
||||
return 1 + Math.max(dg, dd);
|
||||
}
|
||||
|
||||
public int getMaxChildrenAtDepth(int death, int current) {
|
||||
if (current == death)
|
||||
return 1;
|
||||
|
||||
int tmp = 0;
|
||||
if (this.right != null)
|
||||
tmp += this.right.getMaxChildrenAtDepth(death, current + 1);
|
||||
|
||||
if (this.left != null)
|
||||
tmp += this.left.getMaxChildrenAtDepth(death, current + 1);
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
public void getChildrenAtDepth (int death, int current, List<TreeModel> out) {
|
||||
if (current == death) {
|
||||
out.add(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.right != null)
|
||||
this.right.getChildrenAtDepth(death, current + 1, out);
|
||||
|
||||
if (this.left != null)
|
||||
this.left.getChildrenAtDepth(death, current + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CardRepository implements PanacheRepositoryBase<CardModel, Long> {
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardboardModel;
|
||||
import fr.titionfire.ffsaf.data.model.CatPresetModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CardboardRepository implements PanacheRepositoryBase<CardboardModel, Long> {
|
||||
public class CatPresetRepository implements PanacheRepositoryBase<CatPresetModel, Long> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.titionfire.ffsaf.data.repository;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.ClubCardModel;
|
||||
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class ClubCardRepository implements PanacheRepositoryBase<ClubCardModel, Long> {
|
||||
|
||||
}
|
||||
@@ -20,6 +20,9 @@ public class MatchRepository implements PanacheRepositoryBase<MatchModel, Long>
|
||||
}
|
||||
|
||||
public Uni<Void> create(List<MatchModel> matchModel) {
|
||||
if (matchModel.isEmpty())
|
||||
return Uni.createFrom().voidItem();
|
||||
|
||||
matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL));
|
||||
return Panache.withTransaction(() -> this.persist(matchModel)
|
||||
.call(__ -> this.flush())
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardboardModel;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class CardboardEntity {
|
||||
long comb_id;
|
||||
long match_id;
|
||||
long compet_id;
|
||||
|
||||
int red;
|
||||
int yellow;
|
||||
|
||||
public static CardboardEntity fromModel(CardboardModel model) {
|
||||
return new CardboardEntity(
|
||||
model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1,
|
||||
model.getMatch().getId(),
|
||||
model.getCompet().getId(),
|
||||
model.getRed(), model.getYellow());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CatPresetModel;
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
@@ -9,6 +10,10 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
@@ -22,7 +27,9 @@ public class CombEntity {
|
||||
Genre genre;
|
||||
String country;
|
||||
int overCategory;
|
||||
Integer weight;
|
||||
Float weight;
|
||||
List<CombEntity> teamMembers;
|
||||
List<Long> categoriesInscrites;
|
||||
|
||||
public static CombEntity fromModel(MembreModel model) {
|
||||
if (model == null)
|
||||
@@ -31,7 +38,7 @@ public class CombEntity {
|
||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
|
||||
model.getClub() == null ? null : model.getClub().getClubId(),
|
||||
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
|
||||
0, null);
|
||||
0, null, new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +47,18 @@ public class CombEntity {
|
||||
return null;
|
||||
|
||||
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
|
||||
model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight());
|
||||
model.getClub(), model.getGenre(), model.getCountry(), 0,
|
||||
model.getWeight2() != null ? model.getWeight2() : model.getWeight(),
|
||||
Stream.concat(model.getComb().stream().map(CombEntity::fromModel),
|
||||
model.getGuest().stream().map(CombEntity::fromModel)).toList(),
|
||||
new ArrayList<>());
|
||||
}
|
||||
|
||||
public CombEntity addCategoriesInscrites(List<CatPresetModel> categoriesInscrites) {
|
||||
if (categoriesInscrites == null)
|
||||
return this;
|
||||
this.categoriesInscrites = categoriesInscrites.stream().map(CatPresetModel::getId).toList();
|
||||
return this;
|
||||
}
|
||||
|
||||
public static CombEntity fromModel(RegisterModel registerModel) {
|
||||
@@ -51,6 +69,8 @@ public class CombEntity {
|
||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
|
||||
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
|
||||
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
|
||||
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight());
|
||||
model.getCountry(), registerModel.getOverCategory(),
|
||||
registerModel.getWeight2() != null ? registerModel.getWeight2() : registerModel.getWeight(),
|
||||
new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -23,7 +22,6 @@ public class MatchEntity {
|
||||
private Date date;
|
||||
private List<ScoreEmbeddable> scores;
|
||||
private char poule;
|
||||
private List<CardboardEntity> cardboard;
|
||||
|
||||
public static MatchEntity fromModel(MatchModel model) {
|
||||
if (model == null)
|
||||
@@ -35,22 +33,6 @@ public class MatchEntity {
|
||||
model.getC2_id()),
|
||||
model.getCategory_ord(), model.isEnd(), model.getCategory().getId(), model.getDate(),
|
||||
model.getScores(),
|
||||
model.getPoule(),
|
||||
(model.getCardboard() == null) ? new ArrayList<>() : model.getCardboard().stream()
|
||||
.map(CardboardEntity::fromModel).toList());
|
||||
}
|
||||
|
||||
public int win() {
|
||||
int sum = 0;
|
||||
for (ScoreEmbeddable score : scores) {
|
||||
if (score.getS1() == -1000 || score.getS2() == -1000)
|
||||
continue;
|
||||
|
||||
if (score.getS1() > score.getS2())
|
||||
sum++;
|
||||
else if (score.getS1() < score.getS2())
|
||||
sum--;
|
||||
}
|
||||
return sum;
|
||||
model.getPoule());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.utils.ResultPrivacy;
|
||||
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RegisterForReflection
|
||||
public class MatchModelExtend {
|
||||
final MatchModel match;
|
||||
|
||||
@Getter
|
||||
boolean isEnd = false;
|
||||
@Getter
|
||||
List<ScoreEmbeddable> scoresToPrint = new ArrayList<>();
|
||||
@Getter
|
||||
List<ScoreEmbeddable> scoresToCompute = new ArrayList<>();
|
||||
@Getter
|
||||
int win = 0;
|
||||
|
||||
|
||||
public MatchModelExtend(MatchModel match, List<CardModel> cards) {
|
||||
this.match = match;
|
||||
|
||||
List<Long> combIds = extractCombIds(match);
|
||||
List<CardModel> cards2 = cards.stream().filter(c -> combIds.contains(c.getComb()) && c.hasEffect(match))
|
||||
.sorted(Comparator.comparing(CardModel::getType).reversed()).toList();
|
||||
|
||||
|
||||
for (ScoreEmbeddable score : match.getScores()) {
|
||||
if (score.getS1() == -1000 || score.getS2() == -1000)
|
||||
continue;
|
||||
this.scoresToCompute.add(virtualScore(score, cards2, false));
|
||||
}
|
||||
|
||||
calc_win_end(cards2);
|
||||
|
||||
for (ScoreEmbeddable score : match.getScores()) {
|
||||
if (score.getS1() == -1000 || score.getS2() == -1000)
|
||||
continue;
|
||||
this.scoresToPrint.add(virtualScore(score, cards2, true));
|
||||
}
|
||||
if (this.isEnd && this.scoresToPrint.isEmpty()) {
|
||||
this.scoresToPrint.add(virtualScore(new ScoreEmbeddable(0, 0, 0), cards2, true));
|
||||
}
|
||||
}
|
||||
|
||||
private ScoreEmbeddable virtualScore(ScoreEmbeddable score, List<CardModel> cards2, boolean toPrint) {
|
||||
if (cards2.size() > 1) {
|
||||
if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb()))
|
||||
return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, toPrint ? -997 : 0);
|
||||
}
|
||||
if (!cards2.isEmpty()) {
|
||||
if (isC1(cards2.get(0).getComb()))
|
||||
return new ScoreEmbeddable(score.getN_round(), toPrint ? -997 : 0, 10);
|
||||
else
|
||||
return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? -997 : 0);
|
||||
}
|
||||
|
||||
if (score.getS1() < -900 && score.getS2() < -900)
|
||||
return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, toPrint ? score.getS2() : 0);
|
||||
else if (score.getS1() < -900)
|
||||
return new ScoreEmbeddable(score.getN_round(), toPrint ? score.getS1() : 0, 10);
|
||||
else if (score.getS2() < -900)
|
||||
return new ScoreEmbeddable(score.getN_round(), 10, toPrint ? score.getS2() : 0);
|
||||
|
||||
return new ScoreEmbeddable(score.getN_round(), score.getS1(), score.getS2());
|
||||
}
|
||||
|
||||
private void calc_win_end(List<CardModel> cards2) {
|
||||
if (cards2.size() > 1) {
|
||||
if (!Objects.equals(cards2.get(0).getComb(), cards2.get(1).getComb())) {
|
||||
this.win = 0;
|
||||
this.isEnd = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cards2.isEmpty()) {
|
||||
if (match.isC1(cards2.get(0).getComb())) {
|
||||
this.win = -1;
|
||||
} else if (match.isC2(cards2.get(0).getComb())) {
|
||||
this.win = 1;
|
||||
}
|
||||
this.isEnd = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (ScoreEmbeddable score : this.scoresToCompute) {
|
||||
if (score.getS1() > score.getS2())
|
||||
win++;
|
||||
else if (score.getS1() < score.getS2())
|
||||
win--;
|
||||
}
|
||||
this.isEnd = match.isEnd();
|
||||
}
|
||||
|
||||
|
||||
private List<Long> extractCombIds(MatchModel match) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
if (match.getC1_id() != null)
|
||||
ids.add(match.getC1_id().getId());
|
||||
if (match.getC2_id() != null)
|
||||
ids.add(match.getC2_id().getId());
|
||||
if (match.getC1_guest() != null)
|
||||
ids.add(match.getC1_guest().getId() * -1);
|
||||
if (match.getC2_guest() != null)
|
||||
ids.add(match.getC2_guest().getId() * -1);
|
||||
return ids;
|
||||
}
|
||||
|
||||
//--------------- Delegation methods to MatchModel ---------------
|
||||
|
||||
public Long getId() {
|
||||
return match.getId();
|
||||
}
|
||||
|
||||
public MembreModel getC1_id() {
|
||||
return match.getC1_id();
|
||||
}
|
||||
|
||||
public CompetitionGuestModel getC1_guest() {
|
||||
return match.getC1_guest();
|
||||
}
|
||||
|
||||
public MembreModel getC2_id() {
|
||||
return match.getC2_id();
|
||||
}
|
||||
|
||||
public CompetitionGuestModel getC2_guest() {
|
||||
return match.getC2_guest();
|
||||
}
|
||||
|
||||
public CategoryModel getCategory() {
|
||||
return match.getCategory();
|
||||
}
|
||||
|
||||
public long getCategory_ord() {
|
||||
return match.getCategory_ord();
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return match.getDate();
|
||||
}
|
||||
|
||||
public char getPoule() {
|
||||
return match.getPoule();
|
||||
}
|
||||
|
||||
public String getC1Name(MembreModel model, ResultPrivacy privacy) {
|
||||
return match.getC1Name(model, privacy);
|
||||
}
|
||||
|
||||
public String getC2Name(MembreModel model, ResultPrivacy privacy) {
|
||||
return match.getC2Name(model, privacy);
|
||||
}
|
||||
|
||||
public String getC2Name() {
|
||||
return match.getC2Name();
|
||||
}
|
||||
|
||||
public String getC1Name() {
|
||||
return match.getC1Name();
|
||||
}
|
||||
|
||||
public boolean isC1(Object comb) {
|
||||
return match.isC1(comb);
|
||||
}
|
||||
|
||||
public boolean isC2(Object comb) {
|
||||
return match.isC2(comb);
|
||||
}
|
||||
|
||||
public CombModel getC1() {
|
||||
return match.getC1();
|
||||
}
|
||||
|
||||
public CombModel getC2() {
|
||||
return match.getC2();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardModel;
|
||||
import fr.titionfire.ffsaf.data.model.ClubCardModel;
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
|
||||
import fr.titionfire.ffsaf.ws.recv.RCard;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
public class CardService {
|
||||
@Inject
|
||||
CardRepository cardRepository;
|
||||
|
||||
@Inject
|
||||
ClubCardRepository clubCardRepository;
|
||||
|
||||
@Inject
|
||||
RegisterRepository registerRepository;
|
||||
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private static final List<CardModel.CardType> COMPETITION_LEVEL_CARDS = List.of(
|
||||
CardModel.CardType.YELLOW,
|
||||
CardModel.CardType.RED,
|
||||
CardModel.CardType.BLACK
|
||||
);
|
||||
|
||||
private List<Long> extractCombIds(MatchModel match) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
if (match.getC1_id() != null)
|
||||
ids.add(match.getC1_id().getId());
|
||||
if (match.getC2_id() != null)
|
||||
ids.add(match.getC2_id().getId());
|
||||
if (match.getC1_guest() != null)
|
||||
ids.add(match.getC1_guest().getId() * -1);
|
||||
if (match.getC2_guest() != null)
|
||||
ids.add(match.getC2_guest().getId() * -1);
|
||||
return ids;
|
||||
}
|
||||
|
||||
public Uni<List<CardModel>> getForMatch(MatchModel match) {
|
||||
return cardRepository.list(
|
||||
"competition = ?1 AND (type IN ?2 OR (type = CardType.BLUE AND category = ?4)) AND comb IN ?3",
|
||||
match.getCategory().getCompet(), COMPETITION_LEVEL_CARDS,
|
||||
extractCombIds(match), match.getCategory().getId());
|
||||
}
|
||||
|
||||
public Uni<List<CardModel>> getAll(CompetitionModel competition) {
|
||||
return cardRepository.list("competition = ?1", competition);
|
||||
}
|
||||
|
||||
public Uni<RCard.SendCardAdd> checkCanBeAdded(RCard.SendCardAdd card, MatchModel matchModel) {
|
||||
return cardRepository.find("competition = ?1 AND comb = ?2",
|
||||
Sort.descending("type"),
|
||||
matchModel.getCategory().getCompet(), card.combId())
|
||||
.firstResult()
|
||||
.map(card_ -> {
|
||||
if (card.type() == CardModel.CardType.BLUE) {
|
||||
return card_ == null || (card_.getType() == CardModel.CardType.BLUE
|
||||
&& !Objects.equals(card_.getCategory(), matchModel.getCategory().getId()));
|
||||
}
|
||||
if (card.type() == CardModel.CardType.BLACK) {
|
||||
return card_ != null && card_.getType() == CardModel.CardType.RED;
|
||||
}
|
||||
|
||||
return card_ == null || card_.getType().ordinal() < card.type().ordinal();
|
||||
})
|
||||
.chain(b -> {
|
||||
if (b)
|
||||
return Uni.createFrom().item(card);
|
||||
else
|
||||
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<List<CardModel>> addTeamCartToNewComb(Long combId, String teamUuid, String teamName,
|
||||
CompetitionModel competition) {
|
||||
return clubCardRepository.list("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)",
|
||||
Sort.ascending("type"), competition.getId(), teamUuid, teamName)
|
||||
.chain(clubCards -> {
|
||||
Uni<?> queue = Uni.createFrom().voidItem();
|
||||
List<CardModel> addCards = new ArrayList<>();
|
||||
for (ClubCardModel clubCard : clubCards) {
|
||||
CardModel model = new CardModel();
|
||||
model.setCompetition(competition);
|
||||
model.setCompetitionId(competition.getId());
|
||||
model.setComb(combId);
|
||||
model.setTeamCard(true);
|
||||
model.setType(clubCard.getType());
|
||||
model.setDate(clubCard.getDate());
|
||||
|
||||
queue = queue.call(__ -> Panache.withTransaction(() -> cardRepository.persist(model))
|
||||
.invoke(addCards::add)
|
||||
.call(() -> {
|
||||
clubCard.getCardIds().add(model.getId());
|
||||
return Panache.withTransaction(() -> clubCardRepository.persist(clubCard));
|
||||
}));
|
||||
}
|
||||
return queue.replaceWith(addCards);
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<Void> rmTeamCardFromComb(Long combId, String uuid) {
|
||||
return cardRepository.delete("comb = ?1 AND competition.uuid = ?2 AND teamCard = True", combId, uuid)
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
public Uni<List<CardModel>> addTeamCard(CompetitionModel competition, String teamUuid, String teamName,
|
||||
CardModel.CardType type, String reason) {
|
||||
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3)",
|
||||
Sort.descending("type"), competition.getId(), teamUuid, teamName)
|
||||
.firstResult()
|
||||
.map(card_ -> {
|
||||
if (type == CardModel.CardType.BLACK) {
|
||||
return card_ != null && card_.getType() == CardModel.CardType.RED;
|
||||
}
|
||||
|
||||
return card_ == null || card_.getType().ordinal() < type.ordinal();
|
||||
})
|
||||
.chain(b -> {
|
||||
if (!b)
|
||||
return Uni.createFrom().failure(new DBadRequestException(trad.t("card.cannot.be.added")));
|
||||
|
||||
if (teamUuid != null) {
|
||||
return registerRepository.list("competition = ?1 AND club.clubId = ?2", competition, teamUuid)
|
||||
.map(l -> l.stream().map(r -> r.getMembre().getId()).toList());
|
||||
} else {
|
||||
return competitionGuestRepository.list("competition = ?1 AND club = ?2", competition,
|
||||
teamName)
|
||||
.map(l -> l.stream().map(r -> r.getId() * -1).toList());
|
||||
}
|
||||
})
|
||||
.chain(combIds -> cardRepository.list("competition = ?1 AND comb IN ?2", competition, combIds)
|
||||
.map(cards -> {
|
||||
List<CardModel> newCards = new ArrayList<>();
|
||||
for (Long id : combIds) {
|
||||
Optional<CardModel> optional = cards.stream()
|
||||
.filter(c -> id.equals(c.getComb()) && c.getType() == type).findAny();
|
||||
|
||||
CardModel model = new CardModel();
|
||||
model.setCompetition(competition);
|
||||
model.setCompetitionId(competition.getId());
|
||||
model.setComb(id);
|
||||
model.setTeamCard(true);
|
||||
|
||||
if (optional.isEmpty()) {
|
||||
model.setType(type);
|
||||
} else {
|
||||
model.setType(
|
||||
CardModel.CardType.values()[Math.min(optional.get().getType().ordinal() + 1,
|
||||
CardModel.CardType.BLACK.ordinal())]);
|
||||
}
|
||||
newCards.add(model);
|
||||
}
|
||||
return newCards;
|
||||
})
|
||||
)
|
||||
.call(newCards -> Panache.withTransaction(() -> cardRepository.persist(newCards)))
|
||||
.call(newCards -> {
|
||||
ClubCardModel model = new ClubCardModel();
|
||||
model.setCompetition(competition.getId());
|
||||
model.setTeamUuid(teamUuid);
|
||||
model.setTeamName(teamName);
|
||||
model.setType(type);
|
||||
model.setReason(reason);
|
||||
model.setCardIds(newCards.stream().map(CardModel::getId).toList());
|
||||
|
||||
return Panache.withTransaction(() -> clubCardRepository.persist(model));
|
||||
});
|
||||
}
|
||||
|
||||
public Uni<List<CardModel>> recvReturnState(CompetitionModel competition, RCard.SendTeamCardReturnState state) {
|
||||
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
|
||||
competition.getId(), state.teamUuid(), state.teamName(), state.type())
|
||||
.firstResult()
|
||||
.chain(o -> cardRepository.list("id IN ?1", o.getCardIds()))
|
||||
.call(cards -> matchRepository.list("category.compet = ?1 AND category.id IN ?2", competition,
|
||||
state.selectedCategory())
|
||||
.invoke(matches -> {
|
||||
for (CardModel card : cards) {
|
||||
for (MatchModel m : matches.stream()
|
||||
.filter(m -> extractCombIds(m).contains(card.getComb())).toList()) {
|
||||
|
||||
if (state.state() == 1) {
|
||||
card.setCategory(m.getCategory().getId());
|
||||
} else if (state.state() == 2) {
|
||||
card.setCategory(m.getCategory().getId());
|
||||
if (Objects.equals(m.getId(), state.selectedMatch()))
|
||||
card.setMatch(m.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.chain(() -> Panache.withTransaction(() -> cardRepository.persist(cards))));
|
||||
}
|
||||
|
||||
public Uni<List<Long>> rmTeamCard(CompetitionModel competition, String teamUuid, String teamName,
|
||||
CardModel.CardType type) {
|
||||
return clubCardRepository.find("competition = ?1 AND (teamUuid = ?2 OR teamName = ?3) AND type = ?4",
|
||||
competition.getId(), teamUuid, teamName, type)
|
||||
.firstResult()
|
||||
.chain(card -> Uni.createFrom().item(card.getCardIds())
|
||||
.call(() -> Panache.withTransaction(() -> cardRepository.delete("id IN ?1", card.getCardIds())))
|
||||
.call(() -> Panache.withTransaction(() -> clubCardRepository.delete(card))));
|
||||
}
|
||||
}
|
||||
@@ -98,12 +98,14 @@ public class CompetPermService {
|
||||
map.putIfAbsent(model.getId(), "owner");
|
||||
else if (securityCtx.roleHas("federation_admin"))
|
||||
map.putIfAbsent(model.getId(), "admin");
|
||||
else if (securityCtx.isInClubGroup(model.getClub().getId()) && (securityCtx.roleHas(
|
||||
"club_president")
|
||||
|| securityCtx.roleHas("club_respo_intra") || securityCtx.roleHas(
|
||||
"club_secretaire")
|
||||
|| securityCtx.roleHas("club_tresorier")))
|
||||
else if (securityCtx.isInClubGroup(
|
||||
model.getClub().getId()) && (securityCtx.isClubAdmin()))
|
||||
map.putIfAbsent(model.getId(), "admin");
|
||||
else if (model.getAdmin().contains(securityCtx.getSubject()))
|
||||
map.putIfAbsent(model.getId(), "admin");
|
||||
else if (model.getTable().contains(securityCtx.getSubject()))
|
||||
map.putIfAbsent(model.getId(), "table");
|
||||
|
||||
}
|
||||
return map;
|
||||
}));
|
||||
@@ -182,12 +184,14 @@ public class CompetPermService {
|
||||
if (o.getSystem() == CompetitionSystem.SAFCA)
|
||||
return hasSafcaViewPerm(securityCtx, o.getId());
|
||||
|
||||
if (o.getAdmin().contains(securityCtx.getSubject()))
|
||||
return Uni.createFrom().nullItem();
|
||||
|
||||
if (!securityCtx.isInClubGroup(o.getClub().getId())) // Only membre club pass here
|
||||
throw new DForbiddenException();
|
||||
|
||||
if (o.getSystem() == CompetitionSystem.INTERNAL)
|
||||
if (securityCtx.roleHas("club_president") || securityCtx.roleHas("club_respo_intra")
|
||||
|| securityCtx.roleHas("club_secretaire") || securityCtx.roleHas("club_tresorier"))
|
||||
if (securityCtx.isClubAdmin())
|
||||
return Uni.createFrom().nullItem();
|
||||
|
||||
throw new DForbiddenException();
|
||||
|
||||
@@ -7,10 +7,7 @@ import fr.titionfire.ffsaf.net2.data.SimpleCompet;
|
||||
import fr.titionfire.ffsaf.net2.request.SReqCompet;
|
||||
import fr.titionfire.ffsaf.net2.request.SReqRegister;
|
||||
import fr.titionfire.ffsaf.rest.client.dto.NotificationData;
|
||||
import fr.titionfire.ffsaf.rest.data.CompetitionData;
|
||||
import fr.titionfire.ffsaf.rest.data.RegisterRequestData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb;
|
||||
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.DNotFoundException;
|
||||
@@ -63,6 +60,9 @@ public class CompetitionService {
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
CatPresetRepository catPresetRepository;
|
||||
|
||||
@Inject
|
||||
ServerCustom serverCustom;
|
||||
|
||||
@@ -112,16 +112,14 @@ public class CompetitionService {
|
||||
|
||||
public Uni<CompetitionData> getByIdAdmin(SecurityCtx securityCtx, Long id) {
|
||||
if (id == 0) {
|
||||
return Uni.createFrom()
|
||||
.item(new CompetitionData(null, "", "", "", "", new Date(), new Date(),
|
||||
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
|
||||
null, "", "", null, true, true,
|
||||
"", "", "", "", "{}"));
|
||||
return Uni.createFrom().item(new CompetitionData());
|
||||
}
|
||||
return permService.hasAdminViewPerm(securityCtx, id)
|
||||
.call(competitionModel -> Mutiny.fetch(competitionModel.getCatPreset()))
|
||||
.chain(competitionModel -> Mutiny.fetch(competitionModel.getInsc())
|
||||
.chain(insc -> Mutiny.fetch(competitionModel.getGuests())
|
||||
.map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest))))
|
||||
.map(guest -> CompetitionData.fromModel(competitionModel).addInsc(insc, guest)
|
||||
.addPresets(competitionModel.getCatPreset()))))
|
||||
.chain(data ->
|
||||
vertx.getOrCreateContext().executeBlocking(() -> {
|
||||
keycloakService.getUser(UUID.fromString(data.getOwner()))
|
||||
@@ -208,17 +206,21 @@ public class CompetitionService {
|
||||
model.setGuests(new ArrayList<>());
|
||||
model.setUuid(UUID.randomUUID().toString());
|
||||
model.setOwner(securityCtx.getSubject());
|
||||
model.setCatPreset(new ArrayList<>());
|
||||
|
||||
copyData(data, model);
|
||||
|
||||
return Panache.withTransaction(() -> repository.persist(model));
|
||||
}).map(CompetitionData::fromModel)
|
||||
})
|
||||
.call(model -> syncPreset(data, model))
|
||||
.map(CompetitionData::fromModel)
|
||||
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
|
||||
securityCtx.getSubject()) : Uni.createFrom().nullItem())
|
||||
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
|
||||
securityCtx.getSubject()) : Uni.createFrom().nullItem());
|
||||
} else {
|
||||
return permService.hasEditPerm(securityCtx, data.getId())
|
||||
.call(model -> Mutiny.fetch(model.getCatPreset()))
|
||||
.chain(model -> {
|
||||
copyData(data, model);
|
||||
|
||||
@@ -237,7 +239,9 @@ public class CompetitionService {
|
||||
}
|
||||
}))
|
||||
.chain(__ -> Panache.withTransaction(() -> repository.persist(model)));
|
||||
}).map(CompetitionData::fromModel)
|
||||
})
|
||||
.call(model -> syncPreset(data, model))
|
||||
.map(model -> CompetitionData.fromModel(model).addPresets(model.getCatPreset()))
|
||||
.call(c -> (c.getSystem() == CompetitionSystem.SAFCA) ? cacheAccess.invalidate(
|
||||
securityCtx.getSubject()) : Uni.createFrom().nullItem())
|
||||
.call(c -> (c.getSystem() == CompetitionSystem.INTERNAL) ? cacheNoneAccess.invalidate(
|
||||
@@ -245,6 +249,45 @@ public class CompetitionService {
|
||||
}
|
||||
}
|
||||
|
||||
private Uni<?> syncPreset(CompetitionData data, CompetitionModel model) {
|
||||
List<Long> toRemoveId = model.getCatPreset().stream()
|
||||
.map(CatPresetModel::getId)
|
||||
.filter(id -> data.getPresets().stream().noneMatch(preset -> Objects.equals(preset.getId(), id)))
|
||||
.toList();
|
||||
|
||||
for (PresetData preset : data.getPresets()) {
|
||||
CatPresetModel presetModel;
|
||||
if (preset.getId() != null && preset.getId() > 0) {
|
||||
presetModel = model.getCatPreset().stream()
|
||||
.filter(p -> p.getId().equals(preset.getId()))
|
||||
.findFirst()
|
||||
.orElse(new CatPresetModel());
|
||||
} else {
|
||||
presetModel = new CatPresetModel();
|
||||
model.getCatPreset().add(presetModel);
|
||||
}
|
||||
|
||||
presetModel.setCompetition(model);
|
||||
presetModel.setName(preset.getName());
|
||||
presetModel.setSwordType(preset.getSword());
|
||||
presetModel.setShieldType(preset.getShield());
|
||||
presetModel.setCategories(preset.getCategories());
|
||||
presetModel.setMandatoryProtection1(preset.getMandatoryProtection1());
|
||||
presetModel.setMandatoryProtection2(preset.getMandatoryProtection2());
|
||||
}
|
||||
|
||||
// Remove deleted presets
|
||||
model.getCatPreset().removeIf(presetModel -> toRemoveId.contains(presetModel.getId()));
|
||||
|
||||
return Panache.withTransaction(() -> repository.persist(model)
|
||||
.call(__ -> {
|
||||
if (!toRemoveId.isEmpty()) {
|
||||
return catPresetRepository.delete("id IN ?1", toRemoveId);
|
||||
}
|
||||
return Uni.createFrom().nullItem();
|
||||
}));
|
||||
}
|
||||
|
||||
private void copyData(CompetitionData data, CompetitionModel model) {
|
||||
if (model.getBanMembre() == null)
|
||||
model.setBanMembre(new ArrayList<>());
|
||||
@@ -258,6 +301,7 @@ public class CompetitionService {
|
||||
model.setStartRegister(data.getStartRegister());
|
||||
model.setEndRegister(data.getEndRegister());
|
||||
model.setRegisterMode(data.getRegisterMode());
|
||||
model.setRequiredWeight(data.getRequiredWeight());
|
||||
model.setData1(data.getData1());
|
||||
model.setData2(data.getData2());
|
||||
model.setData3(data.getData3());
|
||||
@@ -271,11 +315,18 @@ public class CompetitionService {
|
||||
Uni<List<SimpleRegisterComb>> uni = Mutiny.fetch(c.getInsc())
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
|
||||
.map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences()))
|
||||
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
|
||||
.map(cm -> SimpleRegisterComb.fromModel(cm, cm.getMembre().getLicences())
|
||||
.setCategorieInscrite(cm.getCategoriesInscrites()))
|
||||
.collect().asList();
|
||||
return uni
|
||||
.call(l -> Mutiny.fetch(c.getGuests())
|
||||
.map(guest -> guest.stream().map(SimpleRegisterComb::fromModel).toList())
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.filter(g -> !g.isTeam())
|
||||
.onItem().call(guest -> Mutiny.fetch(guest.getCategoriesInscrites()))
|
||||
.map(guest -> SimpleRegisterComb.fromModel(guest)
|
||||
.setCategorieInscrite(guest.getCategoriesInscrites()))
|
||||
.collect().asList()
|
||||
.invoke(l::addAll));
|
||||
});
|
||||
|
||||
@@ -290,12 +341,17 @@ public class CompetitionService {
|
||||
model.getClub()))
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.onItem().call(combModel -> Mutiny.fetch(combModel.getMembre().getLicences()))
|
||||
.map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences()))
|
||||
.onItem().call(combModel -> Mutiny.fetch(combModel.getCategoriesInscrites()))
|
||||
.map(combModel -> SimpleRegisterComb.fromModel(combModel, combModel.getMembre().getLicences())
|
||||
.setCategorieInscrite(combModel.getCategoriesInscrites()))
|
||||
.collect().asList();
|
||||
|
||||
return membreService.getByAccountId(securityCtx.getSubject())
|
||||
.chain(model -> registerRepository.find("competition.id = ?1 AND membre = ?2", id, model).firstResult()
|
||||
.map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of()))));
|
||||
.call(rm -> rm == null ? Uni.createFrom().voidItem() :
|
||||
Mutiny.fetch(rm.getCategoriesInscrites()))
|
||||
.map(rm -> rm == null ? List.of() : List.of(SimpleRegisterComb.fromModel(rm, List.of())
|
||||
.setCategorieInscrite(rm.getCategoriesInscrites()))));
|
||||
}
|
||||
|
||||
public Uni<SimpleRegisterComb> addRegisterComb(SecurityCtx securityCtx, Long id, RegisterRequestData data,
|
||||
@@ -311,8 +367,12 @@ public class CompetitionService {
|
||||
c.getBanMembre().remove(combModel.getId());
|
||||
return Panache.withTransaction(() -> repository.persist(c));
|
||||
})
|
||||
.chain(combModel -> updateRegister(data, c, combModel, true)))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()));
|
||||
.chain(combModel -> updateRegister(data, c, combModel, true, false)))
|
||||
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
|
||||
.voidItem())
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
|
||||
.setCategorieInscrite(r.getCategoriesInscrites()));
|
||||
} else {
|
||||
return permService.hasEditPerm(securityCtx, id)
|
||||
.chain(c -> competitionGuestRepository.findById(data.getId() * -1)
|
||||
@@ -323,21 +383,35 @@ public class CompetitionService {
|
||||
model.setCompetition(c);
|
||||
return model;
|
||||
}))
|
||||
.chain(model -> {
|
||||
.invoke(model -> {
|
||||
model.setFname(data.getFname());
|
||||
model.setLname(data.getLname());
|
||||
if (data.getLname().equals("__team"))
|
||||
model.setLname("_team");
|
||||
else
|
||||
model.setLname(data.getLname());
|
||||
model.setGenre(data.getGenre());
|
||||
model.setClub(data.getClub());
|
||||
model.setCountry(data.getCountry());
|
||||
model.setWeight(data.getWeight());
|
||||
model.setWeightReal(data.getWeightReal());
|
||||
model.setCategorie(data.getCategorie());
|
||||
|
||||
return Panache.withTransaction(() -> competitionGuestRepository.persist(model))
|
||||
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegister(model.getCompetition().getUuid(),
|
||||
r) : Uni.createFrom().voidItem());
|
||||
if (model.getCompetition().getRequiredWeight().contains(model.getCategorie()))
|
||||
model.setWeight(data.getWeight());
|
||||
})
|
||||
.map(SimpleRegisterComb::fromModel);
|
||||
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
|
||||
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
|
||||
data.getCategoriesInscrites())
|
||||
.invoke(cats -> {
|
||||
g.getCategoriesInscrites().clear();
|
||||
g.getCategoriesInscrites().addAll(cats);
|
||||
g.getCategoriesInscrites()
|
||||
.removeIf(cat -> cat.getCategories().stream()
|
||||
.noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
|
||||
}))
|
||||
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
|
||||
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r)
|
||||
: Uni.createFrom().voidItem()))
|
||||
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
|
||||
}
|
||||
if ("club".equals(source))
|
||||
return repository.findById(id)
|
||||
@@ -356,8 +430,12 @@ public class CompetitionService {
|
||||
if (c.getBanMembre().contains(model.getId()))
|
||||
throw new DForbiddenException(trad.t("insc.err1"));
|
||||
}))
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false)))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences()));
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
|
||||
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom()
|
||||
.voidItem())
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
|
||||
.setCategorieInscrite(r.getCategoriesInscrites()));
|
||||
|
||||
return repository.findById(id)
|
||||
.invoke(Unchecked.consumer(cm -> {
|
||||
@@ -371,20 +449,102 @@ public class CompetitionService {
|
||||
if (c.getBanMembre().contains(model.getId()))
|
||||
throw new DForbiddenException(trad.t("insc.err2"));
|
||||
}))
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false)))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, List.of()));
|
||||
.chain(combModel -> updateRegister(data, c, combModel, false, false)))
|
||||
.call(r -> r.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegisterNoFetch(r.getCompetition().getUuid(), r) : Uni.createFrom().voidItem())
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, List.of()).setCategorieInscrite(r.getCategoriesInscrites()));
|
||||
}
|
||||
|
||||
public Uni<List<SimpleRegisterComb>> addRegistersComb(SecurityCtx securityCtx, Long id,
|
||||
List<RegisterRequestData> datas,
|
||||
String source) {
|
||||
if (!"admin".equals(source))
|
||||
return Uni.createFrom().failure(new DForbiddenException());
|
||||
|
||||
return permService.hasEditPerm(securityCtx, id)
|
||||
.chain(cm -> Multi.createFrom().iterable(datas).onItem().transformToUni(data ->
|
||||
makeImportUpdate(cm, data).onFailure().recoverWithItem(t -> {
|
||||
SimpleRegisterComb errorComb = new SimpleRegisterComb();
|
||||
errorComb.setLicence(-42);
|
||||
errorComb.setFname("ERROR");
|
||||
errorComb.setLname(t.getMessage());
|
||||
return errorComb;
|
||||
})).concatenate().collect().asList());
|
||||
}
|
||||
|
||||
@WithSession
|
||||
public Uni<SimpleRegisterComb> makeImportUpdate(CompetitionModel c, RegisterRequestData data) {
|
||||
if (data.getLicence() == null || data.getLicence() != -1) { // not a guest
|
||||
return findComb(data.getLicence(), data.getFname(), data.getLname())
|
||||
.call(combModel -> Mutiny.fetch(combModel.getLicences()))
|
||||
.call(combModel -> {
|
||||
if (c.getBanMembre() == null)
|
||||
c.setBanMembre(new ArrayList<>());
|
||||
c.getBanMembre().remove(combModel.getId());
|
||||
return Panache.withTransaction(() -> repository.persist(c));
|
||||
})
|
||||
.chain(combModel -> updateRegister(data, c, combModel, true, true))
|
||||
.map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
|
||||
.setCategorieInscrite(r.getCategoriesInscrites()));
|
||||
} else {
|
||||
return findGuestOrInit(data.getFname(), data.getLname(), c)
|
||||
.invoke(Unchecked.consumer(model -> {
|
||||
if (data.getCategorie() == null)
|
||||
throw new DBadRequestException(trad.t("categorie.requise"));
|
||||
model.setCategorie(data.getCategorie());
|
||||
|
||||
if (data.getGenre() == null) {
|
||||
if (model.getGenre() == null)
|
||||
data.setGenre(Genre.NA);
|
||||
} else
|
||||
model.setGenre(data.getGenre());
|
||||
|
||||
if (data.getClub() == null) {
|
||||
if (model.getClub() == null)
|
||||
data.setClub("");
|
||||
} else
|
||||
model.setClub(data.getClub());
|
||||
|
||||
if (data.getCountry() == null) {
|
||||
if (model.getCountry() == null)
|
||||
data.setCountry("FR");
|
||||
} else
|
||||
model.setCountry(data.getCountry());
|
||||
|
||||
if (c.getRequiredWeight().contains(model.getCategorie())) {
|
||||
if (data.getCountry() != null)
|
||||
model.setWeight(data.getWeight());
|
||||
}
|
||||
}))
|
||||
.call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
|
||||
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", c,
|
||||
data.getCategoriesInscrites())
|
||||
.invoke(cats -> {
|
||||
g.getCategoriesInscrites().clear();
|
||||
g.getCategoriesInscrites().addAll(cats);
|
||||
g.getCategoriesInscrites().removeIf(cat -> cat.getCategories().stream()
|
||||
.noneMatch(e -> e.getCategorie().equals(g.getCategorie())));
|
||||
}))
|
||||
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
|
||||
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()))
|
||||
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
|
||||
}
|
||||
}
|
||||
|
||||
private Uni<RegisterModel> updateRegister(RegisterRequestData data, CompetitionModel c,
|
||||
MembreModel combModel, boolean admin) {
|
||||
MembreModel combModel, boolean admin, boolean append) {
|
||||
return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult()
|
||||
.onFailure().recoverWithNull()
|
||||
.map(Unchecked.function(r -> {
|
||||
if (r != null) {
|
||||
if (!admin && r.isLockEdit())
|
||||
throw new DForbiddenException(trad.t("insc.err3"));
|
||||
r.setWeight(data.getWeight());
|
||||
r.setOverCategory(data.getOverCategory());
|
||||
if (data.getOverCategory() != null || !append)
|
||||
if (data.getOverCategory() == null)
|
||||
r.setOverCategory(0);
|
||||
else
|
||||
r.setOverCategory(data.getOverCategory());
|
||||
r.setCategorie(
|
||||
(combModel.getBirth_date() == null) ? combModel.getCategorie() :
|
||||
Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
|
||||
@@ -392,34 +552,75 @@ public class CompetitionService {
|
||||
int days = Utils.getDaysBeforeCompetition(c.getDate());
|
||||
if (days > -7)
|
||||
r.setClub(combModel.getClub());
|
||||
if (admin)
|
||||
if (c.getRequiredWeight().contains(r.getCategorie2()))
|
||||
if (data.getCountry() != null || !append)
|
||||
r.setWeight(data.getWeight());
|
||||
if (admin) {
|
||||
r.setWeightReal(data.getWeightReal());
|
||||
r.setLockEdit(data.isLockEdit());
|
||||
}
|
||||
} else {
|
||||
r = new RegisterModel(c, combModel, data.getWeight(), data.getOverCategory(),
|
||||
(combModel.getBirth_date() == null) ? combModel.getCategorie() :
|
||||
Utils.getCategoryFormBirthDate(combModel.getBirth_date(),
|
||||
c.getDate()),
|
||||
(combModel.getClub() == null) ? null : combModel.getClub());
|
||||
if (admin)
|
||||
if (admin) {
|
||||
r.setWeightReal(data.getWeightReal());
|
||||
r.setLockEdit(data.isLockEdit());
|
||||
else
|
||||
} else
|
||||
r.setLockEdit(false);
|
||||
}
|
||||
|
||||
if (c.getSystem() == CompetitionSystem.SAFCA) {
|
||||
SReqRegister.sendIfNeed(serverCustom.clients,
|
||||
new CompetitionData.SimpleRegister(r.getMembre().getId(),
|
||||
r.getOverCategory(), r.getWeight(), r.getCategorie(),
|
||||
r.getOverCategory(), r.getWeight2(), r.getCategorie(),
|
||||
(r.getClub() == null) ? null : r.getClub().getId(),
|
||||
(r.getClub() == null) ? null : r.getClub().getName()), c.getId());
|
||||
}
|
||||
return r;
|
||||
}))
|
||||
.call(r -> Mutiny.fetch(r.getCategoriesInscrites()).chain(__ ->
|
||||
catPresetRepository.list("competition = ?1 AND id IN ?2", c, data.getCategoriesInscrites())
|
||||
.invoke(cats -> {
|
||||
if (data.isQuick()) {
|
||||
cats.removeIf(cat -> r.getCategoriesInscrites().stream()
|
||||
.anyMatch(cp -> cp.equals(cat)));
|
||||
} else {
|
||||
r.getCategoriesInscrites().clear();
|
||||
}
|
||||
r.getCategoriesInscrites().addAll(cats);
|
||||
r.getCategoriesInscrites()
|
||||
.removeIf(cat -> cat.getCategories().stream()
|
||||
.noneMatch(e -> e.getCategorie().equals(r.getCategorie2())));
|
||||
})))
|
||||
.chain(r -> Panache.withTransaction(() -> registerRepository.persist(r)))
|
||||
.call(r -> c.getSystem() == CompetitionSystem.INTERNAL ?
|
||||
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem());
|
||||
}
|
||||
|
||||
private Uni<CompetitionGuestModel> findGuestOrInit(String fname, String lname, CompetitionModel competition) {
|
||||
if (fname == null || lname == null)
|
||||
return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis")));
|
||||
return competitionGuestRepository.find(
|
||||
"unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2) AND competition = ?3",
|
||||
lname, fname, competition).firstResult()
|
||||
.map(guestModel -> {
|
||||
if (guestModel == null) {
|
||||
CompetitionGuestModel model = new CompetitionGuestModel();
|
||||
model.setFname(fname);
|
||||
if (lname.equals("__team"))
|
||||
model.setLname("_team");
|
||||
else
|
||||
model.setLname(lname);
|
||||
model.setCompetition(competition);
|
||||
return model;
|
||||
}
|
||||
return guestModel;
|
||||
});
|
||||
}
|
||||
|
||||
private Uni<MembreModel> findComb(Long licence, String fname, String lname) {
|
||||
if (licence != null && licence > 0) {
|
||||
return combRepository.find("licence = ?1", licence).firstResult()
|
||||
@@ -467,7 +668,8 @@ public class CompetitionService {
|
||||
.call(cm -> membreService.getById(combId)
|
||||
.invoke(Unchecked.consumer(model -> {
|
||||
if (model == null)
|
||||
throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId));
|
||||
throw new DNotFoundException(
|
||||
String.format(trad.t("le.membre.n.existe.pas"), combId));
|
||||
if (!securityCtx.isInClubGroup(model.getClub().getId()))
|
||||
throw new DForbiddenException();
|
||||
})))
|
||||
@@ -656,6 +858,12 @@ public class CompetitionService {
|
||||
.call(__ -> cache.invalidate(data.getId()));
|
||||
}
|
||||
|
||||
public Uni<List<PresetData>> getPresetsForCompetition(SecurityCtx securityCtx, Long id) {
|
||||
return permService.hasViewPerm(securityCtx, id)
|
||||
.chain(cm -> Mutiny.fetch(cm.getCatPreset()))
|
||||
.map(p -> p.stream().map(PresetData::fromModel).toList());
|
||||
}
|
||||
|
||||
public Uni<Response> unregisterHelloAsso(NotificationData data) {
|
||||
if (!data.getState().equals("Refunded"))
|
||||
return Uni.createFrom().item(Response.ok().build());
|
||||
@@ -689,8 +897,8 @@ public class CompetitionService {
|
||||
public Uni<Response> registerHelloAsso(NotificationData data) {
|
||||
String organizationSlug = data.getOrganizationSlug();
|
||||
String formSlug = data.getFormSlug();
|
||||
RegisterRequestData req = new RegisterRequestData(null, "", "", null, 0, false, null, Categorie.CADET, Genre.NA,
|
||||
null, "fr");
|
||||
RegisterRequestData req = new RegisterRequestData(null, "", "", null, null, 0, false, new ArrayList<>(), null,
|
||||
Categorie.CADET, Genre.NA, null, "fr", false);
|
||||
|
||||
return repository.find("data1 = ?1 AND data2 = ?2", organizationSlug, formSlug).firstResult()
|
||||
.onFailure().recoverWithNull()
|
||||
@@ -724,7 +932,7 @@ public class CompetitionService {
|
||||
.call(m -> Panache.withTransaction(() ->
|
||||
helloAssoRepository.persist(
|
||||
new HelloAssoRegisterModel(cm, m, data.getId()))))
|
||||
.chain(m -> updateRegister(req, cm, m, true)))
|
||||
.chain(m -> updateRegister(req, cm, m, true, true)))
|
||||
.onFailure().recoverWithItem(throwable -> {
|
||||
fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(),
|
||||
item.getUser().getFirstName(), optional.get()));
|
||||
|
||||
@@ -381,11 +381,15 @@ public class KeycloakService {
|
||||
}
|
||||
|
||||
public Optional<UserRepresentation> getUserById(String userId) {
|
||||
UserResource user = keycloak.realm(realm).users().get(userId);
|
||||
if (user == null)
|
||||
try {
|
||||
UserResource user = keycloak.realm(realm).users().get(userId);
|
||||
if (user == null)
|
||||
return Optional.empty();
|
||||
else
|
||||
return Optional.of(user.toRepresentation());
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
else
|
||||
return Optional.of(user.toRepresentation());
|
||||
}
|
||||
}
|
||||
|
||||
private String makeLogin(MembreModel model) {
|
||||
|
||||
@@ -122,7 +122,9 @@ public class LicenceService {
|
||||
.call(genLicenceNumberAndAccountIfNeed())
|
||||
: Uni.createFrom().nullItem()
|
||||
))
|
||||
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD, membreModel.getObjectName(),
|
||||
.call(licenceModel -> ls.logA(LogModel.ActionType.ADD,
|
||||
"%s (valid=%b, pay=%b, %s)" .formatted(membreModel.getObjectName(), model.isValidate(),
|
||||
model.isPay(), model.getCertificate()),
|
||||
licenceModel));
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -184,11 +184,17 @@ public class MembreService {
|
||||
|
||||
String finalSearch = search;
|
||||
return getLicenceListe(licenceRequest, payState)
|
||||
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList())
|
||||
.chain(ids -> {
|
||||
.chain(l -> {
|
||||
PanacheQuery<MembreModel> query;
|
||||
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit));
|
||||
return getPageResult(query, limit, page);
|
||||
query = repository.find(queryStr, sort, finalSearch,
|
||||
l.stream().map(l2 -> l2.getMembre().getId()).toList(), club).page(Page.ofSize(limit));
|
||||
return getPageResult(query, limit, page)
|
||||
.call(r -> (r.getResult().isEmpty() || !l.isEmpty() ?
|
||||
Uni.createFrom().item(l) :
|
||||
licenceRepository.list("saison = ?1 AND membre.id IN ?2", Utils.getSaison(),
|
||||
r.getResult().stream().map(SimpleMembre::getId).toList()))
|
||||
.invoke(l2 -> r.setAdditionalData(l2.stream().map(SimpleLicence::fromModel)))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,7 +216,7 @@ public class MembreService {
|
||||
public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) {
|
||||
return repository.find("userId = ?1", subject).firstResult()
|
||||
.chain(membreModel -> repository.list("club = ?1", membreModel.getClub()))
|
||||
.chain(membres -> licenceRepository.list("saison = ?1 AND membre IN ?2", Utils.getSaison(), membres)
|
||||
.chain(membres -> licenceRepository.list("membre IN ?1", membres)
|
||||
.map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList()));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
|
||||
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.cache.Cache;
|
||||
import io.quarkus.cache.CacheName;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.smallrye.mutiny.Multi;
|
||||
@@ -14,6 +17,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.*;
|
||||
@@ -45,9 +49,17 @@ public class ResultService {
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
CardRepository cardRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
@Inject
|
||||
@CacheName("club-classement")
|
||||
Cache cacheClubClassement;
|
||||
private static final HashMap<String, Long> cacheClubClassementExp = new HashMap<>();
|
||||
|
||||
private static final HashMap<Long, String> combTempIds = new HashMap<>();
|
||||
|
||||
private static String getCombTempId(Long key) {
|
||||
@@ -123,39 +135,44 @@ public class ResultService {
|
||||
}
|
||||
|
||||
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)));
|
||||
return hasAccess(uuid, securityCtx).chain(membreModel -> getData(uuid, poule, membreModel));
|
||||
}
|
||||
|
||||
public Uni<ResultCategoryData> getCategory(String uuid, long poule) {
|
||||
return getData(uuid, poule, null);
|
||||
}
|
||||
|
||||
private Uni<ResultCategoryData> getData(String uuid, long poule, MembreModel membreModel) {
|
||||
List<CardModel> cards = new ArrayList<>();
|
||||
|
||||
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));
|
||||
.chain(list -> cardRepository.list("competition.uuid = ?1", uuid).invoke(cards::addAll)
|
||||
.map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList()))
|
||||
.map(matchModels -> {
|
||||
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());
|
||||
out.setTreeIsClassement(categoryModel.isTreeAreClassement());
|
||||
|
||||
getArray2(matchModels, membreModel, out);
|
||||
getTree(categoryModel.getTree(), membreModel, cards, out);
|
||||
getClassementArray(categoryModel, membreModel, cards, out);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) {
|
||||
ResultCategoryData out = new ResultCategoryData();
|
||||
public void getArray2(List<MatchModelExtend> matchModels_, MembreModel membreModel, ResultCategoryData out) {
|
||||
List<MatchModelExtend> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
|
||||
|
||||
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, membreModel, out);
|
||||
getTree(categoryModel.getTree(), membreModel, out);
|
||||
return 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<>();
|
||||
for (MatchModel model : matchModels) {
|
||||
HashMap<Character, List<MatchModelExtend>> matchMap = new HashMap<>();
|
||||
for (MatchModelExtend model : matchModels) {
|
||||
char g = model.getPoule();
|
||||
if (!matchMap.containsKey(g))
|
||||
matchMap.put(g, new ArrayList<>());
|
||||
@@ -164,7 +181,7 @@ public class ResultService {
|
||||
|
||||
matchMap.forEach((c, matchEntities) -> {
|
||||
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream()
|
||||
.sorted(Comparator.comparing(MatchModel::getCategory_ord))
|
||||
.sorted(Comparator.comparing(MatchModelExtend::getCategory_ord))
|
||||
.map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
|
||||
ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
|
||||
.toList();
|
||||
@@ -174,46 +191,26 @@ public class ResultService {
|
||||
.distinct()
|
||||
.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.isC1(comb) || m.isC2(comb)))
|
||||
.forEach(matchModel -> {
|
||||
int win = matchModel.win();
|
||||
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.isC1(comb)) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
} else {
|
||||
pointMake.addAndGet(score.getS2());
|
||||
pointTake.addAndGet(score.getS1());
|
||||
}
|
||||
}
|
||||
});
|
||||
float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();
|
||||
|
||||
return new ResultCategoryData.RankArray(0,
|
||||
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), w.get(),
|
||||
pointMake.get(), pointTake.get(), pointRate);
|
||||
CombStat stat = makeStat(matchEntities, comb);
|
||||
return new ResultCategoryData.RankArray(0, comb,
|
||||
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
|
||||
stat.pointMake, stat.pointTake, stat.getPointRate());
|
||||
})
|
||||
.filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
|
||||
.sorted(Comparator
|
||||
.comparing(ResultCategoryData.RankArray::getWin)
|
||||
.comparing(ResultCategoryData.RankArray::getScore)
|
||||
.thenComparing(ResultCategoryData.RankArray::getWin)
|
||||
.thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
|
||||
.toList();
|
||||
out.getMatchs().put(c, matchs);
|
||||
|
||||
int lastScore = -1;
|
||||
int lastWin = -1;
|
||||
float pointRate = 0;
|
||||
int rank = 0;
|
||||
for (ResultCategoryData.RankArray rankArray1 : rankArray) {
|
||||
if (rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
|
||||
if (rankArray1.getScore() != lastScore || rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
|
||||
lastScore = rankArray1.getScore();
|
||||
lastWin = rankArray1.getWin();
|
||||
pointRate = rankArray1.getPointRate();
|
||||
rank++;
|
||||
@@ -224,27 +221,86 @@ public class ResultService {
|
||||
});
|
||||
}
|
||||
|
||||
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(), membreModel, privacy);
|
||||
}
|
||||
if (src.getRight() != null) {
|
||||
dst.setRight(new TreeNode<>());
|
||||
convertTree(src.getRight(), dst.getRight(), membreModel, privacy);
|
||||
public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
|
||||
ResultCategoryData out) {
|
||||
if ((categoryModel.getType() & 2) != 0) {
|
||||
AtomicInteger rank = new AtomicInteger(0);
|
||||
categoryModel.getTree().stream()
|
||||
.filter(t -> t.getLevel() > 0)
|
||||
.sorted(Comparator.comparing(TreeModel::getLevel))
|
||||
.forEach(treeModel -> makeClassementRow(membreModel,
|
||||
new MatchModelExtend(treeModel.getMatch(), cards), out, rank));
|
||||
|
||||
categoryModel.getTree().stream()
|
||||
.filter(t -> t.getLevel() <= -10)
|
||||
.sorted(Comparator.comparing(TreeModel::getLevel).reversed())
|
||||
.forEach(treeModel -> makeClassementRow(membreModel,
|
||||
new MatchModelExtend(treeModel.getMatch(), cards), out, rank));
|
||||
} else {
|
||||
for (List<ResultCategoryData.RankArray> list : out.getRankArray().values()) {
|
||||
for (ResultCategoryData.RankArray r : list) {
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(r.getRank(), r.getComb(), r.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, ResultCategoryData out) {
|
||||
private static void makeClassementRow(MembreModel membreModel, MatchModelExtend m, ResultCategoryData out,
|
||||
AtomicInteger rank) {
|
||||
if (m.isEnd()) {
|
||||
if (m.getWin() > 0) {
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
|
||||
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
|
||||
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
} else if (m.getWin() < 0) {
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC2(),
|
||||
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
|
||||
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
} else {
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), m.getC1(),
|
||||
m.getC1Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
out.getClassement()
|
||||
.add(new ResultCategoryData.ClassementData(rank.get(), m.getC2(),
|
||||
m.getC2Name(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)));
|
||||
}
|
||||
} else {
|
||||
out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
|
||||
out.getClassement().add(new ResultCategoryData.ClassementData(rank.incrementAndGet(), null, null));
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel,
|
||||
ResultPrivacy privacy, List<CardModel> cards) {
|
||||
dst.setData(
|
||||
ResultCategoryData.TreeData.from(new MatchModelExtend(src.getMatch(), cards), src.getLevel(),
|
||||
membreModel, privacy));
|
||||
if (src.getLeft() != null) {
|
||||
dst.setLeft(new TreeNode<>());
|
||||
convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy, cards);
|
||||
}
|
||||
if (src.getRight() != null) {
|
||||
dst.setRight(new TreeNode<>());
|
||||
convertTree(src.getRight(), dst.getRight(), membreModel, privacy, cards);
|
||||
}
|
||||
}
|
||||
|
||||
private void getTree(List<TreeModel> treeModels, MembreModel membreModel, List<CardModel> cards,
|
||||
ResultCategoryData out) {
|
||||
ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
|
||||
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);
|
||||
convertTree(treeModel, root, membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS, cards);
|
||||
trees.add(root);
|
||||
});
|
||||
out.setTrees(trees);
|
||||
@@ -262,10 +318,13 @@ public class ResultService {
|
||||
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)))
|
||||
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(cards -> new Pair<>(registers,
|
||||
matchModels.stream().map(m -> new MatchModelExtend(m, cards)).toList()))))
|
||||
|
||||
.map(pair -> {
|
||||
List<RegisterModel> registers = pair.getKey();
|
||||
List<MatchModel> matchModels = pair.getValue();
|
||||
List<MatchModelExtend> matchModels = pair.getValue();
|
||||
|
||||
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
|
||||
|
||||
@@ -275,12 +334,7 @@ public class ResultService {
|
||||
.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();
|
||||
|
||||
makeStat(matchModels, comb, w, l, pointMake, pointTake);
|
||||
CombStat stat = makeStat(matchModels, comb);
|
||||
|
||||
Categorie categorie = null;
|
||||
String clubName = null;
|
||||
@@ -302,14 +356,13 @@ public class ResultService {
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.w(stat.w);
|
||||
builder2.l(stat.l);
|
||||
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
|
||||
builder2.club(clubName);
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
builder2.pointMake(stat.pointMake);
|
||||
builder2.pointTake(stat.pointTake);
|
||||
builder2.ratioPoint(stat.getPointRate());
|
||||
|
||||
return builder2.build();
|
||||
})
|
||||
@@ -317,8 +370,11 @@ public class ResultService {
|
||||
.toList();
|
||||
|
||||
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.tt_match((int) matchModels.stream().filter(MatchModelExtend::isEnd).count());
|
||||
builder.point(matchModels.stream()
|
||||
.filter(MatchModelExtend::isEnd)
|
||||
.flatMap(m -> m.getScoresToCompute().stream())
|
||||
.mapToInt(s -> s.getS1() + s.getS2()).sum());
|
||||
builder.combs(combs);
|
||||
|
||||
return builder.build();
|
||||
@@ -339,12 +395,12 @@ public class ResultService {
|
||||
.map(models -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
models.forEach(
|
||||
r -> map.put(Utils.getFullName(r.getMembre()), getCombTempId(r.getMembre().getId())));
|
||||
r -> map.put(r.getMembre().getName(), getCombTempId(r.getMembre().getId())));
|
||||
return map;
|
||||
})
|
||||
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid)
|
||||
.chain(map -> competitionGuestRepository.list("competition.uuid = ?1 AND lname != \"__team\"", uuid)
|
||||
.map(models -> {
|
||||
models.forEach(guestModel -> map.put(Utils.getFullName(guestModel),
|
||||
models.forEach(guestModel -> map.put(guestModel.getName(),
|
||||
getCombTempId(guestModel.getId() * -1)));
|
||||
return map;
|
||||
})
|
||||
@@ -368,7 +424,7 @@ public class ResultService {
|
||||
return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found")));
|
||||
}
|
||||
|
||||
Uni<List<MatchModel>> uni;
|
||||
Uni<List<MatchModelExtend>> uni;
|
||||
if (id >= 0) {
|
||||
uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
|
||||
uuid, privacy).firstResult()
|
||||
@@ -382,8 +438,13 @@ public class ResultService {
|
||||
builder.cat((registerModel.getCategorie2() == null) ? "---" :
|
||||
registerModel.getCategorie2().getName(trad));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid,
|
||||
registerModel.getMembre());
|
||||
return matchRepository.list(
|
||||
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
|
||||
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id = ?2 OR m.c2_id = ?2 OR c1g = ?2 OR c2g = ?2)",
|
||||
uuid, registerModel.getMembre())
|
||||
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
|
||||
.toList()));
|
||||
}));
|
||||
} else {
|
||||
uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
|
||||
@@ -393,13 +454,18 @@ public class ResultService {
|
||||
builder.cat(
|
||||
(guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad));
|
||||
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)",
|
||||
uuid, guestModel);
|
||||
return matchRepository.list(
|
||||
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
|
||||
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)",
|
||||
uuid, guestModel)
|
||||
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
|
||||
.toList()));
|
||||
});
|
||||
}
|
||||
|
||||
return uni.invoke(matchModels -> {
|
||||
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct()
|
||||
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModelExtend::getCategory).distinct()
|
||||
.toList();
|
||||
List<CombArrayData.MatchsData> matchs = new ArrayList<>();
|
||||
|
||||
@@ -407,7 +473,7 @@ public class ResultService {
|
||||
AtomicInteger sumPointMake = new AtomicInteger(0);
|
||||
AtomicInteger sumPointTake = new AtomicInteger(0);
|
||||
|
||||
for (MatchModel matchModel : matchModels) {
|
||||
for (MatchModelExtend matchModel : matchModels) {
|
||||
if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
|
||||
(matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
|
||||
continue;
|
||||
@@ -416,42 +482,41 @@ public class ResultService {
|
||||
builder2.date(matchModel.getDate());
|
||||
builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory()))
|
||||
.map(CategoryModel::getName).findFirst().orElse(""));
|
||||
builder2.end(matchModel.isEnd());
|
||||
|
||||
AtomicInteger pointMake = new AtomicInteger();
|
||||
AtomicInteger pointTake = new AtomicInteger();
|
||||
|
||||
if (matchModel.isC1(id)) {
|
||||
builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest()));
|
||||
builder2.adv(matchModel.getC2Name());
|
||||
if (matchModel.isEnd()) {
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
matchModel.getScoresToCompute()
|
||||
.forEach(scoreEntity -> {
|
||||
pointMake.addAndGet(scoreEntity.getS1());
|
||||
pointTake.addAndGet(scoreEntity.getS2());
|
||||
});
|
||||
builder2.score(matchModel.getScores().stream()
|
||||
builder2.score(matchModel.getScoresToPrint().stream()
|
||||
.map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
|
||||
} else {
|
||||
builder2.score(new ArrayList<>());
|
||||
}
|
||||
builder2.win(matchModel.win() > 0);
|
||||
builder2.win(matchModel.isEnd() && matchModel.getWin() > 0);
|
||||
} else {
|
||||
builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest()));
|
||||
builder2.adv(matchModel.getC1Name());
|
||||
if (matchModel.isEnd()) {
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
matchModel.getScoresToCompute()
|
||||
.forEach(scoreEntity -> {
|
||||
pointMake.addAndGet(scoreEntity.getS2());
|
||||
pointTake.addAndGet(scoreEntity.getS1());
|
||||
});
|
||||
builder2.score(matchModel.getScores().stream()
|
||||
builder2.score(matchModel.getScoresToPrint().stream()
|
||||
.map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
|
||||
} else {
|
||||
builder2.score(new ArrayList<>());
|
||||
}
|
||||
builder2.win(matchModel.win() < 0);
|
||||
builder2.win(matchModel.isEnd() && matchModel.getWin() < 0);
|
||||
}
|
||||
|
||||
builder2.eq(matchModel.isEnd() && matchModel.getWin() == 0);
|
||||
builder2.ratio(
|
||||
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
|
||||
@@ -495,7 +560,7 @@ public class ResultService {
|
||||
@Builder
|
||||
@RegisterForReflection
|
||||
public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
|
||||
boolean win) {
|
||||
boolean win, boolean eq, boolean end) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,38 +605,59 @@ public class ResultService {
|
||||
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)));
|
||||
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
|
||||
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
|
||||
uuid, guests)
|
||||
|
||||
.chain(mm -> cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.map(cards ->
|
||||
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
|
||||
mm.stream().map(m -> new MatchModelExtend(m, cards)).toList(),
|
||||
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))));
|
||||
return cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.chain(cards -> clubRepository.findById(id).chain(clubModel ->
|
||||
registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
|
||||
.chain(registers -> matchRepository.list(
|
||||
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " +
|
||||
"WHERE m.category.compet.uuid = ?1 AND (m.c1_id IN ?2 OR m.c2_id IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
|
||||
uuid, registers.stream().map(RegisterModel::getMembre).toList())
|
||||
.map(matchModels -> matchModels.stream()
|
||||
.map(m -> new MatchModelExtend(m, cards)))
|
||||
.chain(s -> competitionGuestRepository.list(
|
||||
"competition.uuid = ?1 AND club = ?2", uuid, clubModel.getName())
|
||||
.chain(guests -> matchRepository.list(
|
||||
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " +
|
||||
"WHERE m.category.compet.uuid = ?1 AND (m.c1_guest IN ?2 OR m.c2_guest IN ?2 OR c1g IN ?2 OR c2g IN ?2)",
|
||||
uuid, guests)
|
||||
.map(mm -> new Pair<>(guests, Stream.concat(s, mm.stream()
|
||||
.map(m -> new MatchModelExtend(m, cards)))
|
||||
.distinct().toList()))
|
||||
))
|
||||
.map(p ->
|
||||
getClubArray2(clubModel.getName(),
|
||||
Stream.concat(
|
||||
registers.stream().map(RegisterModel::getMembre),
|
||||
p.getKey().stream()
|
||||
).toList(),
|
||||
p.getValue(), registers, membreModel)
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModel> matchModels,
|
||||
private ClubArrayData getClubArray2(String name, List<CombModel> combs, List<MatchModelExtend> 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);
|
||||
ArrayList<Long> win_ids = new ArrayList<>();
|
||||
ArrayList<Long> match_ids = new ArrayList<>();
|
||||
|
||||
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);
|
||||
CombStat stat = makeStat(matchModels, comb);
|
||||
|
||||
Categorie categorie = null;
|
||||
|
||||
@@ -587,23 +673,23 @@ public class ResultService {
|
||||
|
||||
builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
|
||||
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
|
||||
builder2.w(w.get());
|
||||
builder2.l(l.get());
|
||||
builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
|
||||
builder2.pointMake(pointMake.get());
|
||||
builder2.pointTake(pointTake.get());
|
||||
builder2.ratioPoint((pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
|
||||
builder2.w(stat.w);
|
||||
builder2.l(stat.l);
|
||||
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l);
|
||||
builder2.pointMake(stat.pointMake);
|
||||
builder2.pointTake(stat.pointTake);
|
||||
builder2.ratioPoint(stat.getPointRate());
|
||||
|
||||
tt_win.addAndGet(w.get());
|
||||
tt_match.addAndGet(w.get() + l.get());
|
||||
win_ids.addAll(stat.win_ids);
|
||||
match_ids.addAll(stat.match_ids);
|
||||
|
||||
return builder2.build();
|
||||
})
|
||||
.sorted(Comparator.comparing(ClubArrayData.CombData::name))
|
||||
.toList();
|
||||
|
||||
builder.nb_match(tt_match.get());
|
||||
builder.match_w(tt_win.get());
|
||||
builder.nb_match((int) match_ids.stream().distinct().count());
|
||||
builder.match_w((int) win_ids.stream().distinct().count());
|
||||
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());
|
||||
@@ -615,30 +701,36 @@ public class ResultService {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void makeStat(List<MatchModel> matchModels, CombModel comb, AtomicInteger w, AtomicInteger l,
|
||||
AtomicInteger pointMake, AtomicInteger pointTake) {
|
||||
private static CombStat makeStat(List<MatchModelExtend> matchModels, CombModel comb) {
|
||||
CombStat stat = new CombStat();
|
||||
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();
|
||||
stat.match_ids.add(matchModel.getId());
|
||||
|
||||
int win = matchModel.getWin();
|
||||
if (win == 0) {
|
||||
stat.score += 1;
|
||||
} else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
|
||||
stat.w++;
|
||||
stat.win_ids.add(matchModel.getId());
|
||||
stat.score += 2;
|
||||
} else {
|
||||
l.getAndIncrement();
|
||||
stat.l++;
|
||||
}
|
||||
|
||||
matchModel.getScores().stream()
|
||||
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
|
||||
matchModel.getScoresToCompute()
|
||||
.forEach(score -> {
|
||||
if (matchModel.isC1(comb)) {
|
||||
pointMake.addAndGet(score.getS1());
|
||||
pointTake.addAndGet(score.getS2());
|
||||
stat.pointMake += score.getS1();
|
||||
stat.pointTake += score.getS2();
|
||||
} else {
|
||||
pointMake.addAndGet(score.getS2());
|
||||
pointTake.addAndGet(score.getS1());
|
||||
stat.pointMake += score.getS2();
|
||||
stat.pointTake += score.getS1();
|
||||
}
|
||||
});
|
||||
});
|
||||
return stat;
|
||||
}
|
||||
|
||||
@Builder
|
||||
@@ -652,6 +744,135 @@ public class ResultService {
|
||||
}
|
||||
}
|
||||
|
||||
public Uni<List<ClubClassement>> getAllClubArray(String uuid) {
|
||||
return getAllClubArray(uuid, true);
|
||||
}
|
||||
|
||||
public Uni<List<ClubClassement>> getAllClubArray(String uuid, SecurityCtx securityCtx) {
|
||||
return hasAccess(uuid, securityCtx).chain(membreModel -> getAllClubArray(uuid, true));
|
||||
}
|
||||
|
||||
public Uni<List<ClubClassement>> getAllClubArray(String uuid, boolean cache) {
|
||||
List<CardModel> cards = new java.util.ArrayList<>();
|
||||
|
||||
return (!cache || cacheClubClassementExp.getOrDefault(uuid, 0L) < System.currentTimeMillis() ?
|
||||
cacheClubClassement.invalidate(uuid).invoke(() -> cacheClubClassementExp.remove(uuid)) :
|
||||
Uni.createFrom().voidItem()
|
||||
).chain(o -> cacheClubClassement.getAsync(uuid, k -> cardRepository.list("competition.uuid = ?1", uuid)
|
||||
.invoke(__ -> System.out.println("Cache miss for club classement with uuid " + uuid))
|
||||
.invoke(cards::addAll)
|
||||
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", uuid))
|
||||
.chain(matchs -> {
|
||||
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
|
||||
for (MatchModel match : matchs) {
|
||||
if (!map.containsKey(match.getCategory()))
|
||||
map.put(match.getCategory(), new java.util.ArrayList<>());
|
||||
map.get(match.getCategory()).add(match);
|
||||
}
|
||||
|
||||
return Multi.createFrom().iterable(map.entrySet())
|
||||
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
|
||||
.map(entry -> {
|
||||
ResultCategoryData tmp = new ResultCategoryData();
|
||||
|
||||
getArray2(entry.getValue().stream().map(m -> new MatchModelExtend(m, cards))
|
||||
.toList(),
|
||||
null, tmp);
|
||||
getClassementArray(entry.getKey(), null, cards, tmp);
|
||||
|
||||
return tmp;
|
||||
})
|
||||
.collect().asList();
|
||||
})
|
||||
.map(categoryData -> {
|
||||
HashMap<String, ClubClassement> clubMap = new HashMap<>();
|
||||
|
||||
categoryData.forEach(
|
||||
c -> c.getClassement().stream().map(ResultCategoryData.ClassementData::comb)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(comb -> {
|
||||
if (comb instanceof MembreModel membreModel2) {
|
||||
return (membreModel2.getClub() != null) ? membreModel2.getClub()
|
||||
.getName() : "";
|
||||
} else if (comb instanceof CompetitionGuestModel guestModel) {
|
||||
return guestModel.getClub();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(s -> s != null && !s.isBlank() && !s.equals("Team"))
|
||||
.distinct()
|
||||
.forEach(clubName -> clubMap.putIfAbsent(clubName,
|
||||
new ClubClassement(clubName))));
|
||||
|
||||
categoryData.forEach(c -> c.getClassement().forEach(classementData -> {
|
||||
if (classementData.rank() > 3)
|
||||
return;
|
||||
|
||||
if (classementData.comb() != null) {
|
||||
String clubName = "";
|
||||
if (classementData.comb() instanceof MembreModel membreModel2) {
|
||||
clubName = (membreModel2.getClub() != null) ? membreModel2.getClub()
|
||||
.getName() : "";
|
||||
} else if (classementData.comb() instanceof CompetitionGuestModel guestModel) {
|
||||
clubName = guestModel.getClub();
|
||||
}
|
||||
|
||||
if (clubName != null && !clubName.isBlank()
|
||||
&& !clubName.equals("Team") && clubMap.containsKey(clubName)) {
|
||||
ClubClassement entity = clubMap.get(clubName);
|
||||
entity.score[classementData.rank() - 1]++;
|
||||
entity.tt_score += 4 - classementData.rank();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return clubMap.values().stream()
|
||||
.sorted(Comparator.comparingInt((ClubClassement c) -> c.tt_score)
|
||||
.thenComparingInt(c -> c.score[0])
|
||||
.thenComparingInt(c -> c.score[1])
|
||||
.thenComparingInt(c -> c.score[2]).reversed())
|
||||
.toList();
|
||||
})
|
||||
.invoke(__ -> cacheClubClassementExp.put(uuid, System.currentTimeMillis() + 60 * 1000L))
|
||||
));
|
||||
}
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class ClubClassement {
|
||||
String name;
|
||||
Integer[] score = new Integer[]{0, 0, 0};
|
||||
int tt_score = 0;
|
||||
|
||||
public ClubClassement(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public static class CombStat {
|
||||
public int w;
|
||||
public int l;
|
||||
public int score;
|
||||
public int pointMake;
|
||||
public int pointTake;
|
||||
public ArrayList<Long> win_ids = new ArrayList<>();
|
||||
public ArrayList<Long> match_ids = new ArrayList<>();
|
||||
|
||||
public CombStat() {
|
||||
this.w = 0;
|
||||
this.l = 0;
|
||||
this.score = 0;
|
||||
this.pointMake = 0;
|
||||
this.pointTake = 0;
|
||||
}
|
||||
|
||||
public float getPointRate() {
|
||||
return (pointTake == 0) ? pointMake : (float) pointMake / pointTake;
|
||||
}
|
||||
}
|
||||
|
||||
private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
|
||||
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
|
||||
.firstResult()
|
||||
@@ -678,7 +899,8 @@ public class ResultService {
|
||||
securityCtx.getSubject())
|
||||
.chain(c2 -> {
|
||||
if (c2 > 0) return Uni.createFrom().item(m);
|
||||
return Uni.createFrom().failure(new DForbiddenException(trad.t("access.denied")));
|
||||
return Uni.createFrom()
|
||||
.failure(new DForbiddenException(trad.t("access.denied")));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package fr.titionfire.ffsaf.domain.service;
|
||||
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -21,11 +23,20 @@ public class TradService {
|
||||
return translate(key);
|
||||
}
|
||||
|
||||
public String t(String key, WebSocketConnection connection) {
|
||||
List<String> lang = connection.handshakeRequest().headers().get("Accept-Language");
|
||||
Locale userLocale = lang != null && !lang.isEmpty() ? Locale.forLanguageTag(lang.get(0)) : fallbackLocale;
|
||||
return translate(key, userLocale);
|
||||
}
|
||||
|
||||
public String translate(String key) {
|
||||
ContainerRequestContext requestContext = requestContextInstance.get();
|
||||
Locale userLocale = (Locale) requestContext.getProperty("userLocale");
|
||||
|
||||
return translate(key, userLocale);
|
||||
}
|
||||
|
||||
public String translate(String key, Locale userLocale) {
|
||||
try {
|
||||
ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale);
|
||||
return messages.getString(key);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package fr.titionfire.ffsaf.rest;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.service.CompetitionService;
|
||||
import fr.titionfire.ffsaf.rest.data.CompetitionData;
|
||||
import fr.titionfire.ffsaf.rest.data.RegisterRequestData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleCompetData;
|
||||
import fr.titionfire.ffsaf.rest.data.SimpleRegisterComb;
|
||||
import fr.titionfire.ffsaf.rest.data.*;
|
||||
import fr.titionfire.ffsaf.utils.SecurityCtx;
|
||||
import io.quarkus.security.Authenticated;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
@@ -53,6 +50,16 @@ public class CompetitionEndpoints {
|
||||
return service.addRegisterComb(securityCtx, id, data, source);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{id}/registers/{source}")
|
||||
@Authenticated
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(hidden = true)
|
||||
public Uni<List<SimpleRegisterComb>> addRegistersComb(@PathParam("id") Long id, @PathParam("source") String source,
|
||||
List<RegisterRequestData> data) {
|
||||
return service.addRegistersComb(securityCtx, id, data, source);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{id}/register/{comb_id}/{source}")
|
||||
@Authenticated
|
||||
@@ -79,6 +86,13 @@ public class CompetitionEndpoints {
|
||||
return service.getInternalData(securityCtx, id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{id}/categories")
|
||||
@Authenticated
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<List<PresetData>> getPresetsForCompetition(@PathParam("id") Long id) {
|
||||
return service.getPresetsForCompetition(securityCtx, id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
|
||||
@@ -8,6 +8,7 @@ import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@Path("api/public/result/{id}")
|
||||
public class ExternalResultEndpoints {
|
||||
@@ -73,6 +74,13 @@ public class ExternalResultEndpoints {
|
||||
return resultService.getClubList(id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/club/classement")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Uni<List<ResultService.ClubClassement>> clubClassement() {
|
||||
return resultService.getAllClubArray(id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/club/data")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
||||
@@ -47,6 +47,12 @@ public class ResultEndpoints {
|
||||
return resultService.getClubList(uuid, securityCtx);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{uuid}/club/classement")
|
||||
public Uni<List<ResultService.ClubClassement>> getClubClassement(@PathParam("uuid") String uuid) {
|
||||
return resultService.getAllClubArray(uuid, securityCtx);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{uuid}/club/{id}")
|
||||
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {
|
||||
@@ -64,7 +70,7 @@ public class ResultEndpoints {
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CatPresetModel;
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
@@ -10,6 +11,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
@@ -33,7 +35,7 @@ public class CompetitionData {
|
||||
private Long club;
|
||||
private String clubName;
|
||||
private String owner;
|
||||
private List<SimpleRegister> registers;
|
||||
private List<SimpleRegister> registers; // for SAFCA
|
||||
private boolean canEdit;
|
||||
private boolean canEditRegisters;
|
||||
private String data1;
|
||||
@@ -41,6 +43,15 @@ public class CompetitionData {
|
||||
private String data3;
|
||||
private String data4;
|
||||
private String config;
|
||||
private List<PresetData> presets;
|
||||
private List<Categorie> requiredWeight;
|
||||
|
||||
public CompetitionData() {
|
||||
this(null, "", "", "", "", new Date(), new Date(),
|
||||
CompetitionSystem.INTERNAL, RegisterMode.FREE, new Date(), new Date(), true,
|
||||
null, "", "", null, true, true,
|
||||
"", "", "", "", "{}", new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
|
||||
public static CompetitionData fromModel(CompetitionModel model) {
|
||||
if (model == null)
|
||||
@@ -50,7 +61,8 @@ public class CompetitionData {
|
||||
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, false,
|
||||
model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig());
|
||||
model.getData1(), model.getData2(), model.getData3(), model.getData4(), model.getConfig(),
|
||||
new ArrayList<>(), model.getRequiredWeight());
|
||||
}
|
||||
|
||||
public static CompetitionData fromModelLight(CompetitionModel model) {
|
||||
@@ -61,7 +73,7 @@ public class CompetitionData {
|
||||
model.getAdresse(), "", model.getDate(), model.getTodate(), null,
|
||||
model.getRegisterMode(), model.getStartRegister(), model.getEndRegister(), model.isPublicVisible(),
|
||||
null, model.getClub().getName(), "", null, false, false,
|
||||
"", "", "", "", "{}");
|
||||
"", "", "", "", "{}", new ArrayList<>(), model.getRequiredWeight());
|
||||
|
||||
if (model.getRegisterMode() == RegisterMode.HELLOASSO) {
|
||||
out.setData1(model.getData1());
|
||||
@@ -75,22 +87,27 @@ public class CompetitionData {
|
||||
public CompetitionData addInsc(List<RegisterModel> insc, List<CompetitionGuestModel> guests) {
|
||||
this.registers = Stream.concat(
|
||||
insc.stream()
|
||||
.map(i -> new SimpleRegister(i.getMembre().getId(), i.getOverCategory(), i.getWeight(),
|
||||
.map(i -> new SimpleRegister(i.getMembre().getId(), i.getOverCategory(), i.getWeight2(),
|
||||
i.getCategorie(), (i.getClub() == null) ? null : i.getClub().getId(),
|
||||
(i.getClub() == null) ? null : i.getClub().getName())),
|
||||
guests.stream()
|
||||
.map(i -> new SimpleRegister(i.getId() * -1, 0, i.getWeight(),
|
||||
.map(i -> new SimpleRegister(i.getId() * -1, 0, i.getWeight2(),
|
||||
i.getCategorie(), null, i.getClub()))).toList();
|
||||
return this;
|
||||
}
|
||||
|
||||
public CompetitionData addPresets(List<CatPresetModel> presets) {
|
||||
this.presets = presets.stream().map(PresetData::fromModel).toList();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public static class SimpleRegister {
|
||||
long id;
|
||||
int overCategory;
|
||||
Integer weight;
|
||||
Float weight;
|
||||
Categorie categorie;
|
||||
Long club;
|
||||
String club_str;
|
||||
|
||||
31
src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java
Normal file
31
src/main/java/fr/titionfire/ffsaf/rest/data/PresetData.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CatPresetModel;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class PresetData {
|
||||
private Long id;
|
||||
private String name;
|
||||
private CatPresetModel.SwordType sword;
|
||||
private CatPresetModel.ShieldType shield;
|
||||
private List<CatPresetModel.CategorieEmbeddable> categories;
|
||||
private int mandatoryProtection1;
|
||||
private int mandatoryProtection2;
|
||||
|
||||
public static PresetData fromModel(CatPresetModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new PresetData(model.getId(), model.getName(), model.getSwordType(), model.getShieldType(),
|
||||
model.getCategories(), model.getMandatoryProtection1(), model.getMandatoryProtection2());
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -16,9 +18,11 @@ public class RegisterRequestData {
|
||||
private String fname;
|
||||
private String lname;
|
||||
|
||||
private Integer weight;
|
||||
private int overCategory;
|
||||
private Float weight;
|
||||
private Float weightReal;
|
||||
private Integer overCategory;
|
||||
private boolean lockEdit = false;
|
||||
private List<Long> categoriesInscrites;
|
||||
|
||||
// for guest registration only
|
||||
private Long id = null;
|
||||
@@ -26,4 +30,6 @@ public class RegisterRequestData {
|
||||
private Genre genre = Genre.NA;
|
||||
private String club = null;
|
||||
private String country = null;
|
||||
|
||||
private boolean quick = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import fr.titionfire.ffsaf.data.model.CombModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
|
||||
import fr.titionfire.ffsaf.utils.ResultPrivacy;
|
||||
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
|
||||
import fr.titionfire.ffsaf.utils.TreeNode;
|
||||
@@ -11,6 +13,7 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -20,9 +23,11 @@ import java.util.List;
|
||||
public class ResultCategoryData {
|
||||
int type;
|
||||
String name;
|
||||
boolean treeIsClassement;
|
||||
HashMap<Character, List<PouleArrayData>> matchs = new HashMap<>();
|
||||
HashMap<Character, List<RankArray>> rankArray = new HashMap<>();
|
||||
ArrayList<TreeNode<TreeData>> trees;
|
||||
List<ClassementData> classement = new ArrayList<>();
|
||||
String[] liceName;
|
||||
long genTime;
|
||||
|
||||
@@ -31,7 +36,10 @@ public class ResultCategoryData {
|
||||
@RegisterForReflection
|
||||
public static class RankArray {
|
||||
int rank;
|
||||
@JsonIgnore
|
||||
CombModel comb;
|
||||
String name;
|
||||
int score;
|
||||
int win;
|
||||
int pointMake;
|
||||
int pointTake;
|
||||
@@ -39,25 +47,37 @@ 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, MembreModel membreModel, ResultPrivacy privacy) {
|
||||
public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue,
|
||||
boolean eq, boolean end, Date date) {
|
||||
public static PouleArrayData fromModel(MatchModelExtend matchModel, MembreModel membreModel,
|
||||
ResultPrivacy privacy) {
|
||||
return new PouleArrayData(
|
||||
matchModel.getC1Name(membreModel, privacy),
|
||||
matchModel.isEnd() && matchModel.win() > 0,
|
||||
matchModel.isEnd() && matchModel.getWin() > 0,
|
||||
matchModel.isEnd() ?
|
||||
matchModel.getScores().stream().map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()
|
||||
matchModel.getScoresToPrint().stream().map(s -> new Integer[]{s.getS1(), s.getS2()})
|
||||
.toList()
|
||||
: new ArrayList<>(),
|
||||
matchModel.isEnd() && matchModel.win() < 0,
|
||||
matchModel.isEnd() && matchModel.getWin() < 0,
|
||||
matchModel.getC2Name(membreModel, privacy),
|
||||
matchModel.isEnd());
|
||||
matchModel.isEnd() && matchModel.getWin() == 0,
|
||||
matchModel.isEnd(),
|
||||
matchModel.getDate());
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores,
|
||||
boolean end) {
|
||||
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());
|
||||
boolean end, int win, int level, Date date) {
|
||||
public static TreeData from(MatchModelExtend match, int level, MembreModel membreModel, ResultPrivacy privacy) {
|
||||
return new TreeData(match.getId(), match.getC1Name(membreModel, privacy),
|
||||
match.getC2Name(membreModel, privacy), match.getScoresToPrint(), match.isEnd(), match.getWin(),
|
||||
level, match.getDate());
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public static record ClassementData(int rank, @JsonIgnore CombModel comb, String name) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,7 +28,27 @@ public class SimpleMembreInOutData {
|
||||
|
||||
public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) {
|
||||
LicenceModel currentLicence = lc.stream().filter(l -> l.getMembre().getId().equals(membreModel.getId()))
|
||||
.findFirst().orElse(null);
|
||||
.max(Comparator.comparingInt(LicenceModel::getSaison)).orElse(null);
|
||||
|
||||
String certif = null;
|
||||
if (currentLicence != null && currentLicence.getCertificate() != null) {
|
||||
String[] strings = currentLicence.getCertificate().split("¤");
|
||||
if (currentLicence.getSaison() == Utils.getSaison()) {
|
||||
certif = currentLicence.getCertificate();
|
||||
} else if (strings.length > 1) {
|
||||
try {
|
||||
Date date = new SimpleDateFormat("yyyy-MM-dd").parse(strings[1]);
|
||||
Calendar max = Utils.getFirstDateOfSaison();
|
||||
max.add(Calendar.YEAR, -2);
|
||||
|
||||
if (max.getTime().compareTo(date) < 0) {
|
||||
certif = currentLicence.getCertificate();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new SimpleMembreInOutData(
|
||||
membreModel.getLicence(),
|
||||
@@ -33,8 +57,8 @@ public class SimpleMembreInOutData {
|
||||
membreModel.getEmail(),
|
||||
membreModel.getGenre().str,
|
||||
membreModel.getBirth_date(),
|
||||
currentLicence != null,
|
||||
currentLicence == null ? null : currentLicence.getCertificate()
|
||||
currentLicence != null && currentLicence.getSaison() == Utils.getSaison(),
|
||||
certif
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package fr.titionfire.ffsaf.rest.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.LicenceModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
import fr.titionfire.ffsaf.data.model.*;
|
||||
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.Genre;
|
||||
@@ -11,11 +8,14 @@ import fr.titionfire.ffsaf.utils.Utils;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class SimpleRegisterComb {
|
||||
private long id;
|
||||
@@ -26,10 +26,12 @@ public class SimpleRegisterComb {
|
||||
private Categorie categorie;
|
||||
private SimpleClubModel club;
|
||||
private Integer licence;
|
||||
private Integer weight;
|
||||
private Float weight;
|
||||
private Float weightReal;
|
||||
private int overCategory;
|
||||
private boolean hasLicenceActive;
|
||||
private boolean lockEdit;
|
||||
private List<Long> categoriesInscrites;
|
||||
|
||||
public static SimpleRegisterComb fromModel(RegisterModel register, List<LicenceModel> licences) {
|
||||
MembreModel membreModel = register.getMembre();
|
||||
@@ -37,15 +39,21 @@ public class SimpleRegisterComb {
|
||||
membreModel.getGenre(), membreModel.getCountry(),
|
||||
(register.getCategorie() == null) ? null : register.getCategorie(),
|
||||
SimpleClubModel.fromModel(register.getClub()), membreModel.getLicence(), register.getWeight(),
|
||||
register.getOverCategory(),
|
||||
register.getWeightReal(), register.getOverCategory(),
|
||||
licences.stream().anyMatch(l -> l.isValidate() && l.getSaison() == Utils.getSaison()),
|
||||
register.isLockEdit());
|
||||
register.isLockEdit(), new ArrayList<>());
|
||||
}
|
||||
|
||||
public static SimpleRegisterComb fromModel(CompetitionGuestModel guest) {
|
||||
return new SimpleRegisterComb(guest.getId() * -1, guest.getFname(), guest.getLname(),
|
||||
guest.getGenre(), guest.getCountry(), guest.getCategorie(),
|
||||
new SimpleClubModel(null, guest.getClub(), "fr", null),
|
||||
null, guest.getWeight(), 0, false, false);
|
||||
null, guest.getWeight(), guest.getWeightReal(), 0, false, false,
|
||||
new ArrayList<>());
|
||||
}
|
||||
|
||||
public SimpleRegisterComb setCategorieInscrite(List<CatPresetModel> presets) {
|
||||
this.categoriesInscrites = presets.stream().map(CatPresetModel::getId).toList();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import java.util.List;
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class PageResult<T> {
|
||||
@Schema(description = "Le numéro de la page courante.", example = "1")
|
||||
@Schema(description = "Le numéro de la page courante.", examples = "1")
|
||||
private int page;
|
||||
@Schema(description = "Le nombre d'éléments par page.", example = "10")
|
||||
@Schema(description = "Le nombre d'éléments par page.", examples = "10")
|
||||
private int page_size;
|
||||
@Schema(description = "Le nombre total de pages.", example = "5")
|
||||
@Schema(description = "Le nombre total de pages.", examples = "5")
|
||||
private int page_count;
|
||||
@Schema(description = "Le nombre total d'éléments.", example = "47")
|
||||
@Schema(description = "Le nombre total d'éléments.", examples = "47")
|
||||
private long result_count;
|
||||
private List<T> result = new ArrayList<>();
|
||||
private Object additionalData;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -117,6 +119,15 @@ public class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
public static Calendar getFirstDateOfSaison() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
calendar.set(getSaison(), Calendar.SEPTEMBER, 1, 0, 0, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
public static Calendar toCalendar(Date date) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
@@ -244,15 +255,16 @@ public class Utils {
|
||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
|
||||
File[] files = dirFile.listFiles(filter);
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
for (File f2 : files) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
f2.delete();
|
||||
}
|
||||
}
|
||||
|
||||
File f = file.filePath().toFile();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0]));
|
||||
Path f = file.filePath();
|
||||
System.out.println(f + " -> " + new File(dirFile, id + "." + detectedExtensions[0]));
|
||||
Files.copy(f, new File(dirFile, id + "." + detectedExtensions[0]).toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
return "ok";
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -46,7 +46,16 @@ public class CompetitionWS {
|
||||
RRegister rRegister;
|
||||
|
||||
@Inject
|
||||
RCardboard rCardboard;
|
||||
RCard rCard;
|
||||
|
||||
@Inject
|
||||
RTeam rTeam;
|
||||
|
||||
@Inject
|
||||
RState rState;
|
||||
|
||||
@Inject
|
||||
RPDF rpdf;
|
||||
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
@@ -90,7 +99,10 @@ public class CompetitionWS {
|
||||
getWSReceiverMethods(RMatch.class, rMatch);
|
||||
getWSReceiverMethods(RCategorie.class, rCategorie);
|
||||
getWSReceiverMethods(RRegister.class, rRegister);
|
||||
getWSReceiverMethods(RCardboard.class, rCardboard);
|
||||
getWSReceiverMethods(RCard.class, rCard);
|
||||
getWSReceiverMethods(RTeam.class, rTeam);
|
||||
getWSReceiverMethods(RState.class, rState);
|
||||
getWSReceiverMethods(RPDF.class, rpdf);
|
||||
|
||||
executor = notifyExecutor;
|
||||
}
|
||||
@@ -137,6 +149,7 @@ public class CompetitionWS {
|
||||
LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size());
|
||||
|
||||
waitingResponse.remove(connection);
|
||||
rState.removeConnection(connection);
|
||||
}
|
||||
|
||||
private MessageOut makeReply(MessageIn message, Object data) {
|
||||
@@ -226,6 +239,30 @@ public class CompetitionWS {
|
||||
});
|
||||
}
|
||||
|
||||
public static void sendNotifyState(WebSocketConnection connection, String code, Object data) {
|
||||
String uuid = connection.pathParam("uuid");
|
||||
|
||||
List<Uni<Void>> queue = new ArrayList<>();
|
||||
queue.add(Uni.createFrom().voidItem()); // For avoid empty queue
|
||||
|
||||
connection.getOpenConnections().forEach(c -> {
|
||||
Boolean s = c.userData().get(UserData.TypedKey.forBoolean("needState"));
|
||||
if (uuid.equals(c.pathParam("uuid")) && s != null && s) {
|
||||
queue.add(c.sendText(new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data)));
|
||||
}
|
||||
});
|
||||
|
||||
Uni.join().all(queue)
|
||||
.andCollectFailures()
|
||||
.runSubscriptionOn(executor)
|
||||
.subscribeAsCompletionStage()
|
||||
.whenComplete((v, t) -> {
|
||||
if (t != null) {
|
||||
LOGGER.error("Error sending ws_out message", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnError
|
||||
Uni<Void> error(WebSocketConnection connection, ForbiddenException t) {
|
||||
return connection.close(CloseReason.INTERNAL_SERVER_ERROR);
|
||||
|
||||
159
src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java
Normal file
159
src/main/java/fr/titionfire/ffsaf/ws/recv/RCard.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CardRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.ClubCardRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.domain.service.CardService;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSCard;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RCard {
|
||||
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
ClubCardRepository clubCardRepository;
|
||||
|
||||
@Inject
|
||||
CardRepository cardRepository;
|
||||
|
||||
@Inject
|
||||
CardService cardService;
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver", connection));
|
||||
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied", connection));
|
||||
}));
|
||||
}
|
||||
|
||||
@WSReceiver(code = "getCardForMatch", permission = PermLevel.VIEW)
|
||||
public Uni<List<CardModel>> getCardForMatch(WebSocketConnection connection, Long matchId) {
|
||||
if (matchId == null)
|
||||
return Uni.createFrom().nullItem();
|
||||
return getById(matchId, connection).chain(matchModel -> cardService.getForMatch(matchModel));
|
||||
}
|
||||
|
||||
@WSReceiver(code = "getAllForTeamNoDetail", permission = PermLevel.VIEW)
|
||||
public Uni<List<SendTeamCards>> getAllForTeamNoDetail(WebSocketConnection connection, Object o) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(c -> clubCardRepository.list("competition = ?1", c.getId()))
|
||||
.map(cards -> cards.stream()
|
||||
.map(card -> new SendTeamCards(card.getTeamUuid(), card.getTeamName(), List.of(),
|
||||
card.getType(), card.getReason(), card.getDate()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCardAdd", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCardAdd(WebSocketConnection connection, SendCardAdd card) {
|
||||
return getById(card.matchId(), connection)
|
||||
.chain(matchModel -> cardService.checkCanBeAdded(card, matchModel)
|
||||
.chain(c -> {
|
||||
CardModel model = new CardModel();
|
||||
model.setComb(card.combId());
|
||||
model.setMatch(card.matchId());
|
||||
model.setCategory(matchModel.getCategory().getId());
|
||||
model.setCompetition(matchModel.getCategory().getCompet());
|
||||
model.setCompetitionId(matchModel.getCategory().getCompet().getId());
|
||||
model.setType(card.type());
|
||||
model.setReason(card.reason());
|
||||
|
||||
return Panache.withTransaction(() -> cardRepository.persist(model));
|
||||
})
|
||||
)
|
||||
.invoke(cardModel -> SSCard.sendCards(connection, List.of(cardModel)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCardRm", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> sendCardRm(WebSocketConnection connection, SendCardAdd card) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(competition -> cardRepository.find(
|
||||
"match IS NULL AND comb = ?1 AND type = ?2 AND competition = ?3 AND match " + (card.matchId() == null ? "IS NULL" : "= " + card.matchId()),
|
||||
card.combId(), card.type(), competition)
|
||||
.firstResult()
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("carton.non.trouver", connection));
|
||||
SSCard.sendRmCards(connection, List.of(o.getId()));
|
||||
}))
|
||||
.chain(cardModel -> Panache.withTransaction(() -> cardRepository.delete(cardModel)))
|
||||
)
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "applyTeamCards", permission = PermLevel.TABLE)
|
||||
public Uni<Void> applyTeamCards(WebSocketConnection connection, SendTeamCards teamCards) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(c -> cardService.addTeamCard(c, teamCards.teamUuid(), teamCards.teamName(), teamCards.type,
|
||||
teamCards.reason()))
|
||||
.invoke(cards -> SSCard.sendTeamCard(connection,
|
||||
new SendTeamCards(teamCards.teamUuid(), teamCards.teamName(), cards, teamCards.type(),
|
||||
teamCards.reason(), new Date())))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendTeamCardReturnState", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendTeamCardReturnState(WebSocketConnection connection, SendTeamCardReturnState state) {
|
||||
if (state.state <= 0 || state.state > 2)
|
||||
return Uni.createFrom().voidItem();
|
||||
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(c -> cardService.recvReturnState(c, state))
|
||||
.invoke(cards -> SSCard.sendCards(connection, cards))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "removeTeamCards", permission = PermLevel.TABLE)
|
||||
public Uni<Void> removeTeamCards(WebSocketConnection connection, SendTeamCards teamCards) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(c -> cardService.rmTeamCard(c, teamCards.teamUuid(), teamCards.teamName(), teamCards.type))
|
||||
.invoke(cards -> SSCard.sendRmCards(connection, cards))
|
||||
.invoke(__ -> SSCard.rmTeamCard(connection, teamCards))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record SendCardAdd(Long matchId, long combId, CardModel.CardType type, String reason) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record SendTeamCards(String teamUuid, String teamName, List<CardModel> cards, CardModel.CardType type,
|
||||
String reason, Date date) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record SendTeamCardReturnState(String teamUuid, String teamName, CardModel.CardType type,
|
||||
int state, Long selectedCategory, Long selectedMatch) {
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardboardModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CardboardRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSCardboard;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RCardboard {
|
||||
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver"));
|
||||
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
}));
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCardboardChange", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCardboardChange(WebSocketConnection connection, SendCardboard card) {
|
||||
return getById(card.matchId, connection)
|
||||
.chain(matchModel -> cardboardRepository.find("(comb.id = ?1 OR guestComb.id = ?2) AND match.id = ?3",
|
||||
card.combId, card.combId * -1, card.matchId).firstResult()
|
||||
.chain(model -> {
|
||||
if (model != null) {
|
||||
model.setRed(model.getRed() + card.red);
|
||||
model.setYellow(model.getYellow() + card.yellow);
|
||||
return Panache.withTransaction(() -> cardboardRepository.persist(model));
|
||||
}
|
||||
CardboardModel cardboardModel = new CardboardModel();
|
||||
|
||||
cardboardModel.setCompet(matchModel.getCategory().getCompet());
|
||||
cardboardModel.setMatch(matchModel);
|
||||
cardboardModel.setRed(card.red);
|
||||
cardboardModel.setYellow(card.yellow);
|
||||
cardboardModel.setComb(null);
|
||||
cardboardModel.setGuestComb(null);
|
||||
|
||||
if (card.combId >= 0) {
|
||||
if (matchModel.getC1_id() != null && matchModel.getC1_id().getId() == card.combId)
|
||||
cardboardModel.setComb(matchModel.getC1_id());
|
||||
if (matchModel.getC2_id() != null && matchModel.getC2_id().getId() == card.combId)
|
||||
cardboardModel.setComb(matchModel.getC2_id());
|
||||
} else {
|
||||
if (matchModel.getC1_guest() != null && matchModel.getC1_guest()
|
||||
.getId() == card.combId * -1)
|
||||
cardboardModel.setGuestComb(matchModel.getC1_guest());
|
||||
if (matchModel.getC2_guest() != null && matchModel.getC2_guest()
|
||||
.getId() == card.combId * -1)
|
||||
cardboardModel.setGuestComb(matchModel.getC2_guest());
|
||||
}
|
||||
|
||||
if (cardboardModel.getComb() == null && cardboardModel.getGuestComb() == null)
|
||||
return Uni.createFrom().nullItem();
|
||||
return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel));
|
||||
}))
|
||||
.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)
|
||||
.map(models -> {
|
||||
CardboardAllMatch out = new CardboardAllMatch();
|
||||
|
||||
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;
|
||||
}));
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record SendCardboard(long matchId, long combId, int yellow, int red) {
|
||||
}
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class CardboardAllMatch {
|
||||
int c1_yellow = 0;
|
||||
int c1_red = 0;
|
||||
int c2_yellow = 0;
|
||||
int c2_red = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
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.model.*;
|
||||
import fr.titionfire.ffsaf.data.repository.*;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.CardService;
|
||||
import fr.titionfire.ffsaf.domain.service.ResultService;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.data.PresetData;
|
||||
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.TreeNode;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSCategorie;
|
||||
import fr.titionfire.ffsaf.ws.send.SSMatch;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.UserData;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import io.smallrye.mutiny.unchecked.Unchecked;
|
||||
@@ -23,8 +28,9 @@ import jakarta.inject.Inject;
|
||||
import lombok.Data;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@@ -45,7 +51,16 @@ public class RCategorie {
|
||||
TreeRepository treeRepository;
|
||||
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
CardService cardService;
|
||||
|
||||
@Inject
|
||||
CardRepository cardRepository;
|
||||
|
||||
@Inject
|
||||
CatPresetRepository catPresetRepository;
|
||||
|
||||
@Inject
|
||||
ResultService resultService;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
@@ -54,9 +69,9 @@ public class RCategorie {
|
||||
return categoryRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver", connection));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
throw new DForbiddenException(trad.t("permission.denied", connection));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -76,6 +91,9 @@ public class RCategorie {
|
||||
fullCategory.setName(cat.getName());
|
||||
fullCategory.setLiceName(cat.getLiceName());
|
||||
fullCategory.setType(cat.getType());
|
||||
fullCategory.setTreeAreClassement(cat.isTreeAreClassement());
|
||||
fullCategory.setFullClassement(cat.isFullClassement());
|
||||
fullCategory.setPreset(PresetData.fromModel(cat.getPreset()));
|
||||
})
|
||||
.call(cat -> Mutiny.fetch(cat.getMatchs())
|
||||
.map(matchModels -> matchModels.stream().filter(o -> o.getCategory_ord() >= 0)
|
||||
@@ -84,6 +102,8 @@ public class RCategorie {
|
||||
.call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
|
||||
.map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
|
||||
.invoke(fullCategory::setTrees))
|
||||
.call(cat -> cardService.getAll(cat.getCompet())
|
||||
.invoke(fullCategory::setCards))
|
||||
.map(__ -> fullCategory);
|
||||
}
|
||||
|
||||
@@ -98,20 +118,67 @@ public class RCategorie {
|
||||
categoryModel.setTree(new ArrayList<>());
|
||||
categoryModel.setType(categorie.type);
|
||||
categoryModel.setLiceName(categorie.liceName);
|
||||
categoryModel.setTreeAreClassement(categorie.treeAreClassement);
|
||||
categoryModel.setFullClassement(categorie.fullClassement);
|
||||
|
||||
if (categorie.preset() != null)
|
||||
return catPresetRepository.findById(categorie.preset().getId())
|
||||
.invoke(categoryModel::setPreset)
|
||||
.chain(__ -> categoryRepository.create(categoryModel));
|
||||
return categoryRepository.create(categoryModel);
|
||||
})
|
||||
.invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
|
||||
.map(CategoryModel::getId);
|
||||
}
|
||||
|
||||
@WSReceiver(code = "createOrReplaceCategory", permission = PermLevel.ADMIN)
|
||||
public Uni<Long> createOrReplaceCategory(WebSocketConnection connection, JustCategorie categorie) {
|
||||
return matchRepository.list("category.compet.uuid = ?1 AND category.name = ?2", connection.pathParam("uuid"),
|
||||
categorie.name)
|
||||
.chain(existing -> {
|
||||
if (existing.isEmpty())
|
||||
return createCategory(connection, categorie);
|
||||
|
||||
Map<Long, List<MatchModel>> matchesByCategory = existing.stream()
|
||||
.filter(m -> m.getCategory() != null)
|
||||
.collect(Collectors.groupingBy(m -> m.getCategory().getId()));
|
||||
|
||||
for (Map.Entry<Long, List<MatchModel>> entry : matchesByCategory.entrySet()) {
|
||||
Long categoryId = entry.getKey();
|
||||
List<MatchModel> matches = entry.getValue();
|
||||
|
||||
if (matches.stream().noneMatch(m -> !m.getScores().isEmpty() || m.isEnd()))
|
||||
return Panache.withTransaction(() -> updateCategory(connection, categorie, categoryId)
|
||||
.call(__ -> treeRepository.delete("category = ?1", categoryId))
|
||||
.call(__ -> matchRepository.delete("category.id = ?1", categoryId)))
|
||||
.replaceWith(categoryId);
|
||||
}
|
||||
return createCategory(connection, categorie);
|
||||
});
|
||||
}
|
||||
|
||||
@WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) {
|
||||
return getById(categorie.id, connection)
|
||||
return updateCategory(connection, categorie, categorie.id);
|
||||
}
|
||||
|
||||
private Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie, Long id) {
|
||||
return getById(id, connection)
|
||||
.call(cat -> {
|
||||
if (categorie.preset() == null) {
|
||||
cat.setPreset(null);
|
||||
return Uni.createFrom().item(cat);
|
||||
} else {
|
||||
return catPresetRepository.findById(categorie.preset().getId())
|
||||
.invoke(cat::setPreset);
|
||||
}
|
||||
})
|
||||
.chain(cat -> {
|
||||
cat.setName(categorie.name);
|
||||
cat.setLiceName(categorie.liceName);
|
||||
cat.setType(categorie.type);
|
||||
cat.setTreeAreClassement(categorie.treeAreClassement);
|
||||
cat.setFullClassement(categorie.fullClassement);
|
||||
return Panache.withTransaction(() -> categoryRepository.persist(cat));
|
||||
})
|
||||
.call(cat -> {
|
||||
@@ -214,17 +281,161 @@ public class RCategorie {
|
||||
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();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "listPreset", permission = PermLevel.VIEW)
|
||||
public Uni<List<PresetData>> listPreset(WebSocketConnection connection, Object o) {
|
||||
return catPresetRepository.list("competition.uuid", connection.pathParam("uuid"))
|
||||
.map(presets -> presets.stream().map(PresetData::fromModel).toList());
|
||||
}
|
||||
|
||||
@WSReceiver(code = "createClassementMatchs", permission = PermLevel.TABLE)
|
||||
public Uni<Void> createClassementMatchs(WebSocketConnection connection, Long categoryId) {
|
||||
return getById(categoryId, connection)
|
||||
.call(cat -> {
|
||||
PermLevel perm = PermLevel.valueOf(connection.userData().get(UserData.TypedKey.forString("prem")));
|
||||
if (perm == PermLevel.TABLE) {
|
||||
return matchRepository.list("category = ?1 AND category_ord = -42", cat.getId())
|
||||
.chain(l -> l.stream().anyMatch(MatchModel::isEnd) ?
|
||||
Uni.createFrom().failure(
|
||||
new DForbiddenException(trad.t("err.match.termine", connection))) :
|
||||
Uni.createFrom().voidItem());
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
})
|
||||
.call(cat -> treeRepository.list("category = ?1 AND level <= -10", cat.getId())
|
||||
.map(l -> l.stream().map(o -> o.getMatch().getId()).toList())
|
||||
.call(__ -> treeRepository.delete("category = ?1 AND level <= -10", cat.getId()))
|
||||
.call(ids -> matchRepository.delete("id IN ?1", ids)))
|
||||
.call(cat -> Mutiny.fetch(cat.getTree()))
|
||||
.call(cat -> {
|
||||
List<MatchModel> toSave = new ArrayList<>();
|
||||
if (!cat.getTree().isEmpty()) {
|
||||
for (TreeModel treeModel : cat.getTree())
|
||||
cleanTree(treeModel, toSave);
|
||||
}
|
||||
return Panache.withTransaction(() -> matchRepository.persist(toSave))
|
||||
.invoke(__ -> SSMatch.sendMatch(connection,
|
||||
toSave.stream().map(MatchEntity::fromModel).toList()));
|
||||
})
|
||||
.chain(cat -> Mutiny.fetch(cat.getMatchs())
|
||||
.chain(list -> cardRepository.list("competition = ?1", cat.getCompet())
|
||||
.map(c -> list.stream().map(m -> new MatchModelExtend(m, c)).toList()))
|
||||
.map(matchModels -> {
|
||||
ResultCategoryData out = new ResultCategoryData();
|
||||
resultService.getArray2(matchModels, null, out);
|
||||
out.getRankArray().remove('-');
|
||||
return out;
|
||||
})
|
||||
.invoke(Unchecked.consumer(result -> {
|
||||
if (result.getRankArray().size() != 2) {
|
||||
throw new DForbiddenException(trad.t("configuration.non.supportee", connection));
|
||||
}
|
||||
}))
|
||||
.chain(result -> {
|
||||
List<MatchModel> toSave = new ArrayList<>();
|
||||
List<MatchModel> toCreate = new ArrayList<>();
|
||||
List<TreeModel> toSaveTree = new ArrayList<>();
|
||||
|
||||
int treeSize = 0;
|
||||
List<TreeModel> lastNode = new ArrayList<>();
|
||||
Optional<TreeModel> tree = cat.getTree().stream().filter(t -> t.getLevel() > 0)
|
||||
.min(Comparator.comparing(TreeModel::getLevel));
|
||||
if (tree.isPresent()) {
|
||||
tree.get().getChildrenAtDepth(tree.get().death() - 1, 0, lastNode);
|
||||
treeSize = lastNode.size();
|
||||
}
|
||||
|
||||
Iterator<List<ResultCategoryData.RankArray>> iterator = result.getRankArray().values()
|
||||
.iterator();
|
||||
List<ResultCategoryData.RankArray> poule1 = iterator.next();
|
||||
List<ResultCategoryData.RankArray> poule2 = iterator.next();
|
||||
|
||||
int maxToMixFill = Math.min(poule1.size(), poule2.size());
|
||||
int maxToMixTreeFill = Math.min(treeSize, maxToMixFill);
|
||||
for (int i = 0; i < maxToMixTreeFill; i++) {
|
||||
CombModel comb1 = poule1.get(i).getComb();
|
||||
CombModel comb2 = poule2.get(treeSize - i - 1).getComb();
|
||||
|
||||
fillMatchComb(lastNode.get(i).getMatch(), comb1, comb2);
|
||||
toSave.add(lastNode.get(i).getMatch());
|
||||
}
|
||||
|
||||
if (cat.isFullClassement()) {
|
||||
for (int i = maxToMixTreeFill; i < maxToMixFill; i++) {
|
||||
MatchModel match = new MatchModel();
|
||||
match.setCategory(cat);
|
||||
match.setCategory_ord(-42);
|
||||
match.setEnd(false);
|
||||
|
||||
CombModel comb1 = poule1.get(i).getComb();
|
||||
CombModel comb2 = poule2.get(i).getComb();
|
||||
|
||||
fillMatchComb(match, comb1, comb2);
|
||||
toCreate.add(match);
|
||||
|
||||
toSaveTree.add(new TreeModel(cat.getId(), -10 - i, match));
|
||||
}
|
||||
}
|
||||
|
||||
return Panache.withTransaction(() -> matchRepository.persist(toSave)
|
||||
.call(__ -> matchRepository.create(toCreate)))
|
||||
.call(__ -> toSaveTree.isEmpty() ? Uni.createFrom().voidItem()
|
||||
: Panache.withTransaction(() -> treeRepository.persist(toSaveTree)))
|
||||
.map(__ -> Stream.concat(toSave.stream(), toCreate.stream())
|
||||
.map(MatchEntity::fromModel).toList());
|
||||
})
|
||||
)
|
||||
.onFailure().invoke(t -> System.out.println("error: " + t.getMessage()))
|
||||
.invoke(matchEntities -> SSMatch.sendMatch(connection, matchEntities))
|
||||
.call(__ -> treeRepository.list("category = ?1 AND level != 0", categoryId)
|
||||
.map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
|
||||
.invoke(trees -> SSCategorie.sendTreeCategory(connection, trees)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
private void cleanTree(TreeModel treeModel, List<MatchModel> toSave) {
|
||||
MatchModel model = treeModel.getMatch();
|
||||
if (model != null) {
|
||||
model.setC1_id(null);
|
||||
model.setC1_guest(null);
|
||||
model.setC2_id(null);
|
||||
model.setC2_guest(null);
|
||||
model.setEnd(false);
|
||||
model.setDate(null);
|
||||
model.getScores().clear();
|
||||
|
||||
toSave.add(model);
|
||||
}
|
||||
if (treeModel.getLeft() != null)
|
||||
cleanTree(treeModel.getLeft(), toSave);
|
||||
if (treeModel.getRight() != null)
|
||||
cleanTree(treeModel.getRight(), toSave);
|
||||
}
|
||||
|
||||
private void fillMatchComb(MatchModel match, CombModel comb1, CombModel comb2) {
|
||||
if (comb1 instanceof MembreModel m)
|
||||
match.setC1_id(m);
|
||||
else if (comb1 instanceof CompetitionGuestModel g)
|
||||
match.setC1_guest(g);
|
||||
|
||||
if (comb2 instanceof MembreModel m)
|
||||
match.setC2_id(m);
|
||||
else if (comb2 instanceof CompetitionGuestModel g)
|
||||
match.setC2_guest(g);
|
||||
}
|
||||
|
||||
|
||||
@RegisterForReflection
|
||||
public record JustCategorie(long id, String name, int type, String liceName) {
|
||||
public record JustCategorie(long id, String name, int type, String liceName, boolean treeAreClassement,
|
||||
boolean fullClassement, PresetData preset) {
|
||||
public static JustCategorie from(CategoryModel m) {
|
||||
return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName());
|
||||
return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName(), m.isTreeAreClassement(),
|
||||
m.isFullClassement(), PresetData.fromModel(m.getPreset()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +450,11 @@ public class RCategorie {
|
||||
String name;
|
||||
int type;
|
||||
String liceName;
|
||||
boolean treeAreClassement = false;
|
||||
boolean fullClassement = false;
|
||||
PresetData preset;
|
||||
List<TreeEntity> trees = null;
|
||||
List<MatchEntity> matches;
|
||||
List<CardModel> cards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,18 +46,18 @@ public class RMatch {
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@Inject
|
||||
CardboardRepository cardboardRepository;
|
||||
TradService trad;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
RState rState;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver"));
|
||||
throw new DNotFoundException(trad.t("matche.non.trouver", connection));
|
||||
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
throw new DForbiddenException(trad.t("permission.denied", connection));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ public class RMatch {
|
||||
return categoryRepository.findById(m.categorie)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver", connection));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
throw new DForbiddenException(trad.t("permission.denied", connection));
|
||||
}))
|
||||
.chain(categoryModel -> creatMatch(categoryModel, m))
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
|
||||
@@ -195,6 +195,7 @@ public class RMatch {
|
||||
return Panache.withTransaction(() -> matchRepository.persist(mm));
|
||||
})
|
||||
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
|
||||
.invoke(mm -> rState.setMatchEnd(connection, matchEnd))
|
||||
.chain(mm -> updateEndAndTree(mm, toSend))
|
||||
.invoke(__ -> SSMatch.sendMatch(connection, toSend))
|
||||
.replaceWithVoid();
|
||||
@@ -285,9 +286,7 @@ public class RMatch {
|
||||
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
|
||||
return getById(idMatch, connection)
|
||||
.map(__ -> idMatch)
|
||||
.chain(l -> Panache.withTransaction(() ->
|
||||
cardboardRepository.delete("match.id = ?1", l)
|
||||
.chain(__ -> matchRepository.delete("id = ?1", l))))
|
||||
.chain(l -> Panache.withTransaction(() -> matchRepository.delete("id = ?1", l)))
|
||||
.invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
@@ -298,9 +297,9 @@ public class RMatch {
|
||||
return categoryRepository.findById(data.categorie)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver"));
|
||||
throw new DNotFoundException(trad.t("categorie.non.trouver", connection));
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException(trad.t("permission.denied"));
|
||||
throw new DForbiddenException(trad.t("permission.denied", connection));
|
||||
}))
|
||||
.call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
|
||||
(Panache.withTransaction(
|
||||
|
||||
112
src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java
Normal file
112
src/main/java/fr/titionfire/ffsaf/ws/recv/RPDF.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardModel;
|
||||
import fr.titionfire.ffsaf.data.model.CategoryModel;
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CardRepository;
|
||||
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.domain.entity.MatchModelExtend;
|
||||
import fr.titionfire.ffsaf.domain.service.ResultService;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.rest.data.ResultCategoryData;
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Multi;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RPDF {
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
CardRepository cardRepository;
|
||||
|
||||
@Inject
|
||||
CategoryRepository categoryRepository;
|
||||
|
||||
@Inject
|
||||
ResultService resultService;
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
@Transactional
|
||||
@WSReceiver(code = "getPodium", permission = PermLevel.VIEW)
|
||||
public Uni<List<PodiumEntity>> getPodium(WebSocketConnection connection, Object o) {
|
||||
List<CardModel> cards = new java.util.ArrayList<>();
|
||||
|
||||
return cardRepository.list("competition.uuid = ?1", connection.pathParam("uuid"))
|
||||
.invoke(cards::addAll)
|
||||
.chain(__ -> matchRepository.list("category.compet.uuid = ?1", connection.pathParam("uuid")))
|
||||
.chain(matchs -> {
|
||||
HashMap<CategoryModel, List<MatchModel>> map = new HashMap<>();
|
||||
for (MatchModel match : matchs) {
|
||||
if (!map.containsKey(match.getCategory()))
|
||||
map.put(match.getCategory(), new java.util.ArrayList<>());
|
||||
map.get(match.getCategory()).add(match);
|
||||
}
|
||||
|
||||
return Multi.createFrom().iterable(map.entrySet())
|
||||
.onItem().call(entry -> Mutiny.fetch(entry.getKey().getTree()))
|
||||
.map(entry -> {
|
||||
ResultCategoryData tmp = new ResultCategoryData();
|
||||
|
||||
double cmoy = entry.getValue().stream().flatMap(m -> Stream.of(m.getC1(), m.getC2()))
|
||||
.filter(c -> c != null && c.getCategorie() != null)
|
||||
.mapToInt(c -> c.getCategorie().ordinal())
|
||||
.average().orElse(0);
|
||||
Categorie categorie_moy = Categorie.values()[(int) Math.ceil(cmoy)];
|
||||
|
||||
resultService.getArray2(
|
||||
entry.getValue().stream().map(m -> new MatchModelExtend(m, cards)).toList(),
|
||||
null, tmp);
|
||||
resultService.getClassementArray(entry.getKey(), null, cards, tmp);
|
||||
|
||||
String source = "";
|
||||
if ((entry.getKey().getType() & 2) != 0) {
|
||||
if (entry.getKey().isTreeAreClassement())
|
||||
source = trad.t("podium.source.classement", connection);
|
||||
else
|
||||
source = trad.t("podium.source.tree", connection);
|
||||
} else if ((entry.getKey().getType() & 1) != 0)
|
||||
source = trad.t("podium.source.poule", connection);
|
||||
|
||||
|
||||
return new PodiumEntity(entry.getKey().getName(), source, categorie_moy,
|
||||
tmp.getClassement());
|
||||
})
|
||||
.collect().asList();
|
||||
});
|
||||
}
|
||||
|
||||
@WSReceiver(code = "getPodiumClub", permission = PermLevel.VIEW)
|
||||
public Uni<List<ResultService.ClubClassement>> getPodiumClub(WebSocketConnection connection, Object o) {
|
||||
return resultService.getAllClubArray(connection.pathParam("uuid"), false);
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public static record PodiumEntity(String poule_name, String source, Categorie categorie,
|
||||
List<ResultCategoryData.ClassementData> podium) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Multi;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -24,14 +25,20 @@ public class RRegister {
|
||||
|
||||
@WSReceiver(code = "getRegister", permission = PermLevel.TABLE)
|
||||
public Uni<List<CombEntity>> getRegister(WebSocketConnection connection, Object o) {
|
||||
ArrayList<CombEntity> combEntities = new ArrayList<>();
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.call(cm -> Mutiny.fetch(cm.getInsc()))
|
||||
.call(cm -> Mutiny.fetch(cm.getGuests()))
|
||||
.map(cm -> {
|
||||
ArrayList<CombEntity> combEntities = new ArrayList<>();
|
||||
combEntities.addAll(cm.getInsc().stream().map(CombEntity::fromModel).toList());
|
||||
combEntities.addAll(cm.getGuests().stream().map(CombEntity::fromModel).toList());
|
||||
return combEntities;
|
||||
});
|
||||
.call(cm -> Mutiny.fetch(cm.getInsc())
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.call(r -> Mutiny.fetch(r.getCategoriesInscrites()))
|
||||
.map(r -> CombEntity.fromModel(r).addCategoriesInscrites(r.getCategoriesInscrites()))
|
||||
.collect().asList()
|
||||
.invoke(combEntities::addAll))
|
||||
.call(cm -> Mutiny.fetch(cm.getGuests())
|
||||
.onItem().transformToMulti(Multi.createFrom()::iterable)
|
||||
.call(r -> Mutiny.fetch(r.getCategoriesInscrites()))
|
||||
.map(r -> CombEntity.fromModel(r).addCategoriesInscrites(r.getCategoriesInscrites()))
|
||||
.collect().asList()
|
||||
.invoke(combEntities::addAll))
|
||||
.replaceWith(combEntities);
|
||||
}
|
||||
}
|
||||
|
||||
149
src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java
Normal file
149
src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSState;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.UserData;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RState {
|
||||
|
||||
private static final HashMap<WebSocketConnection, TableState> tableStates = new HashMap<>();
|
||||
|
||||
@WSReceiver(code = "subscribeToState", permission = PermLevel.VIEW)
|
||||
public Uni<List<TableState>> sendCurrentScore(WebSocketConnection connection, Boolean subscribe) {
|
||||
connection.userData().put(UserData.TypedKey.forBoolean("needState"), subscribe);
|
||||
|
||||
if (subscribe) {
|
||||
String uuid = connection.pathParam("uuid");
|
||||
return Uni.createFrom().item(() ->
|
||||
tableStates.values().stream().filter(s -> s.getCompetitionUuid().equals(uuid)).toList()
|
||||
);
|
||||
}
|
||||
return Uni.createFrom().nullItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendState", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendState(WebSocketConnection connection, TableState tableState) {
|
||||
tableState.setCompetitionUuid(connection.pathParam("uuid"));
|
||||
|
||||
if (tableStates.containsKey(connection))
|
||||
tableState.setId(tableStates.get(connection).getId());
|
||||
if (tableState.getChronoState().isRunning() && tableState.getChronoState().state == 0)
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
tableStates.put(connection, tableState);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendSelectCategory", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendSelectCategory(WebSocketConnection connection, Long catId) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setSelectedCategory(catId);
|
||||
tableState.setState(MatchState.NOT_STARTED);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendSelectMatch", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendSelectMatch(WebSocketConnection connection, Long matchId) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setSelectedMatch(matchId);
|
||||
tableState.setState(MatchState.NOT_STARTED);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCurentChrono", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurentChrono(WebSocketConnection connection, ChronoState chronoState) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setChronoState(chronoState);
|
||||
if (chronoState.isRunning())
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendLicenceName", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurrentScore(WebSocketConnection connection, String name) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setLiceName(name);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCurrentScore", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurrentScore(WebSocketConnection connection, ScoreState scoreState) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setScoreState(scoreState);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
public void removeConnection(WebSocketConnection connection) {
|
||||
if (tableStates.containsKey(connection)) {
|
||||
SSState.sendRmStateFull(connection, tableStates.get(connection).getId());
|
||||
tableStates.remove(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMatchEnd(WebSocketConnection connection, RMatch.MatchEnd matchEnd) {
|
||||
if (tableStates.containsKey(connection)) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (matchEnd.end())
|
||||
tableState.setState(MatchState.ENDED);
|
||||
else
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record ChronoState(long time, long startTime, long configTime, long configPause, int state) {
|
||||
public boolean isRunning() {
|
||||
return startTime != 0 || state != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record ScoreState(int scoreRouge, int scoreBleu) {
|
||||
}
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class TableState {
|
||||
UUID id = UUID.randomUUID();
|
||||
String competitionUuid;
|
||||
Long selectedCategory;
|
||||
Long selectedMatch;
|
||||
ChronoState chronoState;
|
||||
ScoreState scoreState;
|
||||
String liceName = "???";
|
||||
MatchState state = MatchState.NOT_STARTED;
|
||||
}
|
||||
|
||||
public enum MatchState {
|
||||
NOT_STARTED,
|
||||
IN_PROGRESS,
|
||||
ENDED
|
||||
}
|
||||
}
|
||||
137
src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java
Normal file
137
src/main/java/fr/titionfire/ffsaf/ws/recv/RTeam.java
Normal file
@@ -0,0 +1,137 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.RegisterRepository;
|
||||
import fr.titionfire.ffsaf.domain.entity.CombEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.TradService;
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.Genre;
|
||||
import fr.titionfire.ffsaf.utils.Pair;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSRegister;
|
||||
import io.quarkus.hibernate.reactive.panache.Panache;
|
||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RTeam {
|
||||
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
@Inject
|
||||
RegisterRepository registerRepository;
|
||||
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
@WSReceiver(code = "setTeam", permission = PermLevel.ADMIN)
|
||||
public Uni<CombEntity> setTeam(WebSocketConnection connection, TeamData data) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.chain(cm -> registerRepository.list("membre.id IN ?1 AND competition = ?2",
|
||||
data.members.stream().filter(id -> id >= 0).toList(), cm)
|
||||
.chain(l -> competitionGuestRepository.list("id IN ?1",
|
||||
data.members.stream().filter(id -> id < 0).map(i -> i * -1).toList())
|
||||
.map(l2 -> new Pair<>(l, l2)))
|
||||
.chain(pair ->
|
||||
competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition = ?3",
|
||||
data.name, "__team", cm).firstResult()
|
||||
.chain(team -> {
|
||||
if (pair.getKey().isEmpty() && pair.getValue().isEmpty()) {
|
||||
if (team != null) {
|
||||
CompetitionGuestModel finalTeam1 = team;
|
||||
SSRegister.sendRegisterRemove(connection, finalTeam1.getId() * -1);
|
||||
return Panache.withTransaction(
|
||||
() -> competitionGuestRepository.delete(finalTeam1))
|
||||
.replaceWith((CombEntity) null);
|
||||
} else
|
||||
return Uni.createFrom().item((CombEntity) null);
|
||||
}
|
||||
|
||||
if (team == null) {
|
||||
// Create new team
|
||||
team = new CompetitionGuestModel();
|
||||
team.setFname(data.name);
|
||||
team.setLname("__team");
|
||||
team.setCompetition(cm);
|
||||
team.setClub("Team");
|
||||
team.setGenre(Genre.NA);
|
||||
} else {
|
||||
team.getComb().clear();
|
||||
team.getGuest().clear();
|
||||
}
|
||||
|
||||
team.setCategorie(Stream.concat(
|
||||
pair.getKey().stream().map(RegisterModel::getCategorie2),
|
||||
pair.getValue().stream().map(CompetitionGuestModel::getCategorie))
|
||||
.map(Enum::ordinal)
|
||||
.max(Integer::compareTo)
|
||||
.map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1));
|
||||
|
||||
List<Float> s = Stream.concat(
|
||||
pair.getKey().stream().map(RegisterModel::getWeight),
|
||||
pair.getValue().stream().map(CompetitionGuestModel::getWeight))
|
||||
.filter(Objects::nonNull).toList();
|
||||
if (s.isEmpty()) {
|
||||
team.setWeight(null);
|
||||
} else if (s.size() == 1) {
|
||||
team.setWeight(s.get(0));
|
||||
} else {
|
||||
team.setWeight((float) s.stream().mapToDouble(Float::doubleValue)
|
||||
.average()
|
||||
.orElse(0));
|
||||
}
|
||||
|
||||
team.setCountry(Stream.concat(
|
||||
pair.getKey().stream().map(m -> m.getMembre().getCountry()),
|
||||
pair.getValue().stream().map(CompetitionGuestModel::getCountry))
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::toUpperCase)
|
||||
.collect(Collectors.groupingBy(
|
||||
e -> e, // Classer par élément
|
||||
Collectors.counting() // Compter les occurrences
|
||||
))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse("FR"));
|
||||
|
||||
team.getComb().addAll(
|
||||
pair.getKey().stream().map(RegisterModel::getMembre).toList());
|
||||
team.getGuest().addAll(pair.getValue());
|
||||
|
||||
CompetitionGuestModel finalTeam = team;
|
||||
return Panache.withTransaction(
|
||||
() -> competitionGuestRepository.persistAndFlush(finalTeam))
|
||||
.map(CombEntity::fromModel);
|
||||
}))
|
||||
)
|
||||
.invoke(combEntity -> {
|
||||
if (combEntity != null)
|
||||
SSRegister.sendRegister(connection, combEntity);
|
||||
});
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record TeamData(List<Long> members, String name) {
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.ws.send;
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
import fr.titionfire.ffsaf.domain.entity.CombEntity;
|
||||
import fr.titionfire.ffsaf.domain.service.CardService;
|
||||
import fr.titionfire.ffsaf.net2.MessageType;
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import fr.titionfire.ffsaf.ws.MessageOut;
|
||||
@@ -13,6 +14,7 @@ import io.quarkus.websockets.next.UserData;
|
||||
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.UUID;
|
||||
@@ -25,16 +27,48 @@ public class SRegister {
|
||||
@Inject
|
||||
OpenConnections connections;
|
||||
|
||||
@Inject
|
||||
CardService cardService;
|
||||
|
||||
public Uni<Void> sendRegister(String uuid, RegisterModel registerModel) {
|
||||
return send(uuid, "sendRegister", CombEntity.fromModel(registerModel));
|
||||
return Mutiny.fetch(registerModel.getCategoriesInscrites()).chain(o ->
|
||||
send(uuid, "sendRegister", CombEntity.fromModel(registerModel).addCategoriesInscrites(o))
|
||||
.call(__ -> registerModel.getClub2() == null ? Uni.createFrom().voidItem() :
|
||||
cardService.addTeamCartToNewComb(registerModel.getMembre().getId(),
|
||||
registerModel.getClub2().getClubId(), registerModel.getClub2().getName(),
|
||||
registerModel.getCompetition())
|
||||
.chain(cardModels -> send(uuid, "sendCards", cardModels))));
|
||||
}
|
||||
|
||||
public Uni<Void> sendRegisterNoFetch(String uuid, RegisterModel registerModel) {
|
||||
return send(uuid, "sendRegister",
|
||||
CombEntity.fromModel(registerModel).addCategoriesInscrites(registerModel.getCategoriesInscrites()))
|
||||
.call(__ -> registerModel.getClub2() == null ? Uni.createFrom().voidItem() :
|
||||
cardService.addTeamCartToNewComb(registerModel.getMembre().getId(),
|
||||
registerModel.getClub2().getClubId(), registerModel.getClub2().getName(),
|
||||
registerModel.getCompetition())
|
||||
.chain(cardModels -> send(uuid, "sendCards", cardModels)));
|
||||
}
|
||||
|
||||
public Uni<Void> sendRegister(String uuid, CompetitionGuestModel model) {
|
||||
return send(uuid, "sendRegister", CombEntity.fromModel(model));
|
||||
return Mutiny.fetch(model.getCategoriesInscrites()).chain(o ->
|
||||
send(uuid, "sendRegister", CombEntity.fromModel(model).addCategoriesInscrites(o))
|
||||
.call(__ -> cardService.addTeamCartToNewComb(model.getId() * -1,
|
||||
null, model.getClub(), model.getCompetition())
|
||||
.chain(cardModels -> send(uuid, "sendCards", cardModels))));
|
||||
}
|
||||
|
||||
public Uni<Void> sendRegisterNoFetch(String uuid, CompetitionGuestModel model) {
|
||||
return send(uuid, "sendRegister",
|
||||
CombEntity.fromModel(model).addCategoriesInscrites(model.getCategoriesInscrites()))
|
||||
.call(__ -> cardService.addTeamCartToNewComb(model.getId() * -1,
|
||||
null, model.getClub(), model.getCompetition())
|
||||
.chain(cardModels -> send(uuid, "sendCards", cardModels)));
|
||||
}
|
||||
|
||||
public Uni<Void> sendRegisterRemove(String uuid, Long combId) {
|
||||
return send(uuid, "sendRegisterRemove", combId);
|
||||
return send(uuid, "sendRegisterRemove", combId)
|
||||
.call(__ -> cardService.rmTeamCardFromComb(combId, uuid));
|
||||
}
|
||||
|
||||
public Uni<Void> send(String uuid, String code, Object data) {
|
||||
|
||||
27
src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java
Normal file
27
src/main/java/fr/titionfire/ffsaf/ws/send/SSCard.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package fr.titionfire.ffsaf.ws.send;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardModel;
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import fr.titionfire.ffsaf.ws.recv.RCard;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SSCard {
|
||||
|
||||
public static void sendCards(WebSocketConnection connection, List<CardModel> cardModel) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCards", cardModel);
|
||||
}
|
||||
|
||||
public static void sendRmCards(WebSocketConnection connection, List<Long> ids) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "rmCards", ids);
|
||||
}
|
||||
|
||||
public static void sendTeamCard(WebSocketConnection connection, RCard.SendTeamCards teamCards) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "sendTeamCard", teamCards);
|
||||
}
|
||||
|
||||
public static void rmTeamCard(WebSocketConnection connection, RCard.SendTeamCards teamCards) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "rmTeamCard", teamCards);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
|
||||
public class SSCardboard {
|
||||
|
||||
public static void sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity);
|
||||
}
|
||||
}
|
||||
16
src/main/java/fr/titionfire/ffsaf/ws/send/SSRegister.java
Normal file
16
src/main/java/fr/titionfire/ffsaf/ws/send/SSRegister.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package fr.titionfire.ffsaf.ws.send;
|
||||
|
||||
import fr.titionfire.ffsaf.domain.entity.CombEntity;
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
|
||||
public class SSRegister {
|
||||
|
||||
public static void sendRegister(WebSocketConnection connection, CombEntity combEntity) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegister", combEntity);
|
||||
}
|
||||
|
||||
public static void sendRegisterRemove(WebSocketConnection connection, Long combId) {
|
||||
CompetitionWS.sendNotifyToOtherEditor(connection, "sendRegisterRemove", combId);
|
||||
}
|
||||
}
|
||||
19
src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java
Normal file
19
src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package fr.titionfire.ffsaf.ws.send;
|
||||
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import fr.titionfire.ffsaf.ws.recv.RState;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SSState {
|
||||
|
||||
public static void sendStateFull(WebSocketConnection connection, RState.TableState state) {
|
||||
CompetitionWS.sendNotifyState(connection, "sendStateFull", state);
|
||||
}
|
||||
|
||||
public static void sendRmStateFull(WebSocketConnection connection, UUID id) {
|
||||
CompetitionWS.sendNotifyState(connection, "rmStateFull", id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -68,7 +68,7 @@ quarkus.http.auth.permission.public.policy=permit
|
||||
quarkus.keycloak.admin-client.server-url=https://auth.safca.fr
|
||||
|
||||
|
||||
quarkus.native.resources.includes=asset/**
|
||||
quarkus.native.resources.includes=asset/**,lang/**
|
||||
|
||||
# HelloAsso Connector
|
||||
helloasso.api=https://api.helloasso.com
|
||||
|
||||
@@ -85,3 +85,10 @@ licence.membre.n.1.inconnue=License member no. 1 unknown
|
||||
licence.membre.n.2.inconnue=License member no. 2 unknown
|
||||
licence.membre.n.3.inconnue=License member no. 3 unknown
|
||||
demande.d.affiliation.non.trouve=Affiliation request not found
|
||||
carton.non.trouver=Card not found
|
||||
card.cannot.be.added=Unable to add the card
|
||||
configuration.non.supportee=Unsupported configuration
|
||||
err.match.termine=Error, a placement match has already been played
|
||||
podium.source.classement=Ranking
|
||||
podium.source.tree=Tournaments
|
||||
podium.source.poule=Pool
|
||||
|
||||
@@ -44,8 +44,8 @@ service.momentanement.indisponible=Service momentan
|
||||
asso.introuvable=Association introuvable
|
||||
erreur.lors.calcul.du.trie=Erreur lors du calcul du tri
|
||||
page.out.of.range=Page out of range
|
||||
le.membre.appartient.pas.a.votre.club=Le membre n°%d n?appartient pas à votre club
|
||||
email.deja.utilise.par=L?adresse e-mail '%s' est déjà utilisée par %s %s
|
||||
le.membre.appartient.pas.a.votre.club=Le membre n°%d n'appartient pas à votre club
|
||||
email.deja.utilise.par=L'adresse e-mail '%s' est déjà utilisée par %s %s
|
||||
try.edit.licence=Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (Tentative de modification non autorisée du nom sur la licence %d pour %s %s)
|
||||
email.deja.utilise=Adresse e-mail déjà utilisée
|
||||
regiter.new.membre=Pour enregistrer un nouveau membre, veuillez utiliser le bouton prévu à cet effet.
|
||||
@@ -61,23 +61,30 @@ licence.rm.err1=Impossible de supprimer une licence pour laquelle un paiement es
|
||||
licence.deja.demandee=Licence déjà demandée
|
||||
impossible.de.supprimer.une.licence.deja.validee=Impossible de supprimer une licence déjà validée
|
||||
impossible.de.supprimer.une.licence.deja.payee=Impossible de supprimer une licence déjà payée
|
||||
vous.ne.pouvez.pas.creer.de.competition=Vous n?êtes pas autorisé à créer une compétition
|
||||
vous.ne.pouvez.pas.creer.de.competition=Vous n'êtes pas autorisé à créer une compétition
|
||||
user.not.found=Utilisateur %s introuvable
|
||||
inscription.fermee=Inscription fermée
|
||||
insc.err1=Vous n?êtes pas autorisé à inscrire ce membre (décision de l?administrateur de la compétition)
|
||||
insc.err2=Vous n?êtes pas autorisé à vous inscrire (décision de l?administrateur de la compétition)
|
||||
insc.err3=Modification bloquée par l?administrateur de la compétition
|
||||
insc.err1=Vous n'êtes pas autorisé à inscrire ce membre (décision de l'administrateur de la compétition)
|
||||
insc.err2=Vous n'êtes pas autorisé à vous inscrire (décision de l'administrateur de la compétition)
|
||||
insc.err3=Modification bloquée par l'administrateur de la compétition
|
||||
licence.non.trouve=Licence %s introuvable
|
||||
nom.et.prenom.requis=Nom et prénom obligatoires
|
||||
combattant.non.trouve=Combattant %s %s introuvable
|
||||
le.membre.n.existe.pas=Le membre n°%d n?existe pas
|
||||
le.membre.n.existe.pas=Le membre n°%d n'existe pas
|
||||
competition.is.not.internal=Competition is not INTERNAL
|
||||
erreur.de.format.des.contacts=Format des contacts invalide
|
||||
competition.not.found=Compétition introuvable
|
||||
saison.non.valid=Saison invalide
|
||||
demande.d.affiliation.deja.existante=Une demande d?affiliation existe déjà
|
||||
demande.d.affiliation.deja.existante=Une demande d'affiliation existe déjà
|
||||
affiliation.deja.existante=Affiliation déjà existante
|
||||
licence.membre.n.1.inconnue=Licence du membre n°1 inconnue
|
||||
licence.membre.n.2.inconnue=Licence du membre n°2 inconnue
|
||||
licence.membre.n.3.inconnue=Licence du membre n°3 inconnue
|
||||
demande.d.affiliation.non.trouve=Demande d?affiliation introuvable
|
||||
demande.d.affiliation.non.trouve=Demande d'affiliation introuvable
|
||||
carton.non.trouver=Carton introuvable
|
||||
card.cannot.be.added=Impossible d'ajouter le carton
|
||||
configuration.non.supportee=Configuration non supportée
|
||||
err.match.termine=Erreur, un match de classement a déjà été joué
|
||||
podium.source.classement=Classement
|
||||
podium.source.tree=Tournois
|
||||
podium.source.poule=Poule
|
||||
5183
src/main/webapp/package-lock.json
generated
5183
src/main/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,40 +13,42 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.3.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.3.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||
"@fortawesome/react-fontawesome": "^3.5.0",
|
||||
"axios": "1.18.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^4.0.1",
|
||||
"jspdf": "4.2.1",
|
||||
"jspdf-autotable": "^5.0.8",
|
||||
"jszip": "^3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"obs-websocket-js": "^5.0.7",
|
||||
"proj4": "^2.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.5.2",
|
||||
"react-is": "^19.0.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"react-router-dom": "^6.21.2",
|
||||
"react-toastify": "^10.0.4",
|
||||
"recharts": "^2.15.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"obs-websocket-js": "^5.0.8",
|
||||
"proj4": "^2.21.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-i18next": "^17.0.11",
|
||||
"react-is": "^19.2.8",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-loader-spinner": "^8.0.2",
|
||||
"react-router-dom": "^7.18.2",
|
||||
"react-toastify": "^11.1.0",
|
||||
"recharts": "^3.10.1",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"xlsx-js-style": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.0.8"
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"vite": "8.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ const rootDiv = document.getElementById("safca_api_data");
|
||||
const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
|
||||
style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png"
|
||||
alt="">`
|
||||
const cupImg2 = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
|
||||
style="width: 16px;" src="https://intra.ffsaf.fr/img/171892.png"
|
||||
alt="">`
|
||||
|
||||
|
||||
const voidFunction = () => {
|
||||
@@ -15,7 +18,7 @@ let lastRf = 0;
|
||||
let rfFonction = voidFunction;
|
||||
|
||||
setInterval(() => {
|
||||
rfFonction();
|
||||
// rfFonction();
|
||||
}, 15000);
|
||||
|
||||
function setSubPage(name) {
|
||||
@@ -36,6 +39,9 @@ function setSubPage(name) {
|
||||
case 'club':
|
||||
clubPage(location);
|
||||
break;
|
||||
case 'clubRank':
|
||||
clubRankPage();
|
||||
break;
|
||||
case 'all':
|
||||
combsPage();
|
||||
break;
|
||||
@@ -51,6 +57,7 @@ function homePage() {
|
||||
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li>
|
||||
<li><a id="combLink" href="javascript:void(0);">${i18next.t('parCombattant')}</a></li>
|
||||
<li><a id="clubLink" href="javascript:void(0);">${i18next.t('parClub')}</a></li>
|
||||
<li><a id="clubClassement" href="javascript:void(0);">${i18next.t('classementClub')}</a></li>
|
||||
<li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
|
||||
</ul>
|
||||
`
|
||||
@@ -59,6 +66,7 @@ function homePage() {
|
||||
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
|
||||
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
|
||||
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
|
||||
document.getElementById('clubClassement').addEventListener('click', () => setSubPage('clubRank'));
|
||||
document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
|
||||
}
|
||||
|
||||
@@ -87,23 +95,27 @@ function stopLoading(loading) {
|
||||
loading['root'].removeChild(loading['element']);
|
||||
}
|
||||
|
||||
function scoreToString(score) {
|
||||
const scorePrint = (s1) => {
|
||||
switch (s1) {
|
||||
case -997:
|
||||
return i18next.t('disc.');
|
||||
case -998:
|
||||
return i18next.t('abs.');
|
||||
case -999:
|
||||
return i18next.t('for.');
|
||||
case -1000:
|
||||
return "";
|
||||
default:
|
||||
return String(s1);
|
||||
}
|
||||
function scorePrint(s1) {
|
||||
switch (s1) {
|
||||
case -997:
|
||||
return i18next.t('disc.');
|
||||
case -998:
|
||||
return i18next.t('abs.');
|
||||
case -999:
|
||||
return i18next.t('for.');
|
||||
case -1000:
|
||||
return "";
|
||||
default:
|
||||
return String(s1);
|
||||
}
|
||||
}
|
||||
|
||||
return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ");
|
||||
function scoreToString(score) {
|
||||
if (score.length === 0)
|
||||
return "";
|
||||
if (score.at(0) instanceof Array)
|
||||
return score.map(s => scorePrint(s.at(0)) + "-" + scorePrint(s.at(1))).join(" | ");
|
||||
return score.map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ");
|
||||
}
|
||||
|
||||
function dateToString(date) {
|
||||
@@ -127,7 +139,7 @@ function dateToString(date) {
|
||||
return date_.toLocaleDateString();
|
||||
}
|
||||
|
||||
function buildPouleMenu(isPoule, change_view) {
|
||||
function buildPouleMenu(isPoule, change_view, isClassement = false) {
|
||||
const menuDiv = document.createElement('div');
|
||||
menuDiv.id = 'menu';
|
||||
menuDiv.style.borderBottom = '1px solid #9EA0A1';
|
||||
@@ -186,7 +198,7 @@ function buildPouleMenu(isPoule, change_view) {
|
||||
change_view(true);
|
||||
});
|
||||
ul.appendChild(li1);
|
||||
const li2 = createTab(i18next.t('tournois'), !isPoule, function () {
|
||||
const li2 = createTab(isClassement ? i18next.t('classement') : i18next.t('tournois'), !isPoule, function () {
|
||||
change_view(false);
|
||||
});
|
||||
ul.appendChild(li2);
|
||||
@@ -214,11 +226,11 @@ function buildMatchArray(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">${match.red_w ? cupImg : (match.eq ? cupImg2 : "")}</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-center" data-align="center">${match.blue_w ? cupImg : (match.eq ? cupImg2 : "")}</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>
|
||||
<td class="has-text-align-center" data-align="center">${dateToString((match.end) ? match.date : null)}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
@@ -234,6 +246,7 @@ function buildRankArray(rankArray) {
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('place')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('score')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('victoire')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
|
||||
@@ -245,6 +258,7 @@ function buildRankArray(rankArray) {
|
||||
<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.score}</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>
|
||||
@@ -258,7 +272,36 @@ function buildRankArray(rankArray) {
|
||||
}
|
||||
|
||||
function buildTree(treeData) {
|
||||
return drawGraph(initTree(treeData))
|
||||
return drawGraph(initTree(treeData.filter(d => d.data.level >= 0)))
|
||||
}
|
||||
|
||||
function buildClassementArray(classement) {
|
||||
const classement2 = classement.sort((a, b) => {
|
||||
if (a.rank === b.rank)
|
||||
return a.name.localeCompare(b.name);
|
||||
return a.rank - b.rank;
|
||||
})
|
||||
|
||||
const arrayDiv = document.createElement('div');
|
||||
let arrayContent = `<figure class="wp-block-table is-style-stripes" style="font-size: 16px; margin-top: 2em">
|
||||
<table style="width: 600px;overflow: auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('place')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const row of classement2) {
|
||||
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>
|
||||
</tr>`
|
||||
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
arrayDiv.innerHTML = arrayContent;
|
||||
return arrayDiv;
|
||||
}
|
||||
|
||||
function poulePage(location) {
|
||||
@@ -306,7 +349,7 @@ function poulePage(location) {
|
||||
dataContainer.append(buildTree(poule['trees']));
|
||||
} else {
|
||||
const change_view = (isPoule) => {
|
||||
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view));
|
||||
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view, poule['treeIsClassement']));
|
||||
|
||||
if (isPoule) {
|
||||
for (const g in poule.matchs) {
|
||||
@@ -321,6 +364,22 @@ function poulePage(location) {
|
||||
}
|
||||
} else {
|
||||
dataContainer.append(buildTree(poule['trees']));
|
||||
if (poule['treeIsClassement'] && poule['trees'].some(d => d.data.level <= -10)) {
|
||||
dataContainer.append(buildMatchArray(
|
||||
poule['trees'].filter(d => d.data.level < 0).reverse().map(d => ({
|
||||
red: d.data.c1FullName,
|
||||
blue: d.data.c2FullName,
|
||||
score: d.data.scores,
|
||||
end: d.data.end,
|
||||
red_w: d.data.win > 0,
|
||||
blue_w: d.data.win < 0,
|
||||
eq: d.data.win === 0,
|
||||
date: d.data.date,
|
||||
}))));
|
||||
}
|
||||
if (poule['treeIsClassement']){
|
||||
dataContainer.append(buildClassementArray(poule['classement']));
|
||||
}
|
||||
}
|
||||
|
||||
location[2] = isPoule ? 1 : 2;
|
||||
@@ -388,10 +447,10 @@ function buildCombView(comb) {
|
||||
<h3>${i18next.t('statistique')} :</h3>
|
||||
<ul>
|
||||
<li>${i18next.t('tauxDeVictoire2', {
|
||||
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0),
|
||||
victoires: comb.totalWin,
|
||||
matchs: comb.matchs.length
|
||||
})}
|
||||
nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.filter(m => m.end).length * 100).toFixed(0),
|
||||
victoires: comb.totalWin,
|
||||
matchs: comb.matchs.filter(m => m.end).length
|
||||
})}
|
||||
</li>
|
||||
<li>${i18next.t('pointsMarqués2', {nb: comb.pointMake})}</li>
|
||||
<li>${i18next.t('pointsReçus2', {nb: comb.pointTake})}</li>
|
||||
@@ -418,8 +477,8 @@ function buildCombView(comb) {
|
||||
<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>
|
||||
<td class="has-text-align-center" data-align="center">${match.end ? match.ratio.toFixed(3) : ""}</td>
|
||||
<td class="has-text-align-center" data-align="center">${match.win ? cupImg : (match.eq ? cupImg2 : "")}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
@@ -586,6 +645,60 @@ function clubPage(location) {
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
function buildClubsView(clubs) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
<h3>${i18next.t('classementDesClub')} :</h3>
|
||||
|
||||
<figure class="wp-block-table is-style-stripes" style="font-size: 16px">
|
||||
<table style="width: 1200px;overflow: auto"><thead>
|
||||
<tr>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('club')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('1er')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('2eme')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('3eme')}</th>
|
||||
<th class="has-text-align-center" data-align="center">${i18next.t('scores')}</th>
|
||||
</tr>
|
||||
</thead><tbody>`
|
||||
for (const club of clubs) {
|
||||
arrayContent += `
|
||||
<tr>
|
||||
<td class="has-text-align-center" data-align="center">${club.name}</td>
|
||||
<td class="has-text-align-center" data-align="center">${club.score[0]}</td>
|
||||
<td class="has-text-align-center" data-align="center">${club.score[1]}</td>
|
||||
<td class="has-text-align-center" data-align="center">${club.score[2]}</td>
|
||||
<td class="has-text-align-center" data-align="center">${club.tt_score}</td>
|
||||
</tr>`
|
||||
}
|
||||
arrayContent += `</tbody></table></figure>`
|
||||
pouleDiv.innerHTML = arrayContent;
|
||||
return pouleDiv;
|
||||
}
|
||||
|
||||
function clubRankPage() {
|
||||
rootDiv.innerHTML = `<h4>${i18next.t('résultatDeLaCompétition')} :</h4><a id="homeLink" href="javascript:void(0);">${i18next.t('back')}</a>`;
|
||||
document.getElementById('homeLink').addEventListener('click', () => setSubPage('home'));
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.marginTop = '1em';
|
||||
|
||||
const dataContainer = document.createElement('div');
|
||||
dataContainer.id = 'data-container';
|
||||
|
||||
const loading = startLoading(content);
|
||||
fetch(`${apiUrlRoot}/club/classement`)
|
||||
.then(response => response.json())
|
||||
.then(clubs => {
|
||||
console.log(clubs);
|
||||
dataContainer.replaceChildren(buildClubsView(clubs));
|
||||
})
|
||||
.catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaListe'))))
|
||||
.finally(() => stopLoading(loading));
|
||||
|
||||
content.append(dataContainer);
|
||||
rootDiv.append(content)
|
||||
}
|
||||
|
||||
function buildCombsView(combs) {
|
||||
const pouleDiv = document.createElement('div');
|
||||
let arrayContent = `
|
||||
@@ -671,12 +784,14 @@ export async function initCompetitionApi(apiUrlRoot_, host) {
|
||||
.use(i18nextHttpBackend)
|
||||
.use(i18nextBrowserLanguagedetector)
|
||||
.init({
|
||||
supportedLngs: ['fr', 'en'],
|
||||
fallbackLng: 'fr',
|
||||
debug: true,
|
||||
debug: host.startsWith('http://localhost'),
|
||||
interpolation: {
|
||||
escapeValue: true,
|
||||
},
|
||||
detection: options,
|
||||
backend: backend,
|
||||
ns: ['result'],
|
||||
defaultNS: 'result',
|
||||
})
|
||||
@@ -860,7 +975,7 @@ function drawGraph(root = []) {
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scores[i].s1 + "-" + scores[i].s2;
|
||||
const score = scorePrint(scores[i].s1) + "-" + scorePrint(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;
|
||||
@@ -938,20 +1053,6 @@ function drawGraph(root = []) {
|
||||
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
|
||||
@@ -964,7 +1065,7 @@ function drawGraph(root = []) {
|
||||
for (const node of root) {
|
||||
let win_name = "";
|
||||
if (node.data.end) {
|
||||
if (win(node.data.scores) > 0)
|
||||
if (node.data.win > 0)
|
||||
win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName;
|
||||
else
|
||||
win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName;
|
||||
|
||||
BIN
src/main/webapp/public/img/171892.png
Normal file
BIN
src/main/webapp/public/img/171892.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,16 +1,34 @@
|
||||
{
|
||||
"--SélectionnerUnCombattant--": "-- Select a fighter --",
|
||||
"--Tous--": "-- All --",
|
||||
"PourLéquipe": "for the team",
|
||||
"actuel": "Current",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Server address",
|
||||
"afficher": "Show",
|
||||
"ajoutAutomatique": "Automatic addition",
|
||||
"ajouter": "Add",
|
||||
"ajouterDesCombattants": "Add fighters",
|
||||
"ajouterUn": "Add one",
|
||||
"ajouterUneTeam": "Add team",
|
||||
"attention": "Warning",
|
||||
"aucuneConfigurationObs": "No OBS configuration found, please import one",
|
||||
"avertissement": "Warning",
|
||||
"bleu": "Blue",
|
||||
"blue": "Blue",
|
||||
"cardAdded": "Card added",
|
||||
"cardRemoved": "Card removed",
|
||||
"carton": "Card",
|
||||
"cartonDéquipe": "Team's card",
|
||||
"cartonJaune": "Yellow card",
|
||||
"cartonNoir": "Black card",
|
||||
"cartonRouge": "Red card",
|
||||
"catégorie": "Category",
|
||||
"catégorieDâgeMoyenne": "Middle-aged category",
|
||||
"catégorieSélectionnée": "Selected category",
|
||||
"catégoriesVontêtreCréées": "weight categories will be created",
|
||||
"ceCartonEstIssuDunCartonDéquipe": "This card comes from a team card, do you really want to delete it?",
|
||||
"certainsCombattantsNontPasDePoidsRenseigné": "Some fighters do not have a weight listed; they will NOT be included in the categories.",
|
||||
"chrono.+/-...S": "+/- ... s",
|
||||
"chrono.+10S": "+10 s",
|
||||
"chrono.+1S": "+1 s",
|
||||
@@ -23,13 +41,16 @@
|
||||
"chrono.entrezLeTempsEnS": "Enter time in seconds",
|
||||
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
|
||||
"chronomètre": "Stopwatch",
|
||||
"classement": "Ranking",
|
||||
"club": "Club",
|
||||
"combattantsCorrespondentAuxSélectionnés": "fighter(s) match the selections above",
|
||||
"compétition": "Competition",
|
||||
"compétitionManager": "Competition manager",
|
||||
"config.obs.dossierDesResources": "Resources folder",
|
||||
"config.obs.motDePasseDuServeur": "Server password",
|
||||
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationDuNomDeLaZone": "Zone name configuration",
|
||||
"configurationObs": "OBS Configuration",
|
||||
"confirm1": "This match already has results; are you sure you want to delete it?",
|
||||
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
|
||||
@@ -38,13 +59,23 @@
|
||||
"confirm3.title": "Change category type",
|
||||
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
|
||||
"confirm4.title": "Delete category",
|
||||
"confirmer": "Confirm",
|
||||
"conserverUniquementLesMatchsTerminés": "Keep only finished matches",
|
||||
"contre": "vs",
|
||||
"couleur": "Color",
|
||||
"créationDeLaLesCatégories": "Creating the category(ies)",
|
||||
"créerLaPhaseFinaleSilYADesPoules": "Create the final phase if there are groups.",
|
||||
"créerLesMatchesDeClassement": "Create the ranking matches",
|
||||
"créerLesMatchesDeClassement.msg": "Ranking matches have already been set up/played; recreating these matches will delete them all (you will therefore lose any results). Please note down any information you wish to keep.",
|
||||
"créerLesMatchs": "Create matches",
|
||||
"créerToutesLesCatégories": "Create all categories",
|
||||
"date": "Date",
|
||||
"demi-finalesEtFinales": "Semi-finals and finals",
|
||||
"depuisUneCatégoriePrédéfinie": "From a predefined category",
|
||||
"duréePause": "Pause duration",
|
||||
"duréeRound": "Round duration",
|
||||
"editionDeLaCatégorie": "Edit category",
|
||||
"editionDuMatch": "Match edition",
|
||||
"enregister": "Save",
|
||||
"enregistrer": "Save",
|
||||
"epéeBouclier": "Sword and shield",
|
||||
@@ -53,30 +84,50 @@
|
||||
"err3": "At least one type (pool or tournament) must be selected.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
|
||||
"etatDesTablesDeMarque": "State of marque tables",
|
||||
"exporter": "Export",
|
||||
"fermer": "Close",
|
||||
"feuilleVierge": "Blank sheet",
|
||||
"finalesUniquement": "Finals only",
|
||||
"genre": "Gender",
|
||||
"genre.f": "F",
|
||||
"genre.h": "M",
|
||||
"genre.na": "NA",
|
||||
"imprimer": "Print",
|
||||
"individuelle": "Individual",
|
||||
"informationCatégorie": "Category information",
|
||||
"inscrit": "Registered",
|
||||
"jusquauRang": "Up to the rank",
|
||||
"leTournoiServiraDePhaseFinaleAuxPoules": "The tournament will serve as the final phase for the group stage.",
|
||||
"lesCombattantsEnDehors": "Fighters not participating in the tournament will have a ranking match.",
|
||||
"lesCombattantsEnDehors2": "Fighters outside the ranking tournament will have a ranking match",
|
||||
"listeDesCartons": "List of cards",
|
||||
"manche": "Round",
|
||||
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
|
||||
"matchTerminé": "Match over",
|
||||
"matches": "Matches",
|
||||
"modeDeCréation": "Creation method",
|
||||
"modifier": "Edit",
|
||||
"msg1": "There are already matches in this pool; what do you want to do with them?",
|
||||
"neRienConserver": "Keep nothing",
|
||||
"no": "No.",
|
||||
"nom": "Name",
|
||||
"nomDeLaZone": "Area name",
|
||||
"nomDeLéquipe": "team name",
|
||||
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
|
||||
"nombreDeCombattants": "Number of fighters",
|
||||
"nouvelle...": "New...",
|
||||
"obs.préfixDesSources": "Source prefix",
|
||||
"pays": "Country",
|
||||
"personnaliser": "Personalize",
|
||||
"podium": "Podium",
|
||||
"podiumDesClubs": "Club podium",
|
||||
"poids": "Weight",
|
||||
"poule": "Pool",
|
||||
"poulePour": "Pool for: ",
|
||||
"préparation...": "Preparing...",
|
||||
"quoiImprimer?": "What print?",
|
||||
"remplacer": "Replace",
|
||||
"rouge": "Red",
|
||||
"réinitialiser": "Reset",
|
||||
"résultat": "Result",
|
||||
@@ -95,21 +146,37 @@
|
||||
"select.sélectionnerDesCombatants": "Select fighters",
|
||||
"select.à": "to",
|
||||
"serveur": "Server",
|
||||
"source": "Source",
|
||||
"suivant": "Next",
|
||||
"supprimer": "Delete",
|
||||
"supprimerUn": "Delete one",
|
||||
"sélectionneLesModesDaffichage": "Select display modes",
|
||||
"sélectionner": "Select",
|
||||
"taille": "Size",
|
||||
"team": "Team",
|
||||
"terminé": "Finished",
|
||||
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.",
|
||||
"toast.card.team.error": "Error while editing team card",
|
||||
"toast.card.team.pending": "Editing team card...",
|
||||
"toast.card.team.success": "Team card edited!",
|
||||
"toast.createCategory.error": "Error while creating the category",
|
||||
"toast.createCategory.pending": "Creating category...",
|
||||
"toast.createCategory.success": "Category created!",
|
||||
"toast.deleteCategory.error": "Error while deleting the category",
|
||||
"toast.deleteCategory.pending": "Deleting category...",
|
||||
"toast.deleteCategory.success": "Category deleted!",
|
||||
"toast.matchs.classement.create.error": "Error while creating ranking matches.",
|
||||
"toast.matchs.classement.create.pending": "Creating ranking matches in progress...",
|
||||
"toast.matchs.classement.create.success": "Ranking matches created successfully.",
|
||||
"toast.matchs.create.error": "Error while creating matches.",
|
||||
"toast.matchs.create.pending": "Creating matches in progress...",
|
||||
"toast.matchs.create.success": "Matches created successfully.",
|
||||
"toast.print.error": "Error while preparing print",
|
||||
"toast.print.pending": "Preparing print...",
|
||||
"toast.print.success": "Print ready!",
|
||||
"toast.team.update.error": "Error while updating team",
|
||||
"toast.team.update.pending": "Updating team...",
|
||||
"toast.team.update.success": "Team updated!",
|
||||
"toast.updateCategory.error": "Error while updating the category",
|
||||
"toast.updateCategory.pending": "Updating category...",
|
||||
"toast.updateCategory.success": "Category updated!",
|
||||
@@ -126,10 +193,12 @@
|
||||
"tournois": "Tournaments",
|
||||
"tousLesMatchs": "All matches",
|
||||
"toutConserver": "Keep all",
|
||||
"touteLaCatégorie": "The entire category",
|
||||
"toutesLesCatégories": "All categories",
|
||||
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
|
||||
"ttm.admin.scripte": "Copy integration script",
|
||||
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
|
||||
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
|
||||
"ttm.table.obs": "Short click: Load configuration and connect.",
|
||||
"ttm.table.pub_aff": "Open public display",
|
||||
"ttm.table.pub_score": "Show scores on public display",
|
||||
"type": "Type",
|
||||
@@ -137,6 +206,7 @@
|
||||
"téléchargementEnCours": "Downloading...",
|
||||
"téléchargementTerminé!": "Download completed!",
|
||||
"uneCatégorie": "a category",
|
||||
"uneCatégorieNePeutContenirPlusDe10Combattants": "A category cannot contain more than 10 fighters, please create weight categories.",
|
||||
"valider": "Validate",
|
||||
"zone": "Zone",
|
||||
"zoneDeCombat": "Combat zone"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"(optionnelle)": "(optional)",
|
||||
"---SansClub---": "--- no club ---",
|
||||
"---TousLesAges---": "--- all ages ---",
|
||||
"---ToutLesClubs---": "--- all clubs ---",
|
||||
"---ToutLesPays---": "--- all countries ---",
|
||||
"---TouteLesCatégories---": "--- all categories ---",
|
||||
@@ -8,6 +9,7 @@
|
||||
"--SélectionnerCatégorie--": "-- Select category --",
|
||||
"1Catégorie": "+1 category",
|
||||
"2Catégorie": "+2 categories",
|
||||
"LesModificationsNontEnregistrer": "/!\\ The changes have not yet been saved, click save /!\\",
|
||||
"activer": "Activate",
|
||||
"admin": "Administration",
|
||||
"administrateur": "Administrator",
|
||||
@@ -74,6 +76,7 @@
|
||||
"aff_req.toast.undo.error": "Failed to cancel affiliation request",
|
||||
"aff_req.toast.undo.pending": "Cancelling affiliation request in progress",
|
||||
"aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉",
|
||||
"afficherLesCombattantsNonPesés": "Show unweighed fighters",
|
||||
"afficherLétatDesAffiliation": "Display affiliation status",
|
||||
"affiliation": "Affiliation",
|
||||
"affiliationNo": "Affiliation no. {{no}}",
|
||||
@@ -81,11 +84,15 @@
|
||||
"ajouterUnClub": "Add a club",
|
||||
"ajouterUnMembre": "Add a member",
|
||||
"all_season": "--- all seasons ---",
|
||||
"ans": "years",
|
||||
"arme": "Weapon",
|
||||
"au": "to",
|
||||
"aucun": "None",
|
||||
"aucunMembreSélectionné": "No member selected",
|
||||
"aucuneCatégorieDisponible": "No categories available at this time.",
|
||||
"back": "« back",
|
||||
"blason": "Coat of arms",
|
||||
"bouclier": "Shield",
|
||||
"bureau": "Board",
|
||||
"button.accepter": "Accept",
|
||||
"button.ajouter": "Add",
|
||||
@@ -93,7 +100,6 @@
|
||||
"button.appliquer": "Apply",
|
||||
"button.confirmer": "Confirm",
|
||||
"button.créer": "Create",
|
||||
"button.enregister": "Save",
|
||||
"button.enregistrer": "Save",
|
||||
"button.fermer": "Close",
|
||||
"button.modifier": "Edit",
|
||||
@@ -101,6 +107,7 @@
|
||||
"button.seDésinscrire": "Unsubscribe",
|
||||
"button.suivant": "Next",
|
||||
"button.supprimer": "Delete",
|
||||
"casque": "Helmet",
|
||||
"cat.benjamin": "Benjamin",
|
||||
"cat.cadet": "Cadet",
|
||||
"cat.catégorieInconnue": "Unknown category",
|
||||
@@ -115,7 +122,9 @@
|
||||
"cat.vétéran2": "Veteran 2",
|
||||
"categorie": "category",
|
||||
"catégorie": "Category",
|
||||
"catégorieàAjouter": "Category to add",
|
||||
"certificatMédical": "Medical certificate",
|
||||
"champAttendu": "Expected field",
|
||||
"chargement...": "Loading...",
|
||||
"chargerLexcel": "Load Excel",
|
||||
"chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.",
|
||||
@@ -146,6 +155,7 @@
|
||||
"club_one": "Club",
|
||||
"club_other": "Clubs",
|
||||
"club_zero": "No club",
|
||||
"colonneDansLeFichier": "Column in the file",
|
||||
"combattant": "fighter",
|
||||
"comp.aff.blason": "Display the club's coat of arms on screens",
|
||||
"comp.aff.flag": "Display the fighter's country on screens",
|
||||
@@ -186,7 +196,9 @@
|
||||
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators",
|
||||
"comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers",
|
||||
"comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing",
|
||||
"comp.modal.annoncé": "Announced",
|
||||
"comp.modal.information": "Information",
|
||||
"comp.modal.pesé": "Weighed",
|
||||
"comp.modal.poids": "Weight (in kg)",
|
||||
"comp.modal.recherche": "Search*",
|
||||
"comp.modal.surclassement": "Overclassification",
|
||||
@@ -221,6 +233,8 @@
|
||||
"comp.toast.register.add.error": "Fighter not found",
|
||||
"comp.toast.register.add.pending": "Search in progress",
|
||||
"comp.toast.register.add.success": "Fighter found and added/updated",
|
||||
"comp.toast.register.addMultiple.success_one": "Successful import for 1 fighter",
|
||||
"comp.toast.register.addMultiple.success_other": "Successful import for {{count}} fighters",
|
||||
"comp.toast.register.ban.error": "Error",
|
||||
"comp.toast.register.ban.pending": "Unregistration in progress",
|
||||
"comp.toast.register.ban.success": "Fighter unregistered and banned",
|
||||
@@ -233,6 +247,9 @@
|
||||
"comp.toast.register.self.del.error": "Error during unregistration",
|
||||
"comp.toast.register.self.del.pending": "Unregistration in progress",
|
||||
"comp.toast.register.self.del.success": "Unregistration completed",
|
||||
"comp.toast.registers.addMultiple.error": "Import failed",
|
||||
"comp.toast.registers.addMultiple.pending": "Import in progress",
|
||||
"comp.toast.registers.addMultiple.success": "Import completed successfully 🎉",
|
||||
"comp.toast.save.error": "Failed to save competition",
|
||||
"comp.toast.save.pending": "Saving competition in progress",
|
||||
"comp.toast.save.success": "Competition saved successfully 🎉",
|
||||
@@ -249,11 +266,13 @@
|
||||
"compte": "Account",
|
||||
"compétition": "Competition",
|
||||
"configuration": "Configuration",
|
||||
"configurationDeLaCatégorie": "Category configuration",
|
||||
"conserverLancienEmail": "Keep the old email",
|
||||
"contactAdministratif": "Administrative contact",
|
||||
"contactInterne": "Internal contact",
|
||||
"contact_one": "Contact",
|
||||
"contact_other": "Contacts",
|
||||
"coquilleProtectionPelvienne": "Shell / Pelvic protection",
|
||||
"date": "Date",
|
||||
"dateDeNaissance": "Date of birth",
|
||||
"days": [
|
||||
@@ -274,6 +293,8 @@
|
||||
"donnéesAdministratives": "Administrative data",
|
||||
"du": "From",
|
||||
"dun": "of a",
|
||||
"duréePause": "Pause duration",
|
||||
"duréeRound": "Round duration",
|
||||
"définirLidDuCompte": "Define account ID",
|
||||
"editionDeL'affiliation": "Editing affiliation",
|
||||
"editionDeLaDemande": "Editing request",
|
||||
@@ -286,13 +307,78 @@
|
||||
"erreurDePaiement": "Payment error😕",
|
||||
"erreurDePaiement.detail": "Error message:",
|
||||
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
|
||||
"erreurPourLinscription": "Registration error",
|
||||
"espaceAdministration": "Administration space",
|
||||
"f": "F",
|
||||
"faitPar": "Done by",
|
||||
"femme": "Female",
|
||||
"fileImport.variants": {
|
||||
"categorie": [
|
||||
"category",
|
||||
"catégorie",
|
||||
"weight category",
|
||||
"age category"
|
||||
],
|
||||
"club": [
|
||||
"club",
|
||||
"club name",
|
||||
"association",
|
||||
"association name"
|
||||
],
|
||||
"genre": [
|
||||
"gender",
|
||||
"genre",
|
||||
"sex",
|
||||
"civility"
|
||||
],
|
||||
"licence": [
|
||||
"license",
|
||||
"licence",
|
||||
"license number",
|
||||
"license ID",
|
||||
"ID license",
|
||||
"licence no"
|
||||
],
|
||||
"nom": [
|
||||
"last name",
|
||||
"nom",
|
||||
"family name",
|
||||
"surname",
|
||||
"lastname"
|
||||
],
|
||||
"overCategory": [
|
||||
"over category",
|
||||
"surclassement",
|
||||
"category override",
|
||||
"over classification"
|
||||
],
|
||||
"pays": [
|
||||
"country",
|
||||
"pays",
|
||||
"country of residence",
|
||||
"origin country"
|
||||
],
|
||||
"prenom": [
|
||||
"first name",
|
||||
"prénom",
|
||||
"given name",
|
||||
"first given name"
|
||||
],
|
||||
"weight": [
|
||||
"weight",
|
||||
"poids",
|
||||
"weight (kg)",
|
||||
"actual weight",
|
||||
"mass"
|
||||
]
|
||||
},
|
||||
"filtre": "Filter",
|
||||
"gantMainBouclier": "Shield hand glove",
|
||||
"gantMainsArmées": "Armed hand(s) glove(s)",
|
||||
"gants": "Gloves",
|
||||
"genre": "Gender",
|
||||
"gestionGroupée": "Group management",
|
||||
"gorgerin": "Gorgerin",
|
||||
"gradeDarbitrage": "Refereeing grade",
|
||||
"h": "M",
|
||||
"home": {
|
||||
@@ -304,6 +390,9 @@
|
||||
},
|
||||
"homme": "Male",
|
||||
"horairesD'entraînements": "Training schedules",
|
||||
"importationDuFichier": "Importing the file",
|
||||
"importerDesCombattants": "Import fighters",
|
||||
"importerDesInvités": "Import guests",
|
||||
"information": "Information",
|
||||
"invité": "guest",
|
||||
"keepEmpty": "Leave blank to make no changes.",
|
||||
@@ -312,6 +401,8 @@
|
||||
"licenceNo": "License no. {{no}}",
|
||||
"lieu": "Place",
|
||||
"lieuxDentraînements": "Training locations",
|
||||
"ligneIgnorée1": "Line ignored: missing name, first name or category.",
|
||||
"ligneIgnorée2": "Line ignored: missing first name or license.",
|
||||
"loading": "Loading...",
|
||||
"me": {
|
||||
"result": {
|
||||
@@ -440,6 +531,8 @@
|
||||
"nouveauClub": "New club",
|
||||
"nouveauMembre": "New member",
|
||||
"nouvelEmail": "New email",
|
||||
"numéroDeLaLigneDentête": "Header line number",
|
||||
"numéroDeLigne": "Line number",
|
||||
"ou": "or",
|
||||
"oui": "Yes",
|
||||
"outdated_session": {
|
||||
@@ -472,8 +565,21 @@
|
||||
"perm.créerDesCompétion": "Create competitions",
|
||||
"perm.ffsafIntra": "FFSAF intra",
|
||||
"permission": "Permission",
|
||||
"peutSinscrire": "Can register?",
|
||||
"photos": "Photos",
|
||||
"plastron": "Breastplate",
|
||||
"poids": "Weight",
|
||||
"poidsDemandéPour": "Weight required for",
|
||||
"prenom": "First name",
|
||||
"protectionDeBras": "Arm protection",
|
||||
"protectionDeBrasArmé": "Protection of armed arm(s)",
|
||||
"protectionDeBrasDeBouclier": "Shield arm protection",
|
||||
"protectionDeCoudes": "Elbow protection",
|
||||
"protectionDeGenoux": "Knee protection",
|
||||
"protectionDeJambes": "Leg protection",
|
||||
"protectionDePieds": "Foot protection",
|
||||
"protectionDorsale": "Back protector",
|
||||
"protectionObligatoire": "Mandatory protection",
|
||||
"prénomEtNom": "First and last name",
|
||||
"rechercher": "Search",
|
||||
"rechercher...": "Search...",
|
||||
@@ -494,8 +600,15 @@
|
||||
"role.vise-secrétaire": "Vice-Secretary",
|
||||
"role.vise-trésorier": "Vice-Treasurer",
|
||||
"saison": "Season",
|
||||
"sans": "Without",
|
||||
"secrétariatsDeLice": "Ring secretariats",
|
||||
"selectionner...": "Select...",
|
||||
"shield.buckler": "Buckler",
|
||||
"shield.none": "$t(sans) / $t(nonDéfinie)",
|
||||
"shield.round": "Round",
|
||||
"shield.standard": "Standard",
|
||||
"shield.teardrop": "Teardrop",
|
||||
"siDisponiblePourLaCatégorieDages": "If available for the age category",
|
||||
"siretOuRna": "SIRET or RNA",
|
||||
"stats": "Statistics",
|
||||
"statue": "Statue",
|
||||
@@ -505,6 +618,10 @@
|
||||
"supprimerLeClub.msg": "Are you sure you want to delete this club?",
|
||||
"supprimerLeCompte": "Delete account",
|
||||
"supprimerLeCompte.msg": "Are you sure you want to delete this account?",
|
||||
"sword.none": "$t(sans) / $t(nonDéfinie)",
|
||||
"sword.oneHand": "One hand sword",
|
||||
"sword.saber": "Saber",
|
||||
"sword.twoHand": "Two hands sword",
|
||||
"sélectionEnéquipeDeFrance": "Selection in the French team",
|
||||
"sélectionner...": "Select...",
|
||||
"toast.edit.error": "Failed to save changes",
|
||||
@@ -536,8 +653,12 @@
|
||||
"validerLicence_other": "Validate the {{count}} selected licenses",
|
||||
"validerLicence_zero": "$t(validerLicence_other)",
|
||||
"validée": "Validated",
|
||||
"veuillezAssocierChaqueChampàUneColonneDuFichier": "Please associate each field with a column in the file",
|
||||
"veuillezIndiqueràQuelle": "Please indicate on which line the headers are located in the file",
|
||||
"veuillezMapperLesColonnesSuivantes": "Please map the following columns",
|
||||
"voir/modifierLesParticipants": "View/Edit participants",
|
||||
"voirLesStatues": "View statues",
|
||||
"vousNêtesPasEncoreInscrit": "You are not yet registered or your registration has not yet been entered on the intranet",
|
||||
"à": "at",
|
||||
"étatDeLaDemande": "Request status"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"--sélectionnerUnClub--": "--Select a club--",
|
||||
"--sélectionnerUnCombattant--": "--Select a fighter--",
|
||||
"--sélectionnerUneCatégorie--": "--Select a category--",
|
||||
"1er": "Gold medal",
|
||||
"2eme": "Silver medal",
|
||||
"3eme": "Bronze medal",
|
||||
"abs.": "abs.",
|
||||
"adversaire": "Opponent",
|
||||
"aujourdhuià": "Today at {{time}}",
|
||||
@@ -10,6 +13,10 @@
|
||||
"bleu": "Blue",
|
||||
"catégorie": "Category",
|
||||
"chargement": "Loading",
|
||||
"classement": "Ranking",
|
||||
"classementClub": "Club ranking",
|
||||
"classementDesClub": "Clubs ranking",
|
||||
"classementFinal": "Final ranking",
|
||||
"club": "Club",
|
||||
"combattant": "Fighter",
|
||||
"combattants": "Fighters",
|
||||
@@ -57,6 +64,7 @@
|
||||
"rechercheParCombattant": "Search by fighter",
|
||||
"rouge": "Red",
|
||||
"résultatDeLaCompétition": "Competition result",
|
||||
"score": "Score",
|
||||
"scores": "Scores",
|
||||
"statistique": "Statistics",
|
||||
"tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})",
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
{
|
||||
"--SélectionnerUnCombattant--": "-- Sélectionner un combattant --",
|
||||
"--Tous--": "-- Tous --",
|
||||
"PourLéquipe": " pour l'équipe",
|
||||
"actuel": "Actuel",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Adresse du serveur",
|
||||
"afficher": "Afficher",
|
||||
"ajoutAutomatique": "Ajout automatique",
|
||||
"ajouter": "Ajouter",
|
||||
"ajouterDesCombattants": "Ajouter des combattants",
|
||||
"ajouterUn": "Ajouter un ",
|
||||
"ajouterUneTeam": "Ajouter une équipe",
|
||||
"attention": "Attention",
|
||||
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
|
||||
"avertissement": "Avertissement",
|
||||
"bleu": "Bleu",
|
||||
"blue": "Blue",
|
||||
"cardAdded": "Carton ajouté",
|
||||
"cardRemoved": "Carton retiré",
|
||||
"carton": "Carton",
|
||||
"cartonDéquipe": "Carton d'équipe",
|
||||
"cartonJaune": "Carton jaune",
|
||||
"cartonNoir": "Carton noir",
|
||||
"cartonRouge": "Carton rouge",
|
||||
"catégorie": "Catégorie",
|
||||
"catégorieDâgeMoyenne": "Catégorie d'âge moyenne",
|
||||
"catégorieSélectionnée": "Catégorie sélectionnée",
|
||||
"catégoriesVontêtreCréées": "catégories de poids vont être créées",
|
||||
"ceCartonEstIssuDunCartonDéquipe": "Ce carton est issu d'un carton d'équipe, voulez-vous vraiment le supprimer ?",
|
||||
"certainsCombattantsNontPasDePoidsRenseigné": "Certains combattants n'ont pas de poids renseigné, ils ne seront PAS insert dans les catégories",
|
||||
"chrono.+/-...S": "+/- ... s",
|
||||
"chrono.+10S": "+10 s",
|
||||
"chrono.+1S": "+1 s",
|
||||
@@ -23,13 +41,16 @@
|
||||
"chrono.entrezLeTempsEnS": "Entrez le temps en s",
|
||||
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
|
||||
"chronomètre": "Chronomètre",
|
||||
"classement": "Classement",
|
||||
"club": "Club",
|
||||
"combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélections ci-dessus.",
|
||||
"compétition": "Compétition",
|
||||
"compétitionManager": "Compétition manager",
|
||||
"config.obs.dossierDesResources": "Dossier des resources",
|
||||
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
|
||||
"config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationDuNomDeLaZone": "Configuration du nom de la zone",
|
||||
"configurationObs": "Configuration OBS",
|
||||
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
|
||||
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!",
|
||||
@@ -38,13 +59,23 @@
|
||||
"confirm3.title": "Changement de type de catégorie",
|
||||
"confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
|
||||
"confirm4.title": "Suppression de la catégorie",
|
||||
"confirmer": "Confirmer",
|
||||
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
|
||||
"contre": "contre",
|
||||
"couleur": "Couleur",
|
||||
"créationDeLaLesCatégories": "Création de la/les catégories",
|
||||
"créerLaPhaseFinaleSilYADesPoules": "Créer la phase finale s'il y a des poules",
|
||||
"créerLesMatchesDeClassement": "Créer les matches de classement",
|
||||
"créerLesMatchesDeClassement.msg": "Des matches de classement ont déjà été configurer/jouer, la recréation de ces matches vont tous les supprimer (vous perdre donc les résultats s'il y en a). Mercie de noter de votre côté les informations que vous voulez conserver.",
|
||||
"créerLesMatchs": "Créer les matchs",
|
||||
"créerToutesLesCatégories": "Créer toutes les catégories",
|
||||
"date": "Date",
|
||||
"demi-finalesEtFinales": "Demi-finales et finales",
|
||||
"depuisUneCatégoriePrédéfinie": "Depuis une catégorie prédéfinie",
|
||||
"duréePause": "Durée pause",
|
||||
"duréeRound": "Durée round",
|
||||
"editionDeLaCatégorie": "Edition de la catégorie",
|
||||
"editionDuMatch": "Edition du match",
|
||||
"enregister": "Enregister",
|
||||
"enregistrer": "Enregistrer",
|
||||
"epéeBouclier": "Epée bouclier",
|
||||
@@ -53,30 +84,50 @@
|
||||
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
|
||||
"etatDesTablesDeMarque": "Etat des tables de marque",
|
||||
"exporter": "Exporter",
|
||||
"fermer": "Fermer",
|
||||
"feuilleVierge": "Feuille vierge",
|
||||
"finalesUniquement": "Finales uniquement",
|
||||
"genre": "Genre",
|
||||
"genre.f": "F",
|
||||
"genre.h": "H",
|
||||
"genre.na": "NA",
|
||||
"imprimer": "Imprimer",
|
||||
"individuelle": "Individuelle",
|
||||
"informationCatégorie": "Information catégorie",
|
||||
"inscrit": "Inscrit",
|
||||
"jusquauRang": "Jusqu'au rang",
|
||||
"leTournoiServiraDePhaseFinaleAuxPoules": "Le tournoi servira de phase finale aux poules",
|
||||
"lesCombattantsEnDehors": "Les combattants en dehors du tournoi auront un match de classement",
|
||||
"lesCombattantsEnDehors2": "Les combattants en dehors du tournoi de classement auront un match de classement",
|
||||
"listeDesCartons": "Liste des cartons",
|
||||
"manche": "Manche",
|
||||
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
|
||||
"matchTerminé": "Match terminé",
|
||||
"matches": "Matches",
|
||||
"modeDeCréation": "Mode de création",
|
||||
"modifier": "Modifier",
|
||||
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
|
||||
"neRienConserver": "Ne rien conserver",
|
||||
"no": "N°",
|
||||
"nom": "Nom",
|
||||
"nomDeLaZone": "Nom de la zone",
|
||||
"nomDeLéquipe": "Nom de l'équipe",
|
||||
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
|
||||
"nombreDeCombattants": "Nombre de combattants",
|
||||
"nouvelle...": "Nouvelle...",
|
||||
"obs.préfixDesSources": "Préfix des sources",
|
||||
"pays": "Pays",
|
||||
"personnaliser": "Personnaliser",
|
||||
"podium": "Podium",
|
||||
"podiumDesClubs": "Podium des clubs",
|
||||
"poids": "Poids",
|
||||
"poule": "Poule",
|
||||
"poulePour": "Poule pour: ",
|
||||
"préparation...": "Préparation...",
|
||||
"quoiImprimer?": "Quoi imprimer ?",
|
||||
"remplacer": "Remplacer",
|
||||
"rouge": "Rouge",
|
||||
"réinitialiser": "Réinitialiser",
|
||||
"résultat": "Résultat",
|
||||
@@ -95,21 +146,37 @@
|
||||
"select.sélectionnerDesCombatants": "Sélectionner des combatants",
|
||||
"select.à": "à",
|
||||
"serveur": "Serveur",
|
||||
"source": "Source",
|
||||
"suivant": "Suivant",
|
||||
"supprimer": "Supprimer",
|
||||
"supprimerUn": "Supprimer un",
|
||||
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
|
||||
"sélectionner": "Sélectionner",
|
||||
"taille": "Taille",
|
||||
"team": "Équipe",
|
||||
"terminé": "Terminé",
|
||||
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.",
|
||||
"toast.card.team.error": "Erreur lors de la modification du carton d'équipe",
|
||||
"toast.card.team.pending": "Modification du carton d'équipe...",
|
||||
"toast.card.team.success": "Carton d'équipe modifié !",
|
||||
"toast.createCategory.error": "Erreur lors de la création de la catégorie",
|
||||
"toast.createCategory.pending": "Création de la catégorie...",
|
||||
"toast.createCategory.success": "Catégorie créée !",
|
||||
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
|
||||
"toast.deleteCategory.pending": "Suppression de la catégorie...",
|
||||
"toast.deleteCategory.success": "Catégorie supprimée !",
|
||||
"toast.matchs.classement.create.error": "Erreur lors de la création des matchs de classement.",
|
||||
"toast.matchs.classement.create.pending": "Création des matchs de classement en cours...",
|
||||
"toast.matchs.classement.create.success": "Matchs de classement créés avec succès.",
|
||||
"toast.matchs.create.error": "Erreur lors de la création des matchs.",
|
||||
"toast.matchs.create.pending": "Création des matchs en cours...",
|
||||
"toast.matchs.create.success": "Matchs créés avec succès.",
|
||||
"toast.print.error": "Erreur lors de la génération du PDF",
|
||||
"toast.print.pending": "Génération du PDF en cours...",
|
||||
"toast.print.success": "PDF généré !",
|
||||
"toast.team.update.error": "Erreur lors de la mise à jour de l'équipe",
|
||||
"toast.team.update.pending": "Mise à jour de l'équipe...",
|
||||
"toast.team.update.success": "Équipe mise à jour !",
|
||||
"toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie",
|
||||
"toast.updateCategory.pending": "Mise à jour de la catégorie...",
|
||||
"toast.updateCategory.success": "Catégorie mise à jour !",
|
||||
@@ -126,10 +193,12 @@
|
||||
"tournois": "Tournois",
|
||||
"tousLesMatchs": "Tous les matchs",
|
||||
"toutConserver": "Tout conserver",
|
||||
"touteLaCatégorie": "Toute la catégorie",
|
||||
"toutesLesCatégories": "Toutes les catégories",
|
||||
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
|
||||
"ttm.admin.scripte": "Copier le scripte d'intégration",
|
||||
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
|
||||
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
|
||||
"ttm.table.obs": "Clique court : Charger la configuration et se connecter.",
|
||||
"ttm.table.pub_aff": "Ouvrir l'affichage public",
|
||||
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
|
||||
"type": "Type",
|
||||
@@ -137,6 +206,7 @@
|
||||
"téléchargementEnCours": "Téléchargement en cours...",
|
||||
"téléchargementTerminé!": "Téléchargement terminé !",
|
||||
"uneCatégorie": "une catégorie",
|
||||
"uneCatégorieNePeutContenirPlusDe10Combattants": "Une catégorie ne peut contenir plus de 10 combattants, veuillez créer des catégories de poids.",
|
||||
"valider": "Valider",
|
||||
"zone": "Zone",
|
||||
"zoneDeCombat": "Zone de combat"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"(optionnelle)": "(optionnelle)",
|
||||
"---SansClub---": "--- sans club ---",
|
||||
"---ToutLesClubs---": "--- tout les clubs ---",
|
||||
"---TousLesAges---": "--- tous les ages ---",
|
||||
"---ToutLesClubs---": "--- tous les clubs ---",
|
||||
"---ToutLesPays---": "--- tout les pays ---",
|
||||
"---TouteLesCatégories---": "--- toute les catégories ---",
|
||||
"--NonLicencier--": "-- Non licencier --",
|
||||
"--SélectionnerCatégorie--": "-- Sélectionner catégorie --",
|
||||
"1Catégorie": "+1 catégorie",
|
||||
"2Catégorie": "+2 catégorie",
|
||||
"LesModificationsNontEnregistrer": "/!\\ Les modifications n'ont pas encore été enregistré, cliqué sur enregistrer /!\\",
|
||||
"activer": "Activer",
|
||||
"admin": "Administration",
|
||||
"administrateur": "Administrateur",
|
||||
@@ -74,6 +76,7 @@
|
||||
"aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation",
|
||||
"aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours",
|
||||
"aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉",
|
||||
"afficherLesCombattantsNonPesés": "Afficher les combattants non pesés",
|
||||
"afficherLétatDesAffiliation": "Afficher l'état des affiliation",
|
||||
"affiliation": "Affiliation",
|
||||
"affiliationNo": "Affiliation n°{{no}}",
|
||||
@@ -81,11 +84,15 @@
|
||||
"ajouterUnClub": "Ajouter un club",
|
||||
"ajouterUnMembre": "Ajouter un membre",
|
||||
"all_season": "--- tout les saisons ---",
|
||||
"ans": "ans",
|
||||
"arme": "Arme",
|
||||
"au": "au",
|
||||
"aucun": "Aucun",
|
||||
"aucunMembreSélectionné": "Aucun membre sélectionné",
|
||||
"aucuneCatégorieDisponible": "Aucune catégorie disponible pour le moment.",
|
||||
"back": "« retour",
|
||||
"blason": "Blason",
|
||||
"bouclier": "Bouclier",
|
||||
"bureau": "Bureau",
|
||||
"button.accepter": "Accepter",
|
||||
"button.ajouter": "Ajouter",
|
||||
@@ -93,7 +100,6 @@
|
||||
"button.appliquer": "Appliquer",
|
||||
"button.confirmer": "Confirmer",
|
||||
"button.créer": "Créer",
|
||||
"button.enregister": "Enregister",
|
||||
"button.enregistrer": "Enregistrer",
|
||||
"button.fermer": "Fermer",
|
||||
"button.modifier": "Modifier",
|
||||
@@ -101,6 +107,7 @@
|
||||
"button.seDésinscrire": "Se désinscrire",
|
||||
"button.suivant": "Suivant",
|
||||
"button.supprimer": "Supprimer",
|
||||
"casque": "Casque",
|
||||
"cat.benjamin": "Benjamin",
|
||||
"cat.cadet": "Cadet",
|
||||
"cat.catégorieInconnue": "Catégorie inconnue",
|
||||
@@ -115,7 +122,9 @@
|
||||
"cat.vétéran2": "Vétéran 2",
|
||||
"categorie": "categorie",
|
||||
"catégorie": "Catégorie",
|
||||
"catégorieàAjouter": "Catégorie à ajouter",
|
||||
"certificatMédical": "Certificat médical",
|
||||
"champAttendu": "Champ attendu",
|
||||
"chargement...": "Chargement...",
|
||||
"chargerLexcel": "Charger l'Excel",
|
||||
"chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.",
|
||||
@@ -146,6 +155,7 @@
|
||||
"club_one": "Club",
|
||||
"club_other": "Clubs",
|
||||
"club_zero": "Sans club",
|
||||
"colonneDansLeFichier": "Colonne dans le fichier",
|
||||
"combattant": "combattant",
|
||||
"comp.aff.blason": "Afficher le blason du club sur les écrans",
|
||||
"comp.aff.flag": "Afficher le pays du combattant sur les écrans",
|
||||
@@ -165,7 +175,7 @@
|
||||
"comp.error1": "La date de fin doit être postérieure à la date de début.",
|
||||
"comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.",
|
||||
"comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.",
|
||||
"comp.exporterLesInscription": "Exporter les inscription",
|
||||
"comp.exporterLesInscription": "Exporter les inscriptions",
|
||||
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué",
|
||||
"comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.",
|
||||
"comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.",
|
||||
@@ -186,7 +196,9 @@
|
||||
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition",
|
||||
"comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club",
|
||||
"comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso",
|
||||
"comp.modal.annoncé": "Annoncé",
|
||||
"comp.modal.information": "Information",
|
||||
"comp.modal.pesé": "Pesé",
|
||||
"comp.modal.poids": "Poids (en kg)",
|
||||
"comp.modal.recherche": "Recherche*",
|
||||
"comp.modal.surclassement": "Surclassement",
|
||||
@@ -221,6 +233,8 @@
|
||||
"comp.toast.register.add.error": "Combattant non trouvé",
|
||||
"comp.toast.register.add.pending": "Recherche en cours",
|
||||
"comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour",
|
||||
"comp.toast.register.addMultiple.success_one": "Importation réussie pour 1 combattant",
|
||||
"comp.toast.register.addMultiple.success_other": "Importation réussie pour {{count}} combattants",
|
||||
"comp.toast.register.ban.error": "Erreur",
|
||||
"comp.toast.register.ban.pending": "Désinscription en cours",
|
||||
"comp.toast.register.ban.success": "Combattant désinscrit et bannie",
|
||||
@@ -233,6 +247,9 @@
|
||||
"comp.toast.register.self.del.error": "Erreur lors de la désinscription",
|
||||
"comp.toast.register.self.del.pending": "Désinscription en cours",
|
||||
"comp.toast.register.self.del.success": "Désinscription réalisée",
|
||||
"comp.toast.registers.addMultiple.error": "Erreur lors de l'importation des combattants",
|
||||
"comp.toast.registers.addMultiple.pending": "Importation des combattants en cours...",
|
||||
"comp.toast.registers.addMultiple.success": "Importation des combattants réussie 🎉",
|
||||
"comp.toast.save.error": "Échec de l'enregistrement de la compétition",
|
||||
"comp.toast.save.pending": "Enregistrement de la compétition en cours",
|
||||
"comp.toast.save.success": "Compétition enregistrée avec succès 🎉",
|
||||
@@ -249,11 +266,13 @@
|
||||
"compte": "Compte",
|
||||
"compétition": "Compétition",
|
||||
"configuration": "Configuration",
|
||||
"configurationDeLaCatégorie": "Configuration de la catégorie",
|
||||
"conserverLancienEmail": "Conserver l'ancien email",
|
||||
"contactAdministratif": "Contact administratif",
|
||||
"contactInterne": "Contact interne",
|
||||
"contact_one": "Contact",
|
||||
"contact_other": "Contacts",
|
||||
"coquilleProtectionPelvienne": "Coquille / Protection pelvienne",
|
||||
"date": "Date",
|
||||
"dateDeNaissance": "Date de naissance",
|
||||
"days": [
|
||||
@@ -274,6 +293,8 @@
|
||||
"donnéesAdministratives": "Données administratives",
|
||||
"du": "Du",
|
||||
"dun": "d'un",
|
||||
"duréePause": "Durée pause",
|
||||
"duréeRound": "Durée round",
|
||||
"définirLidDuCompte": "Définir l'id du compte",
|
||||
"editionDeL'affiliation": "Edition de l'affiliation",
|
||||
"editionDeLaDemande": "Edition de la demande ",
|
||||
@@ -286,13 +307,82 @@
|
||||
"erreurDePaiement": "Erreur de paiement😕",
|
||||
"erreurDePaiement.detail": "Message d'erreur :",
|
||||
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.",
|
||||
"erreurPourLinscription": "Erreur pour l'inscription",
|
||||
"espaceAdministration": "Espace administration",
|
||||
"f": "F",
|
||||
"faitPar": "Fait par",
|
||||
"femme": "Femme",
|
||||
"fileImport.variants": {
|
||||
"categorie": [
|
||||
"catégorie",
|
||||
"category",
|
||||
"catégorie de poids",
|
||||
"weight category",
|
||||
"catégorie d'âge"
|
||||
],
|
||||
"club": [
|
||||
"club",
|
||||
"nom du club",
|
||||
"club name",
|
||||
"association",
|
||||
"nom de l'association"
|
||||
],
|
||||
"genre": [
|
||||
"genre",
|
||||
"sexe",
|
||||
"gender",
|
||||
"sex",
|
||||
"civilité"
|
||||
],
|
||||
"licence": [
|
||||
"licence",
|
||||
"n° licence",
|
||||
"num licence",
|
||||
"id licence",
|
||||
"license",
|
||||
"licence id"
|
||||
],
|
||||
"nom": [
|
||||
"nom",
|
||||
"nom de famille",
|
||||
"lastname",
|
||||
"family name",
|
||||
"nom complet"
|
||||
],
|
||||
"overCategory": [
|
||||
"surclassement",
|
||||
"over category",
|
||||
"surcatégorie",
|
||||
"surclassement de catégorie"
|
||||
],
|
||||
"pays": [
|
||||
"pays",
|
||||
"country",
|
||||
"pays de résidence",
|
||||
"pays d'origine"
|
||||
],
|
||||
"prenom": [
|
||||
"prénom",
|
||||
"prenom",
|
||||
"first name",
|
||||
"given name",
|
||||
"prénom usuel"
|
||||
],
|
||||
"weight": [
|
||||
"poids",
|
||||
"weight",
|
||||
"poids (kg)",
|
||||
"poids réel",
|
||||
"masse"
|
||||
]
|
||||
},
|
||||
"filtre": "Filtre",
|
||||
"gantMainBouclier": "Gant main de bouclier",
|
||||
"gantMainsArmées": "Gant main(s) armée(s)",
|
||||
"gants": "Gants",
|
||||
"genre": "Genre",
|
||||
"gestionGroupée": "Gestion groupée",
|
||||
"gorgerin": "Gorgerin",
|
||||
"gradeDarbitrage": "Grade d'arbitrage",
|
||||
"h": "H",
|
||||
"home": {
|
||||
@@ -304,6 +394,9 @@
|
||||
},
|
||||
"homme": "Homme",
|
||||
"horairesD'entraînements": "Horaires d'entraînements",
|
||||
"importationDuFichier": "Importation du fichier",
|
||||
"importerDesCombattants": "Importer des combattants",
|
||||
"importerDesInvités": "Importer des invités",
|
||||
"information": "Information",
|
||||
"invité": "invité",
|
||||
"keepEmpty": "Laissez vide pour ne rien changer.",
|
||||
@@ -312,6 +405,8 @@
|
||||
"licenceNo": "Licence n°{{no}}",
|
||||
"lieu": "Lieu",
|
||||
"lieuxDentraînements": "Lieux d'entraînements",
|
||||
"ligneIgnorée1": "Ligne ignorée : nom, prénom ou catégorie manquante.",
|
||||
"ligneIgnorée2": "Ligne ignorée : nom prénom ou licence manquante.",
|
||||
"loading": "Chargement...",
|
||||
"me": {
|
||||
"result": {
|
||||
@@ -440,6 +535,8 @@
|
||||
"nouveauClub": "Nouveau club",
|
||||
"nouveauMembre": "Nouveau membre",
|
||||
"nouvelEmail": "Nouvel email",
|
||||
"numéroDeLaLigneDentête": "Numéro de la ligne d'en-tête",
|
||||
"numéroDeLigne": "Numéro de ligne",
|
||||
"ou": "Ou",
|
||||
"oui": "Oui",
|
||||
"outdated_session": {
|
||||
@@ -472,8 +569,21 @@
|
||||
"perm.créerDesCompétion": "Créer des compétion",
|
||||
"perm.ffsafIntra": "FFSAF intra",
|
||||
"permission": "Permission",
|
||||
"peutSinscrire": "Peut s'inscrire?",
|
||||
"photos": "Photos",
|
||||
"plastron": "Plastron",
|
||||
"poids": "Poids",
|
||||
"poidsDemandéPour": "Poids demandé pour",
|
||||
"prenom": "Prénom",
|
||||
"protectionDeBras": "Protection de bras",
|
||||
"protectionDeBrasArmé": "Protection de bras armé(s)",
|
||||
"protectionDeBrasDeBouclier": "Protection de bras de bouclier",
|
||||
"protectionDeCoudes": "Protection de coudes",
|
||||
"protectionDeGenoux": "Protection de genoux",
|
||||
"protectionDeJambes": "Protection de jambes",
|
||||
"protectionDePieds": "Protection de pieds",
|
||||
"protectionDorsale": "Protection dorsale",
|
||||
"protectionObligatoire": "Protection obligatoire",
|
||||
"prénomEtNom": "Prénom et nom",
|
||||
"rechercher": "Rechercher",
|
||||
"rechercher...": "Rechercher...",
|
||||
@@ -494,8 +604,15 @@
|
||||
"role.vise-secrétaire": "Vise-Secrétaire",
|
||||
"role.vise-trésorier": "Vise-Trésorier",
|
||||
"saison": "Saison",
|
||||
"sans": "Sans",
|
||||
"secrétariatsDeLice": "Secrétariats de lice",
|
||||
"selectionner...": "Sélectionner...",
|
||||
"shield.buckler": "Bocle",
|
||||
"shield.none": "$t(sans) / $t(nonDéfinie)",
|
||||
"shield.round": "Rond",
|
||||
"shield.standard": "Standard",
|
||||
"shield.teardrop": "Larme",
|
||||
"siDisponiblePourLaCatégorieDages": "Si disponible pour la catégorie d'ages",
|
||||
"siretOuRna": "SIRET ou RNA",
|
||||
"stats": "Statistiques",
|
||||
"statue": "Statue",
|
||||
@@ -505,6 +622,10 @@
|
||||
"supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?",
|
||||
"supprimerLeCompte": "Supprimer le compte",
|
||||
"supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?",
|
||||
"sword.none": "$t(sans) / $t(nonDéfinie)",
|
||||
"sword.oneHand": "Épée une main",
|
||||
"sword.saber": "Sabre",
|
||||
"sword.twoHand": "Épée deux mains",
|
||||
"sélectionEnéquipeDeFrance": "Sélection en équipe de France",
|
||||
"sélectionner...": "Sélectionner...",
|
||||
"toast.edit.error": "Échec de l'enregistrement des modifications",
|
||||
@@ -536,8 +657,12 @@
|
||||
"validerLicence_other": "Valider les {{count}} licences sélectionnées",
|
||||
"validerLicence_zero": "$t(validerLicence_other)",
|
||||
"validée": "Validée",
|
||||
"veuillezAssocierChaqueChampàUneColonneDuFichier": "Veuillez associer chaque champ à une colonne du fichier",
|
||||
"veuillezIndiqueràQuelle": "Veuillez indiquer à quelle ligne se trouvent les en-têtes dans le fichier",
|
||||
"veuillezMapperLesColonnesSuivantes": "Veuillez mapper les colonnes suivantes",
|
||||
"voir/modifierLesParticipants": "Voir/Modifier les participants",
|
||||
"voirLesStatues": "Voir les statues",
|
||||
"vousNêtesPasEncoreInscrit": "Vous n'êtes pas encore inscrit ou votre inscription n'a pas encore été rentrée sur l'intranet",
|
||||
"à": "à",
|
||||
"étatDeLaDemande": "État de la demande"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"--sélectionnerUnClub--": "--Sélectionner un club--",
|
||||
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
|
||||
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--",
|
||||
"1er": "Médaille d'or",
|
||||
"2eme": "Médaille d'argent",
|
||||
"3eme": "Médaille de bronze",
|
||||
"abs.": "abs.",
|
||||
"adversaire": "Adversaire",
|
||||
"aujourdhuià": "Aujourd'hui à {{time}}",
|
||||
@@ -10,6 +13,10 @@
|
||||
"bleu": "Bleu",
|
||||
"catégorie": "Catégorie",
|
||||
"chargement": "Chargement",
|
||||
"classement": "Classement",
|
||||
"classementClub": "Classement club",
|
||||
"classementDesClub": "Classement des clubs",
|
||||
"classementFinal": "Classement final",
|
||||
"club": "Club",
|
||||
"combattant": "Combattant",
|
||||
"combattants": "Combattants",
|
||||
@@ -57,6 +64,7 @@
|
||||
"rechercheParCombattant": "Recherche par combattant",
|
||||
"rouge": "Rouge",
|
||||
"résultatDeLaCompétition": "Résultat de la compétition",
|
||||
"score": "Score",
|
||||
"scores": "Scores",
|
||||
"statistique": "Statistique",
|
||||
"tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})",
|
||||
|
||||
84
src/main/webapp/public/processor-dtmf.js
Normal file
84
src/main/webapp/public/processor-dtmf.js
Normal file
@@ -0,0 +1,84 @@
|
||||
class ProcessorDTMF extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.sampleRate = sampleRate;
|
||||
this.symbolDuration = 0.03; // 50 ms par symbole
|
||||
this.samplesPerSymbol = Math.floor(this.sampleRate * this.symbolDuration);
|
||||
this.encodeLowPrio = [];
|
||||
this.symbolSamples = [];
|
||||
this.lastBlackStep = 0;
|
||||
this.port.onmessage = (e) => {
|
||||
if (e.data.type === 'encode') {
|
||||
this.symbolSamples.push(...this.encodeSymbols(e.data.symbols));
|
||||
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
|
||||
}
|
||||
if (e.data.type === 'encodeLowPrio') {
|
||||
this.encodeLowPrio.push(e.data.symbols);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
dtmfFrequencies = [
|
||||
[697, 770, 852, 941], // Fréquences basses
|
||||
[1209, 1336, 1477, 1633] // Fréquences hautes
|
||||
];
|
||||
|
||||
encodeSymbols(symbols) {
|
||||
const samples = [];
|
||||
for (const symbol of symbols) {
|
||||
const lf = this.dtmfFrequencies[0][symbol % 4]; // Fréquence basse
|
||||
const hf = this.dtmfFrequencies[1][Math.floor(symbol / 4)]; // Fréquence haute
|
||||
// console.log(`Symbol: ${symbol}, LF: ${lf} Hz, HF: ${hf} Hz`);
|
||||
for (let i = 0; i < this.samplesPerSymbol; i++) {
|
||||
const t = i / this.sampleRate;
|
||||
const t2 = (this.lastBlackStep + i) / this.sampleRate;
|
||||
samples.push(0.5 * Math.sin(2 * Math.PI * lf * t) + 0.5 * Math.sin(2 * Math.PI * hf * t) // Signal DTMF
|
||||
+ Math.sin(2 * Math.PI * 150 * t2) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t2) + 1))); // Ajouter un signal à 150 Hz pour le "black"
|
||||
}
|
||||
this.lastBlackStep += this.samplesPerSymbol;
|
||||
|
||||
// ajouter un silence de 10 ms entre les symboles
|
||||
samples.push(...this.encodeBlack(this.sampleRate * 0.01)); // Silence
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
encodeBlack(size) {
|
||||
const samples = [];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const t = (this.lastBlackStep + i) / this.sampleRate;
|
||||
samples.push(Math.sin(2 * Math.PI * 150 * t) * (0.0625 * (Math.sin(2 * Math.PI * 0.5 * t) + 1))); // Signal à 350 Hz pour le "black"
|
||||
}
|
||||
this.lastBlackStep += size;
|
||||
this.lastBlackStep %= this.sampleRate * 2; // Réinitialiser tous les 2 secondes pour éviter les débordements
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
const output = outputs[0]; // output est un tableau de canaux (ex: [Float32Array, ...])
|
||||
const channelData = output[0]; // Accéder au premier canal (mono)
|
||||
|
||||
if (this.symbolSamples.length === 0 && this.encodeLowPrio.length > 0) {
|
||||
this.symbolSamples.push(...this.encodeSymbols(this.encodeLowPrio.shift()));
|
||||
this.symbolSamples.push(...this.encodeBlack(this.sampleRate * 0.02));
|
||||
}
|
||||
|
||||
if (this.symbolSamples.length === 0) {
|
||||
const samples = this.encodeBlack(channelData.length)
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
channelData[i] = samples[i] || 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
channelData[i] = this.symbolSamples.shift() || 0; // Prendre le prochain échantillon ou 0 si vide
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('dtmf-processor', ProcessorDTMF);
|
||||
@@ -1,14 +0,0 @@
|
||||
import Keycloak from "keycloak-js";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
const client_id = import.meta.env.VITE_CLIENT_ID;
|
||||
|
||||
const keycloak = new Keycloak({
|
||||
url: `${vite_url}/auth-api`,
|
||||
realm: "safca",
|
||||
clientId: client_id,
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default keycloak;
|
||||
115
src/main/webapp/src/assets/CategoryPreset.js
Normal file
115
src/main/webapp/src/assets/CategoryPreset.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const CategoryPreset = [
|
||||
{
|
||||
name: "Épée",
|
||||
sword: "ONE_HAND",
|
||||
shield: "NONE",
|
||||
categories: [
|
||||
{categorie: "MINI_POUSSIN", roundDuration: 30000, pauseDuration: 60000},
|
||||
{categorie: "POUSSIN", roundDuration: 30000, pauseDuration: 60000},
|
||||
{categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "MINIME", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "CADET", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 45,
|
||||
mandatoryProtection2: 13
|
||||
},
|
||||
{
|
||||
name: "Épée Bouclier",
|
||||
sword: "ONE_HAND",
|
||||
shield: "STANDARD",
|
||||
categories: [
|
||||
{categorie: "SUPER_MINI", roundDuration: 30000, pauseDuration: 60000},
|
||||
{categorie: "MINI_POUSSIN", roundDuration: 30000, pauseDuration: 60000},
|
||||
{categorie: "POUSSIN", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "CADET", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 45,
|
||||
mandatoryProtection2: 13
|
||||
},
|
||||
{
|
||||
name: "Épée Bocle",
|
||||
sword: "ONE_HAND",
|
||||
shield: "BUCKLER",
|
||||
categories: [
|
||||
{categorie: "POUSSIN", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "BENJAMIN", roundDuration: 45000, pauseDuration: 60000},
|
||||
{categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "CADET", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 45,
|
||||
mandatoryProtection2: 13
|
||||
},
|
||||
{
|
||||
name: "Épée Longue",
|
||||
sword: "TWO_HAND",
|
||||
shield: "NONE",
|
||||
categories: [
|
||||
{categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 61,
|
||||
mandatoryProtection2: 29
|
||||
},
|
||||
{
|
||||
name: "Sabre",
|
||||
sword: "SABER",
|
||||
shield: "NONE",
|
||||
categories: [
|
||||
{categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "CADET", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 47,
|
||||
mandatoryProtection2: 47
|
||||
},
|
||||
{
|
||||
name: "Sabre Bocle",
|
||||
sword: "SABER",
|
||||
shield: "BUCKLER",
|
||||
categories: [
|
||||
{categorie: "MINIME", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "CADET", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "JUNIOR", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 60000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 60000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 47,
|
||||
mandatoryProtection2: 47
|
||||
},
|
||||
{
|
||||
name: "Profight Léger",
|
||||
sword: "ONE_HAND",
|
||||
shield: "TEARDROP",
|
||||
categories: [
|
||||
{categorie: "SENIOR1", roundDuration: 120000, pauseDuration: 60000},
|
||||
{categorie: "SENIOR2", roundDuration: 120000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN1", roundDuration: 120000, pauseDuration: 60000},
|
||||
{categorie: "VETERAN2", roundDuration: 120000, pauseDuration: 60000},
|
||||
],
|
||||
mandatoryProtection1: 3647,
|
||||
mandatoryProtection2: 3647
|
||||
},
|
||||
]
|
||||
|
||||
export default CategoryPreset;
|
||||
241
src/main/webapp/src/components/FileImport.jsx
Normal file
241
src/main/webapp/src/components/FileImport.jsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, {useId, useRef, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import * as XLSX from "xlsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const parseValue = (value, type) => {
|
||||
if (value === undefined || value === null)
|
||||
return null;
|
||||
|
||||
switch (type) {
|
||||
case 'Integer':
|
||||
if (value === '')
|
||||
return null;
|
||||
const parsedInt = parseInt(value, 10);
|
||||
return isNaN(parsedInt) ? null : parsedInt;
|
||||
case 'Boolean':
|
||||
if (typeof value === 'boolean')
|
||||
return value;
|
||||
if (typeof value === 'string') {
|
||||
const lowerValue = value.toLowerCase().trim();
|
||||
if (lowerValue === 'oui' || lowerValue === 'true' || lowerValue === '1' || lowerValue === 'x') {
|
||||
return true;
|
||||
} else if (lowerValue === 'non' || lowerValue === 'false' || lowerValue === '0' || lowerValue === '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case 'Date':
|
||||
if (value === '')
|
||||
return null;
|
||||
if (typeof value === 'string') {
|
||||
const date = new Date(value);
|
||||
return isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
return null;
|
||||
case 'String':
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
export function FileImport({onDataMapped, expectedFields, textButton}) {
|
||||
const id = useId();
|
||||
const [headerLineNumber, setHeaderLineNumber] = useState(1);
|
||||
const [fileData, setFileData] = useState([]);
|
||||
const [fileHeaders, setFileHeaders] = useState([]);
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [columnMappings, setColumnMappings] = useState({});
|
||||
const fileChooser = useRef(null);
|
||||
const openMappingModal = useRef(null);
|
||||
const closeMappingModal = useRef(null);
|
||||
const openHeaderLineModal = useRef(null);
|
||||
const {t} = useTranslation();
|
||||
|
||||
|
||||
// Fonction pour trouver la meilleure correspondance
|
||||
const findBestMatch = (fileHeaders, expectedField) => {
|
||||
const fieldLabel = expectedField.label.toLowerCase();
|
||||
const fieldKey = expectedField.key.toLowerCase();
|
||||
|
||||
// Variantes possibles pour chaque champ (ex: "Nom" peut être "nom", "Nom de famille", etc.)
|
||||
const variants = {
|
||||
licence: t('fileImport.variants.licence', {returnObjects: true}),
|
||||
pays: t('fileImport.variants.pays', {returnObjects: true}),
|
||||
nom: t('fileImport.variants.nom', {returnObjects: true}),
|
||||
prenom: t('fileImport.variants.prenom', {returnObjects: true}),
|
||||
genre: t('fileImport.variants.genre', {returnObjects: true}),
|
||||
weight: t('fileImport.variants.weight', {returnObjects: true}),
|
||||
categorie: t('fileImport.variants.categorie', {returnObjects: true}),
|
||||
overCategory: t('fileImport.variants.overCategory', {returnObjects: true}),
|
||||
club: t('fileImport.variants.club', {returnObjects: true}),
|
||||
};
|
||||
|
||||
// Recherche de la meilleure correspondance
|
||||
for (const header of fileHeaders) {
|
||||
const lowerHeader = header.toLowerCase();
|
||||
if (lowerHeader === fieldLabel || lowerHeader === fieldKey || (variants[fieldKey] && variants[fieldKey].includes(lowerHeader))) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
// Aucune correspondance trouvée
|
||||
return null;
|
||||
};
|
||||
|
||||
// Gestion du fichier sélectionné
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setSelectedFile(file);
|
||||
setFileName(file.name);
|
||||
openHeaderLineModal.current.click();
|
||||
};
|
||||
|
||||
// Valider le numéro de la ligne d'en-tête et lire le fichier
|
||||
const handleHeaderLineSubmit = () => {
|
||||
if (!selectedFile) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const data = event.target.result;
|
||||
const workbook = XLSX.read(data, {type: 'binary'});
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(sheet, {header: 1});
|
||||
|
||||
// Extraire les en-têtes et les données en fonction du numéro de ligne
|
||||
const headers = jsonData[headerLineNumber - 1];
|
||||
const rows = jsonData.slice(headerLineNumber);
|
||||
setFileHeaders(headers);
|
||||
setFileData(rows);
|
||||
|
||||
// Initialiser le mapping avec pré-remplissage intelligent
|
||||
const initialMappings = {};
|
||||
expectedFields.forEach(field => {
|
||||
const bestMatch = findBestMatch(headers, field);
|
||||
initialMappings[field.key] = bestMatch || '';
|
||||
});
|
||||
setColumnMappings(initialMappings);
|
||||
|
||||
openMappingModal.current.click();
|
||||
fileChooser.current.value = '';
|
||||
};
|
||||
reader.readAsBinaryString(selectedFile);
|
||||
};
|
||||
|
||||
// Mettre à jour le mapping d'une colonne
|
||||
const handleMappingChange = (fieldKey, header) => {
|
||||
setColumnMappings({
|
||||
...columnMappings,
|
||||
[fieldKey]: header,
|
||||
});
|
||||
};
|
||||
|
||||
// Valider le mapping et envoyer les données
|
||||
const handleSubmit = () => {
|
||||
// Vérifier que tous les champs requis sont mappés
|
||||
const missingMappings = expectedFields
|
||||
.filter(field => field.mandatory && !columnMappings[field.key])
|
||||
.map(field => field.label);
|
||||
|
||||
if (missingMappings.length > 0) {
|
||||
toast.error(`${t('veuillezMapperLesColonnesSuivantes')} : ${missingMappings.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer les données mappées et parsées
|
||||
const mappedData = fileData.map(row => {
|
||||
const mappedRow = {};
|
||||
expectedFields.forEach(field => {
|
||||
const headerIndex = fileHeaders.indexOf(columnMappings[field.key]);
|
||||
const rawValue = headerIndex !== -1 ? row[headerIndex] : '';
|
||||
mappedRow[field.key] = parseValue(rawValue, field.type);
|
||||
});
|
||||
return mappedRow;
|
||||
});
|
||||
|
||||
// Envoyer les données au parent ou au backend
|
||||
onDataMapped(mappedData);
|
||||
closeMappingModal.current.click();
|
||||
};
|
||||
|
||||
const handleFileChooser = () => {
|
||||
fileChooser.current.click();
|
||||
}
|
||||
|
||||
return <div>
|
||||
<button type="button" className="btn btn-primary" onClick={handleFileChooser}>{textButton}</button>
|
||||
|
||||
<input ref={fileChooser} type="file" accept=".xlsx, .xls, .csv" onChange={handleFileChange} hidden={true}/>
|
||||
<button ref={openMappingModal} type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target={"#mappingModal" + id}
|
||||
hidden={true}>A
|
||||
</button>
|
||||
<button ref={openHeaderLineModal} type="button" className="btn btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target={"#headerLineModal" + id} hidden={true}>B
|
||||
</button>
|
||||
|
||||
<div className="modal fade" id={"mappingModal" + id} tabIndex="-1" aria-labelledby="mappingModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="mappingModalLabel">{t('importationDuFichier')} {fileName}</h1>
|
||||
<button ref={closeMappingModal} type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{t('veuillezAssocierChaqueChampàUneColonneDuFichier')} :</p>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{t('champAttendu')}</th>
|
||||
<th scope="col">{t('colonneDansLeFichier')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{expectedFields.map(field => <tr key={field.key}>
|
||||
<td>{field.label}</td>
|
||||
<td>
|
||||
<select className="form-select" value={columnMappings[field.key]}
|
||||
onChange={(e) => handleMappingChange(field.key, e.target.value)}>
|
||||
<option value="">{t('sélectionner...')}</option>
|
||||
{fileHeaders.map(header => (<option key={header} value={header}>{header}</option>))}
|
||||
</select>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSubmit}>{t('button.confirmer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id={"headerLineModal" + id} tabIndex="-1" aria-labelledby="headerLineModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="headerLineModalLabel">{t('numéroDeLaLigneDentête')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{t('veuillezIndiqueràQuelle')} :</p>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id={id + "basic-addon1"}>{t('numéroDeLigne')}</span>
|
||||
<input type="number" className="form-control" aria-describedby={id + "basic-addon1"} min="1" value={headerLineNumber}
|
||||
onChange={(e) => setHeaderLineNumber(parseInt(e.target.value) || 1)}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.annuler')}</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handleHeaderLineSubmit}>{t('button.confirmer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
190
src/main/webapp/src/components/ProtectionSelector.jsx
Normal file
190
src/main/webapp/src/components/ProtectionSelector.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const ProtectionSelector = ({
|
||||
shield = true,
|
||||
mandatoryProtection = 0, setMandatoryProtection = () => {
|
||||
}
|
||||
}) => {
|
||||
const {t} = useTranslation();
|
||||
const toggle = (bit) => {
|
||||
bit = 1 << (bit - 1);
|
||||
setMandatoryProtection(v => (v & bit ? v & ~bit : v | bit));
|
||||
};
|
||||
|
||||
const props = {
|
||||
style: ({cursor: "pointer"}),
|
||||
};
|
||||
|
||||
const propsDash = {
|
||||
style: ({cursor: "pointer"}),
|
||||
opacity: "0.8",
|
||||
stroke: "#7b8285",
|
||||
strokeDasharray: "4 3",
|
||||
strokeWidth: "1"
|
||||
};
|
||||
|
||||
const isOn = (bit) => (mandatoryProtection & (1 << (bit - 1))) !== 0;
|
||||
const color = (bit) => (isOn(bit) ? "#4ade80" : "#e5e7eb");
|
||||
|
||||
/*
|
||||
1 - 1 - Casque
|
||||
2 - 2 - Gorgerin
|
||||
3 - 4 - Coquille et Protection pelvienne
|
||||
4 - 8 - Gant main(s) armée(s)
|
||||
5 - 16 - Gant main bouclier
|
||||
6 - 32 - Plastron
|
||||
7 - 64 - Protection de bras armé(s)
|
||||
8 - 128 - Protection de bras de bouclier
|
||||
9 - 256 - Protection de jambes
|
||||
10 - 512 - Protection de genoux
|
||||
11 - 1024 - Protection de coudes
|
||||
12 - 2048 - Protection dorsale
|
||||
13 - 4096 - Protection de pieds
|
||||
*/
|
||||
return (
|
||||
<svg width="200" height="300" viewBox="0 0 160 320">
|
||||
<rect
|
||||
width="160" height="320"
|
||||
fill="#f9fafb00"
|
||||
stroke="#d1d5db"
|
||||
strokeWidth="2"
|
||||
rx="10"
|
||||
/>
|
||||
|
||||
{/* Casque */}
|
||||
<ellipse {...props}
|
||||
cx="80" cy="35" rx="20" ry="22"
|
||||
fill={color(1)}
|
||||
onClick={() => toggle(1)}
|
||||
><title>{t('casque')}</title></ellipse>
|
||||
|
||||
{/* Gorgerin */}
|
||||
<ellipse {...props}
|
||||
cx="80" cy="65" rx="12" ry="8"
|
||||
fill={color(2)}
|
||||
onClick={() => toggle(2)}
|
||||
><title>{t('gorgerin')}</title></ellipse>
|
||||
|
||||
{/* Plastron */}
|
||||
<ellipse {...props}
|
||||
cx="80" cy="118" rx="30" ry="45"
|
||||
fill={color(6)}
|
||||
onClick={() => toggle(6)}
|
||||
><title>{t('plastron')}</title></ellipse>
|
||||
|
||||
{/* Protection dorsale */}
|
||||
<ellipse {...propsDash}
|
||||
cx="80" cy="118" rx="10" ry="35"
|
||||
fill={color(12)}
|
||||
onClick={() => toggle(12)}
|
||||
><title>{t('protectionDorsale')}</title></ellipse>
|
||||
|
||||
{/* Protection de bras armé(s) */}
|
||||
<ellipse {...props}
|
||||
cx="38" cy="118" rx="12" ry="40"
|
||||
fill={color(7)}
|
||||
onClick={() => toggle(7)}
|
||||
><title>{shield ? t('protectionDeBrasArmé') : t('protectionDeBras')}</title></ellipse>
|
||||
{/* Protection de bras de bouclier */}
|
||||
<ellipse {...props}
|
||||
cx="122" cy="118" rx="12" ry="40"
|
||||
fill={color(shield ? 8 : 7)}
|
||||
onClick={() => toggle(shield ? 8 : 7)}
|
||||
><title>{shield ? t('protectionDeBrasDeBouclier') : t('protectionDeBras')}</title></ellipse>
|
||||
|
||||
{/* Protection de coudes */}
|
||||
<ellipse {...propsDash}
|
||||
cx="38" cy="118" rx="12" ry="12"
|
||||
fill={color(11)}
|
||||
onClick={() => toggle(11)}
|
||||
><title>{t('protectionDeCoudes')}</title></ellipse>
|
||||
<ellipse {...propsDash}
|
||||
cx="122" cy="118" rx="12" ry="12"
|
||||
fill={color(11)}
|
||||
onClick={() => toggle(11)}
|
||||
><title>{t('protectionDeCoudes')}</title></ellipse>
|
||||
|
||||
{/* Gant main(s) armée(s) */}
|
||||
<ellipse {...props}
|
||||
cx="38" cy="170" rx="10" ry="12"
|
||||
fill={color(4)}
|
||||
onClick={() => toggle(4)}
|
||||
><title>{shield ? t('gantMainsArmées') : t('gants')}</title></ellipse>
|
||||
{/* Gant main bouclier */}
|
||||
<ellipse {...props}
|
||||
cx="122" cy="170" rx="10" ry="12"
|
||||
fill={color(shield ? 5 : 4)}
|
||||
onClick={() => toggle(shield ? 5 : 4)}
|
||||
><title>{shield ? t('gantMainBouclier') : t('gants')} </title></ellipse>
|
||||
|
||||
{/* Protection de jambes */}
|
||||
<ellipse {...props}
|
||||
cx="65" cy="230" rx="14" ry="55"
|
||||
fill={color(9)}
|
||||
onClick={() => toggle(9)}
|
||||
><title>{t('protectionDeJambes')}</title></ellipse>
|
||||
<ellipse {...props}
|
||||
cx="95" cy="230" rx="14" ry="55"
|
||||
fill={color(9)}
|
||||
onClick={() => toggle(9)}
|
||||
><title>{t('protectionDeJambes')}</title></ellipse>
|
||||
|
||||
{/* Protection de genoux */}
|
||||
<ellipse {...propsDash}
|
||||
cx="65" cy="230" rx="14" ry="14"
|
||||
fill={color(10)}
|
||||
onClick={() => toggle(10)}
|
||||
><title>{t('protectionDeGenoux')}</title></ellipse>
|
||||
<ellipse {...propsDash}
|
||||
cx="95" cy="230" rx="14" ry="14"
|
||||
fill={color(10)}
|
||||
onClick={() => toggle(10)}
|
||||
><title>{t('protectionDeGenoux')}</title></ellipse>
|
||||
|
||||
{/* Coquille et Protection pelvienne */}
|
||||
<ellipse {...props}
|
||||
cx="80" cy="170" rx="20" ry="10"
|
||||
fill={color(3)}
|
||||
onClick={() => toggle(3)}
|
||||
><title>{t('coquilleProtectionPelvienne')}</title></ellipse>
|
||||
|
||||
{/* Protection de pieds */}
|
||||
<ellipse {...props}
|
||||
cx="65" cy="295" rx="16" ry="8"
|
||||
fill={color(13)}
|
||||
onClick={() => toggle(13)}
|
||||
><title>{t('protectionDePieds')}</title></ellipse>
|
||||
<ellipse {...props}
|
||||
cx="95" cy="295" rx="16" ry="8"
|
||||
fill={color(13)}
|
||||
onClick={() => toggle(13)}
|
||||
><title>{t('protectionDePieds')}</title></ellipse>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProtectionSelector;
|
||||
|
||||
|
||||
export function getMandatoryProtectionsList(mandatoryProtection, shield, t) {
|
||||
const protections = [];
|
||||
const isOn = (bit) => (mandatoryProtection & (1 << (bit - 1))) !== 0;
|
||||
|
||||
if (isOn(1)) protections.push(t('casque', {ns: "common"}));
|
||||
if (isOn(2)) protections.push(t('gorgerin', {ns: "common"}));
|
||||
if (isOn(3)) protections.push(t('coquilleProtectionPelvienne', {ns: "common"}));
|
||||
if (isOn(4) && !shield) protections.push(t('gants', {ns: "common"}));
|
||||
if (isOn(4) && shield) protections.push(t('gantMainsArmées', {ns: "common"}));
|
||||
if (isOn(5) && shield) protections.push(t('gantMainBouclier', {ns: "common"}));
|
||||
if (isOn(6)) protections.push(t('plastron', {ns: "common"}));
|
||||
if (isOn(7) && !shield) protections.push(t('protectionDeBras', {ns: "common"}));
|
||||
if (isOn(7) && shield) protections.push(t('protectionDeBrasArmé', {ns: "common"}));
|
||||
if (isOn(8) && shield) protections.push(t('protectionDeBrasDeBouclier', {ns: "common"}));
|
||||
if (isOn(9)) protections.push(t('protectionDeJambes', {ns: "common"}));
|
||||
if (isOn(10)) protections.push(t('protectionDeGenoux', {ns: "common"}));
|
||||
if (isOn(11)) protections.push(t('protectionDeCoudes', {ns: "common"}));
|
||||
if (isOn(12)) protections.push(t('protectionDorsale', {ns: "common"}));
|
||||
if (isOn(13)) protections.push(t('protectionDePieds', {ns: "common"}));
|
||||
|
||||
return protections;
|
||||
}
|
||||
207
src/main/webapp/src/components/cm/AudioEncoder.jsx
Normal file
207
src/main/webapp/src/components/cm/AudioEncoder.jsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useTablesState} from "../../pages/competition/editor/StateWindow.jsx";
|
||||
import {timePrint} from "../../utils/Tools.js";
|
||||
|
||||
let initialized = false;
|
||||
const AudioEncoder = () => {
|
||||
const audioContextRef = useRef(null);
|
||||
const qpskProcessorRef = useRef(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [table, setTable] = useState('1');
|
||||
|
||||
const lastSend = useRef({id: 0});
|
||||
const {state} = useTablesState();
|
||||
|
||||
// Initialisation de l'AudioContext et du AudioWorklet
|
||||
useEffect(() => {
|
||||
const initAudio = async () => {
|
||||
if (initialized)
|
||||
return;
|
||||
console.log("Initialisation de l'audio après interaction utilisateur");
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
console.log("AudioContext state:", audioContext.state);
|
||||
|
||||
if (audioContext.state === 'suspended') {
|
||||
await audioContext.resume(); // Nécessaire pour démarrer le contexte audio
|
||||
console.log("AudioContext resumed");
|
||||
}
|
||||
|
||||
await audioContext.audioWorklet.addModule('/processor-dtmf.js');
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
const processor = new AudioWorkletNode(audioContext, 'dtmf-processor');
|
||||
processor.connect(audioContext.destination);
|
||||
qpskProcessorRef.current = processor;
|
||||
audioContextRef.current = audioContext;
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
setIsReady(true);
|
||||
} catch (err) {
|
||||
initialized = false;
|
||||
console.error("Erreur d'initialisation AudioWorklet:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialiser après un clic utilisateur (pour contourner les restrictions des navigateurs)
|
||||
const handleUserInteraction = () => {
|
||||
document.removeEventListener('click', handleUserInteraction);
|
||||
initAudio();
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleUserInteraction);
|
||||
return () => {
|
||||
if (audioContextRef.current?.state !== 'closed') {
|
||||
audioContextRef.current?.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fonction pour encoder et envoyer un message
|
||||
const encodeAndSend = (data, lowPrio = false) => {
|
||||
if (!isReady) return;
|
||||
|
||||
const symbols = Array.from(data).flatMap(byte => [byte >> 4, byte & 0x0F]);
|
||||
console.log("Bits :", symbols);
|
||||
|
||||
// 5. Envoyer les symboles au processeur audio
|
||||
if (lowPrio) {
|
||||
qpskProcessorRef.current.port.postMessage({type: 'encodeLowPrio', symbols});
|
||||
} else {
|
||||
qpskProcessorRef.current.port.postMessage({type: 'encode', symbols});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const t = state.find(o => o.liceName === table)
|
||||
if (!t)
|
||||
return
|
||||
|
||||
// console.log("Data for table 1:", t, t.selectedMatch)
|
||||
const last = lastSend.current;
|
||||
if (t.selectedMatch !== last.id) {
|
||||
clearTimeout(last.time_id)
|
||||
last.time_id = setTimeout(() => {
|
||||
last.time_id = null
|
||||
if (t.selectedMatch === null) {
|
||||
encodeAndSend(new Uint8Array([0, 0, 0, 0, 0, 0, 0]));
|
||||
} else {
|
||||
const data = [];
|
||||
for (let i = 0; i < 7; i++) { // MaxSafeInteger est sur 7 bytes (53 bits de précision)
|
||||
data.unshift(Number((BigInt(t.selectedMatch) >> BigInt(i * 8)) & 0xFFn))
|
||||
}
|
||||
data[0] = data[0] & 0x1F // 3 premiers bits à 0 pour différencier des autres types de messages (ex: score, chrono, etc.)
|
||||
// console.log("Data to send (selectedMatch):", data)
|
||||
encodeAndSend(new Uint8Array(data), false);
|
||||
}
|
||||
}, 250)
|
||||
|
||||
last.id = t.selectedMatch
|
||||
}
|
||||
|
||||
const isRunning = (c) => c.startTime !== 0
|
||||
const getTime = (c) => {
|
||||
if (c.startTime === 0)
|
||||
return c.time
|
||||
return c.time + Date.now() - c.startTime
|
||||
}
|
||||
|
||||
const timeStr = last.chronoState ? timePrint((last.chronoState.state === 2) ? last.chronoState.configPause : last.chronoState.configTime - getTime(last.chronoState)) : "-"
|
||||
const timeStr2 = timePrint((t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState))
|
||||
|
||||
if (timeStr !== timeStr2) {
|
||||
clearInterval(lastSend.current.time_chronoInter)
|
||||
clearTimeout(lastSend.current.time_chronoText)
|
||||
lastSend.current.time_chronoText = setTimeout(() => {
|
||||
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
|
||||
const ms = time % 1000
|
||||
time = (time - ms) / 1000
|
||||
|
||||
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
|
||||
// console.log("Data to send (time):", data)
|
||||
encodeAndSend(new Uint8Array(data));
|
||||
|
||||
lastSend.current.time_chronoInter = setInterval(() => {
|
||||
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
|
||||
const ms = time % 1000
|
||||
time = (time - ms) / 1000
|
||||
|
||||
const data = [((time >> 8) & 0x1F) + 0x20, time & 0xFF];
|
||||
// console.log("Data to send (time-auto):", data)
|
||||
encodeAndSend(new Uint8Array(data), true);
|
||||
}, 10000);
|
||||
}, 150)
|
||||
}
|
||||
|
||||
if (!last.chronoState || last.chronoState.state !== t.chronoState.state || isRunning(last.chronoState) !== isRunning(t.chronoState)) {
|
||||
let time = (t.chronoState.state === 2) ? t.chronoState.configPause : t.chronoState.configTime - getTime(t.chronoState)
|
||||
const ms = Math.round((time % 1000) / 250)
|
||||
|
||||
const data = [0x40 + (t.chronoState.state << 3) + (isRunning(t.chronoState) << 2) + (ms & 0x03)];
|
||||
// console.log("Data to send (chrono state):", data)
|
||||
encodeAndSend(new Uint8Array(data));
|
||||
}
|
||||
|
||||
last.chronoState = {...t.chronoState}
|
||||
|
||||
// console.log(timeStr, timeStr2)
|
||||
// console.log(last.chronoState, t.chronoState)
|
||||
|
||||
if (last.scoreRouge !== t.scoreState.scoreRouge) {
|
||||
clearTimeout(last.time_sr)
|
||||
last.time_sr = setTimeout(() => {
|
||||
if (last.scoreRouge !== t.scoreState.scoreRouge) {
|
||||
const b = t.scoreState.scoreRouge < 0
|
||||
const s = b ? -t.scoreState.scoreRouge : t.scoreState.scoreRouge
|
||||
const data = [0x60 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
|
||||
console.log("Data to send (score r):", data)
|
||||
encodeAndSend(new Uint8Array(data), true);
|
||||
last.scoreRouge = t.scoreState.scoreRouge
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
if (last.scoreBleu !== t.scoreState.scoreBleu) {
|
||||
clearTimeout(last.time_sb)
|
||||
last.time_sb = setTimeout(() => {
|
||||
if (last.scoreBleu !== t.scoreState.scoreBleu) {
|
||||
const b = t.scoreState.scoreBleu < 0
|
||||
const s = b ? -t.scoreState.scoreBleu : t.scoreState.scoreBleu
|
||||
const data = [0x60 + 0x10 + (b << 3) + ((s >> 8) & 0x07), (s & 0xFF)];
|
||||
console.log("Data to send (score b):", data)
|
||||
encodeAndSend(new Uint8Array(data), true);
|
||||
last.scoreBleu = t.scoreState.scoreBleu
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
}, [state])
|
||||
|
||||
useEffect(() => {
|
||||
const last = lastSend.current;
|
||||
clearTimeout(last.scoreBleu)
|
||||
clearTimeout(last.scoreRouge)
|
||||
clearTimeout(last.time_id)
|
||||
clearTimeout(last.time_chronoText)
|
||||
clearInterval(last.time_chronoInter)
|
||||
last.id = 0
|
||||
last.scoreBleu = 0
|
||||
last.scoreRouge = 0
|
||||
last.chronoState = null
|
||||
}, [table])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={table}
|
||||
onChange={(e) => setTable(e.target.value)}
|
||||
placeholder="Nom de la zone"
|
||||
/>
|
||||
<span>{isReady ? 'Actif' : "Zone non configurée"}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioEncoder;
|
||||
802
src/main/webapp/src/components/cm/AutoCatModalContent.jsx
Normal file
802
src/main/webapp/src/components/cm/AutoCatModalContent.jsx
Normal file
@@ -0,0 +1,802 @@
|
||||
import React, {useEffect, useId, useState} from "react";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {useCountries} from "../../hooks/useCountries.jsx";
|
||||
import {ListPresetSelect} from "./ListPresetSelect.jsx";
|
||||
import {CatList, getCatName} from "../../utils/Tools.js";
|
||||
import {useCombs} from "../../hooks/useComb.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {build_tree} from "../../utils/TreeUtils.js";
|
||||
import {createMatch} from "../../utils/CompetitionTools.js";
|
||||
import {useRequestWS, useWS} from "../../hooks/useWS.jsx";
|
||||
import {AxiosError} from "../AxiosError.jsx";
|
||||
|
||||
export function AutoCatModalContent({data, groups, setGroups, defaultPreset = -1}) {
|
||||
const country = useCountries('fr')
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [country_, setCountry_] = useState("")
|
||||
const [gender, setGender] = useState({H: true, F: true, NA: true})
|
||||
const [cat, setCat] = useState([])
|
||||
const [weightMin, setWeightMin] = useState(0)
|
||||
const [weightMax, setWeightMax] = useState(0)
|
||||
const [team, setTeam] = useState(false)
|
||||
const [preset, setPreset] = useState(-1)
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(defaultPreset)
|
||||
}, [defaultPreset])
|
||||
|
||||
const setCat_ = (e, index) => {
|
||||
if (e.target.checked) {
|
||||
if (!cat.includes(index)) {
|
||||
setCat([...cat, index])
|
||||
}
|
||||
} else {
|
||||
setCat(cat.filter(c => c !== index))
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter(dataIn, dataOut) {
|
||||
dataIn.forEach(comb_ => {
|
||||
const comb = data.find(d => d.id === comb_.id);
|
||||
if (comb == null)
|
||||
return;
|
||||
if ((country_ === "" || comb.country === country_)
|
||||
&& (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
|
||||
&& (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)))
|
||||
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin)
|
||||
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)
|
||||
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team)
|
||||
&& (preset === -1 || comb.categoriesInscrites.includes(preset))) {
|
||||
dataOut.push(comb)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const dispoFiltered = [];
|
||||
if (data != null)
|
||||
applyFilter(data, dispoFiltered);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const toReplace = makePoule(dispoFiltered, groups);
|
||||
setGroups(prev => [...prev.filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]);
|
||||
}
|
||||
|
||||
const handleReplace = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const toReplace = makePoule(dispoFiltered, []);
|
||||
setGroups(prev => [...prev.map(g => ({id: g.id, poule: "-"})).filter(g => !toReplace.some(r => r.id === g.id)), ...toReplace]);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="autoCatModalLabel">{t('ajoutAutomatique')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div style={{width: "12em"}}>
|
||||
<label htmlFor="inputState0" className="form-label">{t('pays')}</label>
|
||||
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
|
||||
<option value={""}>{t('--Tous--')}</option>
|
||||
{country && Object.keys(country).sort((a, b) => {
|
||||
if (a < b) return -1
|
||||
if (a > b) return 1
|
||||
return 0
|
||||
}).map((key, _) => {
|
||||
return (<option key={key} value={key}>{country[key]}</option>)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ListPresetSelect value={preset} onChange={setPreset}/>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap justify-content-around mb-3">
|
||||
<div>
|
||||
<label className="form-label">{t('genre')}</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, H: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
|
||||
</div>
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, F: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, NA: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">{t('team')}</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={team}
|
||||
onChange={e => setTeam(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">{t('team')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="input5" className="form-label">{t('poids')}</label>
|
||||
<div className="row-cols-sm-auto d-flex align-items-center">
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
|
||||
name="999"
|
||||
onChange={e => setWeightMin(Number(e.target.value))}/></div>
|
||||
<div><span>{t('select.à')}</span></div>
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999"
|
||||
onChange={e => setWeightMax(Number(e.target.value))}/></div>
|
||||
<div><small>{t('select.msg1')}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div className="d-flex flex-wrap mb-3">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorie')} :
|
||||
</label>
|
||||
{CatList.map((cat_, index) => {
|
||||
return <div key={index} className="input-group"
|
||||
style={{display: "contents"}}>
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox"
|
||||
id={"categoriesInput" + index} checked={cat.includes(index)} aria-label={getCatName(cat_)}
|
||||
onChange={e => setCat_(e, index)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{getCatName(cat_)}</label>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>{dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')} {dispoFiltered.length > 10 &&
|
||||
<span style={{color: "red"}}>{t('uneCatégorieNePeutContenirPlusDe10Combattants')}</span>}</span>
|
||||
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}
|
||||
disabled={dispoFiltered.length <= 0 || dispoFiltered.length > 10}>{t('ajouter')}</button>
|
||||
<button type="submit" className="btn btn-warning" data-bs-dismiss="modal" onClick={handleReplace}
|
||||
disabled={dispoFiltered.length <= 0 || dispoFiltered.length > 10}>{t('remplacer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
function makePoule(combIn, groups) {
|
||||
combIn = combIn.sort(() => Math.random() - 0.5);
|
||||
const maxInPoule = Math.ceil(combIn.length / 2);
|
||||
const out = []
|
||||
|
||||
const pa = [];
|
||||
const pb = [];
|
||||
|
||||
let nameA;
|
||||
let nameB;
|
||||
groups.forEach(g => {
|
||||
const existsInCombIn = combIn.some(c => c.id === g.id);
|
||||
if (existsInCombIn) {
|
||||
if ((pa.length === 0 || g.poule === nameA) && pa.length < maxInPoule) {
|
||||
nameA = g.poule || "1";
|
||||
pa.push(g.id);
|
||||
} else if ((pb.length === 0 || g.poule === nameB) && pb.length < maxInPoule) {
|
||||
if (!(nameA === (g.poule || (nameA === "1" ? "2" : "1")))) {
|
||||
nameB = g.poule || (nameA === "1" ? "2" : "1");
|
||||
pb.push(g.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
nameA = nameA || (nameB === "1" ? "2" : "1");
|
||||
nameB = nameB || (nameA === "1" ? "2" : "1");
|
||||
|
||||
if (combIn.length <= 5) {
|
||||
combIn.forEach(c => {
|
||||
if (!pa.includes(c.id))
|
||||
pa.push(c.id)
|
||||
});
|
||||
} else {
|
||||
for (const c of combIn) {
|
||||
if (pa.includes(c.id) || pb.includes(c.id))
|
||||
continue;
|
||||
|
||||
const club = c.club_str || (c.teamMembers && c.teamMembers[0].club_str) || "";
|
||||
|
||||
const countInPa = pa.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
|
||||
const countInPb = pb.filter(p => (p.club_str || (p.teamMembers && p.teamMembers[0].club_str) || "") === club).length;
|
||||
|
||||
if (pa.length < maxInPoule && (countInPa <= countInPb || pb.length >= maxInPoule)) {
|
||||
pa.push(c.id);
|
||||
} else if (pb.length < maxInPoule) {
|
||||
pb.push(c.id);
|
||||
} else {
|
||||
pa.push(c.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pa.forEach(id => out.push({id: id, poule: nameA}));
|
||||
pb.forEach(id => out.push({id: id, poule: nameB}));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function makeWeightCategories(combs) {
|
||||
combs = combs.filter(c => c.weight != null).sort((a, b) => a.weight - b.weight); // Add random for same weight ?
|
||||
const catCount = Math.ceil(combs.length / 10);
|
||||
const catSize = combs.length / catCount;
|
||||
const catMaxSize = Math.min(Math.ceil(catSize), 10);
|
||||
const catMinSize = Math.max(Math.floor(catSize), 3); // Add marge ?
|
||||
|
||||
const categories = Array.from({length: catCount}, () => []);
|
||||
for (let i = 0; i < combs.length; i++) {
|
||||
categories[Math.floor(i / catSize)].push(combs[i]);
|
||||
}
|
||||
|
||||
let change = false;
|
||||
let maxIterations = 500;
|
||||
do {
|
||||
change = false;
|
||||
|
||||
// ------ move in upper direction if better and possible ------
|
||||
|
||||
let needFree = -1;
|
||||
let dIfFree = 0;
|
||||
for (let i = 0; i < catCount - 1; i++) {
|
||||
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
|
||||
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
|
||||
|
||||
if (weightDiff > nextWeightDiff && categories.at(i).length > catMinSize) {
|
||||
if (categories.at(i + 1).length < catMaxSize) {
|
||||
const movedComb = categories.at(i).pop();
|
||||
categories.at(i + 1).unshift(movedComb);
|
||||
change = true;
|
||||
} else if (weightDiff - nextWeightDiff > dIfFree) {
|
||||
needFree = i;
|
||||
dIfFree = weightDiff - nextWeightDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needFree !== -1) {
|
||||
let haveSpace = -1;
|
||||
let maxDiff = 0;
|
||||
for (let i = needFree + 1; i < catCount; i++) {
|
||||
if (categories.at(i).length < catMaxSize) {
|
||||
haveSpace = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (haveSpace !== -1) {
|
||||
for (let i = needFree + 1; i < haveSpace; i++) {
|
||||
const weightDiff = categories.at(i).at(-1).weight - categories.at(i).at(-2).weight;
|
||||
const nextWeightDiff = categories.at(i + 1).at(0).weight - categories.at(i).at(-1).weight;
|
||||
const diffIfFree = weightDiff - nextWeightDiff;
|
||||
if (diffIfFree > maxDiff) {
|
||||
maxDiff = diffIfFree;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDiff < dIfFree) {
|
||||
for (let i = needFree; i < haveSpace; i++) {
|
||||
const movedComb = categories.at(i).pop();
|
||||
categories.at(i + 1).unshift(movedComb);
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------ move in lower direction if better and possible ------
|
||||
|
||||
needFree = -1;
|
||||
dIfFree = 0;
|
||||
for (let i = 1; i < catCount; i++) {
|
||||
const currentFirst = categories[i][0];
|
||||
const currentSecondFirst = categories[i][1];
|
||||
const prevLast = categories[i - 1][categories[i - 1].length - 1];
|
||||
|
||||
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
|
||||
const prevWeightDiff = currentFirst.weight - prevLast.weight;
|
||||
|
||||
if (weightDiff > prevWeightDiff && categories.at(i).length > catMinSize) {
|
||||
if (categories.at(i - 1).length < catMaxSize) {
|
||||
const movedComb = categories.at(i).shift();
|
||||
categories.at(i - 1).push(movedComb);
|
||||
change = true;
|
||||
} else if (weightDiff - prevWeightDiff > dIfFree) {
|
||||
needFree = i;
|
||||
dIfFree = weightDiff - prevWeightDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needFree !== -1) {
|
||||
let haveSpace = -1;
|
||||
let maxDiff = 0;
|
||||
for (let i = needFree - 1; i >= 0; i--) {
|
||||
if (categories.at(i).length < catMaxSize) {
|
||||
haveSpace = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (haveSpace !== -1) {
|
||||
for (let i = needFree - 1; i > haveSpace; i--) {
|
||||
const currentFirst = categories[i][0];
|
||||
const currentSecondFirst = categories[i][1];
|
||||
const prevLast = categories[i - 1][categories[i - 1].length - 1];
|
||||
|
||||
const weightDiff = currentSecondFirst.weight - currentFirst.weight;
|
||||
const prevWeightDiff = currentFirst.weight - prevLast.weight;
|
||||
|
||||
const diffIfFree = weightDiff - prevWeightDiff;
|
||||
if (diffIfFree > maxDiff) {
|
||||
maxDiff = diffIfFree;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDiff < dIfFree) {
|
||||
for (let i = needFree; i > haveSpace; i--) {
|
||||
const movedComb = categories.at(i).shift();
|
||||
categories.at(i - 1).push(movedComb);
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (change && maxIterations-- > 0);
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
const getCatNameList = (count) => {
|
||||
const catNameList = [];
|
||||
if (count >= 10) catNameList.push("Paille");
|
||||
if (count >= 9) catNameList.push("Mouche");
|
||||
if (count >= 8) catNameList.push("Coq");
|
||||
if (count >= 7) catNameList.push("Plume");
|
||||
if (count >= 2) catNameList.push("Léger");
|
||||
if (count >= 5) catNameList.push("Mi-moyen");
|
||||
if (count >= 3) catNameList.push("Moyen");
|
||||
if (count >= 6) catNameList.push("Mi-lourd");
|
||||
if (count >= 1) catNameList.push("Lourd");
|
||||
if (count >= 4) catNameList.push("Super-lourd");
|
||||
|
||||
return catNameList;
|
||||
}
|
||||
|
||||
function makeCategory(combs) {
|
||||
const out = Array.from(CatList, (v, i) => ({
|
||||
h: combs.filter(c => c.categorie === v && c.genre !== "F"),
|
||||
f: combs.filter(c => c.categorie === v && c.genre === "F"),
|
||||
m: [],
|
||||
canMakeGenreFusion: i <= CatList.indexOf("BENJAMIN"),
|
||||
c: v,
|
||||
c_index: i,
|
||||
min_c_index: i,
|
||||
done: false
|
||||
}))
|
||||
|
||||
for (let i = 0; i < out.length; i++)
|
||||
out[i].done = out[i].h.length === 0 && out[i].f.length === 0;
|
||||
|
||||
for (let i = 0; i < out.length - 1; i++) {
|
||||
const p = i === 0 ? undefined : out[i - 1];
|
||||
const c = out[i];
|
||||
const n = out[i + 1];
|
||||
|
||||
if (c.done)
|
||||
continue;
|
||||
if (c.canMakeGenreFusion) {
|
||||
if (c.f.length < 6 || c.h.length < 5) {
|
||||
if (c.f.length + c.h.length >= 3) {
|
||||
c.m = c.h.concat(c.f);
|
||||
c.h = [];
|
||||
c.f = [];
|
||||
c.done = true;
|
||||
} else {
|
||||
n.h = n.h.concat(c.h);
|
||||
n.f = n.f.concat(c.f);
|
||||
n.min_c_index = c.min_c_index
|
||||
c.h = [];
|
||||
c.f = [];
|
||||
c.done = true;
|
||||
}
|
||||
} else {
|
||||
c.done = true;
|
||||
}
|
||||
} else {
|
||||
if (c.h.length < 3 && c.h.length > 0) {
|
||||
if (p) {
|
||||
if (p.h.length > 0 && p.h.length + c.h.length <= c.h.length + n.h.length && p.min_c_index - p.c_index < 1) {
|
||||
p.h = p.h.concat(c.h);
|
||||
c.h = [];
|
||||
} else {
|
||||
n.h = n.h.concat(c.h);
|
||||
c.h = [];
|
||||
}
|
||||
} else {
|
||||
n.h = n.h.concat(c.h);
|
||||
c.h = [];
|
||||
}
|
||||
}
|
||||
if (c.f.length < 3 && c.f.length > 0) {
|
||||
if (p) {
|
||||
if (p.f.length > 0 && p.f.length + c.f.length <= c.f.length + n.f.length && p.min_c_index - p.c_index < 1) {
|
||||
p.f = p.f.concat(c.f);
|
||||
c.f = [];
|
||||
} else {
|
||||
n.f = n.f.concat(c.f);
|
||||
c.f = [];
|
||||
}
|
||||
} else {
|
||||
n.f = n.f.concat(c.f);
|
||||
c.f = [];
|
||||
}
|
||||
}
|
||||
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Down fusion if not done
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const p = out[i - 1];
|
||||
const c = out[i];
|
||||
|
||||
if (c.done)
|
||||
continue;
|
||||
if (c.h.length > 0 && c.h.length < 3) {
|
||||
p.h = p.h.concat(c.h);
|
||||
c.h = [];
|
||||
}
|
||||
if (c.f.length > 0 && c.f.length < 3) {
|
||||
p.f = p.f.concat(c.f);
|
||||
c.f = [];
|
||||
}
|
||||
c.done = (c.h.length >= 3 || c.h.length === 0) && (c.f.length >= 3 || c.f.length === 0);
|
||||
p.done = (p.h.length >= 3 || p.h.length === 0) && (p.f.length >= 3 || p.f.length === 0);
|
||||
}
|
||||
|
||||
return out.map(c => [c.h, c.f, c.m]).flat().filter(l => l.length > 0);
|
||||
}
|
||||
|
||||
function sendCatList(toastId, t, catList, sendRequest) {
|
||||
toastId.current = toast(t('créationDeLaLesCatégories'), {progress: 0});
|
||||
|
||||
new Promise(async (resolve) => {
|
||||
for (let i = 0; i < catList.length; i++) {
|
||||
const progress = (i + 1) / catList.length;
|
||||
toast.update(toastId.current, {progress});
|
||||
|
||||
const g = []
|
||||
if (catList[i].combs.some(c => c.genre === "H")) g.push('H');
|
||||
if (catList[i].combs.some(c => c.genre === "F")) g.push('F');
|
||||
|
||||
const cat = []
|
||||
catList[i].combs.forEach(c => {
|
||||
if (!cat.includes(c.categorie))
|
||||
cat.push(c.categorie);
|
||||
})
|
||||
|
||||
const type = catList[i].combs.length > 5 && catList[i].classement ? 3 : 1;
|
||||
const newCat = {
|
||||
name: catList[i].preset.name + " - " + cat.map(c => getCatName(c)).join(", ") +
|
||||
(g.length === 2 ? "" : " - " + g.join("/")) + (catList[i].size === 1 ? "" : " - " + getCatNameList(catList[i].size)[catList[i].index]),
|
||||
liceName: catList[i].lice,
|
||||
type: type,
|
||||
treeAreClassement: catList[i].classement,
|
||||
fullClassement: catList[i].fullClassement,
|
||||
preset: {id: catList[i].preset.id}
|
||||
}
|
||||
console.log(newCat)
|
||||
|
||||
await sendRequest('createOrReplaceCategory', newCat).then(id => {
|
||||
newCat["id"] = id;
|
||||
const groups = makePoule(catList[i].combs, []);
|
||||
const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(newCat, [], groups);
|
||||
|
||||
const p = [];
|
||||
p.push(sendRequest("recalculateMatch", {
|
||||
categorie: newCat.id,
|
||||
newMatch,
|
||||
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
|
||||
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
|
||||
matchesToRemove: []
|
||||
}).then(() => {
|
||||
console.log("Finished creating matches for category", newCat.name);
|
||||
}).catch(err => {
|
||||
console.error("Error creating matches for category", newCat.name, err);
|
||||
}))
|
||||
|
||||
if (type === 3) {
|
||||
const trees = build_tree(4, 1)
|
||||
console.log("Creating trees for new category:", trees);
|
||||
|
||||
p.push(sendRequest('updateTrees', {
|
||||
categoryId: id,
|
||||
trees: trees
|
||||
}).then(() => {
|
||||
console.log("Finished creating trees for category", newCat.name);
|
||||
}).catch(err => {
|
||||
console.error("Error creating trees for category", newCat.name, err);
|
||||
}))
|
||||
}
|
||||
|
||||
return Promise.allSettled(p)
|
||||
}).catch(err => {
|
||||
console.error("Error creating category", newCat.name, err);
|
||||
})
|
||||
console.log("Finished category", i + 1, "/", catList.length);
|
||||
}
|
||||
resolve();
|
||||
}).finally(() => {
|
||||
toast.done(toastId.current);
|
||||
})
|
||||
}
|
||||
|
||||
export function AutoNewCatModalContent() {
|
||||
const {t} = useTranslation("cm");
|
||||
const {combs} = useCombs();
|
||||
const {sendRequest} = useWS();
|
||||
const toastId = React.useRef(null);
|
||||
|
||||
const [gender, setGender] = useState({H: false, F: false, NA: false})
|
||||
const [cat, setCat] = useState([])
|
||||
const [preset, setPreset] = useState(undefined)
|
||||
const [lice, setLice] = useState("1")
|
||||
const [classement, setClassement] = useState(true)
|
||||
const [fullClassement, setFullClassement] = useState(false)
|
||||
|
||||
const setCat_ = (e, index) => {
|
||||
if (e.target.checked) {
|
||||
if (!cat.includes(index)) {
|
||||
setCat([...cat, index])
|
||||
}
|
||||
} else {
|
||||
setCat(cat.filter(c => c !== index))
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter(dataIn, dataOut) {
|
||||
dataIn.forEach(comb => {
|
||||
if (comb == null)
|
||||
return;
|
||||
if ((gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
|
||||
&& (cat.includes(Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)))
|
||||
&& (preset === undefined || comb.categoriesInscrites?.includes(preset.id))) {
|
||||
dataOut.push(comb)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const dispoFiltered = [];
|
||||
if (combs != null)
|
||||
applyFilter(Object.values(combs), dispoFiltered);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let catList
|
||||
if (dispoFiltered.length > 10) {
|
||||
catList = makeWeightCategories(dispoFiltered);
|
||||
} else {
|
||||
catList = [[...dispoFiltered]];
|
||||
}
|
||||
console.log(catList.map(c => c.map(c => ({id: c.id, weight: c.weight, fname: c.fname, lname: c.lname}))))
|
||||
|
||||
sendCatList(toastId, t, catList
|
||||
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})), sendRequest);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="autoNewCatModalLabel">{t('depuisUneCatégoriePrédéfinie')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="d-flex flex-wrap justify-content-around">
|
||||
<ListPresetSelect value={preset} onChange={setPreset} returnId={false}/>
|
||||
|
||||
<div>
|
||||
<label className="form-label">{t('genre')}</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, H: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">{t('genre.h')}</label>
|
||||
</div>
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, F: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck2">{t('genre.f')}</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, NA: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck3">{t('genre.na')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{preset !== undefined && <>
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div className="d-flex flex-wrap mb-3">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorie')} :
|
||||
</label>
|
||||
{preset.categories.map(c => [c.categorie, CatList.indexOf(c.categorie)]).sort((a, b) => a[1] - b[1])
|
||||
.map(([cat_, index]) => {
|
||||
return <div key={index} className="input-group"
|
||||
style={{display: "contents"}}>
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox"
|
||||
id={"categoriesInput" + index} checked={cat.includes(index)} aria-label={getCatName(cat_)}
|
||||
onChange={e => setCat_(e, index)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{getCatName(cat_)}</label>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="liceInput2" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
|
||||
<input type="text" className="form-control" id="liceInput2" placeholder="1;2" name="zone de combat" value={lice}
|
||||
onChange={e => setLice(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault" checked={classement}
|
||||
onChange={e => setClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault">
|
||||
{t('créerLaPhaseFinaleSilYADesPoules')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!classement}
|
||||
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault2">
|
||||
{t('lesCombattantsEnDehors2')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span>{dispoFiltered.length} {t('combattantsCorrespondentAuxSélectionnés')}</span><br/>
|
||||
<span>{Math.ceil(dispoFiltered.length / 10)} {t('catégoriesVontêtreCréées')}</span><br/>
|
||||
{dispoFiltered.length > 10 && dispoFiltered.some(c => !c.weight) &&
|
||||
<span style={{color: "red"}}>{t('certainsCombattantsNontPasDePoidsRenseigné')}</span>}
|
||||
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" onClick={handleSubmit}
|
||||
disabled={dispoFiltered.length <= 0} data-bs-dismiss="modal">{t('ajouter')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
export function AutoNewCatSModalContent() {
|
||||
const {t} = useTranslation("cm");
|
||||
const {combs} = useCombs();
|
||||
const {sendRequest} = useWS();
|
||||
const toastId = React.useRef(null);
|
||||
const {data, error} = useRequestWS("listPreset", {}, null);
|
||||
|
||||
const id = useId()
|
||||
const [categories, setCategories] = useState([])
|
||||
const [lice, setLice] = useState("1")
|
||||
const [classement, setClassement] = useState(true)
|
||||
const [fullClassement, setFullClassement] = useState(false)
|
||||
|
||||
const setCategories_ = (e, catId) => {
|
||||
if (e.target.checked) {
|
||||
if (!categories.includes(catId)) {
|
||||
setCategories([...categories, catId])
|
||||
}
|
||||
} else {
|
||||
setCategories(categories.filter(c => c !== catId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let catList2 = []
|
||||
for (const catId of categories) {
|
||||
const preset = data.find(p => p.id === catId);
|
||||
const dispoFiltered = Object.values(combs).filter(comb => comb.categoriesInscrites?.includes(catId)).sort(() => Math.random() - 0.5)
|
||||
.map(comb => ({...comb, categorie: CatList[Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory)]}));
|
||||
console.log("Creating category for preset", preset.name, "and", dispoFiltered.length, "combattants");
|
||||
|
||||
const catList = makeCategory(dispoFiltered);
|
||||
console.log(catList)
|
||||
|
||||
for (const list of catList) {
|
||||
if (list.length > 10) {
|
||||
catList2.push(...makeWeightCategories(list)
|
||||
.map((combs, index, a) => ({combs, classement, preset, lice, fullClassement, index, size: a.length})));
|
||||
} else {
|
||||
catList2.push(({combs: [...list], classement, preset, lice, fullClassement, index: 1, size: 1}));
|
||||
}
|
||||
}
|
||||
}
|
||||
sendCatList(toastId, t, catList2, sendRequest);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="autoNewCatSModalLabel">{t('créerToutesLesCatégories')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="d-flex flex-wrap justify-content-around">
|
||||
<div className="d-flex flex-wrap mb-3">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorie')} :
|
||||
</label>
|
||||
{error ? <AxiosError error={error}/> : <>
|
||||
{data && data.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
|
||||
{data && data.map((cat, index) =>
|
||||
<div key={cat.id} className="input-group"
|
||||
style={{display: "contents"}}>
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox"
|
||||
id={id + "categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
|
||||
onChange={e => setCategories_(e, cat.id)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={id + "categoriesInput" + index}>{cat.name}</label>
|
||||
</div>
|
||||
</div>)}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="liceInput3" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
|
||||
<input type="text" className="form-control" id="liceInput3" placeholder="1;2" name="zone de combat" value={lice}
|
||||
onChange={e => setLice(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault" checked={classement}
|
||||
onChange={e => setClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault">
|
||||
{t('créerLaPhaseFinaleSilYADesPoules')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!classement}
|
||||
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault2">
|
||||
{t('lesCombattantsEnDehors2')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<button type="submit" className="btn btn-primary" onClick={handleSubmit}>{t('ajouter')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
44
src/main/webapp/src/components/cm/ListPresetSelect.jsx
Normal file
44
src/main/webapp/src/components/cm/ListPresetSelect.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {useRequestWS} from "../../hooks/useWS.jsx";
|
||||
import {AxiosError} from "../AxiosError.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, {useId} from "react";
|
||||
|
||||
export function ListPresetSelect({disabled, value, onChange, returnId = true}) {
|
||||
const id = useId()
|
||||
const {data, error} = useRequestWS("listPreset", {}, null);
|
||||
const {t} = useTranslation();
|
||||
return <>
|
||||
{data
|
||||
? <div className="mb-3">
|
||||
<label className="form-label" htmlFor={id}>{t('catégorie')}</label>
|
||||
<select className="form-select" id={id} disabled={disabled}
|
||||
value={returnId ? value : (value ? value.id : -1)}
|
||||
onChange={e => {
|
||||
if (returnId) {
|
||||
onChange(Number(e.target.value))
|
||||
} else {
|
||||
onChange(data.find(c => c.id === Number(e.target.value)))
|
||||
}
|
||||
}}>
|
||||
<option value={-1}>{t('sélectionner...')}</option>
|
||||
{data.sort((a, b) => a.name.localeCompare(b.name)).map(o => (<option key={o.id} value={o.id}>{o.name}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function Def() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div className="input-group mb-3">
|
||||
<label className="input-group-text" id="inputGroupSelect02">{t('catégorie')}</label>
|
||||
<select className="form-select" id="inputGroupSelect02"
|
||||
defaultValue={t('chargement...')}>
|
||||
<option>{t('chargement...')}</option>
|
||||
</select>
|
||||
</div>;
|
||||
}
|
||||
@@ -3,9 +3,21 @@ import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import {initReactI18next} from 'react-i18next';
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
const options = {
|
||||
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
|
||||
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
|
||||
caches: [],
|
||||
convertDetectedLanguage: (lng) => {
|
||||
const supportedLngs = ['en', 'fr'];
|
||||
const languagePart = lng.split('-')[0];
|
||||
|
||||
if (supportedLngs.includes(languagePart)) {
|
||||
return languagePart; // Manually return 'en' from 'en-US'
|
||||
}
|
||||
|
||||
return lng;
|
||||
},
|
||||
}
|
||||
|
||||
i18n
|
||||
@@ -20,8 +32,13 @@ i18n
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'fr',
|
||||
debug: true,
|
||||
fallbackLng: {
|
||||
'fr-FR': ['fr'],
|
||||
default: ['en']
|
||||
},
|
||||
supportedLngs: ['fr', 'en'],
|
||||
nonExplicitSupportedLngs: true,
|
||||
debug: vite_url.startsWith('http://localhost'),
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
|
||||
167
src/main/webapp/src/hooks/useCard.jsx
Normal file
167
src/main/webapp/src/hooks/useCard.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import {createContext, useContext, useEffect, useReducer} from "react";
|
||||
import {useWS} from "./useWS.jsx";
|
||||
|
||||
const CardContext = createContext({comb: {}, team: []});
|
||||
const CardDispatchContext = createContext(() => {
|
||||
});
|
||||
|
||||
function compareCards(a, b) {
|
||||
for (const keys of Object.keys(a)) {
|
||||
if (a[keys] !== b[keys]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const CARD_TYPE_ORDER = [
|
||||
'BLUE',
|
||||
'YELLOW',
|
||||
'RED',
|
||||
'BLACK'
|
||||
]
|
||||
|
||||
export function compareCardOrder(a, b) {
|
||||
if (!a || !b) return 0;
|
||||
return CARD_TYPE_ORDER.indexOf(a.type) - CARD_TYPE_ORDER.indexOf(b.type);
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'SET_CARD':
|
||||
if (state.comb[action.payload.id] === undefined || !compareCards(action.payload, state.comb[action.payload.id])) {
|
||||
return {
|
||||
comb: {
|
||||
...state.comb,
|
||||
[action.payload.id]: action.payload
|
||||
},
|
||||
team: state.team
|
||||
}
|
||||
}
|
||||
return state
|
||||
case 'SET_ALL':
|
||||
if (action.payload.some(e => state.comb[e.id] === undefined || !compareCards(e, state.comb[e.id]))) {
|
||||
const newCombs = {};
|
||||
for (const o of action.payload) {
|
||||
newCombs[o.id] = o;
|
||||
}
|
||||
|
||||
return {
|
||||
comb: {
|
||||
...state.comb,
|
||||
...newCombs
|
||||
},
|
||||
team: state.team
|
||||
}
|
||||
}
|
||||
return state
|
||||
case 'REMOVE_CARDS':
|
||||
const newState = {...state}
|
||||
for (const id of action.payload)
|
||||
delete newState.comb[id]
|
||||
return newState
|
||||
case 'SET_TEAM_CARD':
|
||||
return {
|
||||
comb: state.comb,
|
||||
team: [...state.team.filter(e => e.teamName !== action.payload.teamName || e.teamUuid !== action.payload.teamUuid || e.type !== action.payload.type),
|
||||
action.payload]
|
||||
}
|
||||
case 'REMOVE_TEAM_CARD':
|
||||
return {
|
||||
comb: state.comb,
|
||||
team: [...state.team.filter(e => e.teamName !== action.payload.teamName || e.teamUuid !== action.payload.teamUuid || e.type !== action.payload.type)]
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function WSListener({dispatch}) {
|
||||
const {dispatch: dispatchWS} = useWS()
|
||||
|
||||
useEffect(() => {
|
||||
const sendCards = ({data}) => {
|
||||
dispatch({type: 'SET_ALL', payload: data});
|
||||
}
|
||||
const rmCards = ({data}) => {
|
||||
dispatch({type: 'REMOVE_CARDS', payload: data});
|
||||
}
|
||||
const sendTeamCard = ({data}) => {
|
||||
dispatch({type: 'SET_ALL', payload: data.cards});
|
||||
dispatch({
|
||||
type: 'SET_TEAM_CARD',
|
||||
payload: {teamName: data.teamName, teamUuid: data.teamUuid, type: data.type, reason: data.reason, date: data.date}
|
||||
});
|
||||
}
|
||||
const rmTeamCard = ({data}) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_TEAM_CARD',
|
||||
payload: {teamName: data.teamName, teamUuid: data.teamUuid, type: data.type, reason: data.reason, date: data.date}
|
||||
});
|
||||
}
|
||||
|
||||
dispatchWS({type: 'addListener', payload: {callback: sendCards, code: 'sendCards'}})
|
||||
dispatchWS({type: 'addListener', payload: {callback: rmCards, code: 'rmCards'}})
|
||||
dispatchWS({type: 'addListener', payload: {callback: sendTeamCard, code: 'sendTeamCard'}})
|
||||
dispatchWS({type: 'addListener', payload: {callback: rmTeamCard, code: 'rmTeamCard'}})
|
||||
return () => {
|
||||
dispatchWS({type: 'removeListener', payload: sendCards})
|
||||
dispatchWS({type: 'removeListener', payload: rmCards})
|
||||
dispatchWS({type: 'removeListener', payload: sendTeamCard})
|
||||
dispatchWS({type: 'removeListener', payload: rmTeamCard})
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
export function CardsProvider({children}) {
|
||||
const [cards, dispatch] = useReducer(reducer, {comb: {}, team: []})
|
||||
|
||||
return <CardContext.Provider value={cards}>
|
||||
<CardDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
<WSListener dispatch={dispatch}/>
|
||||
</CardDispatchContext.Provider>
|
||||
</CardContext.Provider>
|
||||
}
|
||||
|
||||
export function useCards() {
|
||||
const cards = useContext(CardContext);
|
||||
return {
|
||||
cards_t: cards.team,
|
||||
cards_v: Object.values(cards.comb),
|
||||
...useCardsStatic(Object.values(cards.comb))
|
||||
}
|
||||
}
|
||||
|
||||
export function useCardsStatic(cards_v) {
|
||||
return {
|
||||
getCardInMatch: (match) => {
|
||||
return cards_v.filter(card => (card.comb === match.c1 || card.comb === match.c2) && card.match === match.id);
|
||||
},
|
||||
getHeightCardForCombInMatch: (combId, match) => {
|
||||
return cards_v.filter(card => card.comb === combId && (card.category === match.categorie || (card.match !== match.id && card.type !== "BLUE"))).sort(compareCardOrder).pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useCardsDispatch() {
|
||||
return useContext(CardDispatchContext);
|
||||
}
|
||||
|
||||
export function hasEffectCard(card, matchId, categoryId) {
|
||||
switch (card.type) {
|
||||
case 'BLUE':
|
||||
return false;
|
||||
case 'YELLOW':
|
||||
return card.match === matchId;
|
||||
case 'RED':
|
||||
return card.match === matchId || card.category === categoryId;
|
||||
case 'BLACK':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,13 @@ function reducer(state, action) {
|
||||
lname: action.payload.data.lname,
|
||||
genre: action.payload.data.genre,
|
||||
country: action.payload.data.country,
|
||||
teamMembers: action.payload.data.teamMembers,
|
||||
})
|
||||
if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) {
|
||||
//console.debug("Updating comb", comb);
|
||||
return {
|
||||
...state,
|
||||
[comb.id]: comb
|
||||
[comb.id]: {...state[comb.id], ...comb}
|
||||
}
|
||||
}
|
||||
return state
|
||||
@@ -41,13 +42,17 @@ function reducer(state, action) {
|
||||
lname: e.lname,
|
||||
genre: e.genre,
|
||||
country: e.country,
|
||||
teamMembers: e.teamMembers,
|
||||
}
|
||||
});
|
||||
|
||||
if (combs.some(e => state[e.id] === undefined || !compareCombs(e, state[e.id]))) {
|
||||
const newCombs = {};
|
||||
for (const o of combs) {
|
||||
newCombs[o.id] = o;
|
||||
newCombs[o.id] = {
|
||||
...state[o.id],
|
||||
...o
|
||||
};
|
||||
}
|
||||
//console.debug("Updating combs", newCombs);
|
||||
|
||||
@@ -71,12 +76,12 @@ function WSListener({dispatch}) {
|
||||
|
||||
useEffect(() => {
|
||||
const sendRegister = ({data}) => {
|
||||
dispatch({type: 'SET_ALL', payload: {source: "register", data: data}});
|
||||
dispatch({type: 'SET_COMB', payload: {source: "register", data: data}});
|
||||
}
|
||||
|
||||
dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}})
|
||||
return () => {
|
||||
dispatchWS({type: 'removeListener', payload: {callback: sendRegister, code: 'sendRegister'}})
|
||||
dispatchWS({type: 'removeListener', payload: sendRegister})
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -110,6 +115,8 @@ export function CombName({combId}) {
|
||||
const {getComb} = useCombs();
|
||||
const comb = getComb(combId, null);
|
||||
if (comb) {
|
||||
if (comb.lname === "__team")
|
||||
return <>{comb.fname}</>
|
||||
return <>{comb.fname} {comb.lname}</>
|
||||
} else {
|
||||
return <>[Comb #{combId}]</>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function OBSProvider({children}) {
|
||||
}
|
||||
|
||||
function getElementName(element) {
|
||||
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}`
|
||||
return `sub${sessionStorage.getItem("liceName") || 1}.${element}`
|
||||
}
|
||||
|
||||
export function useOBS() {
|
||||
|
||||
@@ -47,6 +47,7 @@ export function WSProvider({url, onmessage, children}) {
|
||||
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
|
||||
const [state, dispatch] = useReducer(reducer, {listener: []})
|
||||
const ws = useRef(null)
|
||||
const tableState = useRef({})
|
||||
const listenersRef = useRef([])
|
||||
const callbackRef = useRef({})
|
||||
const isReadyRef = useRef(isReady)
|
||||
@@ -216,14 +217,14 @@ export function WSProvider({url, onmessage, children}) {
|
||||
}
|
||||
|
||||
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData}
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData, tableState}
|
||||
return <WebsocketContext.Provider value={ret}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>
|
||||
}
|
||||
|
||||
export function useWS() {
|
||||
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext)
|
||||
const {isReady, dispatch, send, wait_length, welcomeData, tableState} = useContext(WebsocketContext)
|
||||
return {
|
||||
dispatch,
|
||||
isReady,
|
||||
@@ -247,6 +248,10 @@ export function useWS() {
|
||||
send(uuidv4(), "error", "ERROR", data)
|
||||
},
|
||||
send,
|
||||
setState: (newState) => {
|
||||
tableState.current = {...tableState.current, ...newState}
|
||||
},
|
||||
tableState
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ export function MemberList({source}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [licenceData, setLicenceData] = useState([]);
|
||||
const [showLicenceState, setShowLicenceState] = useState((sessionStorage.getItem("showLicenceState") || "false") === "true");
|
||||
|
||||
const setFilter = (filter) => {
|
||||
navigate("#" + encodeURI(JSON.stringify(filter)))
|
||||
@@ -37,7 +35,7 @@ export function MemberList({source}) {
|
||||
payment: 2,
|
||||
order: "",
|
||||
categorie: "",
|
||||
archived: false,
|
||||
archived: sessionStorage.getItem("showMembreArchived") || true,
|
||||
...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
|
||||
}
|
||||
|
||||
@@ -72,23 +70,11 @@ export function MemberList({source}) {
|
||||
club: e.club,
|
||||
categorie: e.categorie,
|
||||
licence_number: e.licence,
|
||||
licence: showLicenceState ? licenceData.find(licence => licence.membre === e.id) : null
|
||||
licence: data.additionalData?.find(licence => licence.membre === e.id)
|
||||
})
|
||||
}
|
||||
setMemberData(data2);
|
||||
}, [data, licenceData]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("showLicenceState", showLicenceState);
|
||||
if (!showLicenceState)
|
||||
return;
|
||||
|
||||
toast.promise(
|
||||
apiAxios.get(`/licence/current/${source}`), getToastMessage("membre.toast.licences.load"))
|
||||
.then(data => {
|
||||
setLicenceData(data.data);
|
||||
});
|
||||
}, [showLicenceState]);
|
||||
}, [data]);
|
||||
|
||||
const search = (search) => {
|
||||
if (search === filter.search)
|
||||
@@ -102,8 +88,8 @@ export function MemberList({source}) {
|
||||
<div className="col-lg-9">
|
||||
<SearchBar search={search} defaultValue={filter.search}/>
|
||||
{data
|
||||
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState}
|
||||
page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/>
|
||||
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} page={filter.page}
|
||||
setPage={e => setFilter({...filter, page: e})} source={source}/>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
@@ -130,10 +116,11 @@ export function MemberList({source}) {
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar showLicenceState={showLicenceState}
|
||||
setShowLicenceState={setShowLicenceState}
|
||||
showArchived={filter.archived}
|
||||
setShowArchived={e => setFilter({...filter, archived: e})}
|
||||
<FiltreBar showArchived={filter.archived}
|
||||
setShowArchived={e => {
|
||||
setFilter({...filter, archived: e})
|
||||
sessionStorage.setItem("showMembreArchived", e);
|
||||
}}
|
||||
clubFilter={filter.club}
|
||||
setClubFilter={e => setFilter({...filter, club: e})}
|
||||
source={source}
|
||||
@@ -163,6 +150,7 @@ export function MemberList({source}) {
|
||||
|
||||
function FileOutput() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
function formatColumnDate(worksheet, col) {
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref'])
|
||||
// note: range.s.r + 1 skips the header row
|
||||
@@ -181,6 +169,7 @@ function FileOutput() {
|
||||
toast.promise(
|
||||
apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
|
||||
.then(data => {
|
||||
console.log(data)
|
||||
const dataOut = []
|
||||
for (const e of data.data) {
|
||||
const tmp = {
|
||||
@@ -354,7 +343,7 @@ function FileInput() {
|
||||
);
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page, setPage, source}) {
|
||||
function MakeCentralPanel({data, visibleMember, navigate, page, setPage, source}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const pages = []
|
||||
@@ -374,7 +363,7 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
|
||||
})}</small>
|
||||
<div className="list-group">
|
||||
{visibleMember.map(member => (
|
||||
<MakeRow key={member.id} member={member} navigate={navigate} showLicenceState={showLicenceState} source={source}/>))}
|
||||
<MakeRow key={member.id} member={member} navigate={navigate} source={source}/>))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -391,11 +380,11 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
|
||||
</>
|
||||
}
|
||||
|
||||
function MakeRow({member, showLicenceState, navigate, source}) {
|
||||
function MakeRow({member, navigate, source}) {
|
||||
const rowContent = <>
|
||||
<div className="row" style={{padding: "0.6em 0"}}>
|
||||
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '0') : "-------") + " "}
|
||||
{(showLicenceState && member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <> </>}</span>
|
||||
{(member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <> </>}</span>
|
||||
<div className="ms-2 col-auto">
|
||||
<div className="fw-bold">{member.fname} {member.lname}</div>
|
||||
</div>
|
||||
@@ -411,7 +400,7 @@ function MakeRow({member, showLicenceState, navigate, source}) {
|
||||
|
||||
</>
|
||||
|
||||
if (showLicenceState && member.licence != null) {
|
||||
if (member.licence != null) {
|
||||
return <a className={"list-group-item d-flex justify-content-between align-items-start list-group-item-action list-group-item-"
|
||||
+ (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
|
||||
style={{padding: "0 1em"}}
|
||||
@@ -509,8 +498,6 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
|
||||
}
|
||||
|
||||
function FiltreBar({
|
||||
showLicenceState,
|
||||
setShowLicenceState,
|
||||
showArchived,
|
||||
setShowArchived,
|
||||
clubFilter,
|
||||
@@ -526,9 +513,6 @@ function FiltreBar({
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./PayAndValidateList.css";
|
||||
import * as Tools from "../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {counter} from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
export function PayAndValidateList({source}) {
|
||||
const {t} = useTranslation();
|
||||
@@ -31,8 +30,7 @@ export function PayAndValidateList({source}) {
|
||||
const [lastSearch, setLastSearch] = useState("");
|
||||
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
|
||||
|
||||
const storedMembers = sessionStorage.getItem("selectedMembers");
|
||||
const [selectedMembers, setSelectedMembers] = useState(storedMembers ? JSON.parse(storedMembers) : []);
|
||||
const [selectedMembers, setSelectedMembers] = useState([]);
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {
|
||||
@@ -41,10 +39,6 @@ export function PayAndValidateList({source}) {
|
||||
refresh
|
||||
} = 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}&categorie=${catFilter}`);
|
||||
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);
|
||||
|
||||
@@ -4,7 +4,7 @@ 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, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
@@ -51,13 +51,34 @@ export function LicenceCard({userData}) {
|
||||
dispatch({type: 'SORT'})
|
||||
}, [data]);
|
||||
|
||||
const handleAsk = () => {
|
||||
const currentLicence = licences[0];
|
||||
|
||||
let certif = undefined;
|
||||
if (currentLicence != null && currentLicence.certificate != null) {
|
||||
const strings = currentLicence.certificate.split('¤');
|
||||
if (currentLicence.saison === getSaison()) {
|
||||
certif = currentLicence.certificate;
|
||||
} else if (strings.length > 1) {
|
||||
const date = new Date(strings[1]);
|
||||
const max = getFirstDateOfSaison();
|
||||
max.setFullYear(max.getFullYear() - 2);
|
||||
|
||||
if (max < date) {
|
||||
certif = currentLicence.certificate;
|
||||
}
|
||||
}
|
||||
}
|
||||
setModal({id: -1, membre: userData.id, certificate: certif});
|
||||
}
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">{t('licence')}</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
||||
onClick={_ => setModal({id: -1, membre: userData.id})}>{t('button.ajouter')}
|
||||
onClick={handleAsk}>{t('button.ajouter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,13 +162,6 @@ function ModalContent({licence, dispatch}) {
|
||||
if (licence.id !== -1) {
|
||||
setNew(false)
|
||||
setSaison(licence.saison)
|
||||
if (licence.certificate === null) {
|
||||
setCertificateBy("")
|
||||
setCertificateDate("")
|
||||
} else {
|
||||
setCertificateBy(licence.certificate.split('¤')[0])
|
||||
setCertificateDate(licence.certificate.split('¤')[1])
|
||||
}
|
||||
setValidate(licence.validate)
|
||||
setPay(licence.pay);
|
||||
} else {
|
||||
@@ -158,6 +172,13 @@ function ModalContent({licence, dispatch}) {
|
||||
setValidate(false)
|
||||
setPay(false);
|
||||
}
|
||||
if (licence.certificate) {
|
||||
setCertificateBy(licence.certificate.split('¤')[0])
|
||||
setCertificateDate(licence.certificate.split('¤')[1])
|
||||
} else {
|
||||
setCertificateBy("")
|
||||
setCertificateDate("")
|
||||
}
|
||||
}, [licence]);
|
||||
|
||||
return <form onSubmit={e => sendLicence(e, dispatch)}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {apiAxios, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {apiAxios, getFirstDateOfSaison, getSaison, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {ColoredText} from "../../../components/ColoredCircle.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
@@ -32,7 +32,7 @@ function licenceReducer(licences, action) {
|
||||
}
|
||||
|
||||
export function LicenceCard({userData}) {
|
||||
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: false}
|
||||
const defaultLicence = {id: -1, membre: userData.id, validate: false, saison: getSaison(), certificate: null}
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
@@ -48,13 +48,33 @@ export function LicenceCard({userData}) {
|
||||
dispatch({type: 'SORT'})
|
||||
}, [data]);
|
||||
|
||||
const handleAsk = () => {
|
||||
const currentLicence = licences[0];
|
||||
|
||||
let certif = undefined;
|
||||
if (currentLicence != null && currentLicence.certificate != null) {
|
||||
const strings = currentLicence.certificate.split('¤');
|
||||
if (currentLicence.saison === getSaison()) {
|
||||
certif = currentLicence.certificate;
|
||||
} else if (strings.length > 1) {
|
||||
const date = new Date(strings[1]);
|
||||
const max = getFirstDateOfSaison();
|
||||
max.setFullYear(max.getFullYear() - 2);
|
||||
|
||||
if (max < date)
|
||||
certif = currentLicence.certificate;
|
||||
}
|
||||
}
|
||||
setModal({...defaultLicence, certificate: certif});
|
||||
}
|
||||
|
||||
return <div className="card mb-4 mb-md-0">
|
||||
<div className="card-header container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">{t('licence')}</div>
|
||||
<div className="col" style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal"
|
||||
onClick={() => setModal(defaultLicence)}
|
||||
onClick={handleAsk}
|
||||
disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -127,15 +147,14 @@ function ModalContent({licence, dispatch}) {
|
||||
useEffect(() => {
|
||||
if (licence.id !== -1) {
|
||||
setNew(false)
|
||||
if (licence.certificate === null) {
|
||||
setCertificateBy("")
|
||||
setCertificateDate("")
|
||||
} else {
|
||||
setCertificateBy(licence.certificate.split('¤')[0])
|
||||
setCertificateDate(licence.certificate.split('¤')[1])
|
||||
}
|
||||
|
||||
} else {
|
||||
setNew(true)
|
||||
}
|
||||
if (licence.certificate) {
|
||||
setCertificateBy(licence.certificate.split('¤')[0])
|
||||
setCertificateDate(licence.certificate.split('¤')[1])
|
||||
} else {
|
||||
setCertificateBy("")
|
||||
setCertificateDate("")
|
||||
}
|
||||
|
||||
@@ -6,12 +6,24 @@ import {CheckField, OptionField, TextField} from "../../components/MemberCustomF
|
||||
import {ClubSelect} from "../../components/ClubSelect.jsx";
|
||||
import {ConfirmDialog} from "../../components/ConfirmDialog.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {apiAxios, getToastMessage} from "../../utils/Tools.js";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {
|
||||
apiAxios,
|
||||
CatList,
|
||||
getCatName, getShieldTypeName,
|
||||
getSwordTypeName,
|
||||
getToastMessage,
|
||||
ShieldList,
|
||||
sortCategories,
|
||||
SwordList,
|
||||
timePrint
|
||||
} from "../../utils/Tools.js";
|
||||
import React, {useEffect, useReducer, useState} from "react";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import ProtectionSelector from "../../components/ProtectionSelector.jsx";
|
||||
import CategoryPreset from "../../assets/CategoryPreset.js";
|
||||
|
||||
export function CompetitionEdit() {
|
||||
const {id} = useParams()
|
||||
@@ -44,7 +56,7 @@ 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}`)}>
|
||||
onClick={_ => navigate(`/competition/${data.id}/register`)}>
|
||||
{t('comp.modifierLesParticipants')}</button>}
|
||||
|
||||
{data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
|
||||
@@ -190,10 +202,10 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
}}><FontAwesomeIcon icon={faAdd}/></button>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
<div className="row" style={{marginTop: "1em"}}>
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,8 +217,23 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
|
||||
function Content({data}) {
|
||||
const navigate = useNavigate();
|
||||
const [registerMode, setRegisterMode] = useState(data.registerMode || "FREE");
|
||||
const [modaleState, setModaleState] = useState({})
|
||||
const [presets, setPresets] = useState(data.presets || []);
|
||||
const [cats, setCats] = useState(data.requiredWeight || [])
|
||||
const [presetChange, setPresetChange] = useState(false)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setCat = (e, cat) => {
|
||||
if (e.target.checked) {
|
||||
if (!cats.includes(cat)) {
|
||||
setCats([...cats, cat])
|
||||
}
|
||||
} else {
|
||||
setCats(cats.filter(c => c !== cat))
|
||||
}
|
||||
}
|
||||
const isCatSelected = (cat) => cats.includes(cat)
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -227,6 +254,8 @@ function Content({data}) {
|
||||
out['startRegister'] = event.target.startRegister?.value
|
||||
out['endRegister'] = event.target.endRegister?.value
|
||||
out['registerMode'] = registerMode
|
||||
out['presets'] = presets
|
||||
out['requiredWeight'] = cats
|
||||
|
||||
if (out['registerMode'] === "HELLOASSO") {
|
||||
out['data3'] = event.target.data3?.value
|
||||
@@ -277,14 +306,17 @@ function Content({data}) {
|
||||
toast.promise(
|
||||
apiAxios.post(`/competition`, out), getToastMessage("comp.toast.save")
|
||||
).then(data => {
|
||||
setPresetChange(false)
|
||||
console.log(data.data)
|
||||
if (data.data.id !== undefined)
|
||||
navigate("/competition/" + data.data.id)
|
||||
if (data.data.presets !== undefined)
|
||||
setPresets(data.data.presets)
|
||||
})
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="card mb-4">
|
||||
return <>
|
||||
<form onSubmit={handleSubmit} className="card mb-4">
|
||||
<input name="id" value={data.id || ""} readOnly hidden/>
|
||||
<div className="card-header">{data.id ? t('comp.editionCompétition') : t('comp.créationCompétition')}</div>
|
||||
<div className="card-body text-center">
|
||||
@@ -340,6 +372,56 @@ function Content({data}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="accordion-item">
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour"
|
||||
aria-expanded="false" aria-controls="collapseFour">
|
||||
Catégories proposées
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseFour" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
|
||||
<div className="accordion-body" style={{textAlign: "left"}}>
|
||||
<div className="list-group">
|
||||
{presets.sort((a, b) => a.name.localeCompare(b.name)).map((preset) =>
|
||||
<a key={preset.id} className="list-group-item list-group-item-action" data-bs-toggle="modal"
|
||||
data-bs-target="#catModal" onClick={() => setModaleState(preset)}>
|
||||
<span style={{margin: "0 0.25em 0 0"}}>{preset.name}</span>
|
||||
{preset.categories.map(e => e.categorie).sort(sortCategories).map((cat, index) =>
|
||||
<span key={index} className="badge text-bg-secondary"
|
||||
style={{margin: "0 0.25em"}}>{getCatName(cat)}</span>)}
|
||||
</a>)}
|
||||
</div>
|
||||
<div className="row" style={{marginTop: "1em"}}>
|
||||
<div className="col-auto"
|
||||
style={{color: "red"}}>{presetChange && t('LesModificationsNontEnregistrer')}</div>
|
||||
<div className="col" style={{textAlign: "right"}}>
|
||||
<div className="btn-group">
|
||||
<button type="button" className="btn btn-success" data-bs-toggle="modal"
|
||||
data-bs-target="#catModal"
|
||||
onClick={() => setModaleState({id: Math.min(...presets.map(p => p.id), 0) - 1})}>
|
||||
<FontAwesomeIcon icon={faAdd}/>
|
||||
</button>
|
||||
<button type="button" className="btn btn-success dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span className="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
{CategoryPreset.map((preset, index) =>
|
||||
<li key={index}>
|
||||
<button className="dropdown-item" type="button" data-bs-toggle="modal"
|
||||
data-bs-target="#catModal"
|
||||
onClick={() => setModaleState({id: Math.min(...presets.map(p => p.id), 0) - 1, ...preset})}>
|
||||
{preset.name}
|
||||
</button>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="accordion-item">
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree"
|
||||
@@ -375,6 +457,15 @@ function Content({data}) {
|
||||
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" style={{display: "flex" }}>
|
||||
<span className="input-group-text" id="startRegister">{t('poidsDemandéPour')}</span>
|
||||
{CatList.map((cat, index) => <div key={index} className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox" id={"catInput" + index} checked={isCatSelected(cat)}
|
||||
aria-label={getCatName(cat)} onChange={e => setCat(e, cat)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={"catInput" + index}>{getCatName(cat)}</label>
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<div>{t('comp.ha.text1')}</div>
|
||||
@@ -404,14 +495,192 @@ function Content({data}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{marginTop: "1em"}}>
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
|
||||
<div className="modal fade" id="catModal" tabIndex="-1" aria-labelledby="catModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<CatModalContent setPresets={setPresets} setPresetChange={setPresetChange} state={modaleState}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
|
||||
function CatModalContent({setPresets, setPresetChange, state}) {
|
||||
const [name, setName] = useState(state.name || "")
|
||||
const [sword, setSword] = useState(state.sword || "NONE")
|
||||
const [shield, setShield] = useState(state.shield || "NONE")
|
||||
const [cats, setCats] = useState(state.categories || [])
|
||||
const [mandatoryProtection1, setMandatoryProtection1] = useState(state.mandatoryProtection1 || 5)
|
||||
const [mandatoryProtection2, setMandatoryProtection2] = useState(state.mandatoryProtection2 || 5)
|
||||
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
setName(state.name || "")
|
||||
setSword(state.sword || "NONE")
|
||||
setShield(state.shield || "NONE")
|
||||
setCats(state.categories?.map(c => ({
|
||||
categorie: c.categorie,
|
||||
roundDuration: timePrint(c.roundDuration),
|
||||
pauseDuration: timePrint(c.pauseDuration)
|
||||
})) || [])
|
||||
setMandatoryProtection1(state.mandatoryProtection1 || 5)
|
||||
setMandatoryProtection2(state.mandatoryProtection2 || 5)
|
||||
}, [state]);
|
||||
|
||||
const setCat = (e, cat) => {
|
||||
if (e.target.checked) {
|
||||
if (!cats.includes(cat)) {
|
||||
setCats([...cats, {categorie: cat, roundDuration: "", pauseDuration: ""}])
|
||||
}
|
||||
} else {
|
||||
setCats(cats.filter(c => c.categorie !== cat))
|
||||
}
|
||||
}
|
||||
const setTime = (e, cat) => {
|
||||
const value = e.target.value;
|
||||
setCats(cats.map(c => {
|
||||
if (c.categorie === cat)
|
||||
return {...c, roundDuration: value}
|
||||
return c
|
||||
}))
|
||||
}
|
||||
const setPause = (e, cat) => {
|
||||
const value = e.target.value;
|
||||
setCats(cats.map(c => {
|
||||
if (c.categorie === cat)
|
||||
return {...c, pauseDuration: value}
|
||||
return c
|
||||
}))
|
||||
}
|
||||
|
||||
const isCatSelected = (cat) => cats.some(cat_ => cat_.categorie === cat)
|
||||
|
||||
const parseTime = (str) => {
|
||||
const parts = str.split(":").map(part => parseInt(part, 10));
|
||||
if (parts.length === 1) {
|
||||
return parts[0] * 1000;
|
||||
} else if (parts.length === 2) {
|
||||
return (parts[0] * 60 + parts[1]) * 1000;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const out = {
|
||||
id: state.id,
|
||||
name: name,
|
||||
sword: sword,
|
||||
shield: shield,
|
||||
categories: cats.map(c => ({
|
||||
categorie: c.categorie,
|
||||
roundDuration: parseTime(c.roundDuration),
|
||||
pauseDuration: parseTime(c.pauseDuration)
|
||||
})),
|
||||
mandatoryProtection1: mandatoryProtection1,
|
||||
mandatoryProtection2: mandatoryProtection2
|
||||
}
|
||||
setPresets(presets => [...presets.filter(p => p.id !== out.id), out])
|
||||
setPresetChange(true)
|
||||
}
|
||||
|
||||
const handleRm = () => {
|
||||
setPresets(presets => presets.filter(p => p.id !== state.id))
|
||||
setPresetChange(true)
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('configurationDeLaCatégorie')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-7 mb-3">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="categorie">{t("nom")}</span>
|
||||
<input type="text" className="form-control" placeholder={t("nom")} name="name"
|
||||
value={name} onChange={e => setName(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="sword">{t('arme')}</span>
|
||||
<select className="form-select" aria-label={t('arme')} name="sword" value={sword}
|
||||
onChange={e => setSword(e.target.value)}>
|
||||
{SwordList.map(sword =>
|
||||
<option key={sword} value={sword}>{getSwordTypeName(sword)}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="shield">{t('bouclier')}</span>
|
||||
<select className="form-select" aria-label={t('bouclier')} name="shield" value={shield}
|
||||
onChange={e => setShield(e.target.value)}>
|
||||
{ShieldList.map(shield =>
|
||||
<option key={shield} value={shield}>{getShieldTypeName(shield)}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table className="table" style={{textAlign: "center"}}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{t('catégorie')}</th>
|
||||
<th scope="col">{t('peutSinscrire')}</th>
|
||||
<th scope="col">{t('duréeRound')}</th>
|
||||
<th scope="col">{t('duréePause')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{CatList.map((cat, index) => <tr key={index} style={{verticalAlign: "middle"}}>
|
||||
<th scope="row" style={{width: "7em"}}><label htmlFor={"catInput" + index}>{getCatName(cat)}</label></th>
|
||||
<td><input className="form-check-input" type="checkbox" id={"catInput" + index} checked={isCatSelected(cat)}
|
||||
aria-label={getCatName(cat)} onChange={e => setCat(e, cat)}/></td>
|
||||
<td style={{padding: "0"}}><input type="text" className="form-control form-control-sm" placeholder="mm:ss"
|
||||
value={cats.find(c => c.categorie === cat)?.roundDuration || ""}
|
||||
onChange={e => setTime(e, cat)}
|
||||
aria-label="mm:ss" hidden={!isCatSelected(cat)} style={{width: "4.5em"}}/></td>
|
||||
<td style={{padding: "0"}}><input type="text" className="form-control form-control-sm" placeholder="mm:ss"
|
||||
value={cats.find(c => c.categorie === cat)?.pauseDuration || ""}
|
||||
onChange={e => setPause(e, cat)}
|
||||
aria-label="mm:ss" hidden={!isCatSelected(cat)} style={{width: "4.5em"}}/></td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-5">
|
||||
<div style={{textAlign: "center"}}>
|
||||
<h6>{t('protectionObligatoire')} :</h6>
|
||||
{cats.some(cat_ => CatList.indexOf(cat_.categorie) <= CatList.indexOf("JUNIOR")) && <>
|
||||
<div>< 18 {t('ans')}</div>
|
||||
<ProtectionSelector shield={shield !== "NONE"} mandatoryProtection={mandatoryProtection1}
|
||||
setMandatoryProtection={setMandatoryProtection1}/>
|
||||
</>}
|
||||
{cats.some(cat_ => CatList.indexOf(cat_.categorie) > CatList.indexOf("JUNIOR")) && <>
|
||||
<div>≥ 18 {t('ans')}</div>
|
||||
<ProtectionSelector shield={shield !== "NONE"} mandatoryProtection={mandatoryProtection2}
|
||||
setMandatoryProtection={setMandatoryProtection2}/>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-danger" data-bs-dismiss="modal" onClick={handleRm}>{t('button.supprimer')}</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('button.fermer')}</button>
|
||||
<button type="button" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSave}>{t('button.appliquer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
import {useNavigate, useParams, useSearchParams} from "react-router-dom";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {apiAxios, CatList, getCatName, getToastMessage} from "../../utils/Tools.js";
|
||||
import React, {useEffect, useId, useReducer, useRef, useState} from "react";
|
||||
import {apiAxios, applyOverCategory, CatList, getCatFromName, getCatName, getToastMessage} from "../../utils/Tools.js";
|
||||
import {toast} from "react-toastify";
|
||||
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faAdd, faGavel, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faAdd, faGavel, faLock, faTrashCan} from "@fortawesome/free-solid-svg-icons";
|
||||
import "./CompetitionRegisterAdmin.css"
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import {useCountries} from "../../hooks/useCountries.jsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {Checkbox} from "../../components/MemberCustomFiels.jsx";
|
||||
import {FileImport} from "../../components/FileImport.jsx";
|
||||
|
||||
export function CompetitionRegisterAdmin({source}) {
|
||||
const {id} = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [state, dispatch] = useReducer(SimpleReducer, [])
|
||||
const [clubFilter, setClubFilter] = useState("")
|
||||
const [catFilter, setCatFilter] = useState("")
|
||||
const [catAgeFilter, setCatAgeFilter] = useState("")
|
||||
const [catFilter, setCatFilter] = useState(-1)
|
||||
const [filterNotWeight, setFilterNotWeight] = useState(false)
|
||||
const [modalState, setModalState] = useState({})
|
||||
const {t} = useTranslation();
|
||||
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1)
|
||||
const {data: data2, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1)
|
||||
const {data: data3} = useFetch(`/competition/${id}?light=true`, setLoading, 1)
|
||||
|
||||
const sortName = (a, b) => {
|
||||
if (a.data.fname === b.data.fname) return a.data.lname.localeCompare(b.data.lname);
|
||||
@@ -43,11 +49,31 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), getToastMessage("comp.toast.register.add")
|
||||
).then((response) => {
|
||||
if (response.data.error) {
|
||||
return
|
||||
return null;
|
||||
}
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: response.data.id, data: response.data}})
|
||||
dispatch({type: 'SORT', payload: sortName})
|
||||
document.getElementById("closeModal").click();
|
||||
return response.data
|
||||
})
|
||||
}
|
||||
const sendRegisters = (new_state) => {
|
||||
toast.promise(apiAxios.post(`/competition/${id}/registers/${source}`, new_state), getToastMessage("comp.toast.registers.addMultiple")
|
||||
).then((response) => {
|
||||
if (response.data.error)
|
||||
return;
|
||||
|
||||
let i = 0;
|
||||
response.data.forEach((d) => {
|
||||
if (d.licence === -42) {
|
||||
toast.warn(t('erreurPourLinscription') + " :" + d.lname, {autoClose: false});
|
||||
} else {
|
||||
dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: d}})
|
||||
i++;
|
||||
}
|
||||
})
|
||||
if (i > 0)
|
||||
toast.success(t('comp.toast.register.addMultiple.success', {count: i}))
|
||||
dispatch({type: 'SORT', payload: sortName})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,8 +88,15 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
<div className="col-lg-9">
|
||||
{data ? <div className="">
|
||||
<MakeCentralPanel
|
||||
data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter) && (catFilter.length === 0 || s.data.categorie === catFilter))}
|
||||
dispatch={dispatch} id={id} setModalState={setModalState} source={source}/>
|
||||
data={state.filter(s => (clubFilter.length === 0 || s.data.club?.name === clubFilter)
|
||||
&& (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter)
|
||||
&& (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter))
|
||||
&& (!filterNotWeight || (data3?.requiredWeight.includes(s.data.categorie) && (
|
||||
(source === "admin" && (s.data.weightReal === "" || s.data.weightReal === null)) ||
|
||||
(source !== "admin" && (s.data.weight === "" || s.data.weight === null))
|
||||
)))
|
||||
)}
|
||||
data2={data2} data3={data3} dispatch={dispatch} id={id} setModalState={setModalState} source={source}/>
|
||||
</div> : error ? <AxiosError error={error}/> : <Def/>}
|
||||
</div>
|
||||
<div className="col-lg-3">
|
||||
@@ -77,36 +110,65 @@ export function CompetitionRegisterAdmin({source}) {
|
||||
onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
|
||||
</button>
|
||||
</div>}
|
||||
<QuickAdd sendRegister={sendRegister} source={source}/>
|
||||
<QuickAdd sendRegister={sendRegister} source={source} data2={data2} error2={error2}/>
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">{t('filtre')}</div>
|
||||
<div className="card-body">
|
||||
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
|
||||
setCatFilter={setCatFilter} source={source}/>
|
||||
<FiltreBar data={data} data2={data2} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
|
||||
setCatFilter={setCatFilter} catAgeFilter={catAgeFilter} setCatAgeFilter={setCatAgeFilter}
|
||||
filterNotWeight={filterNotWeight} setFilterNotWeight={setFilterNotWeight} source={source}/>
|
||||
</div>
|
||||
</div>
|
||||
{source === "admin" && <FileOutput data={data}/>}
|
||||
{source === "admin" && <div className="mb-2"><FileOutput data={data} data2={data2}/></div>}
|
||||
{source === "admin" && <div className="mb-2"><FileImportComb data2={data2} sendRegisters={sendRegisters}/></div>}
|
||||
{source === "admin" && <div className="mb-2"><FileImportGuest data2={data2} sendRegisters={sendRegisters}/></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal sendRegister={sendRegister} modalState={modalState} setModalState={setModalState} source={source}/>
|
||||
<Modal_ data2={data2} error2={error2} data3={data3} sendRegister={sendRegister} modalState={modalState} setModalState={setModalState}
|
||||
source={source}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function QuickAdd({sendRegister, source}) {
|
||||
function QuickAdd({sendRegister, source, data2, error2}) {
|
||||
const {t} = useTranslation();
|
||||
const [categories, setCategories] = useState([])
|
||||
|
||||
const handleAdd = (licence) => {
|
||||
console.log("Quick add licence: " + licence)
|
||||
|
||||
sendRegister({
|
||||
licence: licence, fname: "", lname: "", weight: "", overCategory: 0, lockEdit: false, id: null
|
||||
licence: licence,
|
||||
fname: "",
|
||||
lname: "",
|
||||
weight: "",
|
||||
overCategory: 0,
|
||||
lockEdit: false,
|
||||
id: null,
|
||||
quick: true,
|
||||
categoriesInscrites: categories
|
||||
})
|
||||
}
|
||||
|
||||
const setCategories_ = (e, catId) => {
|
||||
if (e.target.checked) {
|
||||
if (!categories.includes(catId)) {
|
||||
setCategories([...categories, catId])
|
||||
}
|
||||
} else {
|
||||
setCategories(categories.filter(c => c !== catId))
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="card mb-4">
|
||||
<div className="card-header">{t('comp.ajoutRapide')}</div>
|
||||
<div className="card-body">
|
||||
<div className="d-flex flex-wrap">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorieàAjouter')}<br/> <small>({t('siDisponiblePourLaCatégorieDages')})</small>
|
||||
</label>
|
||||
<CategoriesList error2={error2} availableCats={data2?.sort((a, b) => a.name.localeCompare(b.name))} categories={categories}
|
||||
setCategories={setCategories_}/>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<span>{t('comp.noDeLicence')}</span>
|
||||
</div>
|
||||
@@ -131,14 +193,14 @@ function QuickAdd({sendRegister, source}) {
|
||||
</button>
|
||||
|
||||
{source === "club" && <LoadingProvider>
|
||||
<SearchMember sendRegister={sendRegister}/>
|
||||
<SearchMember sendRegister={sendRegister} categories={categories}/>
|
||||
</LoadingProvider>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function SearchMember({sendRegister}) {
|
||||
function SearchMember({sendRegister, categories}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/club/members`, setLoading, 1)
|
||||
const [suggestions, setSuggestions] = useState([])
|
||||
@@ -158,7 +220,9 @@ function SearchMember({sendRegister}) {
|
||||
weight: "",
|
||||
overCategory: 0,
|
||||
lockEdit: false,
|
||||
id: null
|
||||
id: null,
|
||||
quick: true,
|
||||
categoriesInscrites: categories
|
||||
})
|
||||
}
|
||||
|
||||
@@ -284,14 +348,41 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
|
||||
</div>);
|
||||
};
|
||||
|
||||
function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
function CategoriesList({error2, availableCats, fistCatInput, categories, setCategories}) {
|
||||
const {t} = useTranslation();
|
||||
const id = useId();
|
||||
|
||||
return <>
|
||||
{error2 ? <AxiosError error={error2}/> : <>
|
||||
{availableCats && availableCats.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
|
||||
{availableCats && availableCats.map((cat, index) =>
|
||||
<div key={cat.id} className="input-group"
|
||||
style={{display: "contents"}}>
|
||||
<div className="input-group-text">
|
||||
<input ref={index === 0 ? fistCatInput : undefined} className="form-check-input mt-0" type="checkbox"
|
||||
id={id + "categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
|
||||
onChange={e => setCategories(e, cat.id)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={id + "categoriesInput" + index}>{cat.name}</label>
|
||||
</div>
|
||||
</div>)}
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
|
||||
function Modal_({data2, data3, error2, sendRegister, modalState, setModalState, source}) {
|
||||
const country = useCountries('fr')
|
||||
const {t} = useTranslation();
|
||||
const closeBtn = useRef(null);
|
||||
const licenceInput = useRef(null);
|
||||
const nameInput = useRef(null);
|
||||
const fistCatInput = useRef(null);
|
||||
const submitBtn = useRef(null);
|
||||
|
||||
const [licence, setLicence] = useState("")
|
||||
const [fname, setFname] = useState("")
|
||||
const [lname, setLname] = useState("")
|
||||
const [weight, setWeight] = useState("")
|
||||
const [weightReal, setWeightReal] = useState("")
|
||||
const [cat, setCat] = useState(0)
|
||||
const [gcat, setGCat] = useState("")
|
||||
const [club, setClub] = useState("")
|
||||
@@ -299,97 +390,127 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
const [genre, setGenre] = useState("NA")
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [lockEdit, setLockEdit] = useState(false)
|
||||
const [categories, setCategories] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(modalState)
|
||||
if (!modalState) {
|
||||
setLicence("")
|
||||
setFname("")
|
||||
setLname("")
|
||||
setWeight("")
|
||||
setCat(0)
|
||||
setEditMode(false)
|
||||
setLockEdit(false)
|
||||
setClub("")
|
||||
setGCat("")
|
||||
setCountry_("FR")
|
||||
setGenre("NA")
|
||||
} else {
|
||||
setLicence(modalState.licence ? modalState.licence : "")
|
||||
setFname(modalState.fname ? modalState.fname : "")
|
||||
setLname(modalState.lname ? modalState.lname : "")
|
||||
setWeight(modalState.weight ? modalState.weight : "")
|
||||
setCat(modalState.overCategory ? modalState.overCategory : 0)
|
||||
setEditMode(modalState.licence || (modalState.fname && modalState.lname))
|
||||
setLockEdit(modalState.lockEdit)
|
||||
setClub(modalState.club ? modalState.club.name : "")
|
||||
setGCat(modalState.categorie ? modalState.categorie : "")
|
||||
setCountry_(modalState.country ? modalState.country : "FR")
|
||||
setGenre(modalState.genre ? modalState.genre : "NA")
|
||||
}
|
||||
setLicence(modalState?.licence ? modalState.licence : "")
|
||||
setFname(modalState?.fname ? modalState.fname : "")
|
||||
setLname(modalState?.lname ? modalState.lname : "")
|
||||
setWeight(modalState?.weight ? modalState.weight : "")
|
||||
setWeightReal(modalState?.weightReal ? modalState.weightReal : "")
|
||||
setCat(modalState?.overCategory ? modalState.overCategory : 0)
|
||||
setEditMode(modalState?.licence || (modalState.fname && modalState.lname))
|
||||
setLockEdit(modalState?.lockEdit === undefined ? false : modalState.lockEdit)
|
||||
setClub(modalState?.club ? modalState.club.name : "")
|
||||
setGCat(modalState?.categorie ? modalState.categorie : "")
|
||||
setCountry_(modalState?.country ? modalState.country : "FR")
|
||||
setGenre(modalState?.genre ? modalState.genre : "NA")
|
||||
setCategories(modalState?.categoriesInscrites ? modalState.categoriesInscrites : [])
|
||||
|
||||
setTimeout(() => {
|
||||
if (modalState?.id === 0) {
|
||||
licenceInput.current?.focus()
|
||||
} else if (modalState?.id < 0) {
|
||||
nameInput.current?.focus()
|
||||
}
|
||||
}, 450)
|
||||
}, [modalState]);
|
||||
|
||||
return <div className="modal fade" id="registerModal" tabIndex="-1" aria-labelledby="registerLabel"
|
||||
aria-hidden="true">
|
||||
const setCategories_ = (e, catId) => {
|
||||
if (e.target.checked) {
|
||||
if (!categories.includes(catId)) {
|
||||
setCategories([...categories, catId])
|
||||
}
|
||||
} else {
|
||||
setCategories(categories.filter(c => c !== catId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
const new_state = {
|
||||
licence: Number.isInteger(licence) ? licence : licence.trim(),
|
||||
fname: fname.trim(),
|
||||
lname: lname.trim(),
|
||||
weight: weight,
|
||||
weightReal: weightReal,
|
||||
overCategory: cat,
|
||||
lockEdit: lockEdit,
|
||||
categoriesInscrites: categories,
|
||||
id: modalState.id !== 0 ? modalState.id : null
|
||||
}
|
||||
if (modalState.id < 0) {
|
||||
new_state.licence = -1
|
||||
new_state.categorie = gcat
|
||||
new_state.club = club
|
||||
new_state.country = country_
|
||||
new_state.genre = genre
|
||||
}
|
||||
sendRegister(new_state)
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
setModalState(data)
|
||||
if (editMode || data.id < 0) {
|
||||
closeBtn.current.click()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (fistCatInput.current) {
|
||||
fistCatInput.current.focus()
|
||||
} else {
|
||||
submitBtn.current.focus()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const currenCat = gcat !== "" ? applyOverCategory(gcat, cat) : "";
|
||||
const availableCats = data2 ? (currenCat !== "" ? data2.filter(c => c.categories.some(c2 => c2.categorie === currenCat)) : data2).sort((a, b) => a.name.localeCompare(b.name)) : []
|
||||
if (availableCats.length === 0) {
|
||||
if (fistCatInput.current) {
|
||||
fistCatInput.current = null
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="modal fade" id="registerModal" tabIndex="-1" aria-labelledby="registerLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
const new_state = {
|
||||
licence: Number.isInteger(licence) ? licence : licence.trim(),
|
||||
fname: fname.trim(),
|
||||
lname: lname.trim(),
|
||||
weight: weight,
|
||||
overCategory: cat,
|
||||
lockEdit: lockEdit,
|
||||
id: modalState.id !== 0 ? modalState.id : null
|
||||
}
|
||||
if (modalState.id < 0) {
|
||||
new_state.licence = -1
|
||||
new_state.categorie = gcat
|
||||
new_state.club = club
|
||||
new_state.country = country_
|
||||
new_state.genre = genre
|
||||
}
|
||||
sendRegister(new_state)
|
||||
.then(() => {
|
||||
setModalState(new_state)
|
||||
})
|
||||
}}>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5"
|
||||
id="registerLabel">{editMode ? t('modification') : t('ajout')} {t('dun')} {modalState.id >= 0 ? t('combattant') : t('invité')}</h1>
|
||||
<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">{t('comp.modal.text1')}</div>}
|
||||
<div className="card" style={{marginBottom: "1em"}}>
|
||||
<div className="card-header">{modalState.id >= 0 ? t('comp.modal.recherche') : t('comp.modal.information')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row" hidden={modalState.id < 0}>
|
||||
<div className="col">
|
||||
<input type="number" min={0} step={1} className="form-control" placeholder={t("comp.noDeLicence")}
|
||||
name="licence"
|
||||
value={licence} onChange={e => setLicence(e.target.value)} disabled={editMode}/>
|
||||
</div>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5"
|
||||
id="registerLabel">{editMode ? t('modification') : t('ajout')} {t('dun')} {modalState.id >= 0 ? t('combattant') : t('invité')}</h1>
|
||||
<button ref={closeBtn} 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">{t('comp.modal.text1')}</div>}
|
||||
<div className="card" style={{marginBottom: "1em"}}>
|
||||
<div className="card-header">{modalState.id >= 0 ? t('comp.modal.recherche') : t('comp.modal.information')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row" hidden={modalState.id < 0}>
|
||||
<div className="col">
|
||||
<input ref={licenceInput} type="number" min={0} step={1} className="form-control"
|
||||
placeholder={t("comp.noDeLicence")} name="licence" value={licence}
|
||||
onChange={e => setLicence(e.target.value)} disabled={editMode}
|
||||
onKeyUp={e => e.key === "Enter" ? handleSubmit(e) : undefined}/>
|
||||
</div>
|
||||
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>{t('ou')}</h5>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<input type="text" className="form-control" placeholder={t('prenom')} name="fname"
|
||||
disabled={editMode && modalState.id >= 0}
|
||||
value={fname} onChange={e => setFname(e.target.value)}/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<input type="text" className="form-control" placeholder={t('nom')} name="lname"
|
||||
disabled={editMode && modalState.id >= 0}
|
||||
value={lname} onChange={e => setLname(e.target.value)}/>
|
||||
</div>
|
||||
</div>
|
||||
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>{t('ou')}</h5>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<input ref={nameInput} type="text" className="form-control" placeholder={t('prenom')} name="fname"
|
||||
disabled={editMode && modalState.id >= 0}
|
||||
value={fname} onChange={e => setFname(e.target.value)}/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<input type="text" className="form-control" placeholder={t('nom')} name="lname"
|
||||
disabled={editMode && modalState.id >= 0} value={lname} onChange={e => setLname(e.target.value)}
|
||||
onKeyUp={e => e.key === "Enter" && modalState.id >= 0 ? handleSubmit(e) : undefined}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(editMode || modalState.id < 0) && <>
|
||||
<div className="input-group mb-3" hidden={modalState.id >= 0}>
|
||||
<span className="input-group-text" id="categorie">{t("club", {count: 1})}</span>
|
||||
<input type="text" className="form-control" placeholder={t("club", {count: 1})} name="club"
|
||||
@@ -432,8 +553,14 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="weight">{t('comp.modal.poids')}</span>
|
||||
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight"
|
||||
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
|
||||
{source === "admin" && <span className="input-group-text" id="weightReal">{t('comp.modal.annoncé')}</span>}
|
||||
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight"
|
||||
name="weight" aria-describedby="weight" disabled={!(data3.requiredWeight.includes(currenCat))} value={weight}
|
||||
onChange={e => setWeight(e.target.value)}/>
|
||||
{source === "admin" && <><span className="input-group-text" id="weightReal">{t('comp.modal.pesé')}</span>
|
||||
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weightReal"
|
||||
name="weightReal" aria-describedby="weightReal" value={weightReal}
|
||||
onChange={e => setWeightReal(e.target.value)}/></>}
|
||||
</div>
|
||||
|
||||
<div className="input-group mb-3" hidden={modalState.id < 0}>
|
||||
@@ -451,31 +578,53 @@ function Modal({sendRegister, modalState, setModalState, source}) {
|
||||
onChange={e => setLockEdit(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="switchCheckReverse">{t('comp.modal.text2')}</label>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary">{editMode ? t('button.modifier') : t('button.ajouter')}</button>
|
||||
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">{t('button.annuler')}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="d-flex flex-wrap">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorie')} :
|
||||
</label>
|
||||
<CategoriesList error2={error2} availableCats={availableCats} fistCatInput={fistCatInput} categories={categories}
|
||||
setCategories={setCategories_}/>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">{t('button.annuler')}</button>
|
||||
<button ref={submitBtn} type="button" className="btn btn-primary"
|
||||
onClick={handleSubmit}>{editMode ? t('button.modifier') : t('button.ajouter')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
let allClub = []
|
||||
let allCat = []
|
||||
|
||||
function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, source}) {
|
||||
function FiltreBar({
|
||||
data,
|
||||
data2,
|
||||
clubFilter,
|
||||
setClubFilter,
|
||||
catFilter,
|
||||
setCatFilter,
|
||||
catAgeFilter,
|
||||
setCatAgeFilter,
|
||||
filterNotWeight,
|
||||
setFilterNotWeight,
|
||||
source
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
allClub.push(...data.map((e) => e.club?.name))
|
||||
allClub = allClub.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
|
||||
allCat.push(...data.map((e) => e.categorie))
|
||||
allCat = allCat.filter((value, index, self) => self.indexOf(value) === index).filter(value => value != null).sort()
|
||||
}, [data]);
|
||||
|
||||
return <div>
|
||||
<div className="mb-3">
|
||||
<Checkbox value={filterNotWeight} onChange={setFilterNotWeight} name="checkbox2" label={t('afficherLesCombattantsNonPesés')}/>
|
||||
</div>
|
||||
|
||||
{source === "admin" && <div className="mb-3">
|
||||
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
|
||||
<option value="">{t('---ToutLesClubs---')}</option>
|
||||
@@ -485,19 +634,24 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
|
||||
</select>
|
||||
</div>}
|
||||
<div className="mb-3">
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
|
||||
<option value="">{t('---TouteLesCatégories---')}</option>
|
||||
{allCat && allCat.map((value, index) => {
|
||||
return <option key={index} value={value}>{value}</option>
|
||||
<select className="form-select" value={catAgeFilter} onChange={event => setCatAgeFilter(event.target.value)}>
|
||||
<option value="">{t('---TousLesAges---')}</option>
|
||||
{CatList && CatList.map((value, index) => {
|
||||
return <option key={index} value={value}>{getCatName(value)}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<select className="form-select" value={catFilter} onChange={event => setCatFilter(Number(event.target.value))}>
|
||||
<option value={-1}>{t('---TouteLesCatégories---')}</option>
|
||||
{data2 && data2.map((cat) => {
|
||||
return <option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const registerType = searchParams.get("type") || "FREE";
|
||||
function MakeCentralPanel({data, data2, data3, dispatch, id, setModalState, source}) {
|
||||
const registerType = data3?.registerMode || "FREE";
|
||||
const {t} = useTranslation();
|
||||
|
||||
return <>
|
||||
@@ -515,14 +669,29 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
<div className="row">
|
||||
<span className="col-auto">{req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"}</span>
|
||||
<div className="ms-2 col-auto">
|
||||
<div><strong>{req.data.fname} {req.data.lname}</strong> <small>{req.data.genre}</small></div>
|
||||
<div><strong>{req.data.fname} {req.data.lname}</strong> <small>{req.data.lockEdit &&
|
||||
<FontAwesomeIcon icon={faLock} style={{color: "#e40101",}}/>}{req.data.genre}</small></div>
|
||||
<small>{req.data.club?.name || t("club", {count: 0})}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-auto" style={{textAlign: "right"}}>
|
||||
<small>{t("comp.surclassement", {count: req.data.overCategory, cat: getCatName(req.data.categorie)})}<br/>
|
||||
{req.data.weight ? req.data.weight : "---"} kg
|
||||
<small>{t("comp.surclassement", {
|
||||
count: req.data.overCategory,
|
||||
cat: getCatName(req.data.categorie)
|
||||
})} {source !== "admin" && data3?.requiredWeight.includes(applyOverCategory(req.data.categorie, req.data.weight)) && <>
|
||||
| {req.data.weight ? req.data.weight : "---"} kg
|
||||
</>}
|
||||
{source === "admin" && (data3?.requiredWeight.includes(applyOverCategory(req.data.categorie, req.data.overCategory)) || req.data.weightReal) && <>
|
||||
| {req.data.weightReal ? <span style={{color: "#3cbc02"}}>{req.data.weightReal} kg</span> :
|
||||
(req.data.weight ? <span style={{color: "#e40101"}}>{req.data.weight} kg</span> : "--- kg")}
|
||||
</>}</small>
|
||||
<br/>
|
||||
<small>
|
||||
{req.data.categoriesInscrites.map(catId => data2?.find(c => c.id === catId)).filter(o => o !== undefined)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)).map(cat =>
|
||||
<span key={cat.id} className="badge text-bg-secondary"
|
||||
style={{margin: "0 0.125em"}}>{cat.name}</span>)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -575,29 +744,78 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
|
||||
</>
|
||||
}
|
||||
|
||||
function FileOutput({data}) {
|
||||
function FileOutput({data, data2}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleFileDownload = () => {
|
||||
const catColumns = {}
|
||||
for (const cat of data2) {
|
||||
catColumns[cat.id] = ""
|
||||
}
|
||||
|
||||
const columnOrder = [
|
||||
"licence", "pays", "nom", "prenom", "genre", "weight",
|
||||
"categorie", "overCategory", "categorie2", "club",
|
||||
...Object.keys(catColumns)
|
||||
];
|
||||
|
||||
const dataOut = []
|
||||
for (const e of data) {
|
||||
const tmp = {
|
||||
licence: e.licence,
|
||||
licence: e.id <= 0 ? -1 : e.licence,
|
||||
pays: e.country,
|
||||
nom: e.lname,
|
||||
prenom: e.fname,
|
||||
genre: e.genre,
|
||||
weight: e.weight,
|
||||
categorie: e.categorie,
|
||||
weight: e.weightReal ? e.weightReal : e.weight,
|
||||
categorie: getCatName(e.categorie),
|
||||
overCategory: e.overCategory,
|
||||
categorie2: getCatName(applyOverCategory(e.categorie, e.overCategory)),
|
||||
club: e.club ? e.club.name : '',
|
||||
...catColumns
|
||||
}
|
||||
for (const c of e.categoriesInscrites) {
|
||||
tmp[c] = "X"
|
||||
}
|
||||
dataOut.push(tmp)
|
||||
}
|
||||
dataOut.sort((a, b) => a.prenom.localeCompare(b.prenom) || a.nom.localeCompare(b.nom));
|
||||
|
||||
const secondHeaders = [
|
||||
"Licence", "Pays", "Nom", "Prénom", "Genre", "Poids",
|
||||
"Catégorie normalizer", "Surclassement", "Catégorie d'inscription", "Club",
|
||||
...Object.keys(catColumns).map(id => data2.find(p => p.id === Number(id))?.name)
|
||||
];
|
||||
const headers = [
|
||||
"", "", "", "", "", "", "", "", "", "", "Catégories",
|
||||
...Object.keys(catColumns).map(() => "")
|
||||
];
|
||||
|
||||
const orderedData = dataOut.map(row => columnOrder.map(col => row[col]));
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.json_to_sheet(dataOut);
|
||||
XLSX.utils.sheet_add_aoa(ws, [["Licence", "Nom", "Prénom", "Genre", "Poids", "Catégorie normalizer", "Surclassement", "Club"]], {origin: 'A1'});
|
||||
const ws = XLSX.utils.json_to_sheet([], {skipHeader: true});
|
||||
|
||||
ws["!cols"] = [{wch: 7}, {wch: 16}, {wch: 16}, {wch: 6}, {wch: 6}, {wch: 10}, {wch: 10}, {wch: 60}]
|
||||
XLSX.utils.sheet_add_aoa(ws, [headers, secondHeaders, ...orderedData], {origin: "A1"});
|
||||
|
||||
// Fusionner les cellules pour le titre "Catégories"
|
||||
const mergeStart = XLSX.utils.encode_cell({r: 0, c: 10}); // Ligne 1, colonne K (index 10)
|
||||
const mergeEnd = XLSX.utils.encode_cell({r: 0, c: 10 + Object.keys(catColumns).length - 1});
|
||||
ws["!merges"] = [{s: mergeStart, e: mergeEnd}];
|
||||
|
||||
// 10. Appliquer une rotation de 45° aux en-têtes
|
||||
const headerRow = ws["!rows"] || (ws["!rows"] = {});
|
||||
headerRow[1] = {hpt: 70}; // Hauteur de la première ligne
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const cellRef = XLSX.utils.encode_cell({r: 1, c: i});
|
||||
if (!ws[cellRef]) ws[cellRef] = {};
|
||||
ws[cellRef].s = {
|
||||
alignment: {textRotation: 45, vertical: "bottom", wrapText: true}
|
||||
};
|
||||
}
|
||||
|
||||
ws["!cols"] = [{wch: 5}, {wch: 4}, {wch: 16}, {wch: 16}, {wch: 4}, {wch: 4}, {wch: 10}, {wch: 4}, {wch: 10}, {wch: 60},
|
||||
...Object.keys(catColumns).map(() => ({wch: 2}))]
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Feuille 1");
|
||||
XLSX.writeFile(wb, "output.xlsx");
|
||||
@@ -610,6 +828,113 @@ function FileOutput({data}) {
|
||||
);
|
||||
}
|
||||
|
||||
function FileImportGuest({data2, sendRegisters}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const expectedFields = [
|
||||
{key: 'nom', label: t('nom'), mandatory: true, type: 'String'},
|
||||
{key: 'prenom', label: t('prenom'), mandatory: true, type: 'String'},
|
||||
{key: 'pays', label: t('pays'), mandatory: false, type: 'String'},
|
||||
{key: 'genre', label: t('genre'), mandatory: false, type: 'String'},
|
||||
{key: 'weight', label: t('poids'), mandatory: false, type: 'Integer'},
|
||||
{key: 'categorie', label: t('catégorie'), mandatory: true, type: 'String'},
|
||||
{key: 'club', label: t('club', {count: 1}), mandatory: false, type: 'String'},
|
||||
];
|
||||
|
||||
if (data2)
|
||||
data2.forEach(row => {
|
||||
expectedFields.push({key: "__" + row.id, label: row.name, mandatory: false, type: 'Boolean'})
|
||||
})
|
||||
|
||||
|
||||
const onDataMapped = (mappedData) => {
|
||||
const out = []
|
||||
mappedData.forEach(row => {
|
||||
if (!row.nom || !row.prenom || !row.categorie) {
|
||||
toast.warn(t('ligneIgnorée1'))
|
||||
return;
|
||||
}
|
||||
|
||||
const categoriesInscrites = []
|
||||
data2.forEach(cat => {
|
||||
if (row["__" + cat.id]) {
|
||||
categoriesInscrites.push(cat.id)
|
||||
}
|
||||
delete row["__" + cat.id]
|
||||
})
|
||||
out.push({
|
||||
id: 0,
|
||||
licence: -1,
|
||||
fname: row.prenom.trim(),
|
||||
lname: row.nom.trim(),
|
||||
country: row.pays ? row.pays.trim() : "FR",
|
||||
genre: row.genre ? row.genre.trim() : "NA",
|
||||
categorie: getCatFromName(row.categorie.trim()),
|
||||
club: row.club ? row.club.trim() : "",
|
||||
weight: row.weight,
|
||||
overCategory: 0,
|
||||
lockEdit: false,
|
||||
categoriesInscrites: categoriesInscrites
|
||||
})
|
||||
})
|
||||
|
||||
sendRegisters(out)
|
||||
}
|
||||
|
||||
return <FileImport onDataMapped={onDataMapped} expectedFields={expectedFields} textButton={t('importerDesInvités')}/>
|
||||
}
|
||||
|
||||
function FileImportComb({data2, sendRegisters}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const expectedFields = [
|
||||
{key: 'licence', label: t('licence'), mandatory: true, type: 'Integer'},
|
||||
{key: 'nom', label: t('nom'), mandatory: true, type: 'String'},
|
||||
{key: 'prenom', label: t('prenom'), mandatory: true, type: 'String'},
|
||||
{key: 'weight', label: t('poids'), mandatory: false, type: 'Integer'},
|
||||
{key: 'overCategory', label: t('comp.modal.surclassement'), mandatory: false, type: 'Integer'},
|
||||
];
|
||||
|
||||
if (data2)
|
||||
data2.forEach(row => {
|
||||
expectedFields.push({key: "__" + row.id, label: row.name, mandatory: false, type: 'Boolean'})
|
||||
})
|
||||
|
||||
const onDataMapped = (mappedData) => {
|
||||
const out = []
|
||||
mappedData.forEach(row => {
|
||||
if (row.licence && row.licence <= 0)
|
||||
return;
|
||||
if (!(row.licence || (row.nom && row.prenom))) {
|
||||
toast.warn(t('ligneIgnorée2'))
|
||||
return;
|
||||
}
|
||||
|
||||
const categoriesInscrites = []
|
||||
data2.forEach(cat => {
|
||||
if (row["__" + cat.id]) {
|
||||
categoriesInscrites.push(cat.id)
|
||||
}
|
||||
delete row["__" + cat.id]
|
||||
})
|
||||
out.push({
|
||||
id: 0,
|
||||
licence: row.licence,
|
||||
fname: row.prenom.trim(),
|
||||
lname: row.nom.trim(),
|
||||
weight: row.weight,
|
||||
overCategory: row.overCategory,
|
||||
lockEdit: false,
|
||||
categoriesInscrites: categoriesInscrites
|
||||
})
|
||||
})
|
||||
|
||||
sendRegisters(out)
|
||||
}
|
||||
|
||||
return <FileImport onDataMapped={onDataMapped} expectedFields={expectedFields} textButton={t('importerDesCombattants')}/>
|
||||
}
|
||||
|
||||
function Def() {
|
||||
return <div className="list-group">
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
|
||||
@@ -3,9 +3,9 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
|
||||
import {useFetch} from "../../hooks/useFetch.js";
|
||||
import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {useAuth} from "../../hooks/useAuth.jsx";
|
||||
import {apiAxios, getToastMessage, isClubAdmin} from "../../utils/Tools.js";
|
||||
import {apiAxios, applyOverCategory, getCatName, getToastMessage, isClubAdmin} from "../../utils/Tools.js";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
@@ -74,8 +74,8 @@ function MakeContent({data}) {
|
||||
disabled={new Date() < new Date(data.startRegister.split('+')[0]) || new Date() > new Date(data.endRegister.split('+')[0])}
|
||||
onClick={_ => navigate("/competition/" + data.id + "/club/register")}>{t('comp.inscription')}</button>
|
||||
}
|
||||
{data.registerMode === "FREE" && !isClubAdmin(userinfo) &&
|
||||
<SelfRegister data2={data}/>
|
||||
{data.registerMode === "FREE" && !isClubAdmin(userinfo) && <SelfRegister data2={data}/>
|
||||
|| <ShowRegister data2={data}/>
|
||||
}
|
||||
{data.registerMode === "HELLOASSO" &&
|
||||
<p><strong>{t('comp.billetterie')} :</strong> <a
|
||||
@@ -97,15 +97,18 @@ function SelfRegister({data2}) {
|
||||
const {id} = useParams()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, refresh, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1)
|
||||
const {data: data3, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [weight, setWeight] = useState("")
|
||||
const [cat, setCat] = useState(0)
|
||||
const [categories, setCategories] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.length > 0) {
|
||||
setWeight(data[0].weight || "")
|
||||
setCat(data[0].overCategory || 0)
|
||||
setCategories(data[0].categoriesInscrites || [])
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -129,22 +132,41 @@ function SelfRegister({data2}) {
|
||||
|
||||
const handleSubmit = () => {
|
||||
sendSubmit({
|
||||
licence: 0, fname: "", lname: "", weight: weight, overCategory: cat, lockEdit: false, id: null
|
||||
licence: 0, fname: "", lname: "", weight: weight, overCategory: cat, lockEdit: false, id: null, categoriesInscrites: categories
|
||||
})
|
||||
}
|
||||
|
||||
const setCategories_ = (e, catId) => {
|
||||
if (e.target.checked) {
|
||||
if (!categories.includes(catId)) {
|
||||
setCategories([...categories, catId])
|
||||
}
|
||||
} else {
|
||||
setCategories(categories.filter(c => c !== catId))
|
||||
}
|
||||
}
|
||||
|
||||
const currenCat = data?.length > 0 && data[0]?.categorie !== "" ? applyOverCategory(data[0]?.categorie, cat) : "";
|
||||
const availableCats = data3 ? (currenCat !== "" ? data3.filter(c => c.categories.some(c2 => c2.categorie === currenCat)) : data3).sort((a, b) => a.name.localeCompare(b.name)) : []
|
||||
|
||||
return <>
|
||||
{data
|
||||
? data.length > 0
|
||||
? <div style={{textAlign: "right", maxWidth: "20em"}}>
|
||||
? <div style={{textAlign: "right", maxWidth: "30em"}}>
|
||||
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
|
||||
<div className="input-group mb-3">
|
||||
<div className="input-group mb-3" hidden={!(data2?.requiredWeight.includes(currenCat))}>
|
||||
<span className="input-group-text" id="weight">{t("comp.modal.poids")}</span>
|
||||
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight" disabled={disabled}
|
||||
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight" disabled={disabled}
|
||||
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
|
||||
{data[0]?.weightReal && <>
|
||||
<span className="input-group-text" id="weight">{t("comp.modal.pesé")}</span>
|
||||
<input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weight" disabled={true}
|
||||
name="weight" aria-describedby="weight" value={data[0]?.weightReal} onChange={() => {
|
||||
}}/>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
<div style={{textAlign: "left"}}>{t('comp.catégorieNormalisée')}: {data[0].categorie}</div>
|
||||
<div style={{textAlign: "left"}}>{t('comp.catégorieNormalisée')}: {getCatName(data[0].categorie)}</div>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text" id="categorie">{t("comp.modal.surclassement")}</span>
|
||||
<select className="form-select" aria-label="categorie" name="categorie" value={cat} disabled={disabled}
|
||||
@@ -155,12 +177,31 @@ function SelfRegister({data2}) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap mb-3">
|
||||
<label htmlFor="inputState2" className="form-label align-self-center" style={{margin: "0 0.5em 0 0"}}>
|
||||
{t('catégorie')} :
|
||||
</label>
|
||||
{error2 ? <AxiosError error={error2}/> : <>
|
||||
{availableCats && availableCats.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
|
||||
{availableCats && availableCats.map((cat, index) =>
|
||||
<div key={cat.id} className="input-group"
|
||||
style={{display: "contents"}}>
|
||||
<div className="input-group-text">
|
||||
<input className="form-check-input mt-0" type="checkbox"
|
||||
id={"categoriesInput" + index} checked={categories.includes(cat.id)} aria-label={cat.name}
|
||||
onChange={e => setCategories_(e, cat.id)}/>
|
||||
<label style={{marginLeft: "0.5em"}} htmlFor={"categoriesInput" + index}>{cat.name}</label>
|
||||
</div>
|
||||
</div>)}
|
||||
</>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" className="btn btn-danger" disabled={disabled} style={{marginRight: "0.5em"}}
|
||||
onClick={handleUnregister}>{t('button.seDésinscrire')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" disabled={disabled}
|
||||
onClick={handleSubmit}>{t('button.enregister')}
|
||||
onClick={handleSubmit}>{t('button.enregistrer')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,6 +213,44 @@ function SelfRegister({data2}) {
|
||||
</>
|
||||
}
|
||||
|
||||
function ShowRegister({data2}) {
|
||||
const {id} = useParams()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1)
|
||||
const {data: data3, error: error2} = useFetch(`/competition/${id}/categories`, setLoading, 1)
|
||||
const {t} = useTranslation();
|
||||
|
||||
const currenCat = data?.length > 0 && data[0]?.categorie !== "" ? applyOverCategory(data[0]?.categorie, data[0].overCategory) : "";
|
||||
|
||||
return <>
|
||||
{data ? data.length > 0
|
||||
? <div style={{textAlign: "right", maxWidth: "30em"}}>
|
||||
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
|
||||
<div style={{textAlign: "left"}}>{t('comp.catégorieNormalisée')} : {getCatName(data[0].categorie)}</div>
|
||||
<div style={{textAlign: "left"}}>{t('comp.modal.surclassement')} :
|
||||
{data[0].overCategory === 0 && ` ${t('aucun')}`}
|
||||
{data[0].overCategory === 1 && ` ${t('1Catégorie')}`}
|
||||
{data[0].overCategory === 2 && ` ${t('2Catégorie')}`}
|
||||
</div>
|
||||
{data2?.requiredWeight.includes(currenCat) &&
|
||||
<div style={{textAlign: "left"}}>{t("comp.modal.poids")} : {data[0].weight} {data[0]?.weightReal && <>
|
||||
({t("comp.modal.pesé")} : {data[0]?.weightReal})</>}</div>}
|
||||
|
||||
<div style={{textAlign: "left"}}>{t('catégorie')} :
|
||||
{error2 ? <AxiosError error={error2}/> : <>
|
||||
{data3 && data3.length === 0 && <div>{t('aucuneCatégorieDisponible')}</div>}
|
||||
{data3 && data3.filter(c => data[0].categoriesInscrites.includes(c.id)).sort((a, b) => a.name.localeCompare(b.name)).map(cat =>
|
||||
<span key={cat.id} className="badge text-bg-secondary" style={{margin: "0 0.25em"}}>{cat.name}</span>)}
|
||||
</>}
|
||||
</div>
|
||||
</div> : <span>{t('vousNêtesPasEncoreInscrit')}</span>
|
||||
: error
|
||||
? <AxiosError error={error}/>
|
||||
: <Def/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function Def() {
|
||||
return <div className="list-group">
|
||||
<li className="list-group-item"><ThreeDots/></li>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import React, {useEffect, useId, 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 {build_tree, from_sendTree, resize_tree} from "../../../utils/TreeUtils.js"
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {CategoryContent} from "./CategoryAdminContent.jsx";
|
||||
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
|
||||
@@ -11,10 +11,17 @@ 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";
|
||||
import {faFile, faGlobe, faPrint, faTableCellsLarge, faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import {getToastMessage} from "../../../utils/Tools.js";
|
||||
import {getToastMessage, toDataURL, win_end} from "../../../utils/Tools.js";
|
||||
import {copyStyles} from "../../../utils/copyStyles.js";
|
||||
import {StateWindow} from "./StateWindow.jsx";
|
||||
import {CombName, useCombs} from "../../../hooks/useComb.jsx";
|
||||
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
|
||||
import {AutoNewCatModalContent, AutoNewCatSModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
|
||||
import {makePDF} from "../../../utils/cmPdf.js";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -32,7 +39,10 @@ export function CMAdmin({compUuid}) {
|
||||
...cat_,
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type
|
||||
type: data.type,
|
||||
treeAreClassement: data.treeAreClassement,
|
||||
fullClassement: data.fullClassement,
|
||||
preset: data.preset,
|
||||
}))
|
||||
}
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
@@ -42,7 +52,7 @@ export function CMAdmin({compUuid}) {
|
||||
return <>
|
||||
<div className="card">
|
||||
<div className='card-header'>
|
||||
<CategoryHeader cat={cat} setCatId={setCatId}/>
|
||||
<CategoryHeader cat={cat} setCatId={setCatId} menuActions={menuActions}/>
|
||||
</div>
|
||||
|
||||
<div className="card-body">
|
||||
@@ -161,12 +171,20 @@ async function downloadResourcesAsZip(resourceList) {
|
||||
progressText.textContent = i18n.t('téléchargementTerminé!');
|
||||
}
|
||||
|
||||
const windowName = "FFSAFTableStateWindow";
|
||||
|
||||
function Menu({menuActions, compUuid}) {
|
||||
const e = document.getElementById("actionMenu")
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
const teamCardModal = useRef(null);
|
||||
const printModal = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [showStateWin, setShowStateWin] = useState(false)
|
||||
const externalWindow = useRef(null)
|
||||
const containerEl = useRef(document.createElement("div"))
|
||||
|
||||
for (const x of tto)
|
||||
x.dispose();
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
|
||||
@@ -178,6 +196,32 @@ function Menu({menuActions, compUuid}) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem(windowName + "_open") === "true") {
|
||||
handleStateWin();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStateWin = __ => {
|
||||
if (showStateWin === false || !externalWindow.current || externalWindow.current.closed) {
|
||||
externalWindow.current = window.open("", windowName, "width=800,height=600,left=200,top=200")
|
||||
externalWindow.current.document.body.innerHTML = ""
|
||||
externalWindow.current.document.body.appendChild(containerEl.current)
|
||||
copyStyles(document, externalWindow.current.document)
|
||||
|
||||
externalWindow.current.addEventListener("beforeunload", () => {
|
||||
setShowStateWin(false);
|
||||
externalWindow.current.close();
|
||||
externalWindow.current = null;
|
||||
sessionStorage.removeItem(windowName + "_open");
|
||||
});
|
||||
setShowStateWin(true);
|
||||
sessionStorage.setItem(windowName + "_open", "true");
|
||||
} else {
|
||||
externalWindow.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const longPressDown = (button) => {
|
||||
longPress.current.button = button;
|
||||
longPress.current.time = new Date();
|
||||
@@ -201,6 +245,8 @@ function Menu({menuActions, compUuid}) {
|
||||
if (button === "obs") {
|
||||
downloadResourcesAsZip(menuActions.current.resourceList || [])
|
||||
.then(__ => console.log("Ressources téléchargées"));
|
||||
} else if (button === "cards") {
|
||||
teamCardModal.current.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +267,8 @@ function Menu({menuActions, compUuid}) {
|
||||
}
|
||||
|
||||
const copyScriptToClipboard = () => {
|
||||
navigator.clipboard.writeText(`<!--suppress ALL -->
|
||||
// noinspection JSFileReferences
|
||||
navigator.clipboard.writeText(`
|
||||
<div id='safca_api_data'></div>
|
||||
<script type="module">
|
||||
import {initCompetitionApi} from '${vite_url}/competition.js';
|
||||
@@ -240,8 +287,19 @@ function Menu({menuActions, compUuid}) {
|
||||
{createPortal(
|
||||
<>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={faFile} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer"}}
|
||||
onMouseDown={() => longPressDown("cards")}
|
||||
onMouseUp={() => longPressUp("cards")}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t("carton")}/>
|
||||
<FontAwesomeIcon icon={faPrint} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer"}}
|
||||
onClick={() => printModal.current.click()}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t('imprimer')}/>
|
||||
<FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
|
||||
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
style={{color: "#6c757d", cursor: "pointer"}}
|
||||
onMouseDown={() => longPressDown("obs")}
|
||||
onMouseUp={() => longPressUp("obs")}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
@@ -251,7 +309,12 @@ function Menu({menuActions, compUuid}) {
|
||||
onClick={() => copyScriptToClipboard()}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t('ttm.admin.scripte')}/>
|
||||
<FontAwesomeIcon icon={faTableCellsLarge} size="xl"
|
||||
style={{color: showStateWin ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handleStateWin}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('etatDesTablesDeMarque')}/>
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
{externalWindow.current && createPortal(<StateWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
@@ -268,9 +331,8 @@ function Menu({menuActions, compUuid}) {
|
||||
<strong>{t('config.obs.warn1')}</strong>
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">{t('adresseDuServeur')}</span>
|
||||
<span className="input-group-text">{t('config.obs.ws')}</span>
|
||||
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label=""
|
||||
defaultValue={"127.0.0.1:4455"}/>
|
||||
<input type="text" className="form-control" placeholder="ws://127.0.0.1:4455/" aria-label=""
|
||||
defaultValue={"ws://127.0.0.1:4455/"}/>
|
||||
<span className="input-group-text">/</span>
|
||||
</div>
|
||||
<div className="input-group mb-3">
|
||||
@@ -308,13 +370,312 @@ function Menu({menuActions, compUuid}) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ref={teamCardModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#TeamCardModal"
|
||||
style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
</button>
|
||||
<div className="modal modal-xl fade" id="TeamCardModal" tabIndex="-1" aria-labelledby="TeamCardModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<TeamCardModal/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ref={printModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#PrintModal"
|
||||
style={{display: 'none'}}>
|
||||
Launch printModal
|
||||
</button>
|
||||
<div className="modal fade" id="PrintModal" tabIndex="-1" aria-labelledby="PrintModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<PrintModal menuActions={menuActions}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function CategoryHeader({cat, setCatId}) {
|
||||
function PrintModal({menuActions}) {
|
||||
const [categorie, setCategorie] = useState(false);
|
||||
const [categorieEmpty, setCategorieEmpty] = useState(false);
|
||||
const [preset, setPreset] = useState(false);
|
||||
const [presetEmpty, setPresetEmpty] = useState(false);
|
||||
const [allCat, setAllCat] = useState(false);
|
||||
const [allCatEmpty, setAllCatEmpty] = useState(false);
|
||||
const [podium, setPodium] = useState(false);
|
||||
const [podiumRank, setPodiumRank] = useState(4);
|
||||
const [podiumClub, setPodiumClub] = useState(false);
|
||||
|
||||
const [presetSelect, setPresetSelect] = useState(-1)
|
||||
|
||||
const {sendRequest, welcomeData} = useWS();
|
||||
const {getComb} = useCombs();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const podiumPromise = (podiumRank_) => {
|
||||
return sendRequest("getPodium", {}).then(data => {
|
||||
return [welcomeData?.name + " - " + t('podium'), [
|
||||
{type: "podium", params: ({data, maxRank: podiumRank_, minRank: Math.min(4, podiumRank_)})},
|
||||
]];
|
||||
});
|
||||
}
|
||||
|
||||
const podiumClubPromise = () => {
|
||||
return sendRequest("getPodiumClub", {}).then(data => {
|
||||
return [welcomeData?.name + " - " + t('classementDesClub', {ns: "result"}), [
|
||||
{type: "podiumClub", params: ({data})},
|
||||
]];
|
||||
});
|
||||
}
|
||||
|
||||
const print = (action) => {
|
||||
const pagesPromise = [];
|
||||
|
||||
if (categorie && menuActions.printCategorie)
|
||||
pagesPromise.push(menuActions.printCategorie(categorieEmpty))
|
||||
|
||||
if (preset && menuActions.printCategoriePreset)
|
||||
pagesPromise.push(menuActions.printCategoriePreset(presetEmpty, presetSelect))
|
||||
|
||||
if (allCat && menuActions.printAllCategorie)
|
||||
pagesPromise.push(menuActions.printAllCategorie(categorieEmpty, welcomeData?.name + " - " + t('toutesLesCatégories')))
|
||||
|
||||
if (podium)
|
||||
pagesPromise.push(podiumPromise(podiumRank));
|
||||
|
||||
if (podiumClub)
|
||||
pagesPromise.push(podiumClubPromise());
|
||||
|
||||
toast.promise(
|
||||
toDataURL("/Logo-FFSAF-2023.png").then(logo => {
|
||||
return Promise.allSettled(pagesPromise).then(results => {
|
||||
const pages = [];
|
||||
const names = [];
|
||||
let errors = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled") {
|
||||
const [name, page, error] = result.value;
|
||||
pages.push(...page);
|
||||
names.push(name);
|
||||
errors += error
|
||||
} else if (result.status === "rejected") {
|
||||
errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors > 0) {
|
||||
toast.error(t('erreurGénérationPages', {count: errors}));
|
||||
}
|
||||
|
||||
if (pages.length !== 0) {
|
||||
makePDF(action, pages, names.join(" - "), welcomeData?.name, getComb, t, logo)
|
||||
}
|
||||
})
|
||||
}), getToastMessage("toast.print", "cm"))
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{t('quoiImprimer?')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={categorie} id="checkPrint"
|
||||
onChange={e => setCategorie(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint">{t('catégorieSélectionnée')}</label>
|
||||
</div>
|
||||
{categorie &&
|
||||
<div className="form-check" style={{marginLeft: "1em"}}>
|
||||
<input className="form-check-input" type="checkbox" checked={categorieEmpty} id="checkPrint2"
|
||||
onChange={e => setCategorieEmpty(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint2">{t('feuilleVierge')}</label>
|
||||
</div>}
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={preset} id="checkPrint3"
|
||||
onChange={e => setPreset(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint3">{t('touteLaCatégorie')}</label>
|
||||
</div>
|
||||
{preset && <div style={{marginLeft: "1em"}}>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={presetEmpty} id="checkPrint4"
|
||||
onChange={e => setPresetEmpty(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint4">{t('feuilleVierge')}</label>
|
||||
</div>
|
||||
<ListPresetSelect value={presetSelect} onChange={setPresetSelect}/>
|
||||
</div>}
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={allCat} id="checkPrint5"
|
||||
onChange={e => setAllCat(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint5">{t('toutesLesCatégories')}</label>
|
||||
</div>
|
||||
{allCat &&
|
||||
<div className="form-check" style={{marginLeft: "1em"}}>
|
||||
<input className="form-check-input" type="checkbox" checked={allCatEmpty} id="checkPrint6"
|
||||
onChange={e => setAllCatEmpty(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint6">{t('feuilleVierge')}</label>
|
||||
</div>}
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={podium} id="checkPrint7"
|
||||
onChange={e => setPodium(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint7">{t('podium')}</label>
|
||||
</div>
|
||||
{podium &&
|
||||
<div style={{marginLeft: "1em"}}>
|
||||
<label htmlFor="range3" className="form-label">{t('jusquauRang')} {podiumRank} </label>
|
||||
<input type="range" className="form-range" min="1" max="20" step="1" id="range3" value={podiumRank}
|
||||
onChange={e => setPodiumRank(Number(e.target.value))}/>
|
||||
</div>}
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" checked={podiumClub} id="checkPrint8"
|
||||
onChange={e => setPodiumClub(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkPrint8">{t('classementDesClub', {ns: "result"})}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("show")}>{t('afficher')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("download")}>{t('enregistrer')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={() => print("print")}>{t('imprimer')}</button>
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function TeamCardModal() {
|
||||
const [club, setClub] = useState("")
|
||||
|
||||
const {t} = useTranslation("cm");
|
||||
const {combs} = useCombs()
|
||||
const {sendRequest} = useWS()
|
||||
const {cards_t, cards_v} = useCards()
|
||||
const cardDispatch = useCardsDispatch();
|
||||
const {data} = useRequestWS("getAllForTeamNoDetail", {}, null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
for (const card of data) {
|
||||
cardDispatch({type: 'SET_TEAM_CARD', payload: card});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
let clubList = [];
|
||||
if (combs != null) {
|
||||
clubList = Object.values(combs).map(d => d.club_str).filter((v, i, a) => v !== "" && v !== undefined && a.indexOf(v) === i);
|
||||
}
|
||||
|
||||
const handleAdd = (e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(sendRequest("applyTeamCards", {
|
||||
teamUuid: Object.values(combs).find(d => d.club_str === club)?.club_uuid,
|
||||
teamName: club,
|
||||
type: "YELLOW"
|
||||
}),
|
||||
getToastMessage("toast.card.team", "cm"))
|
||||
.then(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const GetCard = ({type}) => {
|
||||
if (!type)
|
||||
return <></>
|
||||
let bg = "";
|
||||
switch (type) {
|
||||
case "YELLOW":
|
||||
bg = " bg-warning";
|
||||
break;
|
||||
case "RED":
|
||||
bg = " bg-danger";
|
||||
break;
|
||||
case "BLACK":
|
||||
bg = " bg-dark text-white";
|
||||
break;
|
||||
case "BLUE":
|
||||
bg = " bg-primary text-white";
|
||||
break;
|
||||
}
|
||||
return <span className={"badge border border-light p-2" + bg}><span className="visually-hidden">card</span></span>
|
||||
}
|
||||
|
||||
let cards = [...cards_t, ...cards_v].sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{t('carton')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<h5>{t('cartonDéquipe')}</h5>
|
||||
<div className="input-group mb-3">
|
||||
<label htmlFor="inputGroupSelect09" className="input-group-text">{t('club')}</label>
|
||||
<select id="inputGroupSelect09" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
|
||||
{clubList.sort((a, b) => a.localeCompare(b)).map((club, index) => (
|
||||
<option key={index} value={club}>{club}</option>))}
|
||||
</select>
|
||||
<button className="btn btn-outline-primary" type="button" onClick={handleAdd}>{t("ajouter")}</button>
|
||||
</div>
|
||||
|
||||
<h5>{t('listeDesCartons')}</h5>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{t('date')}</th>
|
||||
<th scope="col">{t('type')}</th>
|
||||
<th scope="col">{t('couleur')}</th>
|
||||
<th scope="col">{t('nom')}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cards.map((card, index) => <tr key={index}>
|
||||
<td scope="row">{new Date(card.date).toLocaleString()}</td>
|
||||
{card.teamName ? <>
|
||||
<td>{t('team')}</td>
|
||||
<td><GetCard type={card.type}/></td>
|
||||
<td>{card.teamName}</td>
|
||||
</> : <>
|
||||
<td>{card.teamCard ? "|-> " + t('team') : t('individuelle')} </td>
|
||||
<td><GetCard type={card.type}/></td>
|
||||
<td><CombName combId={card.comb}/></td>
|
||||
</>}
|
||||
|
||||
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer ce carton ?")) {
|
||||
if (card.teamName) {
|
||||
toast.promise(sendRequest("removeTeamCards", {
|
||||
teamUuid: card.teamUuid, teamName: card.teamName, type: card.type
|
||||
}),
|
||||
getToastMessage("toast.card.team", "cm"))
|
||||
.then(() => {
|
||||
})
|
||||
} else {
|
||||
sendRequest('sendCardRm', {matchId: card.match, combId: card.comb, type: card.type})
|
||||
.then(() => toast.success(t('cardRemoved')))
|
||||
.catch(err => toast.error(err))
|
||||
}
|
||||
}
|
||||
}}><FontAwesomeIcon icon={faTrash}/></td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function CategoryHeader({cat, setCatId, menuActions}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const bthRef = useRef();
|
||||
const confirmRef = useRef();
|
||||
const bthRef = useRef(null);
|
||||
const newBthRef = useRef(null);
|
||||
const confirmRef = useRef(null);
|
||||
const [modal, setModal] = useState({})
|
||||
const [confirm, setConfirm] = useState({})
|
||||
const {t} = useTranslation("cm");
|
||||
@@ -330,6 +691,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
])
|
||||
}
|
||||
const sendAddCategory = ({data}) => {
|
||||
console.log("add cat", data);
|
||||
setCats([...cats, data])
|
||||
}
|
||||
const sendDelCategory = ({data}) => {
|
||||
@@ -363,8 +725,7 @@ function CategoryHeader({cat, setCatId}) {
|
||||
if (selectedCatId !== "-1") {
|
||||
setCatId(selectedCatId);
|
||||
} else { // New category
|
||||
setModal({});
|
||||
bthRef.current.click();
|
||||
newBthRef.current.click();
|
||||
e.target.value = cat?.id;
|
||||
}
|
||||
}
|
||||
@@ -382,14 +743,14 @@ function CategoryHeader({cat, setCatId}) {
|
||||
</div>
|
||||
<div className="col" style={{margin: "auto 0", textAlign: "center"}}>
|
||||
{cat &&
|
||||
<div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? t('tournois') : ""} |
|
||||
<div>Type: {(cat.type & 1) !== 0 ? t('poule') : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? (cat.treeAreClassement ? t('classement') : t('tournois')) : ""} |
|
||||
Zone: {cat.liceName}</div>}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary float-end" onClick={() => {
|
||||
setModal(cat);
|
||||
bthRef.current.click();
|
||||
}} disabled={cat === null}>Modifier
|
||||
}} disabled={cat === null}>{t('modifier')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -404,37 +765,191 @@ function CategoryHeader({cat, setCatId}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="autoNewCatModal" aria-hidden="true" aria-labelledby="autoNewCatModalLabel" tabIndex="-1">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
<AutoNewCatModalContent/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="autoNewCatsModal" aria-hidden="true" aria-labelledby="autoNewCatsModalLabel" tabIndex="-1">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
<AutoNewCatSModalContent/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ref={confirmRef} data-bs-toggle="modal" data-bs-target="#confirm-dialog" style={{display: "none"}}>open</button>
|
||||
<ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
|
||||
}} onCancel={confirm.cancel ? confirm.cancel : () => {
|
||||
}} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/>
|
||||
|
||||
|
||||
<button ref={newBthRef} data-bs-toggle="modal" data-bs-target="#newCatModal" style={{display: "none"}}>open</button>
|
||||
|
||||
<div className="modal fade" id="newCatModal" tabIndex="-1" aria-labelledby="newCatModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="newCatModalLabel">{t('ajouter')} {t('uneCatégorie')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body text-center">
|
||||
{t('modeDeCréation')} :
|
||||
<div className="mb-2">
|
||||
<button className="btn btn-primary" onClick={() => {
|
||||
setModal({});
|
||||
bthRef.current.click();
|
||||
e.target.value = cat?.id;
|
||||
}}>{t('personnaliser')}</button>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<button className="btn btn-primary" data-bs-target="#autoNewCatModal"
|
||||
data-bs-toggle="modal">{t('depuisUneCatégoriePrédéfinie')}</button>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<button className="btn btn-primary" data-bs-target="#autoNewCatsModal"
|
||||
data-bs-toggle="modal">{t('créerToutesLesCatégories')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PrintCats menuActions={menuActions} cats={cats}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function PrintCats({menuActions, cats}) {
|
||||
const {cards_v} = useCards();
|
||||
const {sendRequest} = useWS();
|
||||
|
||||
function readAndConvertMatch(matches, data) {
|
||||
matches.push({
|
||||
...data,
|
||||
c1: data.c1?.id,
|
||||
c2: data.c2?.id,
|
||||
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
|
||||
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
|
||||
})
|
||||
}
|
||||
|
||||
const run = (categorieEmpty, cats2, name = "") => {
|
||||
const pagesPromise = cats2.sort((a, b) => a.name.localeCompare(b.name)).map(cat_ => {
|
||||
return sendRequest('getFullCategory', cat_.id)
|
||||
.then((data) => {
|
||||
const cat = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type,
|
||||
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
|
||||
raw_trees: data.trees.sort((a, b) => a.level - b.level),
|
||||
treeAreClassement: data.treeAreClassement,
|
||||
fullClassement: data.fullClassement,
|
||||
preset: data.preset,
|
||||
}
|
||||
if (name === "") {
|
||||
name = data.preset.name;
|
||||
}
|
||||
|
||||
const newCards = {};
|
||||
for (const o of data.cards)
|
||||
newCards[o.id] = o
|
||||
|
||||
let matches2 = [];
|
||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_));
|
||||
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_));
|
||||
|
||||
const activeMatches = matches2.filter(m => m.poule !== '-')
|
||||
const groups = matches2.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i)
|
||||
.map(d => {
|
||||
let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule
|
||||
if (!poule)
|
||||
poule = '-'
|
||||
return {id: d, poule: poule}
|
||||
})
|
||||
|
||||
matches2 = matches2.filter(m => m.categorie === cat.id)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
matches2.forEach(m => {
|
||||
if (m.end && (!m.scores || m.scores.length === 0))
|
||||
m.scores = [{n_round: 0, s1: 0, s2: 0}];
|
||||
})
|
||||
|
||||
return {
|
||||
type: "categorie",
|
||||
params: ({cat, matches: matches2, groups, cards_v: Object.values({...cards_v, ...newCards}), categorieEmpty})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.allSettled(pagesPromise)
|
||||
.then((results) => {
|
||||
const pages = [];
|
||||
let error = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled") {
|
||||
pages.push(result.value);
|
||||
} else {
|
||||
console.error(result.error);
|
||||
error++;
|
||||
}
|
||||
}
|
||||
|
||||
return [name, pages, error];
|
||||
})
|
||||
}
|
||||
|
||||
menuActions.printCategoriePreset = (categorieEmpty, preset) => {
|
||||
return run(categorieEmpty, cats.filter(cat => cat.preset?.id === preset))
|
||||
}
|
||||
|
||||
menuActions.printAllCategorie = (categorieEmpty, name) => {
|
||||
return run(categorieEmpty, cats, name)
|
||||
}
|
||||
}
|
||||
|
||||
function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
const id = useId()
|
||||
const [name, setName] = useState("")
|
||||
const [lice, setLice] = useState("1")
|
||||
const [poule, setPoule] = useState(true)
|
||||
const [tournoi, setTournoi] = useState(false)
|
||||
const [classement, setClassement] = useState(true)
|
||||
const [fullClassement, setFullClassement] = useState(false)
|
||||
const [size, setSize] = useState(4)
|
||||
const [loserMatch, setLoserMatch] = useState(1)
|
||||
const [preset, setPreset] = useState(-1)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const {sendRequest} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state);
|
||||
setName(state.name || "");
|
||||
setLice(state.liceName || "1");
|
||||
setPoule(((state.type || 1) & 1) !== 0);
|
||||
setTournoi((state.type & 2) !== 0);
|
||||
setClassement(state.treeAreClassement !== undefined && state.treeAreClassement !== false);
|
||||
setFullClassement(state.fullClassement !== undefined && state.fullClassement !== false);
|
||||
setPreset(state.preset?.id || -1);
|
||||
|
||||
if (state?.trees && state.trees.length >= 1) {
|
||||
const tree = state.trees[0];
|
||||
let trees_ = []
|
||||
for (let i = 0; i < state?.raw_trees?.length; i++) {
|
||||
if (state.raw_trees.at(i).level > 0) {
|
||||
trees_.push(state.trees.at(i))
|
||||
}
|
||||
}
|
||||
if (trees_ && trees_.length >= 1) {
|
||||
const tree = trees_[0];
|
||||
setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2);
|
||||
|
||||
if (state.trees.length === 1) {
|
||||
if (trees_.length === 1) {
|
||||
setLoserMatch(0);
|
||||
} else if (state.trees.length === 2) {
|
||||
} else if (trees_.length === 2) {
|
||||
setLoserMatch(1);
|
||||
} else {
|
||||
setLoserMatch(-1);
|
||||
@@ -460,24 +975,33 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
return;
|
||||
}
|
||||
|
||||
let trees_ = []
|
||||
for (let i = 0; i < state?.raw_trees?.length; i++) {
|
||||
if (state.raw_trees.at(i).level > 0) {
|
||||
trees_.push(state.trees.at(i))
|
||||
}
|
||||
}
|
||||
if (state?.id) {
|
||||
const applyChanges = () => {
|
||||
const newData = {
|
||||
id: state.id,
|
||||
name: name.trim(),
|
||||
liceName: lice.trim(),
|
||||
type: nType
|
||||
type: nType,
|
||||
treeAreClassement: classement,
|
||||
fullClassement: fullClassement,
|
||||
preset: {id: preset !== -1 ? preset : null}
|
||||
}
|
||||
|
||||
let nbMatch = -1;
|
||||
let oldSubTree = -1;
|
||||
const oldTrees = state?.trees || [];
|
||||
const oldTrees = trees_ || [];
|
||||
if (oldTrees.length >= 1) {
|
||||
const tree = state.trees[0];
|
||||
const tree = trees_[0];
|
||||
nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1);
|
||||
if (state.trees.length === 1)
|
||||
if (trees_.length === 1)
|
||||
oldSubTree = 0
|
||||
else if (state.trees.length === 2)
|
||||
else if (trees_.length === 2)
|
||||
oldSubTree = 1
|
||||
}
|
||||
|
||||
@@ -500,15 +1024,18 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
newTrees.push(trees2.at(i));
|
||||
}
|
||||
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees")
|
||||
toast.promise(sendRequest('updateTrees', {
|
||||
categoryId: state.id,
|
||||
trees: newTrees
|
||||
}), getToastMessage("toast.updateTrees", "cm")
|
||||
).then(__ => {
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
|
||||
})
|
||||
}
|
||||
})
|
||||
confirmRef.current.click();
|
||||
} else {
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory"))
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,29 +1061,23 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
toast.promise(sendRequest('createCategory', {
|
||||
name: name.trim(),
|
||||
liceName: lice.trim(),
|
||||
type: nType
|
||||
}), getToastMessage("toast.createCategory")
|
||||
type: nType,
|
||||
treeAreClassement: classement,
|
||||
fullClassement: fullClassement,
|
||||
preset: {id: preset !== -1 ? preset : null}
|
||||
}), getToastMessage("toast.createCategory", "cm")
|
||||
).then(id => {
|
||||
if (tournoi) {
|
||||
const trees = build_tree(size, loserMatch)
|
||||
console.log("Creating trees for new category:", trees);
|
||||
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init")
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: id, trees: trees}), getToastMessage("toast.updateTrees.init", "cm")
|
||||
).finally(() => setCatId(id))
|
||||
} else {
|
||||
setCatId(id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: name.trim(),
|
||||
liceName: lice.trim(),
|
||||
type: poule + (tournoi << 1),
|
||||
size: size,
|
||||
loserMatch: loserMatch
|
||||
}
|
||||
console.log("Submitting category data:", data);
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
@@ -572,9 +1093,12 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
onChange={e => setName(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<ListPresetSelect value={preset} onChange={setPreset}/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="liceInput1" className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des ';')</small></Trans></label>
|
||||
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice}
|
||||
<label htmlFor={id + "liceInput1"} className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des
|
||||
';')</small></Trans></label>
|
||||
<input type="text" className="form-control" id={id + "liceInput1"} placeholder="1;2" name="zone de combat" value={lice}
|
||||
onChange={e => setLice(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
@@ -592,8 +1116,17 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
onChange={e => setTournoi(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault" disabled={!tournoi} checked={classement}
|
||||
onChange={e => setClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault">
|
||||
{t('leTournoiServiraDePhaseFinaleAuxPoules')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label>
|
||||
<input type="number" className="form-control" id="sizeInput1" placeholder="4" name="size" disabled={!tournoi} value={size}
|
||||
@@ -631,6 +1164,14 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value="" id="checkDefault2" disabled={!tournoi || !classement}
|
||||
checked={fullClassement} onChange={e => setFullClassement(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkDefault2">
|
||||
{t('lesCombattantsEnDehors')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
@@ -640,7 +1181,7 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
title: t('confirm4.title'),
|
||||
message: t('confirm4.msg', {name: state.name}),
|
||||
confirm: () => {
|
||||
toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory")
|
||||
toast.promise(sendRequest('deleteCategory', state.id), getToastMessage("toast.deleteCategory", "cm")
|
||||
).then(() => setCatId(null));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,14 +2,16 @@ import React, {useEffect, useRef, useState} from "react";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {timePrint} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
|
||||
export function ChronoPanel() {
|
||||
export function ChronoPanel({menuActions}) {
|
||||
const [config, setConfig] = useState({
|
||||
time: Number(sessionStorage.getItem("chronoTime") || "90999"),
|
||||
pause: Number(sessionStorage.getItem("chronoPause") || "60999")
|
||||
})
|
||||
const [chrono, setChrono] = useState({time: 0, startTime: 0})
|
||||
const chronoText = useRef(null)
|
||||
const [chronoState, setChronoState] = useState(0)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
@@ -23,6 +25,10 @@ export function ChronoPanel() {
|
||||
return chrono.time + Date.now() - chrono.startTime
|
||||
}
|
||||
|
||||
menuActions.current.setTimerConfig = (time, pause) => {
|
||||
setConfig({time: time + 999, pause: pause + 999})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: state.current.lastTimeStr, timeColor: state.current.color}})
|
||||
}, [])
|
||||
@@ -59,12 +65,15 @@ export function ChronoPanel() {
|
||||
|
||||
if (state_.chronoState === 0 && isRunning()) {
|
||||
state_.chronoState = 1
|
||||
setChronoState(1)
|
||||
} else if (state_.chronoState === 1 && getTime() >= config.time) {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
|
||||
state_.chronoState = 2
|
||||
setChronoState(2)
|
||||
} else if (state_.chronoState === 2 && getTime() >= config.pause) {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
|
||||
state_.chronoState = 1
|
||||
setChronoState(1)
|
||||
}
|
||||
|
||||
if (isRunning()) {
|
||||
@@ -117,6 +126,7 @@ export function ChronoPanel() {
|
||||
<button className="btn btn-danger col" onClick={__ => {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: 0}))
|
||||
state.current.chronoState = 0
|
||||
setChronoState(0)
|
||||
}}>{t('réinitialiser')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -181,5 +191,19 @@ export function ChronoPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SendChrono chrono={chrono} config={config} chronoState={chronoState}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function SendChrono({chrono, config, chronoState}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({chronoState: {...chrono, configTime: config.time, configPause: config.pause, state: chronoState}});
|
||||
sendNotify("sendCurentChrono", {...chrono, configTime: config.time, configPause: config.pause, state: chronoState});
|
||||
}, [chrono]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useRef, useState, useReducer} from "react";
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
||||
@@ -6,13 +6,24 @@ import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {getToastMessage, scorePrint, win} from "../../../utils/Tools.js";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||
import {toast} from "react-toastify";
|
||||
import {
|
||||
CatList,
|
||||
getCatName, getShieldSize,
|
||||
getShieldTypeName, getSwordSize,
|
||||
getSwordTypeName,
|
||||
getToastMessage, timePrint,
|
||||
virtual_end,
|
||||
virtualScore,
|
||||
win_end
|
||||
} from "../../../utils/Tools.js";
|
||||
import "./CMTMatchPanel.css"
|
||||
import {useOBS} from "../../../hooks/useOBS.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {createPortal} from "react-dom";
|
||||
import ProtectionSelector from "../../../components/ProtectionSelector.jsx";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
@@ -20,10 +31,17 @@ function CupImg() {
|
||||
alt=""/>
|
||||
}
|
||||
|
||||
function CupImg2() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
style={{width: "16px"}} src="/img/171892.png"
|
||||
alt=""/>
|
||||
}
|
||||
|
||||
export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
|
||||
const {dispatch} = useWS();
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {connected, setText} = useOBS();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
@@ -53,6 +71,8 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
|
||||
useEffect(() => {
|
||||
setText("poule", cat ? cat.name : "");
|
||||
if (!cat)
|
||||
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
|
||||
}, [cat, connected]);
|
||||
|
||||
return <>
|
||||
@@ -71,9 +91,10 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
|
||||
function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [trees, setTrees] = useState([]);
|
||||
const [trees, setTrees] = useState({raw: [], formatted: []});
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
const combDispatch = useCombsDispatch();
|
||||
const cardDispatch = useCardsDispatch();
|
||||
|
||||
function readAndConvertMatch(matches, data, combsToAdd) {
|
||||
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
|
||||
@@ -89,7 +110,12 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
setLoading(1);
|
||||
sendRequest('getFullCategory', catId)
|
||||
.then((data) => {
|
||||
setTrees(data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
|
||||
setTrees({
|
||||
raw: data.trees.sort((a, b) => a.level - b.level),
|
||||
formatted: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
|
||||
})
|
||||
|
||||
cardDispatch({type: 'SET_ALL', payload: data.cards});
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
@@ -103,7 +129,10 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
const treeListener = ({data}) => {
|
||||
if (data.length < 1 || data[0].categorie !== catId)
|
||||
return
|
||||
setTrees(data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)))
|
||||
setTrees({
|
||||
raw: data.sort((a, b) => a.level - b.level),
|
||||
formatted: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
|
||||
})
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
@@ -128,29 +157,114 @@ function CMTMatchPanel({catId, cat, menuActions}) {
|
||||
reducer({type: 'REMOVE', payload: data})
|
||||
}
|
||||
|
||||
const sendCardboard = ({data}) => {
|
||||
reducer({type: 'UPDATE_CARDBOARD', payload: {...data}})
|
||||
}
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
||||
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
|
||||
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: treeListener})
|
||||
dispatch({type: 'removeListener', payload: matchListener})
|
||||
dispatch({type: 'removeListener', payload: matchOrder})
|
||||
dispatch({type: 'removeListener', payload: deleteMatch})
|
||||
dispatch({type: 'removeListener', payload: sendCardboard})
|
||||
}
|
||||
}, [catId]);
|
||||
|
||||
return <ListMatch cat={cat} matches={matches} trees={trees} menuActions={menuActions}/>
|
||||
return <>
|
||||
<ListMatch cat={cat} matches={matches} trees={trees} menuActions={menuActions}/>
|
||||
<SetTimeToChrono cat={cat} matches={matches} menuActions={menuActions}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function SetTimeToChrono({cat, matches, menuActions}) {
|
||||
const [catAverage, setCatAverage] = useState("---");
|
||||
const [genreAverage, setGenreAverage] = useState("H");
|
||||
const [nbComb, setNbComb] = useState(0);
|
||||
const [preset, setPreset] = useState(undefined);
|
||||
const [time, setTime] = useState({round: 0, pause: 0});
|
||||
const {cards_v} = useCards();
|
||||
|
||||
const {t} = useTranslation("cm");
|
||||
const {getComb} = useCombs();
|
||||
|
||||
useEffect(() => {
|
||||
if (!cat || matches.filter(m => m.categorie === cat.id).length === 0) {
|
||||
setCatAverage("---");
|
||||
setNbComb(0);
|
||||
return;
|
||||
}
|
||||
setPreset(cat.preset);
|
||||
|
||||
const genres = [];
|
||||
const cats = [];
|
||||
const combs = [];
|
||||
for (const m of matches.filter(m => m.categorie === cat.id)) {
|
||||
if (m.c1 && !combs.includes(m.c1))
|
||||
combs.push(m.c1);
|
||||
if (m.c2 && !combs.includes(m.c2))
|
||||
combs.push(m.c2);
|
||||
}
|
||||
setNbComb(combs.length);
|
||||
|
||||
combs.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
|
||||
.forEach(c => {
|
||||
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
|
||||
genres.push(c.genre)
|
||||
});
|
||||
|
||||
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
|
||||
setCatAverage(CatList.at(catAvg) || "---");
|
||||
|
||||
const genreAvg = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length);
|
||||
setGenreAverage(genreAvg > 0.5 ? "F" : "H");
|
||||
|
||||
if (!cat.preset || !cat.preset.categories)
|
||||
return;
|
||||
|
||||
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
|
||||
|
||||
let p;
|
||||
if (catAvailable.includes(catAvg)) {
|
||||
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
|
||||
} else {
|
||||
const closest = catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a);
|
||||
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === closest);
|
||||
}
|
||||
menuActions.current.setTimerConfig(p.roundDuration, p.pauseDuration)
|
||||
setTime({round: p.roundDuration, pause: p.pauseDuration})
|
||||
|
||||
}, [cat, matches]);
|
||||
|
||||
const marches2 = matches.filter(m => m.categorie === cat.id)
|
||||
.map(m => ({...m, end: m.end || virtual_end(m, cards_v)}))
|
||||
|
||||
return createPortal(<div className="card mb-3">
|
||||
<div className="card-header">{t('informationCatégorie')}</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col text-start">
|
||||
<div>{t('catégorie')} : {getCatName(catAverage)}</div>
|
||||
<div>{t('arme', {ns: 'common'})} : {getSwordTypeName(preset?.sword)} - {t('taille')} {getSwordSize(preset?.sword, catAverage, genreAverage)}</div>
|
||||
<div>{t('bouclier', {ns: 'common'})} : {getShieldTypeName(preset?.shield)} - {t('taille')} {getShieldSize(preset?.shield, catAverage)}</div>
|
||||
<div>{t('duréeRound')} : {timePrint(time.round)}</div>
|
||||
<div>{t('duréePause')} : {timePrint(time.pause)}</div>
|
||||
<div>{t('matchTerminé')}: {marches2.filter(m => m.end).length} sur {marches2.length}</div>
|
||||
<div>{t('nombreDeCombattants')} : {nbComb}</div>
|
||||
</div>
|
||||
<div className="col text-center">
|
||||
<h6>{t('protectionObligatoire', {ns: 'common'})} :</h6>
|
||||
<ProtectionSelector shield={preset?.shield !== "NONE"}
|
||||
mandatoryProtection={CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
|
||||
preset?.mandatoryProtection1 : preset?.mandatoryProtection2} setMandatoryProtection={() => {
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, document.getElementById("infoCategory"))
|
||||
}
|
||||
|
||||
function ListMatch({cat, matches, trees, menuActions}) {
|
||||
const [type, setType] = useState(1);
|
||||
const {sendRequest} = useWS();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,6 +274,10 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
setType(cat.type);
|
||||
}, [cat]);
|
||||
|
||||
const handleCreatClassement = () => {
|
||||
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
|
||||
}
|
||||
|
||||
if (!cat)
|
||||
return <></>;
|
||||
|
||||
@@ -173,7 +291,7 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
||||
onClick={_ => setType(2)}>{t('tournois')}
|
||||
onClick={_ => setType(2)}>{(cat.treeAreClassement ? t('classement') : t('tournois'))}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -185,24 +303,33 @@ function ListMatch({cat, matches, trees, menuActions}) {
|
||||
</>}
|
||||
|
||||
{type === 2 && <>
|
||||
<BuildTree treeData={trees} matches={matches} menuActions={menuActions}/>
|
||||
{cat.treeAreClassement && !matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && (m.c1 !== undefined || m.c2 !== undefined)) ? <>
|
||||
<button className="btn btn-primary" onClick={handleCreatClassement}>{t('créerLesMatchesDeClassement')}</button>
|
||||
</> : <BuildTree treeData={trees} matches={matches} cat={cat} menuActions={menuActions}/>}
|
||||
</>}
|
||||
</div>
|
||||
}
|
||||
|
||||
function MatchList({matches, cat, menuActions}) {
|
||||
function MatchList({matches, cat, menuActions, classement = false, currentMatch = null, setCurrentMatch, getNext}) {
|
||||
const [activeMatch, setActiveMatch] = useState(null)
|
||||
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
const {cards_v, getHeightCardForCombInMatch} = useCards();
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
const liceName = (cat.liceName || "N/A").split(";");
|
||||
const marches2 = matches.filter(m => m.categorie_ord !== -42)
|
||||
.sort((a, b) => a.categorie_ord - b.categorie_ord)
|
||||
.map(m => ({...m, win: win(m.scores)}))
|
||||
const marches2 = classement
|
||||
? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
: matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id)
|
||||
.sort((a, b) => a.categorie_ord - b.categorie_ord)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
||||
|
||||
const isActiveMatch = (index) => {
|
||||
if (classement)
|
||||
return true;
|
||||
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
|
||||
}
|
||||
|
||||
@@ -224,24 +351,69 @@ function MatchList({matches, cat, menuActions}) {
|
||||
});
|
||||
}
|
||||
}, [match]);
|
||||
//useEffect(() => {
|
||||
// if (activeMatch !== null)
|
||||
// setActiveMatch(null);
|
||||
//}, [cat])
|
||||
|
||||
useEffect(() => {
|
||||
if (match && match.poule !== lice)
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
|
||||
if (!classement)
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
|
||||
}, [lice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (marches2.length === 0)
|
||||
return;
|
||||
if (marches2.some(m => m.id === activeMatch))
|
||||
if (marches2.some(m => m.id === (classement ? currentMatch?.matchSelect : activeMatch)))
|
||||
return;
|
||||
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
|
||||
if (classement) {
|
||||
getNext.current = (id) => {
|
||||
if (id === null)
|
||||
return marches2.findLast((m, index) => !m.end && isActiveMatch(index))?.id;
|
||||
const index = marches2.findIndex(m => m.id === id);
|
||||
return marches2.slice(0, index).reverse().find((m, index2) => !m.end && isActiveMatch(marches2.length - 1 - index2))?.id;
|
||||
}
|
||||
} else {
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
|
||||
}
|
||||
}, [matches])
|
||||
|
||||
useEffect(() => {
|
||||
setState({selectedMatch: activeMatch});
|
||||
sendNotify("sendSelectMatch", activeMatch);
|
||||
}, [activeMatch]);
|
||||
|
||||
const handleMatchClick = (matchId) => {
|
||||
if (classement) {
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: marches2.reverse().find(m => !m.end && m.id !== matchId)?.id});
|
||||
} else {
|
||||
setActiveMatch(matchId);
|
||||
}
|
||||
}
|
||||
|
||||
const GetCard = ({combId, match, cat}) => {
|
||||
const c = getHeightCardForCombInMatch(combId, match)
|
||||
if (!c)
|
||||
return <></>
|
||||
let bg = "";
|
||||
switch (c.type) {
|
||||
case "YELLOW":
|
||||
bg = " bg-warning";
|
||||
break;
|
||||
case "RED":
|
||||
bg = " bg-danger";
|
||||
break;
|
||||
case "BLACK":
|
||||
bg = " bg-dark text-white";
|
||||
break;
|
||||
case "BLUE":
|
||||
bg = " bg-primary text-white";
|
||||
break;
|
||||
}
|
||||
return <span
|
||||
className={"position-absolute top-0 start-100 translate-middle-y badge border border-light p-2" + bg +
|
||||
(c.match === match.id ? " rounded-circle" : (hasEffectCard(c, match.id, cat.id) ? "" : " bg-opacity-50"))}>
|
||||
<span className="visually-hidden">card</span></span>
|
||||
}
|
||||
|
||||
return <>
|
||||
{liceName.length > 1 &&
|
||||
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
|
||||
@@ -261,8 +433,8 @@ function MatchList({matches, cat, menuActions}) {
|
||||
<table className="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>}
|
||||
{!classement && <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">{t('no')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
|
||||
@@ -273,35 +445,41 @@ function MatchList({matches, cat, menuActions}) {
|
||||
<tbody className="table-group-divider">
|
||||
{marches2.map((m, index) => (
|
||||
<tr key={m.id}
|
||||
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>
|
||||
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>
|
||||
className={m.id === (classement ? currentMatch?.matchSelect : activeMatch) ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))}
|
||||
onClick={() => handleMatchClick(m.id)}>
|
||||
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
|
||||
{liceName[(index - firstIndex) % liceName.length]}</td>}
|
||||
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>}
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
|
||||
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
|
||||
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
|
||||
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && ((m.win > 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
|
||||
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
|
||||
<small><CombName combId={m.c1}/></small></td>
|
||||
<small className="position-relative"><CombName combId={m.c1}/>
|
||||
<GetCard match={m} combId={m.c1} cat={cat}/></small></td>
|
||||
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
|
||||
<small><CombName combId={m.c2}/></small></td>
|
||||
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
|
||||
<small className="position-relative"><CombName combId={m.c2}/>
|
||||
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
|
||||
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && ((m.win < 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{activeMatch &&
|
||||
{activeMatch && !classement &&
|
||||
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
|
||||
</>
|
||||
}
|
||||
|
||||
function BuildTree({treeData, matches, menuActions}) {
|
||||
function BuildTree({treeData, matches, cat, menuActions}) {
|
||||
const scrollRef = useRef(null)
|
||||
const [currentMatch, setCurrentMatch] = useState(null)
|
||||
const {getComb} = useCombs()
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {cards_v} = useCards();
|
||||
const {sendNotify, setState} = useWS();
|
||||
const getNext = useRef(null);
|
||||
const rtrees = useRef(null);
|
||||
|
||||
const match = matches.find(m => m.id === currentMatch?.matchSelect)
|
||||
useEffect(() => {
|
||||
@@ -320,38 +498,84 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
}
|
||||
}, [next_match]);
|
||||
|
||||
function parseTree(data_in) {
|
||||
function parseTree(data_in, matches_) {
|
||||
if (data_in?.data == null)
|
||||
return null
|
||||
|
||||
const matchData = matches.find(m => m.id === data_in.data)
|
||||
const matchData = matches_.find(m => m.id === data_in.data)
|
||||
const c1 = getComb(matchData?.c1)
|
||||
const c2 = getComb(matchData?.c2)
|
||||
|
||||
const scores2 = []
|
||||
for (const score of matchData?.scores) {
|
||||
scores2.push({
|
||||
...score,
|
||||
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
|
||||
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
let node = new TreeNode({
|
||||
...matchData,
|
||||
...win_end(matchData, cards_v),
|
||||
scores: scores2,
|
||||
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
|
||||
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
|
||||
})
|
||||
node.left = parseTree(data_in?.left)
|
||||
node.right = parseTree(data_in?.right)
|
||||
node.left = parseTree(data_in?.left, matches_)
|
||||
node.right = parseTree(data_in?.right, matches_)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
function initTree(data_in) {
|
||||
function initTree(data_in, matches_) {
|
||||
let out = []
|
||||
for (const din of data_in) {
|
||||
out.push(parseTree(din))
|
||||
let out2 = []
|
||||
for (let i = 0; i < data_in.raw.length; i++) {
|
||||
if (data_in.raw.at(i).level > -10) {
|
||||
out.push(parseTree(data_in.formatted.at(i), matches_))
|
||||
out2.push(parseTree(data_in.formatted.at(i), matches_))
|
||||
}
|
||||
}
|
||||
return out
|
||||
return [out, out2.reverse()]
|
||||
}
|
||||
|
||||
const trees = initTree(treeData);
|
||||
const [trees, rTrees] = initTree(treeData, matches);
|
||||
rtrees.current = rTrees;
|
||||
|
||||
useEffect(() => {
|
||||
if (matches.length === 0)
|
||||
return;
|
||||
if (matches.some(m => m.id === currentMatch?.matchSelect))
|
||||
return;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
|
||||
const matchId = ((getNext.current) ? getNext.current(null) : null) || new TreeNode(null).nextMatchTree(rTrees_);
|
||||
const next = matchId ? ((getNext.current) ? getNext.current(matchId) : null) || new TreeNode(matchId).nextMatchTree(rTrees_) : null;
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: next});
|
||||
setState({selectedMatch: matchId});
|
||||
sendNotify("sendSelectMatch", matchId);
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [matches])
|
||||
|
||||
const setCurrentMatch_ = (o) => {
|
||||
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
|
||||
const matchId = o.matchSelect ? o.matchSelect : new TreeNode(null).nextMatchTree(rTrees_);
|
||||
const next = o.matchNext ? o.matchNext : new TreeNode(matchId).nextMatchTree(rTrees_);
|
||||
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: next});
|
||||
setState({selectedMatch: matchId});
|
||||
sendNotify("sendSelectMatch", matchId);
|
||||
}
|
||||
|
||||
const onMatchClick = (rect, matchId, __) => {
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
|
||||
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(rTrees_)});
|
||||
setState({selectedMatch: matchId});
|
||||
sendNotify("sendSelectMatch", matchId);
|
||||
}
|
||||
|
||||
const onClickVoid = () => {
|
||||
@@ -362,7 +586,11 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
<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}/>
|
||||
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23} cards={cards_v}/>
|
||||
{cat.fullClassement &&
|
||||
<MatchList matches={treeData.raw.filter(n => n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))}
|
||||
cat={cat} menuActions={menuActions} classement={true} currentMatch={currentMatch} setCurrentMatch={setCurrentMatch_}
|
||||
getNext={getNext}/>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,301 +599,3 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
menuActions={menuActions}/></LoadingProvider>}
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScorePanel({matchId, matchs, match, menuActions}) {
|
||||
const onClickVoid = useRef(() => {
|
||||
});
|
||||
|
||||
return <div className="row" onClick={onClickVoid.current}>
|
||||
<ScorePanel_ matchId={matchId} matchs={matchs} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
|
||||
<CardPanel matchId={matchId} match={match}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
|
||||
const {sendRequest} = useWS()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [end, setEnd] = useState(match?.end || false)
|
||||
const [scoreIn, setScoreIn] = useState("")
|
||||
const inputRef = useRef(null)
|
||||
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) => {
|
||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
|
||||
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore"));
|
||||
}
|
||||
return () => menuActions.current.saveScore = undefined;
|
||||
}, [matchId])
|
||||
|
||||
const handleScoreClick = (e, round, comb) => {
|
||||
e.stopPropagation();
|
||||
const tableRect = tableRef.current.getBoundingClientRect();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
|
||||
updateScore();
|
||||
|
||||
const sel = inputRef.current;
|
||||
sel.style.top = (rect.y - tableRect.y) + "px";
|
||||
sel.style.left = (rect.x - tableRect.x) + "px";
|
||||
sel.style.width = rect.width + "px";
|
||||
sel.style.height = rect.height + "px";
|
||||
sel.style.display = "block";
|
||||
|
||||
if (round === -1) {
|
||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||
setScoreIn("");
|
||||
console.log("Setting for new round", maxRound);
|
||||
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
|
||||
} else {
|
||||
const score = match.scores.find(s => s.n_round === round);
|
||||
setScoreIn((comb === 1 ? score?.s1 : score?.s2) || "");
|
||||
lastScoreClick.current = {matchId: matchId, round, comb};
|
||||
setTimeout(() => inputRef.current.select(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
const updateScore = () => {
|
||||
if (lastScoreClick.current !== null) {
|
||||
const {matchId, round, comb} = lastScoreClick.current;
|
||||
lastScoreClick.current = null;
|
||||
|
||||
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);
|
||||
|
||||
let newScore;
|
||||
if (score) {
|
||||
if (comb === 1)
|
||||
newScore = {...score, s1: scoreIn_};
|
||||
else
|
||||
newScore = {...score, s2: scoreIn_};
|
||||
|
||||
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
|
||||
return
|
||||
} else {
|
||||
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
|
||||
if (newScore.s1 === -1000 && newScore.s2 === -1000)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onClickVoid = () => {
|
||||
updateScore();
|
||||
|
||||
const sel = inputRef.current;
|
||||
sel.style.display = "none";
|
||||
lastScoreClick.current = null;
|
||||
}
|
||||
onClickVoid_.current = onClickVoid;
|
||||
|
||||
useEffect(() => {
|
||||
if (!match || match?.end === end)
|
||||
return;
|
||||
|
||||
if (end) {
|
||||
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
|
||||
toast.error(t('score.err1'));
|
||||
setEnd(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchEnd', {matchId: matchId, end})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}, [end]);
|
||||
|
||||
useEffect(() => {
|
||||
onClickVoid()
|
||||
}, [matchId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (match?.scores)
|
||||
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
|
||||
}, [match?.scores]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!match)
|
||||
return;
|
||||
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))
|
||||
|
||||
const tt = t('score.spe')
|
||||
|
||||
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
|
||||
return <div ref={tableRef} className="col" style={{position: "relative"}}>
|
||||
<h6>{t('scores')} <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
||||
data-bs-html="true"/></h6>
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('manche')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('bleu')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-group-divider">
|
||||
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
|
||||
<tr key={score.n_round}>
|
||||
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
|
||||
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)}</td>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
|
||||
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<th style={{textAlign: "center"}}></th>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{textAlign: "right"}}>
|
||||
<div className="form-check" style={{display: "inline-block"}}>
|
||||
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
|
||||
onChange={e => setEnd(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkboxEnd">{t('terminé')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
|
||||
max="999"
|
||||
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Tab") {
|
||||
if (lastScoreClick.current !== null) {
|
||||
const {round, comb} = lastScoreClick.current;
|
||||
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
|
||||
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
|
||||
e.preventDefault();
|
||||
scoreRef.current[nextIndex].click();
|
||||
}
|
||||
}
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onClickVoid();
|
||||
}
|
||||
}}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function CardPanel({matchId, match}) {
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
const {data, refresh} = useRequestWS('getCardboardWithoutThis', matchId, setLoading);
|
||||
|
||||
useEffect(() => {
|
||||
refresh('getCardboardWithoutThis', matchId);
|
||||
|
||||
const sendCardboard = ({data}) => {
|
||||
if (data.comb_id === match.c1 || data.comb_id === match.c2) {
|
||||
refresh('getCardboardWithoutThis', matchId);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}})
|
||||
return () => dispatch({type: 'removeListener', payload: sendCardboard})
|
||||
}, [matchId])
|
||||
|
||||
if (!match) {
|
||||
return <div className="col"></div>
|
||||
}
|
||||
|
||||
const c1Cards = match.cardboard?.find(c => c.comb_id === match.c1) || {red: 0, yellow: 0};
|
||||
const c2Cards = match.cardboard?.find(c => c.comb_id === match.c2) || {red: 0, yellow: 0};
|
||||
|
||||
const handleCard = (combId, yellow, red) => {
|
||||
if (combId === match.c1) {
|
||||
if (c1Cards.red + red < 0 || c1Cards.yellow + yellow < 0)
|
||||
return;
|
||||
} else if (combId === match.c2) {
|
||||
if (c2Cards.red + red < 0 || c2Cards.yellow + yellow < 0)
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('sendCardboardChange', {matchId, combId, yellow, red})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="col">
|
||||
<h6>Carton</h6>
|
||||
<div className="bg-danger-subtle text-danger-emphasis" style={{padding: ".25em", borderRadius: "1em 1em 0 0"}}>
|
||||
<div>Competition: <span className="badge text-bg-danger">{(data?.c1_red || 0) + c1Cards.red}</span> <span
|
||||
className="badge text-bg-warning">{(data?.c1_yellow || 0) + c1Cards.yellow}</span></div>
|
||||
<div className="d-flex justify-content-center align-items-center" style={{margin: ".25em"}}>
|
||||
Match:
|
||||
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
|
||||
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c1, 0, +1)}>+</button>
|
||||
<span className="badge text-bg-danger">{c1Cards.red}</span>
|
||||
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c1, 0, -1)}>-</button>
|
||||
</div>
|
||||
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
|
||||
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c1, +1, 0)}>+</button>
|
||||
<span className="badge text-bg-warning">{c1Cards.yellow}</span>
|
||||
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c1, -1, 0)}>-</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-info-subtle text-info-emphasis" style={{padding: ".25em", borderRadius: "0 0 1em 1em"}}>
|
||||
<div>Competition: <span className="badge text-bg-danger">{(data?.c2_red || 0) + c2Cards.red}</span> <span
|
||||
className="badge text-bg-warning">{(data?.c2_yellow || 0) + c2Cards.yellow}</span></div>
|
||||
<div className="d-flex justify-content-center align-items-center" style={{margin: ".25em"}}>
|
||||
Match:
|
||||
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
|
||||
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c2, 0, +1)}>+</button>
|
||||
<span className="badge text-bg-danger">{c2Cards.red}</span>
|
||||
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c2, 0, -1)}>-</button>
|
||||
</div>
|
||||
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
|
||||
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c2, +1, 0)}>+</button>
|
||||
<span className="badge text-bg-warning">{c2Cards.yellow}</span>
|
||||
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c2, -1, 0)}>-</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
|
||||
export function PointPanel({menuActions}) {
|
||||
const [revers, setRevers] = useState(false)
|
||||
@@ -47,7 +48,20 @@ export function PointPanel({menuActions}) {
|
||||
|
||||
<div className="col row align-items-center">
|
||||
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
|
||||
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
|
||||
{ /*<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button> */}
|
||||
</div>
|
||||
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function SendScore({scoreRouge, scoreBleu}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({scoreState: {scoreRouge, scoreBleu}});
|
||||
sendNotify("sendCurrentScore", {scoreRouge, scoreBleu});
|
||||
}, [scoreRouge, scoreBleu]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {createPortal} from "react-dom";
|
||||
import {copyStyles} from "../../../utils/copyStyles.js";
|
||||
import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faArrowRightArrowLeft, faDisplay, faFile} from "@fortawesome/free-solid-svg-icons";
|
||||
import {PubAffWindow} from "./PubAffWindow.jsx";
|
||||
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
|
||||
import {ChronoPanel} from "./CMTChronoPanel.jsx";
|
||||
@@ -13,8 +13,9 @@ import {CategorieSelect} from "./CMTMatchPanel.jsx";
|
||||
import {PointPanel} from "./CMTPoint.jsx";
|
||||
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
|
||||
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
||||
import {toast} from "react-toastify";
|
||||
import {Flip, toast} from "react-toastify";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {getToastMessage} from "../../../utils/Tools.js";
|
||||
|
||||
export function CMTable() {
|
||||
const combDispatch = useCombsDispatch()
|
||||
@@ -37,7 +38,7 @@ export function CMTable() {
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">{t('chronomètre')}</div>
|
||||
<div className="card-body">
|
||||
<ChronoPanel/>
|
||||
<ChronoPanel menuActions={menuActions}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,9 +48,11 @@ export function CMTable() {
|
||||
<PointPanel menuActions={menuActions}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="infoCategory"></div>
|
||||
</div>
|
||||
<div className="col-md-12 col-xl-6 col-xxl-5">
|
||||
<div className="card mb-3">
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">{t('matches')}</div>
|
||||
<div className="card-body">
|
||||
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
|
||||
@@ -59,6 +62,7 @@ export function CMTable() {
|
||||
</div>
|
||||
<Menu menuActions={menuActions}/>
|
||||
<ObsAutoSyncWhitPubAff/>
|
||||
<SendCatId catId={catId}/>
|
||||
</div>
|
||||
</PubAffProvider>
|
||||
</OBSProvider>
|
||||
@@ -73,9 +77,11 @@ function Menu({menuActions}) {
|
||||
const publicAffDispatch = usePubAffDispatch()
|
||||
const [showPubAff, setShowPubAff] = useState(false)
|
||||
const [showScore, setShowScore] = useState(true)
|
||||
const [zone, setZone] = useState(sessionStorage.getItem("liceName") || "???")
|
||||
const {connected, connect, disconnect} = useOBS();
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
const teamCardModal = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const externalWindow = useRef(null)
|
||||
@@ -124,7 +130,7 @@ function Menu({menuActions}) {
|
||||
|
||||
const longTimeAction = (button) => {
|
||||
if (button === "obs") {
|
||||
obsModal.current.click();
|
||||
// obsModal.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +160,7 @@ function Menu({menuActions}) {
|
||||
} else {
|
||||
importOBSConfiguration()
|
||||
.then(config => {
|
||||
connect("ws://" + config.adresse + "/", config.password, config.assets_dir);
|
||||
connect(config.adresse, config.password, config.assets_dir);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(t('aucuneConfigurationObs'));
|
||||
@@ -169,12 +175,19 @@ function Menu({menuActions}) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleOBSSubmit = (e) => {
|
||||
const handleLiceSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const prefix = form[0].value;
|
||||
|
||||
sessionStorage.setItem("obs_prefix", prefix);
|
||||
if (prefix === "") {
|
||||
sessionStorage.removeItem("liceName");
|
||||
setZone("???");
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem("liceName", prefix);
|
||||
setZone(prefix);
|
||||
}
|
||||
|
||||
if (!e)
|
||||
@@ -183,6 +196,11 @@ function Menu({menuActions}) {
|
||||
{createPortal(
|
||||
<>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<span onClick={() => obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}</span>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={faFile} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={() => teamCardModal.current.click()} data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t('cartonDéquipe')}/>
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t('ttm.table.inverserLaPosition')}/>
|
||||
@@ -203,23 +221,23 @@ function Menu({menuActions}) {
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#LiceNameModal"
|
||||
style={{display: 'none'}}>
|
||||
Launch Lice Name Modal
|
||||
</button>
|
||||
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
|
||||
<div className="modal fade" id="LiceNameModal" tabIndex="-1" aria-labelledby="LiceNameModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Configuration OBS</h5>
|
||||
<h5 className="modal-title">{t('configurationDuNomDeLaZone')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form onSubmit={handleOBSSubmit}>
|
||||
<form onSubmit={handleLiceSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">{t('obs.préfixDesSources')}</span>
|
||||
<span className="input-group-text">sub</span>
|
||||
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
|
||||
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
|
||||
<span className="input-group-text">{t('nomDeLaZone')}</span>
|
||||
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={0} maxLength={1}
|
||||
defaultValue={sessionStorage.getItem("liceName") || "1"}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
@@ -230,6 +248,176 @@ function Menu({menuActions}) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ref={teamCardModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#TeamCardModal"
|
||||
style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
</button>
|
||||
<div className="modal fade" id="TeamCardModal" tabIndex="-1" aria-labelledby="TeamCardModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<TeamCardModal/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SendLiceName name={zone}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function TeamCardModal() {
|
||||
const [club, setClub] = useState("")
|
||||
|
||||
const {t} = useTranslation("cm");
|
||||
const {combs} = useCombs()
|
||||
const {sendRequest} = useWS()
|
||||
|
||||
let clubList = [];
|
||||
if (combs != null) {
|
||||
clubList = Object.values(combs).map(d => d.club_str).filter((v, i, a) => v !== "" && v !== undefined && a.indexOf(v) === i);
|
||||
}
|
||||
|
||||
const handleAdd = (e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(sendRequest("applyTeamCards", {
|
||||
teamUuid: Object.values(combs).find(d => d.club_str === club)?.club_uuid,
|
||||
teamName: club,
|
||||
type: "YELLOW"
|
||||
}),
|
||||
getToastMessage("toast.card.team", "cm"))
|
||||
.then(() => {
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{t('cartonDéquipe')}</h5>
|
||||
<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">
|
||||
<label htmlFor="inputGroupSelect09" className="input-group-text">{t('club')}</label>
|
||||
<select id="inputGroupSelect09" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
|
||||
{clubList.sort((a, b) => a.localeCompare(b)).map((club, index) => (
|
||||
<option key={index} value={club}>{club}</option>))}
|
||||
</select>
|
||||
<button className="btn btn-outline-primary" type="button" onClick={handleAdd}>{t("ajouter")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function SendLiceName({name}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({liceName: name});
|
||||
sendNotify("sendLicenceName", name);
|
||||
}, [name]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
function SendCatId({catId}) {
|
||||
const notifState = useRef(undefined);
|
||||
const {sendNotify, setState, dispatch, tableState} = useWS();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
const welcomeInfo = () => {
|
||||
sendNotify("sendState", tableState.current)
|
||||
}
|
||||
const sendTeamCards = ({data}) => {
|
||||
function content({closeToast, data}) {
|
||||
const sendState = (s) => {
|
||||
sendNotify("sendTeamCardReturnState", {
|
||||
state: s,
|
||||
teamUuid: data.teamUuid,
|
||||
teamName: data.teamName,
|
||||
type: data.type,
|
||||
selectedCategory: tableState.current.selectedCategory,
|
||||
selectedMatch: tableState.current.selectedMatch
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<span>{`Un carton jaune d'équipe a été émis à l'encontre du club ${data.teamName}. Dans votre zone de combat :`}</span><br/>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="radio" name="radioState" id="radioState1"
|
||||
onChange={e => e.target.checked ? notifState.current = 0 : null}/>
|
||||
<label className="form-check-label" htmlFor="radioState1">
|
||||
Rien n'est en cours
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="radio" name="radioState" id="radioState2"
|
||||
onChange={e => e.target.checked ? notifState.current = 1 : null}/>
|
||||
<label className="form-check-label" htmlFor="radioState2">
|
||||
La categorie est en cours
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="radio" name="radioState" id="radioState3"
|
||||
onChange={e => e.target.checked ? notifState.current = 2 : null}/>
|
||||
<label className="form-check-label" htmlFor="radioState3">
|
||||
Le match est en cours
|
||||
</label>
|
||||
</div>
|
||||
<button className="btn btn-sm btn-outline-primary ml-2" onClick={() => {
|
||||
if (notifState.current === undefined) {
|
||||
return;
|
||||
}
|
||||
sendState(notifState.current)
|
||||
closeToast(true);
|
||||
}}>
|
||||
{t('confirmer')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toast.warn(content, {
|
||||
position: "top-center",
|
||||
autoClose: false,
|
||||
closeButton: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
transition: Flip,
|
||||
data: {
|
||||
teamUuid: data.teamUuid,
|
||||
teamName: data.teamName,
|
||||
type: data.type,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
welcomeInfo();
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendTeamCards, code: 'sendTeamCards'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: welcomeInfo});
|
||||
dispatch({type: 'removeListener', payload: sendTeamCards});
|
||||
|
||||
setState({selectedCategory: -1});
|
||||
sendNotify("sendSelectCategory", -1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setState({selectedCategory: catId});
|
||||
sendNotify("sendSelectCategory", catId);
|
||||
}, [catId]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
||||
import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {SelectCombModalContent} from "./SelectCombModalContent.jsx";
|
||||
import {createMatch, scoreToString} from "../../../utils/CompetitionTools.js";
|
||||
import {createMatch, scoreToString2} from "../../../utils/CompetitionTools.js";
|
||||
|
||||
import {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
|
||||
import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable';
|
||||
@@ -14,9 +14,14 @@ import {useSortable} from '@dnd-kit/sortable';
|
||||
import {CSS} from '@dnd-kit/utilities';
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {getToastMessage, win} from "../../../utils/Tools.js";
|
||||
import {faPen, faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {getToastMessage, virtualScore, win_end} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
|
||||
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {AutoCatModalContent} from "../../../components/cm/AutoCatModalContent.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -26,9 +31,16 @@ function CupImg() {
|
||||
alt=""/>
|
||||
}
|
||||
|
||||
function CupImg2() {
|
||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
||||
style={{width: "16px"}} src="/img/171892.png"
|
||||
alt=""/>
|
||||
}
|
||||
|
||||
export function CategoryContent({cat, catId, setCat, menuActions}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const cardDispatch = useCardsDispatch();
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
const [groups, setGroups] = useState([])
|
||||
const groupsRef = useRef(groups);
|
||||
@@ -58,7 +70,8 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
|
||||
return
|
||||
setCat(cat_ => ({
|
||||
...cat_,
|
||||
trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
|
||||
trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
|
||||
raw_trees: data.sort((a, b) => a.level - b.level)
|
||||
}))
|
||||
|
||||
let matches2 = [];
|
||||
@@ -73,6 +86,9 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
|
||||
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
|
||||
|
||||
if (data.categorie !== cat.id)
|
||||
continue;
|
||||
|
||||
setGroups(prev => {
|
||||
if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id))
|
||||
return [...prev, {id: data.c1?.id, poule: data.poule}];
|
||||
@@ -118,9 +134,15 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type,
|
||||
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
|
||||
trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)),
|
||||
raw_trees: data.trees.sort((a, b) => a.level - b.level),
|
||||
treeAreClassement: data.treeAreClassement,
|
||||
fullClassement: data.fullClassement,
|
||||
preset: data.preset,
|
||||
})
|
||||
|
||||
cardDispatch({type: 'SET_ALL', payload: data.cards});
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||
@@ -157,19 +179,39 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
|
||||
|
||||
return <>
|
||||
<div className="col-md-3">
|
||||
<AddComb groups={groups} setGroups={setGroups} removeGroup={removeGroup} menuActions={menuActions}/>
|
||||
<AddComb groups={groups} setGroups={setGroups} removeGroup={removeGroup} menuActions={menuActions} cat={cat}/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
{cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>}
|
||||
</div>
|
||||
<PrintMatch menuActions={menuActions} matches={matches} groups={groups} cat={cat}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function AddComb({groups, setGroups, removeGroup, menuActions}) {
|
||||
function PrintMatch({menuActions, cat, groups, matches}) {
|
||||
const {cards_v} = useCards();
|
||||
const marches2 = matches.filter(m => m.categorie === cat.id)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
|
||||
marches2.forEach(m => {
|
||||
if (m.end && (!m.scores || m.scores.length === 0))
|
||||
m.scores = [{n_round: 0, s1: 0, s2: 0}];
|
||||
})
|
||||
|
||||
menuActions.printCategorie = (categorieEmpty) => {
|
||||
return [cat.name, [
|
||||
{type: "categorie", params: ({cat, matches: marches2, groups, cards_v, categorieEmpty})}
|
||||
]]
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
function AddComb({groups, setGroups, removeGroup, menuActions, cat}) {
|
||||
const {data, setData} = useRequestWS("getRegister", null)
|
||||
const combDispatch = useCombsDispatch()
|
||||
const {dispatch} = useWS()
|
||||
const [modalId, setModalId] = useState(null)
|
||||
const [modalMode, setModalMode] = useState(false)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,13 +262,30 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
|
||||
return <>
|
||||
<GroupsList groups={groups} setModalId={setModalId}/>
|
||||
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
|
||||
disabled={data === null}>{t('ajouterDesCombattants')}
|
||||
disabled={data === null} onClick={() => setModalMode(false)}>{t('ajouterDesCombattants')}
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
|
||||
disabled={data === null} onClick={() => setModalMode(true)}>{t('ajouterUneTeam')}
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#autoCatModal"
|
||||
disabled={data === null}>{t('ajoutAutomatique')}
|
||||
</button>
|
||||
|
||||
<div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<SelectCombModalContent data={data} setGroups={setGroups}/>
|
||||
<SelectCombModalContent data={data} setGroups={setGroups} teamMode={modalMode} groups={groups}
|
||||
defaultPreset={cat?.preset?.id || -1}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal fade" id="autoCatModal" tabIndex="-1" aria-labelledby="autoCatModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<AutoCatModalContent data={data} groups={groups} setGroups={setGroups} defaultPreset={cat?.preset?.id || -1}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,10 +306,13 @@ function GroupsList({groups, setModalId}) {
|
||||
|
||||
const groups2 = groups.map(g => {
|
||||
const comb = getComb(g.id);
|
||||
return {...g, name: comb ? comb.fname + " " + comb.lname : ""};
|
||||
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
|
||||
}).sort((a, b) => {
|
||||
if (a.poule !== b.poule)
|
||||
if (a.poule !== b.poule) {
|
||||
if (a.poule === '-') return 1;
|
||||
if (b.poule === '-') return -1;
|
||||
return a.poule.localeCompare(b.poule);
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
}).reduce((acc, curr) => {
|
||||
const poule = curr.poule;
|
||||
@@ -264,12 +326,20 @@ function GroupsList({groups, setModalId}) {
|
||||
return <>
|
||||
{Object.keys(groups2).map((poule) => (
|
||||
<div key={poule} className="mb-3">
|
||||
<h5>{poule !== '-' ? (t('poule') +" : " + poule) : t('sansPoule')}</h5>
|
||||
<h5>{poule !== '-' ? (t('poule') + " : " + poule) : t('sansPoule')}</h5>
|
||||
<ol className="list-group list-group-numbered">
|
||||
{groups2[poule].map((comb) => (
|
||||
<li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start"
|
||||
data-bs-toggle="modal" data-bs-target="#groupeModal" onClick={_ => setModalId(comb.id)}>
|
||||
<div className="ms-2 me-auto"><CombName combId={comb.id}/></div>
|
||||
<div className="ms-2 me-auto">
|
||||
<CombName combId={comb.id}/>
|
||||
{comb.teamMembers.length > 0 && <>
|
||||
{comb.teamMembers.map((m) => (<small key={m.id}>
|
||||
<br/>
|
||||
<CombName combId={m.id}/>
|
||||
</small>))}
|
||||
</>}
|
||||
</div>
|
||||
<span className="badge text-bg-primary rounded-pill">{comb.poule}</span>
|
||||
</li>)
|
||||
)}
|
||||
@@ -338,6 +408,7 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
const {sendRequest} = useWS();
|
||||
const [type, setType] = useState(1);
|
||||
const bthRef = useRef(null);
|
||||
const bthRef2 = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -353,6 +424,17 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
recalculateMatch(0);
|
||||
}
|
||||
|
||||
const handleCreatClassement_ = () => {
|
||||
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
|
||||
}
|
||||
const handleCreatClassement = () => {
|
||||
if (matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && m.c1 !== undefined && m.c2 !== undefined)) {
|
||||
bthRef2.current.click();
|
||||
return;
|
||||
}
|
||||
handleCreatClassement_()
|
||||
}
|
||||
|
||||
const recalculateMatch = (mode) => {
|
||||
let matchesToKeep = [];
|
||||
let matchesToRemove = [];
|
||||
@@ -369,12 +451,12 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(cat, matchesToKeep, groups.filter(g => g.poule !== '-'))
|
||||
|
||||
toast.promise(sendRequest("recalculateMatch", {
|
||||
categorie: cat.id,
|
||||
newMatch,
|
||||
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
|
||||
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
|
||||
matchesToRemove: matchesToRemove.map(m => m.id)
|
||||
}), getToastMessage("toast.matchs.create"))
|
||||
categorie: cat.id,
|
||||
newMatch,
|
||||
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
|
||||
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
|
||||
matchesToRemove: matchesToRemove.map(m => m.id)
|
||||
}), getToastMessage("toast.matchs.create", "cm"))
|
||||
.finally(() => {
|
||||
console.log("Finished creating matches");
|
||||
})
|
||||
@@ -394,7 +476,7 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
|
||||
onClick={_ => setType(2)}>{t('tournois')}
|
||||
onClick={_ => setType(2)}>{cat.treeAreClassement ? t('classement') : t('tournois')}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -406,7 +488,9 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
</>}
|
||||
|
||||
{type === 2 && <>
|
||||
<BuildTree treeData={cat.trees} matches={matches} groups={groups}/>
|
||||
<BuildTree treeData={cat.trees} treeRaw={cat.raw_trees} cat={cat} matches={matches} groups={groups}/>
|
||||
{cat.treeAreClassement &&
|
||||
<button className="btn btn-primary float-end" onClick={handleCreatClassement}>{t('créerLesMatchesDeClassement')}</button>}
|
||||
</>}
|
||||
|
||||
<button ref={bthRef} data-bs-toggle="modal" data-bs-target="#makeMatchMode" style={{display: "none"}}>open</button>
|
||||
@@ -435,10 +519,16 @@ function ListMatch({cat, matches, groups, reducer}) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button ref={bthRef2} data-bs-toggle="modal" data-bs-target="#confirm-regenerate-classement" style={{display: "none"}}>open</button>
|
||||
<ConfirmDialog id="confirm-regenerate-classement"
|
||||
title={t('créerLesMatchesDeClassement')}
|
||||
message={t('créerLesMatchesDeClassement.msg')}
|
||||
onConfirm={handleCreatClassement_}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function MatchList({matches, cat, groups, reducer}) {
|
||||
function MatchList({matches, cat, groups, reducer, classement = false}) {
|
||||
const {sendRequest} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const selectRef = useRef(null)
|
||||
@@ -447,12 +537,23 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
const [combSelect, setCombSelect] = useState(0)
|
||||
const [combC1nm, setCombC1nm] = useState(null)
|
||||
const [combC2nm, setCombC2nm] = useState(null)
|
||||
const [modalMatchId, setModalMatchId] = useState(null)
|
||||
const {t} = useTranslation("cm");
|
||||
const {cards_v, getHeightCardForCombInMatch} = useCards();
|
||||
const matchModal = useRef(null);
|
||||
|
||||
const liceName = (cat.liceName || "N/A").split(";");
|
||||
const marches2 = matches.filter(m => m.categorie_ord !== -42)
|
||||
.sort((a, b) => a.categorie_ord - b.categorie_ord)
|
||||
.map(m => ({...m, win: win(m.scores)}))
|
||||
const marches2 = classement
|
||||
? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
: matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id)
|
||||
.sort((a, b) => a.categorie_ord - b.categorie_ord)
|
||||
.map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
|
||||
marches2.forEach(m => {
|
||||
if (m.end && (!m.scores || m.scores.length === 0))
|
||||
m.scores = [{n_round: 0, s1: 0, s2: 0}];
|
||||
})
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
@@ -462,9 +563,14 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
);
|
||||
|
||||
const handleDragEnd = async (event) => {
|
||||
if (classement)
|
||||
return;
|
||||
|
||||
const {active, over} = event;
|
||||
if (active.id !== over.id) {
|
||||
const newIndex = marches2.findIndex(m => m.id === over.id);
|
||||
let newIndex = marches2.findIndex(m => m.id === over.id);
|
||||
if (newIndex > 0)
|
||||
newIndex = marches2[newIndex].categorie_ord;
|
||||
reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}});
|
||||
sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => {
|
||||
})
|
||||
@@ -541,7 +647,7 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
|
||||
const handleDelMatch = (matchId) => {
|
||||
const match = matches.find(m => m.id === matchId)
|
||||
if (!match)
|
||||
if (!match || classement)
|
||||
return;
|
||||
|
||||
if (!confirm(t('confirm1')))
|
||||
@@ -552,6 +658,14 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
.finally(() => setLoading(0))
|
||||
}
|
||||
|
||||
const handleEditMatch = (matchId) => {
|
||||
const match = matches.find(m => m.id === matchId)
|
||||
if (!match)
|
||||
return;
|
||||
setModalMatchId(matchId);
|
||||
matchModal.current.click();
|
||||
}
|
||||
|
||||
const handleCombClick = (e, matchId, combId) => {
|
||||
e.stopPropagation();
|
||||
const tableRect = tableRef.current.getBoundingClientRect();
|
||||
@@ -574,6 +688,31 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
lastMatchClick.current = null;
|
||||
}
|
||||
|
||||
const GetCard = ({combId, match, cat}) => {
|
||||
const c = getHeightCardForCombInMatch(combId, match)
|
||||
if (!c)
|
||||
return <></>
|
||||
let bg = "";
|
||||
switch (c.type) {
|
||||
case "YELLOW":
|
||||
bg = " bg-warning";
|
||||
break;
|
||||
case "RED":
|
||||
bg = " bg-danger";
|
||||
break;
|
||||
case "BLACK":
|
||||
bg = " bg-dark text-white";
|
||||
break;
|
||||
case "BLUE":
|
||||
bg = " bg-primary text-white";
|
||||
break;
|
||||
}
|
||||
return <span
|
||||
className={"position-absolute top-0 start-100 translate-middle-y badge border border-light p-2" + bg +
|
||||
(c.match === match.id ? " rounded-circle" : (hasEffectCard(c, match.id, cat.id) ? "" : " bg-opacity-50"))}>
|
||||
<span className="visually-hidden">card</span></span>
|
||||
}
|
||||
|
||||
const combsIDs = groups.map(m => m.id);
|
||||
return <div style={{position: "relative"}}>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
@@ -583,38 +722,48 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>
|
||||
{!classement &&
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('poule')}</th>}
|
||||
{!classement &&
|
||||
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('zone')}</th>}
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('résultat')}</th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
<th style={{textAlign: "center"}} scope="col"></th>
|
||||
{!classement && <th style={{textAlign: "center"}} scope="col"></th>}
|
||||
{!classement && <th style={{textAlign: "center"}} scope="col"></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-group-divider">
|
||||
{marches2.map((m, index) => (
|
||||
<SortableRow key={m.id} id={m.id}>
|
||||
<th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th>
|
||||
<td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>
|
||||
<td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>
|
||||
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
|
||||
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>}
|
||||
{!classement && <td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</td>}
|
||||
<td style={{textAlign: "right", cursor: "auto", paddingRight: "0"}}>{m.end && ((m.win > 0 &&
|
||||
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
|
||||
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}
|
||||
onClick={e => handleCombClick(e, m.id, m.c1)}>
|
||||
<small><CombName combId={m.c1}/></small></td>
|
||||
<small className="position-relative"><CombName combId={m.c1}/>
|
||||
<GetCard match={m} combId={m.c1} cat={cat}/></small></td>
|
||||
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}
|
||||
onClick={e => handleCombClick(e, m.id, m.c2)}>
|
||||
<small><CombName combId={m.c2}/></small></td>
|
||||
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
|
||||
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString(m.scores)}</td>
|
||||
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => handleDelMatch(m.id)}>
|
||||
<FontAwesomeIcon icon={faTrash}/></td>
|
||||
<td style={{textAlign: "center", cursor: "grab"}}>☰</td>
|
||||
<small className="position-relative"><CombName combId={m.c2}/>
|
||||
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
|
||||
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && ((m.win < 0 &&
|
||||
<CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
|
||||
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString2(m, cards_v)}</td>
|
||||
<td style={{textAlign: "center", cursor: "pointer", color: "#1381ff"}} onClick={_ => handleEditMatch(m.id)}>
|
||||
<FontAwesomeIcon icon={faPen}/></td>
|
||||
{!classement &&
|
||||
<td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => handleDelMatch(m.id)}>
|
||||
<FontAwesomeIcon icon={faTrash}/></td>}
|
||||
<td style={{textAlign: "center", cursor: "grab"}} hidden={classement}>☰</td>
|
||||
</SortableRow>
|
||||
))}
|
||||
<tr>
|
||||
{!classement && <tr>
|
||||
<td>-</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
@@ -629,7 +778,8 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<td></td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -643,9 +793,37 @@ function MatchList({matches, cat, groups, reducer}) {
|
||||
<option key={combId} value={combId}><CombName combId={combId}/></option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button ref={matchModal} type="button" style={{display: "none"}} data-bs-toggle="modal" data-bs-target="#editMatchModal">open</button>
|
||||
<div className="modal fade" id="editMatchModal" tabIndex="-1" aria-labelledby="editMatchModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<MatchEditModalContent matchId={modalMatchId} matches={matches}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function MatchEditModalContent({matchId, matches}) {
|
||||
const menuActionsLocal = useRef({});
|
||||
const match = matches.find(m => m.id === matchId)
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="editMatchModalLabel">{t('editionDuMatch')}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body" style={{textAlign: "center"}}>
|
||||
<ScorePanel matchId={matchId} match={match} matchs={matches} menuActions={menuActionsLocal} admin={true}/>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function SortableRow({id, children}) {
|
||||
const {
|
||||
attributes,
|
||||
@@ -676,7 +854,7 @@ function SortableRow({id, children}) {
|
||||
);
|
||||
}
|
||||
|
||||
function BuildTree({treeData, matches, groups}) {
|
||||
function BuildTree({treeData, treeRaw, matches, cat, groups}) {
|
||||
const scrollRef = useRef(null)
|
||||
const selectRef = useRef(null)
|
||||
const lastMatchClick = useRef(null)
|
||||
@@ -685,6 +863,7 @@ function BuildTree({treeData, matches, groups}) {
|
||||
const {sendRequest} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation("cm");
|
||||
const {cards_v} = useCards();
|
||||
|
||||
function parseTree(data_in) {
|
||||
if (data_in?.data == null)
|
||||
@@ -694,9 +873,19 @@ function BuildTree({treeData, matches, groups}) {
|
||||
const c1 = getComb(matchData?.c1)
|
||||
const c2 = getComb(matchData?.c2)
|
||||
|
||||
const scores2 = []
|
||||
for (const score of matchData?.scores) {
|
||||
scores2.push({
|
||||
...score,
|
||||
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
|
||||
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
|
||||
})
|
||||
}
|
||||
|
||||
let node = new TreeNode({
|
||||
...matchData,
|
||||
...win_end(matchData, cards_v),
|
||||
scores: scores2,
|
||||
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
|
||||
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
|
||||
})
|
||||
@@ -706,10 +895,12 @@ function BuildTree({treeData, matches, groups}) {
|
||||
return node
|
||||
}
|
||||
|
||||
function initTree(data_in) {
|
||||
function initTree(data_in, data_raw) {
|
||||
let out = []
|
||||
for (const din of data_in) {
|
||||
out.push(parseTree(din))
|
||||
for (let i = 0; i < data_raw.length; i++) {
|
||||
if (data_raw.at(i).level > -10) {
|
||||
out.push(parseTree(data_in.at(i)))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -773,7 +964,10 @@ function BuildTree({treeData, matches, groups}) {
|
||||
const combsIDs = groups.map(m => m.id);
|
||||
|
||||
return <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
|
||||
<DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}/>
|
||||
<DrawGraph root={initTree(treeData, treeRaw)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid} cards={cards_v}/>
|
||||
{cat.fullClassement &&
|
||||
<MatchList matches={treeRaw.filter(n => n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))}
|
||||
groups={groups} cat={cat} reducer={undefined} classement={true}/>}
|
||||
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
|
||||
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
|
||||
<option value={0}>{t('--SélectionnerUnCombattant--')}</option>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {ThreeDots} from "react-loader-spinner";
|
||||
import {AxiosError} from "../../../components/AxiosError.jsx";
|
||||
import {useFetch} from "../../../hooks/useFetch.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {CardsProvider} from "../../../hooks/useCard.jsx";
|
||||
import AudioEncoder from "../../../components/cm/AudioEncoder.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@@ -66,13 +68,16 @@ function HomeComp() {
|
||||
return <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
|
||||
<WSStatus setPerm={setPerm}/>
|
||||
<CombsProvider>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/>
|
||||
<Route path="/table" element={<CMTable/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
<CardsProvider>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/>
|
||||
<Route path="/table" element={<CMTable/>}/>
|
||||
<Route path="/view/audio" element={<AudioEncoder/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
</CardsProvider>
|
||||
</CombsProvider>
|
||||
</WSProvider>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {compareCardOrder, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCaretLeft} from "@fortawesome/free-solid-svg-icons";
|
||||
import {getToastMessage, scorePrint, virtual_end, virtualScore, win} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
export function ScorePanel({matchId, matchs, match, menuActions, admin = false}) {
|
||||
const {cards_v} = useCards();
|
||||
|
||||
const onClickVoid = useRef(() => {
|
||||
});
|
||||
|
||||
const vEnd = virtual_end(match, cards_v);
|
||||
return <div className="row" onClick={onClickVoid.current}>
|
||||
<ScorePanel_ matchId={matchId} matchs={matchs} match={match} menuActions={menuActions} onClickVoid_={onClickVoid} vEnd={vEnd}/>
|
||||
<CardPanel matchId={matchId} match={match} vEnd={admin ? false : vEnd} admin={admin}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_, vEnd}) {
|
||||
const {sendRequest} = useWS()
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [end, setEnd] = useState(match?.end || false)
|
||||
const [scoreIn, setScoreIn] = useState("")
|
||||
const inputRef = useRef(null)
|
||||
const tableRef = useRef(null)
|
||||
const scoreRef = useRef([])
|
||||
const lastScoreClick = useRef(null)
|
||||
const scoreInRef = useRef(null)
|
||||
const {cards_v} = useCards();
|
||||
|
||||
useEffect(() => {
|
||||
scoreInRef.current = scoreIn;
|
||||
}, [scoreIn]);
|
||||
|
||||
useEffect(() => {
|
||||
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
|
||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
|
||||
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore", "cm"));
|
||||
}
|
||||
return () => menuActions.current.saveScore = undefined;
|
||||
}, [matchId])
|
||||
|
||||
const handleScoreClick = (e, round, comb) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (vEnd)
|
||||
return;
|
||||
|
||||
const tableRect = tableRef.current.getBoundingClientRect();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
|
||||
updateScore();
|
||||
|
||||
const sel = inputRef.current;
|
||||
sel.style.top = (rect.y - tableRect.y) + "px";
|
||||
sel.style.left = (rect.x - tableRect.x) + "px";
|
||||
sel.style.width = rect.width + "px";
|
||||
sel.style.height = rect.height + "px";
|
||||
sel.style.display = "block";
|
||||
|
||||
if (round === -1) {
|
||||
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
|
||||
setScoreIn("");
|
||||
console.log("Setting for new round", maxRound);
|
||||
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
|
||||
} else {
|
||||
const score = match.scores.find(s => s.n_round === round);
|
||||
const tmp= (comb === 1 ? score?.s1 : score?.s2)
|
||||
setScoreIn((tmp === -1000 ? "" : tmp) || "");
|
||||
lastScoreClick.current = {matchId: matchId, round, comb};
|
||||
setTimeout(() => inputRef.current.select(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
const updateScore = () => {
|
||||
if (lastScoreClick.current !== null) {
|
||||
const {matchId, round, comb} = lastScoreClick.current;
|
||||
lastScoreClick.current = null;
|
||||
|
||||
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);
|
||||
|
||||
let newScore;
|
||||
if (score) {
|
||||
if (comb === 1)
|
||||
newScore = {...score, s1: scoreIn_};
|
||||
else
|
||||
newScore = {...score, s2: scoreIn_};
|
||||
|
||||
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
|
||||
return
|
||||
} else {
|
||||
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
|
||||
if (newScore.s1 === -1000 && newScore.s2 === -1000)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onClickVoid = () => {
|
||||
updateScore();
|
||||
|
||||
const sel = inputRef.current;
|
||||
sel.style.display = "none";
|
||||
lastScoreClick.current = null;
|
||||
}
|
||||
onClickVoid_.current = onClickVoid;
|
||||
|
||||
useEffect(() => {
|
||||
if (!match || match?.end === end)
|
||||
return;
|
||||
|
||||
if (end) {
|
||||
if (win(match) === 0 && match.categorie_ord === -42) {
|
||||
toast.error(t('score.err1'));
|
||||
setEnd(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchEnd', {matchId: matchId, end})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}, [end]);
|
||||
|
||||
useEffect(() => {
|
||||
onClickVoid()
|
||||
}, [matchId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (match?.scores)
|
||||
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
|
||||
}, [match?.scores]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!match)
|
||||
return;
|
||||
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);
|
||||
};
|
||||
}, [matchId, match]);
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const o = [...tooltipTriggerList]
|
||||
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
|
||||
const tt = t('score.spe')
|
||||
|
||||
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
|
||||
return <div ref={tableRef} className="col" style={{position: "relative"}}>
|
||||
<h6>{t('scores')} <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
|
||||
data-bs-html="true"/></h6>
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{textAlign: "center"}} scope="col">{t('manche')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('rouge')}</th>
|
||||
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">{t('bleu')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={"table-group-divider" + (vEnd ? " table-secondary" : "")}>
|
||||
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
|
||||
<tr key={score.n_round}>
|
||||
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
|
||||
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(virtualScore(match.c1, score, match, cards_v))}</td>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
|
||||
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(virtualScore(match.c2, score, match, cards_v))}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<th style={{textAlign: "center"}}></th>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
|
||||
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{textAlign: "right"}}>
|
||||
<div className="form-check" style={{display: "inline-block"}}>
|
||||
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end || vEnd} disabled={vEnd}
|
||||
onChange={e => setEnd(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="checkboxEnd">{t('terminé')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
|
||||
max="999"
|
||||
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Tab") {
|
||||
if (lastScoreClick.current !== null) {
|
||||
const {round, comb} = lastScoreClick.current;
|
||||
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
|
||||
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
|
||||
e.preventDefault();
|
||||
scoreRef.current[nextIndex].click();
|
||||
}
|
||||
}
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onClickVoid();
|
||||
}
|
||||
}}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function CardPanel({matchId, match, vEnd, admin}) {
|
||||
const {sendRequest} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const cardDispatch = useCardsDispatch();
|
||||
const {getCardInMatch, getHeightCardForCombInMatch} = useCards();
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const {data, refresh} = useRequestWS('getCardForMatch', matchId, setLoading);
|
||||
useEffect(() => {
|
||||
refresh('getCardForMatch', matchId);
|
||||
}, [matchId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data)
|
||||
return;
|
||||
cardDispatch({type: 'SET_ALL', payload: data})
|
||||
}, [data])
|
||||
|
||||
if (!match) {
|
||||
return <div className="col"></div>
|
||||
}
|
||||
const handleCard = (combId, type) => {
|
||||
setLoading(1)
|
||||
sendRequest('sendCardAdd', {matchId, combId, type})
|
||||
.then(() => {
|
||||
toast.success(t('cardAdded'));
|
||||
})
|
||||
.catch(err => {
|
||||
toast.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const confirmRm = (combId, type) => {
|
||||
function content({closeToast}) {
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<span>{t('ceCartonEstIssuDunCartonDéquipe')}</span>{' '}
|
||||
<button
|
||||
className="btn btn-sm btn-warning ml-2"
|
||||
onClick={() => {
|
||||
closeToast(true);
|
||||
handleCardRm_(combId, type)
|
||||
}}>
|
||||
{t('confirmer')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toast.warn(content);
|
||||
}
|
||||
|
||||
const handleCardRm_ = (combId, type) => {
|
||||
setLoading(1)
|
||||
sendRequest('sendCardRm', {matchId, combId, type})
|
||||
.then(() => {
|
||||
toast.success(t('cardRemoved'));
|
||||
})
|
||||
.catch(err => {
|
||||
toast.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
})
|
||||
}
|
||||
|
||||
const cards = getCardInMatch(match)
|
||||
|
||||
const handleCardRm = (combId, type) => {
|
||||
if (cards.find(c => c.comb === combId && c.type === type)?.teamCard) {
|
||||
confirmRm(combId, type)
|
||||
} else {
|
||||
handleCardRm_(combId, type)
|
||||
}
|
||||
}
|
||||
|
||||
const MakeList = ({comb}) => {
|
||||
const card = getHeightCardForCombInMatch(comb, match);
|
||||
|
||||
return <div className="btn-group-vertical" role="group">
|
||||
<button type="button" className="btn btn-sm btn-primary position-relative"
|
||||
onClick={() => handleCard(comb, "BLUE")}
|
||||
disabled={card && compareCardOrder(card, {type: "BLUE"}) >= 0 || vEnd}>{t('avertissement')}
|
||||
<span className="position-absolute top-0 start-100 p-2 text-danger-emphasis"
|
||||
hidden={!cards.some(c => c.comb === comb && c.type === "BLUE")}>
|
||||
<FontAwesomeIcon icon={faCaretLeft}/></span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-warning position-relative"
|
||||
onClick={() => handleCard(comb, "YELLOW")}
|
||||
disabled={card && compareCardOrder(card, {type: "YELLOW"}) >= 0 || vEnd}>{t('cartonJaune')}
|
||||
<span className="position-absolute top-0 start-100 p-2 text-danger-emphasis"
|
||||
hidden={!cards.some(c => c.comb === comb && c.type === "YELLOW")}>
|
||||
<FontAwesomeIcon icon={faCaretLeft}/></span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-danger position-relative"
|
||||
onClick={() => handleCard(comb, "RED")}
|
||||
disabled={card && compareCardOrder(card, {type: "RED"}) >= 0 || vEnd}>{t('cartonRouge')}
|
||||
<span className="position-absolute top-0 start-100 p-2 text-danger-emphasis"
|
||||
hidden={!cards.some(c => c.comb === comb && c.type === "RED")}>
|
||||
<FontAwesomeIcon icon={faCaretLeft}/></span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-dark position-relative"
|
||||
onClick={() => handleCard(comb, "BLACK")}
|
||||
disabled={card?.type !== "RED" || vEnd}>{t('cartonNoir')}
|
||||
<span className="position-absolute top-0 start-100 p-2 text-danger-emphasis"
|
||||
hidden={!cards.some(c => c.comb === comb && c.type === "BLACK")}>
|
||||
<FontAwesomeIcon icon={faCaretLeft}/></span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
const MakeListAdmin = ({comb}) => {
|
||||
const card = getHeightCardForCombInMatch(comb, match);
|
||||
|
||||
return <div className="btn-group-vertical" role="group">
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className="btn btn-sm btn-secondary">{t('ajouterUn')}</button>
|
||||
<button type="button" className="btn btn-sm btn-secondary">{t('supprimerUn')}</button>
|
||||
</div>
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className="btn btn-sm btn-primary"
|
||||
onClick={() => handleCard(comb, "BLUE")}
|
||||
disabled={card && compareCardOrder(card, {type: "BLUE"}) >= 0 || vEnd}>{t('avertissement')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-primary"
|
||||
onClick={() => handleCardRm(comb, "BLUE")}
|
||||
disabled={!cards.some(c => c.comb === comb && c.type === "BLUE")}>{t('avertissement')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className="btn btn-sm btn-warning"
|
||||
onClick={() => handleCard(comb, "YELLOW")}
|
||||
disabled={card && compareCardOrder(card, {type: "YELLOW"}) >= 0 || vEnd}>{t('cartonJaune')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-warning"
|
||||
onClick={() => handleCardRm(comb, "YELLOW")}
|
||||
disabled={!cards.some(c => c.comb === comb && c.type === "YELLOW")}>{t('cartonJaune')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCard(comb, "RED")}
|
||||
disabled={card && compareCardOrder(card, {type: "RED"}) >= 0 || vEnd}>{t('cartonRouge')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCardRm(comb, "RED")}
|
||||
disabled={!cards.some(c => c.comb === comb && c.type === "RED")}>{t('cartonRouge')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className="btn btn-sm btn-dark"
|
||||
onClick={() => handleCard(comb, "BLACK")}
|
||||
disabled={card?.type !== "RED" || vEnd}>{t('cartonNoir')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-dark"
|
||||
onClick={() => handleCardRm(comb, "BLACK")}
|
||||
disabled={!cards.some(c => c.comb === comb && c.type === "BLACK")}>{t('cartonNoir')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="col">
|
||||
<h6>Carton</h6>
|
||||
<div className="bg-danger-subtle text-danger-emphasis" style={{padding: ".25em", borderRadius: "1em 1em 0 0"}}>
|
||||
<h6>Combattant rouge</h6>
|
||||
{admin ? <MakeListAdmin comb={match.c1}/> :
|
||||
<><span>{t('ajouterUn')}</span><MakeList comb={match.c1}/></>}
|
||||
</div>
|
||||
<div className="bg-info-subtle text-info-emphasis" style={{padding: ".25em", borderRadius: "0 0 1em 1em"}}>
|
||||
<h6>Combattant bleu</h6>
|
||||
{admin ? <MakeListAdmin comb={match.c2}/> :
|
||||
<><span>{t('ajouterUn')}</span><MakeList comb={match.c2}/></>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import {useCountries} from "../../../hooks/useCountries.jsx";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {CatList, getCatName} from "../../../utils/Tools.js";
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {CatList, getCatName, getToastMessage} from "../../../utils/Tools.js";
|
||||
import {CombName} from "../../../hooks/useComb.jsx";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {toast} from "react-toastify";
|
||||
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
|
||||
|
||||
function SelectReducer(state, action) {
|
||||
switch (action.type) {
|
||||
@@ -20,6 +22,11 @@ function SelectReducer(state, action) {
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
case 'ADD_ID':
|
||||
return {
|
||||
...state,
|
||||
[action.payload]: false
|
||||
};
|
||||
case 'CLEAR_ACTIVE':
|
||||
const newState = {...state};
|
||||
Object.keys(newState).forEach(id => {
|
||||
@@ -53,14 +60,15 @@ function SelectReducer(state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
export function SelectCombModalContent({data, setGroups}) {
|
||||
export function SelectCombModalContent({data, groups, setGroups, teamMode = false, defaultPreset = -1}) {
|
||||
const country = useCountries('fr')
|
||||
const {t} = useTranslation("cm");
|
||||
const {dispatch} = useWS()
|
||||
const {sendRequest, dispatch} = useWS()
|
||||
const [dispo, dispoReducer] = useReducer(SelectReducer, {})
|
||||
const [select, selectReducer] = useReducer(SelectReducer, {})
|
||||
const lastClick = useRef({time: 0, id: null});
|
||||
|
||||
const [targetGroupe, setTargetGroupe] = useState("A")
|
||||
const [targetGroupe, setTargetGroupe] = useState("1")
|
||||
const [search, setSearch] = useState("")
|
||||
const [country_, setCountry_] = useState("")
|
||||
const [club, setClub] = useState("")
|
||||
@@ -68,12 +76,32 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
const [cat, setCat] = useState(-1)
|
||||
const [weightMin, setWeightMin] = useState(0)
|
||||
const [weightMax, setWeightMax] = useState(0)
|
||||
const [team, setTeam] = useState(false)
|
||||
const [teamName, setTeamName] = useState("");
|
||||
const [preset, setPreset] = useState(-1)
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(defaultPreset)
|
||||
}, [defaultPreset])
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => {
|
||||
return {id: Number(id), poule: targetGroupe}
|
||||
})])
|
||||
if (teamMode) {
|
||||
toast.promise(
|
||||
sendRequest('setTeam', {
|
||||
name: teamName,
|
||||
members: [...Object.keys(select).map(id => Number(id))]
|
||||
}), getToastMessage("toast.team.update", "cm"))
|
||||
.then(res => {
|
||||
if (res && res.id) {
|
||||
setGroups(prev => [...prev.filter(d => d.id !== Number(res.id)), {id: Number(res.id), poule: targetGroupe}])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => {
|
||||
return {id: Number(id), poule: targetGroupe}
|
||||
})])
|
||||
}
|
||||
|
||||
dispoReducer({type: 'REMOVE_ALL'})
|
||||
selectReducer({type: 'REMOVE_ALL'})
|
||||
@@ -101,6 +129,18 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id).filter(id => !selectedIds.includes(id))})
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null)
|
||||
return
|
||||
const teamIds = data.filter(d => d.teamMembers != null && d.teamMembers.length > 0).map(t => String(t.id));
|
||||
if (teamMode) {
|
||||
dispoReducer({type: 'REMOVE_IN', payload: teamIds});
|
||||
selectReducer({type: 'REMOVE_IN', payload: teamIds});
|
||||
} else {
|
||||
dispoReducer({type: 'ADD_ALL', payload: teamIds});
|
||||
}
|
||||
}, [teamMode])
|
||||
|
||||
function applyFilter(dataIn, dataOut) {
|
||||
Object.keys(dataIn).forEach((id) => {
|
||||
const comb = data.find(d => d.id === Number(id));
|
||||
@@ -113,7 +153,10 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
&& (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
|
||||
&& (cat === -1 || cat === Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory))
|
||||
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin)
|
||||
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)) {
|
||||
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)
|
||||
&& (teamMode && (comb.teamMembers == null || comb.teamMembers.length === 0) || !teamMode
|
||||
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))
|
||||
&& (preset === -1 || comb.categoriesInscrites?.includes(preset))) {
|
||||
dataOut[id] = dataIn[id];
|
||||
}
|
||||
}
|
||||
@@ -155,7 +198,7 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}</h1>
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">{t('select.sélectionnerDesCombatants')}{teamMode && (t('PourLéquipe'))}</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
@@ -179,6 +222,8 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ListPresetSelect value={preset} onChange={setPreset}/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="inputState1" className="form-label">{t('club')}</label>
|
||||
<select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
|
||||
@@ -225,6 +270,16 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
{!teamMode && <div>
|
||||
<label className="form-label">{t('team')}</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={team}
|
||||
onChange={e => setTeam(e.target.checked)}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">{t('team')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<div>
|
||||
<label htmlFor="input5" className="form-label">{t('poids')}</label>
|
||||
<div className="row-cols-sm-auto d-flex align-items-center">
|
||||
@@ -246,8 +301,19 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
{dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>{t('select.aucunCombattantDisponible')}</div>}
|
||||
{Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
|
||||
<button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")}
|
||||
onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}>
|
||||
<button key={id} type="button"
|
||||
className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "") + " " +
|
||||
(groups.find(g => g.id === Number(id) && g.poule !== '-') ? "list-group-item-secondary" : "")}
|
||||
onClick={() => {
|
||||
if (lastClick.current.id === id && (Date.now() - lastClick.current.time) < 500) {
|
||||
// Double click detected
|
||||
selectReducer({type: 'ADD_ID', payload: id})
|
||||
dispoReducer({type: 'REMOVE_ID', payload: id})
|
||||
} else {
|
||||
dispoReducer({type: 'TOGGLE_ID', payload: id})
|
||||
}
|
||||
lastClick.current = {time: Date.now(), id: id};
|
||||
}}>
|
||||
<CombName combId={id}/>
|
||||
</button>))}
|
||||
</div>
|
||||
@@ -267,7 +333,16 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
{Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
|
||||
<button key={id} type="button"
|
||||
className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")}
|
||||
onClick={() => selectReducer({type: 'TOGGLE_ID', payload: id})}>
|
||||
onClick={() => {
|
||||
if (lastClick.current.id === id && (Date.now() - lastClick.current.time) < 500) {
|
||||
// Double click detected
|
||||
dispoReducer({type: 'ADD_ID', payload: id})
|
||||
selectReducer({type: 'REMOVE_ID', payload: id})
|
||||
} else {
|
||||
selectReducer({type: 'TOGGLE_ID', payload: id})
|
||||
}
|
||||
lastClick.current = {time: Date.now(), id: id};
|
||||
}}>
|
||||
<CombName combId={id}/>
|
||||
</button>))}
|
||||
</div>
|
||||
@@ -277,13 +352,19 @@ export function SelectCombModalContent({data, setGroups}) {
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>
|
||||
<div className="vr"></div>
|
||||
<label htmlFor="input6" className="form-label">{t('poule')}</label>
|
||||
<input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe}
|
||||
{teamMode && <>
|
||||
<label htmlFor="input6" className="form-label">{t('nomDeLéquipe')}</label>
|
||||
<input type="text" className="form-control" id="input6" style={{width: "10em"}} value={teamName}
|
||||
onChange={(e) => setTeamName(e.target.value)}/>
|
||||
</>}
|
||||
<label htmlFor="input7" className="form-label">{t('poule')}</label>
|
||||
<input type="text" className="form-control" id="input7" style={{width: "3em"}} maxLength={1} value={targetGroupe}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9]$/.test(e.target.value))
|
||||
if (/^[a-zA-Z0-9]?/.test(e.target.value))
|
||||
setTargetGroupe(e.target.value)
|
||||
}}/>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>{t('ajouter')}</button>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}
|
||||
disabled={targetGroupe.length === 0 || (teamMode && teamName.length === 0)}>{t('ajouter')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
217
src/main/webapp/src/pages/competition/editor/StateWindow.jsx
Normal file
217
src/main/webapp/src/pages/competition/editor/StateWindow.jsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
import {from_sendTree} from "../../../utils/TreeUtils.js";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {CombName, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {timePrint, win_end} from "../../../utils/Tools.js";
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'SET':
|
||||
return [...state.filter(s => s.id !== action.payload.id), action.payload]
|
||||
case 'SET_ALL':
|
||||
return action.payload
|
||||
case 'REMOVE':
|
||||
return state.filter(s => s.id !== action.payload)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function useTablesState() {
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [state, dispatchState] = useReducer(reducer, [])
|
||||
|
||||
const subscribeToState = () => {
|
||||
sendRequest("subscribeToState", true)
|
||||
.then((data) => {
|
||||
dispatchState({type: 'SET_ALL', payload: data});
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const sendStateFull = ({data}) => {
|
||||
dispatchState({type: 'SET', payload: data});
|
||||
}
|
||||
|
||||
const rmStateFull = ({data}) => {
|
||||
dispatchState({type: 'REMOVE', payload: data});
|
||||
}
|
||||
|
||||
const welcomeInfo = () => {
|
||||
subscribeToState();
|
||||
}
|
||||
subscribeToState();
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendStateFull, code: 'sendStateFull'}})
|
||||
dispatch({type: 'addListener', payload: {callback: rmStateFull, code: 'rmStateFull'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: welcomeInfo});
|
||||
dispatch({type: 'removeListener', payload: sendStateFull});
|
||||
dispatch({type: 'removeListener', payload: rmStateFull});
|
||||
|
||||
sendRequest("subscribeToState", false)
|
||||
.then(() => {
|
||||
});
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {state};
|
||||
}
|
||||
|
||||
export function StateWindow({document}) {
|
||||
const {state} = useTablesState();
|
||||
|
||||
document.title = "État des tables de marque";
|
||||
document.body.className = "overflow-hidden";
|
||||
|
||||
console.log(state)
|
||||
return <>
|
||||
<div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto">
|
||||
{state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) =>
|
||||
<div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}>
|
||||
<ShowState table={table}/>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function readAndConvertMatch(matches, data, combsToAdd) {
|
||||
matches.push({
|
||||
...data,
|
||||
c1: data.c1?.id,
|
||||
c2: data.c2?.id,
|
||||
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
|
||||
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
|
||||
})
|
||||
if (data.c1)
|
||||
combsToAdd.push(data.c1)
|
||||
if (data.c2)
|
||||
combsToAdd.push(data.c2)
|
||||
}
|
||||
|
||||
function ShowState({table}) {
|
||||
const cardDispatch = useCardsDispatch();
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
const combDispatch = useCombsDispatch();
|
||||
const {cards_v} = useCards();
|
||||
|
||||
const [cat, setCat] = useState({id: -1, name: ""});
|
||||
|
||||
const marches2 = matches.filter(m => m.categorie === cat.id).map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
|
||||
useEffect(() => {
|
||||
const categoryListener = ({data}) => {
|
||||
if (data.id !== cat.id)
|
||||
return;
|
||||
setCat({id: data.id, name: data.name});
|
||||
}
|
||||
|
||||
const matchListener = ({data: datas}) => {
|
||||
for (const data of datas) {
|
||||
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMatch = ({data: datas}) => {
|
||||
for (const data of datas)
|
||||
reducer({type: 'REMOVE', payload: data});
|
||||
}
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
||||
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: matchListener})
|
||||
dispatch({type: 'removeListener', payload: deleteMatch})
|
||||
dispatch({type: 'removeListener', payload: categoryListener})
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (table.selectedCategory !== cat.id) {
|
||||
if (!table.selectedCategory || table.selectedCategory === -1) {
|
||||
setCat({id: -1, name: ""});
|
||||
return;
|
||||
}
|
||||
sendRequest('getFullCategory', table.selectedCategory)
|
||||
.then((data) => {
|
||||
setCat({id: data.id, name: data.name});
|
||||
cardDispatch({type: 'SET_ALL', payload: data.cards});
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||
|
||||
reducer({type: 'REPLACE_ALL', payload: matches2});
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
||||
console.log(matches2);
|
||||
})
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
return <>
|
||||
<div className="card-header">
|
||||
Zone de combat {table.liceName}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
Catégorie : {cat.name}<br/>
|
||||
Match terminés : {marches2.filter(m => m.end).length}/{marches2.length}<br/>
|
||||
Matchs : <PrintMatch match={matches.find(m => m.id === table.selectedMatch)}/><br/>
|
||||
Statue : {table?.state}<br/>
|
||||
Score : {table?.scoreState?.scoreRouge} - {table?.scoreState?.scoreBleu}<br/>
|
||||
Chronomètre : <PrintChrono chrono={table?.chronoState}/><br/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function PrintMatch({match}) {
|
||||
return <>{match?.c1 && <CombName combId={match?.c1}/>} vs {match?.c2 && <CombName combId={match?.c2}/>}</>
|
||||
}
|
||||
|
||||
|
||||
function PrintChrono({chrono}) {
|
||||
const chronoText = useRef(null)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
|
||||
const isRunning = () => chrono.startTime !== 0
|
||||
const getTime = () => {
|
||||
if (chrono.startTime === 0)
|
||||
return chrono.time
|
||||
return chrono.time + Date.now() - chrono.startTime
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!chrono || !chronoText.current)
|
||||
return;
|
||||
|
||||
const state_ = state.current
|
||||
const text_ = chronoText.current
|
||||
|
||||
const timer = setInterval(() => {
|
||||
let currentDuration = chrono.configTime
|
||||
if (chrono.state === 2) {
|
||||
currentDuration = chrono.configPause
|
||||
}
|
||||
const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)")
|
||||
|
||||
if (timeStr !== state_.lastTimeStr) {
|
||||
text_.textContent = timeStr
|
||||
state_.lastTimeStr = timeStr
|
||||
}
|
||||
|
||||
if (chrono.chronoState === 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 50);
|
||||
return () => clearInterval(timer)
|
||||
}, [chrono])
|
||||
|
||||
return <><span ref={chronoText}>{state.current.lastTimeStr}</span></>
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {useEffect, useRef} from "react";
|
||||
import {scorePrint, win} from "../../utils/Tools.js";
|
||||
import {scorePrint} from "../../utils/Tools.js";
|
||||
import {useCardsStatic} from "../../hooks/useCard.jsx";
|
||||
|
||||
const max_x = 500;
|
||||
|
||||
@@ -20,12 +21,14 @@ export function DrawGraph({
|
||||
},
|
||||
matchSelect = null,
|
||||
matchNext = null,
|
||||
size = 24
|
||||
size = 24,
|
||||
cards = []
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const actionCanvasRef = useRef(null);
|
||||
const ctxARef = useRef(null);
|
||||
const actionMapRef = useRef({});
|
||||
const {getHeightCardForCombInMatch} = useCardsStatic(cards);
|
||||
|
||||
const selectColor = "#30cc30";
|
||||
|
||||
@@ -149,6 +152,40 @@ export function DrawGraph({
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const printCard = (ctx, pos, combId, match) => {
|
||||
const cards2 = getHeightCardForCombInMatch(combId, match)
|
||||
if (cards2 != null) {
|
||||
let oldColor = ctx.fillStyle;
|
||||
switch (cards2.type) {
|
||||
case "BLUE":
|
||||
ctx.fillStyle = "#2e2efd";
|
||||
break;
|
||||
case "YELLOW":
|
||||
ctx.fillStyle = "#d8d800";
|
||||
break;
|
||||
case "RED":
|
||||
ctx.fillStyle = "#FF0000";
|
||||
break;
|
||||
case "BLACK":
|
||||
ctx.fillStyle = "#000000";
|
||||
break;
|
||||
default:
|
||||
ctx.fillStyle = "#FFFFFF00";
|
||||
}
|
||||
|
||||
if (cards2.match === match.id) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
ctx.arc(pos.x + pos.width - 10, pos.y + 5, 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "#000000"
|
||||
} else
|
||||
ctx.fillRect(pos.x + pos.width - 18, pos.y - 5, 12, 12);
|
||||
ctx.fillStyle = oldColor;
|
||||
}
|
||||
}
|
||||
|
||||
const newColor = () => {
|
||||
const letters = '0123456789ABCDEF'
|
||||
let color
|
||||
@@ -182,7 +219,9 @@ export function DrawGraph({
|
||||
|
||||
printScores(ctx, match.scores, px, py, 1);
|
||||
|
||||
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
printCard(ctx, pos, match.c1, match)
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
@@ -190,6 +229,7 @@ export function DrawGraph({
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1}
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
printCard(ctx, pos2, match.c2, match)
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
@@ -228,6 +268,7 @@ export function DrawGraph({
|
||||
printScores(ctx, match.scores, px, py, 1.5);
|
||||
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
printCard(ctx, pos, match.c1, match)
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
@@ -235,6 +276,7 @@ export function DrawGraph({
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1}
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
printCard(ctx, pos2, match.c2, match)
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
@@ -310,7 +352,7 @@ export function DrawGraph({
|
||||
for (const node of root) {
|
||||
let win_name = "";
|
||||
if (node.data.end) {
|
||||
win_name = win(node.data.scores) > 0
|
||||
win_name = node.data.win > 0
|
||||
? (node.data.c1FullName === null ? "???" : node.data.c1FullName)
|
||||
: (node.data.c2FullName === null ? "???" : node.data.c2FullName);
|
||||
}
|
||||
@@ -409,3 +451,279 @@ export function DrawGraph({
|
||||
<canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "relative", opacity: 1}} id="myCanvas"></canvas>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export function drawGraphForPdf(root = [], size = 14, cards = []) {
|
||||
const {getHeightCardForCombInMatch} = useCardsStatic(cards);
|
||||
const sizeY = size * 0.5;
|
||||
|
||||
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 - sizeY - ((sizeY * 1.5 / 2) | 0)) miny = py - sizeY - (sizeY * 1.5 / 2) | 0;
|
||||
if (maxy < py + sizeY + ((sizeY * 1.5 / 2) | 0)) maxy = py + sizeY + (sizeY * 1.5 / 2) | 0;
|
||||
} else {
|
||||
if (miny > py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0))
|
||||
miny = py - sizeY * 2 * death - ((sizeY * 1.5 / 2) | 0);
|
||||
if (maxy < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0))
|
||||
maxy = py + sizeY * 2 * death + ((sizeY * 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 - sizeY * 2 * death);
|
||||
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death);
|
||||
}
|
||||
|
||||
if (root != null) {
|
||||
py = (sizeY * 2 * root.at(0).death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root.at(0).death()) * 2;
|
||||
|
||||
maxx = px;
|
||||
minx = px;
|
||||
miny = py - (sizeY * 1.5 / 2) | 0;
|
||||
maxy = py + (sizeY * 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 + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
|
||||
px = maxx;
|
||||
}
|
||||
} else {
|
||||
minx = 0;
|
||||
maxx = 0;
|
||||
miny = 0;
|
||||
maxy = 0;
|
||||
}
|
||||
|
||||
return [minx, maxx, miny, maxy];
|
||||
}
|
||||
|
||||
// Fonction pour dessiner du texte avec gestion de la taille
|
||||
const printText = (ctx, s, x, y, width, height, lineG, lineD) => {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
let tSize = 17;
|
||||
let ratioX = height * 1.0 / 20.0;
|
||||
ctx.font = "100 " + tSize + "px Arial";
|
||||
|
||||
let mw = width - (ratioX * 2) | 0;
|
||||
if (ctx.measureText(s).width > mw) {
|
||||
do {
|
||||
tSize--;
|
||||
ctx.font = tSize + "px Arial";
|
||||
} while (ctx.measureText(s).width > mw && tSize > 10);
|
||||
|
||||
if (ctx.measureText(s).width > mw) {
|
||||
let truncated = "";
|
||||
const words = s.split(" ");
|
||||
for (const word of words) {
|
||||
if (ctx.measureText(truncated + word).width >= mw) {
|
||||
truncated += "...";
|
||||
break;
|
||||
} else {
|
||||
truncated += word + " ";
|
||||
}
|
||||
}
|
||||
s = truncated;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// Fonction pour afficher les scores
|
||||
const printScores = (ctx, scores, px, py, scale) => {
|
||||
ctx.save();
|
||||
ctx.translate(px - size * 2, py - size * scale);
|
||||
ctx.font = "100 14px Arial";
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
const score = scorePrint(scores[i].s1) + "-" + scorePrint(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();
|
||||
};
|
||||
|
||||
const printCard = (ctx, pos, combId, match) => {
|
||||
const cards2 = getHeightCardForCombInMatch(combId, match)
|
||||
if (cards2 != null) {
|
||||
let oldColor = ctx.fillStyle;
|
||||
switch (cards2.type) {
|
||||
case "BLUE":
|
||||
ctx.fillStyle = "#2e2efd";
|
||||
break;
|
||||
case "YELLOW":
|
||||
ctx.fillStyle = "#d8d800";
|
||||
break;
|
||||
case "RED":
|
||||
ctx.fillStyle = "#FF0000";
|
||||
break;
|
||||
case "BLACK":
|
||||
ctx.fillStyle = "#000000";
|
||||
break;
|
||||
default:
|
||||
ctx.fillStyle = "#FFFFFF00";
|
||||
}
|
||||
|
||||
if (cards2.match === match.id) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
ctx.arc(pos.x + pos.width - 10, pos.y + 5, 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "#000000"
|
||||
} else
|
||||
ctx.fillRect(pos.x + pos.width - 18, pos.y - 5, 12, 12);
|
||||
ctx.fillStyle = oldColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour dessiner un nœud
|
||||
const drawNode = (ctx, tree, px, py, max_y) => {
|
||||
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 + sizeY);
|
||||
ctx.lineTo(px - size, py - sizeY);
|
||||
ctx.moveTo(px - size, py + sizeY);
|
||||
ctx.lineTo(px - size * 2, py + sizeY);
|
||||
ctx.moveTo(px - size, py - sizeY);
|
||||
ctx.lineTo(px - size * 2, py - sizeY);
|
||||
ctx.stroke();
|
||||
|
||||
printScores(ctx, match.scores, px, py, 1);
|
||||
|
||||
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
|
||||
printCard(ctx, pos, match.c1, match)
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
|
||||
printCard(ctx, pos2, match.c2, match)
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
|
||||
|
||||
if (max_y.current < py + sizeY + ((sizeY * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + sizeY + (sizeY * 1.5 / 2 | 0);
|
||||
}
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px - size, py);
|
||||
ctx.lineTo(px - size, py + sizeY * 2 * death);
|
||||
ctx.moveTo(px - size, py);
|
||||
ctx.lineTo(px - size, py - sizeY * 2 * death);
|
||||
ctx.moveTo(px - size, py + sizeY * 2 * death);
|
||||
ctx.lineTo(px - size * 2, py + sizeY * 2 * death);
|
||||
ctx.moveTo(px - size, py - sizeY * 2 * death);
|
||||
ctx.lineTo(px - size * 2, py - sizeY * 2 * death);
|
||||
ctx.stroke();
|
||||
|
||||
printScores(ctx, match.scores, px, py, 1.5);
|
||||
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
|
||||
printCard(ctx, pos, match.c1, match)
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + sizeY * 2 * death - (sizeY * 1.5 / 2 | 0), width: size * 8, height: (sizeY * 1.5 | 0)}
|
||||
printCard(ctx, pos2, match.c2, match)
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
|
||||
|
||||
if (max_y.current < py + sizeY * 2 * death + ((sizeY * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + sizeY * 2 * death + ((sizeY * 1.5 / 2 | 0));
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.left != null) {
|
||||
drawNode(ctx, tree.left, px - size * 2 - size * 8, py - sizeY * 2 * death, max_y);
|
||||
}
|
||||
if (tree.right != null) {
|
||||
drawNode(ctx, tree.right, px - size * 2 - size * 8, py + sizeY * 2 * death, max_y);
|
||||
}
|
||||
};
|
||||
|
||||
// Dessiner sur le canvas principal
|
||||
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";
|
||||
|
||||
let px = maxx;
|
||||
let py;
|
||||
const max_y = {current: 0};
|
||||
|
||||
py = (sizeY * 2 * root[0].death() + (((sizeY * 1.5 / 2) | 0) + sizeY) * root[0].death()) * 2;
|
||||
max_y.current = py + (sizeY * 1.5 / 2 | 0);
|
||||
for (const node of root) {
|
||||
let win_name = "";
|
||||
if (node.data.end) {
|
||||
win_name = node.data.win > 0
|
||||
? (node.data.c1FullName === null ? "???" : node.data.c1FullName)
|
||||
: (node.data.c2FullName === null ? "???" : node.data.c2FullName);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#18A918";
|
||||
printText(ctx, win_name,
|
||||
px - size * 2 - size * 8, py - ((sizeY * 1.5 / 2) | 0),
|
||||
size * 8, (sizeY * 1.5 | 0), true, false);
|
||||
|
||||
px = px - size * 2 - size * 8;
|
||||
drawNode(ctx, node, px, py, max_y);
|
||||
py = max_y.current + ((sizeY * 2 * node.death() + ((sizeY * 1.5 / 2) | 0)));
|
||||
px = maxx;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user