diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java index ad2a032..e018e2a 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/MatchModel.java @@ -63,7 +63,7 @@ public class MatchModel { @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "match", referencedColumnName = "id") - List cardboard; + List cardboard = new ArrayList<>(); public String getC1Name() { if (c1_id == null) diff --git a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java index c2bbe97..46044b7 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java +++ b/src/main/java/fr/titionfire/ffsaf/data/repository/MatchRepository.java @@ -7,6 +7,8 @@ import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; + @ApplicationScoped public class MatchRepository implements PanacheRepositoryBase { @@ -16,4 +18,13 @@ public class MatchRepository implements PanacheRepositoryBase .invoke(matchModel1 -> matchModel1.setSystemId(matchModel1.getId()))) .chain(this::persist); } + + public Uni create(List matchModel) { + matchModel.forEach(model -> model.setSystem(CompetitionSystem.INTERNAL)); + return Panache.withTransaction(() -> this.persist(matchModel) + .call(__ -> this.flush()) + .invoke(__ -> matchModel.forEach(model -> model.setSystemId(model.getId()))) + .map(__ -> matchModel)) + .chain(this::persist); + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index a11a7fd..816cbea 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -12,8 +12,8 @@ import fr.titionfire.ffsaf.domain.entity.TreeEntity; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.utils.TreeNode; -import fr.titionfire.ffsaf.ws.CompetitionWS; import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSCategorie; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -97,8 +97,7 @@ public class RCategorie { return categoryRepository.create(categoryModel); }) - .call(cat -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendAddCategory", - JustCategorie.from(cat))) + .call(cat -> SSCategorie.sendAddCategory(connection, cat)) .map(CategoryModel::getId); } @@ -121,7 +120,7 @@ public class RCategorie { } return uni; }) - .call(cat -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendCategory", JustCategorie.from(cat))) + .call(cat -> SSCategorie.sendCategory(connection, cat)) .replaceWithVoid(); } @@ -202,13 +201,13 @@ public class RCategorie { .call(__ -> treeRepository.flush()) .call(cat -> treeRepository.list("category = ?1 AND level != 0", cat.getId()) .map(treeModels -> treeModels.stream().map(TreeEntity::fromModel).toList()) - .chain(trees -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", trees))) + .chain(trees -> SSCategorie.sendTreeCategory(connection, trees))) .replaceWithVoid(); } @RegisterForReflection public record JustCategorie(long id, String name, int type, String liceName) { - static JustCategorie from(CategoryModel m) { + public static JustCategorie from(CategoryModel m) { return new JustCategorie(m.getId(), m.getName(), m.getType(), m.getLiceName()); } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index 69c1b9d..85e8872 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -1,14 +1,16 @@ package fr.titionfire.ffsaf.ws.recv; +import fr.titionfire.ffsaf.data.model.CategoryModel; import fr.titionfire.ffsaf.data.model.MatchModel; +import fr.titionfire.ffsaf.data.repository.CategoryRepository; import fr.titionfire.ffsaf.data.repository.CombRepository; import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository; import fr.titionfire.ffsaf.data.repository.MatchRepository; import fr.titionfire.ffsaf.domain.entity.MatchEntity; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; -import fr.titionfire.ffsaf.ws.CompetitionWS; import fr.titionfire.ffsaf.ws.PermLevel; +import fr.titionfire.ffsaf.ws.send.SSMatch; import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -19,6 +21,11 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.jboss.logging.Logger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + @WithSession @ApplicationScoped @RegisterForReflection @@ -31,6 +38,9 @@ public class RMatch { @Inject CombRepository combRepository; + @Inject + CategoryRepository categoryRepository; + @Inject CompetitionGuestRepository competitionGuestRepository; @@ -44,12 +54,38 @@ public class RMatch { })); } - @WSReceiver(code = "getAllMatch") - public Uni getAllMatch(WebSocketConnection connection, Long l) { - LOGGER.info("getAllMatch " + l); + private Uni creatMatch(CategoryModel categoryModel, AddMatch m) { + return Uni.createFrom().item(() -> { + MatchModel matchModel = new MatchModel(); - return Uni.createFrom().item(l); - //return matchRepository.count(); + matchModel.setCategory(categoryModel); + matchModel.setCategory_ord(m.categorie_ord); + matchModel.setEnd(false); + matchModel.setPoule(m.poule); + + return matchModel; + }) + .call(mm -> m.c1() >= 0 ? + combRepository.findById(m.c1()).invoke(mm::setC1_id) : + competitionGuestRepository.findById(m.c1() * -1).invoke(mm::setC1_guest)) + .call(mm -> m.c2() >= 0 ? + combRepository.findById(m.c2()).invoke(mm::setC2_id) : + competitionGuestRepository.findById(m.c2() * -1).invoke(mm::setC2_guest)); + } + + @WSReceiver(code = "addMatch", permission = PermLevel.ADMIN) + public Uni addMatch(WebSocketConnection connection, AddMatch m) { + return categoryRepository.findById(m.categorie) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Catégorie non trouver"); + if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) + throw new DForbiddenException("Permission denied"); + })) + .chain(categoryModel -> creatMatch(categoryModel, m)) + .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))) + .call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm))) + .replaceWithVoid(); } @WSReceiver(code = "updateMatchComb", permission = PermLevel.ADMIN) @@ -85,12 +121,84 @@ public class RMatch { mm.setC2_guest(null); })) .chain(mm -> Panache.withTransaction(() -> matchRepository.persist(mm))) - .call(mm -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatch", - MatchEntity.fromModel(mm))) + .call(mm -> SSMatch.sendMatch(connection, MatchEntity.fromModel(mm))) + .replaceWithVoid(); + } + + @WSReceiver(code = "updateMatchOrder", permission = PermLevel.ADMIN) + public Uni updateMatchComb(WebSocketConnection connection, MatchOrder order) { + return getById(order.id(), connection) + .call(m -> matchRepository.update( + "category_ord = category_ord + 1 WHERE category_ord >= ?1 AND category_ord < ?2", order.pos, + m.getCategory_ord())) + .call(m -> matchRepository.update( + "category_ord = category_ord - 1 WHERE category_ord <= ?1 AND category_ord > ?2", order.pos, + m.getCategory_ord())) + .invoke(m -> m.setCategory_ord(order.pos)) + .call(m -> Panache.withTransaction(() -> matchRepository.persist(m))) + .call(mm -> SSMatch.sendMatchOrder(connection, order)) + .replaceWithVoid(); + } + + @WSReceiver(code = "deleteMatch", permission = PermLevel.ADMIN) + public Uni deleteMatch(WebSocketConnection connection, Long idMatch) { + return getById(idMatch, connection) + .chain(matchModel -> Panache.withTransaction(() -> matchRepository.delete(matchModel))) + .call(__ -> SSMatch.sendDeleteMatch(connection, idMatch)) + .replaceWithVoid(); + } + + @WSReceiver(code = "recalculateMatch", permission = PermLevel.ADMIN) + public Uni recalculateMatch(WebSocketConnection connection, RecalculateMatch data) { + ArrayList matches = new ArrayList<>(); + return categoryRepository.findById(data.categorie) + .invoke(Unchecked.consumer(o -> { + if (o == null) + throw new DNotFoundException("Catégorie non trouver"); + if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) + throw new DForbiddenException("Permission denied"); + })) + .call(cm -> matchRepository.delete("id IN ?1 AND category = ?2", data.matchesToRemove, cm) + .call(__ -> SSMatch.sendDeleteMatch(connection, data.matchesToRemove))) + .call(cm -> matchRepository.list("id IN ?1 AND category = ?2", + Stream.concat(data.matchOrderToUpdate.keySet().stream(), + data.matchPouleToUpdate.keySet().stream()) + .distinct().toList(), cm) + .invoke(matchModels -> matchModels.forEach(model -> { + if (data.matchPouleToUpdate.containsKey(model.getId())) + model.setPoule(data.matchPouleToUpdate.get(model.getId())); + if (data.matchOrderToUpdate.containsKey(model.getId())) + model.setCategory_ord(data.matchOrderToUpdate.get(model.getId())); + })) + .call(mm -> Panache.withTransaction(() -> matchRepository.persist(mm))) + .invoke(mm -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList())) + ) + .chain(categoryModel -> { + Uni> uni = Uni.createFrom().item(new ArrayList<>()); + for (AddMatch match : data.newMatch) + uni = uni.call(l -> creatMatch(categoryModel, match).invoke(l::add)); + return uni; + } + ) + .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm)) + .invoke(__ -> matches.addAll(mm.stream().map(MatchEntity::fromModel).toList()))) + .call(__ -> SSMatch.sendMatch(connection, matches)) .replaceWithVoid(); } @RegisterForReflection public record MatchComb(long id, Long c1, Long c2) { } + + @RegisterForReflection + public record MatchOrder(long id, long pos) { + } + + @RegisterForReflection + public record AddMatch(long categorie, long categorie_ord, char poule, long c1, long c2) { + } + + public record RecalculateMatch(long categorie, List newMatch, HashMap matchOrderToUpdate, + HashMap matchPouleToUpdate, List matchesToRemove) { + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java new file mode 100644 index 0000000..23162b9 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java @@ -0,0 +1,33 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.data.model.CategoryModel; +import fr.titionfire.ffsaf.domain.entity.TreeEntity; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import fr.titionfire.ffsaf.ws.recv.RCategorie; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; + +import java.util.List; + +public class SSCategorie { + + public static Uni sendAddCategory(WebSocketConnection connection, CategoryModel category) { + return SSCategorie.sendAddCategory(connection, RCategorie.JustCategorie.from(category)); + } + + public static Uni sendAddCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendAddCategory", justCategorie); + } + + public static Uni sendCategory(WebSocketConnection connection, CategoryModel category) { + return SSCategorie.sendCategory(connection, RCategorie.JustCategorie.from(category)); + } + + public static Uni sendCategory(WebSocketConnection connection, RCategorie.JustCategorie justCategorie) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCategory", justCategorie); + } + + public static Uni sendTreeCategory(WebSocketConnection connection, List treeEntities) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSMatch.java new file mode 100644 index 0000000..b66911d --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSMatch.java @@ -0,0 +1,32 @@ +package fr.titionfire.ffsaf.ws.send; + +import fr.titionfire.ffsaf.domain.entity.MatchEntity; +import fr.titionfire.ffsaf.ws.CompetitionWS; +import fr.titionfire.ffsaf.ws.recv.RMatch; +import io.quarkus.websockets.next.WebSocketConnection; +import io.smallrye.mutiny.Uni; + +import java.util.List; + +public class SSMatch { + + public static Uni sendMatch(WebSocketConnection connection, MatchEntity matchEntity) { + return SSMatch.sendMatch(connection, List.of(matchEntity)); + } + + public static Uni sendMatch(WebSocketConnection connection, List matchEntities) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatch", matchEntities); + } + + public static Uni sendMatchOrder(WebSocketConnection connection, RMatch.MatchOrder matchOrder) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatchOrder", matchOrder); + } + + public static Uni sendDeleteMatch(WebSocketConnection connection, Long l) { + return SSMatch.sendDeleteMatch(connection, List.of(l)); + } + + public static Uni sendDeleteMatch(WebSocketConnection connection, List longs) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendDeleteMatch", longs); + } +} diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 9cf6137..67a1ffd 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -8,6 +8,9 @@ "name": "webapp", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", @@ -399,6 +402,55 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 0cc49fc..0e57209 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 951ad1c..9d1b713 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -1,11 +1,26 @@ import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; -import {useEffect, useReducer, useRef, useState} from "react"; +import React, {useEffect, useReducer, useRef, useState} from "react"; import {MarchReducer} from "../../../utils/MatchReducer.jsx"; import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx"; import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js"; import {DrawGraph} from "../../result/DrawGraph.jsx"; import {SelectCombModalContent} from "./SelectCombModalContent.jsx"; +import {createMatch, scoreToString, win} from "../../../utils/CompetitionTools.js"; + +import {DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core'; +import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable'; +import {useSortable} from '@dnd-kit/sortable'; +import {CSS} from '@dnd-kit/utilities'; +import {toast} from "react-toastify"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faTrash} from "@fortawesome/free-solid-svg-icons"; + +function CupImg() { + return +} export function CategoryContent({cat, catId, setCat}) { const setLoading = useLoadingSwitcher() @@ -43,21 +58,36 @@ export function CategoryContent({cat, catId, setCat}) { combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}}); } - const matchListener = ({data}) => { - 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 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)}}); - if (data.c1 !== null && !groupsRef.current.some(g => g.id === data.c1?.id)) - setGroups(prev => [...prev, {id: data.c1?.id, poule: data.poule}]); - if (data.c2 !== null && !groupsRef.current.some(g => g.id === data.c2?.id)) - setGroups(prev => [...prev, {id: data.c2?.id, poule: data.poule}]); + if (data.c1 !== null && !groupsRef.current.some(g => g.id === data.c1?.id)) + setGroups(prev => [...prev, {id: data.c1?.id, poule: data.poule}]); + if (data.c2 !== null && !groupsRef.current.some(g => g.id === data.c2?.id)) + setGroups(prev => [...prev, {id: data.c2?.id, poule: data.poule}]); + } + } + + const matchOrder = ({data}) => { + reducer({type: 'REORDER', payload: data}); + } + + const deleteMatch = ({data: datas}) => { + for (const data of datas) + reducer({type: 'REMOVE', payload: data}); } dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}}) dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}}) + dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}}) + dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}}) return () => { dispatch({type: 'removeListener', payload: treeListener}) dispatch({type: 'removeListener', payload: matchListener}) + dispatch({type: 'removeListener', payload: matchOrder}) + dispatch({type: 'removeListener', payload: deleteMatch}) } }, [cat]); @@ -84,9 +114,13 @@ export function CategoryContent({cat, catId, setCat}) { combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}}); const activeMatches = matches2.filter(m => m.poule !== '-') + const combsIDs = activeMatches.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i) .map(d => { - return {id: d, poule: activeMatches.find(m => m.c1 === d || m.c2 === d)?.poule} + let poule = activeMatches.find(m => (m.c1 === d || m.c2 === d) && m.categorie_ord !== -42)?.poule + if (!poule) + poule = '-' + return {id: d, poule: poule} }) setGroups(combsIDs) }).finally(() => setLoading(0)) @@ -110,7 +144,7 @@ export function CategoryContent({cat, catId, setCat}) {
- {cat && } + {cat && }
} @@ -266,8 +300,59 @@ function GroupModalContent({combId, groups, setGroups, removeGroup}) { } -function ListMatch({cat, matches, groups}) { +function ListMatch({cat, matches, groups, reducer}) { + const {sendRequest} = useWS(); const [type, setType] = useState(1); + const bthRef = useRef(null); + + useEffect(() => { + if ((cat.type & type) === 0) + setType(cat.type); + }, [cat]); + + const handleCreatMatch = () => { + if (matches.some(m => m.poule !== '-')) { + bthRef.current.click(); + return; + } + recalculateMatch(0); + } + + const recalculateMatch = (mode) => { + let matchesToKeep = []; + let matchesToRemove = []; + if (mode === 2) { + matchesToKeep = matches.filter(m => m.categorie === cat.id && m.categorie_ord !== -42); + } else if (mode === 1) { + matchesToKeep = matches.filter(m => m.categorie === cat.id && m.end === true && m.categorie_ord !== -42) + matchesToRemove = matches.filter(m => m.categorie === cat.id && m.end === false && m.categorie_ord !== -42) + } else { + matchesToRemove = matches.filter(m => m.categorie === cat.id && m.categorie_ord !== -42) + } + + try { + const {newMatch, matchOrderToUpdate, matchPouleToUpdate} = createMatch(cat, matchesToKeep, groups.filter(g => g.poule !== '-')) + + toast.promise(sendRequest("recalculateMatch", { + categorie: cat.id, + newMatch, + matchOrderToUpdate: Object.fromEntries(matchOrderToUpdate), + matchPouleToUpdate: Object.fromEntries(matchPouleToUpdate), + matchesToRemove: matchesToRemove.map(m => m.id) + }), + { + pending: 'Création des matchs en cours...', + success: 'Matchs créés avec succès !', + error: 'Erreur lors de la création des matchs', + }) + .finally(() => { + console.log("Finished creating matches"); + }) + + } catch (e) { + toast.error("Erreur lors de la création des matchs: " + e.message); + } + } return <> {cat.type === 3 && <> @@ -285,12 +370,281 @@ function ListMatch({cat, matches, groups}) { } + {type === 1 && <> + + + } + {type === 2 && <> } + + + } +function MatchList({matches, cat, groups, reducer}) { + const {sendRequest} = useWS(); + const setLoading = useLoadingSwitcher() + const selectRef = useRef(null) + const tableRef = useRef(null) + const lastMatchClick = useRef(null) + const [combSelect, setCombSelect] = useState(0) + const [combC1nm, setCombC1nm] = useState(null) + const [combC2nm, setCombC2nm] = useState(null) + + const liceName = (cat.liceName || "N/A").split(";"); + const marches2 = matches.filter(m => m.categorie_ord !== -42) + .sort((a, b) => a.categorie_ord - b.categorie_ord) + .map(m => ({...m, win: win(m.scores)})) + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = async (event) => { + const {active, over} = event; + if (active.id !== over.id) { + const newIndex = marches2.findIndex(m => m.id === over.id); + reducer({type: 'REORDER', payload: {id: active.id, pos: newIndex}}); + sendRequest('updateMatchOrder', {id: active.id, pos: newIndex}).then(__ => { + }) + } + }; + + useEffect(() => { + if (!combC1nm || !combC2nm) + return; + if (combC1nm === combC2nm) { + toast.error("Un combattant ne peut pas s'affronter lui-même !"); + return; + } + + setLoading(1) + sendRequest('addMatch', { + categorie: cat.id, + categorie_ord: Math.max(...marches2.map(o => o.categorie_ord)) + 1, + poule: groups.find(g => g.id === combC1nm).poule, + c1: combC1nm, + c2: combC2nm + }).then(_ => { + setCombC1nm(null) + setCombC2nm(null) + }).finally(() => setLoading(0)) + + }, [combC1nm, combC2nm]); + + useEffect(() => { + if (lastMatchClick.current == null) + return + + const {matchId, combId} = lastMatchClick.current + if (matchId === -1) { + setCombC1nm(combSelect === 0 ? null : combSelect); + onClickVoid() + return + } + if (matchId === -2) { + setCombC2nm(combSelect === 0 ? null : combSelect); + onClickVoid() + return + } + + const match = matches.find(m => m.id === matchId) + if (!match) + return + + const combSelect_ = combSelect === 0 ? null : combSelect; + const data = {id: match.id, c1: match.c1, c2: match.c2} + if (match.c1 === combId) { + if (match.c1 === combSelect_) + return + data.c1 = combSelect_ + } else if (match.c2 === combId) { + if (match.c2 === combSelect_) + return + data.c2 = combSelect_ + } + + if (data.c1 === data.c2) { + toast.error("Un combattant ne peut pas s'affronter lui-même !"); + onClickVoid() + return; + } + + setLoading(1) + sendRequest('updateMatchComb', data) + .finally(() => { + setLoading(0) + onClickVoid() + }) + }, [combSelect]) + + const handleDelMatch = (matchId) => { + const match = matches.find(m => m.id === matchId) + if (!match) + return; + + if (!confirm("Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?")) + return; + + setLoading(1) + sendRequest('deleteMatch', matchId) + .finally(() => setLoading(0)) + } + + const handleCombClick = (e, matchId, combId) => { + e.stopPropagation(); + const tableRect = tableRef.current.getBoundingClientRect(); + const rect = e.currentTarget.getBoundingClientRect(); + + const sel = selectRef.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"; + + setCombSelect(combId || 0); + lastMatchClick.current = {matchId, combId}; + } + + const onClickVoid = () => { + const sel = selectRef.current; + sel.style.display = "none"; + lastMatchClick.current = null; + } + + const combsIDs = groups.map(m => m.id); + return
+ + m.id)} strategy={verticalListSortingStrategy}> +
+ + + + + + + + + + + + + + + + + {marches2.map((m, index) => ( + + + + + + + + + + + + + ))} + + + + + + + + + + + + + +
PouleLiceRougeBlueRésultat
{index + 1}{m.poule}{liceName[index % liceName.length]}{m.end && m.win > 0 && } handleCombClick(e, m.id, m.c1)}> + handleCombClick(e, m.id, m.c2)}> + {m.end && m.win < 0 && }{scoreToString(m.scores)} handleDelMatch(m.id)}> +
- handleCombClick(e, -1, combC1nm)}> + {combC1nm && } handleCombClick(e, -2, combC2nm)}> + {combC2nm && }
+
+
+
+ + +
+} + +function SortableRow({id, children}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({id}); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + touchAction: 'none', // Désactive le scroll par défaut pendant le drag + }; + + return ( + + {React.Children.map(children, (child, i) => + i === children.length - 1 // Dernière cellule (icône de drag) + ? React.cloneElement(child, { + ...child.props, ...attributes, ...listeners, style: {...child.props.style, cursor: "grab", touchAction: 'none'}, + }) + : child + )} + + ); +} + function BuildTree({treeData, matches, groups}) { const scrollRef = useRef(null) const selectRef = useRef(null) diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index e81ffc1..2977c26 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -88,7 +88,7 @@ export function SelectCombModalContent({data, setGroups}) { } dispatch({type: 'addListener', payload: {callback: sendRegisterRemove, code: 'sendRegisterRemove'}}) return () => { - dispatch({type: 'removeListener', payload: {callback: sendRegisterRemove, code: 'sendRegisterRemove'}}) + dispatch({type: 'removeListener', payload: sendRegisterRemove}) } }, []); diff --git a/src/main/webapp/src/pages/result/DrawGraph.jsx b/src/main/webapp/src/pages/result/DrawGraph.jsx index dd534c1..4224f9c 100644 --- a/src/main/webapp/src/pages/result/DrawGraph.jsx +++ b/src/main/webapp/src/pages/result/DrawGraph.jsx @@ -1,4 +1,5 @@ import {useEffect, useRef} from "react"; +import {win} from "../../utils/CompetitionTools.js"; const max_x = 500; const size = 24; @@ -235,17 +236,6 @@ export function DrawGraph({ } }; - // Fonction pour déterminer le gagnant - const 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; - }; - // Effet pour dessiner le graphique useEffect(() => { if (root.length === 0) return; diff --git a/src/main/webapp/src/pages/result/ResultView.jsx b/src/main/webapp/src/pages/result/ResultView.jsx index 1ed6424..0dedb13 100644 --- a/src/main/webapp/src/pages/result/ResultView.jsx +++ b/src/main/webapp/src/pages/result/ResultView.jsx @@ -6,6 +6,7 @@ import {ThreeDots} from "react-loader-spinner"; import {useEffect, useState} from "react"; import {DrawGraph} from "./DrawGraph.jsx"; import {TreeNode} from "../../utils/TreeUtils.js"; +import {scoreToString} from "../../utils/CompetitionTools.js"; function CupImg() { return { - switch (s1) { - case -997: - return "disc."; - case -998: - return "abs."; - case -999: - return "for."; - case -1000: - return ""; - default: - return String(s1); - } - } - - return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); -} - function BuildMatchArray({matchs}) { return <> diff --git a/src/main/webapp/src/utils/CompetitionTools.js b/src/main/webapp/src/utils/CompetitionTools.js new file mode 100644 index 0000000..30c9f8a --- /dev/null +++ b/src/main/webapp/src/utils/CompetitionTools.js @@ -0,0 +1,238 @@ +export 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 +} + +export function scoreToString(score) { + const scorePrint = (s1) => { + switch (s1) { + case -997: + return "disc." + case -998: + return "abs." + case -999: + return "for." + case -1000: + return "" + default: + return String(s1) + } + } + if (score === null || score === undefined || score.length === 0) + return "" + + if (Array.isArray(score[0])) + return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | ") + else + return score.sort((a, b) => a.n_round - b.n_round).map(o => scorePrint(o.s1) + "-" + scorePrint(o.s2)).join(" | ") +} + +export function createMatch(category, matchs, groups) { + const combs_2 = {} + for (const group of groups) { + if (!combs_2.hasOwnProperty(group.poule)) + combs_2[group.poule] = [] + if (!combs_2[group.poule].includes(group.id)) + combs_2[group.poule].push(group.id) + } + + const ord = new Map() + const newMatch = [] + const matchPouleToUpdate = new Map() + + let groupeNumber = 0 + const numberOfGroup = Object.keys(combs_2).length + + for (const [k, combs_1] of Object.entries(combs_2)) { + let matchsList = calcPoule(combs_1.length, category.liceName.split(";").length) + + let indexMatch = 0; + const combs = new Array(combs_1.length).fill(0); + + // Trouver le premier match valide + while (indexMatch < matchs.length && (!combs_1.includes(matchs[indexMatch].c1) || !combs_1.includes(matchs[indexMatch].c2))) { + indexMatch++; + } + + // Attribution aléatoire des combattants + for (const set of matchsList) { + if (indexMatch < matchs.length) { + if (!combs.includes(matchs[indexMatch].c1)) { + if (combs[set[0] - 1] === 0) + combs[set[0] - 1] = matchs[indexMatch].c1 + else if (combs[set[1] - 1] === 0) + combs[set[1] - 1] = matchs[indexMatch].c1 + + if (combs.includes(matchs[indexMatch].c1)) { + const index = combs_1.indexOf(matchs[indexMatch].c1) + if (index !== -1) + combs_1.splice(index, 1) + } + } + + if (!combs.includes(matchs[indexMatch].c2)) { + if (combs[set[0] - 1] === 0) + combs[set[0] - 1] = matchs[indexMatch].c2 + else if (combs[set[1] - 1] === 0) + combs[set[1] - 1] = matchs[indexMatch].c2 + + if (combs.includes(matchs[indexMatch].c2)) { + const index = combs_1.indexOf(matchs[indexMatch].c2) + if (index !== -1) + combs_1.splice(index, 1) + } + } + // Passer aux matchs suivants si nécessaire + while (indexMatch < matchs.length && + ((combs.includes(matchs[indexMatch].c1) && combs.includes(matchs[indexMatch].c2)) || + !combs_1.includes(matchs[indexMatch].c1) || !combs_1.includes(matchs[indexMatch].c2))) { + indexMatch++ + } + } + + // Remplir les places vides avec des combattants restants + if (combs[set[0] - 1] === 0) { + if (combs_1.length > 0) { + const index = Math.floor(Math.random() * combs_1.length) + combs[set[0] - 1] = combs_1[index] + combs_1.splice(index, 1) + } + } + if (combs[set[1] - 1] === 0) { + if (combs_1.length > 0) { + const index = Math.floor(Math.random() * combs_1.length) + combs[set[1] - 1] = combs_1[index] + combs_1.splice(index, 1) + } + } + + if (combs_1.length === 0) + break + } + + // Créer les nouveaux matchs + for (let i = 0; i < matchsList.length; i++) { + const set = matchsList[i] + const matchExistant = matchs.find( + (match) => + (match.c1 === combs[set[0] - 1] && match.c2 === combs[set[1] - 1]) || + (match.c1 === combs[set[1] - 1] && match.c2 === combs[set[0] - 1])) + + if (matchExistant) { + ord.set(matchExistant.id, i * numberOfGroup + groupeNumber) + if (matchExistant.poule !== k) + matchPouleToUpdate.set(matchExistant.id, k) + matchs = matchs.filter((match) => match.id !== matchExistant.id) + continue + } + + newMatch.push({ + categorie: category.id, + categorie_ord: i * numberOfGroup + groupeNumber, + poule: k, + c1: combs[set[0] - 1], + c2: combs[set[1] - 1], + }) + } + groupeNumber++ + } + + // Mise à jour des ordres et des groupes + const lenToAdd = matchs.length + newMatch.forEach((match) => { + match.categorie_ord += lenToAdd + }) + + ord.forEach((value, key) => { + ord.set(key, value + lenToAdd) + }) + + for (let i = 0; i < matchs.length; i++) { + ord.set(matchs[i].id, i) + matchPouleToUpdate.set(matchs[i].id, '-') + } + + return {newMatch, matchOrderToUpdate: ord, matchPouleToUpdate} +} + +const tt_ronde_opti = new Map() +tt_ronde_opti.set(2, [[1, 2]]) +tt_ronde_opti.set(3, [[2, 3], [1, 3], [1, 2]]) +tt_ronde_opti.set(4, [[1, 4], [2, 3], [1, 3], [2, 4], [3, 4], [1, 2]]) +tt_ronde_opti.set(5, [[1, 2], [3, 4], [5, 1], [2, 3], [5, 4], [1, 3], [2, 5], [4, 1], [3, 5], [4, 2]]) +tt_ronde_opti.set(6, [[1, 2], [4, 5], [2, 3], [5, 6], [3, 1], [6, 4], [2, 5], [1, 4], [5, 3], [1, 6], [4, 2], [3, 6], [5, 1], [3, 4], [6, 2]]) +tt_ronde_opti.set(7, [[1, 4], [2, 5], [3, 6], [7, 1], [5, 4], [2, 3], [6, 7], [5, 1], [4, 3], [6, 2], [5, 7], [3, 1], [4, 6], [7, 2], [3, 5], [1, 6], + [2, 4], [7, 3], [6, 5], [1, 2], [4, 7]]) +tt_ronde_opti.set(8, [[2, 3], [1, 5], [7, 4], [6, 8], [1, 2], [3, 4], [5, 6], [8, 7], [4, 1], [5, 2], [8, 3], [6, 7], [4, 2], [8, 1], [7, 5], [3, 6], + [2, 8], [5, 4], [6, 1], [3, 7], [4, 8], [2, 6], [3, 5], [1, 7], [4, 6], [8, 5], [7, 2], [1, 3]]) +tt_ronde_opti.set(9, [[1, 9], [2, 8], [3, 7], [4, 6], [1, 5], [2, 9], [8, 3], [7, 4], [6, 5], [1, 2], [9, 3], [8, 4], [7, 5], [6, 1], [3, 2], [9, 4], + [5, 8], [7, 6], [3, 1], [2, 4], [5, 9], [8, 6], [7, 1], [4, 3], [5, 2], [6, 9], [8, 7], [4, 1], [5, 3], [6, 2], [9, 7], [1, 8], [4, 5], [3, 6], + [5, 7], [9, 8]]) +tt_ronde_opti.set(10, [[1, 4], [6, 9], [2, 5], [7, 10], [3, 1], [8, 6], [4, 5], [9, 10], [2, 3], [7, 8], [5, 1], [10, 6], [4, 2], [9, 7], [5, 3], + [10, 8], [1, 2], [6, 7], [3, 4], [8, 9], [5, 10], [1, 6], [2, 7], [3, 8], [4, 9], [6, 5], [10, 2], [8, 1], [7, 4], [9, 3], [2, 6] + , [5, 8], [4, 10], [1, 9], [3, 7], [8, 2], [6, 4], [9, 5], [10, 3], [7, 1], [4, 8], [2, 9], [3, 6], [5, 7], [1, 10]]) +tt_ronde_opti.set(11, [[1, 2], [7, 8], [4, 5], [10, 11], [2, 3], [8, 9], [5, 6], [3, 1], [9, 7], [6, 4], [2, 5], [8, 11], [1, 4], [7, 10], [5, 3], + [11, 9], [1, 6], [4, 2], [10, 8], [3, 6], [5, 1], [11, 7], [3, 4], [9, 10], [6, 2], [1, 7], [3, 9], [10, 4], [8, 2], [5, 11], [1, 8], [9, 2], + [3, 10], [4, 11], [6, 7], [9, 1], [2, 10], [11, 3], [7, 5], [6, 8], [10, 1], [11, 2], [4, 7], [8, 5], [6, 9], [11, 1], [7, 3], [4, 8], [9, 5], + [6, 10], [2, 7], [8, 3], [4, 9], [10, 5], [6, 11]]) + +function calcPoule(n_comb, n_lice) { + if (n_comb === 1) + throw new Error("Le nombre de combattants doit être supérieur à 1.") + if (n_comb / 2 < n_lice) + n_lice = Math.floor(n_comb / 2) + + if (n_lice === 1 && n_comb <= 11) { + return [...tt_ronde_opti.get(n_comb)] + } else { + return rutsch(n_comb) + } +} + +function rutsch(n_comb) { + const out = [] + let virt_n_comb = n_comb + if (n_comb % 2 === 1) virt_n_comb++ + + // Initialisation du tableau temporaire + const tmp = Array.from({length: virt_n_comb / 2}, () => [0, 0]) + for (let i = 0; i < virt_n_comb / 2; i++) { + tmp[i][0] = i + 1 + tmp[i][1] = virt_n_comb - i + out.push([...tmp[i]]) + } + + // Boucle pour les rondes + for (let ronde = 1; ronde < virt_n_comb - 1; ronde++) { + // Déplacement des éléments + const old_val = tmp[virt_n_comb / 2 - 1][0] + for (let i = virt_n_comb / 2 - 1; i > 0; i--) { + tmp[i][0] = tmp[i - 1][0] + } + tmp[0][0] = tmp[0][1] + for (let i = 0; i < virt_n_comb / 2 - 1; i++) { + tmp[i][1] = tmp[i + 1][1] + } + tmp[virt_n_comb / 2 - 1][1] = old_val + + // Échange des clés si nécessaire + if (tmp[1][0] === virt_n_comb) { + tmp[1][0] = tmp[0][0] + tmp[0][0] = tmp[0][1] + tmp[0][1] = virt_n_comb + } + + // Ajout des paires à la sortie + for (let i = 0; i < virt_n_comb / 2; i++) { + out.push([...tmp[i]]) + } + } + + // Filtrer les paires dont les valeurs dépassent n_comb + return out.filter(pair => pair[0] <= n_comb && pair[1] <= n_comb) +} diff --git a/src/main/webapp/src/utils/MatchReducer.jsx b/src/main/webapp/src/utils/MatchReducer.jsx index 4884675..d6ba11a 100644 --- a/src/main/webapp/src/utils/MatchReducer.jsx +++ b/src/main/webapp/src/utils/MatchReducer.jsx @@ -38,6 +38,34 @@ export function MarchReducer(datas, action) { } case 'SORT': return datas.sort(action.payload) + case 'REORDER': + const oldIndex = datas.findIndex(data => data.id === action.payload.id) + if (oldIndex === -1 || datas[oldIndex].categorie_ord === action.payload.pos) + return datas // Do nothing + + const oldPos = datas[oldIndex].categorie_ord + const newPos = action.payload.pos + + const new_datas = [] + for (const data of datas) { + if (data.id === action.payload.id) { + data.categorie_ord = newPos + } else { + if (oldPos < newPos) { + // Moving down + if (data.categorie_ord > oldPos && data.categorie_ord <= newPos) { + data.categorie_ord -= 1 + } + } else if (oldPos > newPos) { + // Moving up + if (data.categorie_ord < oldPos && data.categorie_ord >= newPos) { + data.categorie_ord += 1 + } + } + } + new_datas.push(data) + } + return new_datas default: throw new Error() }