mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-18 20:56:43 +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,271 +1,267 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
FileDown,
|
||||
History,
|
||||
Loader2,
|
||||
PencilLine,
|
||||
Save,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { getSnapshots } from "../api/get-snapshots";
|
||||
import { deleteSnapshot } from "../api/delete-snapshot";
|
||||
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Trash2,
|
||||
UploadCloud,
|
||||
Calendar,
|
||||
Store,
|
||||
History,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { SuccessModal } from "@/components/shared/success-modal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StoreBadge } from "@/components/ui/badge";
|
||||
import { EmptyState, ErrorState, Skeleton } from "@/components/ui/states";
|
||||
import { confirmer, toast } from "@/components/ui/feedback";
|
||||
import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format";
|
||||
import { couleurMarge } from "@/lib/marge";
|
||||
import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
|
||||
import { ConfirmModal } from "@/components/shared/confirm-modal";
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}).format(date);
|
||||
};
|
||||
type Snapshot = Awaited<ReturnType<typeof getSnapshots>>[number];
|
||||
/** Modifications enregistrées : codein → gamme avant / après. */
|
||||
type Changements = Record<string, { before?: string | null; after: string }>;
|
||||
interface Resume { totalCa?: number; tauxMargeGlobal?: number }
|
||||
|
||||
const fmtDateLongue = new Intl.DateTimeFormat("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
function changementsDe(s: Snapshot): Changements {
|
||||
return (s.changes ?? {}) as Changements;
|
||||
}
|
||||
|
||||
function resumeDe(s: Snapshot): Resume | null {
|
||||
return (s.summaryJson as Resume | null) ?? null;
|
||||
}
|
||||
|
||||
function titreDe(s: Snapshot): string {
|
||||
return s.label ?? s.nomFournisseur ?? s.codeFournisseur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onglet resté ouvert pendant un déploiement : les identifiants de Server
|
||||
* Action ont changé côté serveur. On propose le rechargement au lieu
|
||||
* d'afficher une erreur technique incompréhensible.
|
||||
*/
|
||||
async function proposerRechargement() {
|
||||
const ok = await confirmer({
|
||||
titre: "Page à recharger",
|
||||
message: STALE_ACTION_MESSAGE,
|
||||
libelleConfirmer: "Recharger la page",
|
||||
});
|
||||
if (ok) window.location.reload();
|
||||
}
|
||||
|
||||
interface SnapshotListProps {
|
||||
type?: "snapshot" | "export";
|
||||
}
|
||||
|
||||
export function SnapshotList({ type }: SnapshotListProps) {
|
||||
const [snapshots, setSnapshots] = useState<any[]>([]);
|
||||
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<{ perime: boolean; detail: string } | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const [modal, setModal] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
variant?: "success" | "error";
|
||||
action?: { label: string; onClick: () => void };
|
||||
}>({
|
||||
isOpen: false,
|
||||
title: "",
|
||||
message: ""
|
||||
});
|
||||
|
||||
/**
|
||||
* Onglet resté ouvert pendant un déploiement : les identifiants de Server
|
||||
* Action ont changé côté serveur. On propose le rechargement au lieu
|
||||
* d'afficher une erreur technique incompréhensible.
|
||||
*/
|
||||
const modalErreur = (err: unknown, titre: string, message: string) =>
|
||||
isStaleServerActionError(err)
|
||||
? {
|
||||
isOpen: true,
|
||||
title: "Page à recharger",
|
||||
message: STALE_ACTION_MESSAGE,
|
||||
variant: "error" as const,
|
||||
action: { label: "Recharger la page", onClick: () => window.location.reload() },
|
||||
}
|
||||
: { isOpen: true, title: titre, message, variant: "error" as const };
|
||||
|
||||
const [confirmModal, setConfirmModal] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
variant?: "danger" | "default";
|
||||
onConfirm: () => void;
|
||||
}>({ isOpen: false, title: "", message: "", onConfirm: () => { } });
|
||||
|
||||
const restoreSnapshot = useGridStore(state => state.restoreSnapshot);
|
||||
const restoreSnapshot = useGridStore((state) => state.restoreSnapshot);
|
||||
const router = useRouter();
|
||||
|
||||
const fetchSnapshots = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const estExport = type === "export";
|
||||
|
||||
// Aucun setState avant le premier await : appelable depuis l'effet de montage.
|
||||
const fetchSnapshots = useCallback(async () => {
|
||||
try {
|
||||
const data = await getSnapshots(type);
|
||||
setSnapshots(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(isStaleServerActionError(err) ? STALE_ACTION_MESSAGE : "Impossible de charger les données.");
|
||||
setError({
|
||||
perime: isStaleServerActionError(err),
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSnapshots();
|
||||
}, [type]);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: "Supprimer",
|
||||
message: `Supprimer cet ${type === 'export' ? 'export' : 'snapshot'} définitivement ?`,
|
||||
variant: "danger",
|
||||
onConfirm: async () => {
|
||||
setIsDeleting(id);
|
||||
try {
|
||||
const res = await deleteSnapshot(id);
|
||||
if (res.success) {
|
||||
setSnapshots(prev => prev.filter(s => s.id !== id));
|
||||
setModal({ isOpen: true, title: "Action Réussie", message: "L'élément a été supprimé de votre historique." });
|
||||
} else {
|
||||
setModal({ isOpen: true, title: "Erreur", message: "Impossible de supprimer l'élément.", variant: "error" });
|
||||
}
|
||||
} catch (err) {
|
||||
setModal(modalErreur(err, "Erreur Technique", "Une erreur est survenue lors de la suppression."));
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
useEffect(() => { void fetchSnapshots(); }, [fetchSnapshots]);
|
||||
|
||||
const reessayer = () => {
|
||||
setLoading(true);
|
||||
void fetchSnapshots();
|
||||
};
|
||||
|
||||
const handleLoad = (snapshot: any) => {
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: "Restaurer la session",
|
||||
message: `Charger la session "${snapshot.label}" ? Cela remplacera vos brouillons actuels.`,
|
||||
variant: "default",
|
||||
onConfirm: () => {
|
||||
const changes: Record<string, string> = {};
|
||||
Object.entries(snapshot.changes as any).forEach(([codein, delta]: [string, any]) => {
|
||||
changes[codein] = delta.after;
|
||||
});
|
||||
restoreSnapshot(changes);
|
||||
const query = new URLSearchParams();
|
||||
if (snapshot.codeFournisseur) query.set("fournisseur", snapshot.codeFournisseur);
|
||||
if (snapshot.magasin) query.set("magasin", snapshot.magasin);
|
||||
router.push(`/grid?${query.toString()}`);
|
||||
},
|
||||
const handleDelete = async (s: Snapshot) => {
|
||||
const ok = await confirmer({
|
||||
titre: estExport ? "Retirer cet export de l'historique ?" : "Supprimer cette session ?",
|
||||
message: estExport
|
||||
? `« ${titreDe(s)} » sera retiré définitivement de votre historique. Le fichier Excel déjà téléchargé n'est pas concerné.`
|
||||
: `« ${titreDe(s)} » sera supprimée définitivement : vous ne pourrez plus la reprendre dans la Grille.`,
|
||||
libelleConfirmer: "Supprimer",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
setIsDeleting(s.id);
|
||||
try {
|
||||
const res = await deleteSnapshot(s.id);
|
||||
if (res.success) {
|
||||
setSnapshots((prev) => prev.filter((x) => x.id !== s.id));
|
||||
toast.succes(estExport ? "L'export a été retiré de votre historique." : "La session a été supprimée de votre historique.");
|
||||
} else {
|
||||
toast.erreur(`La suppression a échoué : ${res.error ?? "erreur inconnue"}.`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isStaleServerActionError(err)) void proposerRechargement();
|
||||
else toast.erreur("Une erreur est survenue pendant la suppression. Réessayez dans un instant.");
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoad = async (s: Snapshot) => {
|
||||
const ok = await confirmer({
|
||||
titre: "Reprendre dans la Grille ?",
|
||||
message: `Vos modifications en cours dans la Grille seront remplacées par celles de ${estExport ? "cet export" : "cette session"}.`,
|
||||
libelleConfirmer: "Reprendre dans la Grille",
|
||||
});
|
||||
if (!ok) return;
|
||||
const changes: Record<string, string> = {};
|
||||
Object.entries(changementsDe(s)).forEach(([codein, delta]) => {
|
||||
changes[codein] = delta.after;
|
||||
});
|
||||
restoreSnapshot(changes);
|
||||
const query = new URLSearchParams();
|
||||
if (s.codeFournisseur) query.set("fournisseur", s.codeFournisseur);
|
||||
if (s.magasin) query.set("magasin", s.magasin);
|
||||
router.push(`/grid?${query.toString()}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-20 space-y-4">
|
||||
<Loader2 className="w-8 h-8 text-[var(--accent)] animate-spin" />
|
||||
<p className="text-[var(--text-muted)] text-sm">Récupération de l'historique...</p>
|
||||
<div className="space-y-3" aria-busy="true" aria-label="Chargement de l'historique">
|
||||
{Array.from({ length: 4 }, (_, i) => <Skeleton key={i} className="h-24 w-full rounded-xl" />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && snapshots.length === 0) {
|
||||
return (
|
||||
<div className="border border-[var(--accent-error)] rounded-2xl p-10 text-center bg-[var(--accent-error-bg)]">
|
||||
<AlertCircle className="w-8 h-8 mx-auto mb-3 text-[var(--accent-error)]" />
|
||||
<h3 className="text-[var(--text-primary)] font-bold mb-1">L'historique n'a pas pu être chargé</h3>
|
||||
<p className="text-[var(--text-secondary)] text-sm mb-4">{error}</p>
|
||||
<button onClick={fetchSnapshots} className="btn-action btn-action-secondary">
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
return error.perime ? (
|
||||
<ErrorState
|
||||
title="Page à recharger"
|
||||
description={STALE_ACTION_MESSAGE}
|
||||
action={<Button onClick={() => window.location.reload()}>Recharger la page</Button>}
|
||||
/>
|
||||
) : (
|
||||
<ErrorState
|
||||
title="L'historique n'a pas pu être chargé"
|
||||
detail={error.detail}
|
||||
action={<Button variant="outline" onClick={reessayer}>Réessayer</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshots.length === 0) {
|
||||
return (
|
||||
<div className="border border-dashed border-[var(--border-strong)] rounded-2xl p-16 text-center bg-[var(--bg-elevated)]/30">
|
||||
<div className="w-16 h-16 rounded-full bg-[var(--bg-elevated)] flex items-center justify-center mx-auto mb-4 border border-[var(--border)]">
|
||||
<History className="w-8 h-8 text-[var(--text-muted)]" />
|
||||
</div>
|
||||
<h3 className="text-[var(--text-primary)] font-bold mb-1">
|
||||
{type === "export" ? "Aucun export pour le moment" : "Aucun snapshot pour le moment"}
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] text-sm max-w-xs mx-auto">
|
||||
{type === "export"
|
||||
? "Vos exports Excel apparaîtront ici pour historique."
|
||||
: "Capturez l'état de votre travail depuis la grille pour le retrouver ici ultérieurement."}
|
||||
</p>
|
||||
</div>
|
||||
return estExport ? (
|
||||
<EmptyState
|
||||
icon={FileDown}
|
||||
title="Aucun export"
|
||||
description="Dans la Grille, chaque export des gammes modifiées (menu « Exporter », « Gammes modifiées seulement ») est noté ici, pour retrouver ce qui a été envoyé."
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={History}
|
||||
title="Aucune session enregistrée"
|
||||
description="Dans la Grille, ouvrez le menu « Exporter » puis choisissez « Garder une copie dans l'Historique » : vos modifications de gammes seront enregistrées ici et vous pourrez les reprendre plus tard."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{snapshots.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="group relative flex items-center gap-6 p-5 rounded-xl border transition-all hover:shadow-lg bg-[var(--bg-surface)] border-[var(--border)] hover:border-[var(--accent-border)]"
|
||||
>
|
||||
<div className="w-11 h-11 rounded-xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center shrink-0">
|
||||
<Store className="w-5 h-5 text-[var(--accent)]" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-sm font-bold text-[var(--text-primary)] truncate">{s.label}</h3>
|
||||
<span className="px-2 py-0.5 rounded-full bg-[var(--accent-bg)] border border-[var(--accent-border)] text-[10px] font-black uppercase text-[var(--accent)] tracking-wider">
|
||||
{s.magasin || "Magasin Inconnu"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)] font-medium">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="w-3.5 h-3.5 opacity-60" />
|
||||
{formatDate(new Date(s.createdAt))}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<UploadCloud className="w-3.5 h-3.5 opacity-60" />
|
||||
{Object.keys(s.changes as any).length} modifications enregistrées
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{s.summaryJson && (
|
||||
<div className="hidden xl:flex items-center gap-8 border-l border-[var(--border)] pl-8 mr-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[9px] uppercase font-black text-[var(--text-muted)] tracking-wider">CA</span>
|
||||
<span className="text-xs font-bold text-[var(--accent-success)]">
|
||||
{(s.summaryJson.totalCa || 0).toLocaleString("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
<ul className="grid gap-3">
|
||||
{snapshots.map((s) => {
|
||||
const nbModifs = Object.keys(changementsDe(s)).length;
|
||||
const resume = resumeDe(s);
|
||||
const taux = resume?.tauxMargeGlobal;
|
||||
const IconeType = estExport ? FileDown : Save;
|
||||
return (
|
||||
<li
|
||||
key={s.id}
|
||||
className="flex flex-wrap items-center gap-x-6 gap-y-3 rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] p-4 shadow-[var(--shadow-sm)] transition-colors hover:border-[var(--accent-border)]"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 basis-72 items-start gap-4">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[var(--accent-border)] bg-[var(--accent-bg)]">
|
||||
<IconeType className="h-5 w-5 text-[var(--accent)]" aria-hidden />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[9px] uppercase font-black text-[var(--text-muted)] tracking-wider">Marge</span>
|
||||
<span className="text-xs font-bold text-[var(--text-secondary)]">
|
||||
{s.summaryJson.tauxMargeGlobal?.toFixed(1)}%
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="truncate text-sm font-semibold text-[var(--text-primary)]">
|
||||
{titreDe(s)}
|
||||
</h3>
|
||||
<StoreBadge code={s.magasin} />
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px] text-[var(--text-secondary)]">
|
||||
{s.createdAt && (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Calendar className="h-4 w-4 text-[var(--text-muted)]" aria-hidden />
|
||||
{fmtDateLongue.format(new Date(s.createdAt))}
|
||||
</span>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<PencilLine className="h-4 w-4 text-[var(--text-muted)]" aria-hidden />
|
||||
{fmtEntier(nbModifs)} changement{nbModifs > 1 ? "s" : ""} de gamme
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleLoad(s)}
|
||||
className="apple-btn-primary flex items-center gap-2"
|
||||
>
|
||||
Restaurer
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
{resume && (
|
||||
<dl className="flex items-center gap-6 md:border-l md:border-[var(--border)] md:pl-6">
|
||||
<div>
|
||||
<dt className="text-xs font-medium text-[var(--text-muted)]">Chiffre d'affaires</dt>
|
||||
<dd className="text-sm font-semibold tabular-nums text-[var(--text-primary)]">
|
||||
{fmtEur0(resume.totalCa ?? 0)}
|
||||
</dd>
|
||||
</div>
|
||||
{taux != null && Number.isFinite(taux) && (
|
||||
<div>
|
||||
<dt className="text-xs font-medium text-[var(--text-muted)]">Taux de marge</dt>
|
||||
<dd className="text-sm font-semibold tabular-nums" style={{ color: couleurMarge(taux) }}>
|
||||
{fmtDecimal1(taux)} %
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(s.id)}
|
||||
disabled={isDeleting === s.id}
|
||||
className="p-2.5 rounded-xl border border-[var(--border)] text-[var(--text-muted)] hover:text-[var(--accent-error)] hover:bg-[var(--accent-error-bg)] hover:border-[var(--accent-error)] transition-all active:scale-95"
|
||||
title="Supprimer"
|
||||
>
|
||||
{isDeleting === s.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<SuccessModal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
|
||||
title={modal.title}
|
||||
message={modal.message}
|
||||
variant={modal.variant}
|
||||
action={modal.action}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={confirmModal.isOpen}
|
||||
onClose={() => setConfirmModal(prev => ({ ...prev, isOpen: false }))}
|
||||
onConfirm={confirmModal.onConfirm}
|
||||
title={confirmModal.title}
|
||||
message={confirmModal.message}
|
||||
variant={confirmModal.variant}
|
||||
confirmLabel={confirmModal.variant === "danger" ? "Supprimer" : "Confirmer"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => handleLoad(s)}>
|
||||
Reprendre dans la Grille
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(s)}
|
||||
disabled={isDeleting === s.id}
|
||||
aria-label={estExport ? "Supprimer cet export" : "Supprimer cette session"}
|
||||
title="Supprimer"
|
||||
className="text-[var(--text-secondary)] hover:border-[var(--accent-error)] hover:bg-[var(--accent-error-bg)] hover:text-[var(--accent-error)]"
|
||||
>
|
||||
{isDeleting === s.id ? <Loader2 className="animate-spin" /> : <Trash2 />}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user