gestion_table_tournante_libre/src/interface/plan/modele.js
Mathieu Benoit a2812cddb9 [FIX] plan: named warning, one places state, highlight, table spacing
The unheld-reservation warning names the person and the table number
instead of their identifiers. The places state is computed once per change
and handed to the side panels, not once per panel. A person can be
highlighted from their menu or from the grid, and the view bar names them.
A created table takes a cell a full pitch away from every table, its own
capacity included, so no name list runs over a neighbour's chairs. The
rendering guards fail on an empty walk and also check focus and selection.
Checked: 2692 node, 496 browser, 60 long tests, from the index alone.

--- FR ---

[FIX] plan : avertissement nommé, état des places unique, évidence, pas

L'avertissement de réservation non honorée nomme la personne et le numéro
de la table plutôt que leurs identifiants. L'état des places se calcule une
fois par changement et passe aux panneaux, non une fois par panneau. Une
personne se met en évidence par son menu ou depuis la grille, et la barre
de vue la nomme. Une table créée prend une case à un pas plein de chaque
table, sa capacité comprise : aucune liste ne court sur les chaises
voisines. Les gardes du rendu échouent sur un parcours vide et éprouvent
aussi le focus et la sélection. Vérifié : 2692 node, 496 navigateur, 60.

Assisted-by: Claude Opus 5.5
2026-10-07 15:05:01 -04:00

