mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: add NoSalesTab component to display products without sales in the last 6 months
This commit is contained in:
1 parent
687009dba1
commit
6492f93bf4
4 files changed
+350
-5
No files matched your search
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(npx tsc:*)"
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(find /c/GIT/CollectFlow/src -type f \\\\\\( -name \"*.ts\" -o -name \"*.tsx\" \\\\\\) -exec grep -l \"dernier\" {} \\\\;)",
|
||||||
|
"Bash(rtk tsc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<!-- rtk-instructions v2 -->
|
||||||
|
# RTK (Rust Token Killer) - Token-Optimized Commands
|
||||||
|
|
||||||
|
## Golden Rule
|
||||||
|
|
||||||
|
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
|
||||||
|
|
||||||
|
**Important**: Even in command chains with `&&`, use `rtk`:
|
||||||
|
```bash
|
||||||
|
# ❌ Wrong
|
||||||
|
git add . && git commit -m "msg" && git push
|
||||||
|
|
||||||
|
# ✅ Correct
|
||||||
|
rtk git add . && rtk git commit -m "msg" && rtk git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTK Commands by Workflow
|
||||||
|
|
||||||
|
### Build & Compile (80-90% savings)
|
||||||
|
```bash
|
||||||
|
rtk cargo build # Cargo build output
|
||||||
|
rtk cargo check # Cargo check output
|
||||||
|
rtk cargo clippy # Clippy warnings grouped by file (80%)
|
||||||
|
rtk tsc # TypeScript errors grouped by file/code (83%)
|
||||||
|
rtk lint # ESLint/Biome violations grouped (84%)
|
||||||
|
rtk prettier --check # Files needing format only (70%)
|
||||||
|
rtk next build # Next.js build with route metrics (87%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test (90-99% savings)
|
||||||
|
```bash
|
||||||
|
rtk cargo test # Cargo test failures only (90%)
|
||||||
|
rtk vitest run # Vitest failures only (99.5%)
|
||||||
|
rtk playwright test # Playwright failures only (94%)
|
||||||
|
rtk test <cmd> # Generic test wrapper - failures only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git (59-80% savings)
|
||||||
|
```bash
|
||||||
|
rtk git status # Compact status
|
||||||
|
rtk git log # Compact log (works with all git flags)
|
||||||
|
rtk git diff # Compact diff (80%)
|
||||||
|
rtk git show # Compact show (80%)
|
||||||
|
rtk git add # Ultra-compact confirmations (59%)
|
||||||
|
rtk git commit # Ultra-compact confirmations (59%)
|
||||||
|
rtk git push # Ultra-compact confirmations
|
||||||
|
rtk git pull # Ultra-compact confirmations
|
||||||
|
rtk git branch # Compact branch list
|
||||||
|
rtk git fetch # Compact fetch
|
||||||
|
rtk git stash # Compact stash
|
||||||
|
rtk git worktree # Compact worktree
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
|
||||||
|
|
||||||
|
### GitHub (26-87% savings)
|
||||||
|
```bash
|
||||||
|
rtk gh pr view <num> # Compact PR view (87%)
|
||||||
|
rtk gh pr checks # Compact PR checks (79%)
|
||||||
|
rtk gh run list # Compact workflow runs (82%)
|
||||||
|
rtk gh issue list # Compact issue list (80%)
|
||||||
|
rtk gh api # Compact API responses (26%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript Tooling (70-90% savings)
|
||||||
|
```bash
|
||||||
|
rtk pnpm list # Compact dependency tree (70%)
|
||||||
|
rtk pnpm outdated # Compact outdated packages (80%)
|
||||||
|
rtk pnpm install # Compact install output (90%)
|
||||||
|
rtk npm run <script> # Compact npm script output
|
||||||
|
rtk npx <cmd> # Compact npx command output
|
||||||
|
rtk prisma # Prisma without ASCII art (88%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files & Search (60-75% savings)
|
||||||
|
```bash
|
||||||
|
rtk ls <path> # Tree format, compact (65%)
|
||||||
|
rtk read <file> # Code reading with filtering (60%)
|
||||||
|
rtk grep <pattern> # Search grouped by file (75%)
|
||||||
|
rtk find <pattern> # Find grouped by directory (70%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analysis & Debug (70-90% savings)
|
||||||
|
```bash
|
||||||
|
rtk err <cmd> # Filter errors only from any command
|
||||||
|
rtk log <file> # Deduplicated logs with counts
|
||||||
|
rtk json <file> # JSON structure without values
|
||||||
|
rtk deps # Dependency overview
|
||||||
|
rtk env # Environment variables compact
|
||||||
|
rtk summary <cmd> # Smart summary of command output
|
||||||
|
rtk diff # Ultra-compact diffs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure (85% savings)
|
||||||
|
```bash
|
||||||
|
rtk docker ps # Compact container list
|
||||||
|
rtk docker images # Compact image list
|
||||||
|
rtk docker logs <c> # Deduplicated logs
|
||||||
|
rtk kubectl get # Compact resource list
|
||||||
|
rtk kubectl logs # Deduplicated pod logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network (65-70% savings)
|
||||||
|
```bash
|
||||||
|
rtk curl <url> # Compact HTTP responses (70%)
|
||||||
|
rtk wget <url> # Compact download output (65%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meta Commands
|
||||||
|
```bash
|
||||||
|
rtk gain # View token savings statistics
|
||||||
|
rtk gain --history # View command history with savings
|
||||||
|
rtk discover # Analyze Claude Code sessions for missed RTK usage
|
||||||
|
rtk proxy <cmd> # Run command without filtering (for debugging)
|
||||||
|
rtk init # Add RTK instructions to CLAUDE.md
|
||||||
|
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Savings Overview
|
||||||
|
|
||||||
|
| Category | Commands | Typical Savings |
|
||||||
|
|----------|----------|-----------------|
|
||||||
|
| Tests | vitest, playwright, cargo test | 90-99% |
|
||||||
|
| Build | next, tsc, lint, prettier | 70-87% |
|
||||||
|
| Git | status, log, diff, add, commit | 59-80% |
|
||||||
|
| GitHub | gh pr, gh run, gh issue | 26-87% |
|
||||||
|
| Package Managers | pnpm, npm, npx | 70-90% |
|
||||||
|
| Files | ls, read, grep, find | 60-75% |
|
||||||
|
| Infrastructure | docker, kubectl | 85% |
|
||||||
|
| Network | curl, wget | 65-70% |
|
||||||
|
|
||||||
|
Overall average: **60-90% token reduction** on common development operations.
|
||||||
|
<!-- /rtk-instructions -->
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useTransition, useCallback } from "react";
|
import { useEffect, useRef, useState, useTransition, useCallback, useMemo } from "react";
|
||||||
import { HeatmapGrid } from "@/features/grid/components/heatmap-grid";
|
import { HeatmapGrid } from "@/features/grid/components/heatmap-grid";
|
||||||
import { FloatingSummaryBar } from "@/features/grid/components/floating-summary-bar";
|
import { FloatingSummaryBar } from "@/features/grid/components/floating-summary-bar";
|
||||||
import { BulkActionToolbar } from "@/features/grid/components/bulk-action-toolbar";
|
import { BulkActionToolbar } from "@/features/grid/components/bulk-action-toolbar";
|
||||||
import { GridFilterBar } from "@/features/grid/components/grid-filter-bar";
|
import { GridFilterBar } from "@/features/grid/components/grid-filter-bar";
|
||||||
import { ExportDropdown } from "@/features/grid/components/export-dropdown";
|
import { ExportDropdown } from "@/features/grid/components/export-dropdown";
|
||||||
|
import { NoSalesTab, countNoSalesIn6Months } from "@/features/grid/components/no-sales-tab";
|
||||||
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||||
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
||||||
import { useInfiniteGrid } from "@/features/grid/hooks/use-infinite-grid";
|
import { useInfiniteGrid } from "@/features/grid/hooks/use-infinite-grid";
|
||||||
@@ -38,6 +39,9 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour
|
|||||||
const [selectedCodeins, setSelectedCodeins] = useState<string[]>([]);
|
const [selectedCodeins, setSelectedCodeins] = useState<string[]>([]);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
|
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
|
||||||
|
const [activeTab, setActiveTab] = useState<"grid" | "no-sales">("grid");
|
||||||
|
|
||||||
|
const noSalesCount = useMemo(() => countNoSalesIn6Months(rows), [rows]);
|
||||||
|
|
||||||
const visibleCodeins = initialRows.map(r => r.codein);
|
const visibleCodeins = initialRows.map(r => r.codein);
|
||||||
const { save, hasDrafts, count } = useSaveDrafts(magasin, visibleCodeins);
|
const { save, hasDrafts, count } = useSaveDrafts(magasin, visibleCodeins);
|
||||||
@@ -181,26 +185,68 @@ export function GridClient({ initialRows, initialTotal, codeFournisseur, nomFour
|
|||||||
<GridFilterBar fournisseurs={fournisseurs} magasins={magasins} />
|
<GridFilterBar fournisseurs={fournisseurs} magasins={magasins} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bulk toolbar (contextual) */}
|
{/* Tab switcher */}
|
||||||
|
<div className="shrink-0 flex gap-0 print:hidden" style={{ borderBottom: "1px solid var(--border)" }}>
|
||||||
|
{(["grid", "no-sales"] as const).map(tab => {
|
||||||
|
const isActive = activeTab === tab;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
className="flex items-center gap-1.5 px-4 py-2 text-[13px] font-medium transition-colors"
|
||||||
|
style={{
|
||||||
|
color: isActive ? "var(--text-primary)" : "var(--text-muted)",
|
||||||
|
borderBottom: isActive ? "2px solid var(--accent)" : "2px solid transparent",
|
||||||
|
marginBottom: "-1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab === "grid" ? "Assortiment" : "Sans vente 6 mois"}
|
||||||
|
{tab === "no-sales" && noSalesCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-bold"
|
||||||
|
style={{
|
||||||
|
background: isActive ? "var(--accent)" : "var(--bg-elevated)",
|
||||||
|
color: isActive ? "white" : "var(--text-secondary)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{noSalesCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk toolbar (contextual) — uniquement sur l'onglet grille */}
|
||||||
|
{activeTab === "grid" && (
|
||||||
<div className="shrink-0 print:hidden">
|
<div className="shrink-0 print:hidden">
|
||||||
<BulkActionToolbar
|
<BulkActionToolbar
|
||||||
selectedCodeins={selectedCodeins}
|
selectedCodeins={selectedCodeins}
|
||||||
onClearSelection={() => setSelectedCodeins([])}
|
onClearSelection={() => setSelectedCodeins([])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main grid */}
|
{/* Main content */}
|
||||||
|
{activeTab === "grid" ? (
|
||||||
<div className="flex-1 min-h-0 min-w-0">
|
<div className="flex-1 min-h-0 min-w-0">
|
||||||
<HeatmapGrid
|
<HeatmapGrid
|
||||||
onSelectionChange={setSelectedCodeins}
|
onSelectionChange={setSelectedCodeins}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 min-h-0 min-w-0 flex flex-col">
|
||||||
|
<NoSalesTab />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Summary bar */}
|
{/* Summary bar — uniquement sur l'onglet grille */}
|
||||||
|
{activeTab === "grid" && (
|
||||||
<div className="print:hidden">
|
<div className="print:hidden">
|
||||||
<FloatingSummaryBar />
|
<FloatingSummaryBar />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||||
|
import type { ProductRow } from "@/types/grid";
|
||||||
|
import { PackageX } from "lucide-react";
|
||||||
|
|
||||||
|
function getNoSalesIn6Months(rows: ProductRow[]): ProductRow[] {
|
||||||
|
const sixMonthsAgo = new Date();
|
||||||
|
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
||||||
|
|
||||||
|
return rows.filter(row => {
|
||||||
|
if (row.derniereVente) {
|
||||||
|
return new Date(row.derniereVente) < sixMonthsAgo;
|
||||||
|
}
|
||||||
|
// Fallback : calcul depuis sales12m
|
||||||
|
const salesMonths = Object.entries(row.sales12m || {})
|
||||||
|
.filter(([, qty]) => qty > 0)
|
||||||
|
.map(([m]) => m)
|
||||||
|
.sort();
|
||||||
|
if (salesMonths.length === 0) return true;
|
||||||
|
const last = salesMonths[salesMonths.length - 1];
|
||||||
|
const lastDate = new Date(parseInt(last.slice(0, 4)), parseInt(last.slice(4, 6)) - 1, 1);
|
||||||
|
return lastDate < sixMonthsAgo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso?: string): string {
|
||||||
|
if (!iso) return "—";
|
||||||
|
return new Date(iso).toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysSince(iso?: string): number | null {
|
||||||
|
if (!iso) return null;
|
||||||
|
return Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCa(ca: number): string {
|
||||||
|
return ca.toLocaleString("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAMME_COLORS: Record<string, string> = {
|
||||||
|
A: "bg-emerald-500/15 text-emerald-700",
|
||||||
|
B: "bg-blue-500/15 text-blue-700",
|
||||||
|
C: "bg-amber-500/15 text-amber-700",
|
||||||
|
Y: "bg-purple-500/15 text-purple-700",
|
||||||
|
Z: "bg-red-500/15 text-red-700",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NoSalesTab() {
|
||||||
|
const rows = useGridStore(s => s.rows);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const result = getNoSalesIn6Months(rows);
|
||||||
|
// Tri : produits les plus anciens en premier, puis par stock décroissant
|
||||||
|
return result.sort((a, b) => {
|
||||||
|
const da = a.derniereVente ? new Date(a.derniereVente).getTime() : 0;
|
||||||
|
const db = b.derniereVente ? new Date(b.derniereVente).getTime() : 0;
|
||||||
|
if (da !== db) return da - db;
|
||||||
|
return (b.stockActuel ?? 0) - (a.stockActuel ?? 0);
|
||||||
|
});
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-sm italic" style={{ color: "var(--text-muted)" }}>
|
||||||
|
Chargement des données en cours…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center gap-3" style={{ color: "var(--text-muted)" }}>
|
||||||
|
<PackageX className="w-10 h-10 opacity-30" />
|
||||||
|
<p className="text-sm italic">Aucun produit sans vente depuis 6 mois.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 min-h-0 gap-3">
|
||||||
|
{/* Résumé */}
|
||||||
|
<div className="shrink-0 px-3 py-2 rounded-lg text-sm font-medium flex items-center gap-2"
|
||||||
|
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}
|
||||||
|
>
|
||||||
|
<PackageX className="w-4 h-4 shrink-0" style={{ color: "var(--accent)" }} />
|
||||||
|
<span>
|
||||||
|
<span className="font-bold" style={{ color: "var(--text-primary)" }}>
|
||||||
|
{filtered.length.toLocaleString("fr-FR")}
|
||||||
|
</span>
|
||||||
|
{" "}produit{filtered.length > 1 ? "s" : ""} sans vente depuis plus de 6 mois
|
||||||
|
{" "}({rows.length.toLocaleString("fr-FR")} références au total)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tableau */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-auto rounded-xl" style={{ border: "1px solid var(--border)" }}>
|
||||||
|
<table className="w-full text-[12px] border-collapse">
|
||||||
|
<thead className="sticky top-0 z-10" style={{ background: "var(--bg-elevated)" }}>
|
||||||
|
<tr style={{ borderBottom: "1px solid var(--border)" }}>
|
||||||
|
{["Code", "Libellé", "Réf.", "Gamme", "Dernière vente", "Jours", "Stock", "CA 12m", "Score"].map(col => (
|
||||||
|
<th key={col}
|
||||||
|
className="px-3 py-2 text-left font-semibold whitespace-nowrap"
|
||||||
|
style={{ color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const days = daysSince(row.derniereVente);
|
||||||
|
const gamme = row.codeGamme?.trim().toUpperCase() ?? "—";
|
||||||
|
const gammeClass = GAMME_COLORS[gamme] ?? "bg-zinc-500/15 text-zinc-600";
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.codein}
|
||||||
|
className="transition-colors hover:bg-white/5"
|
||||||
|
style={{
|
||||||
|
borderBottom: i < filtered.length - 1 ? "1px solid var(--border)" : undefined,
|
||||||
|
background: i % 2 === 0 ? "transparent" : "var(--bg-subtle, rgba(0,0,0,0.02))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="px-3 py-1.5 font-mono" style={{ color: "var(--text-muted)" }}>{row.codein}</td>
|
||||||
|
<td className="px-3 py-1.5 max-w-[220px] truncate" style={{ color: "var(--text-primary)" }} title={row.libelle1}>{row.libelle1}</td>
|
||||||
|
<td className="px-3 py-1.5 font-mono text-[11px]" style={{ color: "var(--text-muted)" }}>{row.reference || "—"}</td>
|
||||||
|
<td className="px-3 py-1.5">
|
||||||
|
<span className={`inline-block px-1.5 py-0.5 rounded text-[11px] font-bold ${gammeClass}`}>
|
||||||
|
{gamme}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-1.5 whitespace-nowrap" style={{ color: row.derniereVente ? "var(--text-secondary)" : "var(--text-muted)" }}>
|
||||||
|
{formatDate(row.derniereVente)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-1.5 text-right font-medium tabular-nums"
|
||||||
|
style={{ color: days && days > 365 ? "var(--color-red, #ef4444)" : "var(--text-secondary)" }}>
|
||||||
|
{days != null ? days.toLocaleString("fr-FR") : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-1.5 text-right tabular-nums" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{row.stockActuel != null ? row.stockActuel.toLocaleString("fr-FR") : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-1.5 text-right tabular-nums" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{row.totalCa > 0 ? formatCa(row.totalCa) : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-1.5 text-right tabular-nums font-semibold"
|
||||||
|
style={{ color: row.score >= 50 ? "var(--color-amber, #f59e0b)" : "var(--text-muted)" }}>
|
||||||
|
{row.score > 0 ? row.score : "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compte les produits sans vente depuis 6 mois — utilisé pour le badge dans le switcher */
|
||||||
|
export function countNoSalesIn6Months(rows: ProductRow[]): number {
|
||||||
|
return getNoSalesIn6Months(rows).length;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user