mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 22:18:08 +02:00
feat: implement product grid with comprehensive product data fetching, processing, and heatmap display.
This commit is contained in:
1 parent
2b7e7bc1f4
commit
8b7d81b20c
4 files changed
+127
-1
No files matched your search
@@ -7,6 +7,7 @@ import {
|
|||||||
getMensuelByArticles,
|
getMensuelByArticles,
|
||||||
getReferentielByArticles,
|
getReferentielByArticles,
|
||||||
getCommandesByFournisseur,
|
getCommandesByFournisseur,
|
||||||
|
getRankingByArticles,
|
||||||
buildLast12MonthsRange,
|
buildLast12MonthsRange,
|
||||||
} from "@/lib/api-ff-client";
|
} from "@/lib/api-ff-client";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
@@ -30,10 +31,11 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
|
|
||||||
// ─── Phase 2 : Mensuel + Commandes (en parallèle) ────────────────────
|
// ─── Phase 2 : Mensuel + Commandes (en parallèle) ────────────────────
|
||||||
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
||||||
const [mensuelMap, referentielMap, commandesMap] = await Promise.all([
|
const [mensuelMap, referentielMap, commandesMap, rankingMap] = await Promise.all([
|
||||||
getMensuelByArticles(articles, dateDebut, dateFin),
|
getMensuelByArticles(articles, dateDebut, dateFin),
|
||||||
getReferentielByArticles(articles),
|
getReferentielByArticles(articles),
|
||||||
getCommandesByFournisseur(codeFournisseur),
|
getCommandesByFournisseur(codeFournisseur),
|
||||||
|
getRankingByArticles(articles),
|
||||||
]);
|
]);
|
||||||
console.log(`[getProductRows] mensuel data for ${mensuelMap.size}/${articles.length} articles`);
|
console.log(`[getProductRows] mensuel data for ${mensuelMap.size}/${articles.length} articles`);
|
||||||
|
|
||||||
@@ -220,6 +222,16 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
}
|
}
|
||||||
console.log(`[getProductRows] Referentiel: ${refCount}/${articles.length} articles enrichis`);
|
console.log(`[getProductRows] Referentiel: ${refCount}/${articles.length} articles enrichis`);
|
||||||
|
|
||||||
|
// ─── Phase 6b : Ranking réseau + magasin ────────────────────────────
|
||||||
|
for (const [codein, ranking] of rankingMap.entries()) {
|
||||||
|
const product = productMap.get(codein);
|
||||||
|
if (!product) continue;
|
||||||
|
product.rankingCa = ranking.ranking_ca;
|
||||||
|
product.rankingQte = ranking.ranking_qte;
|
||||||
|
product.rankingMagCa = ranking.ranking_mag_ca;
|
||||||
|
product.rankingMagQte = ranking.ranking_mag_qte;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Phase 7 : Restaurer gammes depuis dernier snapshot ──────────────
|
// ─── Phase 7 : Restaurer gammes depuis dernier snapshot ──────────────
|
||||||
try {
|
try {
|
||||||
const snaps = await db
|
const snaps = await db
|
||||||
|
|||||||
@@ -399,6 +399,58 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "rankingCa",
|
||||||
|
header: () => <div className="text-center w-full">Rk CA<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||||
|
size: 70,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue<number | undefined>();
|
||||||
|
return (
|
||||||
|
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{val != null ? `#${val}` : "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "rankingQte",
|
||||||
|
header: () => <div className="text-center w-full">Rk Qté<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||||
|
size: 70,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue<number | undefined>();
|
||||||
|
return (
|
||||||
|
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{val != null ? `#${val}` : "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "rankingMagCa",
|
||||||
|
header: () => <div className="text-center w-full">Rk CA<br/><span className="text-[9px] opacity-60">Mag.</span></div>,
|
||||||
|
size: 70,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue<number | undefined>();
|
||||||
|
return (
|
||||||
|
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{val != null ? `#${val}` : "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "rankingMagQte",
|
||||||
|
header: () => <div className="text-center w-full">Rk Qté<br/><span className="text-[9px] opacity-60">Mag.</span></div>,
|
||||||
|
size: 70,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue<number | undefined>();
|
||||||
|
return (
|
||||||
|
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{val != null ? `#${val}` : "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
...MONTHS_12.map((monthKey) => ({
|
...MONTHS_12.map((monthKey) => ({
|
||||||
id: `month_${monthKey}`,
|
id: `month_${monthKey}`,
|
||||||
header: () => <div className="text-center w-full">{formatMonthLabel(monthKey)}</div>,
|
header: () => <div className="text-center w-full">{formatMonthLabel(monthKey)}</div>,
|
||||||
|
|||||||
@@ -419,6 +419,62 @@ export async function getCommandesByFournisseur(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ranking — classement réseau et magasin
|
||||||
|
// GET /api/ranking?gencod=<gtin>
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface FfRanking {
|
||||||
|
ranking_ca?: number;
|
||||||
|
ranking_qte?: number;
|
||||||
|
ranking_mag_ca?: number;
|
||||||
|
ranking_mag_qte?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le ranking (réseau + magasin) pour un lot d'articles via leur GTIN (EAN).
|
||||||
|
* Retourne Map<codein, FfRanking>
|
||||||
|
*/
|
||||||
|
export async function getRankingByArticles(
|
||||||
|
articles: { codein: string; gtin?: string }[],
|
||||||
|
batchSize = 20
|
||||||
|
): Promise<Map<string, FfRanking>> {
|
||||||
|
const result = new Map<string, FfRanking>();
|
||||||
|
|
||||||
|
const withGtin = articles.filter(a => a.gtin && a.gtin.trim().length > 0);
|
||||||
|
if (withGtin.length === 0) return result;
|
||||||
|
|
||||||
|
for (let i = 0; i < withGtin.length; i += batchSize) {
|
||||||
|
const batch = withGtin.slice(i, i + batchSize);
|
||||||
|
await Promise.all(
|
||||||
|
batch.map(async (art) => {
|
||||||
|
try {
|
||||||
|
const url = `${FF_API_BASE}/api/ranking?gencod=${encodeURIComponent(art.gtin!.trim())}`;
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const items = Array.isArray(data) ? data : (data?.data ?? data?.items ?? [data]);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const entry: any = Array.isArray(items) ? items[0] : items;
|
||||||
|
if (!entry) return;
|
||||||
|
result.set(art.codein, {
|
||||||
|
ranking_ca: entry.ranking_ca != null ? Number(entry.ranking_ca) : undefined,
|
||||||
|
ranking_qte: entry.ranking_qte != null ? Number(entry.ranking_qte) : undefined,
|
||||||
|
ranking_mag_ca: entry.ranking_mag_ca != null ? Number(entry.ranking_mag_ca) : undefined,
|
||||||
|
ranking_mag_qte: entry.ranking_mag_qte != null ? Number(entry.ranking_mag_qte) : undefined,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[api-ff] getRanking error for ${art.codein} (gtin: ${art.gtin}):`, err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[api-ff] Ranking: ${result.size}/${withGtin.length} articles enrichis`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Statut de synchronisation
|
// Statut de synchronisation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ export interface ProductRow {
|
|||||||
shareQty?: number;
|
shareQty?: number;
|
||||||
/** Référentiels globaux */
|
/** Référentiels globaux */
|
||||||
totalFournisseurCa?: number;
|
totalFournisseurCa?: number;
|
||||||
|
/** Ranking réseau (classement global) */
|
||||||
|
rankingCa?: number;
|
||||||
|
rankingQte?: number;
|
||||||
|
/** Ranking magasin */
|
||||||
|
rankingMagCa?: number;
|
||||||
|
rankingMagQte?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Summary bar totals for the currently visible/filtered rows */
|
/** Summary bar totals for the currently visible/filtered rows */
|
||||||
|
|||||||
Reference in new issue
Block a user