533 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le modèle de dessin du plan (§ 7.3 à § 7.7), sans DOM : ce que PlanDessin
// peint, tiré de l'état des places d'un tour (EtatPlaces, A5) et de la
// géométrie des tables (P2). Le modèle lit les conflits de l'état des
// places, jamais ne les recalcule (§ 7.5) ; il ne rend que des codes, des
// nombres et des données — noms, libellés de titre —, que les composants
// traduisent. Les mesures de texte sont injectées : getComputedTextLength
// au navigateur, une fonction quelconque sous node.
import { constante } from '../../geometrie/constantes.js';
import { regimeCibles } from '../../geometrie/designation.js';
import { geometrieTable } from '../../geometrie/tables.js';
import { decalagesLignes, tronquer } from '../../geometrie/texte.js';
/** Largeur maximale de la barre de l'échelle graphique, en px d'écran. */
export const LARGEUR_ECHELLE_PX = 120;
/** Rayon du disque de la pastille, en cm de dessin : sous la marque de gel
* d'une table de 2, la plus petite. */
export const RAYON_PASTILLE = 15;
/** Rayon du socle de la pastille, en cm de dessin : un disque du fond du
* plateau, posé sur la trame d'une table signalée, sous la pastille, qui
* l'en sépare (§ 14.5). Plus petit que le plateau le plus petit, de rayon
* PLANCHER_DIAMETRE / 2 : la trame reste peinte autour. */
export const RAYON_SOCLE_PASTILLE = 20;
/** Corps du chiffre de la pastille, en cm de dessin. */
export const TAILLE_TEXTE_PASTILLE_CM = 18;
/** Écart entre le bord d'une chaise et son anneau, ou sa mise en évidence, en cm. */
export const ECART_ANNEAU = 6;
export const ECART_MISE_EN_EVIDENCE = 12;
/** Écart entre le bord d'une chaise, ou d'une emprise, et la marque de
* sélection (anneau en tirets), puis le carré du focus, en cm. */
export const ECART_DESIGNATION = 18;
export const ECART_FOCUS = 26;
// Les cadenas d'une chaise (§ 7.6), en cm, centrés sur l'origine : un corps
// plein et une anse, fermée ou ouverte, remplis d'un seul tenant. Le dessin
// les translate au centre de la chaise. Le centre de la boîte de chacun
// tombe dans son corps.
const CORPS = 'M-8,-6 H8 V9 H-8 Z';
/** Le tracé du cadenas fermé, centré sur l'origine, en cm. */
export const CADENAS_FERME = `${CORPS} M-6,-6 V-10 A6,6 0 0 1 6,-10 V-6 H3.5 V-10 A3.5,3.5 0 0 0 -3.5,-10 V-6 Z`;
/** Le tracé du cadenas ouvert : la branche droite de l'anse ne rejoint pas le corps. */
export const CADENAS_OUVERT = `${CORPS} M-6,-6 V-13 A6,6 0 0 1 6,-13 V-10 H3.5 V-13 A3.5,3.5 0 0 0 -3.5,-13 V-6 Z`;
/** Distance entre le haut du plateau et le centre de la marque de gel, en cm. */
export const RETRAIT_GEL = 13;
/** Le tracé de la marque de gel, un hexagone plein centré sur l'origine, en cm. */
export const TRACE_GEL = 'M0,-8 L7,-4 L7,4 L0,8 L-7,4 L-7,-4 Z';
/**
* Les formes d'une table en attributs SVG, en cm : plateau et emprise —
* disques d'une ronde ({cx, cy, r}), carrés aux côtés parallèles aux axes
* d'une carrée ({x, y, width, height}) —, le centre de la pastille, celui du
* plateau, et celui de la marque de gel, dans le plateau, RETRAIT_GEL sous
* son haut.
*
* @param {import('../../geometrie/tables.js').GeometrieTable} geometrie
*/
export function formesTable({ forme, centre, demiTaille, rayonEmprise }) {
const ronde = forme === 'ronde';
const dessiner = (demi) =>
ronde ? { cx: centre.x, cy: centre.y, r: demi } : { x: centre.x - demi, y: centre.y - demi, width: 2 * demi, height: 2 * demi };
return {
ronde,
plateau: dessiner(demiTaille),
emprise: dessiner(rayonEmprise),
pastille: { x: centre.x, y: centre.y },
gel: { x: centre.x, y: centre.y - demiTaille + RETRAIT_GEL },
};
}
/**
* La ligne d'une chaise (§ 7.6) : son numéro, l'état de la place — réservée
* pour tous les tours, réservée au tour désigné, libre, ou rien pour un
* occupant sans réservation —, le nom de l'occupant, et le titre de la place.
* La réservation dit l'état de la place ; l'occupant, le nom : l'occupant
* d'une place titrée n'en devient pas titulaire (§ 4.1).
*
* Une réservation dont la titulaire n'occupe pas la chaise — une source
* qui assied une autre personne, ou personne, sur la place réservée — ne
* s'attribue pas à l'occupant : l'état devient plan.ligne.reserveeA, de
* mêmes portée et tour, et titulaire porte le nom de la personne réservée,
* que la ligne énonce dans sa propre partie.
*
* @param {Object} chaise EtatChaise
* @param {(id: number) => string} nomDe le nom affiché d'un participant
* @returns {{numero: number, etat: {cle: string, details: Object}|null, titulaire: string|null,
* nom: string|null, titre: {libelle: string, pourvu: boolean}|null}}
*/
export function ligneListe(chaise, nomDe) {
const { siege, occupant, reservation, titre } = chaise;
let etat = null;
let titulaire = null;
if (reservation !== null && reservation.participant !== occupant) {
etat = { cle: 'plan.ligne.reserveeA', details: { portee: reservation.portee, tour: reservation.tour } };
titulaire = nomDe(reservation.participant);
} else if (reservation !== null) {
etat =
reservation.portee === 'tous'
? { cle: 'plan.ligne.reserveeTous', details: {} }
: { cle: 'plan.ligne.reserveeTour', details: { tour: reservation.tour } };
} else if (occupant === null) {
etat = { cle: 'plan.ligne.libre', details: {} };
}
return {
numero: siege,
etat,
titulaire,
nom: occupant === null ? null : nomDe(occupant),
titre: titre === null ? null : { libelle: titre.libelle, pourvu: titre.pourvu },
};
}
/**
* Les textes d'une ligne, partie par partie : tete — numéro et état, suivis
* d'une espace quand un nom suit —, teteSansNom — la même sans l'espace,
* quand le nom tombe —, titulaire et suite, nom, avantTitre, titre — le
* libellé seul —, apresTitre ; null pour une partie absente. Leur
* concaténation, dans cet ordre et sans les formes « SansNom », est la
* ligne entière.
*
* Quand la ligne a une titulaire, l'état s'écrit autour de son nom : tete
* porte le numéro et le début de l'état, titulaire le nom de la personne
* réservée, suite la portée — suivie d'une espace quand un nom suit —, et
* suiteSansNom la même sans l'espace ; tete et teteSansNom sont alors
* égales. Sans titulaire, titulaire, suite et suiteSansNom valent null.
*
* @param {ReturnType<typeof ligneListe>} ligne
* @param {(cle: string, details?: Object) => string} t
*/
export function textesLigne({ numero, etat, titulaire = null, nom, titre }, t) {
const tete = (avecNom) => t('plan.ligne.tete', { numero, etat, avecNom });
const espace = nom === null ? '' : ' ';
const reservee = titulaire === null ? null : t(etat.cle, etat.details);
return {
tete: titulaire === null ? tete(nom !== null) : t('plan.ligne.teteReserveeA', { numero }),
teteSansNom: titulaire === null ? tete(false) : t('plan.ligne.teteReserveeA', { numero }),
titulaire,
suite: reservee === null ? null : `${reservee}${espace}`,
suiteSansNom: reservee,
nom,
avantTitre: titre === null ? null : t(titre.pourvu ? 'plan.ligne.avantTitrePourvu' : 'plan.ligne.avantTitreNonPourvu'),
titre: titre === null ? null : titre.libelle,
apresTitre: titre === null || titre.pourvu ? null : t('plan.ligne.apresTitreNonPourvu'),
};
}
/**
* La ligne entière en un seul texte : les parties de textesLigne bout à
* bout, dans l'ordre de la lecture. C'est le nom accessible d'un siège, que
* le plan annonce quand il devient l'élément actif.
*
* @param {ReturnType<typeof ligneListe>} ligne
* @param {(cle: string, details?: Object) => string} t
* @returns {string}
*/
export function texteLigne(ligne, t) {
const textes = textesLigne(ligne, t);
return ['tete', 'titulaire', 'suite', 'nom', 'avantTitre', 'titre', 'apresTitre']
.map((partie) => textes[partie] ?? '')
.join('');
}
/**
* La ligne qui tient dans largeur (§ 7.4) : le nom s'abrège, suivi de la
* marque, à la largeur que lui laissent les autres parties ; quand la
* marque n'y tient plus, le nom tombe — la tête, ou la suite, perd son
* espace — et le titre s'abrège à son tour ; quand il ne tient plus, il
* tombe avec ce qui l'entoure, puis le nom de la titulaire s'abrège dans sa
* partie ; enfin une tête encore trop large s'abrège seule. Le numéro, en
* tête, part le dernier.
*
* Les parties de même style se mesurent ensemble, comme le moteur de rendu
* les compose : tête, titulaire, suite, nom et avantTitre droits d'un
* tenant, le titre en italique, apresTitre droit ; un changement de style
* coupe la composition.
*
* @param {ReturnType<typeof textesLigne>} textes
* @param {{mesurer: (s: string) => number, mesurerItalique: (s: string) => number,
* largeur: number, marque: string}} mesures
* @returns {{tete: string, titulaire: string|null, suite: string|null, nom: string|null,
* avantTitre: string|null, titre: string|null, apresTitre: string|null}}
*/
export function ajusterLigne(textes, { mesurer, mesurerItalique, largeur, marque }) {
const { tete, teteSansNom, nom, avantTitre, titre, apresTitre } = textes;
const titulaire = textes.titulaire ?? null;
const suite = textes.suite ?? null;
const suiteSansNom = textes.suiteSansNom ?? null;
// Le début droit de la ligne, jusqu'au nom : tête, titulaire et suite.
const debutAvecNom = `${tete}${titulaire ?? ''}${suite ?? ''}`;
const debutSansNom = `${teteSansNom}${titulaire ?? ''}${suiteSansNom ?? ''}`;
// Largeur de ce qui suit le titre, puis du titre en italique.
const apres = apresTitre === null ? 0 : mesurer(apresTitre);
const italique = (texte) => (texte === null ? 0 : mesurerItalique(texte));
const vide = { nom: null, avantTitre: null, titre: null, apresTitre: null };
if (nom !== null) {
const reste = italique(titre) + apres;
const nomAjuste = tronquer(nom, largeur, (candidat) => mesurer(`${debutAvecNom}${candidat}${avantTitre ?? ''}`) + reste, marque);
if (nomAjuste !== '') return { tete, titulaire, suite, nom: nomAjuste, avantTitre, titre, apresTitre };
}
if (titre !== null) {
const debut = mesurer(`${debutSansNom}${avantTitre}`);
const titreAjuste = tronquer(titre, largeur, (candidat) => debut + italique(candidat) + apres, marque);
if (titreAjuste !== '') return { tete: teteSansNom, titulaire, suite: suiteSansNom, ...vide, avantTitre, titre: titreAjuste, apresTitre };
}
if (titulaire !== null) {
const titulaireAjuste = tronquer(titulaire, largeur, (candidat) => mesurer(`${teteSansNom}${candidat}${suiteSansNom}`), marque);
if (titulaireAjuste !== '') return { tete: teteSansNom, titulaire: titulaireAjuste, suite: suiteSansNom, ...vide };
}
return { tete: tronquer(teteSansNom, largeur, mesurer, marque), titulaire: null, suite: null, ...vide };
}
// L'ordre des parties d'une ligne, celui de la lecture.
const PARTIES = ['tete', 'titulaire', 'suite', 'nom', 'avantTitre', 'titre', 'apresTitre'];
/**
* Les parties présentes d'une ligne ajustée, dans l'ordre de la lecture :
* chacune son tspan, nommé par data-partie ; le titre seul en italique.
* @param {ReturnType<typeof ajusterLigne>} ligne
* @returns {Array<{partie: string, texte: string}>}
*/
export function partiesLigne(ligne) {
return PARTIES.filter((partie) => (ligne[partie] ?? null) !== null).map((partie) => ({ partie, texte: ligne[partie] }));
}
/**
* Les lignes de base des n lignes d'un bloc dont le haut est à hautY : la
* première une hauteur de ligne sous le haut, chacune une hauteur sous la
* précédente. hauteur est la hauteur d'encre du gabarit (§ 7.4) : deux
* lignes consécutives ne se chevauchent pas.
* @param {number} hautY @param {number} n @param {number} hauteur
* @returns {number[]}
*/
export function ordonneesLignes(hautY, n, hauteur) {
return decalagesLignes(n, hauteur).map((decalage) => hautY + hauteur + decalage);
}
/**
* La longueur d'une barre d'échelle en mots : sa valeur dans l'unité qui la
* rend lisible — cm sous le mètre, m sous le kilomètre, km au-delà —, le
* nombre de décimales qu'elle demande, et la clé de l'unité.
* @param {number} longueurCm 1, 2 ou 5 × 10ⁿ
* @returns {{valeur: number, decimales: number, cle: string}}
*/
export function libelleLongueur(longueurCm) {
let valeur = longueurCm;
let cle = 'plan.echelle.centimetres';
if (longueurCm >= 100000) {
valeur = longueurCm / 100000;
cle = 'plan.echelle.kilometres';
} else if (longueurCm >= 100) {
valeur = longueurCm / 100;
cle = 'plan.echelle.metres';
}
// Sous l'unité, une mantisse 1, 2 ou 5 × 10⁻ⁿ demande n décimales ; la
// marge absorbe l'arrondi du logarithme d'une puissance de dix.
const decimales = valeur >= 1 ? 0 : Math.ceil(-Math.log10(valeur) - 1e-9);
return { valeur, decimales, cle };
}
// Le bandeau du plan (§ 7.5) : vide tant qu'une table au moins est saine ;
// sinon la phrase, puis collisionPartout quand le diagnostic le prouve, puis
// les planchers — une seule appartenance, ou chaque groupe de collisions,
// ou l'absence de plancher prouvé. Les chiffres sont ceux du moteur, tels
// quels, sans somme ; sans diagnostic, la phrase seule.
function bandeauDe(toutesEnConflit, diagnostic) {
if (!toutesEnConflit) return [];
const entrees = [{ cle: 'plan.bandeau.toutesEnConflit', details: {} }];
if (diagnostic === null) return entrees;
if (diagnostic.collisionPartout) entrees.push({ cle: 'plan.bandeau.collisionPartout', details: {} });
if (diagnostic.uneSeuleAppartenance) {
entrees.push({ cle: 'plan.bandeau.uneSeuleAppartenance', details: {} });
} else if (diagnostic.collisions.length > 0) {
for (const { groupe, effectif, plancherCumulees } of diagnostic.collisions) {
entrees.push({ cle: 'plan.bandeau.plancherGroupe', details: { groupe, effectif, plancherCumulees } });
}
} else {
entrees.push({ cle: 'plan.bandeau.sansPlancher', details: {} });
}
return entrees;
}
// Une chaise libre au siège s : celle qu'ajoute la poignée au-delà de la
// capacité, que rien ne tient ni n'occupe.
const chaiseLibre = (siege) => ({ siege, occupant: null, reservation: null, titre: null, remplissage: 'normal', marques: [] });
// Les chaises d'une table de l'état des places à places sièges : les siennes
// jusqu'à sa capacité, des chaises libres au-delà.
function chaisesA(etatTable, places) {
return Array.from({ length: places }, (_, i) => etatTable.chaises[i] ?? chaiseLibre(i + 1));
}
// Le porteur d'une place {table, siege|null} dans une table dessinée : le
// siège quand la place en nomme un que le régime sièges dessine, 'table'
// quand elle n'en nomme pas ou que le régime table n'en dessine aucun, null
// pour une place d'une autre table.
function porteurDe(place, id, regime) {
if (place === null || place === undefined || place.table !== id) return null;
const siege = place.siege ?? null;
return siege === null || regime === 'table' ? 'table' : siege;
}
// Le dessin d'une table de l'état des places au zoom k ; places, le nombre
// de sièges que la poignée montre, sinon sa capacité.
function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, actif = null }, places = etatTable.capacite) {
const { id, numero, gelee, surchargee, conflits } = etatTable;
const geometrie = geometrieTable(etatTable, places);
const regime = regimeCibles(k, geometrie);
const pastille = conflits === null ? 0 : conflits.nombre;
const etatChaises = chaisesA(etatTable, places);
const selectionnee = porteurDe(selection, id, regime);
const active = porteurDe(actif, id, regime);
const chaises =
regime === 'table'
? []
: etatChaises.map((chaise, i) => {
const { x, y, rayon } = geometrie.chaises[i];
return {
siege: chaise.siege,
x,
y,
rayon,
remplissage: chaise.remplissage,
marques: [...chaise.marques],
miseEnEvidence: miseEnEvidence !== null && chaise.occupant === miseEnEvidence,
selection: selectionnee === chaise.siege,
focus: active === chaise.siege,
};
});
return {
id,
geometrie,
regime,
conflit: pastille > 0,
pastille,
selection: selectionnee === 'table',
focus: active === 'table',
gelee,
surchargee,
nomAccessible: {
cle: 'plan.table.nom',
details: { numero, places, conflits: conflits === null ? null : conflits.nombre, gelee, surchargee },
},
chaises,
enTete: gelee ? { cle: 'plan.liste.gelee', details: {} } : null,
lignes: etatChaises.map((chaise) => ligneListe(chaise, nomDe)),
};
}
/**
* Le modèle de dessin d'un tour (§ 7.3 à § 7.7).
*
* Chaque table, dans l'ordre de l'état des places, porte sa géométrie
* (geometrieTable, P2), son régime des cibles au zoom k (regimeCibles) —
* aucune chaise dessinée en régime table —, son conflit et sa pastille, lus
* dans conflits — null : ni conflit, ni pastille —, son nom accessible, et
* une ligne par siège, de 1 à la capacité, précédée de l'en-tête d'une
* table gelée. Quand toutes les tables sont en conflit, aucune n'est
* signalée — signalerConflits faux — et le bandeau énonce le diagnostic ;
* les pastilles gardent leur nombre. Les listes se rendent quand elles
* sont demandées et que k atteint K_LISTES (§ 17, point 6). La largeur du
* bloc de noms n'est pas dans le modèle : c'est LARGEUR_BLOC_NOMS_CM du
* registre, que le dessin lit.
*
* La place sélectionnée (option selection) et l'élément actif du clavier
* (option actif), {table, siege|null} ou null, marquent leur chaise —
* drapeaux selection et focus d'une chaise — quand la place nomme un siège
* que le régime sièges dessine, leur table sinon — drapeaux selection et
* focus d'une table — ; tout autre porteur garde ses drapeaux faux.
*
* Pendant le tirage d'une poignée (option poignee), la table qu'elle désigne
* se dessine à places sièges — géométrie, régime, chaises, lignes et nom
* accessible —, ses chaises 1 à places, celles au-delà de sa capacité
* libres ; l'état des places n'est pas touché, et rien ne s'écrit (§ 7.1).
*
* @param {Object} etatPlaces EtatPlaces (A5)
* @param {{k: number, listes: boolean, miseEnEvidence: number|null,
* nomDe: (id: number) => string, diagnostic?: Object|null,
* poignee?: {table: number, places: number}|null,
* selection?: {table: number, siege?: number|null}|null,
* actif?: {table: number, siege: number|null}|null}} options
* diagnostic : celui que rend diagnostiquer, compléments de M1 compris
* @returns {Object} ModeleDessin
*/
export function modeleDessin(
etatPlaces,
{ k, listes, miseEnEvidence, nomDe, diagnostic = null, poignee = null, selection = null, actif = null },
) {
const tables = etatPlaces.tables.map((etatTable) =>
tableDessin(
etatTable,
{ k, miseEnEvidence, nomDe, selection, actif },
poignee !== null && poignee.table === etatTable.id ? poignee.places : etatTable.capacite,
),
);
const toutesEnConflit = tables.length > 0 && tables.every(({ conflit }) => conflit);
return {
tables,
signalerConflits: !toutesEnConflit,
bandeau: bandeauDe(toutesEnConflit, diagnostic),
listesRendues: listes && k >= constante('K_LISTES'),
};
}
/**
* Le détail des conflits d'une table au tour montré (§ 7.1, § 7.3), que
* l'infobulle énonce : son numéro, le nombre de violations, et chaque règle
* violée dans l'ordre du moteur (conflitsParTable, M1), sa clé de libellé
* plan.infobulle.<règle> et ses groupes nommés — une paire de noms pour
* séparer les appartenances et nouveaux voisins, un nom seul pour nouvelle
* table. Les noms sont des données : l'infobulle pose chacun dans son nœud.
* null quand les conflits ne s'évaluent pas ou qu'il n'y en a aucun.
*
* @param {Object} etatTable EtatTable (A5)
* @param {(id: number) => string} nomDe
* @returns {null|{numero: number, nombre: number, regles: Array<{cle: string, groupes: string[][]}>}}
*/
export function detailConflits({ numero, conflits }, nomDe) {
if (conflits === null || conflits.nombre === 0) return null;
return {
numero,
nombre: conflits.nombre,
regles: conflits.regles.map((regle) => ({
cle: `plan.infobulle.${regle.regle}`,
groupes: (regle.paires ?? regle.personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe)),
})),
};
}
/**
* Les actions qu'offre le panneau d'une place (§ 7.1, § 2.1 étape 6), dans
* l'ordre de l'affichage : sur une place qu'aucune réservation ne tient,
* assigner, réserver et désigner, qui portent sur la personne choisie ;
* assigner aussi sur une place tenue quand une personne est choisie — le
* dépôt du menu, que la pose refuse sur la place comme un glissement ;
* donner un titre sur une place sans titre ; retirer la réservation et
* retirer le titre sur celles qui en portent ; déplacer la réservation ici
* quand le choix en attente est une réservation.
*
* @param {{reservation: Object|null, titre: Object|null}} chaise EtatChaise
* @param {Object|null} choix EtatUi.choix
* @returns {string[]} parmi assigner, reserver, donnerTitre, designer,
* retirerReservation, retirerTitre, deplacerReservationIci
*/
export function actionsPlace({ reservation, titre }, choix) {
const libre = reservation === null;
const personne = choix !== null && choix.genre === 'personne';
return [
['assigner', libre || personne],
['reserver', libre],
['donnerTitre', titre === null],
['designer', libre],
['retirerReservation', !libre],
['retirerTitre', titre !== null],
['deplacerReservationIci', choix !== null && choix.genre === 'reservation'],
]
.filter(([, offerte]) => offerte)
.map(([action]) => action);
}
/**
* Le menu d'une personne (§ 7.1), dans l'ordre de l'affichage : déplacer —
* le choix en attente —, toujours ; échanger quand elle est assise au tour
* montré ; retirer du tour quand le dépôt sur le sol rend une commande ou un
* refus (retrait non null) ; réserver ici quand elle est assise sur une
* place qu'aucune réservation ne tient ; déplacer sa réservation quand la
* sienne tient sa chaise ; exclure, ou réintégrer une exclue ; la mettre en
* évidence, ou ôter sa mise en évidence quand elle l'est ; ouvrir la fiche.
*
* @param {{exclu: boolean, chaise: Object|null, retrait: Object|null, enEvidence: boolean}} personne
* chaise, l'EtatChaise qu'elle occupe au tour montré, null hors du dessin ;
* enEvidence, vrai quand elle est la personne mise en évidence (ui.miseEnEvidence)
* @returns {string[]} parmi deplacer, echanger, retirerDuTour, reserverIci,
* deplacerReservation, exclure, reintegrer, mettreEnEvidence, oterEvidence, ouvrirFiche
*/
export function actionsPersonne({ exclu, chaise, retrait, enEvidence }) {
const assise = chaise !== null;
const tenue = assise && chaise.reservation !== null && chaise.reservation.participant === chaise.occupant;
return [
['deplacer', true],
['echanger', assise],
['retirerDuTour', retrait !== null],
['reserverIci', assise && !exclu && chaise.reservation === null],
['deplacerReservation', tenue],
['exclure', !exclu],
['reintegrer', exclu],
['mettreEnEvidence', !enEvidence],
['oterEvidence', enEvidence],
['ouvrirFiche', true],
]
.filter(([, offerte]) => offerte)
.map(([action]) => action);
}
/**
* Exécute la commande d'un panneau du plan — panneau d'une place, d'une
* table, menu d'une personne — et pose son refus sur la place, là où le
* geste a lieu (§ 5.9) : la même place qu'un glissement refusé, que
* Plan.svelte montre par ui.pose. Réussie, la commande oublie le choix en
* attente et retire la pose ; elle rend ce que rend la séance. Refusée — une
* erreur qui porte un code —, le refus se pose, et null est rendu. Un code de
* locaux, que l'appelant montre à côté de son champ, une place null et une
* erreur sans code remontent.
*
* @param {{seance: {executer: Function}, ui: {choisir: Function, poserSurPlace: Function}}} dependances
* @param {{table: number, siege?: number|null}|null} place
* @param {string} commande
* @param {Object} args
* @param {{locaux?: string[]}} [options]
* @returns {Promise<Object|null>}
*/
export async function executerSurPlace({ seance, ui }, place, commande, args, { locaux = [] } = {}) {
let rendu;
try {
rendu = await seance.executer(commande, args);
} catch (erreur) {
if (typeof erreur?.code !== 'string' || place === null || locaux.includes(erreur.code)) throw erreur;
ui.poserSurPlace({ place, refus: { code: erreur.code, details: erreur.details ?? {} }, avertissements: [] });
return null;
}
ui.choisir(null);
ui.poserSurPlace(null);
return rendu;
}