mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 14:38:10 +02:00
feat: add analytics page with CA TTC comparison by supplier/nomenclature
- pg-ff-client: add pgGetCaByFournisseur() and pgGetCaByNomenclature() returning CA TTC per site and month for monthly N vs N-1 comparison - analytics/page.tsx: server component with pivot logic to reshape raw query data into store-separated columns (292/579/total) with evolution % - analytics/client.tsx: client controls (mode dropdown, month selector) and responsive table with color-coded evolution badges - sidebar: add "Analytics" link to navigation Features: - Switch between "Fournisseur" and "Nomenclature" views - Select any month for comparison vs same month prior year - Columns split by store: Frouard/Nancy (292), Houdemont (579), Total - Each store shows: CA this month, CA N-1, evolution % - Footer totals row matching sum of detail rows - FR locale currency formatting (€) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a30783285
commit
740aed3181
4 files changed
+483
No files matched your search
@@ -0,0 +1,252 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { BarChart2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface AnalyticsClientProps {
|
||||||
|
mode: string;
|
||||||
|
mois: string;
|
||||||
|
moisN1: string;
|
||||||
|
currentMois: string;
|
||||||
|
pivotted: Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
ca292: number;
|
||||||
|
caN1_292: number;
|
||||||
|
ca579: number;
|
||||||
|
caN1_579: number;
|
||||||
|
caTotal: number;
|
||||||
|
caN1Total: number;
|
||||||
|
evolutionTotal: number | null;
|
||||||
|
}>;
|
||||||
|
totals: {
|
||||||
|
ca292: number;
|
||||||
|
caN1_292: number;
|
||||||
|
ca579: number;
|
||||||
|
caN1_579: number;
|
||||||
|
caTotal: number;
|
||||||
|
caN1Total: number;
|
||||||
|
};
|
||||||
|
evolutionTotal292: number | null;
|
||||||
|
evolutionTotal579: number | null;
|
||||||
|
evolutionTotalReseau: number | null;
|
||||||
|
formatCA: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EvolutionBadge({ value }: { value: number | null }) {
|
||||||
|
if (value === null) return <span className="text-gray-400">N/A</span>;
|
||||||
|
|
||||||
|
const isPositive = value >= 0;
|
||||||
|
const bgColor = isPositive ? "bg-green-100" : "bg-red-100";
|
||||||
|
const textColor = isPositive ? "text-green-700" : "text-red-700";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`${bgColor} ${textColor} inline-block rounded px-2 py-1 text-sm font-semibold`}>
|
||||||
|
{isPositive ? "+" : ""}
|
||||||
|
{value.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnalyticsClient({
|
||||||
|
mode,
|
||||||
|
mois,
|
||||||
|
moisN1,
|
||||||
|
currentMois,
|
||||||
|
pivotted,
|
||||||
|
totals,
|
||||||
|
evolutionTotal292,
|
||||||
|
evolutionTotal579,
|
||||||
|
evolutionTotalReseau,
|
||||||
|
formatCA,
|
||||||
|
}: AnalyticsClientProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const handleModeChange = (newMode: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
params.set("mode", newMode);
|
||||||
|
params.set("mois", mois);
|
||||||
|
router.push(`/analytics?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthChange = (newMois: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
params.set("mode", mode);
|
||||||
|
params.set("mois", newMois);
|
||||||
|
router.push(`/analytics?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Générer liste des 24 derniers mois
|
||||||
|
const months: { value: string; label: string }[] = [];
|
||||||
|
const now = new Date();
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const d = new Date(now.getFullYear(), now.getMonth() - i);
|
||||||
|
const value = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
const label = new Intl.DateTimeFormat("fr-FR", { year: "numeric", month: "long" }).format(d);
|
||||||
|
months.push({ value, label: label.charAt(0).toUpperCase() + label.slice(1) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="flex items-center gap-6 rounded-lg bg-white p-4 shadow">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="font-semibold text-gray-700">Vue :</label>
|
||||||
|
<select
|
||||||
|
value={mode}
|
||||||
|
onChange={(e) => handleModeChange(e.target.value)}
|
||||||
|
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="fournisseur">Fournisseur</option>
|
||||||
|
<option value="nomenclature">Nomenclature</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="font-semibold text-gray-700">Mois :</label>
|
||||||
|
<select
|
||||||
|
value={mois}
|
||||||
|
onChange={(e) => handleMonthChange(e.target.value)}
|
||||||
|
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{months.map((m) => (
|
||||||
|
<option key={m.value} value={m.value}>
|
||||||
|
{m.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Comparaison N-1 : {moisN1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b-2 border-gray-200 bg-gray-50">
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
|
||||||
|
{mode === "fournisseur" ? "Fournisseur" : "Nomenclature"}
|
||||||
|
</th>
|
||||||
|
<th colSpan={3} className="px-4 py-3 text-center text-sm font-semibold text-gray-700">
|
||||||
|
Frouard/Nancy (292)
|
||||||
|
</th>
|
||||||
|
<th colSpan={3} className="px-4 py-3 text-center text-sm font-semibold text-gray-700">
|
||||||
|
Houdemont (579)
|
||||||
|
</th>
|
||||||
|
<th colSpan={3} className="px-4 py-3 text-center text-sm font-semibold text-gray-700">
|
||||||
|
Total Réseau
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-gray-200 bg-gray-50">
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600"></th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{mois}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{moisN1}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-600">
|
||||||
|
Évol.
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{mois}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{moisN1}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-600">
|
||||||
|
Évol.
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{mois}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-600">
|
||||||
|
{moisN1}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-600">
|
||||||
|
Évol.
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{pivotted.map((row, idx) => (
|
||||||
|
<tr
|
||||||
|
key={row.key}
|
||||||
|
className={idx % 2 === 0 ? "border-b border-gray-100 bg-white" : "border-b border-gray-100 bg-gray-50"}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm font-medium text-gray-900">{row.label}</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm text-gray-800">{formatCA(row.ca292)}</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm text-gray-800">{formatCA(row.caN1_292)}</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge
|
||||||
|
value={
|
||||||
|
row.caN1_292 > 0
|
||||||
|
? ((row.ca292 - row.caN1_292) / row.caN1_292) * 100
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm text-gray-800">{formatCA(row.ca579)}</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm text-gray-800">{formatCA(row.caN1_579)}</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge
|
||||||
|
value={
|
||||||
|
row.caN1_579 > 0
|
||||||
|
? ((row.ca579 - row.caN1_579) / row.caN1_579) * 100
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
|
||||||
|
{formatCA(row.caTotal)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
|
||||||
|
{formatCA(row.caN1Total)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge value={row.evolutionTotal} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-gray-300 bg-gray-50">
|
||||||
|
<td className="px-4 py-3 text-sm font-bold text-gray-900">TOTAL</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.ca292)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.caN1_292)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge value={evolutionTotal292} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.ca579)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.caN1_579)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge value={evolutionTotal579} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.caTotal)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900">
|
||||||
|
{formatCA(totals.caN1Total)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-sm">
|
||||||
|
<EvolutionBadge value={evolutionTotalReseau} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { pgGetCaByFournisseur, pgGetCaByNomenclature } from "@/lib/pg-ff-client";
|
||||||
|
import { AnalyticsClient } from "./client";
|
||||||
|
|
||||||
|
// Calcul du mois N-1 (même mois année précédente)
|
||||||
|
function getMoisN1(mois: string): string {
|
||||||
|
const [year, month] = mois.split("-").map(Number);
|
||||||
|
const d = new Date(year, month - 13); // -13 mois
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatage courant français
|
||||||
|
function formatCA(value: number): string {
|
||||||
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper pivot : transformer les lignes de requête en tableau pivotté par site
|
||||||
|
function pivotData(
|
||||||
|
rows: Array<{ code: string; label: string; site: string; mois: string; ca_ttc: number }>,
|
||||||
|
mois: string,
|
||||||
|
moisN1: string
|
||||||
|
): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
ca292: number;
|
||||||
|
caN1_292: number;
|
||||||
|
ca579: number;
|
||||||
|
caN1_579: number;
|
||||||
|
caTotal: number;
|
||||||
|
caN1Total: number;
|
||||||
|
evolutionTotal: number | null;
|
||||||
|
}> {
|
||||||
|
const map = new Map<string, { code: string; label: string; data: Record<string, number> }>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = `${row.code}|${row.label}`;
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { code: row.code, label: row.label, data: {} });
|
||||||
|
}
|
||||||
|
const entry = map.get(key)!;
|
||||||
|
entry.data[`${row.site}_${row.mois}`] = row.ca_ttc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Array.from(map.values()).map(({ code, label, data }) => {
|
||||||
|
const ca292 = data[`292_${mois}`] ?? 0;
|
||||||
|
const caN1_292 = data[`292_${moisN1}`] ?? 0;
|
||||||
|
const ca579 = data[`579_${mois}`] ?? 0;
|
||||||
|
const caN1_579 = data[`579_${moisN1}`] ?? 0;
|
||||||
|
const caTotal = ca292 + ca579;
|
||||||
|
const caN1Total = caN1_292 + caN1_579;
|
||||||
|
const evolutionTotal =
|
||||||
|
caN1Total > 0 ? ((caTotal - caN1Total) / caN1Total) * 100 : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: code,
|
||||||
|
label,
|
||||||
|
ca292,
|
||||||
|
caN1_292,
|
||||||
|
ca579,
|
||||||
|
caN1_579,
|
||||||
|
caTotal,
|
||||||
|
caN1Total,
|
||||||
|
evolutionTotal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trier par CA Total décroissant
|
||||||
|
result.sort((a, b) => b.caTotal - a.caTotal);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AnalyticsPage(props: {
|
||||||
|
searchParams: Promise<Record<string, string | string[]>>;
|
||||||
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const mode = (searchParams.mode as string) || "fournisseur";
|
||||||
|
const moisParam = (searchParams.mois as string) || "";
|
||||||
|
|
||||||
|
// Mois par défaut = mois courant (YYYY-MM)
|
||||||
|
const today = new Date();
|
||||||
|
const currentMois = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
const mois = moisParam || currentMois;
|
||||||
|
const moisN1 = getMoisN1(mois);
|
||||||
|
|
||||||
|
let rows: Array<{ code: string; label: string; site: string; mois: string; ca_ttc: number }> =
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (mode === "fournisseur") {
|
||||||
|
const result = await pgGetCaByFournisseur(mois, moisN1);
|
||||||
|
rows = result.map((r) => ({
|
||||||
|
code: r.code,
|
||||||
|
label: r.nom,
|
||||||
|
site: r.site,
|
||||||
|
mois: r.mois,
|
||||||
|
ca_ttc: r.ca_ttc,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const result = await pgGetCaByNomenclature(mois, moisN1);
|
||||||
|
rows = result.map((r) => ({
|
||||||
|
code: r.code,
|
||||||
|
label: r.libelle,
|
||||||
|
site: r.site,
|
||||||
|
mois: r.mois,
|
||||||
|
ca_ttc: r.ca_ttc,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pivotted = pivotData(rows, mois, moisN1);
|
||||||
|
|
||||||
|
// Calcul des totaux
|
||||||
|
const totals = pivotted.reduce(
|
||||||
|
(acc, row) => ({
|
||||||
|
ca292: acc.ca292 + row.ca292,
|
||||||
|
caN1_292: acc.caN1_292 + row.caN1_292,
|
||||||
|
ca579: acc.ca579 + row.ca579,
|
||||||
|
caN1_579: acc.caN1_579 + row.caN1_579,
|
||||||
|
caTotal: acc.caTotal + row.caTotal,
|
||||||
|
caN1Total: acc.caN1Total + row.caN1Total,
|
||||||
|
}),
|
||||||
|
{ ca292: 0, caN1_292: 0, ca579: 0, caN1_579: 0, caTotal: 0, caN1Total: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const evolutionTotal292 =
|
||||||
|
totals.caN1_292 > 0 ? ((totals.ca292 - totals.caN1_292) / totals.caN1_292) * 100 : null;
|
||||||
|
const evolutionTotal579 =
|
||||||
|
totals.caN1_579 > 0 ? ((totals.ca579 - totals.caN1_579) / totals.caN1_579) * 100 : null;
|
||||||
|
const evolutionTotalReseau =
|
||||||
|
totals.caN1Total > 0 ? ((totals.caTotal - totals.caN1Total) / totals.caN1Total) * 100 : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<h1 className="mb-6 text-3xl font-bold text-gray-900">Analytics</h1>
|
||||||
|
|
||||||
|
<AnalyticsClient
|
||||||
|
mode={mode}
|
||||||
|
mois={mois}
|
||||||
|
moisN1={moisN1}
|
||||||
|
currentMois={currentMois}
|
||||||
|
pivotted={pivotted}
|
||||||
|
totals={totals}
|
||||||
|
evolutionTotal292={evolutionTotal292}
|
||||||
|
evolutionTotal579={evolutionTotal579}
|
||||||
|
evolutionTotalReseau={evolutionTotalReseau}
|
||||||
|
formatCA={formatCA}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { useSession, signOut } from "next-auth/react";
|
|||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ icon: BarChart2, label: "Dashboard", href: "/dashboard" },
|
{ icon: BarChart2, label: "Dashboard", href: "/dashboard" },
|
||||||
{ icon: LayoutGrid, label: "Grille", href: "/grid" },
|
{ icon: LayoutGrid, label: "Grille", href: "/grid" },
|
||||||
|
{ icon: BarChart2, label: "Analytics", href: "/analytics" },
|
||||||
{ icon: Camera, label: "Snapshots", href: "/snapshots" },
|
{ icon: Camera, label: "Snapshots", href: "/snapshots" },
|
||||||
{ icon: FileDown, label: "Exports", href: "/exports" },
|
{ icon: FileDown, label: "Exports", href: "/exports" },
|
||||||
{ icon: Settings, label: "Paramètres", href: "/settings", adminOnly: true },
|
{ icon: Settings, label: "Paramètres", href: "/settings", adminOnly: true },
|
||||||
|
|||||||
@@ -616,3 +616,80 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
top10BySite,
|
top10BySite,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Analytics — CA TTC par fournisseur et nomenclature
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface CaByFournisseurRow {
|
||||||
|
code: string;
|
||||||
|
nom: string;
|
||||||
|
site: string;
|
||||||
|
mois: string;
|
||||||
|
ca_ttc: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaByNomenclatureRow {
|
||||||
|
code: string;
|
||||||
|
libelle: string;
|
||||||
|
site: string;
|
||||||
|
mois: string;
|
||||||
|
ca_ttc: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le CA TTC par fournisseur et site pour deux mois donnés (mois et mois N-1).
|
||||||
|
*/
|
||||||
|
export async function pgGetCaByFournisseur(
|
||||||
|
mois: string,
|
||||||
|
moisN1: string
|
||||||
|
): Promise<CaByFournisseurRow[]> {
|
||||||
|
const result = await pgNoParallel(sql`
|
||||||
|
SELECT
|
||||||
|
af.code AS code,
|
||||||
|
MAX(f.raisonsociale)::text AS nom,
|
||||||
|
m.site,
|
||||||
|
TO_CHAR(m.datmvt, 'YYYY-MM') AS mois,
|
||||||
|
SUM(-m.mntmvtttc)::float AS ca_ttc
|
||||||
|
FROM mvtart m
|
||||||
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
|
JOIN artfou1 af ON af.art_no_id = a.no_id
|
||||||
|
JOIN fouadr1 f ON f.code = af.code AND f.sit_code = '000'
|
||||||
|
WHERE TO_CHAR(m.datmvt, 'YYYY-MM') = ANY(ARRAY[${mois}, ${moisN1}])
|
||||||
|
AND m.site IN ('292', '579')
|
||||||
|
AND m.genremvt = 3
|
||||||
|
GROUP BY af.code, m.site, TO_CHAR(m.datmvt, 'YYYY-MM')
|
||||||
|
ORDER BY af.code, m.site, mois
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`[pg-ff] CaByFournisseur: ${result.rows.length} lignes pour ${mois}/${moisN1}`);
|
||||||
|
return result.rows as unknown as CaByFournisseurRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le CA TTC par nomenclature (code3) et site pour deux mois donnés.
|
||||||
|
*/
|
||||||
|
export async function pgGetCaByNomenclature(
|
||||||
|
mois: string,
|
||||||
|
moisN1: string
|
||||||
|
): Promise<CaByNomenclatureRow[]> {
|
||||||
|
const result = await pgNoParallel(sql`
|
||||||
|
SELECT
|
||||||
|
n.code AS code,
|
||||||
|
n.libelle::text AS libelle,
|
||||||
|
m.site,
|
||||||
|
TO_CHAR(m.datmvt, 'YYYY-MM') AS mois,
|
||||||
|
SUM(-m.mntmvtttc)::float AS ca_ttc
|
||||||
|
FROM mvtart m
|
||||||
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
|
JOIN nomenclature n ON n.no_id = a.nom_no_id
|
||||||
|
WHERE TO_CHAR(m.datmvt, 'YYYY-MM') = ANY(ARRAY[${mois}, ${moisN1}])
|
||||||
|
AND m.site IN ('292', '579')
|
||||||
|
AND m.genremvt = 3
|
||||||
|
GROUP BY n.code, n.libelle, m.site, TO_CHAR(m.datmvt, 'YYYY-MM')
|
||||||
|
ORDER BY n.code, m.site, mois
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`[pg-ff] CaByNomenclature: ${result.rows.length} lignes pour ${mois}/${moisN1}`);
|
||||||
|
return result.rows as unknown as CaByNomenclatureRow[];
|
||||||
|
}
|
||||||
Reference in new issue
Block a user