mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
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:
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user