259 lines
12 KiB
JavaScript
259 lines
12 KiB
JavaScript
|
|
// © 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 });
|
|||
|
|
}
|