gestion_table_tournante_libre/src/application/commandes_places.js

718 lines
33 KiB
JavaScript
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le registre des commandes des places — réservations, titres, pose (§ 5.8,
// § 5.9, § 8.2), que commandes.js réunit aux autres. Ses imports n'atteignent
// jamais commandes.js : le registre réuni dépend de lui, non l'inverse.
2026-10-07 04:14:24 -04:00
//
// Chaque commande suit le contrat de commandes.js : une fonction pure
// (charge, arguments) → { charge, libelle, avertissements }, la charge reçue
// intacte ; un refus lève ErreurCommande(code, details), details.remede
// compris ; un argument de forme fausse, une table inconnue, un retenu absent
// lèvent TypeError, que l'interface n'offre jamais. Un plan bloqué refuse
// tout (PLAN_BLOQUE). Les poses se contrôlent par controlerPose (pose.js),
// qui rend tous les refus ; la commande lève le premier et porte les autres
// dans details.autres.
//
// Avant la retenue, un geste de personne pose ou déplace une réservation ;
// après, il modifie le retenu, au tour visé seulement, jamais la proposition
// d'où il vient (§ 5.8). Sièges attribués, le siège est le rang dans la
// liste, une chaise vide s'y écrit null et une liste ne finit jamais par
// null : un départ libère sa chaise, et tout autre siège reste le même
// (§ 8.9). Sans attribution, la liste reste triée par identifiant, et chaque
// geste verse d'abord en réserve les présents que le plan ne montre pas
// assis aux tables qu'il touche (reduireAuxAssises) : la liste vaut ce que
// l'affichage assied. Chaque geste lit les places comme le plan les montre
// (placeDe). Une retouche qui ajoute une violation de verifierInvariants au
// retenu reçu est refusée (INVARIANT_VIOLE) ; une violation déjà présente,
// celle d'une dérive (§ 9), ne bloque rien.
import { ordreDesReservations, pourvoyeursDesTitres } from '../csv/import.js';
import { verifierInvariants } from '../moteur/verification.js';
import { capacite } from '../stockage/document.js';
import { fautes, planDepuisFichier } from '../stockage/placements.js';
import { refus } from './erreurs.js';
import { libelle } from './libelles.js';
import { conversionsPossibles } from './listes_travail.js';
import { nomAffiche } from './personnes.js';
import { etatPlaces, instanceDe, presents } from './places.js';
import { controlerPose, leverRefus } from './pose.js';
const croissant = (a, b) => a - b;
// --- Les contrôles communs ---------------------------------------------------
// Un plan bloqué refuse toute modification ; débloquer seul le lève (§ 9).
function exigerModifiable(charge) {
if (charge.evenement.etat === 'bloque') throw refus('PLAN_BLOQUE', {}, { geste: 'debloquer' });
}
// Le participant d'identifiant id, ou PARTICIPANT_INCONNU.
function personneDe(charge, id) {
if (!Number.isSafeInteger(id)) throw new TypeError(`participant : entier attendu, reçu ${JSON.stringify(id)}`);
const personne = charge.participants.find((candidat) => candidat.id === id);
if (personne === undefined) throw refus('PARTICIPANT_INCONNU', { id });
return personne;
}
// L'enregistrement de la table d'identifiant id : une table inconnue est
// une faute de l'appelant.
function tableDe(charge, id) {
const table = charge.tables.find((candidate) => candidate.id === id);
if (table === undefined) throw new TypeError(`table inconnue : ${JSON.stringify(id)}`);
return table;
}
// Un siège donné : null, ou un entier ≥ 1.
function exigerSiege(siege) {
if (siege !== null && !(Number.isSafeInteger(siege) && siege >= 1)) {
throw new TypeError(`siege : null ou entier ≥ 1 attendu, reçu ${JSON.stringify(siege)}`);
}
}
// Une portée et son tour : « tous » sans tour, « tour » de 1 à R.
function exigerPortee(charge, portee, tour) {
if (portee === 'tous' && tour === null) return;
if (portee === 'tour' && Number.isSafeInteger(tour) && tour >= 1 && tour <= charge.evenement.tours) return;
throw new TypeError(`portée ${JSON.stringify(portee)}, tour ${JSON.stringify(tour)} : « tous » sans tour ou « tour » de 1 à R attendus`);
}
// Le libellé d'un titre saisi : en NFC, ses blancs de bord retirés ;
// LIBELLE_VIDE quand il ne reste rien.
function libelleSaisi(valeur) {
if (typeof valeur !== 'string') throw new TypeError(`libelle : chaîne attendue, reçu ${JSON.stringify(valeur)}`);
const texte = valeur.normalize('NFC').trim();
if (texte === '') throw refus('LIBELLE_VIDE');
return texte;
}
// Vrai quand la réservation r est celle que désigne d pour ce participant :
// sa table, sa portée, son tour.
const designee = (r, participant, d) =>
r.participant === participant && r.table === d.table && r.portee === d.portee && r.tour === (d.portee === 'tous' ? null : d.tour);
// Le premier titre de la place dans l'ordre canonique, ou undefined.
const titreDe = (charge, idTable, siege) =>
charge.titres
.filter((titre) => titre.table === idTable && titre.siege === siege)
.sort((a, b) => (a.libelle < b.libelle ? -1 : a.libelle > b.libelle ? 1 : 0))[0];
// Le remède d'une table qui manque de places : une de plus que places, par
// la saisie de son champ.
const changerPlaces = (idTable, places) => ({
commande: 'changerPlacesTable',
arguments: { id: idTable, sieges: places + 1, origine: 'champ' },
});
// Le plus petit siège de la table, de 1 à places, qu'aucun titre ne porte ;
// AUCUN_SIEGE_SANS_TITRE quand tous en portent un.
function siegeSansTitre(charge, idTable, places) {
const titres = new Set(charge.titres.filter(({ table }) => table === idTable).map(({ siege }) => siege));
for (let siege = 1; siege <= places; siege += 1) if (!titres.has(siege)) return siege;
throw refus('AUCUN_SIEGE_SANS_TITRE', { table: idTable, capacite: places }, changerPlaces(idTable, places));
}
// SIEGE_INEXISTANT pour un titre au-delà de la capacité : un titre garde
// toujours un siège de sa table.
function exigerSiegeDeTitre(idTable, siege, places) {
if (siege > places) {
throw refus('SIEGE_INEXISTANT', { table: idTable, siege, capacite: places }, {
commande: 'changerPlacesTable',
arguments: { id: idTable, sieges: siege, origine: 'champ' },
});
}
}
// Les avertissements des signaux d'une pose acceptée.
const avertissementsDe = (signaux) => signaux.map(({ code, table }) => ({ code, details: { table } }));
// --- Les réservations (§ 4, § 5.9) -------------------------------------------
/**
* reserver {participant, table, siege|null, portee: 'tous'|'tour', tour|null}
* (§ 4, § 5.9) : une réservation de plus, contrôlée comme une pose. Sans
* attribution des sièges, un siège au-delà de la capacité s'enregistre null :
* la place est la table. Une réservation déjà là : SANS_EFFET.
*/
function reserver(charge, { participant, table: idTable, siege = null, portee, tour = null } = {}) {
exigerModifiable(charge);
const personne = personneDe(charge, participant);
const table = tableDe(charge, idTable);
exigerSiege(siege);
exigerPortee(charge, portee, tour);
const places = capacite(charge, table);
const siegeEcrit = !charge.reglages.attribuerSieges && siege !== null && siege > places ? null : siege;
const nouvelle = { participant, table: idTable, siege: siegeEcrit, portee, tour };
if (charge.reservations.some((r) => ordreDesReservations(r, nouvelle) === 0)) {
throw refus('SANS_EFFET', { commande: 'reserver' });
}
const { refus: refusPose, signaux } = controlerPose(charge, nouvelle);
leverRefus(refusPose);
const suivante = structuredClone(charge);
suivante.reservations.push(nouvelle);
return {
charge: suivante,
libelle: libelle('ENTREE_RESERVER', { personne: nomAffiche(personne), numero: table.numero, siege: siegeEcrit, portee, tour }),
avertissements: avertissementsDe(signaux),
};
}
// Les réservations du participant que désigne { table, portee, tour }, ou
// RESERVATION_ABSENTE.
function reservationsDesignees(charge, participant, designation) {
exigerPortee(charge, designation.portee, designation.tour ?? null);
const trouvees = charge.reservations.filter((r) => designee(r, participant, designation));
if (trouvees.length === 0) {
const { table, portee, tour = null } = designation;
throw refus('RESERVATION_ABSENTE', { participant, table, portee, tour });
}
return trouvees;
}
/** retirerReservation {participant, table, portee, tour|null} : la réservation ainsi désignée part. */
function retirerReservation(charge, { participant, table: idTable, portee, tour = null } = {}) {
exigerModifiable(charge);
const personne = personneDe(charge, participant);
const table = tableDe(charge, idTable);
const designation = { table: idTable, portee, tour };
reservationsDesignees(charge, participant, designation);
const suivante = structuredClone(charge);
suivante.reservations = suivante.reservations.filter((r) => !designee(r, participant, designation));
return {
charge: suivante,
libelle: libelle('ENTREE_RETIRER_RESERVATION', { personne: nomAffiche(personne), numero: table.numero, portee, tour }),
avertissements: [],
};
}
/**
* deplacerReservation {participant, de: {table, portee, tour|null}, vers:
* {table, siege|null}} (§ 5.9) : la réservation désignée change de place et
* garde sa portée ; la pose se contrôle sans elle. Elle garde son rang dans
* la liste des réservations.
*/
function deplacerReservation(charge, { participant, de, vers } = {}) {
exigerModifiable(charge);
const personne = personneDe(charge, participant);
const depart = tableDe(charge, de.table);
const arrivee = tableDe(charge, vers.table);
const siege = vers.siege ?? null;
exigerSiege(siege);
const [premiere] = reservationsDesignees(charge, participant, de).sort(ordreDesReservations);
const places = capacite(charge, arrivee);
const siegeEcrit = !charge.reglages.attribuerSieges && siege !== null && siege > places ? null : siege;
const deplacee = { participant, table: vers.table, siege: siegeEcrit, portee: premiere.portee, tour: premiere.tour };
const { refus: refusPose, signaux } = controlerPose(charge, { ...deplacee, depuis: de });
leverRefus(refusPose);
const suivante = structuredClone(charge);
let remplacee = false;
suivante.reservations = suivante.reservations.flatMap((r) => {
if (!designee(r, participant, de)) return [r];
if (remplacee) return [];
remplacee = true;
return [deplacee];
});
return {
charge: suivante,
libelle: libelle('ENTREE_DEPLACER_RESERVATION', {
personne: nomAffiche(personne),
de: depart.numero,
vers: arrivee.numero,
siege: siegeEcrit,
portee: premiere.portee,
tour: premiere.tour,
}),
avertissements: avertissementsDe(signaux),
};
}
/**
* convertirEnTous {participant, table} (§ 4.2) : les R réservations « tour »
* de la personne à cette table deviennent une seule « tous les tours », au
* siège qu'elles portaient toutes, sans siège sinon. CONVERSION_IMPOSSIBLE
* hors de conversionsPossibles.
*/
function convertirEnTous(charge, { participant, table: idTable } = {}) {
exigerModifiable(charge);
const personne = personneDe(charge, participant);
const table = tableDe(charge, idTable);
if (!conversionsPossibles(charge).some((c) => c.participant === participant && c.table === idTable)) {
throw refus('CONVERSION_IMPOSSIBLE', { participant, table: idTable });
}
const parTour = (r) => r.participant === participant && r.table === idTable && r.portee === 'tour';
const sieges = new Set(charge.reservations.filter(parTour).map(({ siege }) => siege));
const siege = sieges.size === 1 ? [...sieges][0] : null;
const suivante = structuredClone(charge);
suivante.reservations = suivante.reservations.filter((r) => !parTour(r));
suivante.reservations.push({ participant, table: idTable, siege, portee: 'tous', tour: null });
return {
charge: suivante,
libelle: libelle('ENTREE_CONVERTIR_EN_TOUS', { personne: nomAffiche(personne), numero: table.numero }),
avertissements: [],
};
}
// --- Les titres et la désignation (§ 4, § 4.1, § 2.1 étape 6) ---------------
/**
* poserTitre {table, siege|null, libelle} (§ 4.1) : un titre à une place.
* Sans siège, le plus petit qui n'en porte pas. Une place titrée refuse
* PLACE_TITREE, remède le retrait de son titre.
*/
function poserTitre(charge, { table: idTable, siege = null, libelle: saisi } = {}) {
exigerModifiable(charge);
const table = tableDe(charge, idTable);
exigerSiege(siege);
const texte = libelleSaisi(saisi);
const places = capacite(charge, table);
const place = siege ?? siegeSansTitre(charge, idTable, places);
exigerSiegeDeTitre(idTable, place, places);
const titre = titreDe(charge, idTable, place);
if (titre !== undefined) {
throw refus('PLACE_TITREE', { table: idTable, siege: place, libelle: titre.libelle }, {
commande: 'retirerTitre',
arguments: { table: idTable, siege: place },
});
}
const suivante = structuredClone(charge);
suivante.titres.push({ table: idTable, siege: place, libelle: texte });
return {
charge: suivante,
libelle: libelle('ENTREE_POSER_TITRE', { libelle: texte, numero: table.numero, siege: place }),
avertissements: [],
};
}
/** retirerTitre {table, siege} : le premier titre de la place part ; TITRE_ABSENT sans lui. */
function retirerTitre(charge, { table: idTable, siege } = {}) {
exigerModifiable(charge);
const table = tableDe(charge, idTable);
const titre = titreDe(charge, idTable, siege);
if (titre === undefined) throw refus('TITRE_ABSENT', { table: idTable, siege });
const suivante = structuredClone(charge);
const rang = charge.titres.indexOf(titre);
suivante.titres.splice(rang, 1);
return {
charge: suivante,
libelle: libelle('ENTREE_RETIRER_TITRE', { libelle: titre.libelle, numero: table.numero, siege }),
avertissements: [],
};
}
/**
* designer {participant, table, siege|null, libelle} (§ 2.1 étape 6, § 5.8) :
* une réservation « tous les tours » et un titre au même siège, en une
* entrée. Sans siège, la place du premier titre de la table que rien ne
* pourvoit pour la soirée, sinon le plus petit siège sans titre. Un titre
* déjà posé à cette place est pourvu, et aucun second ne s'ajoute.
*/
function designer(charge, { participant, table: idTable, siege = null, libelle: saisi } = {}) {
exigerModifiable(charge);
const personne = personneDe(charge, participant);
const table = tableDe(charge, idTable);
exigerSiege(siege);
const texte = libelleSaisi(saisi);
const places = capacite(charge, table);
let place = siege;
if (place === null) {
const pourvoyeurs = pourvoyeursDesTitres(charge);
const libres = charge.titres
.map((titre, rang) => ({ titre, rang }))
.filter(({ titre, rang }) => titre.table === idTable && pourvoyeurs[rang].soiree === null)
.sort((a, b) => a.titre.siege - b.titre.siege);
place = libres.length > 0 ? libres[0].titre.siege : siegeSansTitre(charge, idTable, places);
}
exigerSiegeDeTitre(idTable, place, places);
const reservation = { participant, table: idTable, siege: place, portee: 'tous', tour: null };
const titre = titreDe(charge, idTable, place);
const dejaReservee = charge.reservations.some((r) => ordreDesReservations(r, reservation) === 0);
if (dejaReservee && titre !== undefined) throw refus('SANS_EFFET', { commande: 'designer' });
const { refus: refusPose, signaux } = controlerPose(charge, reservation);
leverRefus(refusPose);
const suivante = structuredClone(charge);
if (!dejaReservee) suivante.reservations.push(reservation);
if (titre === undefined) suivante.titres.push({ table: idTable, siege: place, libelle: texte });
return {
charge: suivante,
libelle: libelle('ENTREE_DESIGNER', {
personne: nomAffiche(personne),
numero: table.numero,
siege: place,
libelle: titre?.libelle ?? texte,
}),
avertissements: avertissementsDe(signaux),
};
}
// --- Le retenu (§ 7.1, § 8.9, § 9) -------------------------------------------
// Le retenu de la charge à ce tour, lisible : sans retenu, ou à un tour qu'il
// ne porte pas, TypeError — la cible d'etatPlaces ne l'offre jamais ; fautif,
// RETENU_FAUTIF, remède la version précédente du fichier (§ 8.8).
function exigerRetenu(charge, tour) {
const { retenu } = charge;
if (retenu === null) throw new TypeError('retenu : aucun placement retenu');
if (fautes(retenu).length > 0) throw refus('RETENU_FAUTIF', {}, { geste: 'ouvrirSecours', source: 'precedent' });
if (!Number.isSafeInteger(tour) || tour < 1 || tour > retenu.tours) {
throw new TypeError(`tour : de 1 à ${retenu.tours} attendu, reçu ${JSON.stringify(tour)}`);
}
}
// Où le retenu place id à ce tour : { table: rang de la liste, rang } à une
// table, 'reserve', ou null quand il ne le déclare pas.
function lieuDans(retenu, tour, id) {
if (!retenu.participants.includes(id)) return null;
const { sieges } = retenu.placement[tour - 1];
for (let table = 0; table < sieges.length; table += 1) {
const rang = sieges[table].indexOf(id);
if (rang >= 0) return { table, rang };
}
return 'reserve';
}
// Retire les null de fin d'une liste, en place.
function sansNullDeFin(liste) {
while (liste.length > 0 && liste.at(-1) === null) liste.pop();
}
// Déclare id dans le retenu qui ne le place pas encore : parmi ses
// participants, et en réserve à chaque tour — l'opérateur seul décide où il
// s'assied (§ 9).
function declarer(retenu, id) {
retenu.participants = [...retenu.participants, id].sort(croissant);
for (const tour of retenu.placement) tour.reserve = [...tour.reserve, id].sort(croissant);
}
// Ôte id de sa place au tour : sa chaise devient vide sous des sièges
// attribués, la liste se resserre sinon ; ou il quitte la réserve.
function oter(retenu, tour, id) {
const lieu = lieuDans(retenu, tour, id);
const etage = retenu.placement[tour - 1];
if (lieu === 'reserve') {
etage.reserve = etage.reserve.filter((autre) => autre !== id);
} else if (lieu !== null) {
const liste = etage.sieges[lieu.table];
if (retenu.siegesAttribues) {
liste[lieu.rang] = null;
sansNullDeFin(liste);
} else {
liste.splice(lieu.rang, 1);
}
}
}
// Verse id dans la réserve du tour.
function enReserve(retenu, tour, id) {
const etage = retenu.placement[tour - 1];
etage.reserve = [...etage.reserve, id].sort(croissant);
}
// Le rang de la table idTable dans le retenu, la table ajoutée vide à chaque
// tour quand il ne la déclare pas encore — une table créée après la
// retenue (A4 l'y ajoute aussi).
function rangDeTable(retenu, idTable, places) {
const rang = retenu.tables.indexOf(idTable);
if (rang >= 0) return rang;
retenu.tables.push(idTable);
retenu.capacites.push(places);
for (const tour of retenu.placement) tour.sieges.push([]);
return retenu.tables.length - 1;
}
// Clé d'une violation : ses champs, dans un ordre fixe.
const cleDeViolation = ({ code, participant, table, tour, detail }) => JSON.stringify([code, participant, table, tour, detail]);
// Les violations de verifierInvariants que suivant ajoute au retenu
// precedent, la réserve étant une place du plan (§ 12.6) : celles dont la
// clé n'y apparaît pas. Un retenu lisible déclare chaque table et chaque
// personne une fois, et verifierInvariants n'y rend jamais deux violations
// de même clé. [] quand la configuration ne se normalise pas.
function violationsNouvelles(charge, precedent, suivant) {
const instance = instanceDe(charge);
if (instance === null) return [];
const juger = (retenu) => verifierInvariants(instance, planDepuisFichier(retenu), { reserveAutorisee: true });
const connues = new Set(juger(precedent).map(cleDeViolation));
return juger(suivant).filter((violation) => !connues.has(cleDeViolation(violation)));
}
// La charge suivante d'une retouche, ou INVARIANT_VIOLE quand elle ajoute
// une violation au retenu reçu. Le versement en réserve des personnes que le
// plan n'assied pas (reduireAuxAssises, versLaReserveAffichee) n'en ajoute
// aucune : l'affichage assied d'abord, à leur table, les personnes qu'elle
// réserve, et une capacité ne descend jamais sous ses réservations (§ 6.2).
function retouche(charge, suivante) {
const violations = violationsNouvelles(charge, charge.retenu, suivante.retenu);
if (violations.length > 0) {
throw refus('INVARIANT_VIOLE', { proposition: charge.retenu.proposition, violations });
}
return suivante;
}
// Le numéro de la table où le retenu place id à ce tour, null en réserve,
// hors du retenu, ou à une table que la charge n'a plus.
function numeroDuLieu(charge, tour, id) {
const lieu = lieuDans(charge.retenu, tour, id);
if (lieu === null || lieu === 'reserve') return null;
return charge.tables.find(({ id: idTable }) => idTable === charge.retenu.tables[lieu.table])?.numero ?? null;
}
/**
* deplacerDansRetenu {tour, participant, table, siege|null} (§ 7.1, § 9) :
* la personne s'assied à cette table au tour donné, et sa place d'avant se
* libère. Sièges attribués, au siège donné — sans siège, la première chaise
* libre —, les rangs sautés à null ; la chaise d'un présent refuse
* SIEGE_OCCUPE, remède l'échange ; celle d'un absent — exclu, ou que la
* charge n'a plus — l'envoie en réserve. Sans attribution, la liste reste
* triée ; une table pleine de présents refuse TABLE_PLEINE, et, pleine
* d'une liste qui porte un absent, envoie le plus petit en réserve. Une
* personne que le retenu ne place pas y entre, en réserve aux autres tours.
* Le lieu de départ est celui que le plan du tour montre (placeDe) : une
* personne qu'il ne montre pas assise part de la réserve.
*/
function deplacerDansRetenu(charge, { tour, participant, table: idTable, siege = null } = {}) {
exigerModifiable(charge);
exigerRetenu(charge, tour);
const personne = personneDe(charge, participant);
const table = tableDe(charge, idTable);
exigerSiege(siege);
const places = capacite(charge, table);
if (siege !== null && siege > places) throw new TypeError(`siege : de 1 à ${places} attendu, reçu ${siege}`);
leverRefus(controlerPose(charge, { participant, table: idTable, siege, portee: 'tour', tour, retenu: true }).refus);
const ids = presents(charge);
const assisesDuTour = assisesAffichees(charge, tour);
const place = placeDe(charge, tour, participant, assisesDuTour);
const de = place === null ? null : numeroDuLieu(charge, tour, participant);
const memeTable = place !== null && place.table === idTable;
const suivante = structuredClone(charge);
const { retenu } = suivante;
const rangTable = rangDeTable(retenu, idTable, places);
if (lieuDans(retenu, tour, participant) === null) declarer(retenu, participant);
// La table d'arrivée n'a rien à verser : une liste qui porte plus de
// présents que de places refuse TABLE_PLEINE, et une autre les assied tous.
reduireAuxAssises(retenu, tour, rangsDesTables(retenu, tour, [participant]), assisesDuTour, ids);
versLaReserveAffichee(retenu, tour, participant, place);
const etage = retenu.placement[tour - 1];
let siegeLibelle = null;
if (retenu.siegesAttribues) {
const liste = etage.sieges[rangTable];
let cible = siege;
if (cible === null) {
cible = 1;
while (cible <= places && cible <= liste.length && liste[cible - 1] !== null && ids.has(liste[cible - 1])) cible += 1;
if (cible > places) throw refus('TABLE_PLEINE', { table: idTable, tour, capacite: places }, changerPlaces(idTable, places));
}
const occupant = liste[cible - 1] ?? null;
if (occupant === participant) throw refus('SANS_EFFET', { commande: 'deplacerDansRetenu' });
if (occupant !== null && ids.has(occupant)) {
throw refus('SIEGE_OCCUPE', { table: idTable, siege: cible, tour, occupant }, {
commande: 'echangerDansRetenu',
arguments: { tour, a: participant, b: occupant },
});
}
if (occupant !== null) {
liste[cible - 1] = null;
enReserve(retenu, tour, occupant);
}
oter(retenu, tour, participant);
const garnie = etage.sieges[rangTable];
while (garnie.length < cible) garnie.push(null);
garnie[cible - 1] = participant;
sansNullDeFin(garnie);
if (garnie.length > retenu.capacites[rangTable]) retenu.capacites[rangTable] = places;
if (memeTable) siegeLibelle = cible;
} else {
const liste = etage.sieges[rangTable];
if (liste.includes(participant)) throw refus('SANS_EFFET', { commande: 'deplacerDansRetenu' });
const assises = liste.filter((id) => ids.has(id)).length;
if (assises >= places) throw refus('TABLE_PLEINE', { table: idTable, tour, capacite: places }, changerPlaces(idTable, places));
if (liste.length >= places) {
const absent = liste.filter((id) => !ids.has(id)).sort(croissant)[0];
if (absent !== undefined) {
liste.splice(liste.indexOf(absent), 1);
enReserve(retenu, tour, absent);
}
}
oter(retenu, tour, participant);
const garnie = etage.sieges[rangTable];
garnie.push(participant);
garnie.sort(croissant);
if (garnie.length > retenu.capacites[rangTable]) retenu.capacites[rangTable] = places;
}
return {
charge: retouche(charge, suivante),
libelle: libelle('ENTREE_DEPLACER_DANS_RETENU', {
personne: nomAffiche(personne),
de,
vers: table.numero,
siege: siegeLibelle,
tour,
}),
avertissements: [],
};
}
// Les personnes que le plan du tour montre assises (etatPlaces, source le
// retenu). Une place du retenu à une table que la charge n'a plus, ou à un
// rang au-delà de sa capacité courante, n'y figure pas : elle se lit comme
// la réserve.
function assisesAffichees(charge, tour) {
const assises = new Set();
for (const { chaises } of etatPlaces(charge, { tour }).tables) {
for (const { occupant } of chaises) if (occupant !== null) assises.add(occupant);
}
return assises;
}
// La place qu'une personne occupe au tour telle que le plan la montre :
// { table, siege }, ou null quand le plan ne l'assied pas — en réserve, hors
// du retenu, ou absente d'assises (assisesAffichees) : à une table que la
// charge n'a plus, à un rang au-delà de la capacité courante, ou, sans
// attribution, parmi les présents qu'une liste porte au-delà des places. Le
// siège est le rang sous des sièges attribués ; sans attribution, celui de sa
// réservation à un siège à cette table, null sans elle. Les gestes du retenu
// y lisent le lieu de départ, et la pose de l'autre sur elle s'y contrôle.
function placeDe(charge, tour, id, assises) {
const lieu = lieuDans(charge.retenu, tour, id);
if (lieu === null || lieu === 'reserve' || !assises.has(id)) return null;
const idTable = charge.retenu.tables[lieu.table];
if (charge.retenu.siegesAttribues) return { table: idTable, siege: lieu.rang + 1 };
const tenue = charge.reservations.find(
(r) => r.participant === id && r.table === idTable && r.siege !== null && (r.portee === 'tous' || r.tour === tour),
);
return { table: idTable, siege: tenue?.siege ?? null };
}
// Verse dans la réserve du tour une personne que le retenu range à une table
// alors que le plan ne la montre pas assise (place null, de placeDe) : sa
// liste la perd, et le geste part de la réserve, comme l'affichage. Personne
// n'entre ainsi dans la liste d'une table que la charge n'a plus.
function versLaReserveAffichee(retenu, tour, id, place) {
const lieu = lieuDans(retenu, tour, id);
if (place === null && lieu !== null && lieu !== 'reserve') {
oter(retenu, tour, id);
enReserve(retenu, tour, id);
}
}
// Sans attribution, verse dans la réserve du tour chaque présent (ids) que
// le plan ne montre pas assis (assises) aux tables des rangs donnés : leur
// liste vaut alors ce que l'affichage y assied, et le tri qui suit le geste
// n'assied personne d'autre que ceux qu'il nomme. Sous des sièges attribués,
// le rang est le siège et ne glisse pas : rien ne change.
function reduireAuxAssises(retenu, tour, rangs, assises, ids) {
if (retenu.siegesAttribues) return;
const etage = retenu.placement[tour - 1];
for (const rang of rangs) {
for (const id of etage.sieges[rang].filter((autre) => ids.has(autre) && !assises.has(autre))) {
oter(retenu, tour, id);
enReserve(retenu, tour, id);
}
}
}
// Les rangs des tables où le retenu place ces personnes au tour.
function rangsDesTables(retenu, tour, personnes) {
return personnes.map((id) => lieuDans(retenu, tour, id)).filter((lieu) => lieu !== null && lieu !== 'reserve').map(({ table }) => table);
}
/**
* echangerDansRetenu {tour, a, b} (§ 7.1) : a prend la place de b et b celle
* de a, au tour donné ; une place peut être la réserve. Chaque arrivée se
* contrôle comme un dépôt : PLACE_RESERVEE, RESERVEE_AILLEURS. Une personne
* que le retenu ne place pas y entre, en réserve aux autres tours.
*/
function echangerDansRetenu(charge, { tour, a, b } = {}) {
exigerModifiable(charge);
exigerRetenu(charge, tour);
if (a === b) throw new TypeError('echangerDansRetenu : deux personnes distinctes attendues');
const personneA = personneDe(charge, a);
const personneB = personneDe(charge, b);
for (const personne of [personneA, personneB]) {
if (personne.exclu) {
throw refus('PERSONNE_EXCLUE', { participant: personne.id }, { commande: 'reintegrerParticipant', arguments: { id: personne.id } });
}
}
const assises = assisesAffichees(charge, tour);
const placeA = placeDe(charge, tour, a, assises);
const placeB = placeDe(charge, tour, b, assises);
if (placeA === null && placeB === null) throw refus('SANS_EFFET', { commande: 'echangerDansRetenu' });
const poses = [];
if (placeB !== null) poses.push(...controlerPose(charge, { participant: a, ...placeB, portee: 'tour', tour, retenu: true }).refus);
if (placeA !== null) poses.push(...controlerPose(charge, { participant: b, ...placeA, portee: 'tour', tour, retenu: true }).refus);
leverRefus(poses);
const deA = placeA === null ? null : numeroDuLieu(charge, tour, a);
const deB = placeB === null ? null : numeroDuLieu(charge, tour, b);
const suivante = structuredClone(charge);
const { retenu } = suivante;
for (const id of [a, b]) if (lieuDans(retenu, tour, id) === null) declarer(retenu, id);
reduireAuxAssises(retenu, tour, rangsDesTables(retenu, tour, [a, b]), assises, presents(charge));
// L'autre remplace dans la réserve une personne que le plan n'y montre pas assise.
versLaReserveAffichee(retenu, tour, a, placeA);
versLaReserveAffichee(retenu, tour, b, placeB);
const lieuA = lieuDans(retenu, tour, a);
const lieuB = lieuDans(retenu, tour, b);
const etage = retenu.placement[tour - 1];
const poser = (lieu, id) => {
if (lieu === 'reserve') enReserve(retenu, tour, id);
else etage.sieges[lieu.table][lieu.rang] = id;
};
if (lieuA === 'reserve') etage.reserve = etage.reserve.filter((id) => id !== a);
if (lieuB === 'reserve') etage.reserve = etage.reserve.filter((id) => id !== b);
poser(lieuB, a);
poser(lieuA, b);
if (!retenu.siegesAttribues) for (const liste of etage.sieges) liste.sort(croissant);
return {
charge: retouche(charge, suivante),
libelle: libelle('ENTREE_ECHANGER_DANS_RETENU', { a: nomAffiche(personneA), deA, b: nomAffiche(personneB), deB, tour }),
avertissements: [],
};
}
/**
* retirerDuTour {tour, participant} (§ 7.1) : la personne quitte sa table au
* tour donné et attend dans la réserve de ce tour ; sa chaise se libère.
* Une personne que le plan du tour ne montre pas assise (placeDe) — en
* réserve, hors du retenu, à une table que la charge n'a plus ou à un rang
* au-delà de sa capacité courante — refuse SANS_EFFET.
*/
function retirerDuTour(charge, { tour, participant } = {}) {
exigerModifiable(charge);
exigerRetenu(charge, tour);
const personne = personneDe(charge, participant);
if (personne.exclu) {
throw refus('PERSONNE_EXCLUE', { participant }, { commande: 'reintegrerParticipant', arguments: { id: participant } });
}
const assises = assisesAffichees(charge, tour);
if (placeDe(charge, tour, participant, assises) === null) {
throw refus('SANS_EFFET', { commande: 'retirerDuTour' });
}
const numero = numeroDuLieu(charge, tour, participant);
const suivante = structuredClone(charge);
reduireAuxAssises(suivante.retenu, tour, rangsDesTables(suivante.retenu, tour, [participant]), assises, presents(charge));
oter(suivante.retenu, tour, participant);
enReserve(suivante.retenu, tour, participant);
return {
charge: retouche(charge, suivante),
libelle: libelle('ENTREE_RETIRER_DU_TOUR', { personne: nomAffiche(personne), numero, tour }),
avertissements: [],
};
}
/** @type {Map<string, (charge: Object, args: Object) => Object>} */
2026-10-07 04:14:24 -04:00
export const COMMANDES_PLACES = new Map([
['reserver', reserver],
['retirerReservation', retirerReservation],
['deplacerReservation', deplacerReservation],
['poserTitre', poserTitre],
['retirerTitre', retirerTitre],
['designer', designer],
['convertirEnTous', convertirEnTous],
['deplacerDansRetenu', deplacerDansRetenu],
['echangerDansRetenu', echangerDansRetenu],
['retirerDuTour', retirerDuTour],
]);