mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: Implement dashboard layout and a new PostgreSQL client for optimized FF Nancy data fetching via bulk SQL queries.
This commit is contained in:
1 parent
e3d241a5ff
commit
d1bbe290da
3 files changed
+89
-192
No files matched your search
@@ -1,24 +1,21 @@
|
||||
import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardMonthStats } from "@/lib/pg-ff-client";
|
||||
import { TrendingUp, TrendingDown, Minus, Store, Euro, Percent, Users } from "lucide-react";
|
||||
import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardSiteTop10 } from "@/lib/pg-ff-client";
|
||||
import { TrendingUp, TrendingDown, Minus, Store, Euro, Users } from "lucide-react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Codes sites tels que retournés par l'API (ex: "F01") → nom affiché
|
||||
const SITE_NAMES: Record<string, string> = {
|
||||
"F01": "Frouard",
|
||||
"F02": "Houdemont",
|
||||
// Anciens codes PostgreSQL si jamais
|
||||
"292": "Frouard",
|
||||
"579": "Houdemont",
|
||||
};
|
||||
|
||||
function fmtEur(n: number): string {
|
||||
const abs = Math.abs(n);
|
||||
if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString("fr-FR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} M€`;
|
||||
if (abs >= 1_000) return `${(n / 1_000).toLocaleString("fr-FR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} k€`;
|
||||
return `${n.toLocaleString("fr-FR", { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`;
|
||||
if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} M€`;
|
||||
return `${Math.round(n).toLocaleString("fr-FR")} €`;
|
||||
}
|
||||
|
||||
function fmtQte(n: number): string {
|
||||
@@ -35,19 +32,13 @@ function delta(current: number, previous: number): number | null {
|
||||
return ((current - previous) / Math.abs(previous)) * 100;
|
||||
}
|
||||
|
||||
function monthLabel(mois: string): string {
|
||||
const [y, m] = mois.split("-");
|
||||
const names = ["Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"];
|
||||
return `${names[parseInt(m) - 1]} ${y.slice(2)}`;
|
||||
}
|
||||
|
||||
function dateLabel(iso: string): string {
|
||||
const [y, m, d] = iso.split("-");
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-components (server-side, no "use client" needed)
|
||||
// Sub-components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function DeltaBadge({ pct }: { pct: number | null }) {
|
||||
@@ -86,7 +77,6 @@ function KpiCard({ label, value, sub, delta: d, icon: Icon }: {
|
||||
|
||||
function SiteCard({ site }: { site: DashboardSiteStats }) {
|
||||
const name = SITE_NAMES[site.site] ?? site.site;
|
||||
const txMarge = site.ca_hier > 0 ? (site.marge_hier / site.ca_hier) * 100 : 0;
|
||||
const dCa = delta(site.ca_hier, site.ca_n1);
|
||||
const dTickets = delta(site.tickets_hier, site.tickets_n1);
|
||||
|
||||
@@ -124,7 +114,7 @@ function Top10Table({ title, items, sortKey }: {
|
||||
}) {
|
||||
const maxVal = items[0]?.[sortKey] ?? 1;
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden flex flex-col" style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}>
|
||||
<div className="rounded-xl flex flex-col" style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}>
|
||||
<div className="px-4 py-3 flex items-center gap-2" style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<span className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>{title}</span>
|
||||
</div>
|
||||
@@ -157,7 +147,7 @@ function Top10Table({ title, items, sortKey }: {
|
||||
<div className="h-full rounded-full" style={{ width: `${barPct}%`, background: "var(--accent)" }} />
|
||||
</div>
|
||||
<span className="text-xs shrink-0" style={{ color: "var(--text-muted)" }}>
|
||||
{sortKey !== "ca" && `CA ${fmtEur(item.ca)} • `}
|
||||
{sortKey !== "ca" && `CA ${fmtEur(item.ca)} · `}
|
||||
{fmtPct(txMarge, false)} marge
|
||||
</span>
|
||||
</div>
|
||||
@@ -169,117 +159,19 @@ function Top10Table({ title, items, sortKey }: {
|
||||
);
|
||||
}
|
||||
|
||||
function EvolutionTable({ evolution }: { evolution: DashboardMonthStats[] }) {
|
||||
// Regrouper par mois YYYY-MM → data
|
||||
const byMois = new Map(evolution.map(e => [e.mois, e]));
|
||||
|
||||
// Calculer les 12 derniers mois complets (pas le mois en cours)
|
||||
const now = new Date();
|
||||
const months12: string[] = [];
|
||||
for (let i = 13; i >= 2; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
months12.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
|
||||
}
|
||||
|
||||
const rows = months12.map(mois => {
|
||||
const [y, m] = mois.split("-");
|
||||
const prevMois = `${parseInt(y) - 1}-${m}`;
|
||||
const n = byMois.get(mois);
|
||||
const n1 = byMois.get(prevMois);
|
||||
const dCa = n && n1 ? delta(n.ca, n1.ca) : null;
|
||||
const dQte = n && n1 ? delta(n.qte, n1.qte) : null;
|
||||
return { mois, n, n1, dCa, dQte };
|
||||
});
|
||||
|
||||
// Trouver les maxCA pour les barres
|
||||
const maxCa = Math.max(...rows.map(r => Math.max(r.n?.ca ?? 0, r.n1?.ca ?? 0)), 1);
|
||||
|
||||
function SiteHitParade({ siteData, dateHier }: { siteData: DashboardSiteTop10; dateHier: string }) {
|
||||
const name = SITE_NAMES[siteData.site] ?? siteData.site;
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden" style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}>
|
||||
<div className="px-4 py-3 flex items-center justify-between" style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<span className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>Évolution mensuelle — CA réseau</span>
|
||||
<span className="text-xs" style={{ color: "var(--text-muted)" }}>N vs N-1 · 12 mois complets</span>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Store className="w-4 h-4" style={{ color: "var(--accent)" }} />
|
||||
<h3 className="text-sm font-bold" style={{ color: "var(--text-primary)" }}>{name}</h3>
|
||||
<span className="text-xs" style={{ color: "var(--text-muted)" }}>· {dateLabel(dateHier)}</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: "var(--text-muted)" }}>Mois</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>CA N</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>CA N-1</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>Δ CA</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>Qté N</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>Qté N-1</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>Δ Qté</th>
|
||||
<th className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-muted)" }}>Tx Marge N</th>
|
||||
<th className="px-4 py-2 font-medium min-w-[120px]" style={{ color: "var(--text-muted)" }}>Tendance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(({ mois, n, n1, dCa, dQte }) => {
|
||||
const txN = n && n.ca > 0 ? (n.marge / n.ca) * 100 : null;
|
||||
const barN = n ? (n.ca / maxCa) * 100 : 0;
|
||||
const barN1 = n1 ? (n1.ca / maxCa) * 100 : 0;
|
||||
const isPositive = dCa !== null && dCa >= 0;
|
||||
return (
|
||||
<tr key={mois} style={{ borderBottom: "1px solid var(--border)" }}>
|
||||
<td className="px-4 py-2 font-semibold" style={{ color: "var(--text-primary)" }}>
|
||||
{monthLabel(mois)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-primary)" }}>
|
||||
{n ? fmtEur(n.ca) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right" style={{ color: "var(--text-secondary)" }}>
|
||||
{n1 ? fmtEur(n1.ca) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{dCa !== null ? (
|
||||
<span className="font-semibold" style={{ color: isPositive ? "#10b981" : "#ef4444" }}>
|
||||
{fmtPct(dCa)}
|
||||
</span>
|
||||
) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-medium" style={{ color: "var(--text-primary)" }}>
|
||||
{n ? fmtQte(n.qte) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right" style={{ color: "var(--text-secondary)" }}>
|
||||
{n1 ? fmtQte(n1.qte) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{dQte !== null ? (
|
||||
<span className="font-semibold" style={{ color: dQte >= 0 ? "#10b981" : "#ef4444" }}>
|
||||
{fmtPct(dQte)}
|
||||
</span>
|
||||
) : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{txN !== null ? (
|
||||
<span style={{ color: txN >= 25 ? "#10b981" : txN >= 15 ? "#f59e0b" : "#ef4444" }}>
|
||||
{fmtPct(txN, false)}
|
||||
</span>
|
||||
) : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] w-5 shrink-0" style={{ color: "var(--accent)" }}>N</span>
|
||||
<div className="flex-1 h-1.5 rounded-full overflow-hidden" style={{ background: "var(--border)" }}>
|
||||
<div className="h-full rounded-full" style={{ width: `${barN}%`, background: "var(--accent)" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] w-5 shrink-0" style={{ color: "var(--text-muted)" }}>N-1</span>
|
||||
<div className="flex-1 h-1.5 rounded-full overflow-hidden" style={{ background: "var(--border)" }}>
|
||||
<div className="h-full rounded-full" style={{ width: `${barN1}%`, background: "var(--text-muted)" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<Top10Table title="Top 10 CA" items={siteData.ca} sortKey="ca" />
|
||||
<Top10Table title="Top 10 Quantité" items={siteData.qte} sortKey="qte" />
|
||||
<Top10Table title="Top 10 Marge" items={siteData.marge} sortKey="marge" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -305,16 +197,15 @@ export default async function DashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const { dateHier, sites, top10Ca, top10Qte, top10Marge, evolution } = data;
|
||||
const { dateHier, dateN1, sites, top10BySite } = data;
|
||||
|
||||
// Totaux réseau
|
||||
const totalCa = sites.reduce((s, r) => s + r.ca_hier, 0);
|
||||
const totalCaN1 = sites.reduce((s, r) => s + r.ca_n1, 0);
|
||||
const totalTickets = sites.reduce((s, r) => s + r.tickets_hier, 0);
|
||||
const totalTicketsN1 = sites.reduce((s, r) => s + r.tickets_n1, 0);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 pb-12 max-w-screen-2xl mx-auto">
|
||||
<div className="flex flex-col gap-6 pb-12">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight" style={{ color: "var(--text-primary)" }}>
|
||||
@@ -322,7 +213,8 @@ export default async function DashboardPage() {
|
||||
</h1>
|
||||
<p className="text-sm mt-0.5" style={{ color: "var(--text-muted)" }}>
|
||||
Données du <span className="font-semibold" style={{ color: "var(--text-secondary)" }}>{dateLabel(dateHier)}</span>
|
||||
{" "}· Comparaison vs même date N-1
|
||||
{" "}· Comparaison vs même jour de semaine N-1
|
||||
{dateN1 && <span> ({dateLabel(dateN1)})</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -364,24 +256,20 @@ export default async function DashboardPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hit Parade */}
|
||||
{/* Hit Parade par magasin */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold mb-3 uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>
|
||||
Hit Parade — réseau · {dateLabel(dateHier)}
|
||||
<h2 className="text-sm font-semibold mb-4 uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>
|
||||
Hit Parade par magasin
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Top10Table title="Top 10 Chiffre d'affaires" items={top10Ca} sortKey="ca" />
|
||||
<Top10Table title="Top 10 Quantité vendue" items={top10Qte} sortKey="qte" />
|
||||
<Top10Table title="Top 10 Marge brute" items={top10Marge} sortKey="marge" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Évolution N/N-1 */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold mb-3 uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>
|
||||
Évolution annuelle
|
||||
</h2>
|
||||
<EvolutionTable evolution={evolution} />
|
||||
{top10BySite.length === 0 ? (
|
||||
<p className="text-sm" style={{ color: "var(--text-muted)" }}>Aucune donnée pour hier.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-8">
|
||||
{top10BySite.map(s => (
|
||||
<SiteHitParade key={s.site} siteData={s} dateHier={dateHier} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
<Sidebar />
|
||||
<main className="flex-1 flex flex-col h-full min-w-0 overflow-hidden">
|
||||
<Header />
|
||||
<div className="flex-1 overflow-hidden flex flex-col p-4 md:p-8">
|
||||
<div className="flex-1 overflow-y-auto flex flex-col p-4 md:p-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+54
-45
@@ -485,20 +485,18 @@ export interface DashboardTopItem {
|
||||
marge: number;
|
||||
}
|
||||
|
||||
export interface DashboardMonthStats {
|
||||
mois: string; // "YYYY-MM"
|
||||
ca: number;
|
||||
qte: number;
|
||||
marge: number;
|
||||
export interface DashboardSiteTop10 {
|
||||
site: string;
|
||||
ca: DashboardTopItem[];
|
||||
qte: DashboardTopItem[];
|
||||
marge: DashboardTopItem[];
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
dateHier: string;
|
||||
dateN1: string;
|
||||
sites: DashboardSiteStats[];
|
||||
top10Ca: DashboardTopItem[];
|
||||
top10Qte: DashboardTopItem[];
|
||||
top10Marge: DashboardTopItem[];
|
||||
evolution: DashboardMonthStats[];
|
||||
top10BySite: DashboardSiteTop10[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -534,23 +532,27 @@ interface ApiPerfDashboard {
|
||||
/**
|
||||
* Retourne toutes les données nécessaires pour le dashboard quotidien.
|
||||
* 100% via l'API REST api.ffnancy.fr — aucune requête PostgreSQL directe.
|
||||
* N-1 : même jour de semaine (52 semaines = 364 jours avant hier).
|
||||
*/
|
||||
export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const dateHier = yesterday.toISOString().split("T")[0];
|
||||
|
||||
// Même jour de semaine N-1 : 364 jours = 52 semaines exactes (conserve le jour)
|
||||
const n1Date = new Date(yesterday);
|
||||
n1Date.setDate(n1Date.getDate() - 364);
|
||||
const dateN1Param = n1Date.toISOString().split("T")[0];
|
||||
|
||||
const apiBase = "https://api.ffnancy.fr/api/performance";
|
||||
|
||||
// 2 appels API en parallèle : dashboard (hier) + évolution mensuelle (si endpoint dispo)
|
||||
const [apiResult, evoResult] = await Promise.all([
|
||||
fetch(`${apiBase}/dashboard?date=${dateHier}`, { cache: "no-store" })
|
||||
.then(r => { if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`); return r.json() as Promise<ApiPerfDashboard>; }),
|
||||
// Évolution mensuelle — endpoint séparé (à confirmer), fallback à null
|
||||
fetch(`${apiBase}/evolution`, { cache: "no-store" })
|
||||
.then(r => r.ok ? r.json() as Promise<{ months: DashboardMonthStats[] }> : null)
|
||||
.catch(() => null),
|
||||
]);
|
||||
const apiResult = await fetch(
|
||||
`${apiBase}/dashboard?date=${dateHier}&date_n1=${dateN1Param}`,
|
||||
{ cache: "no-store" }
|
||||
).then(r => {
|
||||
if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`);
|
||||
return r.json() as Promise<ApiPerfDashboard>;
|
||||
});
|
||||
|
||||
const sites: DashboardSiteStats[] = (apiResult.sites ?? []).map(s => ({
|
||||
site: s.site,
|
||||
@@ -559,40 +561,47 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
tickets_hier: Number(s.trafic) || 0,
|
||||
tickets_n1: Number(s.trafic_n1) || 0,
|
||||
qte_hier: 0,
|
||||
marge_hier: 0, // non disponible dans l'API — affichera 0
|
||||
marge_hier: 0,
|
||||
lignes_hier: 0,
|
||||
}));
|
||||
|
||||
// Top 10 réseau : agréger les N lignes par site → top 10 global
|
||||
const mapTop = (items: ApiPerfTopItem[], sortKey: "ca" | "qte" | "marge"): DashboardTopItem[] => {
|
||||
const byCodein = new Map<string, DashboardTopItem>();
|
||||
for (const i of items) {
|
||||
const existing = byCodein.get(i.codein);
|
||||
if (existing) {
|
||||
existing.ca += Number(i.ca_ttc) || 0;
|
||||
existing.qte += Number(i.qte_vendue) || 0;
|
||||
existing.marge += Number(i.marge) || 0;
|
||||
} else {
|
||||
byCodein.set(i.codein, {
|
||||
codein: i.codein,
|
||||
libelle1: i.libelle1,
|
||||
ca: Number(i.ca_ttc) || 0,
|
||||
qte: Number(i.qte_vendue) || 0,
|
||||
marge: Number(i.marge) || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...byCodein.values()].sort((a, b) => b[sortKey] - a[sortKey]).slice(0, 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);
|
||||
|
||||
console.log(`[pg-ff] Dashboard API: ${sites.length} sites, date=${apiResult.date}`);
|
||||
const allItems = [
|
||||
...(apiResult.top10_ca ?? []),
|
||||
...(apiResult.top10_qte ?? []),
|
||||
...(apiResult.top10_marge ?? []),
|
||||
];
|
||||
const siteSet = [...new Set(allItems.map(i => i.site).filter(Boolean))].sort();
|
||||
|
||||
const top10BySite: DashboardSiteTop10[] = siteSet.map(site => {
|
||||
const byS = (arr: ApiPerfTopItem[]) => arr.filter(i => i.site === site);
|
||||
return {
|
||||
site,
|
||||
ca: buildTop10(byS(apiResult.top10_ca ?? []), "ca"),
|
||||
qte: buildTop10(byS(apiResult.top10_qte ?? []), "qte"),
|
||||
marge: buildTop10(byS(apiResult.top10_marge ?? []), "marge"),
|
||||
};
|
||||
});
|
||||
|
||||
const dateN1 = apiResult.date_n1 ?? dateN1Param;
|
||||
console.log(`[pg-ff] Dashboard API: ${sites.length} sites, date=${apiResult.date}, date_n1=${dateN1}`);
|
||||
|
||||
return {
|
||||
dateHier: apiResult.date ?? dateHier,
|
||||
dateN1,
|
||||
sites,
|
||||
top10Ca: mapTop(apiResult.top10_ca ?? [], "ca"),
|
||||
top10Qte: mapTop(apiResult.top10_qte ?? [], "qte"),
|
||||
top10Marge: mapTop(apiResult.top10_marge ?? [], "marge"),
|
||||
evolution: evoResult?.months ?? [],
|
||||
top10BySite,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user