mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 06:58:10 +02:00
feat: add stock and supplier info to dashboard top 10 cards
Enrich DashboardTopItem with stock and fournisseur fields via PostgreSQL, then display them under each article in the dashboard top CA/Qte/Marge tables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
fedfbadc35
commit
42e39a1c2b
2 files changed
+73
-12
No files matched your search
@@ -496,6 +496,36 @@ function Top10Table({ title, items, sortKey, accent }: {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Tertiary row: fournisseur + stock ── */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingLeft: "1.9rem",
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}>
|
||||||
|
{item.fournisseur}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: item.stock <= 0 ? "#ef4444" : item.stock < 10 ? "#f59e0b" : "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
Stk {fmtQte(item.stock)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Secondary info row ── */}
|
{/* ── Secondary info row ── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
+43
-12
@@ -496,6 +496,8 @@ export interface DashboardTopItem {
|
|||||||
ca: number;
|
ca: number;
|
||||||
qte: number;
|
qte: number;
|
||||||
marge: number;
|
marge: number;
|
||||||
|
stock: number;
|
||||||
|
fournisseur: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardSiteTop10 {
|
export interface DashboardSiteTop10 {
|
||||||
@@ -592,25 +594,54 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Top 10 par magasin : grouper les items par site, trier, garder top 10
|
// Top 10 par magasin : grouper les items par site, trier, garder top 10
|
||||||
const buildTop10 = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] =>
|
|
||||||
items
|
|
||||||
.map(i => ({
|
|
||||||
codein: i.codein,
|
|
||||||
libelle1: i.libelle1,
|
|
||||||
ca: Number(i.ca_ttc) || 0,
|
|
||||||
qte: Number(i.qte_vendue) || 0,
|
|
||||||
marge: Number(i.marge) || 0,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b[sortKey] - a[sortKey])
|
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
const allItems = [
|
const allItems = [
|
||||||
...(apiResult.top10_ca ?? []),
|
...(apiResult.top10_ca ?? []),
|
||||||
...(apiResult.top10_qte ?? []),
|
...(apiResult.top10_qte ?? []),
|
||||||
...(apiResult.top10_marge ?? []),
|
...(apiResult.top10_marge ?? []),
|
||||||
];
|
];
|
||||||
|
const allCodeins = [...new Set(allItems.map(i => i.codein).filter(Boolean))];
|
||||||
|
|
||||||
|
// Enrichissement : stock et fournisseur via PostgreSQL
|
||||||
|
const [stockMap, fouResult] = await Promise.all([
|
||||||
|
pgGetStockForCodeins(allCodeins),
|
||||||
|
pgNoParallel(sql`
|
||||||
|
SELECT
|
||||||
|
a.codein,
|
||||||
|
COALESCE(fi.nom, af.code, 'Sans fournisseur')::text AS fournisseur
|
||||||
|
FROM articles a
|
||||||
|
LEFT JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1
|
||||||
|
LEFT JOIN fouident fi ON fi.code = af.code
|
||||||
|
WHERE a.codein = ANY(string_to_array(${allCodeins.join(",")}, ','))
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fouMap = new Map<string, string>();
|
||||||
|
for (const row of fouResult.rows as unknown as { codein: string; fournisseur: string }[]) {
|
||||||
|
if (row.codein) fouMap.set(row.codein, row.fournisseur);
|
||||||
|
}
|
||||||
|
|
||||||
const siteSet = [...new Set(allItems.map(i => i.site).filter(Boolean))].sort();
|
const siteSet = [...new Set(allItems.map(i => i.site).filter(Boolean))].sort();
|
||||||
|
|
||||||
|
const buildTop10 = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] =>
|
||||||
|
items
|
||||||
|
.map(i => {
|
||||||
|
const stockEntry = stockMap.get(i.codein);
|
||||||
|
const stock = i.site === "292" ? (stockEntry?.stock292 ?? 0)
|
||||||
|
: i.site === "579" ? (stockEntry?.stock579 ?? 0)
|
||||||
|
: (stockEntry?.stockTotal ?? 0);
|
||||||
|
return {
|
||||||
|
codein: i.codein,
|
||||||
|
libelle1: i.libelle1,
|
||||||
|
ca: Number(i.ca_ttc) || 0,
|
||||||
|
qte: Number(i.qte_vendue) || 0,
|
||||||
|
marge: Number(i.marge) || 0,
|
||||||
|
stock,
|
||||||
|
fournisseur: fouMap.get(i.codein) ?? "—",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => b[sortKey] - a[sortKey])
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
const top10BySite: DashboardSiteTop10[] = siteSet.map(site => {
|
const top10BySite: DashboardSiteTop10[] = siteSet.map(site => {
|
||||||
const byS = (arr: ApiPerfTopItem[]) => arr.filter(i => i.site === site);
|
const byS = (arr: ApiPerfTopItem[]) => arr.filter(i => i.site === site);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user