"use client"; import { useEffect, useState } from "react"; import { Euro, TrendingUp, Users, Cpu, Loader2 } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useTranslationStore } from "@/lib/store"; import { API_BASE } from "@/lib/config"; import { useI18n } from "@/lib/i18n"; import type { AdminStatsResponse } from "./types"; const getToken = () => useTranslationStore.getState().settings.adminToken ?? ""; function formatMoney(value: number, currency: string): string { const currencyLabel = currency === "EUR" ? "€" : currency === "USD" ? "$" : currency; return `${value.toLocaleString("fr-FR", { maximumFractionDigits: 2 })} ${currencyLabel}`; } /** * Cartes business (revenus, MRR estimé, crédits, liste d'attente, paliers IA) * alimentées par GET /api/v1/admin/stats (champs revenue, mrr_estimated, * waitlist_count, ai_tier_usage). */ export function RevenueOverview() { const { t } = useI18n(); const [stats, setStats] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const load = async () => { try { const response = await fetch(`${API_BASE}/api/v1/admin/stats`, { headers: { Authorization: `Bearer ${getToken()}` }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const json = await response.json(); if (!cancelled) { setStats(json.data ?? json); setError(null); } } catch (e) { if (!cancelled) setError(e instanceof Error ? e.message : "Erreur"); } finally { if (!cancelled) setIsLoading(false); } }; load(); const interval = setInterval(load, 30000); return () => { cancelled = true; clearInterval(interval); }; }, []); if (isLoading && !stats) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } if (error || !stats) { return ( {t("admin.stats.unavailable", { error: error ?? "" })} ); } const currency = stats.revenue?.currency ?? "EUR"; const tierTotal = (stats.ai_tier_usage?.essential ?? 0) + (stats.ai_tier_usage?.premium ?? 0) + (stats.ai_tier_usage?.classic ?? 0) + (stats.ai_tier_usage?.other ?? 0); const cards = [ { title: t("admin.stats.revenue30"), icon: , value: formatMoney(stats.revenue?.collected_30d ?? 0, currency), sub: t("admin.stats.payments30", { count: stats.revenue?.payments_30d ?? 0 }), }, { title: t("admin.stats.revenueTotal"), icon: , value: formatMoney(stats.revenue?.collected_total ?? 0, currency), sub: t("admin.stats.includingCredits", { amount: formatMoney(stats.revenue?.credits_purchased ?? 0, currency), }), }, { title: t("admin.stats.mrr"), icon: , value: formatMoney(stats.mrr_estimated?.total ?? 0, currency), sub: Object.entries(stats.mrr_estimated?.by_plan ?? {}) .map(([plan, mrr]) => `${plan} : ${formatMoney(mrr, currency)}`) .join(" · ") || "—", }, { title: t("admin.stats.waitlist"), icon: , value: String(stats.waitlist_count ?? 0), sub: t("admin.stats.waitlistSub"), }, { title: t("admin.stats.aiTiers"), icon: , value: `${stats.ai_tier_usage?.essential ?? 0} / ${stats.ai_tier_usage?.premium ?? 0}`, sub: t("admin.stats.tiersSub", { classic: stats.ai_tier_usage?.classic ?? 0, other: stats.ai_tier_usage?.other ?? 0, }), }, ]; return (
{cards.map((c) => ( {c.title} {c.icon}
{c.value}

{c.sub}

))}
{tierTotal === 0 && (

{t("admin.stats.noTranslationsYet")}

)} {isLoading && (

{t("admin.stats.refreshing")}

)}
); }