mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Merge pull request #24 from R0m1k3/claude/loving-fermi-qdh0qy
Tableaux plus lisibles : colonne extensible, lignes alternées, groupes par magasin, colonnes figées
This commit is contained in:
5 files changed
+240
-113
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useTransition, type ReactNode } from "react";
|
||||
import { useMemo, useTransition } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { DataTable, type DataColumn } from "@/components/ui/data-table";
|
||||
import { Segmented, type TabItem } from "@/components/ui/tabs";
|
||||
@@ -84,16 +84,6 @@ function Montant({ v }: { v: number }) {
|
||||
return <span className={cn("whitespace-nowrap", v === 0 && "text-[var(--text-muted)]")}>{fmtEur2(v)}</span>;
|
||||
}
|
||||
|
||||
/** En-tête sur deux lignes : magasin au-dessus, mesure en dessous. */
|
||||
function EnTete({ groupe, children }: { groupe: string; children: ReactNode }) {
|
||||
return (
|
||||
<span className="flex flex-col items-end leading-tight">
|
||||
<span className="text-xs font-normal text-[var(--text-muted)]">{groupe}</span>
|
||||
<span className="inline-flex items-center gap-1">{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnalyticsClient({
|
||||
mode,
|
||||
mois,
|
||||
@@ -126,42 +116,40 @@ export function AnalyticsClient({
|
||||
sortValue: (r) => r.label,
|
||||
cell: (r) => <span className="font-medium">{r.label}</span>,
|
||||
footer: () => "Total",
|
||||
className: "min-w-[220px]",
|
||||
className: "min-w-[140px] sm:min-w-[220px]",
|
||||
sticky: true,
|
||||
grow: true,
|
||||
},
|
||||
...GROUPES.flatMap((g): DataColumn<AnalyticsRow>[] => {
|
||||
const total = g.id === "total";
|
||||
const bord = "border-l border-[var(--border)]";
|
||||
// En-tête commun, séparation et teinte du total : gérés par DataTable.
|
||||
const commun = { group: g.titre, highlight: total, align: "right" as const };
|
||||
return [
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-ca`,
|
||||
header: <EnTete groupe={g.titre}>{libelleMois}</EnTete>,
|
||||
header: libelleMois,
|
||||
hint: `Chiffre d'affaires TTC (${g.titre}) en ${libelleMois}, retours déduits`,
|
||||
align: "right",
|
||||
sortValue: (r) => r[g.ca],
|
||||
cell: (r) => <Montant v={r[g.ca]} />,
|
||||
footer: (rows) => <Montant v={somme(rows, g.ca)} />,
|
||||
className: cn(bord, total && "font-semibold"),
|
||||
headerClassName: bord,
|
||||
className: cn(total && "font-semibold"),
|
||||
},
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-n1`,
|
||||
header: (
|
||||
<EnTete groupe={g.titre}>
|
||||
{libelleMoisN1} · <Terme id="n1" />
|
||||
</EnTete>
|
||||
),
|
||||
header: <span className="inline-flex items-center gap-1">{libelleMoisN1} · <Terme id="n1" /></span>,
|
||||
hint: `Chiffre d'affaires TTC (${g.titre}) en ${libelleMoisN1}, même mois l'année précédente`,
|
||||
align: "right",
|
||||
sortValue: (r) => r[g.caN1],
|
||||
cell: (r) => <Montant v={r[g.caN1]} />,
|
||||
footer: (rows) => <Montant v={somme(rows, g.caN1)} />,
|
||||
className: cn(total && "font-semibold"),
|
||||
},
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-evolution`,
|
||||
header: <EnTete groupe={g.titre}>Évolution</EnTete>,
|
||||
header: "Évolution",
|
||||
hint: `Évolution du chiffre d'affaires (${g.titre}) entre ${libelleMoisN1} et ${libelleMois}`,
|
||||
align: "right",
|
||||
sortValue: (r) => evolution(r[g.ca], r[g.caN1]),
|
||||
cell: (r) => <DeltaBadge pct={evolution(r[g.ca], r[g.caN1])} />,
|
||||
footer: (rows) => <DeltaBadge pct={evolution(somme(rows, g.ca), somme(rows, g.caN1))} />,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition, type ReactNode } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { DataTable, type DataColumn, type DataFilter } from "@/components/ui/data-table";
|
||||
import { Input, Label } from "@/components/ui/form-controls";
|
||||
@@ -68,56 +68,45 @@ function CelluleStock({ v }: { v: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** En-tête sur deux lignes : magasin au-dessus, mesure en dessous. */
|
||||
function EnTete({ groupe, children }: { groupe: string; children: ReactNode }) {
|
||||
return (
|
||||
<span className="flex flex-col items-end leading-tight">
|
||||
<span className="text-xs font-normal text-[var(--text-muted)]">{groupe}</span>
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function colonnesGroupe(g: Groupe): DataColumn<Ligne>[] {
|
||||
const total = g.id === "total";
|
||||
const bord = "border-l border-[var(--border)]";
|
||||
// En-tête commun, séparation et teinte du total : gérés par DataTable.
|
||||
const commun = { group: g.titre, highlight: total, align: "right" as const };
|
||||
return [
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-qte`,
|
||||
header: <EnTete groupe={g.titre}>Qté</EnTete>,
|
||||
header: "Qté",
|
||||
hint: `Quantité vendue (${g.nom}) sur la période, retours déduits`,
|
||||
align: "right",
|
||||
sortValue: r => r[g.qte],
|
||||
cell: r => (r[g.qte] !== 0 ? fmtEntier(r[g.qte]) : vide),
|
||||
footer: rows => fmtEntier(somme(rows, g.qte)),
|
||||
className: cn(bord, total && "font-semibold"),
|
||||
headerClassName: bord,
|
||||
className: cn(total && "font-semibold"),
|
||||
},
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-ca`,
|
||||
header: <EnTete groupe={g.titre}>CA TTC</EnTete>,
|
||||
header: "CA TTC",
|
||||
hint: `Chiffre d'affaires TTC (${g.nom}) sur la période, retours déduits`,
|
||||
align: "right",
|
||||
sortValue: r => r[g.ca],
|
||||
cell: r => (r[g.ca] !== 0 ? fmtEur2(r[g.ca]) : vide),
|
||||
footer: rows => fmtEur2(somme(rows, g.ca)),
|
||||
className: cn("whitespace-nowrap", total && "font-semibold"),
|
||||
className: cn(total && "font-semibold"),
|
||||
},
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-marge`,
|
||||
header: <EnTete groupe={g.titre}>% marge</EnTete>,
|
||||
header: "% marge",
|
||||
hint: `Taux de marge (${g.nom}) : marge divisée par le chiffre d'affaires TTC`,
|
||||
align: "right",
|
||||
sortValue: r => tauxMarge(r[g.ca], r[g.marge]),
|
||||
cell: r => <CelluleMarge taux={tauxMarge(r[g.ca], r[g.marge])} />,
|
||||
footer: rows => <CelluleMarge taux={tauxMarge(somme(rows, g.ca), somme(rows, g.marge))} />,
|
||||
className: "whitespace-nowrap",
|
||||
},
|
||||
{
|
||||
...commun,
|
||||
id: `${g.id}-stock`,
|
||||
header: <EnTete groupe={g.titre}>Stock</EnTete>,
|
||||
header: "Stock",
|
||||
hint: `Stock actuel (${g.nom}). En rouge : stock négatif`,
|
||||
align: "right",
|
||||
sortValue: r => r[g.stock],
|
||||
cell: r => <CelluleStock v={r[g.stock]} />,
|
||||
footer: rows => fmtEntier(somme(rows, g.stock)),
|
||||
@@ -133,6 +122,17 @@ const COLONNES: DataColumn<Ligne>[] = [
|
||||
cell: r => <span className="font-mono text-[13px] text-[var(--text-secondary)]">{r.codein}</span>,
|
||||
footer: () => "Total",
|
||||
className: "whitespace-nowrap",
|
||||
sticky: true,
|
||||
},
|
||||
{
|
||||
id: "libelle",
|
||||
header: "Désignation",
|
||||
sortValue: r => r.libelle.trim(),
|
||||
cell: r => <span className="font-medium">{r.libelle.trim()}</span>,
|
||||
footer: rows => <span className="whitespace-nowrap">{fmtEntier(rows.length)} articles</span>,
|
||||
className: "min-w-[140px] sm:min-w-[220px]",
|
||||
sticky: true,
|
||||
grow: true,
|
||||
},
|
||||
{
|
||||
id: "reference",
|
||||
@@ -142,14 +142,6 @@ const COLONNES: DataColumn<Ligne>[] = [
|
||||
cell: r => (r.reference ? <span className="font-mono text-[13px] text-[var(--text-secondary)]">{r.reference}</span> : vide),
|
||||
className: "whitespace-nowrap",
|
||||
},
|
||||
{
|
||||
id: "libelle",
|
||||
header: "Désignation",
|
||||
sortValue: r => r.libelle.trim(),
|
||||
cell: r => <span className="font-medium">{r.libelle.trim()}</span>,
|
||||
footer: rows => <span className="whitespace-nowrap">{fmtEntier(rows.length)} articles</span>,
|
||||
className: "min-w-[220px]",
|
||||
},
|
||||
{
|
||||
id: "fournisseur",
|
||||
header: "Fournisseur",
|
||||
@@ -212,16 +204,16 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
||||
feuille: "Meilleures ventes",
|
||||
fichier: `meilleures-ventes_${dateDebut}_${dateFin}`,
|
||||
entetes: [
|
||||
"Code", "Référence", "Désignation", "Fournisseur", "Nomenclature",
|
||||
"Code", "Désignation", "Référence", "Fournisseur", "Nomenclature",
|
||||
...GROUPES.flatMap(g => [
|
||||
`Qté ${nomExport(g)}`, `CA TTC ${nomExport(g)}`, `% Marge ${nomExport(g)}`, `Stock ${nomExport(g)}`,
|
||||
]),
|
||||
],
|
||||
largeurs: [12, 18, 40, 25, 30, ...GROUPES.flatMap(() => [10, 14, 12, 10])],
|
||||
largeurs: [12, 40, 18, 25, 30, ...GROUPES.flatMap(() => [10, 14, 12, 10])],
|
||||
lignes: rows.map(r => [
|
||||
r.codein,
|
||||
r.reference,
|
||||
r.libelle.trim(),
|
||||
r.reference,
|
||||
r.fournisseur,
|
||||
r.nomenclature_code ? `${r.nomenclature_code} — ${r.nomenclature}` : "",
|
||||
...GROUPES.flatMap(g => [r[g.qte], r[g.ca], pct(r[g.ca], r[g.marge]), r[g.stock]]),
|
||||
|
||||
@@ -76,7 +76,7 @@ function StatutBadge({ statut }: { statut?: string }) {
|
||||
/** Intitulé d'abord, code de l'opération en petit dessous. */
|
||||
function Operation({ intitule, code }: { intitule: string; code: string }) {
|
||||
return (
|
||||
<div className="min-w-[220px]">
|
||||
<div className="min-w-[140px] sm:min-w-[220px]">
|
||||
<div className="font-medium text-[var(--text-primary)]">{intitule || "Opération sans intitulé"}</div>
|
||||
{code && <div className="text-xs text-[var(--text-muted)]">Code {code}</div>}
|
||||
</div>
|
||||
@@ -171,6 +171,8 @@ export function PublicitesClient({
|
||||
header: "Opération",
|
||||
sortValue: (p) => p.intitule,
|
||||
cell: (p) => <Operation intitule={p.intitule} code={p.tcr_code} />,
|
||||
sticky: true,
|
||||
grow: true,
|
||||
footer: (rows) =>
|
||||
`Total${paginee ? " de la page" : ""} · ${fmtEntier(rows.length)} publicité${rows.length > 1 ? "s" : ""}`,
|
||||
},
|
||||
@@ -261,6 +263,8 @@ export function PublicitesClient({
|
||||
header: "Opération",
|
||||
sortValue: (h) => h.intitule,
|
||||
cell: (h) => <Operation intitule={h.intitule} code={h.tcr_code} />,
|
||||
sticky: true,
|
||||
grow: true,
|
||||
footer: (rows) => `Total · ${fmtEntier(rows.length)} opération${rows.length > 1 ? "s" : ""}`,
|
||||
},
|
||||
{
|
||||
|
||||
+32
-1
@@ -578,4 +578,35 @@
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
}
|
||||
/* ── Tableau de données commun (components/ui/data-table.tsx) ──
|
||||
Fonds opaques partout : les colonnes figées recouvrent les cellules qui
|
||||
défilent dessous. La ligne porte la couleur, les cellules en héritent. */
|
||||
.dt-table {
|
||||
/* Une ligne sur deux, plus soutenue : suivre une ligne sur un écran large. */
|
||||
--dt-zebra: color-mix(in srgb, var(--text-primary) 3.5%, var(--bg-surface));
|
||||
/* Survol teinté de la couleur d'accent : la ligne pointée se distingue de l'alternance. */
|
||||
--dt-hover: color-mix(in srgb, var(--accent) 10%, var(--bg-surface));
|
||||
}
|
||||
.dt-row {
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
.dt-row:nth-child(even) {
|
||||
background-color: var(--dt-zebra);
|
||||
}
|
||||
.dt-row:hover {
|
||||
background-color: var(--dt-hover);
|
||||
}
|
||||
.dt-sticky {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
background-color: inherit;
|
||||
}
|
||||
/* Ombre portée sur la dernière colonne figée : on voit ce qui passe dessous. */
|
||||
.dt-sticky-last {
|
||||
box-shadow: inset -1px 0 0 var(--border-strong), 4px 0 6px -4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
/* Teinte superposée au fond de la ligne (alternance et survol conservés). */
|
||||
.dt-highlight {
|
||||
background-image: linear-gradient(var(--accent-bg), var(--accent-bg));
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* en réimplémentaient chacune une partie (13 tableaux faits à la main).
|
||||
*/
|
||||
|
||||
import { useDeferredValue, useMemo, useState, type ReactNode } from "react";
|
||||
import { useDeferredValue, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { ChevronDown, ChevronUp, ChevronsUpDown, Download, Loader2, RotateCcw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SearchInput, Select } from "@/components/ui/form-controls";
|
||||
@@ -33,6 +33,25 @@ export interface DataColumn<T> {
|
||||
footer?: (rows: readonly T[]) => ReactNode;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
/**
|
||||
* La colonne prend toute la largeur restante (désignation, libellé…) : les
|
||||
* colonnes de chiffres gardent leur largeur naturelle au lieu de s'étirer
|
||||
* sur un écran large, loin de leur libellé.
|
||||
*/
|
||||
grow?: boolean;
|
||||
/**
|
||||
* Colonne figée à gauche pendant le défilement horizontal. Seules les
|
||||
* premières colonnes consécutives peuvent l'être.
|
||||
*/
|
||||
sticky?: boolean;
|
||||
/**
|
||||
* Groupe de colonnes (un magasin, « Total »…) : un en-tête commun est
|
||||
* affiché au-dessus des colonnes consécutives du même groupe, et un trait
|
||||
* vertical sépare les groupes.
|
||||
*/
|
||||
group?: string;
|
||||
/** Fond légèrement teinté, pour faire ressortir un groupe (le total…). */
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export interface DataFilter<T> {
|
||||
@@ -198,6 +217,126 @@ export function DataTable<T>({
|
||||
const alignement = (a?: "left" | "right" | "center") =>
|
||||
a === "right" ? "text-right justify-end" : a === "center" ? "text-center justify-center" : "text-left";
|
||||
|
||||
// Colonnes figées : seulement les premières, consécutives.
|
||||
let nbFigees = 0;
|
||||
while (nbFigees < columns.length && columns[nbFigees].sticky) nbFigees++;
|
||||
|
||||
// Début de groupe : trait vertical à gauche de la colonne.
|
||||
const debutGroupe = (i: number) => Boolean(columns[i].group) && columns[i - 1]?.group !== columns[i].group;
|
||||
const avecGroupes = columns.some((c) => c.group);
|
||||
|
||||
// Décalage gauche de chaque colonne figée = largeur cumulée des précédentes,
|
||||
// mesurée sur l'en-tête et exposée en variables CSS sur le tableau.
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const [decalages, setDecalages] = useState<number[]>([]);
|
||||
useLayoutEffect(() => {
|
||||
const table = tableRef.current;
|
||||
if (!table || nbFigees === 0) return;
|
||||
const mesurer = () => {
|
||||
const out: number[] = [];
|
||||
let gauche = 0;
|
||||
for (let i = 0; i < nbFigees; i++) {
|
||||
out.push(gauche);
|
||||
const th = table.querySelector<HTMLElement>(`thead th[data-col="${i}"]`);
|
||||
gauche += th?.getBoundingClientRect().width ?? 0;
|
||||
}
|
||||
setDecalages((prev) => (prev.length === out.length && prev.every((v, i) => v === out[i]) ? prev : out));
|
||||
};
|
||||
mesurer();
|
||||
const obs = new ResizeObserver(mesurer);
|
||||
obs.observe(table);
|
||||
return () => obs.disconnect();
|
||||
}, [nbFigees, total, columns]);
|
||||
|
||||
const styleFigee = (i: number): CSSProperties | undefined =>
|
||||
i < nbFigees ? { left: decalages[i] ?? 0 } : undefined;
|
||||
|
||||
/** Classes communes à toutes les cellules d'une colonne (en-tête, corps, totaux). */
|
||||
const classesColonne = (c: DataColumn<T>, i: number) => cn(
|
||||
i < nbFigees && "dt-sticky",
|
||||
i === nbFigees - 1 && "dt-sticky-last",
|
||||
c.highlight && "dt-highlight",
|
||||
debutGroupe(i) && "border-l border-[var(--border-strong)]",
|
||||
c.grow && "w-full",
|
||||
// Désignation figée : bornée sur mobile pour laisser voir les chiffres.
|
||||
c.grow && i < nbFigees && "max-w-[45vw] md:max-w-none",
|
||||
);
|
||||
|
||||
const enTete = (c: DataColumn<T>, i: number, rowSpan?: number) => {
|
||||
const triable = Boolean(c.sortValue);
|
||||
const actif = tri?.id === c.id;
|
||||
const contenu = (
|
||||
<span className={cn("inline-flex items-center gap-1", alignement(c.align))}>
|
||||
{c.header}
|
||||
{triable && (
|
||||
actif
|
||||
? (tri!.dir === "asc" ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />)
|
||||
: <ChevronsUpDown className="h-3.5 w-3.5 opacity-40" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<th
|
||||
key={c.id}
|
||||
scope="col"
|
||||
data-col={i}
|
||||
rowSpan={rowSpan}
|
||||
aria-sort={actif ? (tri!.dir === "asc" ? "ascending" : "descending") : undefined}
|
||||
style={styleFigee(i)}
|
||||
className={cn(
|
||||
"whitespace-nowrap border-b border-[var(--border-strong)] px-3 py-2.5 text-[13px] font-semibold text-[var(--text-secondary)] align-bottom",
|
||||
alignement(c.align),
|
||||
classesColonne(c, i),
|
||||
c.headerClassName,
|
||||
)}
|
||||
>
|
||||
{triable ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => basculerTri(c.id)}
|
||||
className="hover:text-[var(--text-primary)]"
|
||||
title={typeof c.hint === "string" ? c.hint : undefined}
|
||||
>
|
||||
{contenu}
|
||||
</button>
|
||||
) : c.hint ? (
|
||||
<Tooltip content={c.hint}><span className="cursor-help">{contenu}</span></Tooltip>
|
||||
) : contenu}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
// Première ligne d'en-tête quand il y a des groupes : un titre par groupe,
|
||||
// et les colonnes sans groupe sur deux lignes.
|
||||
const ligneGroupes: ReactNode[] = [];
|
||||
if (avecGroupes) {
|
||||
for (let i = 0; i < columns.length;) {
|
||||
const c = columns[i];
|
||||
if (!c.group) {
|
||||
ligneGroupes.push(enTete(c, i, 2));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
let j = i;
|
||||
while (j < columns.length && columns[j].group === c.group) j++;
|
||||
ligneGroupes.push(
|
||||
<th
|
||||
key={`groupe-${i}`}
|
||||
scope="colgroup"
|
||||
colSpan={j - i}
|
||||
className={cn(
|
||||
"whitespace-nowrap border-b border-l border-[var(--border)] px-3 pt-2 pb-1 text-center text-[13px] font-semibold text-[var(--text-primary)]",
|
||||
"border-l-[var(--border-strong)]",
|
||||
columns.slice(i, j).every((x) => x.highlight) && "dt-highlight",
|
||||
)}
|
||||
>
|
||||
{c.group}
|
||||
</th>,
|
||||
);
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
{/* Barre d'outils */}
|
||||
@@ -252,49 +391,18 @@ export function DataTable<T>({
|
||||
className="overflow-auto rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-[var(--shadow-sm)]"
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-[var(--bg-elevated)]">
|
||||
<tr>
|
||||
{columns.map((c) => {
|
||||
const triable = Boolean(c.sortValue);
|
||||
const actif = tri?.id === c.id;
|
||||
const contenu = (
|
||||
<span className={cn("inline-flex items-center gap-1", alignement(c.align))}>
|
||||
{c.header}
|
||||
{triable && (
|
||||
actif
|
||||
? (tri!.dir === "asc" ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />)
|
||||
: <ChevronsUpDown className="h-3.5 w-3.5 opacity-40" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<th
|
||||
key={c.id}
|
||||
scope="col"
|
||||
aria-sort={actif ? (tri!.dir === "asc" ? "ascending" : "descending") : undefined}
|
||||
className={cn(
|
||||
"whitespace-nowrap border-b border-[var(--border-strong)] px-3 py-2.5 text-[13px] font-semibold text-[var(--text-secondary)]",
|
||||
alignement(c.align),
|
||||
c.headerClassName,
|
||||
)}
|
||||
>
|
||||
{triable ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => basculerTri(c.id)}
|
||||
className="hover:text-[var(--text-primary)]"
|
||||
title={typeof c.hint === "string" ? c.hint : undefined}
|
||||
>
|
||||
{contenu}
|
||||
</button>
|
||||
) : c.hint ? (
|
||||
<Tooltip content={c.hint}><span className="cursor-help">{contenu}</span></Tooltip>
|
||||
) : contenu}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
<table ref={tableRef} className="dt-table min-w-full text-sm">
|
||||
<thead className="sticky top-0 z-10">
|
||||
{avecGroupes ? (
|
||||
<>
|
||||
<tr className="bg-[var(--bg-elevated)]">{ligneGroupes}</tr>
|
||||
<tr className="bg-[var(--bg-elevated)]">
|
||||
{columns.map((c, i) => (c.group ? enTete(c, i) : null))}
|
||||
</tr>
|
||||
</>
|
||||
) : (
|
||||
<tr className="bg-[var(--bg-elevated)]">{columns.map((c, i) => enTete(c, i))}</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{lignesAffichees.map((r, i) => (
|
||||
@@ -302,17 +410,19 @@ export function DataTable<T>({
|
||||
key={rowKey(r, i)}
|
||||
onClick={onRowClick ? () => onRowClick(r) : undefined}
|
||||
className={cn(
|
||||
"border-b border-[var(--border)] last:border-b-0 hover:bg-[var(--bg-elevated)]",
|
||||
"dt-row border-b border-[var(--border)] last:border-b-0",
|
||||
onRowClick && "cursor-pointer",
|
||||
)}
|
||||
>
|
||||
{columns.map((c) => (
|
||||
{columns.map((c, ci) => (
|
||||
<td
|
||||
key={c.id}
|
||||
style={styleFigee(ci)}
|
||||
className={cn(
|
||||
"px-3 py-2 text-[var(--text-primary)]",
|
||||
c.align === "right" && "text-right tabular-nums",
|
||||
c.align === "right" && "text-right tabular-nums whitespace-nowrap",
|
||||
c.align === "center" && "text-center",
|
||||
classesColonne(c, ci),
|
||||
c.className,
|
||||
)}
|
||||
>
|
||||
@@ -323,15 +433,17 @@ export function DataTable<T>({
|
||||
))}
|
||||
</tbody>
|
||||
{showFooter && (
|
||||
<tfoot className="sticky bottom-0 bg-[var(--bg-elevated)] font-semibold">
|
||||
<tr>
|
||||
{columns.map((c) => (
|
||||
<tfoot className="sticky bottom-0 z-10 font-semibold">
|
||||
<tr className="bg-[var(--bg-elevated)]">
|
||||
{columns.map((c, ci) => (
|
||||
<td
|
||||
key={c.id}
|
||||
style={styleFigee(ci)}
|
||||
className={cn(
|
||||
"border-t border-[var(--border-strong)] px-3 py-2.5 text-[var(--text-primary)]",
|
||||
c.align === "right" && "text-right tabular-nums",
|
||||
c.align === "right" && "text-right tabular-nums whitespace-nowrap",
|
||||
c.align === "center" && "text-center",
|
||||
classesColonne(c, ci),
|
||||
)}
|
||||
>
|
||||
{c.footer ? c.footer(triees) : null}
|
||||
|
||||
Reference in new issue
Block a user