268 lines
11 KiB
JavaScript
268 lines
11 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
// Le modèle de la section des tables et des tours (§ 4, § 6, § 2.1 étapes 4
|
||
|
|
// et 5), pur : les composants de tables/ le câblent. Il lit la charge et les
|
||
|
|
// bilans d'A4 — bilanChangementDefaut, bilanTours, bilanSuppressionTable —
|
||
|
|
// et rend des clés de libellé et leurs détails, jamais un texte : la
|
||
|
|
// traduction les compose. Il ne refait aucun contrôle : un refus vient d'un
|
||
|
|
// bilan ou de la commande elle-même, essayée sur la charge sans rien écrire.
|
||
|
|
// Aucune fonction ne modifie ce qu'elle reçoit.
|
||
|
|
import { appliquerCommande } from '../../application/commandes.js';
|
||
|
|
import { nomAffiche } from '../../application/personnes.js';
|
||
|
|
|
||
|
|
const parNumero = (a, b) => a.numero - b.numero || a.id - b.id;
|
||
|
|
|
||
|
|
// Le numéro affiché de la table d'identifiant id parmi tables ; une table
|
||
|
|
// inconnue est une faute de l'appelant.
|
||
|
|
function numeroDe(tables, id) {
|
||
|
|
const table = tables.find((candidate) => candidate.id === id);
|
||
|
|
if (table === undefined) throw new TypeError(`table inconnue : ${JSON.stringify(id)}`);
|
||
|
|
return table.numero;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le nom affiché du participant d'identifiant id ; une personne inconnue
|
||
|
|
// est une faute de l'appelant.
|
||
|
|
function nomDe(charge, id) {
|
||
|
|
const personne = charge.participants.find((candidate) => candidate.id === id);
|
||
|
|
if (personne === undefined) throw new TypeError(`participant inconnu : ${JSON.stringify(id)}`);
|
||
|
|
return nomAffiche(personne);
|
||
|
|
}
|
||
|
|
|
||
|
|
// La clé du nom de chaque forme de table (§ 4).
|
||
|
|
const FORMES = new Map([
|
||
|
|
['ronde', 'tables.forme.ronde'],
|
||
|
|
['carree', 'tables.forme.carree'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** La clé du nom d'une forme de table ; RangeError pour une forme inconnue. */
|
||
|
|
export function cleForme(forme) {
|
||
|
|
if (!FORMES.has(forme)) throw new RangeError(`forme inconnue : ${JSON.stringify(forme)}`);
|
||
|
|
return FORMES.get(forme);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Les tables de la charge par numéro affiché croissant, à identifiant égal du plus petit. */
|
||
|
|
export function tablesParNumero(charge) {
|
||
|
|
return [...charge.tables].sort(parNumero);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* L'état d'une table en texte : « suit le défaut » ou « surchargée », et sa
|
||
|
|
* capacité ; realignable, vrai pour une table surchargée, offre
|
||
|
|
* « Réaligner » (§ 6.1). Une table surchargée à la valeur du défaut a la
|
||
|
|
* même capacité qu'une table qui le suit, et un autre état.
|
||
|
|
*
|
||
|
|
* @param {{sieges: number|null}} table
|
||
|
|
* @param {number} siegesParDefaut
|
||
|
|
* @returns {{cle: string, capacite: number, realignable: boolean}}
|
||
|
|
*/
|
||
|
|
export function etatTable(table, siegesParDefaut) {
|
||
|
|
const surchargee = table.sieges !== null;
|
||
|
|
return {
|
||
|
|
cle: surchargee ? 'tables.etat.surchargee' : 'tables.etat.suitDefaut',
|
||
|
|
capacite: surchargee ? table.sieges : siegesParDefaut,
|
||
|
|
realignable: surchargee,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* L'énoncé du § 6.1, en clés et détails, depuis bilanChangementDefaut ; ses
|
||
|
|
* phrases se lisent séparées d'une espace. Accepté : la phrase
|
||
|
|
* d'application — les tables qui suivent le défaut et ce qu'elles
|
||
|
|
* deviennent, puis les surchargées, qui ne changent pas — et celle du total.
|
||
|
|
* Refusé : une phrase d'en-tête, puis chaque refus de bilan.refus, sa table
|
||
|
|
* nommée par son numéro (numero null pour un refus de l'événement entier),
|
||
|
|
* et aucune forme d'application. Avec realigner, le bilan est celui de
|
||
|
|
* bilanChangementDefaut(…, {realigner: true}) : l'en-tête d'un refus dit
|
||
|
|
* que le réalignement est refusé, le changement seul restant possible.
|
||
|
|
*
|
||
|
|
* @param {ReturnType<import('../../application/bilans.js').bilanChangementDefaut>} bilan
|
||
|
|
* @param {Array<{id: number, numero: number}>} [tables] celles de la charge,
|
||
|
|
* qui donnent son numéro à une table qui refuse
|
||
|
|
* @param {{realigner?: boolean}} [options]
|
||
|
|
* @returns {Array<{cle: string, details: Object}>}
|
||
|
|
* @throws {TypeError} une table de bilan.refus absente de tables
|
||
|
|
*/
|
||
|
|
export function enonceDefaut(bilan, tables = [], { realigner = false } = {}) {
|
||
|
|
const { avant, apres } = bilan;
|
||
|
|
if (bilan.refus.length > 0) {
|
||
|
|
return [
|
||
|
|
{ cle: realigner ? 'tables.defaut.realignementRefuse' : 'tables.defaut.refusee', details: { avant, apres } },
|
||
|
|
...bilan.refus.map(({ table, code, details }) => ({
|
||
|
|
cle: 'tables.defaut.refusTable',
|
||
|
|
details: { numero: table === null ? null : numeroDe(tables, table), refus: { code, details } },
|
||
|
|
})),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return [
|
||
|
|
{ cle: 'tables.defaut.application', details: { suivent: bilan.suivent, avant, apres, surchargees: bilan.surchargees } },
|
||
|
|
{
|
||
|
|
cle: 'tables.defaut.total',
|
||
|
|
details: { totalAvant: bilan.totalAvant, totalApres: bilan.totalApres, participants: bilan.participants },
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Le refus qu'un nouveau défaut reçoit sur son champ : celui du bilan qui
|
||
|
|
* ne nomme aucune table — moins de 2 places refusent pour l'événement
|
||
|
|
* entier —, ou null. Les refus d'une table s'énoncent (enonceDefaut).
|
||
|
|
*
|
||
|
|
* @param {{refus: Array<{table: number|null, code: string, details: Object}>}} bilan
|
||
|
|
* @returns {{code: string, details: Object}|null}
|
||
|
|
*/
|
||
|
|
export function refusDuChampDefaut(bilan) {
|
||
|
|
const refus = bilan.refus.find(({ table }) => table === null);
|
||
|
|
return refus === undefined ? null : { code: refus.code, details: refus.details };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ce qu'un réalignement de toutes les tables efface : la surcharge de
|
||
|
|
* chaque table surchargée, par numéro de table (§ 6.1).
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @returns {Array<{cle: string, details: {numero: number, sieges: number}}>}
|
||
|
|
*/
|
||
|
|
export function surchargesPerdues(charge) {
|
||
|
|
return tablesParNumero(charge)
|
||
|
|
.filter(({ sieges }) => sieges !== null)
|
||
|
|
.map(({ numero, sieges }) => ({ cle: 'tables.defaut.surchargePerdue', details: { numero, sieges } }));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Vrai quand le changement de avant à apres tours détruit de la saisie :
|
||
|
|
* une réservation « tour désigné » au-delà, ou, pour une baisse, les tours
|
||
|
|
* retirés d'un placement engendré (§ 2.1 étape 5). Une hausse ne détruit
|
||
|
|
* rien : les placements qu'elle laisse en dérive restent entiers, et la
|
||
|
|
* séance en avertit (DERIVE_NOUVELLE) sans fenêtre (§ 5.9).
|
||
|
|
*
|
||
|
|
* @param {ReturnType<import('../../application/bilans.js').bilanTours>} bilan
|
||
|
|
* @param {number} avant le nombre de tours enregistré
|
||
|
|
* @param {number} apres le nombre de tours demandé
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
export const annonceTours = (bilan, avant, apres) =>
|
||
|
|
bilan.reservationsDetruites.length > 0 ||
|
||
|
|
(apres < avant && (bilan.propositionsTouchees.length > 0 || bilan.retenuTouche));
|
||
|
|
|
||
|
|
// Les placements qu'un changement laisse en dérive : chaque proposition,
|
||
|
|
// puis le retenu.
|
||
|
|
const touches = (bilan) => [
|
||
|
|
...bilan.propositionsTouchees.map((id) => ({ cle: 'tables.touchee.proposition', details: { id } })),
|
||
|
|
...(bilan.retenuTouche ? [{ cle: 'tables.touchee.retenu', details: {} }] : []),
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ce que nomme la fenêtre d'un changement de tours (§ 2.1 étape 5) : chaque
|
||
|
|
* réservation détruite, sa personne et le numéro de sa table, dans l'ordre
|
||
|
|
* du bilan, puis les placements touchés.
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @param {ReturnType<import('../../application/bilans.js').bilanTours>} bilan
|
||
|
|
* @returns {Array<{cle: string, details: Object}>}
|
||
|
|
*/
|
||
|
|
export function elementsTours(charge, bilan) {
|
||
|
|
return [
|
||
|
|
...bilan.reservationsDetruites.map(({ participant, table, tour }) => ({
|
||
|
|
cle: 'tables.tours.reservation',
|
||
|
|
details: { nom: nomDe(charge, participant), numero: numeroDe(charge.tables, table), tour },
|
||
|
|
})),
|
||
|
|
...touches(bilan),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Vrai quand supprimer la table détruit une réservation ou un titre. */
|
||
|
|
export const annonceSuppression = (bilan) => bilan.reservations.length > 0 || bilan.titres.length > 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Ce que nomme la fenêtre de la suppression d'une table (§ 6.2) : ses
|
||
|
|
* réservations, leur personne, leur portée et leur siège, puis ses titres,
|
||
|
|
* dans l'ordre du bilan ; puis les placements touchés. Rien pour une table
|
||
|
|
* nue, que la suppression ne demande pas de confirmer.
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @param {ReturnType<import('../../application/bilans.js').bilanSuppressionTable>} bilan
|
||
|
|
* @returns {Array<{cle: string, details: Object}>}
|
||
|
|
*/
|
||
|
|
export function elementsSuppression(charge, bilan) {
|
||
|
|
if (!annonceSuppression(bilan)) return [];
|
||
|
|
return [
|
||
|
|
...bilan.reservations.map(({ participant, portee, tour, siege }) => ({
|
||
|
|
cle: 'tables.suppression.reservation',
|
||
|
|
details: { nom: nomDe(charge, participant), portee, tour, siege },
|
||
|
|
})),
|
||
|
|
...bilan.titres.map(({ siege, libelle }) => ({ cle: 'tables.suppression.titre', details: { siege, libelle } })),
|
||
|
|
...touches(bilan),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Les sièges qu'un refus SIEGES_RETIRES retire, nommés : chaque siège dans
|
||
|
|
* l'ordre du refus, son titre, ou le nom de la personne qui le réserve —
|
||
|
|
* le texte du refus désigne les personnes par identifiant. Sous des sièges
|
||
|
|
* non attribués, le refus compte ses titres sans les lister : ils ne le
|
||
|
|
* sont pas ici non plus. Rien pour un autre refus.
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @param {{code: string, details: Object}} refus
|
||
|
|
* @returns {Array<{cle: string, details: Object, nom: string|null}>}
|
||
|
|
*/
|
||
|
|
export function siegesRetiresNommes(charge, refus) {
|
||
|
|
if (refus.code !== 'SIEGES_RETIRES') return [];
|
||
|
|
const comptes = refus.details.titres !== undefined;
|
||
|
|
return refus.details.sieges
|
||
|
|
.filter(({ cause }) => !(comptes && cause === 'SIEGE_TITRE'))
|
||
|
|
.map((retire) =>
|
||
|
|
retire.cause === 'SIEGE_TITRE'
|
||
|
|
? { cle: 'tables.siegeRetire.titre', details: { siege: retire.siege, libelle: retire.libelle }, nom: null }
|
||
|
|
: {
|
||
|
|
cle: 'tables.siegeRetire.reserve',
|
||
|
|
details: { siege: retire.siege, suspendue: retire.suspendue },
|
||
|
|
nom: nomDe(charge, retire.participant),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Les avertissements d'un geste, nommés : numero, celui de la table qu'ils
|
||
|
|
* désignent — details.table, ou table pour un avertissement d'un bilan —,
|
||
|
|
* null sinon ; noms, les personnes de details.personnes dans leur ordre.
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @param {Array<{code: string, details: Object, table?: number}>} avertissements
|
||
|
|
* @returns {Array<{code: string, details: Object, numero: number|null, noms: string[]}>}
|
||
|
|
*/
|
||
|
|
export function avertissementsNommes(charge, avertissements) {
|
||
|
|
return avertissements.map(({ code, details, table }) => {
|
||
|
|
const id = details.table ?? table ?? null;
|
||
|
|
return {
|
||
|
|
code,
|
||
|
|
details,
|
||
|
|
numero: id === null ? null : numeroDe(charge.tables, id),
|
||
|
|
noms: (details.personnes ?? []).map((personne) => nomDe(charge, personne)),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Le refus que la commande nom lèverait sur la charge, essayée sans rien
|
||
|
|
* écrire, ou null quand elle passe. Les fenêtres l'appellent avant de
|
||
|
|
* s'ouvrir : une saisie que la commande refuse reçoit son refus sur le
|
||
|
|
* champ, et aucune fenêtre n'annonce ce qui ne s'appliquera pas.
|
||
|
|
*
|
||
|
|
* @param {Object} charge
|
||
|
|
* @param {string} nom
|
||
|
|
* @param {Object} args
|
||
|
|
* @returns {{code: string, details: Object}|null}
|
||
|
|
* @throws {TypeError} une commande inconnue, un argument de forme fausse
|
||
|
|
*/
|
||
|
|
export function refusDeCommande(charge, nom, args) {
|
||
|
|
try {
|
||
|
|
appliquerCommande(nom, charge, args);
|
||
|
|
return null;
|
||
|
|
} catch (erreur) {
|
||
|
|
if (typeof erreur?.code !== 'string') throw erreur;
|
||
|
|
return { code: erreur.code, details: erreur.details };
|
||
|
|
}
|
||
|
|
}
|