Repousser un projet en bloc, sauf ce dont on ne décide pas
Glisser la barre résumé décale toutes les phases du même nombre de jours, écarts conservés : un projet engagé glisse souvent, et le faire phase par phase les déformait. Restent en place ce qui est terminé — on ne réécrit pas le passé — et les phases à date imposée, nouveau booléen `fixed` pour ce qui est délégué ou tenu du dehors. D'où le format en version 6 : sans l'incrément, un binaire antérieur effacerait ces dates imposées à la première sauvegarde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
100
js/model.js
100
js/model.js
@@ -15,16 +15,17 @@
|
||||
/**
|
||||
* Version du format de fichier.
|
||||
*
|
||||
* Elle a changé pour la dernière fois quand le projet a reçu son cycle de vie —
|
||||
* `horizon`, `completedDate`, `discardedDate` (v5). Un champ ajouté n'oblige
|
||||
* pourtant à rien : la v4 se relit sans encombre, et le validateur ne refuse
|
||||
* qu'un fichier *plus récent* que lui. C'est justement là qu'est la raison
|
||||
* d'incrémenter — le validateur reconstruit chaque projet champ par champ et
|
||||
* laisse tomber ce qu'il ne connaît pas. Sans ce numéro, un binaire antérieur
|
||||
* ouvrirait un fichier v5 sans broncher et en effacerait tous les horizons à la
|
||||
* première sauvegarde.
|
||||
* Elle a changé pour la dernière fois quand la phase a reçu sa date imposée —
|
||||
* `fixed` (v6) ; avant cela, quand le projet a reçu son cycle de vie (v5). Un
|
||||
* champ ajouté n'oblige pourtant à rien : la v5 se relit sans encombre, et le
|
||||
* validateur ne refuse qu'un fichier *plus récent* que lui. C'est justement là
|
||||
* qu'est la raison d'incrémenter — le validateur reconstruit chaque projet champ
|
||||
* par champ et laisse tomber ce qu'il ne connaît pas. Sans ce numéro, un binaire
|
||||
* antérieur ouvrirait un fichier v6 sans broncher et en effacerait toutes les
|
||||
* dates imposées à la première sauvegarde : les phases concernées se remettraient
|
||||
* alors à suivre le décalage de leur projet, en silence.
|
||||
*/
|
||||
export const VERSION_FORMAT = 5;
|
||||
export const VERSION_FORMAT = 6;
|
||||
|
||||
export const STATUTS = ['todo', 'doing', 'done', 'blocked'];
|
||||
|
||||
@@ -652,6 +653,10 @@ function validerPhase(phase, rang, repereProjet, idsPhases) {
|
||||
// chargement : la valeur est cosmétique, contrairement aux dates.
|
||||
status: STATUTS.includes(phase.status) ? phase.status : 'todo',
|
||||
milestone: jalon,
|
||||
// Absente d'un fichier en version 5, où elle vaut donc `false` : aucune date
|
||||
// n'y était déclarée imposée, et rien dans les données ne permettrait de
|
||||
// deviner laquelle l'était.
|
||||
fixed: Boolean(phase.fixed),
|
||||
notes: typeof phase.notes === 'string' ? phase.notes : '',
|
||||
...(reference ? { baseline: reference } : {}),
|
||||
};
|
||||
@@ -996,6 +1001,7 @@ export function creerPhase(projet, nom, debut, fin, options = {}) {
|
||||
end: jalon ? debut : fin,
|
||||
status: STATUTS.includes(options.status) ? options.status : 'todo',
|
||||
milestone: jalon,
|
||||
fixed: Boolean(options.fixed),
|
||||
notes: options.notes || '',
|
||||
};
|
||||
}
|
||||
@@ -1006,6 +1012,82 @@ export function deplacerPhase(phase, nouveauDebut) {
|
||||
return { ...phase, start: nouveauDebut, end: ajouterJours(nouveauDebut, jours) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Les phases qu'un décalage de projet emporte. Deux sortes en sont exclues, pour
|
||||
* deux raisons qui n'ont rien à voir.
|
||||
*
|
||||
* **Ce qui est terminé.** Décaler un projet, c'est reconnaître qu'il prendra plus
|
||||
* de temps que prévu — et ce qui est fait est fait. Emporter les phases `done`
|
||||
* réécrirait un passé qu'on a vécu, et effacerait du même coup la dérive qu'on
|
||||
* cherche justement à lire : la référence figée, elle, ne bouge pas. C'est le
|
||||
* statut qui décide, jamais la position dans le calendrier : une phase terminée
|
||||
* en avance reste où elle est, une phase `blocked` ou `doing` déjà commencée se
|
||||
* décale avec le reste — c'est bien elle qui glisse.
|
||||
*
|
||||
* **Ce dont on ne décide pas.** Une phase à `fixed` porte une date qui ne nous
|
||||
* appartient pas : elle est déléguée, contractuelle ou réglementaire. Repousser
|
||||
* le projet ne repousse pas l'audit du commissaire aux comptes ; le décalage lui
|
||||
* passe à travers, et le chevauchement qui en résulte est précisément
|
||||
* l'information qu'on veut voir.
|
||||
*/
|
||||
export function phasesDecalables(projet) {
|
||||
return projet.phases.filter((phase) => phase.status !== 'done' && !phase.fixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Date à laquelle s'accroche le geste de décalage : le début de la première
|
||||
* phase qui bougera. Null si tout est terminé — il n'y a alors rien à décaler.
|
||||
*
|
||||
* C'est cette date, et non le début du projet, qui sert d'ancre : sur un projet
|
||||
* dont les premières phases sont closes, le bord gauche de ce qui bouge doit
|
||||
* suivre le pointeur, pas un bord qui reste sur place.
|
||||
*/
|
||||
export function ancreDecalage(projet) {
|
||||
const decalables = phasesDecalables(projet);
|
||||
if (!decalables.length) return null;
|
||||
return decalables.reduce(
|
||||
(debut, phase) => (versUTC(phase.start) < versUTC(debut) ? phase.start : debut),
|
||||
decalables[0].start
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Décale d'un même nombre de jours les phases que le projet emporte —
|
||||
* `phasesDecalables` dit lesquelles.
|
||||
*
|
||||
* Le même delta partout, jalons compris : les écarts entre phases emportées sont
|
||||
* conservés au jour près, ce qui est tout l'intérêt du geste — on repousse un
|
||||
* plan sans le replanifier. Celles qui restent font que le projet ne se déplace
|
||||
* pas tant qu'il s'étire.
|
||||
*
|
||||
* L'appartenance se décide **une fois**, avant de rien décaler, et se retient par
|
||||
* identifiant : la parcourir au fil de la transformation reviendrait à interroger
|
||||
* des phases déjà déplacées.
|
||||
*
|
||||
* Rien n'empêche le décalage de faire chevaucher deux phases : il n'y a pas de
|
||||
* dépendances entre phases dans ce modèle (docs/decisions.md, section 2), et
|
||||
* inventer une butée ici reviendrait à en poser une par la bande.
|
||||
*/
|
||||
export function decalerProjet(projet, jours) {
|
||||
if (!jours) return projet;
|
||||
const emportees = new Set(phasesDecalables(projet).map((phase) => phase.id));
|
||||
|
||||
return {
|
||||
...projet,
|
||||
phases: trierPhases(
|
||||
projet.phases.map((phase) =>
|
||||
emportees.has(phase.id)
|
||||
? {
|
||||
...phase,
|
||||
start: ajouterJours(phase.start, jours),
|
||||
end: ajouterJours(phase.end, jours),
|
||||
}
|
||||
: phase
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change une seule borne — le glisser d'un bord de barre.
|
||||
* La borne opposée fait butée : une phase ne peut pas se retourner.
|
||||
|
||||
Reference in New Issue
Block a user