403 lines
21 KiB
JavaScript
403 lines
21 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Le contrôle de capacité (§ 6.2, § 7.1) : ce qu'une capacité de table
|
|||
|
|
// refuse et ce dont elle avertit, et le seul — les commandes du mobilier et
|
|||
|
|
// leurs bilans l'appellent, aucun composant ne le refait (§ 13.2). Aussi la
|
|||
|
|
// seule lecture d'un entier saisi (lireEntier), et la réécriture du retenu
|
|||
|
|
// qu'une capacité changée entraîne (retenuAuxCapacites).
|
|||
|
|
//
|
|||
|
|
// Un refus a la forme { code, details }, le remède dans details.remede. Ils
|
|||
|
|
// se contrôlent dans cet ordre, et le premier seul est rendu :
|
|||
|
|
// MOINS_DE_DEUX ; SOUS_RESERVATIONS, sous le plus grand compte de
|
|||
|
|
// reservationsParTour ; SIEGES_RETIRES, un siège au-delà de la capacité qui
|
|||
|
|
// porte un titre ou une réservation, suspendue comprise — l'analyse du
|
|||
|
|
// fichier refuse un tel siège (§ 6.2) — ; SOUS_OCCUPES, pour la poignée, sous
|
|||
|
|
// l'occupation de chaque tour telle qu'etatPlaces la montre. Le champ et la
|
|||
|
|
// poignée décrivent deux gestes (§ 6.2, § 7.1) : la poignée, geste de
|
|||
|
|
// géométrie fait sans regarder les autres tours, ne désassoit personne ; le
|
|||
|
|
// champ, saisie délibérée, verse dans la réserve les occupants des chaises
|
|||
|
|
// retirées et le dit (SURNUMERAIRES).
|
|||
|
|
//
|
|||
|
|
// Le retenu suit les capacités : ce qui ne tient plus va dans la réserve de
|
|||
|
|
// chaque tour, puis la capacité s'écrit dans retenu.capacites, de sorte que
|
|||
|
|
// le retenu ne gagne aucune raison de dérive. Sièges attribués, le siège est
|
|||
|
|
// le rang : une personne que la table réserve à ce tour, assise sur une
|
|||
|
|
// chaise que le plan montrait et que la nouvelle capacité retire, revient
|
|||
|
|
// d'abord sur une chaise libre dans la capacité, sinon sur celle du plus
|
|||
|
|
// haut rang qu'occupe un non-réservé ; puis les rangs au-delà de la capacité
|
|||
|
|
// partent, et la liste perd ses chaises vides de fin. Aucune réservation que
|
|||
|
|
// le plan montrait tenue ne cesse ainsi de l'être, et un rang au-delà de la
|
|||
|
|
// capacité courante, que le plan ne montre pas, n'assied personne. Sans
|
|||
|
|
// attribution, la liste garde les personnes que
|
|||
|
|
// le plan montre assises à la table avant et après le geste — l'affichage
|
|||
|
|
// élit d'abord celles que la table réserve —, puis, tant qu'elle dépasse,
|
|||
|
|
// perd ses absents, le plus petit identifiant d'abord : un geste n'assied
|
|||
|
|
// personne qu'il ne nomme. Un retenu fautif ne se réécrit pas. Aucune
|
|||
|
|
// fonction ne modifie ce qu'elle reçoit.
|
|||
|
|
import { ordreDesReservations } from '../csv/import.js';
|
|||
|
|
import { constante } from '../geometrie/constantes.js';
|
|||
|
|
import { capacite } from '../stockage/document.js';
|
|||
|
|
import { fautes } from '../stockage/placements.js';
|
|||
|
|
import { couvre, etatPlaces, presents, repartirSieges, reservationsActives } from './places.js';
|
|||
|
|
import { reservationsParTour } from './pose.js';
|
|||
|
|
|
|||
|
|
const croissant = (a, b) => a - b;
|
|||
|
|
const comparerTextes = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|||
|
|
|
|||
|
|
// Un entier écrit en chiffres décimaux seuls : ni signe, ni séparateur, ni
|
|||
|
|
// exposant.
|
|||
|
|
const CHIFFRES = /^[0-9]+$/;
|
|||
|
|
|
|||
|
|
const ORIGINES = new Set(['champ', 'poignee']);
|
|||
|
|
|
|||
|
|
// Le seuil de lisibilité, nommé dans le registre des constantes mesurées
|
|||
|
|
// (§ 14.1) : un nom de constante, que la table des libellés ne connaît pas.
|
|||
|
|
const SEUIL_LISIBLE = { nom: 'SEUIL_SIEGES_LISIBLES' };
|
|||
|
|
|
|||
|
|
const refusDe = (code, details, remede) => ({ code, details: { ...details, remede } });
|
|||
|
|
const avertissement = (code, details) => ({ code, details });
|
|||
|
|
|
|||
|
|
// Le remède d'une capacité sous un compte : montrer le tour qui l'atteint,
|
|||
|
|
// sur le plan du retenu ou des réservations.
|
|||
|
|
const afficherTour = (tour) => ({ geste: 'afficherTour', tour, proposition: null });
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Lit un entier saisi (§ 6.2) : des chiffres décimaux seuls, blancs de bord
|
|||
|
|
* admis, ou un nombre entier ≥ 0 ; tout autre texte — signe, décimale,
|
|||
|
|
* virgule, lettres, vide, ou un entier au-delà de 2^53 − 1 — refuse
|
|||
|
|
* NON_ENTIER {saisie}, remède null. Le domaine de la valeur — au moins 2
|
|||
|
|
* places, au moins un tour — appartient à son contrôle.
|
|||
|
|
*
|
|||
|
|
* @param {string|number} saisie
|
|||
|
|
* @returns {{valeur: number}|{refus: {code: 'NON_ENTIER', details: {saisie: string|number, remede: null}}}}
|
|||
|
|
* @throws {TypeError} une saisie qui n'est ni un texte ni un nombre
|
|||
|
|
*/
|
|||
|
|
export function lireEntier(saisie) {
|
|||
|
|
if (typeof saisie === 'number') {
|
|||
|
|
if (Number.isSafeInteger(saisie) && saisie >= 0) return { valeur: saisie + 0 };
|
|||
|
|
} else if (typeof saisie === 'string') {
|
|||
|
|
const texte = saisie.trim();
|
|||
|
|
const valeur = CHIFFRES.test(texte) ? Number(texte) : Number.NaN;
|
|||
|
|
if (Number.isSafeInteger(valeur)) return { valeur };
|
|||
|
|
} else {
|
|||
|
|
throw new TypeError(`lireEntier : texte ou nombre attendu, reçu ${JSON.stringify(saisie)}`);
|
|||
|
|
}
|
|||
|
|
return { refus: refusDe('NON_ENTIER', { saisie }, null) };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le plus grand compte d'une liste par tour, et le premier tour qui
|
|||
|
|
// l'atteint.
|
|||
|
|
function maximum(comptes) {
|
|||
|
|
const n = Math.max(0, ...comptes);
|
|||
|
|
return { n, tour: comptes.indexOf(n) + 1 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les sièges au-delà de sieges qui portent une saisie, par numéro
|
|||
|
|
// décroissant ; à un même siège, ses titres par libellé, puis ses
|
|||
|
|
// réservations dans l'ordre canonique. Une réservation suspendue — sa
|
|||
|
|
// personne exclue — y est, sièges attribués ou non.
|
|||
|
|
function siegesPortes(charge, idTable, sieges) {
|
|||
|
|
const ids = presents(charge);
|
|||
|
|
const titres = charge.titres.filter(({ table, siege }) => table === idTable && siege > sieges);
|
|||
|
|
const reservations = charge.reservations.filter(({ table, siege }) => table === idTable && siege !== null && siege > sieges);
|
|||
|
|
const numeros = [...new Set([...titres, ...reservations].map(({ siege }) => siege))].sort((a, b) => b - a);
|
|||
|
|
return numeros.flatMap((numero) => [
|
|||
|
|
...titres
|
|||
|
|
.filter(({ siege }) => siege === numero)
|
|||
|
|
.sort((a, b) => comparerTextes(a.libelle, b.libelle))
|
|||
|
|
.map(({ siege, libelle }) => ({ siege, cause: 'SIEGE_TITRE', libelle })),
|
|||
|
|
...reservations
|
|||
|
|
.filter(({ siege }) => siege === numero)
|
|||
|
|
.sort(ordreDesReservations)
|
|||
|
|
.map(({ siege, participant }) => ({ siege, cause: 'SIEGE_RESERVE', participant, suspendue: !ids.has(participant) })),
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le remède d'un siège retiré : le retrait de son titre, ou de sa
|
|||
|
|
// réservation, désignée par ses quatre arguments.
|
|||
|
|
function remedeDuSiege(charge, idTable, retire) {
|
|||
|
|
if (retire.cause === 'SIEGE_TITRE') return { commande: 'retirerTitre', arguments: { table: idTable, siege: retire.siege } };
|
|||
|
|
const { portee, tour } = charge.reservations
|
|||
|
|
.filter((r) => r.table === idTable && r.siege === retire.siege && r.participant === retire.participant)
|
|||
|
|
.sort(ordreDesReservations)[0];
|
|||
|
|
return { commande: 'retirerReservation', arguments: { participant: retire.participant, table: idTable, portee, tour } };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// L'occupation de la table à chaque tour, lue dans etatPlaces : les chaises
|
|||
|
|
// qui portent un occupant ou une réservation ; sièges attribués, le plus
|
|||
|
|
// haut numéro de ces chaises quand il est plus grand.
|
|||
|
|
function occupation(charge, idTable) {
|
|||
|
|
return Array.from({ length: charge.evenement.tours }, (_, rang) => {
|
|||
|
|
const etat = etatPlaces(charge, { tour: rang + 1 });
|
|||
|
|
const { chaises } = etat.tables.find(({ id }) => id === idTable);
|
|||
|
|
const portees = chaises.filter(({ occupant, reservation }) => occupant !== null || reservation !== null);
|
|||
|
|
const plusHaut = portees.length === 0 ? 0 : portees.at(-1).siege;
|
|||
|
|
return etat.siegesAttribues ? Math.max(portees.length, plusHaut) : portees.length;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le premier refus d'une capacité, ou null.
|
|||
|
|
function refusDeCapacite(charge, idTable, sieges, origine) {
|
|||
|
|
if (sieges < 2) return refusDe('MOINS_DE_DEUX', {}, null);
|
|||
|
|
const reservees = maximum(reservationsParTour(charge, idTable));
|
|||
|
|
if (sieges < reservees.n) {
|
|||
|
|
return refusDe('SOUS_RESERVATIONS', { table: idTable, ...reservees }, afficherTour(reservees.tour));
|
|||
|
|
}
|
|||
|
|
const retires = siegesPortes(charge, idTable, sieges);
|
|||
|
|
if (retires.length > 0) {
|
|||
|
|
const titres = charge.reglages.attribuerSieges ? {} : { titres: retires.filter(({ cause }) => cause === 'SIEGE_TITRE').length };
|
|||
|
|
return refusDe('SIEGES_RETIRES', { table: idTable, sieges: retires, ...titres }, remedeDuSiege(charge, idTable, retires[0]));
|
|||
|
|
}
|
|||
|
|
if (origine === 'poignee') {
|
|||
|
|
const occupees = maximum(occupation(charge, idTable));
|
|||
|
|
if (sieges < occupees.n) return refusDe('SOUS_OCCUPES', { table: idTable, ...occupees }, afficherTour(occupees.tour));
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les personnes que le plan montre assises à la table au tour, dans l'ordre
|
|||
|
|
// de leurs chaises : celles d'etatPlaces (etat) pour un tour de la soirée.
|
|||
|
|
// Un tour du retenu que la soirée n'a plus ne se montre pas : il se lit
|
|||
|
|
// selon la même règle, la liste de la source répartie aux places courantes
|
|||
|
|
// par repartirSieges, les réservations « tous » de la table seules
|
|||
|
|
// couvrantes.
|
|||
|
|
function assises(charge, etat, idTable, tour) {
|
|||
|
|
if (etat !== null) {
|
|||
|
|
return etat.tables
|
|||
|
|
.find(({ id }) => id === idTable)
|
|||
|
|
.chaises.map(({ occupant }) => occupant)
|
|||
|
|
.filter((occupant) => occupant !== null);
|
|||
|
|
}
|
|||
|
|
const { retenu } = charge;
|
|||
|
|
const ids = presents(charge);
|
|||
|
|
const places = capacite(charge, tableDe(charge, idTable));
|
|||
|
|
const liste = retenu.placement[tour - 1].sieges[retenu.tables.indexOf(idTable)];
|
|||
|
|
if (retenu.siegesAttribues) return liste.slice(0, places).filter((id) => id !== null && ids.has(id));
|
|||
|
|
const tous = reservationsActives(charge).filter(({ table, portee }) => table === idTable && portee === 'tous');
|
|||
|
|
const siegeDe = new Map();
|
|||
|
|
for (const { participant, siege } of tous) if (siege !== null && !siegeDe.has(participant)) siegeDe.set(participant, siege);
|
|||
|
|
const prioritaires = new Set(tous.map(({ participant }) => participant));
|
|||
|
|
return repartirSieges(liste.filter((id) => ids.has(id)), siegeDe, places, prioritaires).filter((id) => id !== null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sièges attribués, la liste de la table réduite à ses limite premiers
|
|||
|
|
// rangs, sans chaise vide de fin. reservees (Map personne → siège réservé ou
|
|||
|
|
// null) : les personnes que la table réserve à ce tour. Chacune, assise à un
|
|||
|
|
// rang que le plan montrait — de limite à montres, la capacité courante — et
|
|||
|
|
// que la réduction retire, revient, dans l'ordre des rangs, à son siège
|
|||
|
|
// réservé quand il est libre, sinon à la première chaise libre — vide, ou
|
|||
|
|
// d'un absent, qui passe alors en réserve —, sinon à la chaise du plus haut
|
|||
|
|
// rang qu'occupe un non-réservé, qui passe en réserve. Un rang à partir de
|
|||
|
|
// montres ne ramène personne : une hausse (limite égale à montres) n'en
|
|||
|
|
// ramène aucun. La capacité a passé SIEGES_RETIRES et SOUS_RESERVATIONS :
|
|||
|
|
// chaque siège réservé est dans limite, et les réservés de la table à ce
|
|||
|
|
// tour n'y sont pas plus nombreux que les chaises ; une chaise se trouve
|
|||
|
|
// toujours.
|
|||
|
|
function ramenerReservees(liste, limite, montres, reservees, ids) {
|
|||
|
|
const reste = Array.from({ length: limite }, (_, rang) => liste[rang] ?? null);
|
|||
|
|
const libre = (occupant) => occupant === null || !ids.has(occupant);
|
|||
|
|
for (const id of liste.slice(limite, montres)) {
|
|||
|
|
if (id === null || !reservees.has(id)) continue;
|
|||
|
|
const voulu = reservees.get(id);
|
|||
|
|
let rang = voulu !== null && libre(reste[voulu - 1]) ? voulu - 1 : reste.findIndex(libre);
|
|||
|
|
if (rang < 0) rang = reste.findLastIndex((occupant) => !reservees.has(occupant));
|
|||
|
|
reste[rang] = id;
|
|||
|
|
}
|
|||
|
|
while (reste.length > 0 && reste.at(-1) === null) reste.pop();
|
|||
|
|
return reste;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les personnes que la table idTable réserve au tour, chacune avec son siège
|
|||
|
|
// réservé ou null : les réservations actives (actives, dans l'ordre
|
|||
|
|
// canonique) qui couvrent ce tour ; un tour que la soirée n'a plus, l'analyse
|
|||
|
|
// n'admettant aucune portée « tour » au-delà des tours, seules les « tous »
|
|||
|
|
// le couvrent. Le siège réservé d'une personne est le premier siège non nul
|
|||
|
|
// de ses réservations, comme l'affichage le lit.
|
|||
|
|
function reserveesAuTour(actives, idTable, tour) {
|
|||
|
|
const reservees = new Map();
|
|||
|
|
for (const reservation of actives) {
|
|||
|
|
const { table, participant, siege } = reservation;
|
|||
|
|
if (table !== idTable || !couvre(reservation, tour)) continue;
|
|||
|
|
if (!reservees.has(participant) || reservees.get(participant) === null) reservees.set(participant, siege);
|
|||
|
|
}
|
|||
|
|
return reservees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La charge où chaque table de changements (Map identifiant → places) porte
|
|||
|
|
// ses nouvelles places, le retenu tel quel.
|
|||
|
|
const avecCapacites = (charge, changements) => ({
|
|||
|
|
...charge,
|
|||
|
|
tables: charge.tables.map((table) => (changements.has(table.id) ? { ...table, sieges: changements.get(table.id) } : table)),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Ce que de nouvelles capacités font au retenu de la charge, par table de
|
|||
|
|
* changements qu'il déclare, dans l'ordre de charge.tables, et par tour du
|
|||
|
|
* retenu : la liste qui reste, ce qui en est versé à la réserve, et les
|
|||
|
|
* personnes que le plan du tour montrait assises à la table et qui passent
|
|||
|
|
* dans la réserve, dans l'ordre de leurs chaises — pour un tour que la
|
|||
|
|
* soirée n'a plus, celles qu'assises y lit. null sans retenu, ou pour un
|
|||
|
|
* retenu fautif, qui ne se réécrit pas.
|
|||
|
|
*
|
|||
|
|
* @param {import('../stockage/types.js').Charge} charge
|
|||
|
|
* @param {Map<number, number>} changements identifiant de table → places
|
|||
|
|
* @returns {null|Array<{table: number, rang: number, sieges: number,
|
|||
|
|
* tours: Array<{tour: number, liste: Array<number|null>, verses: number[], personnes: number[]}>}>}
|
|||
|
|
*/
|
|||
|
|
function versements(charge, changements) {
|
|||
|
|
const { retenu } = charge;
|
|||
|
|
if (retenu === null || fautes(retenu).length > 0) return null;
|
|||
|
|
const hypothese = avecCapacites(charge, changements);
|
|||
|
|
const ids = presents(charge);
|
|||
|
|
const actives = reservationsActives(charge);
|
|||
|
|
const touchees = charge.tables
|
|||
|
|
.filter(({ id }) => changements.has(id) && retenu.tables.includes(id))
|
|||
|
|
.map(({ id }) => ({ table: id, rang: retenu.tables.indexOf(id), sieges: changements.get(id), tours: [] }));
|
|||
|
|
for (let tour = 1; tour <= retenu.tours; tour += 1) {
|
|||
|
|
const visible = tour <= charge.evenement.tours;
|
|||
|
|
const etatAvant = visible ? etatPlaces(charge, { tour }) : null;
|
|||
|
|
// Sièges attribués, ce qui reste ne se lit pas dans le plan d'après.
|
|||
|
|
const etatApres = visible && !retenu.siegesAttribues ? etatPlaces(hypothese, { tour }) : null;
|
|||
|
|
for (const touchee of touchees) {
|
|||
|
|
const { table, rang, sieges } = touchee;
|
|||
|
|
const liste = retenu.placement[tour - 1].sieges[rang];
|
|||
|
|
const avant = assises(charge, etatAvant, table, tour);
|
|||
|
|
let reste;
|
|||
|
|
if (retenu.siegesAttribues) {
|
|||
|
|
// Le siège est le rang : un rang au-delà des places d'aujourd'hui ne
|
|||
|
|
// se montre pas, et une hausse ne l'assied pas.
|
|||
|
|
const montres = capacite(charge, tableDe(charge, table));
|
|||
|
|
const limite = Math.min(sieges, montres);
|
|||
|
|
reste = ramenerReservees(liste, limite, montres, reserveesAuTour(actives, table, tour), ids);
|
|||
|
|
} else {
|
|||
|
|
const apres = new Set(assises(hypothese, etatApres, table, tour));
|
|||
|
|
const gardees = new Set(avant.filter((id) => apres.has(id)));
|
|||
|
|
reste = liste.filter((id) => !ids.has(id) || gardees.has(id));
|
|||
|
|
const absents = reste.filter((id) => !ids.has(id)).sort(croissant);
|
|||
|
|
// Les présents gardés tiennent dans sieges : ôter les absents suffit.
|
|||
|
|
while (reste.length > sieges && absents.length > 0) {
|
|||
|
|
const absent = absents.shift();
|
|||
|
|
reste = reste.filter((id) => id !== absent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const restants = new Set(reste);
|
|||
|
|
const verses = liste.filter((id) => id !== null && !restants.has(id));
|
|||
|
|
const personnes = avant.filter((id) => !restants.has(id));
|
|||
|
|
touchee.tours.push({ tour, liste: reste, verses, personnes });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return touchees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Le retenu de la charge aux nouvelles capacités (§ 6.2, § 9) : à chaque
|
|||
|
|
* tour, ce qui ne tient plus versé dans la réserve, puis la capacité de
|
|||
|
|
* chaque table touchée écrite dans retenu.capacites. Un objet neuf ; le
|
|||
|
|
* retenu reçu tel quel sans retenu, pour un retenu fautif, ou quand il ne
|
|||
|
|
* déclare aucune table touchée.
|
|||
|
|
*
|
|||
|
|
* @param {import('../stockage/types.js').Charge} charge
|
|||
|
|
* @param {Map<number, number>} changements identifiant de table → places
|
|||
|
|
* @returns {import('../stockage/types.js').Retenu|null}
|
|||
|
|
*/
|
|||
|
|
export function retenuAuxCapacites(charge, changements) {
|
|||
|
|
const touchees = versements(charge, changements);
|
|||
|
|
if (touchees === null || touchees.length === 0) return charge.retenu;
|
|||
|
|
const retenu = structuredClone(charge.retenu);
|
|||
|
|
for (const { rang, sieges, tours } of touchees) {
|
|||
|
|
for (const { tour, liste, verses } of tours) {
|
|||
|
|
const etage = retenu.placement[tour - 1];
|
|||
|
|
etage.sieges[rang] = liste;
|
|||
|
|
etage.reserve = [...etage.reserve, ...verses].sort(croissant);
|
|||
|
|
}
|
|||
|
|
retenu.capacites[rang] = sieges;
|
|||
|
|
}
|
|||
|
|
return retenu;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les refus et les avertissements de plusieurs capacités à la fois, chacune
|
|||
|
|
* contrôlée comme le ferait controlerCapacite, sur la même charge : les
|
|||
|
|
* tables ne partagent aucune chaise. Les tables de changements dans l'ordre
|
|||
|
|
* de charge.tables ; refus, le premier de chaque table qui refuse ;
|
|||
|
|
* avertissements, ceux des tables qui ne refusent pas.
|
|||
|
|
*
|
|||
|
|
* @param {import('../stockage/types.js').Charge} charge
|
|||
|
|
* @param {Map<number, number>} changements identifiant de table → places
|
|||
|
|
* @param {{origine: 'champ'|'poignee'}} options
|
|||
|
|
* @returns {{refus: Array<{table: number, code: string, details: Object}>,
|
|||
|
|
* avertissements: Array<{table: number, code: string, details: Object}>}}
|
|||
|
|
* @throws {TypeError} une table inconnue, des places qui ne sont pas un entier, une origine inconnue
|
|||
|
|
*/
|
|||
|
|
export function controlerCapacites(charge, changements, { origine }) {
|
|||
|
|
if (!ORIGINES.has(origine)) throw new TypeError(`origine : 'champ' ou 'poignee' attendue, reçu ${JSON.stringify(origine)}`);
|
|||
|
|
for (const [idTable, sieges] of changements) {
|
|||
|
|
tableDe(charge, idTable);
|
|||
|
|
if (!Number.isSafeInteger(sieges)) throw new TypeError(`places : entier attendu, reçu ${JSON.stringify(sieges)}`);
|
|||
|
|
}
|
|||
|
|
const ordre = charge.tables.filter(({ id }) => changements.has(id)).map(({ id }) => id);
|
|||
|
|
const refus = [];
|
|||
|
|
const acceptees = new Map();
|
|||
|
|
for (const idTable of ordre) {
|
|||
|
|
const premier = refusDeCapacite(charge, idTable, changements.get(idTable), origine);
|
|||
|
|
if (premier === null) acceptees.set(idTable, changements.get(idTable));
|
|||
|
|
else refus.push({ table: idTable, ...premier });
|
|||
|
|
}
|
|||
|
|
const parTable = new Map(ordre.map((id) => [id, []]));
|
|||
|
|
for (const { table, tours } of versements(charge, acceptees) ?? []) {
|
|||
|
|
for (const { tour, personnes } of tours) {
|
|||
|
|
if (personnes.length > 0) parTable.get(table).push(avertissement('SURNUMERAIRES', { table, tour, personnes }));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const seuil = constante(SEUIL_LISIBLE.nom);
|
|||
|
|
const avertissements = [];
|
|||
|
|
for (const [idTable, sieges] of acceptees) {
|
|||
|
|
for (const { code, details } of parTable.get(idTable)) avertissements.push({ table: idTable, code, details });
|
|||
|
|
if (sieges > seuil) avertissements.push({ table: idTable, code: 'AU_DELA_LISIBLE', details: { seuil } });
|
|||
|
|
}
|
|||
|
|
return { refus, avertissements };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Le contrôle d'une capacité (§ 6.2, § 7.1), sans rien écrire : sieges
|
|||
|
|
* places pour la table idTable, saisies au champ ou tirées à la poignée.
|
|||
|
|
* refus, le premier dans l'ordre de l'en-tête du module, ou null ;
|
|||
|
|
* avertissements, quand il n'y a pas de refus : SURNUMERAIRES {table, tour,
|
|||
|
|
* personnes}, un par tour du retenu — un tour que la soirée n'a plus
|
|||
|
|
* compris — où il verse dans la réserve des personnes que le plan montrait
|
|||
|
|
* assises à la table, nommées dans
|
|||
|
|
* l'ordre de leurs chaises — la poignée, qui refuse sous l'occupation, n'en
|
|||
|
|
* verse aucune ; puis AU_DELA_LISIBLE {seuil} au-delà de
|
|||
|
|
* SEUIL_SIEGES_LISIBLES places.
|
|||
|
|
*
|
|||
|
|
* Refus : MOINS_DE_DEUX {}, remède null ; SOUS_RESERVATIONS {table, n,
|
|||
|
|
* tour} et SOUS_OCCUPES {table, n, tour}, tour le premier qui atteint n,
|
|||
|
|
* remède { geste: 'afficherTour', tour, proposition: null } ;
|
|||
|
|
* SIEGES_RETIRES {table, sieges, titres?} — chaque siège retiré qui porte un
|
|||
|
|
* titre {siege, cause: 'SIEGE_TITRE', libelle} ou une réservation {siege,
|
|||
|
|
* cause: 'SIEGE_RESERVE', participant, suspendue}, par numéro décroissant ;
|
|||
|
|
* titres, le compte des titres, sous des sièges non attribués seulement —,
|
|||
|
|
* remède celui du premier siège listé, retirerTitre ou retirerReservation.
|
|||
|
|
*
|
|||
|
|
* @param {import('../stockage/types.js').Charge} charge
|
|||
|
|
* @param {number} idTable
|
|||
|
|
* @param {number} sieges
|
|||
|
|
* @param {{origine: 'champ'|'poignee'}} options
|
|||
|
|
* @returns {{refus: {code: string, details: Object}|null, avertissements: Array<{code: string, details: Object}>}}
|
|||
|
|
* @throws {TypeError} une table inconnue, des places qui ne sont pas un entier, une origine inconnue
|
|||
|
|
*/
|
|||
|
|
export function controlerCapacite(charge, idTable, sieges, { origine }) {
|
|||
|
|
const { refus, avertissements } = controlerCapacites(charge, new Map([[idTable, sieges]]), { origine });
|
|||
|
|
if (refus.length > 0) return { refus: { code: refus[0].code, details: refus[0].details }, avertissements: [] };
|
|||
|
|
return { refus: null, avertissements: avertissements.map(({ code, details }) => ({ code, details })) };
|
|||
|
|
}
|