gestion_table_tournante_libre/src/application/bilans.js
Mathieu Benoit 964532fa19 [ADD] application: furniture — tables, default, tours, capacity checks
Changing a table, the default seat count or the number of tours reshapes
a plan the operator may already have retained. Eight commands create,
remove and resize tables and set the default and the tours; every change
of capacity first passes controlerCapacite, which refuses to cut a reserved
or titled seat, states who goes to the reserve, and only brings back a
reserved person the display showed seated (§ 6.1, § 6.2). bilans.js states
the figures of a change before it applies.
Checked: 2033 node and 127 browser tests.

--- FR ---

[ADD] application : mobilier — tables, défaut, tours, contrôles

Changer une table, le défaut de sièges ou le nombre de tours remodèle un
plan que l'opérateur a peut-être retenu. Huit commandes créent, retirent et
redimensionnent les tables, et règlent le défaut et les tours ; tout
changement de capacité passe d'abord par controlerCapacite, qui refuse de
couper un siège réservé ou titré, dit qui part en réserve, et ne ramène une
personne réservée que si l'écran la montrait assise (§ 6.1, § 6.2).
bilans.js énonce les chiffres d'un changement avant qu'il s'applique.
Vérifié : 2033 node et 127 navigateur.

Assisted-by: Claude Opus 5.5
2026-10-07 05:12:43 -04:00

