mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-24 15:38:09 +02:00
feat: Implement initial dashboard page displaying network-level KPIs and individual store statistics.
This commit is contained in:
1 parent
38979debb8
commit
2eb196861b
1 file changed
+287
-117
@@ -356,136 +356,237 @@ function Top10Table({ title, items, sortKey, accent }: {
|
|||||||
sortKey: "ca" | "qte" | "marge";
|
sortKey: "ca" | "qte" | "marge";
|
||||||
accent: typeof tableAccents.ca;
|
accent: typeof tableAccents.ca;
|
||||||
}) {
|
}) {
|
||||||
const maxVal = Math.max(items[0]?.[sortKey] ?? 1, 1);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-2xl flex flex-col"
|
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-elevated)",
|
background: "var(--bg-elevated)",
|
||||||
border: `1px solid ${accent.border}`,
|
border: `1px solid ${accent.border}`,
|
||||||
|
borderRadius: "1rem",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
boxShadow: `0 4px 24px rgba(0,0,0,0.18), 0 0 0 0px ${accent.bar}`,
|
boxShadow: "0 4px 24px rgba(0,0,0,0.22), 0 1px 6px rgba(0,0,0,0.14)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table header */}
|
{/* ── Header strip ── */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2.5"
|
|
||||||
style={{
|
style={{
|
||||||
background: accent.header,
|
background: accent.header,
|
||||||
borderBottom: `1px solid ${accent.border}`,
|
borderBottom: `1px solid ${accent.border}`,
|
||||||
padding: "0.75rem 1rem",
|
padding: "0.625rem 0.875rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="rounded-lg flex items-center justify-center"
|
|
||||||
style={{
|
style={{
|
||||||
width: "1.75rem",
|
width: "1.6rem",
|
||||||
height: "1.75rem",
|
height: "1.6rem",
|
||||||
background: `${accent.bar}20`,
|
borderRadius: "0.4rem",
|
||||||
border: `1px solid ${accent.bar}40`,
|
background: `${accent.bar}22`,
|
||||||
|
border: `1px solid ${accent.bar}44`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sortKey === "ca" && <Euro style={{ width: "0.8rem", height: "0.8rem", color: accent.bar }} />}
|
{sortKey === "ca" && <Euro style={{ width: "0.75rem", height: "0.75rem", color: accent.bar }} />}
|
||||||
{sortKey === "qte" && <ShoppingCart style={{ width: "0.8rem", height: "0.8rem", color: accent.bar }} />}
|
{sortKey === "qte" && <ShoppingCart style={{ width: "0.75rem", height: "0.75rem", color: accent.bar }} />}
|
||||||
{sortKey === "marge" && <BarChart3 style={{ width: "0.8rem", height: "0.8rem", color: accent.bar }} />}
|
{sortKey === "marge" && <BarChart3 style={{ width: "0.75rem", height: "0.75rem", color: accent.bar }} />}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: accent.label, textTransform: "uppercase", letterSpacing: "0.07em" }}>
|
<span style={{
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: accent.label,
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.09em",
|
||||||
|
}}>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
|
{/* column labels */}
|
||||||
|
<div style={{ marginLeft: "auto", display: "flex", gap: "0.5rem", alignItems: "center" }}>
|
||||||
|
{sortKey === "ca" && (
|
||||||
|
<>
|
||||||
|
<span style={{ fontSize: "0.6rem", color: accent.label, opacity: 0.7, fontVariantNumeric: "tabular-nums" as const, letterSpacing: "0.05em" }}>QTÉ</span>
|
||||||
|
<span style={{ fontSize: "0.6rem", color: accent.label, opacity: 0.7, letterSpacing: "0.05em", marginLeft: "0.5rem" }}>CA</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{sortKey === "qte" && (
|
||||||
|
<span style={{ fontSize: "0.6rem", color: accent.label, opacity: 0.7, letterSpacing: "0.05em" }}>QTÉ</span>
|
||||||
|
)}
|
||||||
|
{sortKey === "marge" && (
|
||||||
|
<span style={{ fontSize: "0.6rem", color: accent.label, opacity: 0.7, letterSpacing: "0.05em" }}>MARGE</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rows */}
|
{/* ── Rows ── */}
|
||||||
<div className="flex flex-col">
|
{items.length === 0 ? (
|
||||||
{items.length === 0 ? (
|
<div style={{ padding: "2rem", textAlign: "center", fontSize: "0.78rem", color: "var(--text-muted)" }}>
|
||||||
<div style={{ padding: "2rem", textAlign: "center", fontSize: "0.8rem", color: "var(--text-muted)" }}>
|
Aucune donnée
|
||||||
Aucune donnée
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
items.map((item, i) => {
|
||||||
items.map((item, i) => {
|
const txMarge = item.ca > 0 ? (item.marge / item.ca) * 100 : 0;
|
||||||
const val = item[sortKey];
|
const margeColor = txMarge > 25 ? "#22c55e" : txMarge > 15 ? "#f59e0b" : "var(--text-muted)";
|
||||||
const barPct = maxVal > 0 ? Math.max((val / maxVal) * 100, 2) : 0;
|
const margeBg = txMarge > 25 ? "rgba(34,197,94,0.1)" : txMarge > 15 ? "rgba(245,158,11,0.1)" : "rgba(100,116,139,0.1)";
|
||||||
const txMarge = item.ca > 0 ? (item.marge / item.ca) * 100 : 0;
|
const margeBorder = txMarge > 25 ? "rgba(34,197,94,0.25)" : txMarge > 15 ? "rgba(245,158,11,0.25)" : "rgba(100,116,139,0.18)";
|
||||||
const isTopThree = i < 3;
|
const isEven = i % 2 === 0;
|
||||||
|
const isTopThree = i < 3;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.codein}
|
key={item.codein}
|
||||||
style={{
|
style={{
|
||||||
padding: "0.55rem 1rem",
|
padding: "0.45rem 0.875rem",
|
||||||
borderBottom: i < items.length - 1 ? "1px solid var(--border)" : undefined,
|
borderBottom: i < items.length - 1 ? "1px solid var(--border)" : undefined,
|
||||||
background: isTopThree ? `${accent.bar}06` : undefined,
|
background: isEven ? "transparent" : "rgba(255,255,255,0.022)",
|
||||||
transition: "background 0.15s",
|
display: "flex",
|
||||||
}}
|
flexDirection: "column",
|
||||||
>
|
gap: "0.2rem",
|
||||||
{/* Main row */}
|
}}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<RankBadge rank={i} />
|
{/* ── Primary row: rank + name + main value ── */}
|
||||||
<span
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0 }}>
|
||||||
style={{
|
<RankBadge rank={i} />
|
||||||
flex: 1,
|
|
||||||
fontSize: "0.78rem",
|
|
||||||
color: isTopThree ? "var(--text-primary)" : "var(--text-secondary)",
|
|
||||||
fontWeight: isTopThree ? 600 : 400,
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
title={item.libelle1}
|
|
||||||
>
|
|
||||||
{item.libelle1 ?? item.codein}
|
|
||||||
</span>
|
|
||||||
<span style={{
|
|
||||||
fontSize: "0.82rem",
|
|
||||||
fontWeight: 700,
|
|
||||||
color: "var(--text-primary)",
|
|
||||||
flexShrink: 0,
|
|
||||||
fontVariantNumeric: "tabular-nums",
|
|
||||||
}}>
|
|
||||||
{sortKey === "qte" ? fmtQte(val) : fmtEur(val)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar + meta */}
|
<span
|
||||||
<div className="flex items-center gap-2" style={{ marginTop: "0.3rem", paddingLeft: "1.9rem" }}>
|
title={item.libelle1}
|
||||||
<div style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: "3px",
|
fontSize: "0.775rem",
|
||||||
borderRadius: "9999px",
|
fontWeight: isTopThree ? 600 : 400,
|
||||||
background: "var(--border)",
|
color: isTopThree ? "var(--text-primary)" : "var(--text-secondary)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
textOverflow: "ellipsis",
|
||||||
<div style={{
|
whiteSpace: "nowrap",
|
||||||
height: "100%",
|
minWidth: 0,
|
||||||
width: `${barPct}%`,
|
lineHeight: 1.3,
|
||||||
borderRadius: "9999px",
|
}}
|
||||||
background: i === 0
|
>
|
||||||
? `linear-gradient(90deg, ${accent.bar}, ${clrGold})`
|
{item.libelle1 ?? item.codein}
|
||||||
: accent.bar,
|
</span>
|
||||||
opacity: 1 - i * 0.07,
|
|
||||||
}} />
|
{/* Main sort-key value */}
|
||||||
</div>
|
<span style={{
|
||||||
<span style={{
|
fontSize: "0.825rem",
|
||||||
fontSize: "0.65rem",
|
fontWeight: 700,
|
||||||
color: "var(--text-muted)",
|
color: isTopThree ? "var(--text-primary)" : "var(--text-secondary)",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
}}>
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
{sortKey === "ca" && `${fmtQte(item.qte)} pcs · `}
|
letterSpacing: "-0.01em",
|
||||||
{sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `}
|
}}>
|
||||||
<span style={{ color: txMarge > 25 ? "#22c55e" : txMarge > 15 ? "#f59e0b" : "var(--text-muted)" }}>
|
{sortKey === "qte"
|
||||||
{fmtPct(txMarge, false)} mg
|
? `${fmtQte(item.qte)} pcs`
|
||||||
</span>
|
: sortKey === "marge"
|
||||||
</span>
|
? fmtEur(item.marge)
|
||||||
</div>
|
: fmtEur(item.ca)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})
|
{/* ── Secondary info row ── */}
|
||||||
)}
|
<div style={{
|
||||||
</div>
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingLeft: "1.9rem",
|
||||||
|
flexWrap: "wrap" as const,
|
||||||
|
}}>
|
||||||
|
{/* For CA table: show qte + marge */}
|
||||||
|
{sortKey === "ca" && (
|
||||||
|
<>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
{fmtQte(item.qte)} pcs
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: "0.55rem", color: "var(--border)", lineHeight: 1 }}>│</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
mg {fmtEur(item.marge)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* For QTE table: show CA + marge */}
|
||||||
|
{sortKey === "qte" && (
|
||||||
|
<>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
CA {fmtEur(item.ca)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: "0.55rem", color: "var(--border)", lineHeight: 1 }}>│</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
mg {fmtEur(item.marge)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* For Marge table: show CA + qte */}
|
||||||
|
{sortKey === "marge" && (
|
||||||
|
<>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
CA {fmtEur(item.ca)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: "0.55rem", color: "var(--border)", lineHeight: 1 }}>│</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
||||||
|
}}>
|
||||||
|
{fmtQte(item.qte)} pcs
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Taux de marge pill — always shown */}
|
||||||
|
<span style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
fontSize: "0.6rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: margeColor,
|
||||||
|
background: margeBg,
|
||||||
|
border: `1px solid ${margeBorder}`,
|
||||||
|
borderRadius: "9999px",
|
||||||
|
padding: "0.1rem 0.375rem",
|
||||||
|
fontVariantNumeric: "tabular-nums" as const,
|
||||||
|
letterSpacing: "0.01em",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
{fmtPct(txMarge, false)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -494,32 +595,101 @@ function SiteHitParade({ siteData, dateHier }: { siteData: DashboardSiteTop10; d
|
|||||||
const name = SITE_NAMES[siteData.site] ?? siteData.site;
|
const name = SITE_NAMES[siteData.site] ?? siteData.site;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
{/* Section title */}
|
{/* ── Section header ── */}
|
||||||
<div className="flex items-center gap-3">
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "1rem",
|
||||||
|
background: "var(--bg-elevated)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "0.875rem",
|
||||||
|
padding: "0.875rem 1.25rem",
|
||||||
|
boxShadow: "0 4px 24px rgba(0,0,0,0.22), 0 1px 6px rgba(0,0,0,0.14)",
|
||||||
|
position: "relative" as const,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* subtle glow behind icon */}
|
||||||
|
<div style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "-10px",
|
||||||
|
top: "-10px",
|
||||||
|
width: "80px",
|
||||||
|
height: "80px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: `radial-gradient(circle, ${clrGold}18 0%, transparent 70%)`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="rounded-xl flex items-center justify-center"
|
|
||||||
style={{
|
style={{
|
||||||
width: "2.25rem",
|
width: "2.5rem",
|
||||||
height: "2.25rem",
|
height: "2.5rem",
|
||||||
background: "var(--accent-bg)",
|
borderRadius: "0.625rem",
|
||||||
border: "1px solid var(--accent-border)",
|
background: `${clrGold}15`,
|
||||||
|
border: `1px solid ${clrGold}35`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
position: "relative" as const,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Award style={{ width: "1rem", height: "1rem", color: "var(--accent)" }} />
|
<Award style={{ width: "1.1rem", height: "1.1rem", color: clrGold }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3 style={{ fontSize: "1rem", fontWeight: 700, color: "var(--text-primary)", lineHeight: 1.2 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "0.1rem" }}>
|
||||||
Hit Parade — {name}
|
<div style={{ display: "flex", alignItems: "baseline", gap: "0.5rem" }}>
|
||||||
</h3>
|
<h3 style={{
|
||||||
<p style={{ fontSize: "0.7rem", color: "var(--text-muted)", marginTop: "0.1rem" }}>
|
fontSize: "1.05rem",
|
||||||
Données du {dateLabel(dateHier)} · site {siteData.site}
|
fontWeight: 800,
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
lineHeight: 1,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
margin: 0,
|
||||||
|
}}>
|
||||||
|
Hit Parade
|
||||||
|
</h3>
|
||||||
|
<span style={{
|
||||||
|
fontSize: "1.05rem",
|
||||||
|
fontWeight: 800,
|
||||||
|
color: clrGold,
|
||||||
|
lineHeight: 1,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
}}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "0.68rem", color: "var(--text-muted)", margin: 0 }}>
|
||||||
|
Données du {dateLabel(dateHier)} · Site {siteData.site}
|
||||||
|
 · Top 10 produits par CA, Quantité et Marge
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Store badge */}
|
||||||
|
<div style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.4rem",
|
||||||
|
background: "var(--accent-bg)",
|
||||||
|
border: "1px solid var(--accent-border)",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
padding: "0.3rem 0.625rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<Store style={{ width: "0.75rem", height: "0.75rem", color: "var(--accent)" }} />
|
||||||
|
<span style={{ fontSize: "0.7rem", fontWeight: 600, color: "var(--accent)" }}>{name}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Three tables */}
|
{/* ── Three tables side by side ── */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4" style={{ alignItems: "start" }}>
|
<div
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||||
|
style={{ alignItems: "start" }}
|
||||||
|
>
|
||||||
<Top10Table title="Top CA" items={siteData.ca} sortKey="ca" accent={tableAccents.ca} />
|
<Top10Table title="Top CA" items={siteData.ca} sortKey="ca" accent={tableAccents.ca} />
|
||||||
<Top10Table title="Top Quantité" items={siteData.qte} sortKey="qte" accent={tableAccents.qte} />
|
<Top10Table title="Top Quantité" items={siteData.qte} sortKey="qte" accent={tableAccents.qte} />
|
||||||
<Top10Table title="Top Marge" items={siteData.marge} sortKey="marge" accent={tableAccents.marge} />
|
<Top10Table title="Top Marge" items={siteData.marge} sortKey="marge" accent={tableAccents.marge} />
|
||||||
|
|||||||
Reference in new issue
Block a user