gestion_table_tournante_libre/src/interface/qualite/animateurs.js
Mathieu Benoit 01c9296c73 [ADD] quality page views: header, profiles, memberships, pairs, goals
The Qualité tab now draws its five views from the placement's detail.
A numeric header with each figure's percentage and worst case; the
profile as counts with the diversity bound dotted, and as percentages
on a second chart; membership collisions and a table grid behind a
three-position selector, seats by default; repeated pairs with matrix
and a card section; goals per round on servable couples and
facilitators over the evening and per round; the course of the search.
Each chart has its twin table and keyboard access.
Checked: node 5083, navigateur 1102, node-long 128; captures stable.

--- FR ---

[ADD] vues de la qualité : en-tête, profils, appartenances, paires

L'onglet Qualité dessine désormais ses cinq vues à partir du détail du
placement : un en-tête chiffré avec le pourcentage et le pire cas de
chaque chiffre ; le profil en comptes, borne de diversité en pointillé,
et en pourcentages sur un second graphique ; les collisions et la grille
des tables derrière un sélecteur à trois positions, les sièges par
défaut ; les paires répétées, leur matrice et la fiche ; les objectifs
par tour et les animateurs sur la soirée et par tour ; la course. Chaque
graphique a son tableau jumeau et se parcourt au clavier.
Vérifié : node 5083, navigateur 1102, longues 128 ; captures stables.

Assisted-by: Claude Opus 5.5
2026-10-12 11:26:02 -04:00

