mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: add PublicitesPage to display advertising data with year-based filtering
This commit is contained in:
1 parent
78495cb8b6
commit
2bd40ae876
4 files changed
+221
-1
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user