feat: add PublicitesPage to display advertising data with year-based filtering

This commit is contained in:
Michael committed 2026-04-07 11:09:54 +02:00
1 parent 78495cb8b6
commit 2bd40ae876
4 files changed
+221 -1

No files matched your search

+146
View File
@@ -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<HTMLSelectElement>) {
router.push(`/publicites?year=${e.target.value}`);
}
return (
<div className="space-y-4">
{/* Barre de contrôles */}
<div className="flex items-end gap-4 rounded-xl bg-white px-5 py-4 shadow-sm border border-gray-100">
<div className="flex flex-col gap-1">
<label className="text-xs font-semibold text-gray-500">Année</label>
<select
value={year}
onChange={handleYearChange}
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-400 min-w-[120px]"
>
{years.map(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
<div className="h-8 w-px bg-gray-200 self-center" />
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-600 self-center">
{publicites.length} publicité{publicites.length !== 1 ? "s" : ""}
</span>
</div>
{/* Tableau */}
<div className="overflow-x-auto rounded-xl bg-white shadow-sm border border-gray-100">
{publicites.length === 0 ? (
<div className="py-16 text-center text-gray-400 text-sm">
Aucune publicité trouvée pour {year}.
</div>
) : (
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b-2 border-gray-200 bg-gray-50">
<th className="px-3 py-3 text-left text-xs font-semibold text-gray-500 w-20">N°</th>
<th className="px-3 py-3 text-left text-xs font-semibold text-gray-500">Libellé</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-500 whitespace-nowrap">Date début</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-500 whitespace-nowrap">Date fin</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-orange-600 whitespace-nowrap bg-orange-50">CA pub période</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-orange-600 whitespace-nowrap bg-orange-50">% CA total</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-gray-500 whitespace-nowrap">Qté vendue</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-gray-500 whitespace-nowrap">Clients</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-gray-500 whitespace-nowrap">Taux sortie</th>
<th className="px-3 py-3 text-right text-xs font-semibold text-gray-500 whitespace-nowrap">Nb articles</th>
</tr>
</thead>
<tbody>
{publicites.map((pub, idx) => (
<tr
key={`${pub.tcr_code}-${idx}`}
className="border-b border-gray-100 hover:bg-gray-50 transition-colors cursor-pointer"
>
<td className="px-3 py-2.5 font-mono text-xs text-gray-400">{pub.tcr_code}</td>
<td className="px-3 py-2.5 text-gray-900 font-medium">{pub.intitule}</td>
<td className="px-3 py-2.5 text-center text-xs text-gray-600 whitespace-nowrap tabular-nums">
{fmtDate(pub.date_debut)}
</td>
<td className="px-3 py-2.5 text-center text-xs text-gray-600 whitespace-nowrap tabular-nums">
{fmtDate(pub.date_fin)}
</td>
<td className="bg-orange-50/60 px-3 py-2.5 text-right font-semibold tabular-nums text-gray-900">
{fmtCA(pub.ca_pub_periode_pub)}
</td>
<td className="bg-orange-50/60 px-3 py-2.5 text-right tabular-nums">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${
pub.pourc_capub_catotal >= 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)}
</span>
</td>
<td className="px-3 py-2.5 text-right tabular-nums text-gray-700 text-xs">
{pub.qte_vendue_pub.toLocaleString("fr-FR")}
</td>
<td className="px-3 py-2.5 text-right tabular-nums text-gray-700 text-xs">
{pub.client_pub_periode.toLocaleString("fr-FR")}
</td>
<td className="px-3 py-2.5 text-right tabular-nums text-gray-700 text-xs">
{pub.taux_sortie.toFixed(1).replace(".", ",")} %
</td>
<td className="px-3 py-2.5 text-right tabular-nums text-gray-500 text-xs">
{pub.nb_articles}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t-2 border-gray-300 bg-gray-50">
<td colSpan={4} className="px-4 py-3 text-sm font-bold text-gray-800">
TOTAL — {publicites.length} publicités
</td>
<td className="bg-orange-100 px-3 py-3 text-right font-bold tabular-nums text-gray-900">
{fmtCA(publicites.reduce((s, p) => s + p.ca_pub_periode_pub, 0))}
</td>
<td className="bg-orange-100 px-3 py-3 text-right tabular-nums text-xs font-semibold text-orange-700">
—
</td>
<td className="px-3 py-3 text-right font-bold tabular-nums text-gray-700 text-xs">
{publicites.reduce((s, p) => s + p.qte_vendue_pub, 0).toLocaleString("fr-FR")}
</td>
<td colSpan={3} />
</tr>
</tfoot>
</table>
)}
</div>
</div>
);
}
@@ -0,0 +1,25 @@
export default function PublicitesLoading() {
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="mx-auto max-w-screen-xl">
<div className="mb-6 h-9 w-48 animate-pulse rounded-lg bg-gray-200" />
<div className="space-y-4">
<div className="h-16 animate-pulse rounded-xl bg-white shadow-sm border border-gray-100" />
<div className="animate-pulse rounded-xl bg-white shadow-sm border border-gray-100">
<div className="h-10 bg-gray-50 rounded-t-xl border-b border-gray-200" />
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-4 border-b border-gray-100 px-4 py-3">
<div className="h-4 w-12 bg-gray-100 rounded" />
<div className="h-4 flex-1 bg-gray-100 rounded" />
<div className="h-4 w-24 bg-gray-100 rounded" />
<div className="h-4 w-24 bg-gray-100 rounded" />
<div className="h-4 w-28 bg-orange-50 rounded" />
<div className="h-4 w-16 bg-orange-50 rounded" />
</div>
))}
</div>
</div>
</div>
</div>
);
}
+48
View File
@@ -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<Publicite[]> {
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<Record<string, string | string[]>>;
}) {
const searchParams = await props.searchParams;
const currentYear = new Date().getFullYear();
const year = Number(searchParams.year) || currentYear;
const publicites = await fetchPublicites(year);
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="mx-auto max-w-screen-xl">
<h1 className="mb-6 text-3xl font-bold text-gray-900">Publicités</h1>
<PublicitesClient year={year} publicites={publicites} />
</div>
</div>
);
}
+2 -1
View File
@@ -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 },