Ajouter une vue des tâches et jalons groupés par mois
La frise répond mal à « qu'est-ce qui arrive le mois prochain » : la question traverse les projets, alors que la frise les range précisément en couloirs séparés. Il fallait balayer une colonne de dates à travers tous les couloirs en gardant en tête ce qu'on avait vu plus haut — d'où le tableur ressorti à côté. Deux boutons dans la barre d'outils basculent vers une liste qui déroule toutes les phases, projets mêlés, dans l'ordre du calendrier. Chaque ligne se lit quand, chez qui, puis quoi : période, projet et ses tags, intitulé, statut. Une phase est rattachée au seul mois de son début. La répéter dans chaque mois traversé aurait donné un tableau de charge — utile, mais c'est une autre vue, et le doublon coûte la lecture en diagonale qui fait tout l'intérêt de celle-ci. Sa fin est écrite sur la ligne, ce qui suffit à voir qu'elle déborde. Les mois sans rien ne sont pas représentés : l'absence se lit déjà dans le saut d'un titre au suivant. La liste ne s'édite pas, et ce n'est pas une lacune à combler : déplacer une phase demande de voir ce qu'elle chevauche et ce qu'elle repousse, or c'est justement ce voisinage que l'ordre chronologique dissout. Seules les pastilles de tags restent cliquables — filtrer ne change pas les données. Le filtre par tags vaut pour les deux vues, sans rien changer à sa mécanique, si bien qu'on ne se demande jamais si l'une montre plus que l'autre. L'œil (hidden) écarte un projet de la liste comme il écarte ses barres. La vue choisie n'est pas enregistrée, contrairement à la largeur de la colonne des libellés : cette largeur est un réglage d'écran, la vue est un geste de consultation. L'outil s'ouvre donc sur la frise, celle où l'on modifie. Le défilement horizontal de la frise, lui, est mémorisé le temps de l'aller-retour, puisque masquer un élément lui fait perdre son scrollLeft. Deux extractions au passage, chacune parce que deux vues sœurs en ont besoin et qu'aucune n'a à importer l'autre : les noms de mois et le formatage des dates rejoignent model.js, les pastilles de tags un module tags.js. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
104
js/model.js
104
js/model.js
@@ -29,6 +29,21 @@ export const COULEURS = [
|
||||
'#8b5cf6', '#06b6d4', '#ec4899', '#84cc16',
|
||||
];
|
||||
|
||||
/**
|
||||
* Noms de mois, en deux longueurs. Ils vivent ici plutôt que dans une vue :
|
||||
* la frise et la vue par mois les emploient toutes les deux, et aucune des deux
|
||||
* n'a à dépendre de l'autre pour écrire « septembre ».
|
||||
*/
|
||||
export const MOIS_COURTS = [
|
||||
'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
|
||||
'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.',
|
||||
];
|
||||
|
||||
export const MOIS_LONGS = [
|
||||
'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
|
||||
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre',
|
||||
];
|
||||
|
||||
const MOTIF_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const MOTIF_COULEUR = /^#[0-9a-f]{6}$/i;
|
||||
const MS_PAR_JOUR = 86400000;
|
||||
@@ -123,6 +138,43 @@ export function semaineISO(texte) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Date « AAAA-MM-JJ » rendue lisible : « 12 sept. 2026 ». */
|
||||
export function formaterDateLongue(date) {
|
||||
const [a, m, j] = date.split('-');
|
||||
return `${j} ${MOIS_COURTS[Number(m) - 1]} ${a}`;
|
||||
}
|
||||
|
||||
/** Mois d'une date, sous la forme « AAAA-MM » — la clé de regroupement. */
|
||||
export function moisDe(date) {
|
||||
return date.slice(0, 7);
|
||||
}
|
||||
|
||||
/** Mois « AAAA-MM » rendu lisible : « septembre 2026 ». */
|
||||
export function formaterMois(mois) {
|
||||
return `${MOIS_LONGS[Number(mois.slice(5, 7)) - 1]} ${mois.slice(0, 4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Période d'une phase, abrégée pour une liste où le mois est déjà écrit en
|
||||
* titre : « 12 » pour un jalon, « 12 → 30 » pour une tâche qui tient dans le
|
||||
* mois, « 12 → 4 nov. » pour une qui en déborde, « 12 → 4 janv. 2027 » pour une
|
||||
* qui déborde de l'année. Rien n'est répété tant que rien ne change, ce qui
|
||||
* fait de l'apparition d'un mois — ou d'une année — le signal du dépassement.
|
||||
*/
|
||||
export function formaterPeriode(phase) {
|
||||
const quantieme = (date) => String(Number(date.slice(8, 10)));
|
||||
if (phase.milestone) return quantieme(phase.start);
|
||||
|
||||
if (phase.end.slice(0, 7) === phase.start.slice(0, 7)) {
|
||||
return `${quantieme(phase.start)} → ${quantieme(phase.end)}`;
|
||||
}
|
||||
|
||||
const moisFin = MOIS_COURTS[Number(phase.end.slice(5, 7)) - 1];
|
||||
const anneeFin =
|
||||
phase.end.slice(0, 4) === phase.start.slice(0, 4) ? '' : ` ${phase.end.slice(0, 4)}`;
|
||||
return `${quantieme(phase.start)} → ${quantieme(phase.end)} ${moisFin}${anneeFin}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identifiants
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -499,6 +551,58 @@ export function formulerDerive(jours) {
|
||||
return jours > 0 ? `+${quantite}` : `−${quantite}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Regroupement par mois
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toutes les phases du planning regroupées par mois, projets mêlés.
|
||||
*
|
||||
* Une phase est rattachée au **mois de son début**, et à lui seul. Une phase de
|
||||
* trois mois n'apparaît donc qu'une fois, au mois où elle démarre : la liste
|
||||
* répond à « qu'est-ce qui commence, et quand », question à laquelle la frise
|
||||
* répond mal parce qu'elle éparpille les départs sur autant de couloirs qu'il y
|
||||
* a de projets. La répéter dans chaque mois traversé en aurait fait un tableau
|
||||
* de charge — utile, mais c'est une autre vue, et le doublon coûte la lecture
|
||||
* en diagonale qui fait tout l'intérêt de celle-ci. Sa fin est écrite sur la
|
||||
* ligne, ce qui suffit à voir qu'elle déborde.
|
||||
*
|
||||
* Les deux tamis de la frise s'appliquent de la même façon (timeline.js) : le
|
||||
* filtre par tags retire le projet, l'œil (`hidden`) aussi — un projet dont on
|
||||
* a masqué les barres n'a pas à revenir par la liste.
|
||||
*
|
||||
* Les mois sans rien ne sont pas représentés : intercaler « novembre 2026 —
|
||||
* rien » entre deux mois pleins allongerait la liste de tout le temps mort d'un
|
||||
* planning, alors que l'absence se lit déjà dans le saut d'un titre à l'autre.
|
||||
*
|
||||
* @returns {Array<{mois: string, entrees: Array<{projet: object, phase: object}>}>}
|
||||
* trié du plus ancien au plus récent, chaque mois trié par date puis par nom.
|
||||
*/
|
||||
export function phasesParMois(planning, tagsActifs = []) {
|
||||
const groupes = new Map();
|
||||
|
||||
for (const projet of planning.projects) {
|
||||
if (projet.hidden || !projetFiltre(projet, tagsActifs)) continue;
|
||||
for (const phase of projet.phases) {
|
||||
const mois = moisDe(phase.start);
|
||||
if (!groupes.has(mois)) groupes.set(mois, []);
|
||||
groupes.get(mois).push({ projet, phase });
|
||||
}
|
||||
}
|
||||
|
||||
return [...groupes.keys()]
|
||||
.sort() // « AAAA-MM » se trie comme du texte
|
||||
.map((mois) => ({
|
||||
mois,
|
||||
entrees: groupes.get(mois).sort(
|
||||
(a, b) =>
|
||||
versUTC(a.phase.start) - versUTC(b.phase.start) ||
|
||||
a.projet.name.localeCompare(b.projet.name, 'fr') ||
|
||||
a.phase.name.localeCompare(b.phase.name, 'fr')
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modifications
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user