From 07712da774b2cf5bd49facb9efa6aedeec96cbf1 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Wed, 6 May 2026 23:29:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(plans/wizard):=20calendrier=20vraiment=20pr?= =?UTF-8?q?atique=20(3=20probl=C3=A8mes=20UX)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues remontées : - Cellules étirées en horizontal sur desktop (rectangles plats) - Échéance et "Doux" indiscernables (tous deux en rubis-glow) - Pas de feedback au clic, "Étape 1" déjà affichée par défaut sans qu'on l'ait sélectionnée - "Étape 1 · 20 juin · J+5" pas parlant - Cliquer sur une case vide ne faisait rien Refonte : - Calendrier max-w-md mx-auto + cells aspect-square → carrés équilibrés - Échéance = bg-rubis solide (pas glow) + ◆ blanc + ombre rubis → visuellement distincte de toutes les tonalités - Cellule étape = couleur tonalité + badge "J+X" en coin haut-droit - Sélection forte : ring-4 + scale-1.08 + shadow-rubis-hover sur la case sélectionnée → impossible de la rater - Default selectedIdx = -1 (pas de présélection) → hint clair : "Touchez une case colorée pour modifier, ou un jour vide pour ajouter" - **Click sur case vide → crée une étape à cet offset**, triée par ordre temporel (insertion smart, pas en bout). Plus l'usage le plus naturel de l'outil : "je veux relancer le 5 juin" → clic. - Click sur échéance → toast explicatif (pas une no-op silencieuse) - Header de l'éditeur : "Relance du **5 juin** · J-10" (pas "Étape 1") - Hover sur jour vide : "+" rubis apparaît → affordance d'ajout claire - Hors plage [-30, +180] ou >= 8 étapes : cellule disabled, toast info Co-Authored-By: Claude Opus 4.7 --- .../plans/wizard/CadenceCalendar.tsx | 332 ++++++++++++------ apps/web/src/routes/_app/plans_.nouveau.tsx | 30 +- 2 files changed, 247 insertions(+), 115 deletions(-) diff --git a/apps/web/src/components/plans/wizard/CadenceCalendar.tsx b/apps/web/src/components/plans/wizard/CadenceCalendar.tsx index 0058c48..c47bdd3 100644 --- a/apps/web/src/components/plans/wizard/CadenceCalendar.tsx +++ b/apps/web/src/components/plans/wizard/CadenceCalendar.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { ChevronLeft, ChevronRight, Plus, Trash2, AlertTriangle } from "lucide-react"; +import { toast } from "sonner"; import type { RelanceTone } from "@rubis/shared"; import { cn } from "@/lib/utils"; @@ -17,14 +18,17 @@ export type DraftStepLite = { }; /** - * Mini-calendrier mensuel comme outil de **navigation** dans la cadence. - * Compact : un mois à la fois, prev/next, éditeur compact en dessous. + * Mini-calendrier mensuel pratique : on **navigue** dans la cadence, + * on **ajoute** une relance en touchant une case vide, on **modifie** + * en touchant une case colorée. Édition compacte juste en dessous. * - * - Date d'échéance fictive : le 15 du mois prochain. - * - Auto-jump au mois de l'étape sélectionnée → la cellule reste visible. - * - Cellule échéance = ◆ rubis (info-only). - * - Cellule étape = couleur de fond selon la tonalité, cliquable pour - * sélectionner et éditer juste en dessous. + * Distinctions visuelles importantes : + * - Échéance : ◆ rubis solide (le repère héros, pas une "tonalité") + * - Étape : couleur de fond selon la tonalité (4 niveaux escalade) + * - Sélectionnée : ring rubis + scale + * - Vide : numéro discret, hover montre que c'est cliquable + * + * Date de référence : le 15 du mois prochain. */ export function CadenceCalendar({ steps, @@ -32,14 +36,20 @@ export function CadenceCalendar({ onSelectStep, onUpdateStep, onAddStep, + onAddStepAtOffset, onRemoveStep, + globalTone, }: { steps: DraftStepLite[]; selectedIndex?: number; onSelectStep?: (idx: number) => void; onUpdateStep?: (idx: number, patch: Partial) => void; onAddStep: () => void; + /** Création directe en cliquant une case vide. Reçoit l'offset depuis l'échéance. */ + onAddStepAtOffset?: (offsetDays: number) => void; onRemoveStep: (idx: number) => void; + /** Tonalité par défaut pour une étape créée depuis le calendrier. */ + globalTone?: RelanceTone; }) { const dueDate = useMemo(() => { const now = new Date(); @@ -51,12 +61,11 @@ export function CadenceCalendar({ [steps, dueDate], ); - // Mois affiché : par défaut = mois de l'échéance ; auto-jump au mois de - // l'étape sélectionnée si on en a une. const [viewMonth, setViewMonth] = useState( () => new Date(dueDate.getFullYear(), dueDate.getMonth(), 1), ); + // Auto-jump au mois de l'étape sélectionnée pour ne pas perdre la cellule. useEffect(() => { if (selectedIndex < 0) return; const target = stepDates[selectedIndex]; @@ -71,10 +80,23 @@ export function CadenceCalendar({ const selected = selectedIndex >= 0 ? steps[selectedIndex] : null; + const handleEmptyDayClick = (date: Date) => { + if (steps.length >= 8) { + toast.info("8 étapes max par plan."); + return; + } + const offset = daysBetween(dueDate, date); + if (offset < -30 || offset > 180) { + toast.info("Hors plage : on accepte de J-30 à J+180."); + return; + } + onAddStepAtOffset?.(offset); + }; + return ( -
- {/* Header — month nav + compteur */} -
+
+ {/* Header — month nav */} +
- {/* Grille du mois */} + toast.info( + `Le 15 du mois est l'échéance fictive de votre facture. Elle ne se modifie pas — c'est juste un repère pour visualiser le timing.`, + ) + } + onClickEmptyDay={handleEmptyDayClick} + canAddMore={steps.length < 8} /> - {/* Légende compacte */} -

- ◆ Échéance fictive le{" "} - {formatLongDate(dueDate)} · couleur = - tonalité -

+ {/* Légende compacte avec les 4 tonalités + échéance */} +
+ + + + + +
- {/* Éditeur inline compact */} + {/* Éditeur ou hint */}
{selected ? ( 1} onUpdate={(patch) => onUpdateStep?.(selectedIndex, patch)} onRemove={() => onRemoveStep(selectedIndex)} /> ) : ( -

- Touchez une case colorée pour modifier la relance. +

+ Touchez une case colorée pour + modifier une relance, ou un jour vide{" "} + pour en ajouter une.

)}
- {/* Footer — ajouter + compteur */} -
- {steps.length < 8 && ( - - )} -

- {steps.length}/8 -

-
+ {/* Footer — bouton Ajouter de secours */} + {steps.length < 8 && ( + + )}
); } +function LegendDot({ + label, + className, + isDiamond = false, +}: { + label: string; + className: string; + isDiamond?: boolean; +}) { + return ( + + + ); +} + // ============================================================================ -// Grille d'un mois — cellules compactes, h-fixed pour stabilité visuelle +// Grille mensuelle — cellules carrées (aspect-square), max-w via container // ============================================================================ const TONE_CELL_CLASS: Record = { @@ -180,6 +234,9 @@ function MonthGrid({ stepDates, selectedIndex, onSelectStep, + onClickDueDate, + onClickEmptyDay, + canAddMore, }: { month: Date; dueDate: Date; @@ -187,6 +244,9 @@ function MonthGrid({ stepDates: Date[]; selectedIndex: number; onSelectStep?: (idx: number) => void; + onClickDueDate: () => void; + onClickEmptyDay: (date: Date) => void; + canAddMore: boolean; }) { const firstDay = new Date(month.getFullYear(), month.getMonth(), 1); const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0); @@ -218,6 +278,9 @@ function MonthGrid({ stepDates={stepDates} selectedIndex={selectedIndex} onSelectStep={onSelectStep} + onClickDueDate={onClickDueDate} + onClickEmptyDay={onClickEmptyDay} + canAddMore={canAddMore} /> ))}
@@ -231,6 +294,9 @@ function DayCell({ stepDates, selectedIndex, onSelectStep, + onClickDueDate, + onClickEmptyDay, + canAddMore, }: { date: Date | null; dueDate: Date; @@ -238,37 +304,42 @@ function DayCell({ stepDates: Date[]; selectedIndex: number; onSelectStep?: (idx: number) => void; + onClickDueDate: () => void; + onClickEmptyDay: (date: Date) => void; + canAddMore: boolean; }) { - if (!date) return