75 Commits

Author SHA1 Message Date
dcaa79245c Merge pull request 'dev' (#124) from dev into master
Reviewed-on: #124
2026-08-06 14:17:49 +00:00
2772e44456 feat: auto fill certificate fields on add licence
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 11m30s
2026-08-06 16:16:47 +02:00
8f0e3c0738 feat: show old member by default 2026-08-06 15:14:30 +02:00
81f6169d41 feat: add licence detail to add log 2026-08-06 15:03:03 +02:00
931c5076b7 feat: update libs 2026-08-06 15:02:44 +02:00
a328e71035 feat: add licence state to membre find result 2026-08-05 16:45:18 +02:00
72f8a27272 Merge pull request 'fix: cache refresh v2' (#123) from dev into master
Reviewed-on: #123
2026-03-23 21:12:00 +00:00
d3805ea0e7 fix: cache refresh v2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m13s
2026-03-23 22:11:42 +01:00
a1e5006e95 Merge pull request 'fix: cache refresh' (#122) from dev into master
Reviewed-on: #122
2026-03-23 15:31:19 +00:00
2d883ca1da fix: cache refresh
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m10s
2026-03-23 16:29:11 +01:00
d357328e48 Merge pull request 'fix: add log in getAllClubArray' (#121) from dev into master
Reviewed-on: #121
2026-03-23 15:06:51 +00:00
2501b998fa fix: add log in getAllClubArray
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-23 16:05:09 +01:00
f9e4b83ad3 Merge pull request 'hot fix: remove save score button 2' (#120) from dev into master
Reviewed-on: #120
2026-03-21 11:09:17 +00:00
a7c89dbd73 hot fix: remove save score button 2
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m55s
2026-03-21 12:08:45 +01:00
ae087084e9 Merge pull request 'hot fix: remove save score button' (#119) from dev into master
Reviewed-on: #119
2026-03-21 11:00:12 +00:00
b2ee06d3b9 hot fix: remove save score button
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 44s
2026-03-21 11:59:37 +01:00
5afef2bcab Merge pull request 'fix: change obs lice name location' (#118) from dev into master
Reviewed-on: #118
2026-03-20 15:40:37 +00:00
3ed44bc044 fix: change obs lice name location
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m57s
2026-03-20 16:40:16 +01:00
55d39e02d2 Merge pull request 'fix: clear pub aff when no cat select' (#117) from dev into master
Reviewed-on: #117
2026-03-20 15:11:17 +00:00
d091503f19 fix: clear pub aff when no cat select
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-20 16:10:59 +01:00
d5c1273e18 Merge pull request 'fix: change methode for move upload file' (#116) from dev into master
Reviewed-on: #116
2026-03-18 12:07:58 +00:00
ea17989a82 fix: change methode for move upload file
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-03-18 13:07:39 +01:00
eda96d5ed2 Merge pull request 'fix: add more log for file copy' (#115) from dev into master
Reviewed-on: #115
2026-03-18 10:07:00 +00:00
e8555f7992 fix: add more log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m0s
2026-03-18 11:06:40 +01:00
3a4e2f160f Merge pull request 'fix: add log for file copy' (#114) from dev into master
Reviewed-on: #114
2026-03-18 09:48:37 +00:00
fdd42c6a63 fix: add log for file copy
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m1s
2026-03-18 10:48:09 +01:00
2b84854ef2 Merge pull request 'fix: obs support wss' (#113) from dev into master
Reviewed-on: #113
2026-03-17 22:16:29 +00:00
ddf530ce4c fix: obs support wss
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m8s
2026-03-17 23:16:06 +01:00
fc3bea7f76 Merge pull request 'fix: language detection' (#112) from dev into master
Reviewed-on: #112
2026-03-16 11:19:50 +00:00
286911cf89 fix: language detection
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m2s
2026-03-16 12:19:30 +01:00
e67e01640a Merge pull request 'fix: club guest mixing in result' (#111) from dev into master
Reviewed-on: #111
2026-03-10 17:47:33 +00:00
4bfa6e845d fix: club guest mixing in result
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m3s
2026-03-10 13:59:35 +01:00
e870ffdda0 Merge pull request 'fix: eslint version' (#110) from dev into master
Reviewed-on: #110
2026-03-09 12:19:27 +00:00
806ead5b98 fix: eslint version
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m29s
2026-03-09 13:19:12 +01:00
378a17ba75 Merge pull request 'dev' (#109) from dev into master
Reviewed-on: #109
2026-03-09 12:10:46 +00:00
ee0a7d87e9 feat: cm Add audio encodeur generator
Some checks failed
Deploy Production Server / if_merged (pull_request) Failing after 34s
2026-03-09 13:07:51 +01:00
812d873d5d fix: Match reorder refresh 2026-03-08 21:58:57 +01:00
d6d9f86254 feat: PDF for classement club 2026-02-27 11:10:31 +01:00
a22225ee7a feat: classement club 2026-02-23 12:01:36 +01:00
3cb12826d0 Merge pull request 'dev' (#108) from dev into master
Reviewed-on: #108
2026-02-20 16:59:40 +00:00
2f390b03e2 feat: add podium PDF
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s
2026-02-19 15:55:46 +01:00
ed5d73c25f feat: improve generation PDF for categories 2026-02-17 21:35:29 +01:00
0a368454c4 fix: duplicate id liceInput1 2026-02-16 22:49:49 +01:00
752f03cba5 feat: PDF for categories 2026-02-16 22:43:14 +01:00
d857fce71f feat: support decimal on weight 2026-02-13 21:42:48 +01:00
2fd09af0ea feat: auto categories full competition 2026-02-13 21:33:19 +01:00
d43cdc1a4e feat: auto categories 2026-02-11 14:08:13 +01:00
8663aa61cf feat: add silver cup png to cm 2026-02-06 18:02:11 +01:00
758e02dc5b feat: remove ^/api$ path log 2026-02-06 17:26:33 +01:00
72b248d09d Merge pull request 'dev' (#107) from dev into master
Reviewed-on: #107
2026-02-06 16:18:39 +00:00
31315c951a feat: auto poule
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 7m28s
2026-02-06 17:17:44 +01:00
ab4d57985c fix: add classement array in competition.js 2026-02-06 13:23:09 +01:00
2e846bf801 feat: use preset in cmt 2026-02-06 13:15:18 +01:00
9954dd002c feat: add config for preset in category + filter on select comb 2026-02-05 23:53:49 +01:00
7dab5b8880 fix: tree detection on cat edit 2026-02-05 22:32:31 +01:00
b37902466c feat: add team card when comb is register 2026-02-05 22:07:14 +01:00
89d9e04a6f feat: add classement match system 2026-02-05 21:29:27 +01:00
e2197d0712 fix: score panel fonction update 2026-02-01 15:25:49 +01:00
34a6911fa0 Merge pull request 'dev' (#106) from dev into master
Reviewed-on: #106
2026-01-30 14:07:08 +00:00
172dcdaa67 fix: ghost selection in PayAndValidateList
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m59s
2026-01-30 15:00:32 +01:00
cdd7221e86 fix: add weight config for all register source 2026-01-30 14:54:44 +01:00
952300d063 feat: card in result view 2026-01-30 14:50:21 +01:00
541d3824f3 feat: update CardModel 2026-01-30 11:58:31 +01:00
4c260b86b9 feat: remove old card system 2026-01-29 22:43:59 +01:00
9018ecef12 feat: card for team 2026-01-29 22:35:54 +01:00
d749dea6f4 feat: add return of table state 2026-01-27 22:11:49 +01:00
189eb135bb wip: competition card system 2026-01-26 22:36:26 +01:00
59ba31ae2d Merge pull request 'dev' (#105) from dev into master
Reviewed-on: #105
2026-01-23 15:49:24 +00:00
197ee0d5b1 feat: register import
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 6m49s
2026-01-23 16:48:09 +01:00
3d8597869d feat: update register export 2026-01-22 19:04:08 +01:00
4b56aa0209 Merge pull request 'dev' (#104) from dev into master
Reviewed-on: #104
2026-01-18 15:19:12 +00:00
eefa77369a Merge pull request 'feat: update js npm lib' (#103) from dev into master
Reviewed-on: #103
2026-01-16 15:21:11 +00:00
a942798a6c Merge pull request 'feat: add match eq support' (#102) from dev into master
Reviewed-on: #102
2026-01-16 14:37:55 +00:00
876545630a Merge pull request 'fix: translate backend asset includes in native' (#101) from dev into master
Reviewed-on: #101
2026-01-16 13:22:09 +00:00
cad6d14ba8 Merge pull request 'fix: translate backend add log' (#100) from dev into master
Reviewed-on: #100
2026-01-16 12:59:33 +00:00
92 changed files with 8645 additions and 2246 deletions

View File

@@ -44,7 +44,9 @@ public class FrontendForwardingFilter implements ContainerResponseFilter {
final String path = info.getPath(); final String path = info.getPath();
final String address = request.remoteAddress().toString(); final String address = request.remoteAddress().toString();
if (!path.equals("/api")) {
LOG.infof("Request %s %s from IP %s", method, path, address); LOG.infof("Request %s %s from IP %s", method, path, address);
}
int status = responseContext.getStatus(); int status = responseContext.getStatus();
if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) { if (status != 404 && !(status == 405 && "GET".equals(requestContext.getMethod()))) {

View 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
}
}

View File

@@ -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;
}

View File

@@ -45,6 +45,12 @@ public class CategoryModel {
String liceName = "1"; 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) @ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "id_preset", referencedColumnName = "id") @JoinColumn(name = "id_preset", referencedColumnName = "id")
CatPresetModel preset; CatPresetModel preset;

View File

@@ -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;
}

View File

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

View File

@@ -43,8 +43,8 @@ public class CompetitionGuestModel implements CombModel {
String country = "fr"; String country = "fr";
Integer weight = null; Float weight = null;
Integer weightReal = null; Float weightReal = null;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable( @JoinTable(
@@ -109,7 +109,7 @@ public class CompetitionGuestModel implements CombModel {
return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_)); return Stream.concat(comb.stream(), guest.stream()).anyMatch(c -> Objects.equals(c, comb_));
} }
public Integer getWeight2() { public Float getWeight2() {
return (this.weightReal != null) ? this.weightReal : this.weight; return (this.weightReal != null) ? this.weightReal : this.weight;
} }
} }

View File

@@ -63,10 +63,6 @@ public class MatchModel {
char poule = 'A'; 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) { public String getC1Name(MembreModel model, ResultPrivacy privacy) {
if (c1_id != null) if (c1_id != null)
return c1_id.getName(model, privacy); return c1_id.getName(model, privacy);
@@ -113,6 +109,22 @@ public class MatchModel {
return sum; 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) { public boolean isC1(Object comb) {
if (this.c1_guest != null && this.c1_guest.isInTeam(comb)) if (this.c1_guest != null && this.c1_guest.isInTeam(comb))
return true; return true;

View File

@@ -37,8 +37,8 @@ public class RegisterModel {
@JoinColumn(name = "id_membre") @JoinColumn(name = "id_membre")
MembreModel membre; MembreModel membre;
Integer weight; Float weight;
Integer weightReal; Float weightReal;
int overCategory = 0; int overCategory = 0;
Categorie categorie; Categorie categorie;
@@ -61,7 +61,7 @@ public class RegisterModel {
) )
List<CatPresetModel> categoriesInscrites = new ArrayList<>(); List<CatPresetModel> categoriesInscrites = new ArrayList<>();
public RegisterModel(CompetitionModel competition, MembreModel membre, Integer weight, int overCategory, public RegisterModel(CompetitionModel competition, MembreModel membre, Float weight, int overCategory,
Categorie categorie, ClubModel club) { Categorie categorie, ClubModel club) {
this.id = new RegisterId(competition.getId(), membre.getId()); this.id = new RegisterId(competition.getId(), membre.getId());
this.competition = competition; this.competition = competition;
@@ -91,7 +91,7 @@ public class RegisterModel {
return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)];
} }
public Integer getWeight2() { public Float getWeight2() {
if (weightReal != null) if (weightReal != null)
return weightReal; return weightReal;
return weight; return weight;

View File

@@ -40,6 +40,14 @@ public class TreeModel {
@JoinColumn(referencedColumnName = "id") @JoinColumn(referencedColumnName = "id")
TreeModel right; 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() { public List<TreeModel> flat() {
List<TreeModel> out = new ArrayList<>(); List<TreeModel> out = new ArrayList<>();
this.flat(out); this.flat(out);
@@ -55,4 +63,44 @@ public class TreeModel {
if (this.left != null) if (this.left != null)
this.left.flat(out); 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);
}
} }

View File

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

View File

@@ -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> {
}

View File

@@ -20,6 +20,9 @@ public class MatchRepository implements PanacheRepositoryBase<MatchModel, Long>
} }
public Uni<Void> create(List<MatchModel> matchModel) { public Uni<Void> create(List<MatchModel> matchModel) {
if (matchModel.isEmpty())
return Uni.createFrom().voidItem();
matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL)); matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL));
return Panache.withTransaction(() -> this.persist(matchModel) return Panache.withTransaction(() -> this.persist(matchModel)
.call(__ -> this.flush()) .call(__ -> this.flush())

View File

@@ -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());
}
}

View File

@@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.domain.entity; 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.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.model.RegisterModel; import fr.titionfire.ffsaf.data.model.RegisterModel;
@@ -26,8 +27,9 @@ public class CombEntity {
Genre genre; Genre genre;
String country; String country;
int overCategory; int overCategory;
Integer weight; Float weight;
List<CombEntity> teamMembers; List<CombEntity> teamMembers;
List<Long> categoriesInscrites;
public static CombEntity fromModel(MembreModel model) { public static CombEntity fromModel(MembreModel model) {
if (model == null) if (model == null)
@@ -36,7 +38,7 @@ public class CombEntity {
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(), return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
model.getClub() == null ? null : model.getClub().getClubId(), model.getClub() == null ? null : model.getClub().getClubId(),
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(), model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
0, null, new ArrayList<>()); 0, null, new ArrayList<>(), new ArrayList<>());
} }
@@ -45,9 +47,18 @@ public class CombEntity {
return null; return null;
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), 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), Stream.concat(model.getComb().stream().map(CombEntity::fromModel),
model.getGuest().stream().map(CombEntity::fromModel)).toList()); 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) { public static CombEntity fromModel(RegisterModel registerModel) {
@@ -58,6 +69,8 @@ public class CombEntity {
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(), return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(), registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(), registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight(), new ArrayList<>()); model.getCountry(), registerModel.getOverCategory(),
registerModel.getWeight2() != null ? registerModel.getWeight2() : registerModel.getWeight(),
new ArrayList<>(), new ArrayList<>());
} }
} }

View File

@@ -6,7 +6,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -23,7 +22,6 @@ public class MatchEntity {
private Date date; private Date date;
private List<ScoreEmbeddable> scores; private List<ScoreEmbeddable> scores;
private char poule; private char poule;
private List<CardboardEntity> cardboard;
public static MatchEntity fromModel(MatchModel model) { public static MatchEntity fromModel(MatchModel model) {
if (model == null) if (model == null)
@@ -35,22 +33,6 @@ public class MatchEntity {
model.getC2_id()), model.getC2_id()),
model.getCategory_ord(), model.isEnd(), model.getCategory().getId(), model.getDate(), model.getCategory_ord(), model.isEnd(), model.getCategory().getId(), model.getDate(),
model.getScores(), model.getScores(),
model.getPoule(), 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;
} }
} }

View File

@@ -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();
}
}

View File

@@ -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))));
}
}

View File

@@ -367,7 +367,10 @@ public class CompetitionService {
c.getBanMembre().remove(combModel.getId()); c.getBanMembre().remove(combModel.getId());
return Panache.withTransaction(() -> repository.persist(c)); return Panache.withTransaction(() -> repository.persist(c));
}) })
.chain(combModel -> updateRegister(data, c, combModel, true))) .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()) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites())); .setCategorieInscrite(r.getCategoriesInscrites()));
} else { } else {
@@ -390,9 +393,9 @@ public class CompetitionService {
model.setClub(data.getClub()); model.setClub(data.getClub());
model.setCountry(data.getCountry()); model.setCountry(data.getCountry());
model.setWeightReal(data.getWeightReal()); model.setWeightReal(data.getWeightReal());
model.setCategorie(data.getCategorie());
if (model.getCompetition().getRequiredWeight().contains(model.getCategorie())) if (model.getCompetition().getRequiredWeight().contains(model.getCategorie()))
model.setWeight(data.getWeight()); model.setWeight(data.getWeight());
model.setCategorie(data.getCategorie());
}) })
.call(g -> Mutiny.fetch(g.getCategoriesInscrites())) .call(g -> Mutiny.fetch(g.getCategoriesInscrites()))
.call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(), .call(g -> catPresetRepository.list("competition = ?1 AND id IN ?2", g.getCompetition(),
@@ -406,8 +409,8 @@ public class CompetitionService {
})) }))
.chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model)) .chain(model -> Panache.withTransaction(() -> competitionGuestRepository.persist(model))
.call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ? .call(r -> model.getCompetition().getSystem() == CompetitionSystem.INTERNAL ?
sRegister.sendRegister(model.getCompetition().getUuid(), sRegister.sendRegisterNoFetch(model.getCompetition().getUuid(), r)
r) : Uni.createFrom().voidItem())) : Uni.createFrom().voidItem()))
.map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites())); .map(g -> SimpleRegisterComb.fromModel(g).setCategorieInscrite(g.getCategoriesInscrites()));
} }
if ("club".equals(source)) if ("club".equals(source))
@@ -427,7 +430,10 @@ public class CompetitionService {
if (c.getBanMembre().contains(model.getId())) if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(trad.t("insc.err1")); throw new DForbiddenException(trad.t("insc.err1"));
})) }))
.chain(combModel -> updateRegister(data, c, combModel, false))) .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()) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())
.setCategorieInscrite(r.getCategoriesInscrites())); .setCategorieInscrite(r.getCategoriesInscrites()));
@@ -443,18 +449,101 @@ public class CompetitionService {
if (c.getBanMembre().contains(model.getId())) if (c.getBanMembre().contains(model.getId()))
throw new DForbiddenException(trad.t("insc.err2")); throw new DForbiddenException(trad.t("insc.err2"));
})) }))
.chain(combModel -> updateRegister(data, c, combModel, false))) .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())); .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, 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() return registerRepository.find("competition = ?1 AND membre = ?2", c, combModel).firstResult()
.onFailure().recoverWithNull() .onFailure().recoverWithNull()
.map(Unchecked.function(r -> { .map(Unchecked.function(r -> {
if (r != null) { if (r != null) {
if (!admin && r.isLockEdit()) if (!admin && r.isLockEdit())
throw new DForbiddenException(trad.t("insc.err3")); throw new DForbiddenException(trad.t("insc.err3"));
if (data.getOverCategory() != null || !append)
if (data.getOverCategory() == null)
r.setOverCategory(0);
else
r.setOverCategory(data.getOverCategory()); r.setOverCategory(data.getOverCategory());
r.setCategorie( r.setCategorie(
(combModel.getBirth_date() == null) ? combModel.getCategorie() : (combModel.getBirth_date() == null) ? combModel.getCategorie() :
@@ -464,6 +553,7 @@ public class CompetitionService {
if (days > -7) if (days > -7)
r.setClub(combModel.getClub()); r.setClub(combModel.getClub());
if (c.getRequiredWeight().contains(r.getCategorie2())) if (c.getRequiredWeight().contains(r.getCategorie2()))
if (data.getCountry() != null || !append)
r.setWeight(data.getWeight()); r.setWeight(data.getWeight());
if (admin) { if (admin) {
r.setWeightReal(data.getWeightReal()); r.setWeightReal(data.getWeightReal());
@@ -510,6 +600,27 @@ public class CompetitionService {
sRegister.sendRegister(c.getUuid(), r) : Uni.createFrom().voidItem()); 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) { private Uni<MembreModel> findComb(Long licence, String fname, String lname) {
if (licence != null && licence > 0) { if (licence != null && licence > 0) {
return combRepository.find("licence = ?1", licence).firstResult() return combRepository.find("licence = ?1", licence).firstResult()
@@ -821,7 +932,7 @@ public class CompetitionService {
.call(m -> Panache.withTransaction(() -> .call(m -> Panache.withTransaction(() ->
helloAssoRepository.persist( helloAssoRepository.persist(
new HelloAssoRegisterModel(cm, m, data.getId())))) new HelloAssoRegisterModel(cm, m, data.getId()))))
.chain(m -> updateRegister(req, cm, m, true))) .chain(m -> updateRegister(req, cm, m, true, true)))
.onFailure().recoverWithItem(throwable -> { .onFailure().recoverWithItem(throwable -> {
fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(), fail.add("%s %s - licence n°%d".formatted(item.getUser().getLastName(),
item.getUser().getFirstName(), optional.get())); item.getUser().getFirstName(), optional.get()));

View File

@@ -122,7 +122,9 @@ public class LicenceService {
.call(genLicenceNumberAndAccountIfNeed()) .call(genLicenceNumberAndAccountIfNeed())
: Uni.createFrom().nullItem() : 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)); licenceModel));
}); });
} else { } else {

View File

@@ -184,11 +184,17 @@ public class MembreService {
String finalSearch = search; String finalSearch = search;
return getLicenceListe(licenceRequest, payState) return getLicenceListe(licenceRequest, payState)
.map(l -> l.stream().map(l2 -> l2.getMembre().getId()).toList()) .chain(l -> {
.chain(ids -> {
PanacheQuery<MembreModel> query; PanacheQuery<MembreModel> query;
query = repository.find(queryStr, sort, finalSearch, ids, club).page(Page.ofSize(limit)); query = repository.find(queryStr, sort, finalSearch,
return getPageResult(query, limit, page); 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) { public Uni<List<SimpleMembreInOutData>> getAllExport(String subject) {
return repository.find("userId = ?1", subject).firstResult() return repository.find("userId = ?1", subject).firstResult()
.chain(membreModel -> repository.list("club = ?1", membreModel.getClub())) .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())); .map(l -> membres.stream().map(m -> SimpleMembreInOutData.fromModel(m, l)).toList()));
} }

View File

@@ -2,10 +2,13 @@ package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.repository.*; 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.data.ResultCategoryData;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException; import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.utils.*; 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.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Multi;
@@ -14,6 +17,7 @@ import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import lombok.Builder; import lombok.Builder;
import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import java.util.*; import java.util.*;
@@ -45,9 +49,17 @@ public class ResultService {
@Inject @Inject
MatchRepository matchRepository; MatchRepository matchRepository;
@Inject
CardRepository cardRepository;
@Inject @Inject
TradService trad; 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 final HashMap<Long, String> combTempIds = new HashMap<>();
private static String getCombTempId(Long key) { private static String getCombTempId(Long key) {
@@ -123,39 +135,44 @@ public class ResultService {
} }
public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) { public Uni<ResultCategoryData> getCategory(String uuid, long poule, SecurityCtx securityCtx) {
return hasAccess(uuid, securityCtx).chain(membreModel -> return hasAccess(uuid, securityCtx).chain(membreModel -> getData(uuid, poule, membreModel));
matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Mutiny.fetch(list.get(0).getCategory().getTree()))
.map(list -> getData(list, membreModel)));
} }
public Uni<ResultCategoryData> getCategory(String uuid, long poule) { 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) return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule)
.call(list -> list.isEmpty() ? Uni.createFrom().voidItem() : .call(list -> list.isEmpty() ? Uni.createFrom().voidItem() :
Mutiny.fetch(list.get(0).getCategory().getTree())) 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 -> {
private ResultCategoryData getData(List<MatchModel> matchModels, MembreModel membreModel) {
ResultCategoryData out = new ResultCategoryData(); ResultCategoryData out = new ResultCategoryData();
CategoryModel categoryModel = matchModels.get(0).getCategory(); CategoryModel categoryModel = matchModels.get(0).getCategory();
out.setName(categoryModel.getName()); out.setName(categoryModel.getName());
out.setType(categoryModel.getType()); out.setType(categoryModel.getType());
out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";")); out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName()
.split(";"));
out.setGenTime(System.currentTimeMillis()); out.setGenTime(System.currentTimeMillis());
out.setTreeIsClassement(categoryModel.isTreeAreClassement());
getArray2(matchModels, membreModel, out); getArray2(matchModels, membreModel, out);
getTree(categoryModel.getTree(), membreModel, out); getTree(categoryModel.getTree(), membreModel, cards, out);
getClassementArray(categoryModel, membreModel, cards, out);
return out; return out;
});
} }
private void getArray2(List<MatchModel> matchModels_, MembreModel membreModel, ResultCategoryData out) { public void getArray2(List<MatchModelExtend> matchModels_, MembreModel membreModel, ResultCategoryData out) {
List<MatchModel> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList(); List<MatchModelExtend> matchModels = matchModels_.stream().filter(o -> o.getCategory_ord() >= 0).toList();
HashMap<Character, List<MatchModel>> matchMap = new HashMap<>(); HashMap<Character, List<MatchModelExtend>> matchMap = new HashMap<>();
for (MatchModel model : matchModels) { for (MatchModelExtend model : matchModels) {
char g = model.getPoule(); char g = model.getPoule();
if (!matchMap.containsKey(g)) if (!matchMap.containsKey(g))
matchMap.put(g, new ArrayList<>()); matchMap.put(g, new ArrayList<>());
@@ -164,7 +181,7 @@ public class ResultService {
matchMap.forEach((c, matchEntities) -> { matchMap.forEach((c, matchEntities) -> {
List<ResultCategoryData.PouleArrayData> matchs = matchEntities.stream() 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, .map(o -> ResultCategoryData.PouleArrayData.fromModel(o, membreModel,
ResultPrivacy.REGISTERED_ONLY_NO_DETAILS)) ResultPrivacy.REGISTERED_ONLY_NO_DETAILS))
.toList(); .toList();
@@ -175,10 +192,11 @@ public class ResultService {
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(comb -> { .map(comb -> {
CombStat stat = makeStat(matchEntities, comb); CombStat stat = makeStat(matchEntities, comb);
return new ResultCategoryData.RankArray(0, return new ResultCategoryData.RankArray(0, comb,
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w, comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w,
stat.pointMake, stat.pointTake, stat.getPointRate()); stat.pointMake, stat.pointTake, stat.getPointRate());
}) })
.filter(r -> r.getPointMake() > 0 || r.getPointTake() > 0)
.sorted(Comparator .sorted(Comparator
.comparing(ResultCategoryData.RankArray::getScore) .comparing(ResultCategoryData.RankArray::getScore)
.thenComparing(ResultCategoryData.RankArray::getWin) .thenComparing(ResultCategoryData.RankArray::getWin)
@@ -203,27 +221,86 @@ public class ResultService {
}); });
} }
private static void convertTree(TreeModel src, TreeNode<ResultCategoryData.TreeData> dst, MembreModel membreModel, public void getClassementArray(CategoryModel categoryModel, MembreModel membreModel, List<CardModel> cards,
ResultPrivacy privacy) { ResultCategoryData out) {
dst.setData(ResultCategoryData.TreeData.from(src.getMatch(), membreModel, privacy)); if ((categoryModel.getType() & 2) != 0) {
if (src.getLeft() != null) { AtomicInteger rank = new AtomicInteger(0);
dst.setLeft(new TreeNode<>()); categoryModel.getTree().stream()
convertTree(src.getLeft(), dst.getLeft(), membreModel, privacy); .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()));
}
} }
if (src.getRight() != null) {
dst.setRight(new TreeNode<>());
convertTree(src.getRight(), dst.getRight(), membreModel, privacy);
} }
} }
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<>(); ArrayList<TreeNode<ResultCategoryData.TreeData>> trees = new ArrayList<>();
treeModels.stream() treeModels.stream()
.filter(t -> t.getLevel() != 0) .filter(t -> t.getLevel() != 0)
.sorted(Comparator.comparing(TreeModel::getLevel)) .sorted(Comparator.comparing(TreeModel::getLevel))
.forEach(treeModel -> { .forEach(treeModel -> {
TreeNode<ResultCategoryData.TreeData> root = new TreeNode<>(); 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); trees.add(root);
}); });
out.setTrees(trees); out.setTrees(trees);
@@ -241,10 +318,13 @@ public class ResultService {
private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) { private Uni<CombsArrayData> getAllCombArray_(String uuid, MembreModel membreModel) {
return registerRepository.list("competition.uuid = ?1", uuid) return registerRepository.list("competition.uuid = ?1", uuid)
.chain(registers -> matchRepository.list("category.compet.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 -> { .map(pair -> {
List<RegisterModel> registers = pair.getKey(); List<RegisterModel> registers = pair.getKey();
List<MatchModel> matchModels = pair.getValue(); List<MatchModelExtend> matchModels = pair.getValue();
CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder();
@@ -290,11 +370,10 @@ public class ResultService {
.toList(); .toList();
builder.nb_insc(combs.size()); builder.nb_insc(combs.size());
builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); builder.tt_match((int) matchModels.stream().filter(MatchModelExtend::isEnd).count());
builder.point(matchModels.stream() builder.point(matchModels.stream()
.filter(MatchModel::isEnd) .filter(MatchModelExtend::isEnd)
.flatMap(m -> m.getScores().stream()) .flatMap(m -> m.getScoresToCompute().stream())
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.mapToInt(s -> s.getS1() + s.getS2()).sum()); .mapToInt(s -> s.getS1() + s.getS2()).sum());
builder.combs(combs); builder.combs(combs);
@@ -345,7 +424,7 @@ public class ResultService {
return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found"))); return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found")));
} }
Uni<List<MatchModel>> uni; Uni<List<MatchModelExtend>> uni;
if (id >= 0) { if (id >= 0) {
uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id, uni = registerRepository.find("membre.id = ?1 AND competition.uuid = ?2 AND membre.resultPrivacy <= ?3", id,
uuid, privacy).firstResult() uuid, privacy).firstResult()
@@ -362,7 +441,10 @@ public class ResultService {
return matchRepository.list( return matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + "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)", "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()); uuid, registerModel.getMembre())
.chain(matchModels -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards -> matchModels.stream().map(m -> new MatchModelExtend(m, cards))
.toList()));
})); }));
} else { } else {
uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult() uni = competitionGuestRepository.find("id = ?1 AND competition.uuid = ?2", -id, uuid).firstResult()
@@ -375,12 +457,15 @@ public class ResultService {
return matchRepository.list( return matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + "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)", "WHERE m.category.compet.uuid = ?1 AND (m.c1_guest = ?2 OR m.c2_guest = ?2 OR c1g = ?2 OR c2g = ?2)",
uuid, guestModel); 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 -> { return uni.invoke(matchModels -> {
List<CategoryModel> pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct() List<CategoryModel> pouleModels = matchModels.stream().map(MatchModelExtend::getCategory).distinct()
.toList(); .toList();
List<CombArrayData.MatchsData> matchs = new ArrayList<>(); List<CombArrayData.MatchsData> matchs = new ArrayList<>();
@@ -388,7 +473,7 @@ public class ResultService {
AtomicInteger sumPointMake = new AtomicInteger(0); AtomicInteger sumPointMake = new AtomicInteger(0);
AtomicInteger sumPointTake = 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) || if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) ||
(matchModel.getC2_id() == null && matchModel.getC2_guest() == null)) (matchModel.getC2_id() == null && matchModel.getC2_guest() == null))
continue; continue;
@@ -405,35 +490,33 @@ public class ResultService {
if (matchModel.isC1(id)) { if (matchModel.isC1(id)) {
builder2.adv(matchModel.getC2Name()); builder2.adv(matchModel.getC2Name());
if (matchModel.isEnd()) { if (matchModel.isEnd()) {
matchModel.getScores().stream() matchModel.getScoresToCompute()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(scoreEntity -> { .forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS1()); pointMake.addAndGet(scoreEntity.getS1());
pointTake.addAndGet(scoreEntity.getS2()); pointTake.addAndGet(scoreEntity.getS2());
}); });
builder2.score(matchModel.getScores().stream() builder2.score(matchModel.getScoresToPrint().stream()
.map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()); .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList());
} else { } else {
builder2.score(new ArrayList<>()); builder2.score(new ArrayList<>());
} }
builder2.win(matchModel.isEnd() && matchModel.win() > 0); builder2.win(matchModel.isEnd() && matchModel.getWin() > 0);
} else { } else {
builder2.adv(matchModel.getC1Name()); builder2.adv(matchModel.getC1Name());
if (matchModel.isEnd()) { if (matchModel.isEnd()) {
matchModel.getScores().stream() matchModel.getScoresToCompute()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(scoreEntity -> { .forEach(scoreEntity -> {
pointMake.addAndGet(scoreEntity.getS2()); pointMake.addAndGet(scoreEntity.getS2());
pointTake.addAndGet(scoreEntity.getS1()); pointTake.addAndGet(scoreEntity.getS1());
}); });
builder2.score(matchModel.getScores().stream() builder2.score(matchModel.getScoresToPrint().stream()
.map(s -> new Integer[]{s.getS2(), s.getS1()}).toList()); .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList());
} else { } else {
builder2.score(new ArrayList<>()); builder2.score(new ArrayList<>());
} }
builder2.win(matchModel.isEnd() && matchModel.win() < 0); builder2.win(matchModel.isEnd() && matchModel.getWin() < 0);
} }
builder2.eq(matchModel.isEnd() && matchModel.win() == 0); builder2.eq(matchModel.isEnd() && matchModel.getWin() == 0);
builder2.ratio( builder2.ratio(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
@@ -525,24 +608,45 @@ public class ResultService {
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.guest c1g LEFT JOIN m.c2_guest.guest c2g " + "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)", "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) uuid, guests)
.map(matchModels ->
.chain(mm -> cardRepository.list("competition.uuid = ?1", uuid)
.map(cards ->
getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(), getClubArray2(clubName, guests.stream().map(o -> (CombModel) o).toList(),
matchModels, new ArrayList<>(), membreModel))); mm.stream().map(m -> new MatchModelExtend(m, cards)).toList(),
new ArrayList<>(), membreModel)
)));
} else { } else {
return clubRepository.findById(id).chain(clubModel -> 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) registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel)
.chain(registers -> matchRepository.list( .chain(registers -> matchRepository.list(
"SELECT DISTINCT m FROM MatchModel m LEFT JOIN m.c1_guest.comb c1g LEFT JOIN m.c2_guest.comb c2g " + "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)", "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()) uuid, registers.stream().map(RegisterModel::getMembre).toList())
.map(matchModels -> .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(), getClubArray2(clubModel.getName(),
registers.stream().map(o -> (CombModel) o.getMembre()).toList(), Stream.concat(
matchModels, registers, membreModel)))); 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) { List<RegisterModel> registers, MembreModel membreModel) {
ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder();
builder.name(name); builder.name(name);
@@ -597,14 +701,14 @@ public class ResultService {
return builder.build(); return builder.build();
} }
private static CombStat makeStat(List<MatchModel> matchModels, CombModel comb) { private static CombStat makeStat(List<MatchModelExtend> matchModels, CombModel comb) {
CombStat stat = new CombStat(); CombStat stat = new CombStat();
matchModels.stream() matchModels.stream()
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb))) .filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> { .forEach(matchModel -> {
stat.match_ids.add(matchModel.getId()); stat.match_ids.add(matchModel.getId());
int win = matchModel.win(); int win = matchModel.getWin();
if (win == 0) { if (win == 0) {
stat.score += 1; stat.score += 1;
} else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) { } else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
@@ -615,8 +719,7 @@ public class ResultService {
stat.l++; stat.l++;
} }
matchModel.getScores().stream() matchModel.getScoresToCompute()
.filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> { .forEach(score -> {
if (matchModel.isC1(comb)) { if (matchModel.isC1(comb)) {
stat.pointMake += score.getS1(); stat.pointMake += score.getS1();
@@ -641,6 +744,112 @@ 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 @RegisterForReflection
public static class CombStat { public static class CombStat {
public int w; public int w;

View File

@@ -1,10 +1,12 @@
package fr.titionfire.ffsaf.domain.service; package fr.titionfire.ffsaf.domain.service;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.MissingResourceException; import java.util.MissingResourceException;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -21,11 +23,20 @@ public class TradService {
return translate(key); 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) { public String translate(String key) {
ContainerRequestContext requestContext = requestContextInstance.get(); ContainerRequestContext requestContext = requestContextInstance.get();
Locale userLocale = (Locale) requestContext.getProperty("userLocale"); Locale userLocale = (Locale) requestContext.getProperty("userLocale");
return translate(key, userLocale);
}
public String translate(String key, Locale userLocale) {
try { try {
ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale); ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale);
return messages.getString(key); return messages.getString(key);

View File

@@ -50,6 +50,16 @@ public class CompetitionEndpoints {
return service.addRegisterComb(securityCtx, id, data, source); 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 @DELETE
@Path("{id}/register/{comb_id}/{source}") @Path("{id}/register/{comb_id}/{source}")
@Authenticated @Authenticated

View File

@@ -8,6 +8,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
@Path("api/public/result/{id}") @Path("api/public/result/{id}")
public class ExternalResultEndpoints { public class ExternalResultEndpoints {
@@ -73,6 +74,13 @@ public class ExternalResultEndpoints {
return resultService.getClubList(id); return resultService.getClubList(id);
} }
@GET
@Path("/club/classement")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<ResultService.ClubClassement>> clubClassement() {
return resultService.getAllClubArray(id);
}
@GET @GET
@Path("/club/data") @Path("/club/data")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View File

@@ -47,6 +47,12 @@ public class ResultEndpoints {
return resultService.getClubList(uuid, securityCtx); 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 @GET
@Path("{uuid}/club/{id}") @Path("{uuid}/club/{id}")
public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) { public Uni<ResultService.ClubArrayData> getClub(@PathParam("uuid") String uuid, @PathParam("id") long id) {

View File

@@ -107,7 +107,7 @@ public class CompetitionData {
public static class SimpleRegister { public static class SimpleRegister {
long id; long id;
int overCategory; int overCategory;
Integer weight; Float weight;
Categorie categorie; Categorie categorie;
Long club; Long club;
String club_str; String club_str;

View File

@@ -4,10 +4,12 @@ import fr.titionfire.ffsaf.data.model.CatPresetModel;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
@Data @Data
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@RegisterForReflection @RegisterForReflection
public class PresetData { public class PresetData {

View File

@@ -18,9 +18,9 @@ public class RegisterRequestData {
private String fname; private String fname;
private String lname; private String lname;
private Integer weight; private Float weight;
private Integer weightReal; private Float weightReal;
private int overCategory; private Integer overCategory;
private boolean lockEdit = false; private boolean lockEdit = false;
private List<Long> categoriesInscrites; private List<Long> categoriesInscrites;

View File

@@ -1,7 +1,9 @@
package fr.titionfire.ffsaf.rest.data; 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.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.entity.MatchModelExtend;
import fr.titionfire.ffsaf.utils.ResultPrivacy; import fr.titionfire.ffsaf.utils.ResultPrivacy;
import fr.titionfire.ffsaf.utils.ScoreEmbeddable; import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
import fr.titionfire.ffsaf.utils.TreeNode; import fr.titionfire.ffsaf.utils.TreeNode;
@@ -21,9 +23,11 @@ import java.util.List;
public class ResultCategoryData { public class ResultCategoryData {
int type; int type;
String name; String name;
boolean treeIsClassement;
HashMap<Character, List<PouleArrayData>> matchs = new HashMap<>(); HashMap<Character, List<PouleArrayData>> matchs = new HashMap<>();
HashMap<Character, List<RankArray>> rankArray = new HashMap<>(); HashMap<Character, List<RankArray>> rankArray = new HashMap<>();
ArrayList<TreeNode<TreeData>> trees; ArrayList<TreeNode<TreeData>> trees;
List<ClassementData> classement = new ArrayList<>();
String[] liceName; String[] liceName;
long genTime; long genTime;
@@ -32,6 +36,8 @@ public class ResultCategoryData {
@RegisterForReflection @RegisterForReflection
public static class RankArray { public static class RankArray {
int rank; int rank;
@JsonIgnore
CombModel comb;
String name; String name;
int score; int score;
int win; int win;
@@ -43,16 +49,18 @@ public class ResultCategoryData {
@RegisterForReflection @RegisterForReflection
public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue, public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue,
boolean eq, boolean end, Date date) { boolean eq, boolean end, Date date) {
public static PouleArrayData fromModel(MatchModel matchModel, MembreModel membreModel, ResultPrivacy privacy) { public static PouleArrayData fromModel(MatchModelExtend matchModel, MembreModel membreModel,
ResultPrivacy privacy) {
return new PouleArrayData( return new PouleArrayData(
matchModel.getC1Name(membreModel, privacy), matchModel.getC1Name(membreModel, privacy),
matchModel.isEnd() && matchModel.win() > 0, matchModel.isEnd() && matchModel.getWin() > 0,
matchModel.isEnd() ? 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<>(), : new ArrayList<>(),
matchModel.isEnd() && matchModel.win() < 0, matchModel.isEnd() && matchModel.getWin() < 0,
matchModel.getC2Name(membreModel, privacy), matchModel.getC2Name(membreModel, privacy),
matchModel.isEnd() && matchModel.win() == 0, matchModel.isEnd() && matchModel.getWin() == 0,
matchModel.isEnd(), matchModel.isEnd(),
matchModel.getDate()); matchModel.getDate());
} }
@@ -60,10 +68,16 @@ public class ResultCategoryData {
@RegisterForReflection @RegisterForReflection
public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores, public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores,
boolean end) { boolean end, int win, int level, Date date) {
public static TreeData from(MatchModel match, MembreModel membreModel, ResultPrivacy privacy) { public static TreeData from(MatchModelExtend match, int level, MembreModel membreModel, ResultPrivacy privacy) {
return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), return new TreeData(match.getId(), match.getC1Name(membreModel, privacy),
match.getC2Name(membreModel, privacy), match.getScores(), match.isEnd()); 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) {
}
} }

View File

@@ -2,10 +2,14 @@ package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.LicenceModel; import fr.titionfire.ffsaf.data.model.LicenceModel;
import fr.titionfire.ffsaf.data.model.MembreModel; import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -24,7 +28,27 @@ public class SimpleMembreInOutData {
public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) { public static SimpleMembreInOutData fromModel(MembreModel membreModel, List<LicenceModel> lc) {
LicenceModel currentLicence = lc.stream().filter(l -> l.getMembre().getId().equals(membreModel.getId())) 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( return new SimpleMembreInOutData(
membreModel.getLicence(), membreModel.getLicence(),
@@ -33,8 +57,8 @@ public class SimpleMembreInOutData {
membreModel.getEmail(), membreModel.getEmail(),
membreModel.getGenre().str, membreModel.getGenre().str,
membreModel.getBirth_date(), membreModel.getBirth_date(),
currentLicence != null, currentLicence != null && currentLicence.getSaison() == Utils.getSaison(),
currentLicence == null ? null : currentLicence.getCertificate() certif
); );
} }
} }

View File

@@ -8,12 +8,14 @@ import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection @RegisterForReflection
public class SimpleRegisterComb { public class SimpleRegisterComb {
private long id; private long id;
@@ -24,8 +26,8 @@ public class SimpleRegisterComb {
private Categorie categorie; private Categorie categorie;
private SimpleClubModel club; private SimpleClubModel club;
private Integer licence; private Integer licence;
private Integer weight; private Float weight;
private Integer weightReal; private Float weightReal;
private int overCategory; private int overCategory;
private boolean hasLicenceActive; private boolean hasLicenceActive;
private boolean lockEdit; private boolean lockEdit;

View File

@@ -10,13 +10,14 @@ import java.util.List;
@Data @Data
@RegisterForReflection @RegisterForReflection
public class PageResult<T> { 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; 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; 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; 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 long result_count;
private List<T> result = new ArrayList<>(); private List<T> result = new ArrayList<>();
private Object additionalData;
} }

View File

@@ -20,6 +20,8 @@ import java.io.*;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.concurrent.CompletableFuture; 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) { public static Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(date); cal.setTime(date);
@@ -244,15 +255,16 @@ public class Utils {
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + "."); FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter); File[] files = dirFile.listFiles(filter);
if (files != null) { if (files != null) {
for (File f : files) { for (File f2 : files) {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
f.delete(); f2.delete();
} }
} }
File f = file.filePath().toFile(); Path f = file.filePath();
//noinspection ResultOfMethodCallIgnored System.out.println(f + " -> " + new File(dirFile, id + "." + detectedExtensions[0]));
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0])); Files.copy(f, new File(dirFile, id + "." + detectedExtensions[0]).toPath(),
StandardCopyOption.REPLACE_EXISTING);
return "ok"; return "ok";
}))); })));
} }

View File

@@ -46,11 +46,17 @@ public class CompetitionWS {
RRegister rRegister; RRegister rRegister;
@Inject @Inject
RCardboard rCardboard; RCard rCard;
@Inject @Inject
RTeam rTeam; RTeam rTeam;
@Inject
RState rState;
@Inject
RPDF rpdf;
@Inject @Inject
SecurityCtx securityCtx; SecurityCtx securityCtx;
@@ -93,8 +99,10 @@ public class CompetitionWS {
getWSReceiverMethods(RMatch.class, rMatch); getWSReceiverMethods(RMatch.class, rMatch);
getWSReceiverMethods(RCategorie.class, rCategorie); getWSReceiverMethods(RCategorie.class, rCategorie);
getWSReceiverMethods(RRegister.class, rRegister); getWSReceiverMethods(RRegister.class, rRegister);
getWSReceiverMethods(RCardboard.class, rCardboard); getWSReceiverMethods(RCard.class, rCard);
getWSReceiverMethods(RTeam.class, rTeam); getWSReceiverMethods(RTeam.class, rTeam);
getWSReceiverMethods(RState.class, rState);
getWSReceiverMethods(RPDF.class, rpdf);
executor = notifyExecutor; executor = notifyExecutor;
} }
@@ -141,6 +149,7 @@ public class CompetitionWS {
LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size()); LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size());
waitingResponse.remove(connection); waitingResponse.remove(connection);
rState.removeConnection(connection);
} }
private MessageOut makeReply(MessageIn message, Object data) { private MessageOut makeReply(MessageIn message, Object data) {
@@ -230,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 @OnError
Uni<Void> error(WebSocketConnection connection, ForbiddenException t) { Uni<Void> error(WebSocketConnection connection, ForbiddenException t) {
return connection.close(CloseReason.INTERNAL_SERVER_ERROR); return connection.close(CloseReason.INTERNAL_SERVER_ERROR);

View 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) {
}
}

View File

@@ -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;
}
}

View File

@@ -1,20 +1,25 @@
package fr.titionfire.ffsaf.ws.recv; package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CategoryModel; import fr.titionfire.ffsaf.data.model.*;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.model.TreeModel;
import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.data.repository.*;
import fr.titionfire.ffsaf.domain.entity.MatchEntity; 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.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.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.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.utils.TreeNode; import fr.titionfire.ffsaf.utils.TreeNode;
import fr.titionfire.ffsaf.ws.PermLevel; import fr.titionfire.ffsaf.ws.PermLevel;
import fr.titionfire.ffsaf.ws.send.SSCategorie; 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.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.UserData;
import io.quarkus.websockets.next.WebSocketConnection; import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked; import io.smallrye.mutiny.unchecked.Unchecked;
@@ -23,8 +28,9 @@ import jakarta.inject.Inject;
import lombok.Data; import lombok.Data;
import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import java.util.stream.Stream;
@WithSession @WithSession
@ApplicationScoped @ApplicationScoped
@@ -45,7 +51,16 @@ public class RCategorie {
TreeRepository treeRepository; TreeRepository treeRepository;
@Inject @Inject
CardboardRepository cardboardRepository; CardService cardService;
@Inject
CardRepository cardRepository;
@Inject
CatPresetRepository catPresetRepository;
@Inject
ResultService resultService;
@Inject @Inject
TradService trad; TradService trad;
@@ -54,9 +69,9 @@ public class RCategorie {
return categoryRepository.findById(id) return categoryRepository.findById(id)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
if (o == null) 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"))) 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.setName(cat.getName());
fullCategory.setLiceName(cat.getLiceName()); fullCategory.setLiceName(cat.getLiceName());
fullCategory.setType(cat.getType()); fullCategory.setType(cat.getType());
fullCategory.setTreeAreClassement(cat.isTreeAreClassement());
fullCategory.setFullClassement(cat.isFullClassement());
fullCategory.setPreset(PresetData.fromModel(cat.getPreset()));
}) })
.call(cat -> Mutiny.fetch(cat.getMatchs()) .call(cat -> Mutiny.fetch(cat.getMatchs())
.map(matchModels -> matchModels.stream().filter(o -> o.getCategory_ord() >= 0) .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()) .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId())
.map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList())
.invoke(fullCategory::setTrees)) .invoke(fullCategory::setTrees))
.call(cat -> cardService.getAll(cat.getCompet())
.invoke(fullCategory::setCards))
.map(__ -> fullCategory); .map(__ -> fullCategory);
} }
@@ -98,20 +118,67 @@ public class RCategorie {
categoryModel.setTree(new ArrayList<>()); categoryModel.setTree(new ArrayList<>());
categoryModel.setType(categorie.type); categoryModel.setType(categorie.type);
categoryModel.setLiceName(categorie.liceName); 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); return categoryRepository.create(categoryModel);
}) })
.invoke(cat -> SSCategorie.sendAddCategory(connection, cat)) .invoke(cat -> SSCategorie.sendAddCategory(connection, cat))
.map(CategoryModel::getId); .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) @WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) { 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 -> { .chain(cat -> {
cat.setName(categorie.name); cat.setName(categorie.name);
cat.setLiceName(categorie.liceName); cat.setLiceName(categorie.liceName);
cat.setType(categorie.type); cat.setType(categorie.type);
cat.setTreeAreClassement(categorie.treeAreClassement);
cat.setFullClassement(categorie.fullClassement);
return Panache.withTransaction(() -> categoryRepository.persist(cat)); return Panache.withTransaction(() -> categoryRepository.persist(cat));
}) })
.call(cat -> { .call(cat -> {
@@ -214,17 +281,161 @@ public class RCategorie {
public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) { public Uni<Void> deleteCategory(WebSocketConnection connection, Long id) {
return getById(id, connection) return getById(id, connection)
.call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId()) .call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId())
.call(__ -> cardboardRepository.delete("match.category = ?1", cat))
.call(__ -> matchRepository.delete("category = ?1", cat)))) .call(__ -> matchRepository.delete("category = ?1", cat))))
.chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat))) .chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat)))
.invoke(__ -> SSCategorie.sendDelCategory(connection, id)) .invoke(__ -> SSCategorie.sendDelCategory(connection, id))
.replaceWithVoid(); .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 @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) { 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; String name;
int type; int type;
String liceName; String liceName;
boolean treeAreClassement = false;
boolean fullClassement = false;
PresetData preset;
List<TreeEntity> trees = null; List<TreeEntity> trees = null;
List<MatchEntity> matches; List<MatchEntity> matches;
List<CardModel> cards;
} }
} }

