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