179 lines
8.4 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les modèles des deux vues des animateurs (§ 12.13, B22, D37, D38), purs :
// VueAnimateurs.svelte et VueDiversite.svelte les rendent. Ils mettent en
// forme les animateurs du détail d'un placement (detaillerQualite), dans
// l'ordre où le détail les porte, sans rien trier ni recompter.
//
// Les animateurs rencontrés : une série, m(p), et la borne des animateurs
// B(p) en repère pointillé, sous ce nom. Ni manque ni pourcentage : la
// borne n'est pas toujours atteignable (§ 4.5), et rien n'en mesure la part.
//
// La diversité par animateur : le graphique dessine la soirée, une ligne
// par animateur, le moins varié en tête, dans l'ordre du détail ; les
// appartenances et les étiquettes en deux colonnes juxtaposées, chacune son
// axe, jamais empilées. Une colonne seule quand le détail n'a que celle-là,
// sous le jeton de sa série ; aucune quand il n'en a ni l'une ni l'autre. Le
// jumeau porte la soirée de chaque animateur, puis une ligne par tour animé,
// dans l'ordre des tours : il détaille au-delà du graphique (§ 12.7).
// Les colonnes de la diversité, dans l'ordre : chacune sous le nom de son
// drapeau dans detail.animateurs.diversite.colonnes, de son compte dans une
// ligne et de son libellé, avec le genre de sa série.
const COLONNES_DIVERSITE = [
['appartenances', 'serie1'],
['etiquettes', 'serie2'],
];
/**
* Le modèle de la vue des animateurs rencontrés, ou ce qui la remplace.
*
* - null sans détail mesuré ;
* - {cas: 'aucun'} sans animateur ou sans personne concernée : la vue cède
* la place à sa phrase ;
* - sinon {cas: 'animateurs', escalier, infobulle, jumeau} : escalier,
* {nom, synthese, series, reperes} pour Escalier, une ligne par rang du
* profil des animateurs ; la synthèse lit le minimum et son effectif à la
* première ligne de la répartition, croissante ; infobulle, {colonnes,
* lignes}, la ligne du jumeau de chaque rang ; jumeau, le profil — nom,
* m(p), B(p), statut —, puis la répartition de m(p).
*
* @param {Object|null} detail le détail du placement (detaillerQualite)
* @param {(cle: string, details?: Object) => string} t
*/
export function modeleAnimateurs(detail, t) {
if (detail === null) return null;
const { animateurs, personnes } = detail;
if (animateurs === null || animateurs.profil === null) return { cas: 'aucun' };
const { profil, repartition } = animateurs;
const nomDe = new Map(personnes.map(({ id, nom }) => [id, nom]));
const colonnes = [
{ cle: 'nom', libelle: t('qualite.animateurs.colonne.nom'), genre: 'texte' },
{ cle: 'rencontres', libelle: t('qualite.animateurs.colonne.rencontres'), genre: 'nombre' },
{ cle: 'borne', libelle: t('qualite.animateurs.colonne.borne'), genre: 'nombre' },
{ cle: 'statut', libelle: t('qualite.animateurs.colonne.statut'), genre: 'texte' },
];
const lignes = profil.map(({ id, rencontres, borne, statut }) => ({
nom: nomDe.get(id),
rencontres,
borne,
statut: t(`participants.statut.${statut}`),
}));
const [minimum] = repartition;
return {
cas: 'animateurs',
escalier: {
nom: t('qualite.animateurs.graphique'),
synthese: t('qualite.animateurs.synthese', { minimum: minimum.valeur, effectif: minimum.effectif }),
series: [{ libelle: t('qualite.animateurs.serie'), valeurs: profil.map(({ rencontres }) => rencontres) }],
reperes: [{ genre: 'borne', libelle: t('qualite.animateurs.repere.borne'), valeurs: profil.map(({ borne }) => borne) }],
},
infobulle: { colonnes, lignes },
jumeau: [
{ nom: t('qualite.animateurs.jumeau.profil'), colonnes, lignes },
{
nom: t('qualite.animateurs.repartition.titre'),
colonnes: [
{ cle: 'valeur', libelle: t('qualite.animateurs.repartition.valeur'), genre: 'nombre' },
{ cle: 'effectif', libelle: t('qualite.animateurs.repartition.effectif'), genre: 'nombre' },
],
lignes: repartition.map(({ valeur, effectif }) => ({ valeur, effectif })),
},
],
};
}
/**
* Le modèle de la vue de la diversité par animateur, ou ce qui la remplace.
*
* - null sans détail mesuré ;
* - {cas: 'aucunAnimateur'} sans animateur : la vue cède la place à sa
* phrase ;
* - sinon {cas: 'diversite', phrase, equite, barres, infobulle, jumeau} :
* phrase, null quand les deux colonnes se dessinent, sinon le cas du
* § 12.6 — 'sansEtiquette', 'sansAppartenance' ou 'rien' ;
* equite, une entrée {genre, titre, moins, plus, auMinimum} par colonne
* dessinée, ses trois nombres ;
* barres, null sans colonne, sinon {nom, synthese, lignes, colonnes,
* segments} pour Barres — deux colonnes juxtaposées, ou un segment seul
* (colonnes null) —, une ligne {id, nom, valeurs, secondaire} par
* animateur, id son identifiant, valeurs la soirée, secondaire ses tours
* animés et ses personnes rencontrées en animant ;
* infobulle, null sans barres, sinon la ligne de la soirée du jumeau de
* chaque animateur ;
* jumeau, un tableau : la soirée de chaque animateur, puis chacun de ses
* tours animés — tours animés, personnes, et les colonnes dessinées.
*
* @param {Object|null} detail le détail du placement (detaillerQualite)
* @param {(cle: string, details?: Object) => string} t
*/
export function modeleDiversite(detail, t) {
if (detail === null) return null;
const { animateurs, personnes, idsTables, numerosTables } = detail;
if (animateurs === null || animateurs.diversite.lignes.length === 0) return { cas: 'aucunAnimateur' };
const { colonnes: drapeaux, lignes, equite } = animateurs.diversite;
const nomDe = new Map(personnes.map(({ id, nom }) => [id, nom]));
const numeroDe = new Map(idsTables.map((id, rang) => [id, numerosTables[rang]]));
const dessinees = COLONNES_DIVERSITE.filter(([cle]) => drapeaux[cle]);
let phrase = null;
if (dessinees.length === 0) phrase = 'rien';
else if (!drapeaux.etiquettes) phrase = 'sansEtiquette';
else if (!drapeaux.appartenances) phrase = 'sansAppartenance';
const parts = dessinees.map(([cle, genre]) => ({ genre, libelle: t(`qualite.diversite.colonne.${cle}`) }));
const colonnes = [
{ cle: 'animateur', libelle: t('qualite.diversite.colonne.animateur'), genre: 'texte' },
{ cle: 'portee', libelle: t('qualite.diversite.colonne.portee'), genre: 'texte' },
{ cle: 'toursAnimes', libelle: t('qualite.diversite.colonne.toursAnimes'), genre: 'nombre' },
{ cle: 'personnes', libelle: t('qualite.diversite.colonne.personnes'), genre: 'nombre' },
...dessinees.map(([cle]) => ({ cle, libelle: t(`qualite.diversite.colonne.${cle}`), genre: 'nombre' })),
];
// Une ligne du jumeau : l'animateur, sa portée, et les comptes des
// colonnes dessinées.
const rangee = (animateur, portee, toursAnimes, comptes) => {
const ligne = { animateur, portee, toursAnimes, personnes: comptes.personnes };
for (const [cle] of dessinees) ligne[cle] = comptes[cle];
return ligne;
};
const soirees = [];
const rangees = [];
for (const ligne of lignes) {
const nom = nomDe.get(ligne.id);
const soiree = rangee(nom, t('qualite.diversite.soiree'), ligne.toursAnimes, ligne);
soirees.push(soiree);
rangees.push(soiree);
for (const tour of ligne.parTour) {
const portee = t('qualite.diversite.tour', { tour: tour.tour + 1, table: numeroDe.get(tour.table) });
rangees.push(rangee(nom, portee, null, tour));
}
}
const barres =
dessinees.length === 0
? null
: {
nom: t('qualite.diversite.graphique'),
synthese: t('qualite.diversite.synthese', equite),
colonnes: dessinees.length === 2 ? parts : null,
segments: dessinees.length === 2 ? null : parts,
lignes: lignes.map((ligne) => ({
id: ligne.id,
nom: nomDe.get(ligne.id),
valeurs: dessinees.map(([cle]) => ligne[cle]),
secondaire: t('qualite.diversite.secondaire', { tours: ligne.toursAnimes, personnes: ligne.personnes }),
})),
};
return {
cas: 'diversite',
phrase,
equite: dessinees.map(([cle]) => ({ genre: cle, titre: t(`qualite.diversite.colonne.${cle}`), ...equite[cle] })),
barres,
infobulle: barres === null ? null : { colonnes, lignes: soirees },
jumeau: [{ nom: t('qualite.diversite.jumeau'), colonnes, lignes: rangees }],
};
}