dev-comp #72

Merged
Thibaut merged 60 commits from dev into master 2025-12-19 13:47:40 +00:00
9 changed files with 349 additions and 70 deletions
Showing only changes of commit 4b969e6d69 - Show all commits

View File

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

View File

@ -17,7 +17,10 @@ public class CardboardEntity {
int yellow; int yellow;
public static CardboardEntity fromModel(CardboardModel model) { public static CardboardEntity fromModel(CardboardModel model) {
return new CardboardEntity(model.getComb().getId(), model.getMatch().getId(), model.getCompet().getId(), return new CardboardEntity(
model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1,
model.getMatch().getId(),
model.getCompet().getId(),
model.getRed(), model.getYellow()); model.getRed(), model.getYellow());
} }
} }

View File

@ -6,10 +6,7 @@ import fr.titionfire.ffsaf.domain.service.CompetPermService;
import fr.titionfire.ffsaf.net2.MessageType; import fr.titionfire.ffsaf.net2.MessageType;
import fr.titionfire.ffsaf.utils.SecurityCtx; import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.ws.data.WelcomeInfo; import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
import fr.titionfire.ffsaf.ws.recv.RCategorie; import fr.titionfire.ffsaf.ws.recv.*;
import fr.titionfire.ffsaf.ws.recv.RMatch;
import fr.titionfire.ffsaf.ws.recv.RRegister;
import fr.titionfire.ffsaf.ws.recv.WSReceiver;
import fr.titionfire.ffsaf.ws.send.JsonUni; import fr.titionfire.ffsaf.ws.send.JsonUni;
import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.security.Authenticated; import io.quarkus.security.Authenticated;
@ -44,6 +41,9 @@ public class CompetitionWS {
@Inject @Inject
RRegister rRegister; RRegister rRegister;
@Inject
RCardboard rCardboard;
@Inject @Inject
SecurityCtx securityCtx; SecurityCtx securityCtx;
@ -77,6 +77,7 @@ 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);
} }
@OnOpen @OnOpen

View File

@ -0,0 +1,128 @@
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.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;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Matche non trouver");
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("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));
}))
.call(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();
models.stream().filter(c -> (matchModel.getC1_id() != null
&& Objects.equals(c.getComb(), matchModel.getC1_id()))
|| (matchModel.getC1_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC1_guest())))
.forEach(c -> {
out.c1_yellow += c.getYellow();
out.c1_red += c.getRed();
});
models.stream().filter(c -> (matchModel.getC2_id() != null
&& Objects.equals(c.getComb(), matchModel.getC2_id()))
|| (matchModel.getC2_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC2_guest())))
.forEach(c -> {
out.c2_yellow += c.getYellow();
out.c2_red += c.getRed();
});
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

@ -0,0 +1,13 @@
package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
public class SSCardboard {
public static Uni<Void> sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity);
}
}

View File

@ -0,0 +1,5 @@
.btn-xs {
--bs-btn-padding-y: .05rem;
--bs-btn-padding-x: .6rem;
--bs-btn-font-size: .75rem;
}

View File