View File

@@ -46,18 +46,18 @@ public class RMatch {
CompetitionGuestRepository competitionGuestRepository; CompetitionGuestRepository competitionGuestRepository;
@Inject @Inject
CardboardRepository cardboardRepository; TradService trad;
@Inject @Inject
TradService trad; RState rState;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) { private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id) return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
if (o == null) 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"))) 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) return categoryRepository.findById(m.categorie)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
if (o == null) 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"))) 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(categoryModel -> creatMatch(categoryModel, m))
.chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))) .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)))
@@ -195,6 +195,7 @@ public class RMatch {
return Panache.withTransaction(() -> matchRepository.persist(mm)); return Panache.withTransaction(() -> matchRepository.persist(mm));
}) })
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm))) .invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
.invoke(mm -> rState.setMatchEnd(connection, matchEnd))
.chain(mm -> updateEndAndTree(mm, toSend)) .chain(mm -> updateEndAndTree(mm, toSend))
.invoke(__ -> SSMatch.sendMatch(connection, toSend)) .invoke(__ -> SSMatch.sendMatch(connection, toSend))
.replaceWithVoid(); .replaceWithVoid();
@@ -285,9 +286,7 @@ public class RMatch {
public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) { public Uni<Void> deleteMatch(WebSocketConnection connection, Long idMatch) {
return getById(idMatch, connection) return getById(idMatch, connection)
.map(__ -> idMatch) .map(__ -> idMatch)
.chain(l -> Panache.withTransaction(() -> .chain(l -> Panache.withTransaction(() -> matchRepository.delete("id = ?1", l)))
cardboardRepository.delete("match.id = ?1", l)
.chain(__ -> matchRepository.delete("id = ?1", l))))
.invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch)) .invoke(__ -> SSMatch.sendDeleteMatch(connection, idMatch))
.replaceWithVoid(); .replaceWithVoid();
} }
@@ -298,9 +297,9 @@ public class RMatch {
return categoryRepository.findById(data.categorie) return categoryRepository.findById(data.categorie)
.invoke(Unchecked.consumer(o -> { .invoke(Unchecked.consumer(o -> {
if (o == null) 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"))) 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() : .call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() :
(Panache.withTransaction( (Panache.withTransaction(

View 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) {
}
}

View File

@@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.ws.PermLevel;
import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.WebSocketConnection; import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@@ -24,14 +25,20 @@ public class RRegister {
@WSReceiver(code = "getRegister", permission = PermLevel.TABLE) @WSReceiver(code = "getRegister", permission = PermLevel.TABLE)
public Uni<List<CombEntity>> getRegister(WebSocketConnection connection, Object o) { public Uni<List<CombEntity>> getRegister(WebSocketConnection connection, Object o) {
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<>(); ArrayList<CombEntity> combEntities = new ArrayList<>();
combEntities.addAll(cm.getInsc().stream().map(CombEntity::fromModel).toList()); return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
combEntities.addAll(cm.getGuests().stream().map(CombEntity::fromModel).toList()); .call(cm -> Mutiny.fetch(cm.getInsc())
return combEntities; .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);
} }
} }

View 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
}
}

View File

@@ -86,7 +86,7 @@ public class RTeam {
.max(Integer::compareTo) .max(Integer::compareTo)
.map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1)); .map(i -> Categorie.values()[i]).orElse(Categorie.SENIOR1));
List<Integer> s = Stream.concat( List<Float> s = Stream.concat(
pair.getKey().stream().map(RegisterModel::getWeight), pair.getKey().stream().map(RegisterModel::getWeight),
pair.getValue().stream().map(CompetitionGuestModel::getWeight)) pair.getValue().stream().map(CompetitionGuestModel::getWeight))
.filter(Objects::nonNull).toList(); .filter(Objects::nonNull).toList();
@@ -95,7 +95,7 @@ public class RTeam {
} else if (s.size() == 1) { } else if (s.size() == 1) {
team.setWeight(s.get(0)); team.setWeight(s.get(0));
} else { } else {
team.setWeight((int) s.stream().mapToInt(Integer::intValue) team.setWeight((float) s.stream().mapToDouble(Float::doubleValue)
.average() .average()
.orElse(0)); .orElse(0));
} }

View File

@@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.RegisterModel; import fr.titionfire.ffsaf.data.model.RegisterModel;
import fr.titionfire.ffsaf.domain.entity.CombEntity; import fr.titionfire.ffsaf.domain.entity.CombEntity;
import fr.titionfire.ffsaf.domain.service.CardService;
import fr.titionfire.ffsaf.net2.MessageType; import fr.titionfire.ffsaf.net2.MessageType;
import fr.titionfire.ffsaf.ws.CompetitionWS; import fr.titionfire.ffsaf.ws.CompetitionWS;
import fr.titionfire.ffsaf.ws.MessageOut; import fr.titionfire.ffsaf.ws.MessageOut;
@@ -13,6 +14,7 @@ import io.quarkus.websockets.next.UserData;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.hibernate.reactive.mutiny.Mutiny;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -25,16 +27,48 @@ public class SRegister {
@Inject @Inject
OpenConnections connections; OpenConnections connections;
@Inject
CardService cardService;
public Uni<Void> sendRegister(String uuid, RegisterModel registerModel) { 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) { 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) { 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) { public Uni<Void> send(String uuid, String code, Object data) {

View 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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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.2.inconnue=License member no. 2 unknown
licence.membre.n.3.inconnue=License member no. 3 unknown licence.membre.n.3.inconnue=License member no. 3 unknown
demande.d.affiliation.non.trouve=Affiliation request not found 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

View File

@@ -81,3 +81,10 @@ licence.membre.n.1.inconnue=Licence du membre n
licence.membre.n.2.inconnue=Licence du membre n°2 inconnue licence.membre.n.2.inconnue=Licence du membre n°2 inconnue
licence.membre.n.3.inconnue=Licence du membre n°3 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,40 +13,42 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.3.1",
"@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.3.1",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.3.1",
"@fortawesome/react-fontawesome": "^3.1.1", "@fortawesome/react-fontawesome": "^3.5.0",
"axios": "^1.13.2", "axios": "1.18.0",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"i18next": "^25.7.4", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^4.0.1",
"jspdf": "4.2.1",
"jspdf-autotable": "^5.0.8",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"obs-websocket-js": "^5.0.7", "obs-websocket-js": "^5.0.8",
"proj4": "^2.20.2", "proj4": "^2.21.0",
"react": "^19.2.3", "react": "^19.2.8",
"react-dom": "^19.2.3", "react-dom": "^19.2.8",
"react-i18next": "^16.5.3", "react-i18next": "^17.0.11",
"react-is": "^19.2.3", "react-is": "^19.2.8",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-loader-spinner": "^8.0.2", "react-loader-spinner": "^8.0.2",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.18.2",
"react-toastify": "^11.0.5", "react-toastify": "^11.1.0",
"recharts": "^3.6.0", "recharts": "^3.10.1",
"xlsx": "^0.18.5", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"xlsx-js-style": "^1.2.0" "xlsx-js-style": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.8", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^6.0.5",
"eslint": "^9.39.2", "eslint": "^9.39.5",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.5.3",
"vite": "^7.3.1" "vite": "8.0.16"
} }
} }

View File

@@ -39,6 +39,9 @@ function setSubPage(name) {
case 'club': case 'club':
clubPage(location); clubPage(location);
break; break;
case 'clubRank':
clubRankPage();
break;
case 'all': case 'all':
combsPage(); combsPage();
break; break;
@@ -54,6 +57,7 @@ function homePage() {
<li><a id="pouleLink" href="javascript:void(0);">${i18next.t('parCatégorie')}</a></li> <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="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="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> <li><a id="allLink" href="javascript:void(0);">${i18next.t('tousLesCombattants')}</a></li>
</ul> </ul>
` `
@@ -62,6 +66,7 @@ function homePage() {
document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule')); document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule'));
document.getElementById('combLink').addEventListener('click', () => setSubPage('comb')); document.getElementById('combLink').addEventListener('click', () => setSubPage('comb'));
document.getElementById('clubLink').addEventListener('click', () => setSubPage('club')); document.getElementById('clubLink').addEventListener('click', () => setSubPage('club'));
document.getElementById('clubClassement').addEventListener('click', () => setSubPage('clubRank'));
document.getElementById('allLink').addEventListener('click', () => setSubPage('all')); document.getElementById('allLink').addEventListener('click', () => setSubPage('all'));
} }
@@ -90,8 +95,7 @@ function stopLoading(loading) {
loading['root'].removeChild(loading['element']); loading['root'].removeChild(loading['element']);
} }
function scoreToString(score) { function scorePrint(s1) {
const scorePrint = (s1) => {
switch (s1) { switch (s1) {
case -997: case -997:
return i18next.t('disc.'); return i18next.t('disc.');
@@ -104,9 +108,14 @@ function scoreToString(score) {
default: default:
return String(s1); 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) { function dateToString(date) {
@@ -130,7 +139,7 @@ function dateToString(date) {
return date_.toLocaleDateString(); return date_.toLocaleDateString();
} }
function buildPouleMenu(isPoule, change_view) { function buildPouleMenu(isPoule, change_view, isClassement = false) {
const menuDiv = document.createElement('div'); const menuDiv = document.createElement('div');
menuDiv.id = 'menu'; menuDiv.id = 'menu';
menuDiv.style.borderBottom = '1px solid #9EA0A1'; menuDiv.style.borderBottom = '1px solid #9EA0A1';
@@ -189,7 +198,7 @@ function buildPouleMenu(isPoule, change_view) {
change_view(true); change_view(true);
}); });
ul.appendChild(li1); 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); change_view(false);
}); });
ul.appendChild(li2); ul.appendChild(li2);
@@ -263,7 +272,36 @@ function buildRankArray(rankArray) {
} }
function buildTree(treeData) { 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) { function poulePage(location) {
@@ -311,7 +349,7 @@ function poulePage(location) {
dataContainer.append(buildTree(poule['trees'])); dataContainer.append(buildTree(poule['trees']));
} else { } else {
const change_view = (isPoule) => { const change_view = (isPoule) => {
dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view)); dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view, poule['treeIsClassement']));
if (isPoule) { if (isPoule) {
for (const g in poule.matchs) { for (const g in poule.matchs) {
@@ -326,6 +364,22 @@ function poulePage(location) {
} }
} else { } else {
dataContainer.append(buildTree(poule['trees'])); 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; location[2] = isPoule ? 1 : 2;
@@ -591,6 +645,60 @@ function clubPage(location) {
rootDiv.append(content) 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) { function buildCombsView(combs) {
const pouleDiv = document.createElement('div'); const pouleDiv = document.createElement('div');
let arrayContent = ` let arrayContent = `
@@ -867,7 +975,7 @@ function drawGraph(root = []) {
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
for (let i = 0; i < scores.length; i++) { 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 div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3;
const text = ctx.measureText(score); const text = ctx.measureText(score);
let dx = (size * 2 - text.width) / 2; let dx = (size * 2 - text.width) / 2;
@@ -945,20 +1053,6 @@ function drawGraph(root = []) {
if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death);
} }
function win(scores) {
let sum = 0;
for (const score of scores) {
if (score.s1 === -1000 || score.s2 === -1000)
continue;
if (score.s1 > score.s2)
sum++;
else if (score.s1 < score.s2)
sum--;
}
return sum;
}
let px = max_x; let px = max_x;
let py; let py;
let max_y let max_y
@@ -971,7 +1065,7 @@ function drawGraph(root = []) {
for (const node of root) { for (const node of root) {
let win_name = ""; let win_name = "";
if (node.data.end) { if (node.data.end) {
if (win(node.data.scores) > 0) if (node.data.win > 0)
win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName; win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName;
else else
win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName; win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName;

View File

@@ -5,14 +5,30 @@
"actuel": "Current", "actuel": "Current",
"administration": "Administration", "administration": "Administration",
"adresseDuServeur": "Server address", "adresseDuServeur": "Server address",
"afficher": "Show",
"ajoutAutomatique": "Automatic addition",
"ajouter": "Add", "ajouter": "Add",
"ajouterDesCombattants": "Add fighters", "ajouterDesCombattants": "Add fighters",
"ajouterUn": "Add one",
"ajouterUneTeam": "Add team", "ajouterUneTeam": "Add team",
"attention": "Warning", "attention": "Warning",
"aucuneConfigurationObs": "No OBS configuration found, please import one", "aucuneConfigurationObs": "No OBS configuration found, please import one",
"avertissement": "Warning",
"bleu": "Blue", "bleu": "Blue",
"blue": "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é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.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s", "chrono.+10S": "+10 s",
"chrono.+1S": "+1 s", "chrono.+1S": "+1 s",
@@ -25,13 +41,16 @@
"chrono.entrezLeTempsEnS": "Enter time in seconds", "chrono.entrezLeTempsEnS": "Enter time in seconds",
"chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}", "chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}",
"chronomètre": "Stopwatch", "chronomètre": "Stopwatch",
"classement": "Ranking",
"club": "Club", "club": "Club",
"combattantsCorrespondentAuxSélectionnés": "fighter(s) match the selections above",
"compétition": "Competition", "compétition": "Competition",
"compétitionManager": "Competition manager", "compétitionManager": "Competition manager",
"config.obs.dossierDesResources": "Resources folder", "config.obs.dossierDesResources": "Resources folder",
"config.obs.motDePasseDuServeur": "Server password", "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.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://", "config.obs.ws": "ws://",
"configurationDuNomDeLaZone": "Zone name configuration",
"configurationObs": "OBS Configuration", "configurationObs": "OBS Configuration",
"confirm1": "This match already has results; are you sure you want to delete it?", "confirm1": "This match already has results; are you sure you want to delete it?",
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!", "confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
@@ -40,13 +59,23 @@
"confirm3.title": "Change category type", "confirm3.title": "Change category type",
"confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!", "confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!",
"confirm4.title": "Delete category", "confirm4.title": "Delete category",
"confirmer": "Confirm",
"conserverUniquementLesMatchsTerminés": "Keep only finished matches", "conserverUniquementLesMatchsTerminés": "Keep only finished matches",
"contre": "vs", "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éerLesMatchs": "Create matches",
"créerToutesLesCatégories": "Create all categories",
"date": "Date",
"demi-finalesEtFinales": "Semi-finals and finals", "demi-finalesEtFinales": "Semi-finals and finals",
"depuisUneCatégoriePrédéfinie": "From a predefined category",
"duréePause": "Pause duration", "duréePause": "Pause duration",
"duréeRound": "Round duration", "duréeRound": "Round duration",
"editionDeLaCatégorie": "Edit category", "editionDeLaCatégorie": "Edit category",
"editionDuMatch": "Match edition",
"enregister": "Save", "enregister": "Save",
"enregistrer": "Save", "enregistrer": "Save",
"epéeBouclier": "Sword and shield", "epéeBouclier": "Sword and shield",
@@ -55,31 +84,50 @@
"err3": "At least one type (pool or tournament) must be selected.", "err3": "At least one type (pool or tournament) must be selected.",
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ", "erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ", "erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
"etatDesTablesDeMarque": "State of marque tables",
"exporter": "Export", "exporter": "Export",
"fermer": "Close", "fermer": "Close",
"feuilleVierge": "Blank sheet",
"finalesUniquement": "Finals only", "finalesUniquement": "Finals only",
"genre": "Gender", "genre": "Gender",
"genre.f": "F", "genre.f": "F",
"genre.h": "M", "genre.h": "M",
"genre.na": "NA", "genre.na": "NA",
"imprimer": "Print",
"individuelle": "Individual",
"informationCatégorie": "Category information",
"inscrit": "Registered", "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", "manche": "Round",
"matchPourLesPerdantsDuTournoi": "Match for tournament losers:", "matchPourLesPerdantsDuTournoi": "Match for tournament losers:",
"matchTerminé": "Match over",
"matches": "Matches", "matches": "Matches",
"modeDeCréation": "Creation method",
"modifier": "Edit", "modifier": "Edit",
"msg1": "There are already matches in this pool; what do you want to do with them?", "msg1": "There are already matches in this pool; what do you want to do with them?",
"neRienConserver": "Keep nothing", "neRienConserver": "Keep nothing",
"no": "No.", "no": "No.",
"nom": "Name", "nom": "Name",
"nomDeLaZone": "Area name",
"nomDeLéquipe": "team name", "nomDeLéquipe": "team name",
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>", "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
"nombreDeCombattants": "Number of fighters",
"nouvelle...": "New...", "nouvelle...": "New...",
"obs.préfixDesSources": "Source prefix", "obs.préfixDesSources": "Source prefix",
"pays": "Country", "pays": "Country",
"personnaliser": "Personalize",
"podium": "Podium",
"podiumDesClubs": "Club podium",
"poids": "Weight", "poids": "Weight",
"poule": "Pool", "poule": "Pool",
"poulePour": "Pool for: ", "poulePour": "Pool for: ",
"préparation...": "Preparing...", "préparation...": "Preparing...",
"quoiImprimer?": "What print?",
"remplacer": "Replace",
"rouge": "Red", "rouge": "Red",
"réinitialiser": "Reset", "réinitialiser": "Reset",
"résultat": "Result", "résultat": "Result",
@@ -98,22 +146,34 @@
"select.sélectionnerDesCombatants": "Select fighters", "select.sélectionnerDesCombatants": "Select fighters",
"select.à": "to", "select.à": "to",
"serveur": "Server", "serveur": "Server",
"source": "Source",
"suivant": "Next", "suivant": "Next",
"supprimer": "Delete", "supprimer": "Delete",
"supprimerUn": "Delete one",
"sélectionneLesModesDaffichage": "Select display modes", "sélectionneLesModesDaffichage": "Select display modes",
"sélectionner": "Select", "sélectionner": "Select",
"taille": "Size",
"team": "Team", "team": "Team",
"terminé": "Finished", "terminé": "Finished",
"texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.", "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.error": "Error while creating the category",
"toast.createCategory.pending": "Creating category...", "toast.createCategory.pending": "Creating category...",
"toast.createCategory.success": "Category created!", "toast.createCategory.success": "Category created!",
"toast.deleteCategory.error": "Error while deleting the category", "toast.deleteCategory.error": "Error while deleting the category",
"toast.deleteCategory.pending": "Deleting category...", "toast.deleteCategory.pending": "Deleting category...",
"toast.deleteCategory.success": "Category deleted!", "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.error": "Error while creating matches.",
"toast.matchs.create.pending": "Creating matches in progress...", "toast.matchs.create.pending": "Creating matches in progress...",
"toast.matchs.create.success": "Matches created successfully.", "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.error": "Error while updating team",
"toast.team.update.pending": "Updating team...", "toast.team.update.pending": "Updating team...",
"toast.team.update.success": "Team updated!", "toast.team.update.success": "Team updated!",
@@ -133,10 +193,12 @@
"tournois": "Tournaments", "tournois": "Tournaments",
"tousLesMatchs": "All matches", "tousLesMatchs": "All matches",
"toutConserver": "Keep all", "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.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script", "ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", "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_aff": "Open public display",
"ttm.table.pub_score": "Show scores on public display", "ttm.table.pub_score": "Show scores on public display",
"type": "Type", "type": "Type",
@@ -144,6 +206,7 @@
"téléchargementEnCours": "Downloading...", "téléchargementEnCours": "Downloading...",
"téléchargementTerminé!": "Download completed!", "téléchargementTerminé!": "Download completed!",
"uneCatégorie": "a category", "uneCatégorie": "a category",
"uneCatégorieNePeutContenirPlusDe10Combattants": "A category cannot contain more than 10 fighters, please create weight categories.",
"valider": "Validate", "valider": "Validate",
"zone": "Zone", "zone": "Zone",
"zoneDeCombat": "Combat zone" "zoneDeCombat": "Combat zone"

View File

@@ -124,6 +124,7 @@
"catégorie": "Category", "catégorie": "Category",
"catégorieàAjouter": "Category to add", "catégorieàAjouter": "Category to add",
"certificatMédical": "Medical certificate", "certificatMédical": "Medical certificate",
"champAttendu": "Expected field",
"chargement...": "Loading...", "chargement...": "Loading...",
"chargerLexcel": "Load Excel", "chargerLexcel": "Load Excel",
"chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.", "chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.",
@@ -154,6 +155,7 @@
"club_one": "Club", "club_one": "Club",
"club_other": "Clubs", "club_other": "Clubs",
"club_zero": "No club", "club_zero": "No club",
"colonneDansLeFichier": "Column in the file",
"combattant": "fighter", "combattant": "fighter",
"comp.aff.blason": "Display the club's coat of arms on screens", "comp.aff.blason": "Display the club's coat of arms on screens",
"comp.aff.flag": "Display the fighter's country on screens", "comp.aff.flag": "Display the fighter's country on screens",
@@ -231,6 +233,8 @@
"comp.toast.register.add.error": "Fighter not found", "comp.toast.register.add.error": "Fighter not found",
"comp.toast.register.add.pending": "Search in progress", "comp.toast.register.add.pending": "Search in progress",
"comp.toast.register.add.success": "Fighter found and added/updated", "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.error": "Error",
"comp.toast.register.ban.pending": "Unregistration in progress", "comp.toast.register.ban.pending": "Unregistration in progress",
"comp.toast.register.ban.success": "Fighter unregistered and banned", "comp.toast.register.ban.success": "Fighter unregistered and banned",
@@ -243,6 +247,9 @@
"comp.toast.register.self.del.error": "Error during unregistration", "comp.toast.register.self.del.error": "Error during unregistration",
"comp.toast.register.self.del.pending": "Unregistration in progress", "comp.toast.register.self.del.pending": "Unregistration in progress",
"comp.toast.register.self.del.success": "Unregistration completed", "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.error": "Failed to save competition",
"comp.toast.save.pending": "Saving competition in progress", "comp.toast.save.pending": "Saving competition in progress",
"comp.toast.save.success": "Competition saved successfully 🎉", "comp.toast.save.success": "Competition saved successfully 🎉",
@@ -300,10 +307,71 @@
"erreurDePaiement": "Payment error😕", "erreurDePaiement": "Payment error😕",
"erreurDePaiement.detail": "Error message:", "erreurDePaiement.detail": "Error message:",
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.", "erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
"erreurPourLinscription": "Registration error",
"espaceAdministration": "Administration space", "espaceAdministration": "Administration space",
"f": "F", "f": "F",
"faitPar": "Done by", "faitPar": "Done by",
"femme": "Female", "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", "filtre": "Filter",
"gantMainBouclier": "Shield hand glove", "gantMainBouclier": "Shield hand glove",
"gantMainsArmées": "Armed hand(s) glove(s)", "gantMainsArmées": "Armed hand(s) glove(s)",
@@ -322,6 +390,9 @@
}, },
"homme": "Male", "homme": "Male",
"horairesD'entraînements": "Training schedules", "horairesD'entraînements": "Training schedules",
"importationDuFichier": "Importing the file",
"importerDesCombattants": "Import fighters",
"importerDesInvités": "Import guests",
"information": "Information", "information": "Information",
"invité": "guest", "invité": "guest",
"keepEmpty": "Leave blank to make no changes.", "keepEmpty": "Leave blank to make no changes.",
@@ -330,6 +401,8 @@
"licenceNo": "License no. {{no}}", "licenceNo": "License no. {{no}}",
"lieu": "Place", "lieu": "Place",
"lieuxDentraînements": "Training locations", "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...", "loading": "Loading...",
"me": { "me": {
"result": { "result": {
@@ -458,6 +531,8 @@
"nouveauClub": "New club", "nouveauClub": "New club",
"nouveauMembre": "New member", "nouveauMembre": "New member",
"nouvelEmail": "New email", "nouvelEmail": "New email",
"numéroDeLaLigneDentête": "Header line number",
"numéroDeLigne": "Line number",
"ou": "or", "ou": "or",
"oui": "Yes", "oui": "Yes",
"outdated_session": { "outdated_session": {
@@ -493,6 +568,7 @@
"peutSinscrire": "Can register?", "peutSinscrire": "Can register?",
"photos": "Photos", "photos": "Photos",
"plastron": "Breastplate", "plastron": "Breastplate",
"poids": "Weight",
"poidsDemandéPour": "Weight required for", "poidsDemandéPour": "Weight required for",
"prenom": "First name", "prenom": "First name",
"protectionDeBras": "Arm protection", "protectionDeBras": "Arm protection",
@@ -543,9 +619,9 @@
"supprimerLeCompte": "Delete account", "supprimerLeCompte": "Delete account",
"supprimerLeCompte.msg": "Are you sure you want to delete this account?", "supprimerLeCompte.msg": "Are you sure you want to delete this account?",
"sword.none": "$t(sans) / $t(nonDéfinie)", "sword.none": "$t(sans) / $t(nonDéfinie)",
"sword.oneHand": "One hand", "sword.oneHand": "One hand sword",
"sword.saber": "Saber", "sword.saber": "Saber",
"sword.twoHand": "Two hands", "sword.twoHand": "Two hands sword",
"sélectionEnéquipeDeFrance": "Selection in the French team", "sélectionEnéquipeDeFrance": "Selection in the French team",
"sélectionner...": "Select...", "sélectionner...": "Select...",
"toast.edit.error": "Failed to save changes", "toast.edit.error": "Failed to save changes",
@@ -577,6 +653,9 @@
"validerLicence_other": "Validate the {{count}} selected licenses", "validerLicence_other": "Validate the {{count}} selected licenses",
"validerLicence_zero": "$t(validerLicence_other)", "validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validated", "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", "voir/modifierLesParticipants": "View/Edit participants",
"voirLesStatues": "View statues", "voirLesStatues": "View statues",
"vousNêtesPasEncoreInscrit": "You are not yet registered or your registration has not yet been entered on the intranet", "vousNêtesPasEncoreInscrit": "You are not yet registered or your registration has not yet been entered on the intranet",

View File

@@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Select a club--", "--sélectionnerUnClub--": "--Select a club--",
"--sélectionnerUnCombattant--": "--Select a fighter--", "--sélectionnerUnCombattant--": "--Select a fighter--",
"--sélectionnerUneCatégorie--": "--Select a category--", "--sélectionnerUneCatégorie--": "--Select a category--",
"1er": "Gold medal",
"2eme": "Silver medal",
"3eme": "Bronze medal",
"abs.": "abs.", "abs.": "abs.",
"adversaire": "Opponent", "adversaire": "Opponent",
"aujourdhuià": "Today at {{time}}", "aujourdhuià": "Today at {{time}}",
@@ -10,6 +13,10 @@
"bleu": "Blue", "bleu": "Blue",
"catégorie": "Category", "catégorie": "Category",
"chargement": "Loading", "chargement": "Loading",
"classement": "Ranking",
"classementClub": "Club ranking",
"classementDesClub": "Clubs ranking",
"classementFinal": "Final ranking",
"club": "Club", "club": "Club",
"combattant": "Fighter", "combattant": "Fighter",
"combattants": "Fighters", "combattants": "Fighters",

View File

@@ -5,14 +5,30 @@
"actuel": "Actuel", "actuel": "Actuel",
"administration": "Administration", "administration": "Administration",
"adresseDuServeur": "Adresse du serveur", "adresseDuServeur": "Adresse du serveur",
"afficher": "Afficher",
"ajoutAutomatique": "Ajout automatique",
"ajouter": "Ajouter", "ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants", "ajouterDesCombattants": "Ajouter des combattants",
"ajouterUn": "Ajouter un ",
"ajouterUneTeam": "Ajouter une équipe", "ajouterUneTeam": "Ajouter une équipe",
"attention": "Attention", "attention": "Attention",
"aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une", "aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une",
"avertissement": "Avertissement",
"bleu": "Bleu", "bleu": "Bleu",
"blue": "Blue", "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é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.+/-...S": "+/- ... s",
"chrono.+10S": "+10 s", "chrono.+10S": "+10 s",
"chrono.+1S": "+1 s", "chrono.+1S": "+1 s",
@@ -25,13 +41,16 @@
"chrono.entrezLeTempsEnS": "Entrez le temps en s", "chrono.entrezLeTempsEnS": "Entrez le temps en s",
"chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}", "chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}",
"chronomètre": "Chronomètre", "chronomètre": "Chronomètre",
"classement": "Classement",
"club": "Club", "club": "Club",
"combattantsCorrespondentAuxSélectionnés": "combattant(s) correspondent aux sélections ci-dessus.",
"compétition": "Compétition", "compétition": "Compétition",
"compétitionManager": "Compétition manager", "compétitionManager": "Compétition manager",
"config.obs.dossierDesResources": "Dossier des resources", "config.obs.dossierDesResources": "Dossier des resources",
"config.obs.motDePasseDuServeur": "Mot de passe du serveur", "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.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://", "config.obs.ws": "ws://",
"configurationDuNomDeLaZone": "Configuration du nom de la zone",
"configurationObs": "Configuration OBS", "configurationObs": "Configuration OBS",
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?", "confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!", "confirm2.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)!",
@@ -40,13 +59,23 @@
"confirm3.title": "Changement de type de catégorie", "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.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !",
"confirm4.title": "Suppression de la catégorie", "confirm4.title": "Suppression de la catégorie",
"confirmer": "Confirmer",
"conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés", "conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés",
"contre": "contre", "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éerLesMatchs": "Créer les matchs",
"créerToutesLesCatégories": "Créer toutes les catégories",
"date": "Date",
"demi-finalesEtFinales": "Demi-finales et finales", "demi-finalesEtFinales": "Demi-finales et finales",
"depuisUneCatégoriePrédéfinie": "Depuis une catégorie prédéfinie",
"duréePause": "Durée pause", "duréePause": "Durée pause",
"duréeRound": "Durée round", "duréeRound": "Durée round",
"editionDeLaCatégorie": "Edition de la catégorie", "editionDeLaCatégorie": "Edition de la catégorie",
"editionDuMatch": "Edition du match",
"enregister": "Enregister", "enregister": "Enregister",
"enregistrer": "Enregistrer", "enregistrer": "Enregistrer",
"epéeBouclier": "Epée bouclier", "epéeBouclier": "Epée bouclier",
@@ -55,31 +84,50 @@
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.", "err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ", "erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ", "erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
"etatDesTablesDeMarque": "Etat des tables de marque",
"exporter": "Exporter", "exporter": "Exporter",
"fermer": "Fermer", "fermer": "Fermer",
"feuilleVierge": "Feuille vierge",
"finalesUniquement": "Finales uniquement", "finalesUniquement": "Finales uniquement",
"genre": "Genre", "genre": "Genre",
"genre.f": "F", "genre.f": "F",
"genre.h": "H", "genre.h": "H",
"genre.na": "NA", "genre.na": "NA",
"imprimer": "Imprimer",
"individuelle": "Individuelle",
"informationCatégorie": "Information catégorie",
"inscrit": "Inscrit", "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", "manche": "Manche",
"matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:", "matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:",
"matchTerminé": "Match terminé",
"matches": "Matches", "matches": "Matches",
"modeDeCréation": "Mode de création",
"modifier": "Modifier", "modifier": "Modifier",
"msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?", "msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?",
"neRienConserver": "Ne rien conserver", "neRienConserver": "Ne rien conserver",
"no": "N°", "no": "N°",
"nom": "Nom", "nom": "Nom",
"nomDeLaZone": "Nom de la zone",
"nomDeLéquipe": "Nom de l'équipe", "nomDeLéquipe": "Nom de l'équipe",
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>", "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
"nombreDeCombattants": "Nombre de combattants",
"nouvelle...": "Nouvelle...", "nouvelle...": "Nouvelle...",
"obs.préfixDesSources": "Préfix des sources", "obs.préfixDesSources": "Préfix des sources",
"pays": "Pays", "pays": "Pays",
"personnaliser": "Personnaliser",
"podium": "Podium",
"podiumDesClubs": "Podium des clubs",
"poids": "Poids", "poids": "Poids",
"poule": "Poule", "poule": "Poule",
"poulePour": "Poule pour: ", "poulePour": "Poule pour: ",
"préparation...": "Préparation...", "préparation...": "Préparation...",
"quoiImprimer?": "Quoi imprimer ?",
"remplacer": "Remplacer",
"rouge": "Rouge", "rouge": "Rouge",
"réinitialiser": "Réinitialiser", "réinitialiser": "Réinitialiser",
"résultat": "Résultat", "résultat": "Résultat",
@@ -98,22 +146,34 @@
"select.sélectionnerDesCombatants": "Sélectionner des combatants", "select.sélectionnerDesCombatants": "Sélectionner des combatants",
"select.à": "à", "select.à": "à",
"serveur": "Serveur", "serveur": "Serveur",
"source": "Source",
"suivant": "Suivant", "suivant": "Suivant",
"supprimer": "Supprimer", "supprimer": "Supprimer",
"supprimerUn": "Supprimer un",
"sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage", "sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage",
"sélectionner": "Sélectionner", "sélectionner": "Sélectionner",
"taille": "Taille",
"team": "Équipe", "team": "Équipe",
"terminé": "Terminé", "terminé": "Terminé",
"texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.", "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.error": "Erreur lors de la création de la catégorie",
"toast.createCategory.pending": "Création de la catégorie...", "toast.createCategory.pending": "Création de la catégorie...",
"toast.createCategory.success": "Catégorie créée !", "toast.createCategory.success": "Catégorie créée !",
"toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie", "toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie",
"toast.deleteCategory.pending": "Suppression de la catégorie...", "toast.deleteCategory.pending": "Suppression de la catégorie...",
"toast.deleteCategory.success": "Catégorie supprimée !", "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.error": "Erreur lors de la création des matchs.",
"toast.matchs.create.pending": "Création des matchs en cours...", "toast.matchs.create.pending": "Création des matchs en cours...",
"toast.matchs.create.success": "Matchs créés avec succès.", "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.error": "Erreur lors de la mise à jour de l'équipe",
"toast.team.update.pending": "Mise à jour de l'équipe...", "toast.team.update.pending": "Mise à jour de l'équipe...",
"toast.team.update.success": "Équipe mise à jour !", "toast.team.update.success": "Équipe mise à jour !",
@@ -133,10 +193,12 @@
"tournois": "Tournois", "tournois": "Tournois",
"tousLesMatchs": "Tous les matchs", "tousLesMatchs": "Tous les matchs",
"toutConserver": "Tout conserver", "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.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.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", "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_aff": "Ouvrir l'affichage public",
"ttm.table.pub_score": "Afficher les scores sur l'affichage public", "ttm.table.pub_score": "Afficher les scores sur l'affichage public",
"type": "Type", "type": "Type",
@@ -144,6 +206,7 @@
"téléchargementEnCours": "Téléchargement en cours...", "téléchargementEnCours": "Téléchargement en cours...",
"téléchargementTerminé!": "Téléchargement terminé !", "téléchargementTerminé!": "Téléchargement terminé !",
"uneCatégorie": "une catégorie", "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", "valider": "Valider",
"zone": "Zone", "zone": "Zone",
"zoneDeCombat": "Zone de combat" "zoneDeCombat": "Zone de combat"

View File

@@ -124,6 +124,7 @@
"catégorie": "Catégorie", "catégorie": "Catégorie",
"catégorieàAjouter": "Catégorie à ajouter", "catégorieàAjouter": "Catégorie à ajouter",
"certificatMédical": "Certificat médical", "certificatMédical": "Certificat médical",
"champAttendu": "Champ attendu",
"chargement...": "Chargement...", "chargement...": "Chargement...",
"chargerLexcel": "Charger l'Excel", "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.", "chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.",
@@ -154,6 +155,7 @@
"club_one": "Club", "club_one": "Club",
"club_other": "Clubs", "club_other": "Clubs",
"club_zero": "Sans club", "club_zero": "Sans club",
"colonneDansLeFichier": "Colonne dans le fichier",
"combattant": "combattant", "combattant": "combattant",
"comp.aff.blason": "Afficher le blason du club sur les écrans", "comp.aff.blason": "Afficher le blason du club sur les écrans",
"comp.aff.flag": "Afficher le pays du combattant sur les écrans", "comp.aff.flag": "Afficher le pays du combattant sur les écrans",
@@ -173,7 +175,7 @@
"comp.error1": "La date de fin doit être postérieure à la date de début.", "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.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.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.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.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.",
"comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.", "comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.",
@@ -231,6 +233,8 @@
"comp.toast.register.add.error": "Combattant non trouvé", "comp.toast.register.add.error": "Combattant non trouvé",
"comp.toast.register.add.pending": "Recherche en cours", "comp.toast.register.add.pending": "Recherche en cours",
"comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour", "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.error": "Erreur",
"comp.toast.register.ban.pending": "Désinscription en cours", "comp.toast.register.ban.pending": "Désinscription en cours",
"comp.toast.register.ban.success": "Combattant désinscrit et bannie", "comp.toast.register.ban.success": "Combattant désinscrit et bannie",
@@ -243,6 +247,9 @@
"comp.toast.register.self.del.error": "Erreur lors de la désinscription", "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.pending": "Désinscription en cours",
"comp.toast.register.self.del.success": "Désinscription réalisée", "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.error": "Échec de l'enregistrement de la compétition",
"comp.toast.save.pending": "Enregistrement de la compétition en cours", "comp.toast.save.pending": "Enregistrement de la compétition en cours",
"comp.toast.save.success": "Compétition enregistrée avec succès 🎉", "comp.toast.save.success": "Compétition enregistrée avec succès 🎉",
@@ -300,10 +307,75 @@
"erreurDePaiement": "Erreur de paiement😕", "erreurDePaiement": "Erreur de paiement😕",
"erreurDePaiement.detail": "Message d'erreur :", "erreurDePaiement.detail": "Message d'erreur :",
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.", "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", "espaceAdministration": "Espace administration",
"f": "F", "f": "F",
"faitPar": "Fait par", "faitPar": "Fait par",
"femme": "Femme", "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", "filtre": "Filtre",
"gantMainBouclier": "Gant main de bouclier", "gantMainBouclier": "Gant main de bouclier",
"gantMainsArmées": "Gant main(s) armée(s)", "gantMainsArmées": "Gant main(s) armée(s)",
@@ -322,6 +394,9 @@
}, },
"homme": "Homme", "homme": "Homme",
"horairesD'entraînements": "Horaires d'entraînements", "horairesD'entraînements": "Horaires d'entraînements",
"importationDuFichier": "Importation du fichier",
"importerDesCombattants": "Importer des combattants",
"importerDesInvités": "Importer des invités",
"information": "Information", "information": "Information",
"invité": "invité", "invité": "invité",
"keepEmpty": "Laissez vide pour ne rien changer.", "keepEmpty": "Laissez vide pour ne rien changer.",
@@ -330,6 +405,8 @@
"licenceNo": "Licence n°{{no}}", "licenceNo": "Licence n°{{no}}",
"lieu": "Lieu", "lieu": "Lieu",
"lieuxDentraînements": "Lieux d'entraînements", "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...", "loading": "Chargement...",
"me": { "me": {
"result": { "result": {
@@ -458,6 +535,8 @@
"nouveauClub": "Nouveau club", "nouveauClub": "Nouveau club",
"nouveauMembre": "Nouveau membre", "nouveauMembre": "Nouveau membre",
"nouvelEmail": "Nouvel email", "nouvelEmail": "Nouvel email",
"numéroDeLaLigneDentête": "Numéro de la ligne d'en-tête",
"numéroDeLigne": "Numéro de ligne",
"ou": "Ou", "ou": "Ou",
"oui": "Oui", "oui": "Oui",
"outdated_session": { "outdated_session": {
@@ -493,6 +572,7 @@
"peutSinscrire": "Peut s'inscrire?", "peutSinscrire": "Peut s'inscrire?",
"photos": "Photos", "photos": "Photos",
"plastron": "Plastron", "plastron": "Plastron",
"poids": "Poids",
"poidsDemandéPour": "Poids demandé pour", "poidsDemandéPour": "Poids demandé pour",
"prenom": "Prénom", "prenom": "Prénom",
"protectionDeBras": "Protection de bras", "protectionDeBras": "Protection de bras",
@@ -543,9 +623,9 @@
"supprimerLeCompte": "Supprimer le compte", "supprimerLeCompte": "Supprimer le compte",
"supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?", "supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?",
"sword.none": "$t(sans) / $t(nonDéfinie)", "sword.none": "$t(sans) / $t(nonDéfinie)",
"sword.oneHand": "Une main", "sword.oneHand": "Épée une main",
"sword.saber": "Sabre", "sword.saber": "Sabre",
"sword.twoHand": "Deux mains", "sword.twoHand": "Épée deux mains",
"sélectionEnéquipeDeFrance": "Sélection en équipe de France", "sélectionEnéquipeDeFrance": "Sélection en équipe de France",
"sélectionner...": "Sélectionner...", "sélectionner...": "Sélectionner...",
"toast.edit.error": "Échec de l'enregistrement des modifications", "toast.edit.error": "Échec de l'enregistrement des modifications",
@@ -577,6 +657,9 @@
"validerLicence_other": "Valider les {{count}} licences sélectionnées", "validerLicence_other": "Valider les {{count}} licences sélectionnées",
"validerLicence_zero": "$t(validerLicence_other)", "validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validée", "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", "voir/modifierLesParticipants": "Voir/Modifier les participants",
"voirLesStatues": "Voir les statues", "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", "vousNêtesPasEncoreInscrit": "Vous n'êtes pas encore inscrit ou votre inscription n'a pas encore été rentrée sur l'intranet",

View File

@@ -2,6 +2,9 @@
"--sélectionnerUnClub--": "--Sélectionner un club--", "--sélectionnerUnClub--": "--Sélectionner un club--",
"--sélectionnerUnCombattant--": "--Sélectionner un combattant--", "--sélectionnerUnCombattant--": "--Sélectionner un combattant--",
"--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--", "--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.", "abs.": "abs.",
"adversaire": "Adversaire", "adversaire": "Adversaire",
"aujourdhuià": "Aujourd'hui à {{time}}", "aujourdhuià": "Aujourd'hui à {{time}}",
@@ -10,6 +13,10 @@
"bleu": "Bleu", "bleu": "Bleu",
"catégorie": "Catégorie", "catégorie": "Catégorie",
"chargement": "Chargement", "chargement": "Chargement",
"classement": "Classement",
"classementClub": "Classement club",
"classementDesClub": "Classement des clubs",
"classementFinal": "Classement final",
"club": "Club", "club": "Club",
"combattant": "Combattant", "combattant": "Combattant",
"combattants": "Combattants", "combattants": "Combattants",

View 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);

View 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>
}

View File

@@ -164,3 +164,27 @@ const ProtectionSelector = ({
} }
export default ProtectionSelector; 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;
}

View 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;

View 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>
</>
}

View 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>;
}

View File

@@ -6,8 +6,18 @@ import {initReactI18next} from 'react-i18next';
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
const options = { const options = {
order: [ 'querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'], order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
caches: [], 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 i18n
@@ -22,8 +32,12 @@ i18n
// init i18next // init i18next
// for all options read: https://www.i18next.com/overview/configuration-options // for all options read: https://www.i18next.com/overview/configuration-options
.init({ .init({
fallbackLng: {
'fr-FR': ['fr'],
default: ['en']
},
supportedLngs: ['fr', 'en'], supportedLngs: ['fr', 'en'],
fallbackLng: 'fr', nonExplicitSupportedLngs: true,
debug: vite_url.startsWith('http://localhost'), debug: vite_url.startsWith('http://localhost'),
interpolation: { interpolation: {
escapeValue: false, // not needed for react as it escapes by default escapeValue: false, // not needed for react as it escapes by default

View 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;
}
}

View File

@@ -29,7 +29,7 @@ function reducer(state, action) {
//console.debug("Updating comb", comb); //console.debug("Updating comb", comb);
return { return {
...state, ...state,
[comb.id]: comb [comb.id]: {...state[comb.id], ...comb}
} }
} }
return state return state
@@ -49,7 +49,10 @@ function reducer(state, action) {
if (combs.some(e => state[e.id] === undefined || !compareCombs(e, state[e.id]))) { if (combs.some(e => state[e.id] === undefined || !compareCombs(e, state[e.id]))) {
const newCombs = {}; const newCombs = {};
for (const o of combs) { for (const o of combs) {
newCombs[o.id] = o; newCombs[o.id] = {
...state[o.id],
...o
};
} }
//console.debug("Updating combs", newCombs); //console.debug("Updating combs", newCombs);
@@ -78,7 +81,7 @@ function WSListener({dispatch}) {
dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}}) dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}})
return () => { return () => {
dispatchWS({type: 'removeListener', payload: {callback: sendRegister, code: 'sendRegister'}}) dispatchWS({type: 'removeListener', payload: sendRegister})
} }
}, []); }, []);

View File

@@ -74,7 +74,7 @@ export function OBSProvider({children}) {
} }
function getElementName(element) { function getElementName(element) {
return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}` return `sub${sessionStorage.getItem("liceName") || 1}.${element}`
} }
export function useOBS() { export function useOBS() {

View File

@@ -47,6 +47,7 @@ export function WSProvider({url, onmessage, children}) {
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false}) const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
const [state, dispatch] = useReducer(reducer, {listener: []}) const [state, dispatch] = useReducer(reducer, {listener: []})
const ws = useRef(null) const ws = useRef(null)
const tableState = useRef({})
const listenersRef = useRef([]) const listenersRef = useRef([])
const callbackRef = useRef({}) const callbackRef = useRef({})
const isReadyRef = useRef(isReady) 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}> return <WebsocketContext.Provider value={ret}>
{children} {children}
</WebsocketContext.Provider> </WebsocketContext.Provider>
} }
export function useWS() { export function useWS() {
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext) const {isReady, dispatch, send, wait_length, welcomeData, tableState} = useContext(WebsocketContext)
return { return {
dispatch, dispatch,
isReady, isReady,
@@ -247,6 +248,10 @@ export function useWS() {
send(uuidv4(), "error", "ERROR", data) send(uuidv4(), "error", "ERROR", data)
}, },
send, send,
setState: (newState) => {
tableState.current = {...tableState.current, ...newState}
},
tableState
} }
} }

View File

@@ -23,8 +23,6 @@ export function MemberList({source}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [licenceData, setLicenceData] = useState([]);
const [showLicenceState, setShowLicenceState] = useState((sessionStorage.getItem("showLicenceState") || "false") === "true");
const setFilter = (filter) => { const setFilter = (filter) => {
navigate("#" + encodeURI(JSON.stringify(filter))) navigate("#" + encodeURI(JSON.stringify(filter)))
@@ -37,7 +35,7 @@ export function MemberList({source}) {
payment: 2, payment: 2,
order: "", order: "",
categorie: "", categorie: "",
archived: false, archived: sessionStorage.getItem("showMembreArchived") || true,
...JSON.parse(decodeURI(hash.substring(1)) || "{}"), ...JSON.parse(decodeURI(hash.substring(1)) || "{}"),
} }
@@ -72,23 +70,11 @@ export function MemberList({source}) {
club: e.club, club: e.club,
categorie: e.categorie, categorie: e.categorie,
licence_number: e.licence, 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); setMemberData(data2);
}, [data, licenceData]); }, [data]);
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]);
const search = (search) => { const search = (search) => {
if (search === filter.search) if (search === filter.search)
@@ -102,8 +88,8 @@ export function MemberList({source}) {
<div className="col-lg-9"> <div className="col-lg-9">
<SearchBar search={search} defaultValue={filter.search}/> <SearchBar search={search} defaultValue={filter.search}/>
{data {data
? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} showLicenceState={showLicenceState} ? <MakeCentralPanel data={data} visibleMember={memberData} navigate={navigate} page={filter.page}
page={filter.page} setPage={e => setFilter({...filter, page: e})} source={source}/> setPage={e => setFilter({...filter, page: e})} source={source}/>
: error : error
? <AxiosError error={error}/> ? <AxiosError error={error}/>
: <Def/> : <Def/>
@@ -130,10 +116,11 @@ export function MemberList({source}) {
<div className="card mb-4"> <div className="card mb-4">
<div className="card-header">{t('filtre')}</div> <div className="card-header">{t('filtre')}</div>
<div className="card-body"> <div className="card-body">
<FiltreBar showLicenceState={showLicenceState} <FiltreBar showArchived={filter.archived}
setShowLicenceState={setShowLicenceState} setShowArchived={e => {
showArchived={filter.archived} setFilter({...filter, archived: e})
setShowArchived={e => setFilter({...filter, archived: e})} sessionStorage.setItem("showMembreArchived", e);
}}
clubFilter={filter.club} clubFilter={filter.club}
setClubFilter={e => setFilter({...filter, club: e})} setClubFilter={e => setFilter({...filter, club: e})}
source={source} source={source}
@@ -163,6 +150,7 @@ export function MemberList({source}) {
function FileOutput() { function FileOutput() {
const {t} = useTranslation(); const {t} = useTranslation();
function formatColumnDate(worksheet, col) { function formatColumnDate(worksheet, col) {
const range = XLSX.utils.decode_range(worksheet['!ref']) const range = XLSX.utils.decode_range(worksheet['!ref'])
// note: range.s.r + 1 skips the header row // note: range.s.r + 1 skips the header row
@@ -181,6 +169,7 @@ function FileOutput() {
toast.promise( toast.promise(
apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export")) apiAxios.get(`/member/club/export`), getToastMessage("membre.toast.licences.export"))
.then(data => { .then(data => {
console.log(data)
const dataOut = [] const dataOut = []
for (const e of data.data) { for (const e of data.data) {
const tmp = { 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 {t} = useTranslation();
const pages = [] const pages = []
@@ -374,7 +363,7 @@ function MakeCentralPanel({data, visibleMember, navigate, showLicenceState, page
})}</small> })}</small>
<div className="list-group"> <div className="list-group">
{visibleMember.map(member => ( {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> </div>
<div className="mb-4"> <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 = <> const rowContent = <>
<div className="row" style={{padding: "0.6em 0"}}> <div className="row" style={{padding: "0.6em 0"}}>
<span className="col-auto">{(member.licence_number ? String(member.licence_number).padStart(5, '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}/> : <>&nbsp;&nbsp;</>}</span> {(member.licence != null && member.licence.pay) ? <FontAwesomeIcon icon={faEuroSign}/> : <>&nbsp;&nbsp;</>}</span>
<div className="ms-2 col-auto"> <div className="ms-2 col-auto">
<div className="fw-bold">{member.fname} {member.lname}</div> <div className="fw-bold">{member.fname} {member.lname}</div>
</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-" 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"))} + (member.licence.validate ? "success" : (member.licence.certificate.length > 1 ? "warning" : "danger"))}
style={{padding: "0 1em"}} style={{padding: "0 1em"}}
@@ -509,8 +498,6 @@ function OrderBar({onOrderChange, defaultValues = "", source}) {
} }
function FiltreBar({ function FiltreBar({
showLicenceState,
setShowLicenceState,
showArchived, showArchived,
setShowArchived, setShowArchived,
clubFilter, clubFilter,
@@ -526,9 +513,6 @@ function FiltreBar({
const {t} = useTranslation(); const {t} = useTranslation();
return <div> return <div>
<div className="mb-3">
<Checkbox value={showLicenceState} onChange={setShowLicenceState} label={t('membre.filtre.licence')}/>
</div>
<div className="mb-3"> <div className="mb-3">
<Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/> <Checkbox value={showArchived} onChange={setShowArchived} name="checkbox2" label={t('membre.filtre.inactif')}/>
</div> </div>

View File

@@ -13,7 +13,6 @@ import {faCircleInfo, faEuroSign} from "@fortawesome/free-solid-svg-icons";
import "./PayAndValidateList.css"; import "./PayAndValidateList.css";
import * as Tools from "../utils/Tools.js"; import * as Tools from "../utils/Tools.js";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {counter} from "@fortawesome/fontawesome-svg-core";
export function PayAndValidateList({source}) { export function PayAndValidateList({source}) {
const {t} = useTranslation(); const {t} = useTranslation();
@@ -31,8 +30,7 @@ export function PayAndValidateList({source}) {
const [lastSearch, setLastSearch] = useState(""); const [lastSearch, setLastSearch] = useState("");
const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2); const [paymentFilter, setPaymentFilter] = useState((source === "club") ? 0 : 2);
const storedMembers = sessionStorage.getItem("selectedMembers"); const [selectedMembers, setSelectedMembers] = useState([]);
const [selectedMembers, setSelectedMembers] = useState(storedMembers ? JSON.parse(storedMembers) : []);
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const { const {
@@ -41,10 +39,6 @@ export function PayAndValidateList({source}) {
refresh refresh
} = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1) } = useFetch(`/member/find/${source}?page=${page}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`, setLoading, 1)
useEffect(() => {
sessionStorage.setItem("selectedMembers", JSON.stringify(selectedMembers));
}, [selectedMembers]);
useEffect(() => { useEffect(() => {
refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`); refresh(`/member/find/${source}?page=${page}&search=${lastSearch}&club=${clubFilter}&licenceRequest=${stateFilter}&payment=${paymentFilter}&categorie=${catFilter}`);
}, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]); }, [hash, clubFilter, stateFilter, lastSearch, paymentFilter, catFilter]);

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons"; import {faEuroSign, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; 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 {toast} from "react-toastify";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
@@ -51,13 +51,34 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
}, [data]); }, [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"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">{t('licence')}</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <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> </button>
</div> </div>
</div> </div>
@@ -141,13 +162,6 @@ function ModalContent({licence, dispatch}) {
if (licence.id !== -1) { if (licence.id !== -1) {
setNew(false) setNew(false)
setSaison(licence.saison) setSaison(licence.saison)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
setValidate(licence.validate) setValidate(licence.validate)
setPay(licence.pay); setPay(licence.pay);
} else { } else {
@@ -158,6 +172,13 @@ function ModalContent({licence, dispatch}) {
setValidate(false) setValidate(false)
setPay(false); setPay(false);
} }
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
} else {
setCertificateBy("")
setCertificateDate("")
}
}, [licence]); }, [licence]);
return <form onSubmit={e => sendLicence(e, dispatch)}> return <form onSubmit={e => sendLicence(e, dispatch)}>

View File

@@ -4,7 +4,7 @@ import {useEffect, useReducer, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons"; import {faInfo, faPen} from "@fortawesome/free-solid-svg-icons";
import {AxiosError} from "../../../components/AxiosError.jsx"; 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 {toast} from "react-toastify";
import {ColoredText} from "../../../components/ColoredCircle.jsx"; import {ColoredText} from "../../../components/ColoredCircle.jsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
@@ -32,7 +32,7 @@ function licenceReducer(licences, action) {
} }
export function LicenceCard({userData}) { 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 {t} = useTranslation();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
@@ -48,13 +48,33 @@ export function LicenceCard({userData}) {
dispatch({type: 'SORT'}) dispatch({type: 'SORT'})
}, [data]); }, [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"> return <div className="card mb-4 mb-md-0">
<div className="card-header container-fluid"> <div className="card-header container-fluid">
<div className="row"> <div className="row">
<div className="col">{t('licence')}</div> <div className="col">{t('licence')}</div>
<div className="col" style={{textAlign: 'right'}}> <div className="col" style={{textAlign: 'right'}}>
<button className="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#LicenceModal" <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')} disabled={licences.some(licence => licence.saison === getSaison())}>{t('demander')}
</button> </button>
</div> </div>
@@ -127,15 +147,14 @@ function ModalContent({licence, dispatch}) {
useEffect(() => { useEffect(() => {
if (licence.id !== -1) { if (licence.id !== -1) {
setNew(false) setNew(false)
if (licence.certificate === null) {
setCertificateBy("")
setCertificateDate("")
} else {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
}
} else { } else {
setNew(true) setNew(true)
}
if (licence.certificate) {
setCertificateBy(licence.certificate.split('¤')[0])
setCertificateDate(licence.certificate.split('¤')[1])
} else {
setCertificateBy("") setCertificateBy("")
setCertificateDate("") setCertificateDate("")
} }

View File

@@ -457,8 +457,7 @@ function Content({data}) {
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/> defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
</div> </div>
<div className="input-group mb-3" <div className="input-group mb-3" style={{display: "flex" }}>
style={{display: registerMode === "FREE" || registerMode === "CLUB_ADMIN" ? "flex" : "none"}}>
<span className="input-group-text" id="startRegister">{t('poidsDemandéPour')}</span> <span className="input-group-text" id="startRegister">{t('poidsDemandéPour')}</span>
{CatList.map((cat, index) => <div key={index} className="input-group-text"> {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)} <input className="form-check-input mt-0" type="checkbox" id={"catInput" + index} checked={isCatSelected(cat)}

View File

@@ -4,7 +4,7 @@ import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx"; import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner"; import {ThreeDots} from "react-loader-spinner";
import React, {useEffect, useId, useReducer, useRef, useState} from "react"; import React, {useEffect, useId, useReducer, useRef, useState} from "react";
import {apiAxios, applyOverCategory, CatList, getCatName, getToastMessage} from "../../utils/Tools.js"; import {apiAxios, applyOverCategory, CatList, getCatFromName, getCatName, getToastMessage} from "../../utils/Tools.js";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@@ -14,6 +14,7 @@ import * as XLSX from "xlsx-js-style";
import {useCountries} from "../../hooks/useCountries.jsx"; import {useCountries} from "../../hooks/useCountries.jsx";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import {Checkbox} from "../../components/MemberCustomFiels.jsx"; import {Checkbox} from "../../components/MemberCustomFiels.jsx";
import {FileImport} from "../../components/FileImport.jsx";
export function CompetitionRegisterAdmin({source}) { export function CompetitionRegisterAdmin({source}) {
const {id} = useParams() const {id} = useParams()
@@ -55,6 +56,26 @@ export function CompetitionRegisterAdmin({source}) {
return response.data 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})
})
}
return <div> return <div>
<h2>{t('comp.combattantsInscrits')}</h2> <h2>{t('comp.combattantsInscrits')}</h2>
@@ -67,7 +88,7 @@ export function CompetitionRegisterAdmin({source}) {
<div className="col-lg-9"> <div className="col-lg-9">
{data ? <div className=""> {data ? <div className="">
<MakeCentralPanel <MakeCentralPanel
data={state.filter(s => (clubFilter.length === 0 || s.data.club.name === clubFilter) data={state.filter(s => (clubFilter.length === 0 || s.data.club?.name === clubFilter)
&& (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter) && (catAgeFilter.length === 0 || s.data.categorie === catAgeFilter)
&& (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter)) && (catFilter === -1 || s.data.categoriesInscrites.includes(catFilter))
&& (!filterNotWeight || (data3?.requiredWeight.includes(s.data.categorie) && ( && (!filterNotWeight || (data3?.requiredWeight.includes(s.data.categorie) && (
@@ -98,11 +119,13 @@ export function CompetitionRegisterAdmin({source}) {
filterNotWeight={filterNotWeight} setFilterNotWeight={setFilterNotWeight} source={source}/> filterNotWeight={filterNotWeight} setFilterNotWeight={setFilterNotWeight} source={source}/>
</div> </div>
</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>
</div> </div>
<Modal data2={data2} error2={error2} data3={data3} sendRegister={sendRegister} modalState={modalState} setModalState={setModalState} <Modal_ data2={data2} error2={error2} data3={data3} sendRegister={sendRegister} modalState={modalState} setModalState={setModalState}
source={source}/> source={source}/>
</div> </div>
} }
@@ -346,7 +369,7 @@ function CategoriesList({error2, availableCats, fistCatInput, categories, setCat
</> </>
} }
function Modal({data2, data3, error2, sendRegister, modalState, setModalState, source}) { function Modal_({data2, data3, error2, sendRegister, modalState, setModalState, source}) {
const country = useCountries('fr') const country = useCountries('fr')
const {t} = useTranslation(); const {t} = useTranslation();
const closeBtn = useRef(null); const closeBtn = useRef(null);
@@ -531,11 +554,11 @@ function Modal({data2, data3, error2, sendRegister, modalState, setModalState, s
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text" id="weight">{t('comp.modal.poids')}</span> <span className="input-group-text" id="weight">{t('comp.modal.poids')}</span>
{source === "admin" && <span className="input-group-text" id="weightReal">{t('comp.modal.annoncé')}</span>} {source === "admin" && <span className="input-group-text" id="weightReal">{t('comp.modal.annoncé')}</span>}
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weight" <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} name="weight" aria-describedby="weight" disabled={!(data3.requiredWeight.includes(currenCat))} value={weight}
onChange={e => setWeight(e.target.value)}/> onChange={e => setWeight(e.target.value)}/>
{source === "admin" && <><span className="input-group-text" id="weightReal">{t('comp.modal.pesé')}</span> {source === "admin" && <><span className="input-group-text" id="weightReal">{t('comp.modal.pesé')}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weightReal" <input type="number" min={1} step={0.1} className="form-control" placeholder="--" aria-label="weightReal"
name="weightReal" aria-describedby="weightReal" value={weightReal} name="weightReal" aria-describedby="weightReal" value={weightReal}
onChange={e => setWeightReal(e.target.value)}/></>} onChange={e => setWeightReal(e.target.value)}/></>}
</div> </div>
@@ -721,29 +744,78 @@ function MakeCentralPanel({data, data2, data3, dispatch, id, setModalState, sour
</> </>
} }
function FileOutput({data}) { function FileOutput({data, data2}) {
const {t} = useTranslation(); const {t} = useTranslation();
const handleFileDownload = () => { 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 = [] const dataOut = []
for (const e of data) { for (const e of data) {
const tmp = { const tmp = {
licence: e.licence, licence: e.id <= 0 ? -1 : e.licence,
pays: e.country,
nom: e.lname, nom: e.lname,
prenom: e.fname, prenom: e.fname,
genre: e.genre, genre: e.genre,
weight: e.weight, weight: e.weightReal ? e.weightReal : e.weight,
categorie: e.categorie, categorie: getCatName(e.categorie),
overCategory: e.overCategory, overCategory: e.overCategory,
categorie2: getCatName(applyOverCategory(e.categorie, e.overCategory)),
club: e.club ? e.club.name : '', club: e.club ? e.club.name : '',
...catColumns
}
for (const c of e.categoriesInscrites) {
tmp[c] = "X"
} }
dataOut.push(tmp) 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 wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(dataOut); const ws = XLSX.utils.json_to_sheet([], {skipHeader: true});
XLSX.utils.sheet_add_aoa(ws, [["Licence", "Nom", "Prénom", "Genre", "Poids", "Catégorie normalizer", "Surclassement", "Club"]], {origin: 'A1'});
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.utils.book_append_sheet(wb, ws, "Feuille 1");
XLSX.writeFile(wb, "output.xlsx"); XLSX.writeFile(wb, "output.xlsx");
@@ -756,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() { function Def() {
return <div className="list-group"> return <div className="list-group">
<li className="list-group-item"><ThreeDots/></li> <li className="list-group-item"><ThreeDots/></li>

View File

@@ -156,11 +156,11 @@ function SelfRegister({data2}) {
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4> <h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
<div className="input-group mb-3" hidden={!(data2?.requiredWeight.includes(currenCat))}> <div className="input-group mb-3" hidden={!(data2?.requiredWeight.includes(currenCat))}>
<span className="input-group-text" id="weight">{t("comp.modal.poids")}</span> <span className="input-group-text" id="weight">{t("comp.modal.poids")}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" 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)}/> name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
{data[0]?.weightReal && <> {data[0]?.weightReal && <>
<span className="input-group-text" id="weight">{t("comp.modal.pesé")}</span> <span className="input-group-text" id="weight">{t("comp.modal.pesé")}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="--" aria-label="weight" disabled={true} <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={() => { name="weight" aria-describedby="weight" value={data[0]?.weightReal} onChange={() => {
}}/> }}/>
</>} </>}

View File

@@ -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 {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {toast} from "react-toastify"; 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 {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
import {CategoryContent} from "./CategoryAdminContent.jsx"; import {CategoryContent} from "./CategoryAdminContent.jsx";
import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx"; import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx";
@@ -11,10 +11,17 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip"; import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx"; 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 {Trans, useTranslation} from "react-i18next";
import i18n from "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; const vite_url = import.meta.env.VITE_URL;
@@ -32,7 +39,10 @@ export function CMAdmin({compUuid}) {
...cat_, ...cat_,
name: data.name, name: data.name,
liceName: data.liceName, 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'}}) dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
@@ -42,7 +52,7 @@ export function CMAdmin({compUuid}) {
return <> return <>
<div className="card"> <div className="card">
<div className='card-header'> <div className='card-header'>
<CategoryHeader cat={cat} setCatId={setCatId}/> <CategoryHeader cat={cat} setCatId={setCatId} menuActions={menuActions}/>
</div> </div>
<div className="card-body"> <div className="card-body">
@@ -161,12 +171,20 @@ async function downloadResourcesAsZip(resourceList) {
progressText.textContent = i18n.t('téléchargementTerminé!'); progressText.textContent = i18n.t('téléchargementTerminé!');
} }
const windowName = "FFSAFTableStateWindow";
function Menu({menuActions, compUuid}) { function Menu({menuActions, compUuid}) {
const e = document.getElementById("actionMenu") const e = document.getElementById("actionMenu")
const longPress = useRef({time: null, timer: null, button: null}); const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null); const obsModal = useRef(null);
const teamCardModal = useRef(null);
const printModal = useRef(null);
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const [showStateWin, setShowStateWin] = useState(false)
const externalWindow = useRef(null)
const containerEl = useRef(document.createElement("div"))
for (const x of tto) for (const x of tto)
x.dispose(); x.dispose();
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') 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) => { const longPressDown = (button) => {
longPress.current.button = button; longPress.current.button = button;
longPress.current.time = new Date(); longPress.current.time = new Date();
@@ -201,6 +245,8 @@ function Menu({menuActions, compUuid}) {
if (button === "obs") { if (button === "obs") {
downloadResourcesAsZip(menuActions.current.resourceList || []) downloadResourcesAsZip(menuActions.current.resourceList || [])
.then(__ => console.log("Ressources téléchargées")); .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 = () => { const copyScriptToClipboard = () => {
navigator.clipboard.writeText(`<!--suppress ALL --> // noinspection JSFileReferences
navigator.clipboard.writeText(`
<div id='safca_api_data'></div> <div id='safca_api_data'></div>
<script type="module"> <script type="module">
import {initCompetitionApi} from '${vite_url}/competition.js'; import {initCompetitionApi} from '${vite_url}/competition.js';
@@ -240,8 +287,19 @@ function Menu({menuActions, compUuid}) {
{createPortal( {createPortal(
<> <>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <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" <FontAwesomeIcon icon={SimpleIconsOBS} size="xl"
style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}} style={{color: "#6c757d", cursor: "pointer"}}
onMouseDown={() => longPressDown("obs")} onMouseDown={() => longPressDown("obs")}
onMouseUp={() => longPressUp("obs")} onMouseUp={() => longPressUp("obs")}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-toggle="tooltip2" data-bs-placement="top"
@@ -251,7 +309,12 @@ function Menu({menuActions, compUuid}) {
onClick={() => copyScriptToClipboard()} onClick={() => copyScriptToClipboard()}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('ttm.admin.scripte')}/> 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"))} </>, 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'}}> <button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal Launch OBS Modal
@@ -268,9 +331,8 @@ function Menu({menuActions, compUuid}) {
<strong>{t('config.obs.warn1')}</strong> <strong>{t('config.obs.warn1')}</strong>
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">{t('adresseDuServeur')}</span> <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="ws://127.0.0.1:4455/" aria-label=""
<input type="text" className="form-control" placeholder="127.0.0.1:4455" aria-label="" defaultValue={"ws://127.0.0.1:4455/"}/>
defaultValue={"127.0.0.1:4455"}/>
<span className="input-group-text">/</span> <span className="input-group-text">/</span>
</div> </div>
<div className="input-group mb-3"> <div className="input-group mb-3">
@@ -308,13 +370,312 @@ function Menu({menuActions, compUuid}) {
</div> </div>
</div> </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 setLoading = useLoadingSwitcher()
const bthRef = useRef(); const bthRef = useRef(null);
const confirmRef = useRef(); const newBthRef = useRef(null);
const confirmRef = useRef(null);
const [modal, setModal] = useState({}) const [modal, setModal] = useState({})
const [confirm, setConfirm] = useState({}) const [confirm, setConfirm] = useState({})
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
@@ -330,6 +691,7 @@ function CategoryHeader({cat, setCatId}) {
]) ])
} }
const sendAddCategory = ({data}) => { const sendAddCategory = ({data}) => {
console.log("add cat", data);
setCats([...cats, data]) setCats([...cats, data])
} }
const sendDelCategory = ({data}) => { const sendDelCategory = ({data}) => {
@@ -363,8 +725,7 @@ function CategoryHeader({cat, setCatId}) {
if (selectedCatId !== "-1") { if (selectedCatId !== "-1") {
setCatId(selectedCatId); setCatId(selectedCatId);
} else { // New category } else { // New category
setModal({}); newBthRef.current.click();
bthRef.current.click();
e.target.value = cat?.id; e.target.value = cat?.id;
} }
} }
@@ -382,14 +743,14 @@ function CategoryHeader({cat, setCatId}) {
</div> </div>
<div className="col" style={{margin: "auto 0", textAlign: "center"}}> <div className="col" style={{margin: "auto 0", textAlign: "center"}}>
{cat && {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>} Zone: {cat.liceName}</div>}
</div> </div>
<div className="col-auto"> <div className="col-auto">
<button className="btn btn-primary float-end" onClick={() => { <button className="btn btn-primary float-end" onClick={() => {
setModal(cat); setModal(cat);
bthRef.current.click(); bthRef.current.click();
}} disabled={cat === null}>Modifier }} disabled={cat === null}>{t('modifier')}
</button> </button>
</div> </div>
@@ -404,37 +765,191 @@ function CategoryHeader({cat, setCatId}) {
</div> </div>
</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> <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 : () => { <ConfirmDialog id="confirm-dialog" onConfirm={confirm.confirm ? confirm.confirm : () => {
}} onCancel={confirm.cancel ? confirm.cancel : () => { }} onCancel={confirm.cancel ? confirm.cancel : () => {
}} title={confirm ? confirm.title : ""} message={confirm ? confirm.message : ""}/> }} 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> </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}) { function ModalContent({state, setCatId, setConfirm, confirmRef}) {
const id = useId()
const [name, setName] = useState("") const [name, setName] = useState("")
const [lice, setLice] = useState("1") const [lice, setLice] = useState("1")
const [poule, setPoule] = useState(true) const [poule, setPoule] = useState(true)
const [tournoi, setTournoi] = useState(false) const [tournoi, setTournoi] = useState(false)
const [classement, setClassement] = useState(true)
const [fullClassement, setFullClassement] = useState(false)
const [size, setSize] = useState(4) const [size, setSize] = useState(4)
const [loserMatch, setLoserMatch] = useState(1) const [loserMatch, setLoserMatch] = useState(1)
const [preset, setPreset] = useState(-1)
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {sendRequest} = useWS(); const {sendRequest} = useWS();
useEffect(() => { useEffect(() => {
console.log(state);
setName(state.name || ""); setName(state.name || "");
setLice(state.liceName || "1"); setLice(state.liceName || "1");
setPoule(((state.type || 1) & 1) !== 0); setPoule(((state.type || 1) & 1) !== 0);
setTournoi((state.type & 2) !== 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) { let trees_ = []
const tree = state.trees[0]; 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); setSize(tree.getMaxChildrenAtDepth(tree.death() - 1) * 2);
if (state.trees.length === 1) { if (trees_.length === 1) {
setLoserMatch(0); setLoserMatch(0);
} else if (state.trees.length === 2) { } else if (trees_.length === 2) {
setLoserMatch(1); setLoserMatch(1);
} else { } else {
setLoserMatch(-1); setLoserMatch(-1);
@@ -460,24 +975,33 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
return; 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) { if (state?.id) {
const applyChanges = () => { const applyChanges = () => {
const newData = { const newData = {
id: state.id, id: state.id,
name: name.trim(), name: name.trim(),
liceName: lice.trim(), liceName: lice.trim(),
type: nType type: nType,
treeAreClassement: classement,
fullClassement: fullClassement,
preset: {id: preset !== -1 ? preset : null}
} }
let nbMatch = -1; let nbMatch = -1;
let oldSubTree = -1; let oldSubTree = -1;
const oldTrees = state?.trees || []; const oldTrees = trees_ || [];
if (oldTrees.length >= 1) { if (oldTrees.length >= 1) {
const tree = state.trees[0]; const tree = trees_[0];
nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1); nbMatch = tree.getMaxChildrenAtDepth(tree.death() - 1);
if (state.trees.length === 1) if (trees_.length === 1)
oldSubTree = 0 oldSubTree = 0
else if (state.trees.length === 2) else if (trees_.length === 2)
oldSubTree = 1 oldSubTree = 1
} }
@@ -500,7 +1024,10 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
newTrees.push(trees2.at(i)); newTrees.push(trees2.at(i));
} }
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees", "cm") toast.promise(sendRequest('updateTrees', {
categoryId: state.id,
trees: newTrees
}), getToastMessage("toast.updateTrees", "cm")
).then(__ => { ).then(__ => {
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm")) toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
}) })
@@ -534,7 +1061,10 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
toast.promise(sendRequest('createCategory', { toast.promise(sendRequest('createCategory', {
name: name.trim(), name: name.trim(),
liceName: lice.trim(), liceName: lice.trim(),
type: nType type: nType,
treeAreClassement: classement,
fullClassement: fullClassement,
preset: {id: preset !== -1 ? preset : null}
}), getToastMessage("toast.createCategory", "cm") }), getToastMessage("toast.createCategory", "cm")
).then(id => { ).then(id => {
if (tournoi) { if (tournoi) {
@@ -548,15 +1078,6 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
} }
}) })
} }
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}> return <form onSubmit={handleSubmit}>
@@ -572,9 +1093,12 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
onChange={e => setName(e.target.value)}/> onChange={e => setName(e.target.value)}/>
</div> </div>
<ListPresetSelect value={preset} onChange={setPreset}/>
<div className="mb-3"> <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> <label htmlFor={id + "liceInput1"} className="form-label"><Trans i18nKey="nomDesZonesDeCombat" ns="cm">t <small>(séparée par des
<input type="text" className="form-control" id="liceInput1" placeholder="1;2" name="zone de combat" value={lice} ';')</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)}/> onChange={e => setLice(e.target.value)}/>
</div> </div>
@@ -592,7 +1116,16 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
onChange={e => setTournoi(e.target.checked)}/> onChange={e => setTournoi(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label> <label className="form-check-label" htmlFor="switchCheckDefault2">{t('tournoi')}</label>
</div> </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>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label> <label htmlFor="sizeInput1" className="form-label">Nombre de combattants</label>
@@ -631,6 +1164,14 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
</div> </div>
</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>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button> <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">{t('fermer')}</button>

View File

@@ -2,14 +2,16 @@ import React, {useEffect, useRef, useState} from "react";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {timePrint} from "../../../utils/Tools.js"; import {timePrint} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useWS} from "../../../hooks/useWS.jsx";
export function ChronoPanel() { export function ChronoPanel({menuActions}) {
const [config, setConfig] = useState({ const [config, setConfig] = useState({
time: Number(sessionStorage.getItem("chronoTime") || "90999"), time: Number(sessionStorage.getItem("chronoTime") || "90999"),
pause: Number(sessionStorage.getItem("chronoPause") || "60999") pause: Number(sessionStorage.getItem("chronoPause") || "60999")
}) })
const [chrono, setChrono] = useState({time: 0, startTime: 0}) const [chrono, setChrono] = useState({time: 0, startTime: 0})
const chronoText = useRef(null) const chronoText = useRef(null)
const [chronoState, setChronoState] = useState(0)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"}) const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch(); const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
@@ -23,6 +25,10 @@ export function ChronoPanel() {
return chrono.time + Date.now() - chrono.startTime return chrono.time + Date.now() - chrono.startTime
} }
menuActions.current.setTimerConfig = (time, pause) => {
setConfig({time: time + 999, pause: pause + 999})
}
useEffect(() => { useEffect(() => {
publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: state.current.lastTimeStr, timeColor: state.current.color}}) 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()) { if (state_.chronoState === 0 && isRunning()) {
state_.chronoState = 1 state_.chronoState = 1
setChronoState(1)
} else if (state_.chronoState === 1 && getTime() >= config.time) { } else if (state_.chronoState === 1 && getTime() >= config.time) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()})) setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 2 state_.chronoState = 2
setChronoState(2)
} else if (state_.chronoState === 2 && getTime() >= config.pause) { } else if (state_.chronoState === 2 && getTime() >= config.pause) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()})) setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 1 state_.chronoState = 1
setChronoState(1)
} }
if (isRunning()) { if (isRunning()) {
@@ -117,6 +126,7 @@ export function ChronoPanel() {
<button className="btn btn-danger col" onClick={__ => { <button className="btn btn-danger col" onClick={__ => {
setChrono(prev => ({...prev, time: 0, startTime: 0})) setChrono(prev => ({...prev, time: 0, startTime: 0}))
state.current.chronoState = 0 state.current.chronoState = 0
setChronoState(0)
}}>{t('réinitialiser')} }}>{t('réinitialiser')}
</button> </button>
</div> </div>
@@ -181,5 +191,19 @@ export function ChronoPanel() {
</div> </div>
</div> </div>
</div> </div>
<SendChrono chrono={chrono} config={config} chronoState={chronoState}/>
</div> </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 <>
</>
}

View File

@@ -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 {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; 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 {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {MarchReducer} from "../../../utils/MatchReducer.jsx"; import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {getToastMessage, scorePrint, win} from "../../../utils/Tools.js"; import {
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; CatList,
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; getCatName, getShieldSize,
import {toast} from "react-toastify"; getShieldTypeName, getSwordSize,
getSwordTypeName,
getToastMessage, timePrint,
virtual_end,
virtualScore,
win_end
} from "../../../utils/Tools.js";
import "./CMTMatchPanel.css" import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx"; import {useOBS} from "../../../hooks/useOBS.jsx";
import {useTranslation} from "react-i18next"; 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() { function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635" return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@@ -20,10 +31,17 @@ function CupImg() {
alt=""/> 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}) { export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS(); const {dispatch} = useWS();
const publicAffDispatch = usePubAffDispatch();
const {connected, setText} = useOBS(); const {connected, setText} = useOBS();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
@@ -53,6 +71,8 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
useEffect(() => { useEffect(() => {
setText("poule", cat ? cat.name : ""); setText("poule", cat ? cat.name : "");
if (!cat)
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
}, [cat, connected]); }, [cat, connected]);
return <> return <>
@@ -71,9 +91,10 @@ export function CategorieSelect({catId, setCatId, menuActions}) {
function CMTMatchPanel({catId, cat, menuActions}) { function CMTMatchPanel({catId, cat, menuActions}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS(); const {sendRequest, dispatch} = useWS();
const [trees, setTrees] = useState([]); const [trees, setTrees] = useState({raw: [], formatted: []});
const [matches, reducer] = useReducer(MarchReducer, []); const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch(); const combDispatch = useCombsDispatch();
const cardDispatch = useCardsDispatch();
function readAndConvertMatch(matches, data, combsToAdd) { function readAndConvertMatch(matches, data, combsToAdd) {
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id}) matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
@@ -89,7 +110,12 @@ function CMTMatchPanel({catId, cat, menuActions}) {
setLoading(1); setLoading(1);
sendRequest('getFullCategory', catId) sendRequest('getFullCategory', catId)
.then((data) => { .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 matches2 = [];
let combsToAdd = []; let combsToAdd = [];
@@ -103,7 +129,10 @@ function CMTMatchPanel({catId, cat, menuActions}) {
const treeListener = ({data}) => { const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId) if (data.length < 1 || data[0].categorie !== catId)
return 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 matches2 = [];
let combsToAdd = []; let combsToAdd = [];
@@ -128,29 +157,114 @@ function CMTMatchPanel({catId, cat, menuActions}) {
reducer({type: 'REMOVE', payload: data}) 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: treeListener, code: 'sendTreeCategory'}})
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}}) dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}}) dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}}) dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}})
return () => { return () => {
dispatch({type: 'removeListener', payload: treeListener}) dispatch({type: 'removeListener', payload: treeListener})
dispatch({type: 'removeListener', payload: matchListener}) dispatch({type: 'removeListener', payload: matchListener})
dispatch({type: 'removeListener', payload: matchOrder}) dispatch({type: 'removeListener', payload: matchOrder})
dispatch({type: 'removeListener', payload: deleteMatch}) dispatch({type: 'removeListener', payload: deleteMatch})
dispatch({type: 'removeListener', payload: sendCardboard})
} }
}, [catId]); }, [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}) { function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1); const [type, setType] = useState(1);
const {sendRequest} = useWS();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
@@ -160,6 +274,10 @@ function ListMatch({cat, matches, trees, menuActions}) {
setType(cat.type); setType(cat.type);
}, [cat]); }, [cat]);
const handleCreatClassement = () => {
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
}
if (!cat) if (!cat)
return <></>; return <></>;
@@ -173,7 +291,7 @@ function ListMatch({cat, matches, trees, menuActions}) {
</li> </li>
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} <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> </div>
</li> </li>
</ul> </ul>
@@ -185,24 +303,33 @@ function ListMatch({cat, matches, trees, menuActions}) {
</>} </>}
{type === 2 && <> {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> </div>
} }
function MatchList({matches, cat, menuActions}) { function MatchList({matches, cat, menuActions, classement = false, currentMatch = null, setCurrentMatch, getNext}) {
const [activeMatch, setActiveMatch] = useState(null) const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1") const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
const publicAffDispatch = usePubAffDispatch(); const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {cards_v, getHeightCardForCombInMatch} = useCards();
const {sendNotify, setState} = useWS();
const liceName = (cat.liceName || "N/A").split(";"); const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42) 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) .sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, win: win(m.scores)})) .map(m => ({...m, ...win_end(m, cards_v)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => { const isActiveMatch = (index) => {
if (classement)
return true;
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice) return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
} }
@@ -224,24 +351,69 @@ function MatchList({matches, cat, menuActions}) {
}); });
} }
}, [match]); }, [match]);
//useEffect(() => {
// if (activeMatch !== null)
// setActiveMatch(null);
//}, [cat])
useEffect(() => { useEffect(() => {
if (match && match.poule !== lice) if (match && match.poule !== lice)
if (!classement)
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id) setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
}, [lice]); }, [lice]);
useEffect(() => { useEffect(() => {
if (marches2.length === 0) if (marches2.length === 0)
return; return;
if (marches2.some(m => m.id === activeMatch)) if (marches2.some(m => m.id === (classement ? currentMatch?.matchSelect : activeMatch)))
return; return;
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); setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}
}, [matches]) }, [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 <> return <>
{liceName.length > 1 && {liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}> <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"> <table className="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<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">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">P</th>}
<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('no')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th> <th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
@@ -273,35 +445,41 @@ function MatchList({matches, cat, menuActions}) {
<tbody className="table-group-divider"> <tbody className="table-group-divider">
{marches2.map((m, index) => ( {marches2.map((m, index) => (
<tr key={m.id} <tr key={m.id}
className={m.id === activeMatch ? "table-info" : (isActiveMatch(index) ? "" : "table-warning")} className={m.id === (classement ? currentMatch?.matchSelect : activeMatch) ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))}
onClick={() => setActiveMatch(m.id)}> onClick={() => handleMatchClick(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}> {!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td> {liceName[(index - firstIndex) % liceName.length]}</td>}
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</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"}}> <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th> {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"}}> <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"}}> <td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<small><CombName combId={m.c2}/></small></td> <small className="position-relative"><CombName combId={m.c2}/>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td> <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> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
{activeMatch && {activeMatch && !classement &&
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>} <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 scrollRef = useRef(null)
const [currentMatch, setCurrentMatch] = useState(null) const [currentMatch, setCurrentMatch] = useState(null)
const {getComb} = useCombs() const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch(); 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) const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => { useEffect(() => {
@@ -320,38 +498,84 @@ function BuildTree({treeData, matches, menuActions}) {
} }
}, [next_match]); }, [next_match]);
function parseTree(data_in) { function parseTree(data_in, matches_) {
if (data_in?.data == null) if (data_in?.data == null)
return 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 c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2) 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({ let node = new TreeNode({
...matchData, ...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
}) })
node.left = parseTree(data_in?.left) node.left = parseTree(data_in?.left, matches_)
node.right = parseTree(data_in?.right) node.right = parseTree(data_in?.right, matches_)
return node return node
} }
function initTree(data_in) { function initTree(data_in, matches_) {
let out = [] let out = []
for (const din of data_in) { let out2 = []
out.push(parseTree(din)) 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, __) => { 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 = () => { const onClickVoid = () => {
@@ -362,7 +586,11 @@ function BuildTree({treeData, matches, menuActions}) {
<div className="overflow-y-auto" style={{maxHeight: "50vh"}}> <div className="overflow-y-auto" style={{maxHeight: "50vh"}}>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}> <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid} <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>
</div> </div>
@@ -371,301 +599,3 @@ function BuildTree({treeData, matches, menuActions}) {
menuActions={menuActions}/></LoadingProvider>} menuActions={menuActions}/></LoadingProvider>}
</div> </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", "cm"));
}
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>
}

View File

@@ -1,8 +1,9 @@
import {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons"; import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useWS} from "../../../hooks/useWS.jsx";
export function PointPanel({menuActions}) { export function PointPanel({menuActions}) {
const [revers, setRevers] = useState(false) const [revers, setRevers] = useState(false)
@@ -47,7 +48,20 @@ export function PointPanel({menuActions}) {
<div className="col row align-items-center"> <div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button> <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> </div>
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
</div> </div>
} }
function SendScore({scoreRouge, scoreBleu}) {
const {sendNotify, setState} = useWS();
useEffect(() => {
setState({scoreState: {scoreRouge, scoreBleu}});
sendNotify("sendCurrentScore", {scoreRouge, scoreBleu});
}, [scoreRouge, scoreBleu]);
return <>
</>
}

View File

@@ -5,7 +5,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {createPortal} from "react-dom"; import {createPortal} from "react-dom";
import {copyStyles} from "../../../utils/copyStyles.js"; import {copyStyles} from "../../../utils/copyStyles.js";
import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx"; 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 {PubAffWindow} from "./PubAffWindow.jsx";
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts"; import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
import {ChronoPanel} from "./CMTChronoPanel.jsx"; import {ChronoPanel} from "./CMTChronoPanel.jsx";
@@ -13,8 +13,9 @@ import {CategorieSelect} from "./CMTMatchPanel.jsx";
import {PointPanel} from "./CMTPoint.jsx"; import {PointPanel} from "./CMTPoint.jsx";
import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx"; import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import {toast} from "react-toastify"; import {Flip, toast} from "react-toastify";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {getToastMessage} from "../../../utils/Tools.js";
export function CMTable() { export function CMTable() {
const combDispatch = useCombsDispatch() const combDispatch = useCombsDispatch()
@@ -37,7 +38,7 @@ export function CMTable() {
<div className="card mb-3"> <div className="card mb-3">
<div className="card-header">{t('chronomètre')}</div> <div className="card-header">{t('chronomètre')}</div>
<div className="card-body"> <div className="card-body">
<ChronoPanel/> <ChronoPanel menuActions={menuActions}/>
</div> </div>
</div> </div>
@@ -47,6 +48,8 @@ export function CMTable() {
<PointPanel menuActions={menuActions}/> <PointPanel menuActions={menuActions}/>
</div> </div>
</div> </div>
<div id="infoCategory"></div>
</div> </div>
<div className="col-md-12 col-xl-6 col-xxl-5"> <div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card mb-3"> <div className="card mb-3">
@@ -59,6 +62,7 @@ export function CMTable() {
</div> </div>
<Menu menuActions={menuActions}/> <Menu menuActions={menuActions}/>
<ObsAutoSyncWhitPubAff/> <ObsAutoSyncWhitPubAff/>
<SendCatId catId={catId}/>
</div> </div>
</PubAffProvider> </PubAffProvider>
</OBSProvider> </OBSProvider>
@@ -73,9 +77,11 @@ function Menu({menuActions}) {
const publicAffDispatch = usePubAffDispatch() const publicAffDispatch = usePubAffDispatch()
const [showPubAff, setShowPubAff] = useState(false) const [showPubAff, setShowPubAff] = useState(false)
const [showScore, setShowScore] = useState(true) const [showScore, setShowScore] = useState(true)
const [zone, setZone] = useState(sessionStorage.getItem("liceName") || "???")
const {connected, connect, disconnect} = useOBS(); const {connected, connect, disconnect} = useOBS();
const longPress = useRef({time: null, timer: null, button: null}); const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null); const obsModal = useRef(null);
const teamCardModal = useRef(null);
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const externalWindow = useRef(null) const externalWindow = useRef(null)
@@ -124,7 +130,7 @@ function Menu({menuActions}) {
const longTimeAction = (button) => { const longTimeAction = (button) => {
if (button === "obs") { if (button === "obs") {
obsModal.current.click(); // obsModal.current.click();
} }
} }
@@ -154,7 +160,7 @@ function Menu({menuActions}) {
} else { } else {
importOBSConfiguration() importOBSConfiguration()
.then(config => { .then(config => {
connect("ws://" + config.adresse + "/", config.password, config.assets_dir); connect(config.adresse, config.password, config.assets_dir);
}) })
.catch(() => { .catch(() => {
toast.error(t('aucuneConfigurationObs')); toast.error(t('aucuneConfigurationObs'));
@@ -169,12 +175,19 @@ function Menu({menuActions}) {
} }
} }
const handleOBSSubmit = (e) => { const handleLiceSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const form = e.target; const form = e.target;
const prefix = form[0].value; 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) if (!e)
@@ -183,6 +196,11 @@ function Menu({menuActions}) {
{createPortal( {createPortal(
<> <>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <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"}} <FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top" onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('ttm.table.inverserLaPosition')}/> data-bs-title={t('ttm.table.inverserLaPosition')}/>
@@ -203,23 +221,23 @@ function Menu({menuActions}) {
</>, document.getElementById("actionMenu"))} </>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)} {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'}}> <button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#LiceNameModal"
Launch OBS Modal style={{display: 'none'}}>
Launch Lice Name Modal
</button> </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-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <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> <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form onSubmit={handleOBSSubmit}> <form onSubmit={handleLiceSubmit}>
<div className="modal-body"> <div className="modal-body">
<div className="input-group mb-3"> <div className="input-group mb-3">
<span className="input-group-text">{t('obs.préfixDesSources')}</span> <span className="input-group-text">{t('nomDeLaZone')}</span>
<span className="input-group-text">sub</span> <input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={0} maxLength={1}
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1} defaultValue={sessionStorage.getItem("liceName") || "1"}/>
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
@@ -230,6 +248,176 @@ function Menu({menuActions}) {
</div> </div>
</div> </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 <>
</> </>
} }

View File

@@ -6,7 +6,7 @@ import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
import {DrawGraph} from "../../result/DrawGraph.jsx"; import {DrawGraph} from "../../result/DrawGraph.jsx";
import {SelectCombModalContent} from "./SelectCombModalContent.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 {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable'; 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 {CSS} from '@dnd-kit/utilities';
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {faPen, faTrash} from "@fortawesome/free-solid-svg-icons";
import {getToastMessage, win} from "../../../utils/Tools.js"; import {getToastMessage, virtualScore, win_end} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next"; 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; const vite_url = import.meta.env.VITE_URL;
@@ -26,9 +31,16 @@ function CupImg() {
alt=""/> 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}) { export function CategoryContent({cat, catId, setCat, menuActions}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS(); const {sendRequest, dispatch} = useWS();
const cardDispatch = useCardsDispatch();
const [matches, reducer] = useReducer(MarchReducer, []); const [matches, reducer] = useReducer(MarchReducer, []);
const [groups, setGroups] = useState([]) const [groups, setGroups] = useState([])
const groupsRef = useRef(groups); const groupsRef = useRef(groups);
@@ -58,7 +70,8 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
return return
setCat(cat_ => ({ setCat(cat_ => ({
...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 = []; 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}}); 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)}}); combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
if (data.categorie !== cat.id)
continue;
setGroups(prev => { setGroups(prev => {
if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id)) if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id))
return [...prev, {id: data.c1?.id, poule: data.poule}]; return [...prev, {id: data.c1?.id, poule: data.poule}];
@@ -118,9 +134,15 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
name: data.name, name: data.name,
liceName: data.liceName, liceName: data.liceName,
type: data.type, 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 matches2 = [];
let combsToAdd = []; let combsToAdd = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd)); data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
@@ -157,15 +179,34 @@ export function CategoryContent({cat, catId, setCat, menuActions}) {
return <> return <>
<div className="col-md-3"> <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>
<div className="col-md-9"> <div className="col-md-9">
{cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>} {cat && <ListMatch cat={cat} matches={matches} groups={groups} reducer={reducer}/>}
</div> </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 {data, setData} = useRequestWS("getRegister", null)
const combDispatch = useCombsDispatch() const combDispatch = useCombsDispatch()
const {dispatch} = useWS() const {dispatch} = useWS()
@@ -228,10 +269,23 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) {
disabled={data === null} onClick={() => setModalMode(true)}>{t('ajouterUneTeam')} disabled={data === null} onClick={() => setModalMode(true)}>{t('ajouterUneTeam')}
</button> </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 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-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
<div className="modal-content"> <div className="modal-content">
<SelectCombModalContent data={data} setGroups={setGroups} teamMode={modalMode} groups={groups}/> <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> </div>
</div> </div>
@@ -354,6 +408,7 @@ function ListMatch({cat, matches, groups, reducer}) {
const {sendRequest} = useWS(); const {sendRequest} = useWS();
const [type, setType] = useState(1); const [type, setType] = useState(1);
const bthRef = useRef(null); const bthRef = useRef(null);
const bthRef2 = useRef(null);
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
useEffect(() => { useEffect(() => {
@@ -369,6 +424,17 @@ function ListMatch({cat, matches, groups, reducer}) {
recalculateMatch(0); 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) => { const recalculateMatch = (mode) => {
let matchesToKeep = []; let matchesToKeep = [];
let matchesToRemove = []; let matchesToRemove = [];
@@ -390,7 +456,7 @@ function ListMatch({cat, matches, groups, reducer}) {
matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate),
matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate),
matchesToRemove: matchesToRemove.map(m => m.id) matchesToRemove: matchesToRemove.map(m => m.id)
}), getToastMessage("toast.matchs.create", "ns")) }), getToastMessage("toast.matchs.create", "cm"))
.finally(() => { .finally(() => {
console.log("Finished creating matches"); console.log("Finished creating matches");
}) })
@@ -410,7 +476,7 @@ function ListMatch({cat, matches, groups, reducer}) {
</li> </li>
<li className="nav-item"> <li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} <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> </div>
</li> </li>
</ul> </ul>
@@ -422,7 +488,9 @@ function ListMatch({cat, matches, groups, reducer}) {
</>} </>}
{type === 2 && <> {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> <button ref={bthRef} data-bs-toggle="modal" data-bs-target="#makeMatchMode" style={{display: "none"}}>open</button>
@@ -451,10 +519,16 @@ function ListMatch({cat, matches, groups, reducer}) {
</div> </div>
</div> </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 {sendRequest} = useWS();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const selectRef = useRef(null) const selectRef = useRef(null)
@@ -463,12 +537,23 @@ function MatchList({matches, cat, groups, reducer}) {
const [combSelect, setCombSelect] = useState(0) const [combSelect, setCombSelect] = useState(0)
const [combC1nm, setCombC1nm] = useState(null) const [combC1nm, setCombC1nm] = useState(null)
const [combC2nm, setCombC2nm] = useState(null) const [combC2nm, setCombC2nm] = useState(null)
const [modalMatchId, setModalMatchId] = useState(null)
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {cards_v, getHeightCardForCombInMatch} = useCards();
const matchModal = useRef(null);
const liceName = (cat.liceName || "N/A").split(";"); const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42) 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) .sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, win: win(m.scores)})) .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( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
@@ -478,9 +563,14 @@ function MatchList({matches, cat, groups, reducer}) {
); );
const handleDragEnd = async (event) => { const handleDragEnd = async (event) => {
if (classement)
return;
const {active, over} = event; const {active, over} = event;
if (active.id !== over.id) { 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}}); reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}});
sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => { sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => {
}) })
@@ -557,7 +647,7 @@ function MatchList({matches, cat, groups, reducer}) {
const handleDelMatch = (matchId) => { const handleDelMatch = (matchId) => {
const match = matches.find(m => m.id === matchId) const match = matches.find(m => m.id === matchId)
if (!match) if (!match || classement)
return; return;
if (!confirm(t('confirm1'))) if (!confirm(t('confirm1')))
@@ -568,6 +658,14 @@ function MatchList({matches, cat, groups, reducer}) {
.finally(() => setLoading(0)) .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) => { const handleCombClick = (e, matchId, combId) => {
e.stopPropagation(); e.stopPropagation();
const tableRect = tableRef.current.getBoundingClientRect(); const tableRect = tableRef.current.getBoundingClientRect();
@@ -590,6 +688,31 @@ function MatchList({matches, cat, groups, reducer}) {
lastMatchClick.current = null; 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); const combsIDs = groups.map(m => m.id);
return <div style={{position: "relative"}}> return <div style={{position: "relative"}}>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
@@ -599,38 +722,48 @@ function MatchList({matches, cat, groups, reducer}) {
<thead> <thead>
<tr> <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('no')}</th>
<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", 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"></th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</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">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th> <th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('résultat')}</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>
<th style={{textAlign: "center"}} scope="col"></th> {!classement && <th style={{textAlign: "center"}} scope="col"></th>}
{!classement && <th style={{textAlign: "center"}} scope="col"></th>}
</tr> </tr>
</thead> </thead>
<tbody className="table-group-divider"> <tbody className="table-group-divider">
{marches2.map((m, index) => ( {marches2.map((m, index) => (
<SortableRow key={m.id} id={m.id}> <SortableRow key={m.id} id={m.id}>
<th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th> <th style={{textAlign: "center", cursor: "auto"}} scope="row">{index + 1}</th>
<td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td> {!classement && <td style={{textAlign: "center", cursor: "auto"}}>{m.poule}</td>}
<td style={{textAlign: "center", cursor: "auto"}}>{liceName[index % liceName.length]}</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/>}</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"}} <td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c1)}> 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"}} <td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}
onClick={e => handleCombClick(e, m.id, m.c2)}> onClick={e => handleCombClick(e, m.id, m.c2)}>
<small><CombName combId={m.c2}/></small></td> <small className="position-relative"><CombName combId={m.c2}/>
<td style={{textAlign: "left", cursor: "auto", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td> <GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "center", cursor: "auto"}}>{scoreToString(m.scores)}</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)}> <td style={{textAlign: "center", cursor: "pointer", color: "#ff1313"}} onClick={_ => handleDelMatch(m.id)}>
<FontAwesomeIcon icon={faTrash}/></td> <FontAwesomeIcon icon={faTrash}/></td>}
<td style={{textAlign: "center", cursor: "grab"}}></td> <td style={{textAlign: "center", cursor: "grab"}} hidden={classement}></td>
</SortableRow> </SortableRow>
))} ))}
<tr> {!classement && <tr>
<td>-</td> <td>-</td>
<td></td> <td></td>
<td></td> <td></td>
@@ -645,7 +778,8 @@ function MatchList({matches, cat, groups, reducer}) {
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>
</tr> <td></td>
</tr>}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -659,7 +793,35 @@ function MatchList({matches, cat, groups, reducer}) {
<option key={combId} value={combId}><CombName combId={combId}/></option> <option key={combId} value={combId}><CombName combId={combId}/></option>
))} ))}
</select> </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>
</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}) { function SortableRow({id, children}) {
@@ -692,7 +854,7 @@ function SortableRow({id, children}) {
); );
} }
function BuildTree({treeData, matches, groups}) { function BuildTree({treeData, treeRaw, matches, cat, groups}) {
const scrollRef = useRef(null) const scrollRef = useRef(null)
const selectRef = useRef(null) const selectRef = useRef(null)
const lastMatchClick = useRef(null) const lastMatchClick = useRef(null)
@@ -701,6 +863,7 @@ function BuildTree({treeData, matches, groups}) {
const {sendRequest} = useWS(); const {sendRequest} = useWS();
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {cards_v} = useCards();
function parseTree(data_in) { function parseTree(data_in) {
if (data_in?.data == null) if (data_in?.data == null)
@@ -710,9 +873,19 @@ function BuildTree({treeData, matches, groups}) {
const c1 = getComb(matchData?.c1) const c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2) 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({ let node = new TreeNode({
...matchData, ...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null, c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
}) })
@@ -722,10 +895,12 @@ function BuildTree({treeData, matches, groups}) {
return node return node
} }
function initTree(data_in) { function initTree(data_in, data_raw) {
let out = [] let out = []
for (const din of data_in) { for (let i = 0; i < data_raw.length; i++) {
out.push(parseTree(din)) if (data_raw.at(i).level > -10) {
out.push(parseTree(data_in.at(i)))
}
} }
return out return out
} }
@@ -789,7 +964,10 @@ function BuildTree({treeData, matches, groups}) {
const combsIDs = groups.map(m => m.id); const combsIDs = groups.map(m => m.id);
return <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}> 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"}} <select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}> value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
<option value={0}>{t('--SélectionnerUnCombattant--')}</option> <option value={0}>{t('--SélectionnerUnCombattant--')}</option>

View File

@@ -10,6 +10,8 @@ import {ThreeDots} from "react-loader-spinner";
import {AxiosError} from "../../../components/AxiosError.jsx"; import {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js"; import {useFetch} from "../../../hooks/useFetch.js";
import {useTranslation} from "react-i18next"; 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; 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}> return <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
<WSStatus setPerm={setPerm}/> <WSStatus setPerm={setPerm}/>
<CombsProvider> <CombsProvider>
<CardsProvider>
<LoadingProvider> <LoadingProvider>
<Routes> <Routes>
<Route path="/" element={<Home2 perm={perm}/>}/> <Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/> <Route path="/admin" element={<CMAdmin compUuid={compUuid}/>}/>
<Route path="/table" element={<CMTable/>}/> <Route path="/table" element={<CMTable/>}/>
<Route path="/view/audio" element={<AudioEncoder/>}/>
</Routes> </Routes>
</LoadingProvider> </LoadingProvider>
</CardsProvider>
</CombsProvider> </CombsProvider>
</WSProvider> </WSProvider>
} }

View File

@@ -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>
}

View File

@@ -1,10 +1,11 @@
import {useCountries} from "../../../hooks/useCountries.jsx"; import {useCountries} from "../../../hooks/useCountries.jsx";
import {useEffect, useReducer, useRef, useState} from "react"; import React, {useEffect, useReducer, useRef, useState} from "react";
import {CatList, getCatName, getToastMessage} from "../../../utils/Tools.js"; import {CatList, getCatName, getToastMessage} from "../../../utils/Tools.js";
import {CombName} from "../../../hooks/useComb.jsx"; import {CombName} from "../../../hooks/useComb.jsx";
import {useWS} from "../../../hooks/useWS.jsx"; import {useWS} from "../../../hooks/useWS.jsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {ListPresetSelect} from "../../../components/cm/ListPresetSelect.jsx";
function SelectReducer(state, action) { function SelectReducer(state, action) {
switch (action.type) { switch (action.type) {
@@ -59,7 +60,7 @@ function SelectReducer(state, action) {
} }
} }
export function SelectCombModalContent({data, groups, setGroups, teamMode = false}) { export function SelectCombModalContent({data, groups, setGroups, teamMode = false, defaultPreset = -1}) {
const country = useCountries('fr') const country = useCountries('fr')
const {t} = useTranslation("cm"); const {t} = useTranslation("cm");
const {sendRequest, dispatch} = useWS() const {sendRequest, dispatch} = useWS()
@@ -77,6 +78,11 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
const [weightMax, setWeightMax] = useState(0) const [weightMax, setWeightMax] = useState(0)
const [team, setTeam] = useState(false) const [team, setTeam] = useState(false)
const [teamName, setTeamName] = useState(""); const [teamName, setTeamName] = useState("");
const [preset, setPreset] = useState(-1)
useEffect(() => {
setPreset(defaultPreset)
}, [defaultPreset])
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
@@ -149,7 +155,8 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin) && (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 && (teamMode && (comb.teamMembers == null || comb.teamMembers.length === 0) || !teamMode
&& ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))) { && ((comb.teamMembers == null || comb.teamMembers.length === 0) !== team))
&& (preset === -1 || comb.categoriesInscrites?.includes(preset))) {
dataOut[id] = dataIn[id]; dataOut[id] = dataIn[id];
} }
} }
@@ -215,6 +222,8 @@ export function SelectCombModalContent({data, groups, setGroups, teamMode = fals
</select> </select>
</div> </div>
<ListPresetSelect value={preset} onChange={setPreset}/>
<div> <div>
<label htmlFor="inputState1" className="form-label">{t('club')}</label> <label htmlFor="inputState1" className="form-label">{t('club')}</label>
<select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}> <select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>

View 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></>
}

View File

@@ -1,5 +1,6 @@
import {useEffect, useRef} from "react"; 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; const max_x = 500;
@@ -20,12 +21,14 @@ export function DrawGraph({
}, },
matchSelect = null, matchSelect = null,
matchNext = null, matchNext = null,
size = 24 size = 24,
cards = []
}) { }) {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const actionCanvasRef = useRef(null); const actionCanvasRef = useRef(null);
const ctxARef = useRef(null); const ctxARef = useRef(null);
const actionMapRef = useRef({}); const actionMapRef = useRef({});
const {getHeightCardForCombInMatch} = useCardsStatic(cards);
const selectColor = "#30cc30"; const selectColor = "#30cc30";
@@ -149,6 +152,40 @@ export function DrawGraph({
ctx.restore(); 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 newColor = () => {
const letters = '0123456789ABCDEF' const letters = '0123456789ABCDEF'
let color let color
@@ -182,7 +219,9 @@ export function DrawGraph({
printScores(ctx, match.scores, px, py, 1); 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)} 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" ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true) printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
ctxA.fillStyle = newColor() ctxA.fillStyle = newColor()
@@ -190,6 +229,7 @@ export function DrawGraph({
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1} 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)} 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" ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true) printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
ctxA.fillStyle = newColor() ctxA.fillStyle = newColor()
@@ -228,6 +268,7 @@ export function DrawGraph({
printScores(ctx, match.scores, px, py, 1.5); 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)} 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" ctx.fillStyle = "#FF0000"
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true) printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
ctxA.fillStyle = newColor() ctxA.fillStyle = newColor()
@@ -235,6 +276,7 @@ export function DrawGraph({
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1} 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)} 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" ctx.fillStyle = "#0000FF"
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true) printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
ctxA.fillStyle = newColor() ctxA.fillStyle = newColor()
@@ -310,7 +352,7 @@ export function DrawGraph({
for (const node of root) { for (const node of root) {
let win_name = ""; let win_name = "";
if (node.data.end) { 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.c1FullName === null ? "???" : node.data.c1FullName)
: (node.data.c2FullName === null ? "???" : node.data.c2FullName); : (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> <canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "relative", opacity: 1}} id="myCanvas"></canvas>
</div> </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;
}

View File

@@ -14,6 +14,7 @@ function CupImg() {
style={{width: "16px"}} src="/img/171891.png" style={{width: "16px"}} src="/img/171891.png"
alt=""/> alt=""/>
} }
function CupImg2() { function CupImg2() {
return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635" return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png" style={{width: "16px"}} src="/img/171892.png"
@@ -38,7 +39,8 @@ export function ResultView() {
{resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/> {resultShow && resultShow === "cat" && <CategoryList uuid={uuid}/>
|| resultShow && resultShow === "club" && <ClubList uuid={uuid}/> || resultShow && resultShow === "club" && <ClubList uuid={uuid}/>
|| resultShow && resultShow === "comb" && <CombList uuid={uuid}/> || resultShow && resultShow === "comb" && <CombList uuid={uuid}/>
|| resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>} || resultShow && resultShow === "combs" && <CombsResult uuid={uuid}/>
|| resultShow && resultShow === "clubs" && <ClubsResult uuid={uuid}/>}
</div> </div>
</div> </div>
</> </>
@@ -65,6 +67,10 @@ function MenuBar({resultShow, setResultShow}) {
<a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")} <a className={"nav-link my-1" + (resultShow === "combs" ? " active" : "")} aria-current={(resultShow === "combs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a> href="#" onClick={_ => setResultShow("combs")}>{t('combattants')}</a>
</li> </li>
<li className="nav-item">
<a className={"nav-link my-1" + (resultShow === "clubs" ? " active" : "")} aria-current={(resultShow === "clubs" ? " page" : "false")}
href="#" onClick={_ => setResultShow("clubs")}>{t('classementClub')}</a>
</li>
</ul> </ul>
/* /*
@@ -146,13 +152,52 @@ function BuildTree({treeData}) {
function initTree(data_in) { function initTree(data_in) {
let out = []; let out = [];
for (const din of data_in) { for (const din of data_in.filter(d => d.data.level >= 0)) {
out.push(parseTree(din)); out.push(parseTree(din));
} }
return out; return out;
} }
return <DrawGraph root={initTree(treeData)}/> return <>
<DrawGraph root={initTree(treeData)}/>
{treeData.some(d => d.data.level <= -10) &&
<BuildMatchArray matchs={treeData.filter(d => d.data.level <= -10).reverse().map(d => ({
red: d.data.c1FullName,
blue: d.data.c2FullName,
score: d.data.scores,
red_w: d.data.win > 0,
blue_w: d.data.win < 0,
eq: d.data.win === 0
}))}/>}
</>
}
function ClassementList({classement}) {
const {t} = useTranslation('result');
const classement2 = classement.sort((a, b) => {
if (a.rank === b.rank)
return a.name.localeCompare(b.name);
return a.rank - b.rank;
})
return <>
<h3 style={{marginTop: "2em"}}>{t('classementFinal')}</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>{t('place')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('nom')}</th>
</tr>
</thead>
<tbody>
{classement2.map((row, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{row.rank}</td>
<td style={{textAlign: "left"}}>{row.name}</td>
</tr>)}
</tbody>
</table>
</>
} }
function CategoryList({uuid}) { function CategoryList({uuid}) {
@@ -212,7 +257,7 @@ function CategoryResult({uuid, catId}) {
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} href="#" <a className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")} href="#"
onClick={_ => setType(2)}>{t('tournois')}</a> onClick={_ => setType(2)}>{data.treeIsClassement ? t('classement') : t('tournois')}</a>
</li> </li>
</ul> </ul>
</>} </>}
@@ -227,6 +272,7 @@ function CategoryResult({uuid, catId}) {
{type === 2 && <> {type === 2 && <>
<BuildTree treeData={data.trees}/> <BuildTree treeData={data.trees}/>
{data.treeIsClassement && <ClassementList classement={data.classement}/>}
</>} </>}
</> </>
@@ -409,6 +455,43 @@ function CombResult({uuid, combId}) {
</div> </div>
} }
function ClubsResult({uuid}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/club/classement`, setLoading, 1)
const {t} = useTranslation('result');
return <>
{data ? <>
<h3>{t('classementDesClub')} :</h3>
<table className="table table-striped">
<thead>
<tr>
<th scope="col" style={{textAlign: "center"}}>{t('club')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('1er')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('2eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('3eme')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('scores')}</th>
</tr>
</thead>
<tbody>
{data.map((club, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{club.name}</td>
<td style={{textAlign: "center"}}>{club.score[0]}</td>
<td style={{textAlign: "center"}}>{club.score[1]}</td>
<td style={{textAlign: "center"}}>{club.score[2]}</td>
<td style={{textAlign: "center"}}>{club.tt_score}</td>
</tr>)}
</tbody>
</table>
</>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function CombsResult({uuid}) { function CombsResult({uuid}) {
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1)

View File

@@ -1,4 +1,4 @@
import {scorePrint} from "./Tools.js"; import {scorePrint, virtualScore} from "./Tools.js";
export function scoreToString(score) { export function scoreToString(score) {
if (score === null || score === undefined || score.length === 0) if (score === null || score === undefined || score.length === 0)
@@ -10,6 +10,18 @@ export function scoreToString(score) {
return score.sort((a, b) => a.n_round - b.n_round).map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ") return score.sort((a, b) => a.n_round - b.n_round).map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ")
} }
export function scoreToString2(match, cards = []) {
if (!match || match.scores === null || match.scores === undefined || match.scores.length === 0)
return ""
if (Array.isArray(match.scores[0]))
return match.scores.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ")
else {
return match.scores.sort((a, b) => a.n_round - b.n_round).map(o =>
scorePrint(virtualScore(match.c1, o, match, cards)) + "-" + scorePrint(virtualScore(match.c2, o, match, cards))).join(" | ")
}
}
export function createMatch(category, matchs, groups) { export function createMatch(category, matchs, groups) {
const combs_2 = {} const combs_2 = {}
for (const group of groups) { for (const group of groups) {

View File

@@ -56,8 +56,10 @@ export function MarchReducer(datas, action) {
return datas.sort(action.payload) return datas.sort(action.payload)
case 'REORDER': case 'REORDER':
const oldIndex = datas.findIndex(data => data.id === action.payload.id) const oldIndex = datas.findIndex(data => data.id === action.payload.id)
if (oldIndex === -1 || datas[oldIndex].categorie_ord === action.payload.pos) if (oldIndex === -1)
return datas // Do nothing return datas // Do nothing
if (datas[oldIndex].categorie_ord === action.payload.pos)
return [...datas] // Do nothing
const oldPos = datas[oldIndex].categorie_ord const oldPos = datas[oldIndex].categorie_ord
const newPos = action.payload.pos const newPos = action.payload.pos

View File

@@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import i18n from "../config/i18n.js"; import i18n from "../config/i18n.js";
import {compareCardOrder, hasEffectCard} from "../hooks/useCard.jsx";
const vite_url = import.meta.env.VITE_URL; const vite_url = import.meta.env.VITE_URL;
@@ -21,8 +22,18 @@ export function isClubAdmin(userinfo) {
export const errFormater = (data, msg) => { export const errFormater = (data, msg) => {
if (!data) if (!data)
return msg return msg
if (!data.response) if (!data.response) {
return `${msg} (😕 ${data.message})` if (data.message)
return `${msg} (${data.message} 😕)`
if (typeof data === 'string' || data instanceof String) {
if (/.*D[a-zA-Z]+Exception:.*/.test(data))
return `${msg} (${data.split(":").slice(1).join(":").trim()} 😕)`
else
return `${msg}`
}
return `${msg} (${data} 😕)`
}
if (typeof data.response.data === 'string' || data.response.data instanceof String) if (typeof data.response.data === 'string' || data.response.data instanceof String)
return `${msg} (${data.response.statusText}: ${data.response.data}) 😕` return `${msg} (${data.response.statusText}: ${data.response.data}) 😕`
return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕` return `${msg} (${data.response.statusText}: ${JSON.stringify(data.response.data)}) 😕`
@@ -106,6 +117,14 @@ export function getSaison(currentDate = new Date()) {
} }
} }
export function getFirstDateOfSaison() {
const year = getSaison();
const firstDate = new Date(year, 8, 1); // 8 = septembre, 1 = premier jour
firstDate.setHours(0, 0, 0, 0); // Heure, minutes, secondes, millisecondes à 0
return firstDate;
}
export function getCatName(cat) { export function getCatName(cat) {
switch (cat) { switch (cat) {
case "SUPER_MINI": case "SUPER_MINI":
@@ -137,6 +156,35 @@ export function getCatName(cat) {
} }
} }
export function getCatFromName(name) {
switch (name.toLowerCase()) {
case i18n.t('cat.superMini').toLowerCase():
return "SUPER_MINI";
case i18n.t('cat.miniPoussin').toLowerCase():
return "MINI_POUSSIN";
case i18n.t('cat.poussin').toLowerCase():
return "POUSSIN";
case i18n.t('cat.benjamin').toLowerCase():
return "BENJAMIN";
case i18n.t('cat.minime').toLowerCase():
return "MINIME";
case i18n.t('cat.cadet').toLowerCase():
return "CADET";
case i18n.t('cat.junior').toLowerCase():
return "JUNIOR";
case i18n.t('cat.senior1').toLowerCase():
return "SENIOR1";
case i18n.t('cat.senior2').toLowerCase():
return "SENIOR2";
case i18n.t('cat.vétéran1').toLowerCase():
return "VETERAN1";
case i18n.t('cat.vétéran2').toLowerCase():
return "VETERAN2";
default:
return name;
}
}
export const SwordList = [ export const SwordList = [
"NONE", "NONE",
"ONE_HAND", "ONE_HAND",
@@ -159,6 +207,38 @@ export function getSwordTypeName(type) {
} }
} }
export function getSwordSize(type, cat, gender) {
const catIndex = CatList.indexOf(cat)
if (type === "ONE_HAND") {
if (catIndex <= 1) {
return "XS"
} else if (catIndex <= 3) {
return "S"
} else if (catIndex <= 6 || gender === "F") {
return "M"
} else {
return "L"
}
} else if (type === "TWO_HAND") {
if (catIndex <= 5) {
return "--"
} else if (catIndex <= 6 || gender === "F") {
return "XL"
} else {
return "XXL"
}
} else if (type === "SABER") {
if (catIndex <= 3) {
return "--"
} else if (catIndex <= 6) {
return "S"
} else {
return "L"
}
}
return "--"
}
export const ShieldList = [ export const ShieldList = [
"NONE", "NONE",
"STANDARD", "STANDARD",
@@ -184,6 +264,18 @@ export function getShieldTypeName(type) {
} }
} }
export function getShieldSize(type, cat) {
if (type === "STANDARD") {
const catIndex = CatList.indexOf(cat)
if (catIndex <= 1) {
return "S"
} else {
return "L"
}
}
return "--"
}
export function getToastMessage(msgKey, ns = 'common') { export function getToastMessage(msgKey, ns = 'common') {
return { return {
pending: i18n.t(msgKey + '.pending', {ns}), pending: i18n.t(msgKey + '.pending', {ns}),
@@ -196,9 +288,18 @@ export function getToastMessage(msgKey, ns = 'common') {
} }
} }
export function win(scores) { export function win(match, cards = []) {
const cards2 = cards.filter(c => (c.comb === match.c1 || c.comb === match.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder).pop()
if (cards2) {
if (cards2.comb === match.c1)
return 1
else
return -1
}
let sum = 0 let sum = 0
for (const score of scores) { for (const score of match?.scores) {
if (score.s1 === -1000 || score.s2 === -1000) continue if (score.s1 === -1000 || score.s2 === -1000) continue
if (score.s1 > score.s2) sum++ if (score.s1 > score.s2) sum++
else if (score.s1 < score.s2) sum-- else if (score.s1 < score.s2) sum--
@@ -206,7 +307,61 @@ export function win(scores) {
return sum return sum
} }
export function win_end(match, cards = []) {
const cards2 = cards?.filter(c => (c.comb === match.c1 || c.comb === match.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder)
if (cards2.length > 1) {
if (cards2[0].comb !== cards2[1].comb)
return {win: 0, end: true}
}
if (cards2.length > 0) {
if (cards2[0].comb === match.c1)
return {win: -1, end: true}
else
return {win: 1, end: true}
}
let sum = 0
for (const score of match?.scores) {
if (score.s1 === -1000 || score.s2 === -1000) continue
if (score.s1 > score.s2) sum++
else if (score.s1 < score.s2) sum--
}
return {win: sum, end: match?.end}
}
export function virtual_end(match, cards) {
if (!cards)
return false
return cards.some(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie))
}
export function virtualScore(combId, score, match, cards) {
const cards2 = cards?.filter(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie)).sort(compareCardOrder)
if (cards2.length > 1) {
if (cards2[0].comb !== cards2[1].comb)
return -997
}
if (cards2.length > 0) {
if (cards2[0].comb === combId)
return -997
else
return 10
}
if (combId === match?.c1) {
return score.s1
} else if (combId === match?.c2) {
return score.s2
} else {
return ""
}
}
export function scorePrint(s1) { export function scorePrint(s1) {
switch (s1) { switch (s1) {
case -997: case -997:
return "disc." return "disc."
@@ -271,3 +426,29 @@ export function hex2rgb(hex) {
// return {r, g, b} // return {r, g, b}
return {r, g, b}; return {r, g, b};
} }
export function toDataURL(src, outputFormat) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
const canvas = document.createElement('CANVAS');
const ctx = canvas.getContext('2d');
let dataURL;
canvas.height = this.naturalHeight;
canvas.width = this.naturalWidth;
ctx.drawImage(this, 0, 0);
dataURL = canvas.toDataURL(outputFormat);
resolve(dataURL);
};
img.onerror = function() {
reject(new Error('Could not load image at ' + src));
}
img.src = src;
if (img.complete || img.complete === undefined) {
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.src = src;
}
});
}

View File

@@ -0,0 +1,502 @@
import {jsPDF} from 'jspdf'
import {useCardsStatic} from "../hooks/useCard.jsx";
import {
CatList,
getCatName,
getShieldSize,
getShieldTypeName,
getSwordSize,
getSwordTypeName,
sortCategories,
timePrint,
virtualScore,
win_end
} from "./Tools.js";
import {getMandatoryProtectionsList} from "../components/ProtectionSelector.jsx";
import {scoreToString2} from "./CompetitionTools.js";
import {TreeNode} from "./TreeUtils.js";
import {drawGraphForPdf} from "../pages/result/DrawGraph.jsx";
import autoTable from 'jspdf-autotable'
function combName(getComb, combId) {
if (!combId)
return " "
const comb = getComb(combId, null);
if (comb) {
if (comb.lname === "__team")
return `${comb.fname}`
return `${comb.fname} ${comb.lname}`
} else {
return `[Comb #${combId}]`
}
}
export function makePDF(action, pagesList, name, c_name, getComb, t, logo) {
//https://github.com/parallax/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L59
const doc = new jsPDF('p', 'pt', 'a4');
doc.setProperties({title: name, author: "FFSAF - Intranet", subject: c_name + " - " + name, creator: "FFSAF - Intranet"});
for (let i = 0; i < pagesList.length; i++) {
const context = {pdf_doc: doc, pdf_name: name, c_name, getComb, t, ...pagesList[i].params, logo}
switch (pagesList[i].type) {
case "categorie":
generateCategoriePDF(context);
break;
case "podium":
generatePodium(context);
break;
case "podiumClub":
generateClubPodium(context);
break;
default:
break
}
if (i !== pagesList.length - 1)
doc.addPage();
}
switch (action) {
case "show":
window.open(doc.output('bloburl', {filename: name + '.pdf'}));
break;
case "print":
const iframe = document.createElement('iframe'); //load content in an iframe to print later
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.src = doc.output('bloburl', {filename: name + '.pdf'});
iframe.onload = function () {
setTimeout(function () {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
break;
case "download":
default:
doc.save(name + '.pdf');
break;
}
}
function matchList(pdf_doc, matches, cat, cards_v, classement, getComb, t) {
const {getHeightCardForCombInMatch} = useCardsStatic(cards_v);
const liceName = (cat.liceName || "N/A").split(";");
const getBG = (combId, match) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return ""
let bg = "";
let text = "#000";
switch (c.type) {
case "YELLOW":
bg = "#ffc107";
break;
case "RED":
bg = "#dc3545";
text = "#FFF";
break;
case "BLACK":
bg = "#000000";
text = "#FFF";
break;
case "BLUE":
bg = "#0d6efd";
text = "#FFF";
break;
}
return {fillColor: bg, textColor: text}
}
const head = [
...(classement ? [
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
] : [
{content: t('no'), styles: {halign: "center"}},
{content: t('poule'), styles: {halign: "center"}},
{content: t('zone'), styles: {halign: "center"}},
]),
{content: "", styles: {halign: "center"}},
{content: t('rouge'), styles: {halign: "center"}},
{content: t('résultat'), styles: {halign: "center"}},
{content: t('blue'), styles: {halign: "center"}},
{content: "", styles: {halign: "center"}},
]
const body = matches.map((m, index) => ([
...(classement ? [
{content: `${m.categorie_ord + 1}-${m.categorie_ord + 2}`, styles: {halign: "center", padding: "2pt 0"}}
] : [
{content: index + 1, styles: {halign: "center", padding: "2pt 0"}},
{content: m.poule, styles: {halign: "center"}},
{content: liceName[index % liceName.length], styles: {halign: "center"}},
]),
{content: m.end ? (m.win > 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c1, m)}},
{content: combName(getComb, m.c1), styles: {halign: "center", minWidth: "11em", paddingLeft: "0.2em"}},
{content: scoreToString2(m, cards_v), styles: {halign: "center"}},
{content: combName(getComb, m.c2), styles: {halign: "center", minWidth: "11em", paddingRight: "0.2em"}},
{content: m.end ? (m.win < 0 ? "X" : (m.win === 0 ? "-" : "")) : " ", styles: {halign: "center", ...getBG(m.c2, m)}},
]))
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: classement ? {
0: {cellWidth: 35},
1: {cellWidth: 15},
2: {cellWidth: "auto"},
3: {cellWidth: 110},
4: {cellWidth: "auto"},
5: {cellWidth: 15},
} : {
0: {cellWidth: 20},
1: {cellWidth: 35},
2: {cellWidth: 30},
3: {cellWidth: 15},
4: {cellWidth: "auto"},
5: {cellWidth: 110},
6: {cellWidth: "auto"},
7: {cellWidth: 15},
},
head: [head],
body: body,
theme: 'grid',
})
}
function buildTree(pdf_doc, treeData, treeRaw, matches, cat, cards_v, getComb, t, categorieEmpty, comb_count = 0) {
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = categorieEmpty ? null : getComb(matchData?.c1)
const c2 = categorieEmpty ? null : 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)
return node
}
let out = []
for (let i = 0; i < treeRaw.length; i++) {
if (treeRaw.at(i).level > -10) {
out.push(parseTree(treeData.at(i)))
}
}
const canvas = drawGraphForPdf(out, 24, cards_v)
const imgData = canvas.toDataURL('image/png');
const height = canvas.height * (pdf_doc.internal.pageSize.getWidth() - 80) / canvas.width;
pdf_doc.addImage(imgData, 'PNG', 40, pdf_doc.lastAutoTable.finalY, pdf_doc.internal.pageSize.getWidth() - 80, height);
pdf_doc.lastAutoTable.finalY += height;
if (cat.fullClassement) {
let size = 0;
if (out.length > 0)
size = out[0].getMaxChildrenAtDepth(out[0].death() - 1)
const matches2 = cat.raw_trees?.filter(n => n.level <= -10).reverse().map(d => categorieEmpty ? ({}) : matches.find(m => m.id === d.match?.id))
if (matches2.length === 0) {
while (Math.ceil(comb_count / 2) - size > matches2.length)
matches2.push({})
}
matches2.forEach((v, i) => {
v.categorie_ord = (i + size) * 2
})
matchList(pdf_doc, matches2, cat, cards_v, true, getComb, t)
}
}
function pouleList(pdf_doc, groups, getComb, t) {
const groups2 = groups.map(g => {
const comb = getComb(g.id);
return {...g, name: comb ? comb.fname + " " + comb.lname : "", teamMembers: comb ? comb.teamMembers : []};
}).sort((a, b) => {
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;
if (!acc[poule]) {
acc[poule] = [];
}
acc[poule].push(curr);
return acc;
}, {});
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
head: [Object.keys(groups2).map((poule) => ({content: `${t('poule')} ${poule}`, styles: {halign: "center"}}))],
body: [Object.keys(groups2).map((poule) => ({
content: groups2[poule].map(o => o.name).join(", "),
styles: {halign: "center", valign: "middle"}
}))],
theme: 'grid',
})
}
function makeHeader(pdf_doc, c_name, p_name, logo) {
autoTable(pdf_doc, {
startY: 20,
body: [[
{content: "", src: `${logo}`, rowSpan: 2, styles: {halign: 'center', valign: 'middle'}},
{content: c_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}},
], [
{content: p_name, colSpan: 3, styles: {halign: "center", valign: 'middle', fontSize: 20}}
]],
theme: 'plain',
columnStyles: {
0: {cellWidth: 60, minCellHeight: 60},
},
didDrawCell: function (data) {
if (data.column.index === 0 && data.row.index === 0) {
const dim = data.cell.height - data.cell.padding('vertical');
const textPos = data.cell;
pdf_doc.addImage(data.cell.raw.src, textPos.x, textPos.y + data.cell.padding('vertical') / 2, dim, dim);
}
}
})
}
function generateCategoriePDF({pdf_doc, cat, matches, groups, getComb, cards_v, c_name, categorieEmpty, t, logo}) {
let catAverage = "---";
let genreAverage = "H";
let nbComb = 0;
let time = 0;
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => categorieEmpty ? ({...m, end: false, scores: []}) : m)
if (marches2.length !== 0) {
const genres = [];
const cats = [];
const combs_ = [];
for (const m of marches2) {
if (m.c1 && !combs_.includes(m.c1))
combs_.push(m.c1);
if (m.c2 && !combs_.includes(m.c2))
combs_.push(m.c2);
}
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);
nbComb = combs_.length;
catAverage = CatList.at(catAvg) || "---";
genreAverage = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length) > 0.5 ? "F" : "H";
if (cat.preset && cat.preset.categories) {
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 {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) ===
catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a));
}
time = {round: p.roundDuration, pause: p.pauseDuration}
}
}
makeHeader(pdf_doc, c_name, cat.name, logo)
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY,
styles: {fontSize: 10, cellPadding: 3},
body: [[
{content: `${t('catégorieDâgeMoyenne')} : ${getCatName(catAverage)}`, styles: {halign: "left"}},
{
content: `${t('arme', {ns: 'common'})} : ${getSwordTypeName(cat.preset?.sword)} - ${t('taille')} ${getSwordSize(cat.preset?.sword, catAverage, genreAverage)}`,
styles: {halign: "left"}
},
{
content: `${t('bouclier', {ns: 'common'})} : ${getShieldTypeName(cat.preset?.shield)} - ${t('taille')} ${getShieldSize(cat.preset?.shield, catAverage)}`,
styles: {halign: "left"}
},
], [
{content: `${t('duréeRound')} : ${timePrint(time.round)}`, styles: {halign: "left"}},
{content: `${t('duréePause')} : ${timePrint(time.pause)}`, styles: {halign: "left"}},
{content: `${t('nombreDeCombattants')} : ${nbComb}`, styles: {halign: "left"}},
], [
{
content: `${t('protectionObligatoire', {ns: 'common'})} ${getMandatoryProtectionsList(CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
cat.preset?.mandatoryProtection1 : cat.preset?.mandatoryProtection2, cat.preset?.shield, t).join(", ") || "---"}`,
colSpan: 3,
styles: {halign: "left"}
},
]],
theme: 'grid',
})
pouleList(pdf_doc, groups, getComb, t)
if ((cat.type & 1) === 1)
matchList(pdf_doc, marches2.filter(m => m.categorie_ord !== -42).sort((a, b) => a.categorie_ord - b.categorie_ord),
cat, categorieEmpty ? [] : cards_v, false, getComb, t)
if ((cat.type & 2) === 2) {
pdf_doc.setFontSize(12);
pdf_doc.text((cat.treeAreClassement ? t('classement') : t('tournois')) + ':', 40, pdf_doc.lastAutoTable.finalY + 16)
pdf_doc.lastAutoTable.finalY += 16;
buildTree(pdf_doc, cat.trees, cat.raw_trees, marches2, cat, categorieEmpty ? [] : cards_v, getComb, t, categorieEmpty, nbComb)
}
}
function generatePodium({pdf_doc, data, t, logo, c_name, minRank = 4, maxRank = 4}) {
makeHeader(pdf_doc, c_name, t('podium'), logo)
const data2 = data.sort((a, b) => {
let tmp = sortCategories(a.categorie, b.categorie);
if (tmp !== 0)
return tmp;
return a.poule_name.localeCompare(b.poule_name)
})
let finalY2 = pdf_doc.lastAutoTable.finalY;
let finalY3 = pdf_doc.lastAutoTable.finalY;
for (let i = 0; i < data2.length; i++) {
const p = data2[i];
let pageNumber = pdf_doc.internal.getNumberOfPages()
const body = Array.from({length: minRank}, () => []);
for (const c of p.podium) {
if (c.rank > maxRank)
continue;
if (body[c.rank - 1])
body[c.rank - 1].push(c.name)
else
body[c.rank - 1] = [c.name]
}
for (let j = 0; j < body.length; j++) {
if (body[j].length === 0)
body[j].push(" ")
body[j] = [
{content: j + 1, styles: {halign: "center"}},
{content: body[j].join(", "), styles: {halign: "center"}}
]
}
autoTable(pdf_doc, {
startY: finalY3 + 7,
margin: i % 2 ? {left: pdf_doc.internal.pageSize.getWidth() / 2 + 7} : {right: pdf_doc.internal.pageSize.getWidth() / 2 + 7},
styles: {fontSize: 10, cellPadding: 3},
columnStyles: {
0: {cellWidth: 35},
1: {cellWidth: "auto"},
},
pageBreak: "avoid",
showHead: 'firstPage',
head: [[
{content: p.poule_name, colSpan: 2, styles: {halign: "center"}},
], [
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
{content: t('combattants', {ns: 'result'}), styles: {halign: "center"}},
]],
body: body,
foot: [[
{content: t('source') + " : " + p.source, colSpan: 2, styles: {halign: "left", fontSize: 8}},
]],
rowPageBreak: 'auto',
theme: 'grid',
})
if (i % 2 === 0) {
finalY2 = pdf_doc.lastAutoTable.finalY;
if (pageNumber !== pdf_doc.internal.getNumberOfPages())
finalY3 = 33
} else {
pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY);
finalY3 = pdf_doc.lastAutoTable.finalY;
}
}
pdf_doc.lastAutoTable.finalY = Math.max(finalY2, pdf_doc.lastAutoTable.finalY);
}
function generateClubPodium({pdf_doc, data, t, logo, c_name}) {
makeHeader(pdf_doc, c_name, t('podiumDesClubs'), logo)
const body = data.map(c => [
{content: [c.name], styles: {halign: "center"}},
{content: c.score[0], styles: {halign: "center"}},
{content: c.score[1], styles: {halign: "center"}},
{content: c.score[2], styles: {halign: "center"}},
{content: c.tt_score, styles: {halign: "center"}},
]);
let rank = 0;
let lastScores = null;
let lastB = null;
for (const b of body) {
const scores = [b[1].content, b[2].content, b[3].content, b[4].content];
if (lastScores !== scores) {
rank++;
b.unshift({content: rank, styles: {halign: "center"}})
lastB = b
} else {
lastB[1].content.push(b[0].content);
delete body.indexOf(b);
}
}
for (const b of body) {
b[1].content = b[1].content.join(", ")
}
autoTable(pdf_doc, {
startY: pdf_doc.lastAutoTable.finalY + 7,
styles: {fontSize: 10, cellPadding: 3},
columnStyles: {
0: {cellWidth: 35},
1: {cellWidth: "auto"},
2: {cellWidth: 45},
3: {cellWidth: 45},
4: {cellWidth: 45},
5: {cellWidth: 40},
},
pageBreak: "avoid",
showHead: 'firstPage',
head: [[
{content: t('place', {ns: "result"}), styles: {halign: "center"}},
{content: t('club', {ns: 'result'}), styles: {halign: "center"}},
{content: t('1er', {ns: 'result'}), styles: {halign: "center"}},
{content: t('2eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('3eme', {ns: 'result'}), styles: {halign: "center"}},
{content: t('scores', {ns: 'result'}), styles: {halign: "center"}},
]],
body: body,
rowPageBreak: 'auto',
theme: 'grid',
})
}