gestion_table_tournante_libre/src/application/pose.js

259 lines
12 KiB
JavaScript
Raw Normal View History

2026-10-07 04:14:24 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le contrôle de pose (§ 5.9) : ce que le logiciel refuse au moment du geste,
// avant toute écriture, et le seul — les commandes l'appellent, aucun
// composant ne le refait (§ 13.2). Le moteur ignore sièges et titres et
// s'arrête au premier refus par une exception ; controlerPose rend tous les
// refus d'une pose, chacun avec le remède qui le lève, écrit en entier : une
// commande et ses arguments, exécutables tels quels, ou un geste.
//
// Un refus a la forme { code, details }, le remède dans details.remede.
// Plusieurs se rangent dans l'ordre SURRESERVATION, SIEGE_INEXISTANT,
// DEUX_PLACES_MEME_TOUR, PERSONNE_EXCLUE, PLACE_RESERVEE, RESERVEE_AILLEURS ;
// une commande lève le premier et porte les suivants dans details.autres
// (leverRefus). Une réservation suspendue — celle d'une personne exclue —
// ne compte nulle part : une pose peut prendre sa place (§ 4.4), et sa
// réintégration se contrôle comme une pose (controlerReintegration). Aucune
// fonction ne modifie ce qu'elle reçoit.
import { ordreDesReservations } from '../csv/import.js';
import { capacite } from '../stockage/document.js';
import { ErreurCommande } from './erreurs.js';
import { couvre, instanceDe, reservationsActives } from './places.js';
// Un refus : son code, ses détails, et le remède parmi eux.
const refusDe = (code, details, remede) => ({ code, details: { ...details, remede } });
// Le remède d'une capacité trop petite : la table portée à sieges places,
// par la saisie de son champ.
const changerPlaces = (table, sieges) => ({ commande: 'changerPlacesTable', arguments: { id: table, sieges, origine: 'champ' } });
// Ce qui désigne une réservation dans une commande : sa table, sa portée, son
// tour, null pour la portée « tous ».
const designation = ({ table, portee, tour }) => ({ table, portee, tour: portee === 'tous' ? null : tour });
// Vrai quand la réservation r est celle que désigne d, pour ce participant.
const designee = (r, participant, d) =>
r.participant === participant && r.table === d.table && r.portee === d.portee && r.tour === (d.portee === 'tous' ? null : d.tour);
// Les tours qu'une réservation couvre, croissants.
function toursCouverts(charge, { portee, tour }) {
if (portee === 'tous') return Array.from({ length: charge.evenement.tours }, (_, rang) => rang + 1);
return [tour];
}
// L'enregistrement de la table d'identifiant id ; une table inconnue est une
// faute de l'appelant, que l'interface n'offre jamais.
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;
}
// Les personnes distinctes dont une réservation de la liste couvre le tour
// à cette table.
const reserveesAuTour = (actives, idTable, tour) =>
new Set(actives.filter((r) => r.table === idTable && couvre(r, tour)).map(({ participant }) => participant));
/**
* [r − 1] : les personnes distinctes non exclues dont une réservation couvre
* le tour r à cette table — le compte de SURRESERVATION du moteur, sans
* lever.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {number} idTable
* @returns {number[]} R comptes
*/
export function reservationsParTour(charge, idTable) {
const actives = reservationsActives(charge);
return Array.from({ length: charge.evenement.tours }, (_, rang) => reserveesAuTour(actives, idTable, rang + 1).size);
}
/**
* Contrôle d'une pose (§ 5.9), sans rien écrire ; pose : { participant,
* table, siege, portee, tour, depuis?, retenu? } — depuis : la réservation
* que la pose déplace, { table, portee, tour }, qui ne compte plus ;
* retenu : vrai pour un dépôt dans le placement retenu au tour donné, qui ne
* pose aucune réservation. Chaque remède s'écrit en entier. Rend tous les
* refus, dans l'ordre du contrat :
* - SURRESERVATION {table, tour, reservees, capacite} : le code et les
* détails de l'ErreurConfiguration du moteur, reservees compté après la
* pose, au premier tour couvert qui déborde ; remède la table portée à
* reservees places ;
* - SIEGE_INEXISTANT {table, siege, capacite} : sièges attribués seulement ;
* remède la table portée à siege places ;
* - DEUX_PLACES_MEME_TOUR {participant, tour, autre} : une autre réservation
* active de la personne couvre un tour de la pose, à une autre table, ou à
* un autre siège de la même ; remède le déplacement de cette réservation
* vers la place de la pose ;
* - PERSONNE_EXCLUE {participant} : remède la réintégration ;
* - PLACE_RESERVEE {table, siege, tour, titulaire, portee} : la réservation
* active d'une autre personne tient ce siège à un tour de la pose, dans les
* deux modes ; remède le choix d'une place pour la réservation de la
* titulaire ;
* - RESERVEE_AILLEURS {participant, tour, reservation} : dans le retenu, la
* personne est réservée à ce tour à une autre table — ou, sièges du retenu
* attribués, à un autre siège ; remède le déplacement de sa réservation
* vers le dépôt.
* Les trois premiers ne valent que pour une réservation. signaux porte
* TABLE_GELEE {table} quand, la pose faite, les ancrés de la table en
* occupent toutes les places (k_t = c_t) : accepté, mais rarement voulu.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {Object} pose
* @returns {{refus: Array<{code: string, details: Object}>, signaux: Array<{code: 'TABLE_GELEE', table: number}>}}
*/
export function controlerPose(charge, pose) {
const { participant, table: idTable, siege = null, portee, tour = null, depuis = null, retenu = false } = pose;
const places = capacite(charge, tableDe(charge, idTable));
const exclu = charge.participants.some(({ id, exclu: estExclu }) => id === participant && estExclu);
const couverts = retenu ? [tour] : toursCouverts(charge, { portee, tour });
const actives = reservationsActives(charge).filter((r) => depuis === null || !designee(r, participant, depuis));
const premierCommun = (r) => couverts.find((t) => couvre(r, t));
const refus = [];
if (!retenu) {
for (const t of couverts) {
const reservees = reserveesAuTour(actives, idTable, t);
if (!exclu) reservees.add(participant);
if (reservees.size > places) {
const details = { table: idTable, tour: t, reservees: reservees.size, capacite: places };
refus.push(refusDe('SURRESERVATION', details, changerPlaces(idTable, reservees.size)));
break;
}
}
if (charge.reglages.attribuerSieges && siege !== null && siege > places) {
refus.push(refusDe('SIEGE_INEXISTANT', { table: idTable, siege, capacite: places }, changerPlaces(idTable, siege)));
}
const autre = actives.find(
(r) =>
r.participant === participant &&
premierCommun(r) !== undefined &&
(r.table !== idTable || (r.siege !== null && siege !== null && r.siege !== siege)),
);
if (autre !== undefined) {
const remede = {
commande: 'deplacerReservation',
arguments: { participant, de: designation(autre), vers: { table: idTable, siege } },
};
const details = {
participant,
tour: premierCommun(autre),
autre: { table: autre.table, siege: autre.siege, portee: autre.portee, tour: autre.tour },
};
refus.push(refusDe('DEUX_PLACES_MEME_TOUR', details, remede));
}
}
if (exclu) {
refus.push(refusDe('PERSONNE_EXCLUE', { participant }, { commande: 'reintegrerParticipant', arguments: { id: participant } }));
}
if (siege !== null) {
const titulaire = actives.find(
(r) => r.participant !== participant && r.table === idTable && r.siege === siege && premierCommun(r) !== undefined,
);
if (titulaire !== undefined) refus.push(placeReservee(titulaire, premierCommun(titulaire)));
}
if (retenu && !exclu) {
const attribues = charge.retenu?.siegesAttribues === true;
const propre = actives.find(
(r) =>
r.participant === participant &&
couvre(r, tour) &&
(r.table !== idTable || (attribues && r.siege !== null && siege !== null && r.siege !== siege)),
);
if (propre !== undefined) {
const remede = {
commande: 'deplacerReservation',
arguments: { participant, de: designation(propre), vers: { table: idTable, siege } },
};
const reservation = { table: propre.table, siege: propre.siege, portee: propre.portee, tour: propre.tour };
refus.push(refusDe('RESERVEE_AILLEURS', { participant, tour, reservation }, remede));
}
}
return { refus, signaux: retenu ? [] : signauxDe(charge, pose, places) };
}
// PLACE_RESERVEE de la réservation r d'une titulaire, au tour donné : le
// remède fait choisir une place sur le plan, l'écrit dans
// puis.arguments.vers, puis déplace la réservation de la titulaire.
function placeReservee(r, tour) {
const remede = {
geste: 'choisirPlace',
puis: { commande: 'deplacerReservation', arguments: { participant: r.participant, de: designation(r) } },
};
return refusDe('PLACE_RESERVEE', { table: r.table, siege: r.siege, tour, titulaire: r.participant, portee: r.portee }, remede);
}
// TABLE_GELEE quand la charge où la pose serait faite se normalise et que
// les ancrés de la table en occupent toutes les places.
function signauxDe(charge, { participant, table: idTable, siege = null, portee, tour = null, depuis = null }, places) {
const gardees = charge.reservations.filter((r) => depuis === null || !designee(r, participant, depuis));
const posee = { participant, table: idTable, siege, portee, tour: portee === 'tous' ? null : tour };
const instance = instanceDe({ ...charge, reservations: [...gardees, posee] });
if (instance === null || instance.ancresParTable[instance.indexTableDe.get(idTable)] !== places) return [];
return [{ code: 'TABLE_GELEE', table: idTable }];
}
/**
* Les refus de la réintégration de id (§ 4.4, § 5.9) : ses réservations
* suspendues, posées contre les actives. SURRESERVATION quand une table ne
* peut plus les recevoir, au premier tour qui déborde, une fois par table,
* dans l'ordre de ses réservations ; puis PLACE_RESERVEE, une par
* réservation à un siège qu'une autre personne a pris depuis à un tour
* qu'elle couvre. [] quand rien ne s'y oppose, et pour une personne qui
* n'est pas exclue.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {number} id
* @returns {Array<{code: string, details: Object}>}
*/
export function controlerReintegration(charge, id) {
if (!charge.participants.some((personne) => personne.id === id && personne.exclu)) return [];
const apres = {
...charge,
participants: charge.participants.map((personne) => (personne.id === id ? { ...personne, exclu: false } : personne)),
};
const siennes = charge.reservations.filter(({ participant }) => participant === id).sort(ordreDesReservations);
const refus = [];
const vues = new Set();
for (const { table: idTable } of siennes) {
if (vues.has(idTable)) continue;
vues.add(idTable);
const places = capacite(apres, tableDe(apres, idTable));
const comptes = reservationsParTour(apres, idTable);
const rang = comptes.findIndex((n) => n > places);
if (rang >= 0) {
const details = { table: idTable, tour: rang + 1, reservees: comptes[rang], capacite: places };
refus.push(refusDe('SURRESERVATION', details, changerPlaces(idTable, comptes[rang])));
}
}
const actives = reservationsActives(charge);
for (const sienne of siennes) {
if (sienne.siege === null) continue;
const couverts = toursCouverts(charge, sienne);
const autre = actives.find(
(r) => r.table === sienne.table && r.siege === sienne.siege && couverts.some((t) => couvre(r, t)),
);
if (autre !== undefined) refus.push(placeReservee(autre, couverts.find((t) => couvre(autre, t))));
}
return refus;
}
/**
* Lève le premier refus d'une liste, en ErreurCommande de son code et de ses
* détails, remède compris, les suivants dans details.autres ; ne fait rien
* d'une liste vide.
*
* @param {Array<{code: string, details: Object}>} refus
* @throws {ErreurCommande}
*/
export function leverRefus(refus) {
if (refus.length === 0) return;
const [premier, ...autres] = refus;
throw new ErreurCommande(premier.code, { ...premier.details, autres });
}