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
179 lines
8.4 KiB
JavaScript
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 }],
|
|
};
|
|
}
|