feat(ui): lot 3 — toutes les pages lisibles, cohérentes et en français clair

Chaque page reprend le socle du lot 2 : en-tête avec une phrase
d'explication, couleurs du thème (plus de pages figées en blanc), textes
de 12 px minimum, noms de magasins, infobulles sur le jargon, retours par
toast / confirmation, états vides et d'erreur expliqués.

- Accueil : CA, tickets et panier moyen d'hier vs N-1, par magasin, top 10
  avec lien vers la fiche produit.
- Révision d'assortiment : un seul endroit pour enregistrer et exporter
  (menu Export unique, mêmes droits pour tous), annulation confirmée, sens
  des gammes affiché partout, magasin choisi en un clic, colonnes nommées
  en clair, bandeau de progression au lieu de la fenêtre floue, droits lus
  côté serveur, ligne sélectionnée opaque sur les colonnes figées.
- Recherche produit, Stocks à surveiller (export FF inchangé), Meilleures
  ventes, Ventes par mois (dernier mois complet par défaut), Publicités,
  Commandes fournisseurs, Historique, Paramètres (onglets), Synchronisation
  et Connexion migrés sur DataTable / Tabs / Card.
- Le nom de l'utilisateur est transmis à la session (affiché dans
  l'en-tête).
- Composants devenus inutiles supprimés (anciennes fenêtres modales,
  export-dropdown, financial-cell, supplier-selection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
Claude committed 2026-10-03 21:05:46 +00:00
1 parent 0c890c84c5
commit 220ca2263a
61 files changed
+6101 -6864

No files matched your search

@@ -1,203 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { X, KeyRound, Eye, EyeOff, Loader2, ShieldCheck } from "lucide-react";
interface ChangePasswordModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (newPassword: string) => Promise<void>;
username: string;
}
export function ChangePasswordModal({
isOpen,
onClose,
onConfirm,
username,
}: ChangePasswordModalProps) {
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isVisible, setIsVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
setPassword("");
setConfirm("");
setError(null);
setTimeout(() => inputRef.current?.focus(), 50);
} else {
setIsVisible(false);
}
}, [isOpen]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") handleClose();
};
const handleClose = () => {
setIsVisible(false);
setTimeout(onClose, 250);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (password.length < 4) {
setError("Le mot de passe doit faire au moins 4 caractères.");
return;
}
if (password !== confirm) {
setError("Les mots de passe ne correspondent pas.");
return;
}
setIsLoading(true);
try {
await onConfirm(password);
handleClose();
} catch (err: any) {
setError(err?.message ?? "Une erreur est survenue.");
} finally {
setIsLoading(false);
}
};
if (!isOpen && !isVisible) return null;
return (
<div
className={`fixed inset-0 z-[100] flex items-center justify-center p-4 transition-all duration-250 ${isVisible ? "opacity-100 backdrop-blur-sm" : "opacity-0"}`}
onKeyDown={handleKeyDown}
>
{/* Backdrop */}
<div
className="fixed inset-0"
style={{ background: "rgba(0,0,0,0.55)" }}
onClick={handleClose}
/>
{/* Panel */}
<div
className={`relative w-full max-w-sm rounded-2xl overflow-hidden shadow-2xl transition-all duration-250 ${isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"}`}
style={{ background: "var(--bg-surface)", border: "1px solid var(--border)" }}
>
{/* Accent bar */}
<div className="absolute top-0 left-0 w-full h-0.5" style={{ background: "linear-gradient(to right, var(--accent), var(--accent-success))" }} />
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] text-[var(--text-muted)] transition-colors"
>
<X className="w-4 h-4" />
</button>
<div className="p-7">
{/* Header */}
<div className="flex items-center gap-4 mb-7">
<div className="w-11 h-11 rounded-xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center shrink-0">
<KeyRound className="w-5 h-5 text-[var(--accent)]" />
</div>
<div>
<h3 className="text-base font-bold text-[var(--text-primary)]">Changer le mot de passe</h3>
<p className="text-xs text-[var(--text-muted)]">Compte : <span className="font-semibold text-[var(--text-secondary)]">{username}</span></p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* New password */}
<div className="space-y-1.5">
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)]">
Nouveau mot de passe
</label>
<div className="relative">
<input
ref={inputRef}
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 4 caractères"
className="apple-input pr-10"
required
/>
<button
type="button"
onClick={() => setShowPassword(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{/* Confirm */}
<div className="space-y-1.5">
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)]">
Confirmer le mot de passe
</label>
<div className="relative">
<input
type={showConfirm ? "text" : "password"}
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
placeholder="Répétez le mot de passe"
className="apple-input pr-10"
required
/>
<button
type="button"
onClick={() => setShowConfirm(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
{showConfirm ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{/* Password match indicator */}
{confirm.length > 0 && (
<div className={`flex items-center gap-2 text-xs font-semibold ${password === confirm ? "text-[var(--accent-success)]" : "text-[var(--accent-error)]"}`}>
<ShieldCheck className="w-3.5 h-3.5" />
{password === confirm ? "Les mots de passe correspondent" : "Les mots de passe ne correspondent pas"}
</div>
)}
{/* Error */}
{error && (
<div className="px-3 py-2.5 rounded-lg bg-[var(--accent-error-bg)] border border-[var(--accent-error)] text-[var(--accent-error)] text-xs font-semibold">
{error}
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={handleClose}
className="apple-btn-secondary flex-1 justify-center"
>
Annuler
</button>
<button
type="submit"
disabled={isLoading || password !== confirm || password.length < 4}
className="apple-btn-primary flex-1 justify-center disabled:opacity-40"
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <KeyRound className="w-4 h-4" />}
{isLoading ? "Mise à jour..." : "Valider"}
</button>
</div>
</form>
</div>
</div>
</div>
);
}
-127
View File
@@ -1,127 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { X, AlertTriangle } from "lucide-react";
interface ConfirmModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title?: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: "danger" | "default";
}
export function ConfirmModal({
isOpen,
onClose,
onConfirm,
title = "Confirmation",
message,
confirmLabel = "OK",
cancelLabel = "Annuler",
variant = "default",
}: ConfirmModalProps) {
const [isVisible, setIsVisible] = useState(false);
const confirmBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
setTimeout(() => confirmBtnRef.current?.focus(), 50);
} else {
setIsVisible(false);
}
}, [isOpen]);
const handleClose = () => {
setIsVisible(false);
setTimeout(onClose, 250);
};
const handleConfirm = () => {
setIsVisible(false);
setTimeout(onConfirm, 250);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") handleClose();
if (e.key === "Enter") handleConfirm();
};
if (!isOpen && !isVisible) return null;
const isDanger = variant === "danger";
return (
<div
className={`fixed inset-0 z-[200] flex items-center justify-center p-4 transition-all duration-250 ${isVisible ? "opacity-100 backdrop-blur-sm" : "opacity-0"}`}
onKeyDown={handleKeyDown}
>
{/* Backdrop */}
<div
className="fixed inset-0"
style={{ background: "rgba(0,0,0,0.55)" }}
onClick={handleClose}
/>
{/* Panel */}
<div
className={`relative w-full max-w-sm rounded-2xl overflow-hidden shadow-2xl transition-all duration-250 ${isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"}`}
style={{ background: "var(--bg-surface)", border: "1px solid var(--border)" }}
>
{/* Accent bar */}
<div
className="absolute top-0 left-0 w-full h-0.5"
style={{
background: isDanger
? "linear-gradient(to right, var(--accent-error), #fb7185)"
: "linear-gradient(to right, var(--accent), var(--accent-success))"
}}
/>
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] text-[var(--text-muted)] transition-colors"
>
<X className="w-4 h-4" />
</button>
<div className="p-7">
{/* Icon + Title */}
<div className="flex items-center gap-3 mb-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${isDanger ? "bg-[var(--accent-error-bg)] border border-[var(--accent-error)]" : "bg-[var(--accent-bg)] border border-[var(--accent-border)]"}`}>
<AlertTriangle className={`w-5 h-5 ${isDanger ? "text-[var(--accent-error)]" : "text-[var(--accent)]"}`} />
</div>
<h3 className="text-base font-bold text-[var(--text-primary)]">{title}</h3>
</div>
{/* Message */}
<p className="text-sm text-[var(--text-secondary)] mb-7 leading-relaxed pl-[52px]">
{message}
</p>
{/* Actions */}
<div className="flex gap-3 justify-end">
<button
onClick={handleClose}
className="apple-btn-secondary"
>
{cancelLabel}
</button>
<button
ref={confirmBtnRef}
onClick={handleConfirm}
className={isDanger ? "apple-btn-danger" : "apple-btn-primary"}
>
{confirmLabel}
</button>
</div>
</div>
</div>
</div>
);
}
-45
View File
@@ -1,45 +0,0 @@
interface LoadingModalProps {
message?: string;
subMessage?: string;
}
export default function LoadingModal({
message = "Chargement des données",
subMessage = "Veuillez patienter...",
}: LoadingModalProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div
className="flex flex-col items-center gap-5 px-10 py-8 rounded-2xl shadow-2xl border"
style={{
background: "var(--bg-elevated)",
borderColor: "var(--border)",
}}
>
<div className="relative w-12 h-12">
<div
className="absolute inset-0 rounded-full border-[3px] animate-spin"
style={{
borderColor: "var(--border)",
borderTopColor: "var(--accent)",
}}
/>
</div>
<div className="flex flex-col items-center gap-1.5">
<span
className="text-sm font-semibold tracking-wide"
style={{ color: "var(--text-primary)" }}
>
{message}
</span>
<span
className="text-xs"
style={{ color: "var(--text-muted)" }}
>
{subMessage}
</span>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -54,7 +54,7 @@ export function Sidebar() {
<aside
className={cn(
"flex-shrink-0 flex flex-col h-screen z-30 glass transition-[width] duration-200 relative border-r border-[var(--border)]",
isExpanded ? "w-[272px]" : "w-16",
isExpanded ? "w-[280px]" : "w-16",
)}
aria-label="Menu principal"
>
@@ -78,7 +78,7 @@ export function Sidebar() {
</div>
{/* Navigation */}
<nav className={cn("flex-1 overflow-y-auto py-3", isExpanded ? "px-3" : "px-2")}>
<nav className={cn("flex-1 overflow-y-auto py-3 px-2")}>
{groupes.map((groupe, gi) => (
<div key={groupe.title} className={cn(gi > 0 && "mt-4")}>
{isExpanded ? (
-130
View File
@@ -1,130 +0,0 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { AlertCircle, CheckCircle2, X } from "lucide-react";
interface SuccessModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
message: string;
duration?: number;
/**
* `error` change l'icône, l'accent et **désactive la fermeture automatique** :
* un message d'échec doit rester à l'écran le temps d'être lu, et il ne doit
* pas s'afficher sous une coche verte de succès.
*/
variant?: "success" | "error";
/** Action secondaire optionnelle (ex. « Recharger la page »). */
action?: { label: string; onClick: () => void };
}
/** Durée de l'animation de sortie, alignée sur les classes `animate-out`. */
const EXIT_MS = 200;
export function SuccessModal({
isOpen,
onClose,
title,
message,
duration = 3000,
variant = "success",
action,
}: SuccessModalProps) {
// Seul l'état de SORTIE est porté par un state : l'entrée est jouée par CSS
// au montage (`animate-in`), ce qui évite un setState synchrone dans l'effet.
const [closing, setClosing] = useState(false);
const isError = variant === "error";
const handleClose = useCallback(() => {
setClosing(true);
setTimeout(() => {
setClosing(false);
onClose();
}, EXIT_MS);
}, [onClose]);
useEffect(() => {
// Une erreur ne disparaît pas toute seule : l'utilisateur la ferme.
if (!isOpen || isError) return;
const timer = setTimeout(() => handleClose(), duration);
return () => clearTimeout(timer);
}, [isOpen, duration, isError, handleClose]);
if (!isOpen) return null;
const state = closing ? "closed" : "open";
const Icon = isError ? AlertCircle : CheckCircle2;
return (
<div
data-state={state}
className="fixed inset-0 z-[100] flex items-center justify-center p-4 backdrop-blur-sm duration-200 data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out"
>
{/* Backdrop */}
<div
className="fixed inset-0"
style={{ background: "rgba(0,0,0,0.45)" }}
onClick={handleClose}
/>
{/* Panel */}
<div
data-state={state}
className="relative w-full max-w-sm rounded-2xl overflow-hidden shadow-2xl duration-200 data-[state=open]:animate-in data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:zoom-out-95"
style={{ background: "var(--bg-surface)", border: "1px solid var(--border)" }}
>
{/* Accent bar */}
<div
className="absolute top-0 left-0 w-full h-0.5"
style={{
background: isError
? "var(--accent-error)"
: "linear-gradient(to right, var(--accent), var(--accent-success))",
}}
/>
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] text-[var(--text-muted)] transition-colors"
>
<X className="w-4 h-4" />
</button>
<div className="p-8 flex flex-col items-center text-center">
{/* Icon */}
<div
className="w-16 h-16 rounded-2xl flex items-center justify-center mb-5"
style={
isError
? { background: "var(--accent-error-bg)", border: "1px solid var(--accent-error)" }
: { background: "var(--accent-bg)", border: "1px solid var(--accent-border)" }
}
>
<Icon className="w-8 h-8" style={{ color: isError ? "var(--accent-error)" : "var(--accent)" }} />
</div>
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2 tracking-tight">
{title}
</h3>
<p className="text-sm text-[var(--text-secondary)] font-medium leading-relaxed">
{message}
</p>
{action && (
<button onClick={action.onClick} className="apple-btn-primary mt-7 w-full justify-center">
{action.label}
</button>
)}
<button
onClick={handleClose}
className={`${action ? "apple-btn-secondary mt-2" : "apple-btn-primary mt-7"} w-full justify-center`}
>
{action ? "Plus tard" : "Continuer"}
</button>
</div>
</div>
</div>
);
}
+12
View File
@@ -74,3 +74,15 @@ export function Terme({ id, children, className }: TermeProps) {
</Tooltip>
);
}
/** Texte suivi d'un « i » : explication libre au survol (hors glossaire). */
export function InfoBulle({ children, explication, className }: { children: ReactNode; explication: ReactNode; className?: string }) {
return (
<Tooltip content={explication}>
<span tabIndex={0} className={cn("inline-flex items-center gap-1 cursor-help outline-none", className)}>
{children}
<Info className="h-3.5 w-3.5 shrink-0 opacity-60" aria-hidden />
</span>
</Tooltip>
);
}