@ -10,6 +10,7 @@ import {scorePrint, win} from "../../../utils/Tools.js";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import "./CMTMatchPanel.css"
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"
@ -105,15 +106,21 @@ 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]);
@ -164,6 +171,7 @@ function MatchList({matches, cat, menuActions}) {
const marches2 = matches.filter(m => m.categorie_ord !== -42) const marches2 = matches.filter(m => m.categorie_ord !== -42)
.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: win(m.scores)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const match = matches.find(m => m.id === activeMatch) const match = matches.find(m => m.id === activeMatch)
useEffect(() => { useEffect(() => {
@ -175,7 +183,10 @@ function MatchList({matches, cat, menuActions}) {
payload: { payload: {
c1: match.c1, c1: match.c1,
c2: match.c2, c2: match.c2,
next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2})) next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({
c1: m.c1,
c2: m.c2
}))
} }
}); });
} }
@ -187,7 +198,7 @@ function MatchList({matches, cat, menuActions}) {
useEffect(() => { useEffect(() => {
if (match && match.poule !== lice) if (match && match.poule !== lice)
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id) setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id)
}, [lice]); }, [lice]);
useEffect(() => { useEffect(() => {
@ -196,10 +207,8 @@ function MatchList({matches, cat, menuActions}) {
if (marches2.some(m => m.id === activeMatch)) if (marches2.some(m => m.id === activeMatch))
return; return;
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id); setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id);
}, [matches]) }, [matches])
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
return <> return <>
{liceName.length > 1 && {liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}> <div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
@ -230,7 +239,8 @@ function MatchList({matches, cat, menuActions}) {
</thead> </thead>
<tbody className="table-group-divider"> <tbody className="table-group-divider">
{marches2.map((m, index) => ( {marches2.map((m, index) => (
<tr key={m.id} className={m.id === activeMatch ? "table-info" : (m.poule === lice ? "" : "table-warning")} <tr key={m.id}
className={m.id === activeMatch ? "table-info" : (liceName[(index - firstIndex) % liceName.length] === lice ? "" : "table-warning")}
onClick={() => setActiveMatch(m.id)}> onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}> <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td> {liceName[(index - firstIndex) % liceName.length]}</td>
@ -326,6 +336,16 @@ function BuildTree({treeData, matches, menuActions}) {
} }
function ScorePanel({matchId, match, menuActions}) { function ScorePanel({matchId, match, menuActions}) {
const onClickVoid = useRef(() => {
});
return <div className="row" onClick={onClickVoid.current}>
<ScorePanel_ matchId={matchId} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
<CardPanel matchId={matchId} match={match}/>
</div>
}
function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS() const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher() const setLoading = useLoadingSwitcher()
@ -418,6 +438,7 @@ function ScorePanel({matchId, match, menuActions}) {
sel.style.display = "none"; sel.style.display = "none";
lastScoreClick.current = null; lastScoreClick.current = null;
} }
onClickVoid_.current = onClickVoid;
useEffect(() => { useEffect(() => {
if (!match || match?.end === end) if (!match || match?.end === end)
@ -463,8 +484,7 @@ function ScorePanel({matchId, match, menuActions}) {
"-999 : forfait" "-999 : forfait"
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
return <div className="row" onClick={onClickVoid}> return <div ref={tableRef} className="col" style={{position: "relative"}}>
<div ref={tableRef} className="col" style={{position: "relative"}}>
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt} <h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
data-bs-html="true"/></h6> data-bs-html="true"/></h6>
<table className="table table-striped"> <table className="table table-striped">
@ -520,7 +540,87 @@ function ScorePanel({matchId, match, menuActions}) {
} }
}}/> }}/>
</div> </div>
<div className="col"> }
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>
</div> </div>
} }

View File

@ -61,6 +61,8 @@ export function CMTable() {
const windowName = "FFSAFScorePublicWindow"; const windowName = "FFSAFScorePublicWindow";
let tto = [];
function Menu({menuActions}) { function Menu({menuActions}) {
const e = document.getElementById("actionMenu") const e = document.getElementById("actionMenu")
const publicAffDispatch = usePubAffDispatch() const publicAffDispatch = usePubAffDispatch()
@ -102,6 +104,11 @@ function Menu({menuActions}) {
} }
} }
for (const x of tto)
x.dispose();
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const handleScore = __ => { const handleScore = __ => {
setShowScore(!showScore); setShowScore(!showScore);
publicAffDispatch({type: 'SET_DATA', payload: {showScore: !showScore}}); publicAffDispatch({type: 'SET_DATA', payload: {showScore: !showScore}});
@ -111,10 +118,6 @@ function Menu({menuActions}) {
menuActions.current.switchSore?.(); menuActions.current.switchSore?.();
} }
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
if (!e) if (!e)
return <></>; return <></>;
return <> return <>
@ -122,7 +125,8 @@ function Menu({menuActions}) {
<> <>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer"}} onClick={handleSwitchScore} <FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer"}} onClick={handleSwitchScore}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Inverser la position des combattants sur cette écran"/> data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Inverser la position des combattants sur cette écran"/>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div> <div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faDisplay} size="xl" <FontAwesomeIcon icon={faDisplay} size="xl"
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}} style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}

View File

@ -36,6 +36,22 @@ export function MarchReducer(datas, action) {
datas[index] = action.payload datas[index] = action.payload
return [...datas] return [...datas]
} }
case 'UPDATE_CARDBOARD':
const idx = datas.findIndex(data => data.id === action.payload.match_id)
if (idx === -1)
return datas // Do nothing
const data = datas[idx]
const tmp = data.cardboard?.find(c => c.comb_id === action.payload.comb_id)
if (tmp) {
tmp.red = action.payload.red
tmp.yellow = action.payload.yellow
} else {
if (!data.cardboard)
data.cardboard = []
data.cardboard.push(action.payload)
}
return [...datas]
case 'SORT': case 'SORT':
return datas.sort(action.payload) return datas.sort(action.payload)
case 'REORDER': case 'REORDER':