mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-20 21:48:33 +02:00
feat: implement interactive stock negative and no-sales data table with filtering, sorting, and Excel export functionality
This commit is contained in:
1 parent
5a13640884
commit
ff81bc1fc4
1 file changed
+106
-13
@@ -14,6 +14,13 @@ function fmtDate(raw: string | null) {
|
|||||||
return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getYear(raw: string | null): string {
|
||||||
|
if (!raw) return "";
|
||||||
|
const d = new Date(raw);
|
||||||
|
if (isNaN(d.getTime())) return "";
|
||||||
|
return String(d.getFullYear());
|
||||||
|
}
|
||||||
|
|
||||||
function SortIcon<T extends string>({ col, sortKey, sortDir }: { col: T; sortKey: T; sortDir: SortDir }) {
|
function SortIcon<T extends string>({ col, sortKey, sortDir }: { col: T; sortKey: T; sortDir: SortDir }) {
|
||||||
if (col !== sortKey) return <ChevronsUpDown className="inline w-3 h-3 ml-1 opacity-30" />;
|
if (col !== sortKey) return <ChevronsUpDown className="inline w-3 h-3 ml-1 opacity-30" />;
|
||||||
return sortDir === "asc"
|
return sortDir === "asc"
|
||||||
@@ -21,6 +28,27 @@ function SortIcon<T extends string>({ col, sortKey, sortDir }: { col: T; sortKey
|
|||||||
: <ChevronDown className="inline w-3 h-3 ml-1" />;
|
: <ChevronDown className="inline w-3 h-3 ml-1" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterSelect({ label, value, onChange, options }: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
options: string[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium text-gray-600 whitespace-nowrap">{label}</label>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={e => onChange(e.target.value)}
|
||||||
|
className="rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-[150px]"
|
||||||
|
>
|
||||||
|
<option value="">Tous</option>
|
||||||
|
{options.map(o => <option key={o} value={o}>{o}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useSortedRows<T extends Record<string, unknown>>(rows: T[], numericKeys: string[]) {
|
function useSortedRows<T extends Record<string, unknown>>(rows: T[], numericKeys: string[]) {
|
||||||
const [sortKey, setSortKey] = useState<keyof T>(Object.keys(rows[0] ?? { codein: "" })[0] as keyof T);
|
const [sortKey, setSortKey] = useState<keyof T>(Object.keys(rows[0] ?? { codein: "" })[0] as keyof T);
|
||||||
const [sortDir, setSortDir] = useState<SortDir>("asc");
|
const [sortDir, setSortDir] = useState<SortDir>("asc");
|
||||||
@@ -50,8 +78,26 @@ function useSortedRows<T extends Record<string, unknown>>(rows: T[], numericKeys
|
|||||||
|
|
||||||
function TabStockNegatif({ rows, magasin }: { rows: PgStockNegatifRow[]; magasin: string }) {
|
function TabStockNegatif({ rows, magasin }: { rows: PgStockNegatifRow[]; magasin: string }) {
|
||||||
type SK = keyof PgStockNegatifRow;
|
type SK = keyof PgStockNegatifRow;
|
||||||
|
|
||||||
|
const [filterFournisseur, setFilterFournisseur] = useState("");
|
||||||
|
const [filterAnnee, setFilterAnnee] = useState("");
|
||||||
|
|
||||||
|
const fournisseurs = useMemo(() =>
|
||||||
|
[...new Set(rows.map(r => r.fournisseur).filter(Boolean))].sort((a, b) => a.localeCompare(b, "fr")),
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
const annees = useMemo(() =>
|
||||||
|
[...new Set(rows.map(r => getYear(r.derniereentree)).filter(Boolean))].sort((a, b) => b.localeCompare(a)),
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => rows.filter(r =>
|
||||||
|
(!filterFournisseur || r.fournisseur === filterFournisseur) &&
|
||||||
|
(!filterAnnee || getYear(r.derniereentree) === filterAnnee)
|
||||||
|
), [rows, filterFournisseur, filterAnnee]);
|
||||||
|
|
||||||
const { sorted, sortKey, sortDir, handleSort } = useSortedRows<PgStockNegatifRow>(
|
const { sorted, sortKey, sortDir, handleSort } = useSortedRows<PgStockNegatifRow>(
|
||||||
rows.length ? rows : [] as PgStockNegatifRow[],
|
filtered,
|
||||||
["stockdispo"]
|
["stockdispo"]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,11 +133,25 @@ function TabStockNegatif({ rows, magasin }: { rows: PgStockNegatifRow[]; magasin
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-end items-center gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<span className="text-sm text-gray-500">{sorted.length} article{sorted.length !== 1 ? "s" : ""}</span>
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<button onClick={handleExport} className="flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors">
|
<FilterSelect label="Fournisseur" value={filterFournisseur} onChange={setFilterFournisseur} options={fournisseurs} />
|
||||||
<Download className="w-4 h-4" />Exporter Excel
|
<FilterSelect label="Année entrée" value={filterAnnee} onChange={setFilterAnnee} options={annees} />
|
||||||
</button>
|
{(filterFournisseur || filterAnnee) && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setFilterFournisseur(""); setFilterAnnee(""); }}
|
||||||
|
className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-1.5 text-xs font-medium text-orange-700 hover:bg-orange-100 transition-colors"
|
||||||
|
>
|
||||||
|
✕ Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-500">{sorted.length} article{sorted.length !== 1 ? "s" : ""}</span>
|
||||||
|
<button onClick={handleExport} className="flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors">
|
||||||
|
<Download className="w-4 h-4" />Exporter Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto rounded-xl border border-gray-200 bg-white shadow-sm">
|
<div className="overflow-x-auto rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
@@ -136,9 +196,28 @@ function TabStockNegatif({ rows, magasin }: { rows: PgStockNegatifRow[]; magasin
|
|||||||
|
|
||||||
function TabEntreesSansVente({ rows, magasin }: { rows: PgStockSansVenteRow[]; magasin: string }) {
|
function TabEntreesSansVente({ rows, magasin }: { rows: PgStockSansVenteRow[]; magasin: string }) {
|
||||||
type SK = keyof PgStockSansVenteRow;
|
type SK = keyof PgStockSansVenteRow;
|
||||||
const filteredRows = useMemo(() => rows.filter(r => r.stock_actuel !== 0), [rows]);
|
|
||||||
|
const [filterFournisseur, setFilterFournisseur] = useState("");
|
||||||
|
const [filterAnnee, setFilterAnnee] = useState("");
|
||||||
|
|
||||||
|
const baseRows = useMemo(() => rows.filter(r => r.stock_actuel !== 0), [rows]);
|
||||||
|
|
||||||
|
const fournisseurs = useMemo(() =>
|
||||||
|
[...new Set(baseRows.map(r => r.fournisseur).filter(Boolean))].sort((a, b) => a.localeCompare(b, "fr")),
|
||||||
|
[baseRows]
|
||||||
|
);
|
||||||
|
const annees = useMemo(() =>
|
||||||
|
[...new Set(baseRows.map(r => getYear(r.derniere_entree)).filter(Boolean))].sort((a, b) => b.localeCompare(a)),
|
||||||
|
[baseRows]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => baseRows.filter(r =>
|
||||||
|
(!filterFournisseur || r.fournisseur === filterFournisseur) &&
|
||||||
|
(!filterAnnee || getYear(r.derniere_entree) === filterAnnee)
|
||||||
|
), [baseRows, filterFournisseur, filterAnnee]);
|
||||||
|
|
||||||
const { sorted, sortKey, sortDir, handleSort } = useSortedRows<PgStockSansVenteRow>(
|
const { sorted, sortKey, sortDir, handleSort } = useSortedRows<PgStockSansVenteRow>(
|
||||||
filteredRows,
|
filtered,
|
||||||
["stock_actuel"]
|
["stock_actuel"]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,11 +253,25 @@ function TabEntreesSansVente({ rows, magasin }: { rows: PgStockSansVenteRow[]; m
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-end items-center gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<span className="text-sm text-gray-500">{sorted.length} article{sorted.length !== 1 ? "s" : ""}</span>
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<button onClick={handleExport} className="flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors">
|
<FilterSelect label="Fournisseur" value={filterFournisseur} onChange={setFilterFournisseur} options={fournisseurs} />
|
||||||
<Download className="w-4 h-4" />Exporter Excel
|
<FilterSelect label="Année entrée" value={filterAnnee} onChange={setFilterAnnee} options={annees} />
|
||||||
</button>
|
{(filterFournisseur || filterAnnee) && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setFilterFournisseur(""); setFilterAnnee(""); }}
|
||||||
|
className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-1.5 text-xs font-medium text-orange-700 hover:bg-orange-100 transition-colors"
|
||||||
|
>
|
||||||
|
✕ Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-gray-500">{sorted.length} article{sorted.length !== 1 ? "s" : ""}</span>
|
||||||
|
<button onClick={handleExport} className="flex items-center gap-2 rounded-lg bg-emerald-600 px-4 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors">
|
||||||
|
<Download className="w-4 h-4" />Exporter Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto rounded-xl border border-gray-200 bg-white shadow-sm">
|
<div className="overflow-x-auto rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
|
|||||||
Reference in new issue
Block a user