230 lines
11 KiB
JavaScript
230 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 vue des paires répétées (§ 12.5, B14, D32, D33, D50) et de
|
|||
|
|
// la rubrique « Rencontres » de la fiche, pur : VuePaires.svelte et
|
|||
|
|
// Fiche.svelte le câblent.
|
|||
|
|
//
|
|||
|
|
// Il met en forme ce que le détail du placement porte, dans l'ordre où il
|
|||
|
|
// le porte : les paires choisies puis les paires imposées, chacune triée par
|
|||
|
|
// le moteur (fois décroissant, puis identifiants) ; la matrice dans l'ordre
|
|||
|
|
// du profil (§ 12.3), jamais par l'alphabet. Il ne trie, ne recompte et ne
|
|||
|
|
// mesure rien : un tour du détail, compté à partir de 0, s'écrit à partir
|
|||
|
|
// de 1, une table, nommée par son identifiant, s'écrit par son numéro, et
|
|||
|
|
// une liste du détail se lit par sa longueur et sa tête, sans s'ajouter à
|
|||
|
|
// une autre ni s'y comparer.
|
|||
|
|
import { rencontresDePersonne } from '../../application/evaluation.js';
|
|||
|
|
import { nomAffiche } from '../../application/personnes.js';
|
|||
|
|
import { pagine } from '../propositions/modele.js';
|
|||
|
|
|
|||
|
|
/** Les cas de la vue des paires, dans l'ordre où elle les éprouve :
|
|||
|
|
* uneTable, une seule table ; unTour, un seul tour ; aucune, aucune paire
|
|||
|
|
* ne se revoit ; tableau, le tableau des paires. */
|
|||
|
|
export const CAS_PAIRES = Object.freeze(['uneTable', 'unTour', 'aucune', 'tableau']);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Le cas de la vue des paires pour un détail comparable : une seule table
|
|||
|
|
* l'emporte, puis un seul tour ; sans paire choisie ni imposée, aucune ;
|
|||
|
|
* sinon le tableau.
|
|||
|
|
*
|
|||
|
|
* @param {{T: number, R: number, paires: {choisies: Object[], imposees: Object[]}}} detail
|
|||
|
|
* @returns {string} l'un de CAS_PAIRES
|
|||
|
|
*/
|
|||
|
|
export function casDesPaires({ T, R, paires }) {
|
|||
|
|
if (T === 1) return 'uneTable';
|
|||
|
|
if (R === 1) return 'unTour';
|
|||
|
|
if (paires.choisies.length === 0 && paires.imposees.length === 0) return 'aucune';
|
|||
|
|
return 'tableau';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les personnes données par identifiant, chacune {id, nom, appartenance,
|
|||
|
|
// homonyme} : homonyme vrai quand une autre personne de la liste porte le
|
|||
|
|
// même nom affiché — la vue écrit alors son identifiant à côté du nom.
|
|||
|
|
function personnesParId(personnes) {
|
|||
|
|
const parNom = new Map();
|
|||
|
|
for (const { nom } of personnes) parNom.set(nom, (parNom.get(nom) ?? 0) + 1);
|
|||
|
|
return new Map(
|
|||
|
|
personnes.map(({ id, nom, appartenance }) => [id, { id, nom, appartenance, homonyme: parNom.get(nom) > 1 }]),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Le nom d'une personne là où un seul texte la désigne — une infobulle, un
|
|||
|
|
* intitulé de la matrice, un nom accessible : son nom, suivi de son
|
|||
|
|
* identifiant pour un homonyme, comme une ligne l'écrit en deux morceaux.
|
|||
|
|
*
|
|||
|
|
* @param {{id: number, nom: string, homonyme: boolean}} personne
|
|||
|
|
* @param {(cle: string, details?: Object) => string} t
|
|||
|
|
*/
|
|||
|
|
export const nomDistinct = ({ id, nom, homonyme }, t) => (homonyme ? t('qualite.paires.nomHomonyme', { nom, id }) : nom);
|
|||
|
|
|
|||
|
|
// Le numéro de chaque table du détail, par son identifiant.
|
|||
|
|
const numerosParTable = ({ idsTables, numerosTables }) => new Map(idsTables.map((id, t) => [id, numerosTables[t]]));
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Les lignes du tableau des paires (§ 12.5, D32) : {choisies, imposees},
|
|||
|
|
* chacune dans l'ordre du détail. Une ligne : {cle, genre, a, b, fois,
|
|||
|
|
* tours, repetition} — cle, « a:b » par identifiants ; genre, 'choisie' ou
|
|||
|
|
* 'imposee' ; a et b, {id, nom, appartenance, homonyme} ; tours, chaque
|
|||
|
|
* tour où la paire est assise ensemble, {tour, table}, tour à partir de 1,
|
|||
|
|
* table son numéro ; repetition, le premier tour où la paire se revoit —
|
|||
|
|
* son second tour ensemble —, où mène la première commande de la ligne.
|
|||
|
|
*
|
|||
|
|
* @param {Object} detail un détail comparable (detaillerQualite)
|
|||
|
|
* @returns {{choisies: Object[], imposees: Object[]}}
|
|||
|
|
*/
|
|||
|
|
export function lignesDesPaires(detail) {
|
|||
|
|
const personnes = personnesParId(detail.personnes);
|
|||
|
|
const numeros = numerosParTable(detail);
|
|||
|
|
const ligne = (genre) => ({ a, b, fois, tours }) => {
|
|||
|
|
const ecrits = tours.map(({ tour, table }) => ({ tour: tour + 1, table: numeros.get(table) }));
|
|||
|
|
return { cle: `${a}:${b}`, genre, a: personnes.get(a), b: personnes.get(b), fois, tours: ecrits, repetition: ecrits[1].tour };
|
|||
|
|
};
|
|||
|
|
return { choisies: detail.paires.choisies.map(ligne('choisie')), imposees: detail.paires.imposees.map(ligne('imposee')) };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La page p du tableau des paires (§ 12.5, D50) : le découpage du tableau
|
|||
|
|
* des propositions (pagine, TAILLE_PAGE lignes par page), les paires
|
|||
|
|
* choisies d'abord, puis les imposées, comptées à la suite. Rend
|
|||
|
|
* {choisies, imposees, page, pages, premier, dernier, total, paginee} :
|
|||
|
|
* les lignes de chaque groupe que la page porte, puis la forme de pagine.
|
|||
|
|
*
|
|||
|
|
* @param {{choisies: Object[], imposees: Object[]}} lignes lignesDesPaires
|
|||
|
|
* @param {number} page entière, à partir de 1 ; bornée aux pages existantes
|
|||
|
|
*/
|
|||
|
|
export function pageDesPaires({ choisies, imposees }, page) {
|
|||
|
|
const { lignes, horsClassement, ...reste } = pagine({ lignes: choisies, horsClassement: imposees }, page);
|
|||
|
|
return { choisies: lignes, imposees: horsClassement, ...reste };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La matrice « qui a rencontré qui » (§ 12.5, D33) mise en forme pour la
|
|||
|
|
* grille, son infobulle et son jumeau ; null quand le détail ne la porte
|
|||
|
|
* pas (au-delà du seuil). Les personnes viennent dans l'ordre de la
|
|||
|
|
* matrice, celui du profil, chacune sous son rang à partir de 1 : une ligne
|
|||
|
|
* de la grille s'intitule « rang. nom », une colonne par le rang seul. Le
|
|||
|
|
* nom d'une personne, celui de nomDistinct, s'écrit une fois et sert à
|
|||
|
|
* l'intitulé de sa ligne comme à l'infobulle de chacune de ses cases. Les
|
|||
|
|
* valeurs sont le triangle du détail déplié en n × n, ligne après ligne,
|
|||
|
|
* symétrique, null sur la diagonale, que la grille laisse vide.
|
|||
|
|
*
|
|||
|
|
* Rend {personnes, lignes, colonnes, valeurs, infobulle, jumeau} :
|
|||
|
|
* personnes, [{id, nom, rang}], nom celui de nomDistinct ; lignes et
|
|||
|
|
* colonnes, [{libelle}] ;
|
|||
|
|
* infobulle, {colonnes, lignes}, une ligne par case dans l'ordre des
|
|||
|
|
* valeurs — la paire et son compte, la personne seule sur la diagonale ;
|
|||
|
|
* jumeau, {nom, colonnes, lignes}, une ligne par personne, son intitulé
|
|||
|
|
* puis un compte par colonne, vide sur la diagonale.
|
|||
|
|
*
|
|||
|
|
* @param {Object} detail un détail comparable
|
|||
|
|
* @param {{t: Function, format: {nombre: Function}}} outils
|
|||
|
|
*/
|
|||
|
|
export function matriceDesPaires(detail, { t, format }) {
|
|||
|
|
if (detail.matrice === null) return null;
|
|||
|
|
const { ordre, comptes } = detail.matrice;
|
|||
|
|
const parId = personnesParId(detail.personnes);
|
|||
|
|
const n = ordre.length;
|
|||
|
|
const personnes = ordre.map((id, i) => ({ id, nom: nomDistinct(parId.get(id), t), rang: i + 1 }));
|
|||
|
|
// Le rang d'une paire i < j dans le triangle du détail, ligne après ligne.
|
|||
|
|
const rangPaire = (i, j) => (i * (2 * n - i - 1)) / 2 + (j - i - 1);
|
|||
|
|
const compte = (i, j) => (i === j ? null : comptes[i < j ? rangPaire(i, j) : rangPaire(j, i)]);
|
|||
|
|
|
|||
|
|
const lignes = personnes.map(({ nom, rang }) => ({ libelle: t('qualite.paires.matrice.ligne', { rang: format.nombre(rang), nom }) }));
|
|||
|
|
const colonnes = personnes.map(({ rang }) => ({ libelle: format.nombre(rang) }));
|
|||
|
|
const valeurs = [];
|
|||
|
|
const bulles = [];
|
|||
|
|
for (let i = 0; i < n; i += 1) {
|
|||
|
|
for (let j = 0; j < n; j += 1) {
|
|||
|
|
valeurs.push(compte(i, j));
|
|||
|
|
bulles.push({
|
|||
|
|
paire: i === j ? personnes[i].nom : t('qualite.paires.matrice.paire', { a: personnes[i].nom, b: personnes[j].nom }),
|
|||
|
|
fois: compte(i, j),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const colonnesBulle = [
|
|||
|
|
{ cle: 'paire', libelle: t('qualite.paires.colonne.paire'), genre: 'texte' },
|
|||
|
|
{ cle: 'fois', libelle: t('qualite.paires.colonne.fois'), genre: 'nombre' },
|
|||
|
|
];
|
|||
|
|
const colonnesJumeau = [
|
|||
|
|
{ cle: 'personne', libelle: t('qualite.paires.matrice.personne'), genre: 'texte' },
|
|||
|
|
...personnes.map(({ rang }, j) => ({ cle: `c${j}`, libelle: format.nombre(rang), genre: 'nombre' })),
|
|||
|
|
];
|
|||
|
|
const lignesJumeau = personnes.map((personne, i) => {
|
|||
|
|
const ligne = { personne: lignes[i].libelle };
|
|||
|
|
for (let j = 0; j < n; j += 1) ligne[`c${j}`] = i === j ? '' : compte(i, j);
|
|||
|
|
return ligne;
|
|||
|
|
});
|
|||
|
|
return {
|
|||
|
|
personnes,
|
|||
|
|
lignes,
|
|||
|
|
colonnes,
|
|||
|
|
valeurs,
|
|||
|
|
infobulle: { colonnes: colonnesBulle, lignes: bulles },
|
|||
|
|
jumeau: { nom: t('qualite.paires.matrice.titre'), colonnes: colonnesJumeau, lignes: lignesJumeau },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La phrase de synthèse de la matrice (§ 12.7) : chaque liste du détail qui
|
|||
|
|
* n'est pas vide, les choisies puis les imposées, lue telle que le moteur la
|
|||
|
|
* rend — son effectif et le fois de sa tête, la plus revue de la liste — ;
|
|||
|
|
* sans paire revue, la phrase qui le dit. Rien ne s'y additionne ni ne s'y
|
|||
|
|
* compare : les deux têtes s'écrivent l'une après l'autre. Rend {cle,
|
|||
|
|
* details}, details {choisies, imposees}, chacune {paires, fois}, null pour
|
|||
|
|
* une liste vide.
|
|||
|
|
*
|
|||
|
|
* @param {{paires: {choisies: Object[], imposees: Object[]}}} detail
|
|||
|
|
*/
|
|||
|
|
export function syntheseMatrice({ paires: { choisies, imposees } }) {
|
|||
|
|
if (choisies.length === 0 && imposees.length === 0) return { cle: 'qualite.paires.matrice.syntheseAucune', details: {} };
|
|||
|
|
const liste = (paires) => (paires.length === 0 ? null : { paires: paires.length, fois: paires[0].fois });
|
|||
|
|
return { cle: 'qualite.paires.matrice.synthese', details: { choisies: liste(choisies), imposees: liste(imposees) } };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* La rubrique « Rencontres » de la fiche d'une personne (§ 12.5, B14, D33)
|
|||
|
|
* : la ligne de la personne dans le placement que le plan montre, la
|
|||
|
|
* source de la page de qualité — la proposition demandée au plan quand la
|
|||
|
|
* liste la porte, sinon le retenu, sinon aucun, comme sourceQualite
|
|||
|
|
* (source.js) le choisit, sans en évaluer la table.
|
|||
|
|
*
|
|||
|
|
* Rend {statut, source, lignes} : statut, celui de rencontresDePersonne —
|
|||
|
|
* mesuree, nonMesurable, exclue ou sansPlacement ; source, {cle, details},
|
|||
|
|
* le libellé du placement, null sans placement ; lignes, chaque personne
|
|||
|
|
* rencontrée, {id, nom, appartenance, homonyme, fois, tours: [{tour,
|
|||
|
|
* table}]}, les plus revues d'abord, tour à partir de 1, table son numéro.
|
|||
|
|
* appartenance et homonyme sont ceux du tableau des paires, sur les
|
|||
|
|
* personnes non exclues de la charge, celles que le placement mesure. Ne
|
|||
|
|
* modifie pas la charge.
|
|||
|
|
*
|
|||
|
|
* @param {Object} charge la charge de l'état observable de la séance
|
|||
|
|
* @param {number|null} demandee la proposition demandée au plan (ui.proposition)
|
|||
|
|
* @param {number} personne l'identifiant de la personne de la fiche
|
|||
|
|
*/
|
|||
|
|
export function rubriqueRencontres(charge, demandee, personne) {
|
|||
|
|
let id = null;
|
|||
|
|
if (demandee !== null && charge.propositions.some((proposition) => proposition.id === demandee)) id = demandee;
|
|||
|
|
else if (charge.retenu !== null) id = 'retenu';
|
|||
|
|
const { statut, lignes } = rencontresDePersonne(charge, id, personne);
|
|||
|
|
let source = null;
|
|||
|
|
if (id === 'retenu') source = { cle: 'participants.fiche.rencontres.retenu', details: {} };
|
|||
|
|
else if (id !== null) source = { cle: 'participants.fiche.rencontres.proposition', details: { proposition: id } };
|
|||
|
|
const numeros = new Map(charge.tables.map(({ id: table, numero }) => [table, numero]));
|
|||
|
|
const personnes = personnesParId(
|
|||
|
|
charge.participants
|
|||
|
|
.filter(({ exclu }) => !exclu)
|
|||
|
|
.map((p) => ({ id: p.id, nom: nomAffiche(p), appartenance: (p.appartenance ?? '').trim() === '' ? null : p.appartenance })),
|
|||
|
|
);
|
|||
|
|
return {
|
|||
|
|
statut,
|
|||
|
|
source,
|
|||
|
|
lignes: lignes.map(({ id: autre, fois, tours }) => ({
|
|||
|
|
...personnes.get(autre),
|
|||
|
|
fois,
|
|||
|
|
tours: tours.map(({ tour, table }) => ({ tour: tour + 1, table: numeros.get(table) })),
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
}
|