From 2bd40ae8762028d0fed7ea9dbee1e6a5d7eff953 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 7 Apr 2026 11:09:54 +0200 Subject: [PATCH] feat: add PublicitesPage to display advertising data with year-based filtering --- src/app/(dashboard)/publicites/client.tsx | 146 +++++++++++++++++++++ src/app/(dashboard)/publicites/loading.tsx | 25 ++++ src/app/(dashboard)/publicites/page.tsx | 48 +++++++ src/components/shared/sidebar.tsx | 3 +- 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/publicites/client.tsx create mode 100644 src/app/(dashboard)/publicites/loading.tsx create mode 100644 src/app/(dashboard)/publicites/page.tsx diff --git a/src/app/(dashboard)/publicites/client.tsx b/src/app/(dashboard)/publicites/client.tsx new file mode 100644 index 0000000..3c4aeda --- /dev/null +++ b/src/app/(dashboard)/publicites/client.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import type { Publicite } from "./page"; + +const FIRST_YEAR = 2020; + +function fmtDate(iso: string) { + const d = new Date(iso); + return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" }); +} + +function fmtCA(v: number) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v); +} + +function fmtPct(v: number) { + return v.toFixed(2).replace(".", ",") + " %"; +} + +interface Props { + year: number; + publicites: Publicite[]; +} + +export function PublicitesClient({ year, publicites }: Props) { + const router = useRouter(); + const currentYear = new Date().getFullYear(); + const years = Array.from({ length: currentYear - FIRST_YEAR + 1 }, (_, i) => currentYear - i); + + function handleYearChange(e: React.ChangeEvent) { + router.push(`/publicites?year=${e.target.value}`); + } + + return ( +
+ {/* Barre de contrôles */} +
+
+ + +
+ +
+ + + {publicites.length} publicité{publicites.length !== 1 ? "s" : ""} + +
+ + {/* Tableau */} +
+ {publicites.length === 0 ? ( +
+ Aucune publicité trouvée pour {year}. +
+ ) : ( + + + + + + + + + + + + + + + + + {publicites.map((pub, idx) => ( + + + + + + + + + + + + + ))} + + + + + + + + + +
N°LibelléDate débutDate finCA pub période% CA totalQté vendueClientsTaux sortieNb articles
{pub.tcr_code}{pub.intitule} + {fmtDate(pub.date_debut)} + + {fmtDate(pub.date_fin)} + + {fmtCA(pub.ca_pub_periode_pub)} + + = 20 + ? "bg-orange-100 text-orange-700" + : pub.pourc_capub_catotal >= 10 + ? "bg-amber-100 text-amber-700" + : "bg-gray-100 text-gray-600" + }`}> + {fmtPct(pub.pourc_capub_catotal)} + + + {pub.qte_vendue_pub.toLocaleString("fr-FR")} + + {pub.client_pub_periode.toLocaleString("fr-FR")} + + {pub.taux_sortie.toFixed(1).replace(".", ",")} % + + {pub.nb_articles} +
+ TOTAL — {publicites.length} publicités + + {fmtCA(publicites.reduce((s, p) => s + p.ca_pub_periode_pub, 0))} + + — + + {publicites.reduce((s, p) => s + p.qte_vendue_pub, 0).toLocaleString("fr-FR")} + +
+ )} +
+
+ ); +} diff --git a/src/app/(dashboard)/publicites/loading.tsx b/src/app/(dashboard)/publicites/loading.tsx new file mode 100644 index 0000000..8e74239 --- /dev/null +++ b/src/app/(dashboard)/publicites/loading.tsx @@ -0,0 +1,25 @@ +export default function PublicitesLoading() { + return ( +
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+
+
+
+
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/publicites/page.tsx b/src/app/(dashboard)/publicites/page.tsx new file mode 100644 index 0000000..e651b50 --- /dev/null +++ b/src/app/(dashboard)/publicites/page.tsx @@ -0,0 +1,48 @@ +import { PublicitesClient } from "./client"; + +export interface Publicite { + tcr_code: string; + intitule: string; + date_debut: string; + date_fin: string; + site: string; + ca_pub_periode_pub: number; + qte_vendue_pub: number; + ca_total_periode_pub: number; + pourc_capub_catotal: number; + client_pub_periode: number; + client_total_periode: number; + taux_sortie: number; + marge: number; + taux_marge: number; + nb_articles: number; +} + +async function fetchPublicites(year: number): Promise { + const dateDebut = `${year}-01-01`; + const dateFin = `${year}-12-31`; + const url = `https://api.ffnancy.fr/api/publicites?dateDebut=${dateDebut}&dateFin=${dateFin}&limit=500`; + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) throw new Error(`Erreur API publicités: ${res.status}`); + const data = await res.json(); + return (data.publicites ?? []) as Publicite[]; +} + +export default async function PublicitesPage(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const currentYear = new Date().getFullYear(); + const year = Number(searchParams.year) || currentYear; + + const publicites = await fetchPublicites(year); + + return ( +
+
+

Publicités

+ +
+
+ ); +} diff --git a/src/components/shared/sidebar.tsx b/src/components/shared/sidebar.tsx index b56631d..620e3e3 100644 --- a/src/components/shared/sidebar.tsx +++ b/src/components/shared/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { LayoutGrid, Camera, FileDown, Settings, Package, BarChart2, TrendingUp, Trophy, LogOut, User as UserIcon, Loader2, ChevronRight, ChevronLeft } from "lucide-react"; +import { LayoutGrid, Camera, FileDown, Settings, Package, BarChart2, TrendingUp, Trophy, Megaphone, LogOut, User as UserIcon, Loader2, ChevronRight, ChevronLeft } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; @@ -13,6 +13,7 @@ const NAV_ITEMS = [ { icon: LayoutGrid, label: "Grille", href: "/grid" }, { icon: TrendingUp, label: "Analytics", href: "/analytics" }, { icon: Trophy, label: "Hit Parade", href: "/hit-parade" }, + { icon: Megaphone, label: "Publicités", href: "/publicites" }, { icon: Camera, label: "Snapshots", href: "/snapshots" }, { icon: FileDown, label: "Exports", href: "/exports", adminOnly: true }, { icon: Settings, label: "Paramètres", href: "/settings", adminOnly: true },