/** * Panneau de détail : édition précise d'une phase. * * Le panneau applique les changements au fil de la saisie plutôt qu'à la * validation d'un formulaire. La frise se met à jour en direct, et la * sauvegarde debouncée de storage.js absorbe la fréquence des modifications. * * C'est aussi ici qu'on saisit une date exacte : le glisser sur la frise * s'accroche au lundi, ce panneau est le recours quand une phase doit démarrer * un jeudi précis. */ import { STATUTS, dateValide, modifierPhase } from './model.js'; export function creerPanneau(refs, rappels) { const { panneau, voile, formulaire, titre, erreur, champFin, supprimer, fermer } = refs; let selection = null; // { projet, phase } let phaseCourante = null; function afficherErreur(message) { erreur.textContent = message || ''; erreur.hidden = !message; } function remplir(projet, phase) { titre.textContent = `${projet.name} — ${phase.name}`; formulaire.name.value = phase.name; formulaire.start.value = phase.start; formulaire.end.value = phase.end; formulaire.status.value = STATUTS.includes(phase.status) ? phase.status : 'todo'; formulaire.milestone.checked = phase.milestone; formulaire.notes.value = phase.notes; // Un jalon tient sur une seule date : le champ « fin » n'a rien à dire. champFin.hidden = phase.milestone; afficherErreur(null); } /** Lit le formulaire, applique les invariants et remonte le changement. */ function appliquer() { if (!selection || !phaseCourante) return; const nom = formulaire.name.value.trim(); if (!nom) { afficherErreur('Le nom ne peut pas être vide.'); return; } const debut = formulaire.start.value; if (!dateValide(debut)) { // Saisie en cours dans le champ date : on attend sans rien changer. return; } const jalon = formulaire.milestone.checked; const fin = jalon ? debut : formulaire.end.value; if (!jalon && !dateValide(fin)) return; const avant = phaseCourante; const apres = modifierPhase(phaseCourante, { name: nom, start: debut, end: fin, status: formulaire.status.value, milestone: jalon, notes: formulaire.notes.value, }); // modifierPhase a pu corriger la fin (jalon, ou fin antérieure au début). // On reflète cette correction dans le formulaire pour que l'utilisateur // voie ce qui a réellement été enregistré. if (apres.end !== fin) { formulaire.end.value = apres.end; if (!jalon) { afficherErreur('La fin précédait le début : la durée précédente a été conservée.'); } } else { afficherErreur(null); } champFin.hidden = jalon; phaseCourante = apres; if (!sontEgales(avant, apres)) { rappels.surModification(selection.projet, selection.phase, apres); } } formulaire.addEventListener('input', appliquer); formulaire.addEventListener('change', appliquer); formulaire.addEventListener('submit', (evenement) => evenement.preventDefault()); supprimer.addEventListener('click', async () => { if (!selection) return; const cible = selection; const nom = phaseCourante.name; if (!(await rappels.demanderSuppression(cible.projet, cible.phase, nom))) return; // Le panneau a pu être fermé ou changer de phase pendant la confirmation. if (selection?.phase !== cible.phase || selection?.projet !== cible.projet) return; api.fermer(); rappels.surSuppression(cible.projet, cible.phase); }); fermer.addEventListener('click', () => api.fermer()); voile.addEventListener('click', () => api.fermer()); document.addEventListener('keydown', (evenement) => { // Échap ferme le panneau, sauf pendant une saisie où il annule d'abord la // complétion native du champ. if (evenement.key === 'Escape' && !panneau.hidden) { api.fermer(); } }); const api = { ouvrir(projet, phase) { selection = { projet: projet.id, phase: phase.id }; phaseCourante = phase; remplir(projet, phase); panneau.hidden = false; voile.hidden = false; formulaire.name.focus(); formulaire.name.select(); }, /** Recharge le contenu après une modification venue de la frise. */ rafraichir(projet, phase) { if (!selection || selection.phase !== phase.id || selection.projet !== projet.id) return; // On ne réécrit pas le champ qui a le focus : cela déplacerait le curseur // au milieu d'une frappe. const actif = document.activeElement; phaseCourante = phase; titre.textContent = `${projet.name} — ${phase.name}`; for (const champ of ['name', 'start', 'end', 'status', 'notes']) { if (formulaire[champ] !== actif) formulaire[champ].value = phase[champ]; } if (formulaire.milestone !== actif) formulaire.milestone.checked = phase.milestone; champFin.hidden = phase.milestone; }, fermer() { selection = null; phaseCourante = null; panneau.hidden = true; voile.hidden = true; rappels.surFermeture(); }, selection() { return selection; }, }; return api; } function sontEgales(a, b) { return ( a.name === b.name && a.start === b.start && a.end === b.end && a.status === b.status && a.milestone === b.milestone && a.notes === b.notes ); }