Porter le cycle de vie et l'horizon dans le modèle, en version 5
L'état n'est pas un champ : il se lit dans horizon, completedDate, discardedDate et la présence de phases. Les phases commandent, sauf quand on a prononcé quelque chose. L'emprise d'un projet retombe sur son horizon faute de phases, et une saisie n'est jamais réécrite par un calcul : un projet qui perd sa dernière phase retrouve l'horizon qu'il avait déclaré. Clore un projet termine aussi toutes ses phases, seule opération du cycle qui écrase des données. phasesParMois devient entreesParMois : elle émet aussi les projets sans phase, à leur horizon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
121
js/mois.js
121
js/mois.js
@@ -19,15 +19,19 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
LIBELLES_ETAT,
|
||||
LIBELLES_STATUT,
|
||||
aujourdhui,
|
||||
duree,
|
||||
entreesParMois,
|
||||
etatProjet,
|
||||
formaterDateLongue,
|
||||
formaterHorizon,
|
||||
formaterMois,
|
||||
formaterPeriode,
|
||||
moisDe,
|
||||
phasesParMois,
|
||||
projetFiltre,
|
||||
projetFiltreEtat,
|
||||
} from './model.js';
|
||||
import { aplatirMarkdown } from './markdown.js';
|
||||
import { rendreMarkdown } from './notes.js';
|
||||
@@ -38,13 +42,13 @@ import { pastillesTags } from './tags.js';
|
||||
*
|
||||
* @param {HTMLElement} conteneur
|
||||
* @param {object} planning
|
||||
* @param {object} options { tagsActifs }
|
||||
* @param {object} options { tagsActifs, etatsActifs }
|
||||
*/
|
||||
export function rendreMois(conteneur, planning, { tagsActifs = [] } = {}) {
|
||||
const groupes = phasesParMois(planning, tagsActifs);
|
||||
export function rendreMois(conteneur, planning, { tagsActifs = [], etatsActifs = [] } = {}) {
|
||||
const groupes = entreesParMois(planning, tagsActifs, etatsActifs);
|
||||
|
||||
if (!groupes.length) {
|
||||
conteneur.replaceChildren(messageVide(planning, tagsActifs));
|
||||
conteneur.replaceChildren(messageVide(planning, tagsActifs, etatsActifs));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,8 +69,10 @@ export function rendreMois(conteneur, planning, { tagsActifs = [] } = {}) {
|
||||
* Le cas des projets masqués mérite son message : rien à l'écran ne rappelle
|
||||
* l'œil ici, puisque la liste n'a pas de colonne de libellés où le rouvrir.
|
||||
*/
|
||||
function messageVide(planning, tagsActifs) {
|
||||
const retenus = planning.projects.filter((projet) => projetFiltre(projet, tagsActifs));
|
||||
function messageVide(planning, tagsActifs, etatsActifs = []) {
|
||||
const retenus = planning.projects.filter(
|
||||
(projet) => projetFiltre(projet, tagsActifs) && projetFiltreEtat(projet, etatsActifs)
|
||||
);
|
||||
|
||||
const paragraphe = document.createElement('p');
|
||||
paragraphe.className = 'mois__vide';
|
||||
@@ -120,18 +126,89 @@ function construireGroupe({ mois, entrees }, moisCourant, tagsActifs) {
|
||||
return section;
|
||||
}
|
||||
|
||||
/** « 4 tâches · 2 jalons », en taisant celui des deux qui vaut zéro. */
|
||||
/** « 4 tâches · 2 jalons · 1 envisagé », en taisant ce qui vaut zéro. */
|
||||
function resumerCompte(entrees) {
|
||||
const jalons = entrees.filter(({ phase }) => phase.milestone).length;
|
||||
const taches = entrees.length - jalons;
|
||||
const sansPhase = entrees.filter(({ horizon }) => horizon);
|
||||
// Les projets sans phase ne se comptent pas ensemble : « 3 envisagés » et
|
||||
// « 3 écartés » ne disent pas du tout la même chose du mois qui vient.
|
||||
const envisages = sansPhase.filter(({ projet }) => etatProjet(projet) === 'considered').length;
|
||||
const ecartes = sansPhase.length - envisages;
|
||||
const jalons = entrees.filter(({ phase }) => phase?.milestone).length;
|
||||
const taches = entrees.length - jalons - sansPhase.length;
|
||||
|
||||
const morceaux = [];
|
||||
if (taches) morceaux.push(`${taches} tâche${taches > 1 ? 's' : ''}`);
|
||||
if (jalons) morceaux.push(`${jalons} jalon${jalons > 1 ? 's' : ''}`);
|
||||
if (envisages) morceaux.push(`${envisages} envisagé${envisages > 1 ? 's' : ''}`);
|
||||
if (ecartes) morceaux.push(`${ecartes} écarté${ecartes > 1 ? 's' : ''}`);
|
||||
return morceaux.join(' · ');
|
||||
}
|
||||
|
||||
function construireEntree({ projet, phase }, tagsActifs) {
|
||||
function construireEntree(entree, tagsActifs) {
|
||||
return entree.horizon
|
||||
? construireEntreeHorizon(entree, tagsActifs)
|
||||
: construireEntreePhase(entree, tagsActifs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ligne d'un projet qui n'a pas de phase : mêmes colonnes que celle d'une phase,
|
||||
* pour que la liste reste une grille qu'on lit en diagonale, mais un contenu qui
|
||||
* dit partout que rien n'est arrêté.
|
||||
*
|
||||
* L'intitulé est en italique et l'emplacement du statut porte l'état : c'est
|
||||
* bien la même question — qu'est-ce qui arrive, et où ça en est — posée à un
|
||||
* objet qui n'a pas encore de dates. Un projet écarté avant d'avoir été engagé
|
||||
* passe par ici lui aussi, et se lit « Écarté » à la même place.
|
||||
*/
|
||||
function construireEntreeHorizon({ projet, horizon }, tagsActifs) {
|
||||
const etat = etatProjet(projet);
|
||||
|
||||
const ligne = document.createElement('li');
|
||||
ligne.className = 'mois-entree mois-entree--envisage';
|
||||
ligne.dataset.etat = etat;
|
||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||
if (projet.notes) ligne.classList.add('mois-entree--commentee');
|
||||
ligne.title =
|
||||
`${projet.name}\n\n${LIBELLES_ETAT[etat]} — ${formaterHorizon(horizon)}` +
|
||||
(projet.notes ? `\n\n${aplatirMarkdown(projet.notes)}` : '');
|
||||
|
||||
ligne.append(triangle(projet.notes, projet.name));
|
||||
|
||||
const marque = document.createElement('span');
|
||||
marque.className = 'mois-entree__marque';
|
||||
ligne.append(marque);
|
||||
|
||||
const dates = document.createElement('span');
|
||||
dates.className = 'mois-entree__dates';
|
||||
// Le mois du groupe est déjà écrit en titre : on n'en répète que la fin quand
|
||||
// l'horizon déborde, comme `formaterPeriode` le fait pour une phase.
|
||||
dates.textContent = horizon.start === horizon.end ? '—' : `→ ${formaterMois(horizon.end)}`;
|
||||
ligne.append(dates);
|
||||
|
||||
const nomProjet = document.createElement('span');
|
||||
nomProjet.className = 'mois-entree__projet';
|
||||
nomProjet.textContent = projet.name;
|
||||
ligne.append(nomProjet);
|
||||
|
||||
ligne.append(pastillesTags(projet.tags, tagsActifs));
|
||||
|
||||
const nom = document.createElement('span');
|
||||
nom.className = 'mois-entree__nom';
|
||||
nom.textContent = formaterHorizon(horizon);
|
||||
ligne.append(nom);
|
||||
|
||||
const statut = document.createElement('span');
|
||||
statut.className = 'mois-entree__statut';
|
||||
statut.dataset.statut = etat;
|
||||
statut.textContent = LIBELLES_ETAT[etat];
|
||||
ligne.append(statut);
|
||||
|
||||
if (projet.notes) ligne.append(notes(projet.notes));
|
||||
|
||||
return ligne;
|
||||
}
|
||||
|
||||
function construireEntreePhase({ projet, phase }, tagsActifs) {
|
||||
const ligne = document.createElement('li');
|
||||
ligne.className = `mois-entree mois-entree--${phase.status}`;
|
||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||
@@ -141,7 +218,7 @@ function construireEntree({ projet, phase }, tagsActifs) {
|
||||
if (phase.notes) ligne.classList.add('mois-entree--commentee');
|
||||
ligne.title = infobulle(projet, phase);
|
||||
|
||||
ligne.append(triangle(phase));
|
||||
ligne.append(triangle(phase.notes, phase.name));
|
||||
|
||||
// La marque reprend les formes de la frise — losange pour un jalon, barre
|
||||
// pour une tâche — pour qu'on reconnaisse au même dessin ce qu'on a appris à
|
||||
@@ -179,7 +256,7 @@ function construireEntree({ projet, phase }, tagsActifs) {
|
||||
statut.textContent = LIBELLES_STATUT[phase.status];
|
||||
ligne.append(statut);
|
||||
|
||||
if (phase.notes) ligne.append(notes(phase));
|
||||
if (phase.notes) ligne.append(notes(phase.notes));
|
||||
|
||||
return ligne;
|
||||
}
|
||||
@@ -196,9 +273,13 @@ function construireEntree({ projet, phase }, tagsActifs) {
|
||||
* Les lignes sans notes reçoivent tout de même une case vide : les colonnes de
|
||||
* la grille sont comptées, et il n'y a pas de raison qu'une phase commentée
|
||||
* décale toutes les autres.
|
||||
*
|
||||
* Il reçoit le texte et le nom plutôt que l'objet qui les porte : une ligne de
|
||||
* projet envisagé déplie les notes du projet là où une ligne de phase déplie les
|
||||
* siennes, et le triangle n'a pas à savoir laquelle des deux il sert.
|
||||
*/
|
||||
function triangle(phase) {
|
||||
if (!phase.notes) {
|
||||
function triangle(texte, nom) {
|
||||
if (!texte) {
|
||||
const vide = document.createElement('span');
|
||||
vide.className = 'mois-entree__plier';
|
||||
return vide;
|
||||
@@ -210,9 +291,9 @@ function triangle(phase) {
|
||||
bouton.dataset.action = 'notes';
|
||||
bouton.setAttribute('aria-expanded', 'false');
|
||||
// La ligne entière déplie, mais elle n'est pas focusable : ce bouton est le
|
||||
// seul chemin clavier vers les notes, il lui faut donc un nom qui dise de
|
||||
// quelle phase il parle.
|
||||
bouton.setAttribute('aria-label', `Notes de ${phase.name}`);
|
||||
// seul chemin clavier vers les notes, il lui faut donc un nom qui dise de quoi
|
||||
// il parle.
|
||||
bouton.setAttribute('aria-label', `Notes de ${nom}`);
|
||||
bouton.title = 'Afficher les notes';
|
||||
bouton.textContent = '▶';
|
||||
return bouton;
|
||||
@@ -230,10 +311,10 @@ function triangle(phase) {
|
||||
* qu'on n'y casse rien. On les voit cochées ou non, ce qui est justement ce
|
||||
* qu'on vient y chercher quand on fait le point.
|
||||
*/
|
||||
function notes(phase) {
|
||||
function notes(texte) {
|
||||
const bloc = document.createElement('div');
|
||||
bloc.className = 'mois-entree__notes';
|
||||
bloc.append(rendreMarkdown(phase.notes));
|
||||
bloc.append(rendreMarkdown(texte));
|
||||
bloc.hidden = true;
|
||||
return bloc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user