mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: implement PostgreSQL client for FF Nancy data, replacing per-article HTTP calls with bulk SQL queries.
This commit is contained in:
1 parent
d5a422e738
commit
7cb96966aa
2 files changed
+99
-44
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { pgGetDashboardData, type DashboardTopItem, type DashboardSiteStats, type DashboardMonthStats } from "@/lib/pg-ff-client";
|
||||
import { TrendingUp, TrendingDown, Minus, Store, ShoppingCart, Euro, Percent } from "lucide-react";
|
||||
import { TrendingUp, TrendingDown, Minus, Store, ShoppingCart, Euro, Percent, Users } from "lucide-react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -80,8 +80,8 @@ 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_d7);
|
||||
const dQte = delta(site.qte_hier, site.qte_d7);
|
||||
const dCa = delta(site.ca_hier, site.ca_n1);
|
||||
const dTickets = delta(site.tickets_hier, site.tickets_n1);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl p-5 flex flex-col gap-4" style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)" }}>
|
||||
@@ -94,12 +94,18 @@ function SiteCard({ site }: { site: DashboardSiteStats }) {
|
||||
<div>
|
||||
<div className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>CA</div>
|
||||
<div className="text-lg font-bold" style={{ color: "var(--text-primary)" }}>{fmtEur(site.ca_hier)}</div>
|
||||
<DeltaBadge pct={dCa} />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DeltaBadge pct={dCa} />
|
||||
{site.ca_n1 > 0 && <span className="text-xs" style={{ color: "var(--text-muted)" }}>N-1 {fmtEur(site.ca_n1)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>Quantité</div>
|
||||
<div className="text-lg font-bold" style={{ color: "var(--text-primary)" }}>{fmtQte(site.qte_hier)}</div>
|
||||
<DeltaBadge pct={dQte} />
|
||||
<div className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>Trafic (tickets)</div>
|
||||
<div className="text-lg font-bold" style={{ color: "var(--text-primary)" }}>{fmtQte(site.tickets_hier)}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DeltaBadge pct={dTickets} />
|
||||
{site.tickets_n1 > 0 && <span className="text-xs" style={{ color: "var(--text-muted)" }}>N-1 {fmtQte(site.tickets_n1)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>Marge</div>
|
||||
@@ -113,7 +119,7 @@ function SiteCard({ site }: { site: DashboardSiteStats }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs pt-2" style={{ color: "var(--text-muted)", borderTop: "1px solid var(--border)" }}>
|
||||
{fmtQte(site.lignes_hier)} lignes vendues
|
||||
{fmtQte(site.qte_hier)} articles · {fmtQte(site.lignes_hier)} lignes
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -307,14 +313,13 @@ export default async function DashboardPage() {
|
||||
|
||||
// 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);
|
||||
const totalQte = sites.reduce((s, r) => s + r.qte_hier, 0);
|
||||
const totalMarge = sites.reduce((s, r) => s + r.marge_hier, 0);
|
||||
const txMarge = totalCa > 0 ? (totalMarge / totalCa) * 100 : 0;
|
||||
const totalLignes = sites.reduce((s, r) => s + r.lignes_hier, 0);
|
||||
const totalCaD7 = sites.reduce((s, r) => s + r.ca_d7, 0);
|
||||
const totalQteD7 = sites.reduce((s, r) => s + r.qte_d7, 0);
|
||||
const totalMargeD7 = sites.reduce((s, r) => s + r.marge_d7, 0);
|
||||
const txMargeD7 = totalCaD7 > 0 ? (totalMargeD7 / totalCaD7) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 pb-12 max-w-screen-2xl mx-auto">
|
||||
@@ -325,7 +330,7 @@ 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 semaine précédente (J-7)
|
||||
{" "}· Comparaison vs même date N-1
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -334,29 +339,26 @@ export default async function DashboardPage() {
|
||||
<KpiCard
|
||||
label="CA Réseau"
|
||||
value={fmtEur(totalCa)}
|
||||
delta={delta(totalCa, totalCaD7)}
|
||||
sub={`J-7 : ${fmtEur(totalCaD7)}`}
|
||||
delta={delta(totalCa, totalCaN1)}
|
||||
sub={`N-1 : ${fmtEur(totalCaN1)}`}
|
||||
icon={Euro}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Quantité vendue"
|
||||
value={fmtQte(totalQte)}
|
||||
delta={delta(totalQte, totalQteD7)}
|
||||
sub={`J-7 : ${fmtQte(totalQteD7)}`}
|
||||
icon={ShoppingCart}
|
||||
label="Trafic (tickets)"
|
||||
value={fmtQte(totalTickets)}
|
||||
delta={delta(totalTickets, totalTicketsN1)}
|
||||
sub={`N-1 : ${fmtQte(totalTicketsN1)}`}
|
||||
icon={Users}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Marge Brute"
|
||||
value={fmtEur(totalMarge)}
|
||||
delta={delta(totalMarge, totalMargeD7)}
|
||||
sub={`J-7 : ${fmtEur(totalMargeD7)}`}
|
||||
sub={`${fmtQte(totalQte)} articles · ${fmtQte(totalLignes)} lignes`}
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Taux de Marge"
|
||||
value={fmtPct(txMarge, false)}
|
||||
delta={txMarge - txMargeD7}
|
||||
sub={`J-7 : ${fmtPct(txMargeD7, false)} · ${fmtQte(totalLignes)} lignes`}
|
||||
icon={Percent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+73
-20
@@ -469,12 +469,12 @@ export async function pgGetCommandesByFournisseur(codefou: string): Promise<Map<
|
||||
export interface DashboardSiteStats {
|
||||
site: string;
|
||||
ca_hier: number;
|
||||
ca_n1: number; // même date N-1 (statopcajour)
|
||||
tickets_hier: number; // nb tickets hier (statopcajour)
|
||||
tickets_n1: number; // nb tickets même date N-1 (statopcajour)
|
||||
qte_hier: number;
|
||||
marge_hier: number;
|
||||
lignes_hier: number;
|
||||
ca_d7: number;
|
||||
qte_d7: number;
|
||||
marge_d7: number;
|
||||
}
|
||||
|
||||
export interface DashboardTopItem {
|
||||
@@ -503,34 +503,52 @@ export interface DashboardData {
|
||||
|
||||
/**
|
||||
* Retourne toutes les données nécessaires pour le dashboard quotidien.
|
||||
* 5 requêtes SQL en parallèle — données réseau (sites 292 + 579).
|
||||
* 6 requêtes SQL en parallèle — données réseau (sites 292 + 579).
|
||||
*
|
||||
* CA + Trafic : statopcajour (précalculé, N-1 même date réelle)
|
||||
* Top 10 + Évolution : mvtart avec cumstat = 1 (mouvements statistiques)
|
||||
*/
|
||||
export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const dateHier = yesterday.toISOString().split("T")[0];
|
||||
|
||||
const [sitesResult, top10CaResult, top10QteResult, top10MargeResult, evolutionResult] =
|
||||
const [caTicketsResult, mvtSitesResult, top10CaResult, top10QteResult, top10MargeResult, evolutionResult] =
|
||||
await Promise.all([
|
||||
// Stats hier + même jour S-1 par magasin (1 seule requête avec FILTER)
|
||||
// CA + Tickets par site depuis statopcajour : hier vs même date N-1
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
s.site,
|
||||
SUM(CASE WHEN s.datmvt = CURRENT_DATE - 1
|
||||
THEN s.mnt ELSE 0 END)::float AS ca_hier,
|
||||
SUM(CASE WHEN s.datmvt = (CURRENT_DATE - 1 - INTERVAL '1 year')
|
||||
THEN s.mnt ELSE 0 END)::float AS ca_n1,
|
||||
SUM(CASE WHEN s.datmvt = CURRENT_DATE - 1
|
||||
THEN s.nbticket ELSE 0 END)::int AS tickets_hier,
|
||||
SUM(CASE WHEN s.datmvt = (CURRENT_DATE - 1 - INTERVAL '1 year')
|
||||
THEN s.nbticket ELSE 0 END)::int AS tickets_n1
|
||||
FROM statopcajour s
|
||||
WHERE s.datmvt IN (CURRENT_DATE - 1, CURRENT_DATE - 1 - INTERVAL '1 year')
|
||||
AND s.site IN ('292', '579')
|
||||
GROUP BY s.site
|
||||
ORDER BY s.site
|
||||
`),
|
||||
// QTE + Marge + Lignes par site depuis mvtart (cumstat = 1)
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
m.site,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 1 THEN -m.mntmvtttc ELSE 0 END)::float AS ca_hier,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 1 THEN -m.qtemvt ELSE 0 END)::float AS qte_hier,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 1 THEN m.margemvt ELSE 0 END)::float AS marge_hier,
|
||||
COUNT(CASE WHEN m.datmvt = CURRENT_DATE - 1 THEN 1 END)::int AS lignes_hier,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 7 THEN -m.mntmvtttc ELSE 0 END)::float AS ca_d7,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 7 THEN -m.qtemvt ELSE 0 END)::float AS qte_d7,
|
||||
SUM(CASE WHEN m.datmvt = CURRENT_DATE - 7 THEN m.margemvt ELSE 0 END)::float AS marge_d7
|
||||
SUM(-m.qtemvt)::float AS qte_hier,
|
||||
SUM(m.margemvt)::float AS marge_hier,
|
||||
COUNT(*)::int AS lignes_hier
|
||||
FROM mvtart m
|
||||
WHERE m.datmvt IN (CURRENT_DATE - 1, CURRENT_DATE - 7)
|
||||
WHERE m.datmvt = CURRENT_DATE - 1
|
||||
AND m.genremvt = 3
|
||||
AND m.cumstat = 1
|
||||
AND m.site IN ('292', '579')
|
||||
GROUP BY m.site
|
||||
ORDER BY m.site
|
||||
`),
|
||||
// Top 10 CA réseau hier
|
||||
// Top 10 CA réseau hier (cumstat = 1)
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
a.codein,
|
||||
@@ -542,12 +560,13 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
JOIN articles a ON a.no_id = m.artnoid
|
||||
WHERE m.datmvt = CURRENT_DATE - 1
|
||||
AND m.genremvt = 3
|
||||
AND m.cumstat = 1
|
||||
AND m.site IN ('292', '579')
|
||||
GROUP BY a.codein, a.libelle1
|
||||
ORDER BY ca DESC
|
||||
LIMIT 10
|
||||
`),
|
||||
// Top 10 QTE réseau hier
|
||||
// Top 10 QTE réseau hier (cumstat = 1)
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
a.codein,
|
||||
@@ -559,12 +578,13 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
JOIN articles a ON a.no_id = m.artnoid
|
||||
WHERE m.datmvt = CURRENT_DATE - 1
|
||||
AND m.genremvt = 3
|
||||
AND m.cumstat = 1
|
||||
AND m.site IN ('292', '579')
|
||||
GROUP BY a.codein, a.libelle1
|
||||
ORDER BY qte DESC
|
||||
LIMIT 10
|
||||
`),
|
||||
// Top 10 Marge réseau hier
|
||||
// Top 10 Marge réseau hier (cumstat = 1)
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
a.codein,
|
||||
@@ -576,12 +596,13 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
JOIN articles a ON a.no_id = m.artnoid
|
||||
WHERE m.datmvt = CURRENT_DATE - 1
|
||||
AND m.genremvt = 3
|
||||
AND m.cumstat = 1
|
||||
AND m.site IN ('292', '579')
|
||||
GROUP BY a.codein, a.libelle1
|
||||
ORDER BY marge DESC
|
||||
LIMIT 10
|
||||
`),
|
||||
// Évolution mensuelle sur 25 mois (N et N-1)
|
||||
// Évolution mensuelle sur 25 mois (cumstat = 1)
|
||||
pgNoParallel(sql`
|
||||
SELECT
|
||||
TO_CHAR(m.datmvt, 'YYYY-MM') AS mois,
|
||||
@@ -592,17 +613,49 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
WHERE m.datmvt >= (CURRENT_DATE - INTERVAL '25 months')
|
||||
AND m.datmvt < CURRENT_DATE
|
||||
AND m.genremvt = 3
|
||||
AND m.cumstat = 1
|
||||
AND m.site IN ('292', '579')
|
||||
GROUP BY TO_CHAR(m.datmvt, 'YYYY-MM')
|
||||
ORDER BY mois
|
||||
`),
|
||||
]);
|
||||
|
||||
console.log(`[pg-ff] Dashboard: ${sitesResult.rows.length} sites, ${evolutionResult.rows.length} mois d'évolution`);
|
||||
// Fusionner CA+tickets (statopcajour) avec QTE+marge (mvtart) par site
|
||||
type CaTickets = { ca_hier: number; ca_n1: number; tickets_hier: number; tickets_n1: number };
|
||||
const caMap = new Map<string, CaTickets>();
|
||||
for (const row of caTicketsResult.rows as unknown as (CaTickets & { site: string })[]) {
|
||||
caMap.set(row.site, {
|
||||
ca_hier: Number(row.ca_hier) || 0,
|
||||
ca_n1: Number(row.ca_n1) || 0,
|
||||
tickets_hier: Number(row.tickets_hier) || 0,
|
||||
tickets_n1: Number(row.tickets_n1) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
const sitesMap = new Map<string, DashboardSiteStats>();
|
||||
for (const row of mvtSitesResult.rows as unknown as { site: string; qte_hier: number; marge_hier: number; lignes_hier: number }[]) {
|
||||
const caData = caMap.get(row.site) ?? { ca_hier: 0, ca_n1: 0, tickets_hier: 0, tickets_n1: 0 };
|
||||
sitesMap.set(row.site, {
|
||||
site: row.site,
|
||||
...caData,
|
||||
qte_hier: Number(row.qte_hier) || 0,
|
||||
marge_hier: Number(row.marge_hier) || 0,
|
||||
lignes_hier: Number(row.lignes_hier) || 0,
|
||||
});
|
||||
}
|
||||
// Sites présents dans statopcajour mais pas dans mvtart
|
||||
for (const [site, caData] of caMap.entries()) {
|
||||
if (!sitesMap.has(site)) {
|
||||
sitesMap.set(site, { site, ...caData, qte_hier: 0, marge_hier: 0, lignes_hier: 0 });
|
||||
}
|
||||
}
|
||||
const sites = [...sitesMap.values()].sort((a, b) => a.site.localeCompare(b.site));
|
||||
|
||||
console.log(`[pg-ff] Dashboard: ${sites.length} sites, ${evolutionResult.rows.length} mois d'évolution`);
|
||||
|
||||
return {
|
||||
dateHier,
|
||||
sites: sitesResult.rows as unknown as DashboardSiteStats[],
|
||||
sites,
|
||||
top10Ca: top10CaResult.rows as unknown as DashboardTopItem[],
|
||||
top10Qte: top10QteResult.rows as unknown as DashboardTopItem[],
|
||||
top10Marge: top10MargeResult.rows as unknown as DashboardTopItem[],
|
||||
|
||||
Reference in new issue
Block a user