The screens of iteration 3 need state they can subscribe to and commands that keep the plan sound. The session follows the Svelte store contract, unblocks a plan, cancels a gesture, keeps the framing, and orders the warnings of a gesture. The facade lists events, settles the working folder and runs copy, rename, import and delete. Places cover reservations, titles, designation and the retained plan: each gesture reads seats as the display shows them, reserved people keep their table, and a person removed frees her chair while every other seat stays. Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept. --- FR --- [ADD] application : séance observable, façade des événements, places Les écrans de l'itération 3 exigent un état auquel s'abonner et des commandes qui gardent le plan sain. La séance suit le contrat des magasins de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne les avertissements d'un geste. La façade liste les événements, règle le dossier de travail et mène copie, renommage, import et suppression. Les places couvrent réservations, titres, désignation et plan retenu : chaque geste lit les sièges comme l'écran les montre, une personne réservée garde sa table, et une personne retirée libère sa chaise sans déplacer les autres. Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture. Assisted-by: Claude Opus 5.5
258 lines
12 KiB
JavaScript
258 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 });
|
||
}
|