144 lines
7.1 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les bilans du mobilier (§ 6.1, § 6.2, § 2.1 étape 5) : ce qu'un changement
// de défaut, de tours ou la suppression d'une table emporterait, compté sur
// la charge courante sans rien écrire, pour l'énoncer avant d'appliquer. La
// commande qui applique lit le même bilan : changerDefaut lève le premier de
// ses refus. Les refus et avertissements d'une capacité sont ceux de
// controlerCapacites (capacite.js), le seul contrôle. Aucune fonction ne
// modifie ce qu'elle reçoit.
import { ordreDesReservations } from '../csv/import.js';
import { capacite } from '../stockage/document.js';
import { controlerCapacites } from './capacite.js';
import { presents } from './places.js';
const croissant = (a, b) => a - b;
const comparerTextes = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
const parNumero = (a, b) => a.numero - b.numero || a.id - b.id;
// Un nombre entier attendu ; sa lecture depuis une saisie appartient à
// lireEntier.
function exigerEntier(valeur, nom) {
if (!Number.isSafeInteger(valeur)) throw new TypeError(`${nom} : entier attendu, reçu ${JSON.stringify(valeur)}`);
}
// L'enregistrement de la table d'identifiant id, ou TypeError.
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 identifiants croissants des placements de la liste qui satisfont
// touche ; un placement fautif, de forme quelconque, se lit sans lever.
const identifiants = (placements, touche) =>
placements.filter((placement) => touche(placement)).map(({ id }) => id).sort(croissant);
/**
* Le changement du défaut à sieges places (§ 6.1), énoncé avant d'être
* appliqué : suivent, le nombre de tables qui suivent le défaut ; avant et
* apres, le défaut d'aujourd'hui et sieges ; surchargees, les capacités des
* tables surchargées par numéro de table ; totalAvant et totalApres, la
* somme des capacités avant et après ; participants, les personnes non
* exclues. realigner : les surchargées passent aussi à sieges.
*
* Chaque table dont la capacité change se contrôle comme une saisie au champ
* (§ 6.2) : refus, le premier de chaque table qui refuse, {table, code,
* details}, par ordre de charge.tables ; avertissements, de même, ceux des
* autres — SURNUMERAIRES, AU_DELA_LISIBLE. Moins de 2 places refusent pour
* l'événement entier : un seul refus, MOINS_DE_DEUX, table null.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {number} sieges
* @param {{realigner?: boolean}} [options]
* @returns {{suivent: number, avant: number, apres: number, surchargees: number[], totalAvant: number,
* totalApres: number, participants: number, refus: Array<{table: number|null, code: string, details: Object}>,
* avertissements: Array<{table: number, code: string, details: Object}>}}
* @throws {TypeError} sieges qui n'est pas un entier
*/
export function bilanChangementDefaut(charge, sieges, { realigner = false } = {}) {
exigerEntier(sieges, 'sieges');
const apresDe = (table) => (table.sieges === null || realigner ? sieges : table.sieges);
const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0);
const changements = new Map(
charge.tables.filter((table) => apresDe(table) !== capacite(charge, table)).map((table) => [table.id, apresDe(table)]),
);
const controle =
sieges < 2
? { refus: [{ table: null, code: 'MOINS_DE_DEUX', details: { remede: null } }], avertissements: [] }
: controlerCapacites(charge, changements, { origine: 'champ' });
return {
suivent: charge.tables.filter((table) => table.sieges === null).length,
avant: charge.evenement.siegesParDefaut,
apres: sieges,
surchargees: charge.tables
.filter((table) => table.sieges !== null)
.sort(parNumero)
.map((table) => table.sieges),
totalAvant: somme(charge.tables.map((table) => capacite(charge, table))),
totalApres: somme(charge.tables.map(apresDe)),
participants: presents(charge).size,
refus: controle.refus,
avertissements: controle.avertissements,
};
}
/**
* Le changement du nombre de tours à tours (§ 2.1 étape 5, § 4.3) :
* reservationsDetruites, les réservations « tour désigné » au-delà, dans
* l'ordre canonique ; propositionsTouchees, les identifiants croissants des
* propositions qui ne porteront pas tours tours, et retenuTouche de même
* pour le retenu — ils restent, en dérive TOURS_CHANGES (§ 9). Le nombre de
* tours d'aujourd'hui ne touche rien. La portée « tous » ne se détruit
* jamais : elle n'énumère aucun tour.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {number} tours
* @returns {{reservationsDetruites: Array<{participant: number, table: number, tour: number}>,
* propositionsTouchees: number[], retenuTouche: boolean}}
* @throws {TypeError} tours qui n'est pas un entier
*/
export function bilanTours(charge, tours) {
exigerEntier(tours, 'tours');
const change = tours !== charge.evenement.tours;
const touche = (placement) => change && placement?.tours !== tours;
return {
reservationsDetruites: charge.reservations
.filter(({ portee, tour }) => portee === 'tour' && tour > tours)
.sort(ordreDesReservations)
.map(({ participant, table, tour }) => ({ participant, table, tour })),
propositionsTouchees: identifiants(charge.propositions, touche),
retenuTouche: charge.retenu !== null && touche(charge.retenu),
};
}
/**
* La suppression de la table id (§ 6.2, § 4) : ses réservations, suspendues
* comprises, dans l'ordre canonique, et ses titres par siège puis libellé,
* qui partent avec elle ; propositionsTouchees, les identifiants croissants
* des propositions qui la déclarent, et retenuTouche de même — ils restent,
* en dérive TABLE_SUPPRIMEE (§ 9). Une table nue rend deux listes vides.
*
* @param {import('../stockage/types.js').Charge} charge
* @param {number} id
* @returns {{reservations: Array<{participant: number, portee: 'tous'|'tour', tour: number|null, siege: number|null}>,
* titres: Array<{siege: number, libelle: string}>, propositionsTouchees: number[], retenuTouche: boolean}}
* @throws {TypeError} une table inconnue
*/
export function bilanSuppressionTable(charge, id) {
tableDe(charge, id);
const declare = (placement) => Array.isArray(placement?.tables) && placement.tables.includes(id);
return {
reservations: charge.reservations
.filter(({ table }) => table === id)
.sort(ordreDesReservations)
.map(({ participant, portee, tour, siege }) => ({ participant, portee, tour, siege })),
titres: charge.titres
.filter(({ table }) => table === id)
.sort((a, b) => a.siege - b.siege || comparerTextes(a.libelle, b.libelle))
.map(({ siege, libelle }) => ({ siege, libelle })),
propositionsTouchees: identifiants(charge.propositions, declare),
retenuTouche: charge.retenu !== null && declare(charge.retenu),
};
}