mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-20 05:28:11 +02:00
feat: implement PostgreSQL-based bulk data retrieval client for FF Nancy to replace HTTP API calls
This commit is contained in:
1 parent
a4debe992b
commit
659019e722
3 files changed
+71
-3
No files matched your search
@@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import type { HitParadePivotRow } from "./page";
|
import type { HitParadePivotRow } from "./page";
|
||||||
|
|
||||||
type SortKey = "caTotal" | "ca292" | "ca579" | "qteTotal" | "qte292" | "qte579";
|
type SortKey = "caTotal" | "ca292" | "ca579" | "qteTotal" | "qte292" | "qte579" | "stockTotal" | "stock292" | "stock579";
|
||||||
type SortDir = "desc" | "asc";
|
type SortDir = "desc" | "asc";
|
||||||
|
|
||||||
const DEFAULT_SORT: SortKey = "caTotal";
|
const DEFAULT_SORT: SortKey = "caTotal";
|
||||||
@@ -82,8 +82,9 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
|||||||
qte292: acc.qte292 + r.qte292, ca292: acc.ca292 + r.ca292, marge292: acc.marge292 + r.marge292,
|
qte292: acc.qte292 + r.qte292, ca292: acc.ca292 + r.ca292, marge292: acc.marge292 + r.marge292,
|
||||||
qte579: acc.qte579 + r.qte579, ca579: acc.ca579 + r.ca579, marge579: acc.marge579 + r.marge579,
|
qte579: acc.qte579 + r.qte579, ca579: acc.ca579 + r.ca579, marge579: acc.marge579 + r.marge579,
|
||||||
qteTotal: acc.qteTotal + r.qteTotal, caTotal: acc.caTotal + r.caTotal, margeTotal: acc.margeTotal + r.margeTotal,
|
qteTotal: acc.qteTotal + r.qteTotal, caTotal: acc.caTotal + r.caTotal, margeTotal: acc.margeTotal + r.margeTotal,
|
||||||
|
stock292: acc.stock292 + r.stock292, stock579: acc.stock579 + r.stock579, stockTotal: acc.stockTotal + r.stockTotal,
|
||||||
}),
|
}),
|
||||||
{ qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0 }
|
{ qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0, stock292: 0, stock579: 0, stockTotal: 0 }
|
||||||
), [sorted]);
|
), [sorted]);
|
||||||
|
|
||||||
function ColHeader({ label, sortable, k }: { label: string; sortable?: SortKey; k: string }) {
|
function ColHeader({ label, sortable, k }: { label: string; sortable?: SortKey; k: string }) {
|
||||||
@@ -189,6 +190,9 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
|||||||
<th colSpan={3} className="border-b border-l-2 border-emerald-200 bg-emerald-50 px-4 py-2 text-center text-xs font-bold text-emerald-700 tracking-wide">
|
<th colSpan={3} className="border-b border-l-2 border-emerald-200 bg-emerald-50 px-4 py-2 text-center text-xs font-bold text-emerald-700 tracking-wide">
|
||||||
Total Réseau
|
Total Réseau
|
||||||
</th>
|
</th>
|
||||||
|
<th colSpan={3} className="border-b border-l-2 border-amber-200 bg-amber-50 px-4 py-2 text-center text-xs font-bold text-amber-700 tracking-wide">
|
||||||
|
Stock actuel
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b-2 border-gray-200">
|
<tr className="border-b-2 border-gray-200">
|
||||||
<ColHeader label="Qté" sortable="qte292" k="qte292" />
|
<ColHeader label="Qté" sortable="qte292" k="qte292" />
|
||||||
@@ -200,6 +204,9 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
|||||||
<ColHeader label="Qté" sortable="qteTotal" k="qteTotal" />
|
<ColHeader label="Qté" sortable="qteTotal" k="qteTotal" />
|
||||||
<ColHeader label="CA TTC" sortable="caTotal" k="caTotal" />
|
<ColHeader label="CA TTC" sortable="caTotal" k="caTotal" />
|
||||||
<th className="bg-emerald-50/40 px-4 py-2 text-right text-xs font-semibold text-emerald-600">% Marge</th>
|
<th className="bg-emerald-50/40 px-4 py-2 text-right text-xs font-semibold text-emerald-600">% Marge</th>
|
||||||
|
<ColHeader label="292" sortable="stock292" k="stock292" />
|
||||||
|
<ColHeader label="579" sortable="stock579" k="stock579" />
|
||||||
|
<ColHeader label="Total" sortable="stockTotal" k="stockTotal" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -235,6 +242,15 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
|||||||
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-emerald-600">
|
<td className="px-4 py-2.5 text-right text-xs tabular-nums text-emerald-600">
|
||||||
{row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""}
|
{row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""}
|
||||||
</td>
|
</td>
|
||||||
|
<td className={`border-l-2 border-amber-100 px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "stock292" ? "bg-amber-50/50 font-semibold" : ""}`}>
|
||||||
|
{row.stock292 > 0 ? formatQte(row.stock292) : <span className="text-gray-300">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className={`px-4 py-2.5 text-right tabular-nums text-gray-700 ${sortKey === "stock579" ? "bg-amber-50/50 font-semibold" : ""}`}>
|
||||||
|
{row.stock579 > 0 ? formatQte(row.stock579) : <span className="text-gray-300">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className={`px-4 py-2.5 text-right tabular-nums font-semibold ${sortKey === "stockTotal" ? "bg-amber-50/50" : ""} ${row.stockTotal <= 0 ? "text-red-500" : "text-amber-700"}`}>
|
||||||
|
{row.stockTotal > 0 ? formatQte(row.stockTotal) : <span className="text-red-400 font-semibold">0</span>}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -252,6 +268,9 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
|
|||||||
<td className="border-l-2 border-emerald-200 px-4 py-3 text-right font-bold tabular-nums text-gray-900">{formatQte(totals.qteTotal)}</td>
|
<td className="border-l-2 border-emerald-200 px-4 py-3 text-right font-bold tabular-nums text-gray-900">{formatQte(totals.qteTotal)}</td>
|
||||||
<td className="px-4 py-3 text-right font-bold tabular-nums text-gray-900">{formatCA(totals.caTotal)}</td>
|
<td className="px-4 py-3 text-right font-bold tabular-nums text-gray-900">{formatCA(totals.caTotal)}</td>
|
||||||
<td className="px-4 py-3 text-right text-xs tabular-nums text-emerald-700 font-semibold">{formatPct(totals.caTotal, totals.margeTotal)}</td>
|
<td className="px-4 py-3 text-right text-xs tabular-nums text-emerald-700 font-semibold">{formatPct(totals.caTotal, totals.margeTotal)}</td>
|
||||||
|
<td className="border-l-2 border-amber-200 px-4 py-3 text-right font-bold tabular-nums text-amber-700">{formatQte(totals.stock292)}</td>
|
||||||
|
<td className="px-4 py-3 text-right font-bold tabular-nums text-amber-700">{formatQte(totals.stock579)}</td>
|
||||||
|
<td className="px-4 py-3 text-right font-bold tabular-nums text-amber-700">{formatQte(totals.stockTotal)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pgGetHitParade } from "@/lib/pg-ff-client";
|
import { pgGetHitParade, pgGetStockForCodeins } from "@/lib/pg-ff-client";
|
||||||
import { HitParadeClient } from "./client";
|
import { HitParadeClient } from "./client";
|
||||||
|
|
||||||
export interface HitParadePivotRow {
|
export interface HitParadePivotRow {
|
||||||
@@ -14,6 +14,9 @@ export interface HitParadePivotRow {
|
|||||||
qteTotal: number;
|
qteTotal: number;
|
||||||
caTotal: number;
|
caTotal: number;
|
||||||
margeTotal: number;
|
margeTotal: number;
|
||||||
|
stock292: number;
|
||||||
|
stock579: number;
|
||||||
|
stockTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pivotHitParade(
|
function pivotHitParade(
|
||||||
@@ -30,6 +33,7 @@ function pivotHitParade(
|
|||||||
qte292: 0, ca292: 0, marge292: 0,
|
qte292: 0, ca292: 0, marge292: 0,
|
||||||
qte579: 0, ca579: 0, marge579: 0,
|
qte579: 0, ca579: 0, marge579: 0,
|
||||||
qteTotal: 0, caTotal: 0, margeTotal: 0,
|
qteTotal: 0, caTotal: 0, margeTotal: 0,
|
||||||
|
stock292: 0, stock579: 0, stockTotal: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const entry = map.get(row.codein)!;
|
const entry = map.get(row.codein)!;
|
||||||
@@ -71,6 +75,13 @@ export default async function HitParadePage(props: {
|
|||||||
const rows = await pgGetHitParade(dateDebut, dateFin);
|
const rows = await pgGetHitParade(dateDebut, dateFin);
|
||||||
const pivotted = pivotHitParade(rows);
|
const pivotted = pivotHitParade(rows);
|
||||||
|
|
||||||
|
const codeins = pivotted.map(r => r.codein);
|
||||||
|
const stockMap = await pgGetStockForCodeins(codeins);
|
||||||
|
for (const row of pivotted) {
|
||||||
|
const s = stockMap.get(row.codein);
|
||||||
|
if (s) { row.stock292 = s.stock292; row.stock579 = s.stock579; row.stockTotal = s.stockTotal; }
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
<div className="mx-auto max-w-screen-2xl">
|
<div className="mx-auto max-w-screen-2xl">
|
||||||
|
|||||||
@@ -735,3 +735,41 @@ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promis
|
|||||||
console.log(`[pg-ff] HitParade: ${result.rows.length} lignes pour ${dateDebut}→${dateFin}`);
|
console.log(`[pg-ff] HitParade: ${result.rows.length} lignes pour ${dateDebut}→${dateFin}`);
|
||||||
return result.rows as unknown as HitParadeRow[];
|
return result.rows as unknown as HitParadeRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stock par liste de codeins (pour Hit Parade)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface StockBySite {
|
||||||
|
stock292: number;
|
||||||
|
stock579: number;
|
||||||
|
stockTotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le stock actuel (stockdispo) par codein pour une liste de codeins.
|
||||||
|
* 1 seule requête bulk — utilisé pour enrichir le Hit Parade.
|
||||||
|
*/
|
||||||
|
export async function pgGetStockForCodeins(codeins: string[]): Promise<Map<string, StockBySite>> {
|
||||||
|
if (codeins.length === 0) return new Map();
|
||||||
|
|
||||||
|
const result = await pgNoParallel(sql`
|
||||||
|
SELECT
|
||||||
|
a.codein,
|
||||||
|
cs.site,
|
||||||
|
cs.stockdispo::float AS stockdispo
|
||||||
|
FROM cube_stock cs
|
||||||
|
JOIN articles a ON a.no_id = cs.artnoid
|
||||||
|
WHERE a.codein = ANY(${codeins})
|
||||||
|
`);
|
||||||
|
|
||||||
|
const map = new Map<string, StockBySite>();
|
||||||
|
for (const r of result.rows as Array<{ codein: string; site: string; stockdispo: number }>) {
|
||||||
|
if (!map.has(r.codein)) map.set(r.codein, { stock292: 0, stock579: 0, stockTotal: 0 });
|
||||||
|
const entry = map.get(r.codein)!;
|
||||||
|
if (r.site === "292") entry.stock292 += r.stockdispo;
|
||||||
|
else if (r.site === "579") entry.stock579 += r.stockdispo;
|
||||||
|
entry.stockTotal += r.stockdispo;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user