gestion_table_tournante_libre/src/interface/qualite/entete.js

190 lines
9.1 KiB
JavaScript
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le modèle de l'en-tête chiffré de la page de qualité (§ 12.2, B11, D29,
// D34, D48, D52), pur : VueEntete.svelte le câble.
//
// Une tuile par colonne de mesure du tableau des propositions, dans l'ordre
// de COLONNES et sous ses groupes (GROUPES_COLONNES) : la tuile lit sa
// mesure dans la ligne d'évaluation de la source par lireColonne, la
// cellule même que le tableau écrit, mise en forme par texteCellule. Une
// colonne que le tableau porte donne à sa tuile son libellé et son
// explication ; une colonne qu'il ne porte pas — critère décoché, valeur
// que le mode réglé ne compare pas — donne la sienne sous le libellé
// qualite.tuile.<nom>, qui dit qu'elle ne classe pas. Un critère sans objet
// n'a pas de tuile : sa mesure est absente de la ligne (null), seul cas où
// une colonne conditionnelle n'a pas de valeur.
//
// Les bornes des tuiles viennent du détail du placement (detail.bornes),
// chacune celle d'une personne nommée, que la tuile nomme : rien ne s'y
// recompte, rien ne s'y moyenne. Le plancher, lui, ne nomme personne : il
// borne la mesure de la tuile. Chaque tuile dont le détail porte le plancher
// (detail.bornes.planchers, là où le diagnostic du § 5.6 l'écrit) l'écrit
// en dernière ligne, sous son chiffre, et dit « atteint : impossible de
// faire moins » quand le détail le dit atteint ; sans plancher, aucune
// ligne, jamais un 0. Les rencontres répétées
// et les retours choisis énoncent à part les imposés, lus dans la ligne
// d'évaluation et dans la table. Trois tuiles portent un pourcentage, un
// entier que le détail porte déjà, suivi du compte du pire : le minimum
// brut (la personne au rang 1 du profil), les tours hors objectif (la
// soirée), le minimum de s(p) (la personne à ce minimum). Aucune autre.
import { COLONNES, GROUPES_COLONNES, explicationDeColonne, lireColonne, tableau, texteCellule } from '../propositions/modele.js';
/** Les tuiles qui portent un pourcentage (B11, D34), dans l'ordre de
* COLONNES : aucune autre n'en porte. */
export const TUILES_A_POURCENTAGE = Object.freeze(['rencontresTous', 'toursHorsObjectif', 'toursSelonObjectif']);
// La clé de l'en-tête de chaque colonne, celle que le tableau écrit.
const EN_TETES = new Map(COLONNES.map(({ nom, cle }) => [nom, cle]));
/**
* @typedef {Object} Tuile
* @property {string} nom le nom de sa colonne
* @property {string} libelle traduit : celui de la colonne, ou qualite.tuile.<nom>
* @property {string} explication la clé de l'explication de la colonne
* @property {string} texte la valeur, écrite comme au tableau
* @property {boolean} classe vrai quand le tableau porte la colonne
* @property {string|null} neClassePas la mention, null quand elle classe
* @property {string[]} bornes les lignes de la borne, traduites, la
* ligne du plancher en dernier quand il est prouvé
* @property {{texte: string|null, compte: string}|null} pourcentage
* le pourcentage écrit, null quand son dénominateur est nul, et
* le compte du pire ; null hors des trois tuiles
*/
// Les lignes de borne et le pourcentage d'une tuile, lus dans le détail.
// nommer(id) rend la ligne qui nomme la personne id ; lire(nom) la valeur
// d'une colonne de la ligne d'évaluation ; ligne et table, la ligne
// d'évaluation et la table, qui portent les imposés.
function bornesDe(nom, { detail, ligne, table, t, nommer, lire }) {
const { bornes } = detail;
const pourcent = (valeur) => (valeur === null ? null : t('qualite.pourcentage.valeur', { pourcent: valeur }));
const rendu = (lignes, pourcentage = null) => ({ bornes: lignes, pourcentage });
switch (nom) {
case 'rencontresTous': {
const { rang1 } = bornes;
if (rang1 === null) return rendu([]);
const lignes = [nommer(rang1.id), t('qualite.tuile.plafondRealise', { plafond: rang1.plafondRealise })];
if (rang1.plafondAPriori !== null) lignes.push(t('qualite.tuile.plafondAPriori', { plafond: rang1.plafondAPriori }));
return rendu(lignes, {
texte: pourcent(rang1.pourcent),
compte: t('qualite.tuile.pireRencontres', { rencontres: rang1.rencontres, plafond: rang1.plafondRealise }),
});
}
case 'manqueMax': {
const { manque } = bornes;
if (manque === null) return rendu([]);
const effectif = lire('effectifManque');
return rendu([
nommer(manque.id),
...(effectif === null ? [] : [t('qualite.tuile.effectifManque', { effectif })]),
t('qualite.tuile.plafondRealise', { plafond: manque.plafondRealise }),
]);
}
case 'appartenancesVues': {
const { diversite } = bornes;
if (diversite === null) return rendu([t('qualite.tuile.sansAppartenance')]);
return rendu([nommer(diversite.id), t('qualite.tuile.borneDiversite', { borne: diversite.aMax })]);
}
case 'toursHorsObjectif': {
const { soiree } = bornes;
if (soiree === null) return rendu([]);
return rendu([], {
texte: soiree.pourcent === null ? null : t('qualite.pourcentage.servis', { pourcent: soiree.pourcent }),
compte: t('qualite.tuile.pireJamaisServies', { personnes: soiree.jamaisServies }),
});
}
case 'toursSelonObjectif': {
const { objectifs } = bornes;
if (objectifs === null) return rendu([]);
return rendu([nommer(objectifs.id), t('qualite.tuile.toursPossibles', { possibles: objectifs.possibles })], {
texte: pourcent(objectifs.pourcent),
compte: t('qualite.tuile.pireServis', { servis: objectifs.servis, possibles: objectifs.possibles }),
});
}
case 'rencontresRepetees':
return rendu([t('qualite.tuile.imposees', { imposees: ligne.mesures.rencontresRepetees.imposees })]);
case 'retoursChoisis':
return rendu([t('qualite.tuile.imposes', { imposes: table.imposes.retours })]);
case 'animateursRencontres': {
const { animateurs } = bornes;
if (animateurs === null) return rendu([]);
return rendu([nommer(animateurs.id), t('qualite.tuile.borneAnimateurs', { borne: animateurs.borne })]);
}
default:
return rendu([]);
}
}
// La ligne du plancher de la tuile nom, lue dans le détail : {plancher,
// atteint} ou null, que le moteur ne rend que prouvé et non nul. Une liste
// d'une ligne, ou vide.
function plancherDe(nom, { detail, t }) {
const { planchers } = detail.bornes;
const lu = Object.hasOwn(planchers, nom) ? planchers[nom] : null;
if (lu === null) return [];
const { plancher, atteint } = lu;
return [t(atteint ? 'qualite.tuile.plancherAtteint' : 'qualite.tuile.plancher', { plancher })];
}
// Les lignes de la borne d'une tuile, son plancher en dernier, et son
// pourcentage.
function bornesEtPlancher(nom, contexte) {
const { bornes, pourcentage } = bornesDe(nom, contexte);
return { bornes: [...bornes, ...plancherDe(nom, contexte)], pourcentage };
}
/**
* L'en-tête chiffré d'une source mesurée, sous le contrat de propriétés des
* vues : null sans détail comparable, sans ligne ou sans table.
*
* @param {{detail: Object|null, ligne: Object|null, table: Object|null}} vue
* le détail du placement (detaillerQualite), sa ligne d'évaluation
* et la table d'évaluation de la charge
* @param {{t: Function, format: {nombre: Function}}} outils
* @returns {null|{population: string, groupes: Array<{cle: string,
* libelle: string, tuiles: Tuile[]}>, certificat: string}}
* population : la mention de la population mesurée ; groupes : ceux de
* GROUPES_COLONNES qui portent une tuile au moins, dans leur ordre ;
* certificat : « minimum atteint » ou « atteignabilité inconnue », qui
* suit les tuiles
*/
export function enteteDe({ detail, ligne, table }, { t, format }) {
if (detail === null || detail.statut !== 'comparable' || ligne === null || table === null) return null;
const portees = new Set(tableau(table).colonnes.map(({ nom }) => nom));
const noms = new Map(detail.personnes.map(({ id, nom }) => [id, nom]));
const contexte = {
detail,
ligne,
table,
t,
nommer: (id) => t('qualite.tuile.personne', { nom: noms.get(id) }),
lire: (nom) => lireColonne(ligne, nom).valeur,
};
const groupes = [];
for (const { cle, colonnes } of GROUPES_COLONNES) {
if (cle === null) continue;
const deGroupe = [];
for (const nom of colonnes) {
const cellule = lireColonne(ligne, nom);
const classe = portees.has(nom);
if (!classe && cellule.valeur === null) continue;
deGroupe.push({
nom,
libelle: t(classe ? EN_TETES.get(nom) : `qualite.tuile.${nom}`),
explication: explicationDeColonne(nom),
texte: texteCellule(cellule, { t, format }),
classe,
neClassePas: classe ? null : t('qualite.tuile.neClassePas'),
...bornesEtPlancher(nom, contexte),
});
}
if (deGroupe.length > 0) groupes.push({ cle, libelle: t(cle), tuiles: deGroupe });
}
return {
population: t('qualite.tuile.population', detail.population),
groupes,
certificat: texteCellule(lireColonne(ligne, 'certificat'), { t, format }),
};
}