import React from "react"; import { fetchStats, fetchUsers, StatsResponse, UserDto } from "@/lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/app/components/ui"; import { RcDonutChart, RcBarChart, RcAreaChart, RcStackedBar, RcHorizontalBar, RcMultiLineChart } from "@/app/components/DashboardCharts"; import { PeriodToggle } from "@/app/components/PeriodToggle"; import { CurrentlyReadingList, RecentlyReadList } from "@/app/components/ReadingUserFilter"; import Link from "next/link"; import { getServerTranslations } from "@/lib/i18n/server"; import type { TranslateFunction } from "@/lib/i18n/dictionaries"; export const dynamic = "force-dynamic"; function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; } function formatNumber(n: number, locale: string): string { return n.toLocaleString(locale === "fr" ? "fr-FR" : "en-US"); } function formatChartLabel(raw: string, period: "day" | "week" | "month", locale: string): string { const loc = locale === "fr" ? "fr-FR" : "en-US"; if (period === "month") { // raw = "YYYY-MM" const [y, m] = raw.split("-"); const d = new Date(Number(y), Number(m) - 1, 1); return d.toLocaleDateString(loc, { month: "short" }); } if (period === "week") { // raw = "YYYY-MM-DD" (Monday of the week) const d = new Date(raw + "T00:00:00"); return d.toLocaleDateString(loc, { day: "numeric", month: "short" }); } // day: raw = "YYYY-MM-DD" const d = new Date(raw + "T00:00:00"); return d.toLocaleDateString(loc, { weekday: "short", day: "numeric" }); } // Horizontal progress bar for metadata quality (stays server-rendered, no recharts needed) function HorizontalBar({ label, value, max, subLabel, color = "var(--color-primary)" }: { label: string; value: number; max: number; subLabel?: string; color?: string }) { const pct = max > 0 ? (value / max) * 100 : 0; return (
{label} {subLabel || value}
); } export default async function DashboardPage({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const searchParamsAwaited = await searchParams; const rawPeriod = searchParamsAwaited.period; const period = rawPeriod === "day" ? "day" as const : rawPeriod === "week" ? "week" as const : "month" as const; const { t, locale } = await getServerTranslations(); let stats: StatsResponse | null = null; let users: UserDto[] = []; try { [stats, users] = await Promise.all([ fetchStats(period), fetchUsers().catch(() => []), ]); } catch (e) { console.error("Failed to fetch stats:", e); } if (!stats) { return (

StripStream Backoffice

{t("dashboard.loadError")}

); } const { overview, reading_status, currently_reading = [], recently_read = [], reading_over_time = [], users_reading_over_time = [], by_format, by_library, top_series, additions_over_time, jobs_over_time = [], metadata = { total_series: 0, series_linked: 0, series_unlinked: 0, books_with_summary: 0, books_with_isbn: 0, by_provider: [] }, } = stats; const readingColors = ["hsl(220 13% 70%)", "hsl(45 93% 47%)", "hsl(142 60% 45%)"]; const formatColors = [ "hsl(198 78% 37%)", "hsl(142 60% 45%)", "hsl(45 93% 47%)", "hsl(2 72% 48%)", "hsl(280 60% 50%)", "hsl(32 80% 50%)", "hsl(170 60% 45%)", "hsl(220 60% 50%)", ]; const noDataLabel = t("common.noData"); return (
{/* Header */}

{t("dashboard.title")}

{t("dashboard.subtitle")}

{/* Overview stat cards */}
{/* Currently reading + Recently read */} {(currently_reading.length > 0 || recently_read.length > 0) && (
{/* Currently reading */} {t("dashboard.currentlyReading")} {/* Recently read */} {t("dashboard.recentlyRead")}
)} {/* Reading activity line chart */} {t("dashboard.readingActivity")} {(() => { const userColors = [ "hsl(142 60% 45%)", "hsl(198 78% 37%)", "hsl(45 93% 47%)", "hsl(2 72% 48%)", "hsl(280 60% 50%)", "hsl(32 80% 50%)", ]; const usernames = [...new Set(users_reading_over_time.map(r => r.username))]; if (usernames.length === 0) { return ( ({ label: formatChartLabel(m.month, period, locale), value: m.books_read }))} color="hsl(142 60% 45%)" /> ); } // Pivot: { label, username1: n, username2: n, ... } const byMonth = new Map>(); for (const row of users_reading_over_time) { const label = formatChartLabel(row.month, period, locale); if (!byMonth.has(row.month)) byMonth.set(row.month, { label }); byMonth.get(row.month)![row.username] = row.books_read; } const chartData = [...byMonth.values()]; const lines = usernames.map((u, i) => ({ key: u, label: u, color: userColors[i % userColors.length], })); return ; })()} {/* Charts row */}
{/* Reading status par lecteur */} {t("dashboard.readingStatus")} {users.length === 0 ? ( ) : (
{users.map((user) => { const total = overview.total_books; const read = user.books_read; const reading = user.books_reading; const unread = Math.max(0, total - read - reading); const readPct = total > 0 ? (read / total) * 100 : 0; const readingPct = total > 0 ? (reading / total) * 100 : 0; return (
{user.username} {read} {reading > 0 && · {reading}} / {total}
); })}
)} {/* By format donut */} {t("dashboard.byFormat")} ({ name: (f.format || t("dashboard.unknown")).toUpperCase(), value: f.count, color: formatColors[i % formatColors.length], }))} /> {/* By library donut */} {t("dashboard.byLibrary")} ({ name: l.library_name, value: l.book_count, color: formatColors[i % formatColors.length], }))} />
{/* Metadata row */}
{/* Series metadata coverage donut */} {t("dashboard.metadataCoverage")} {/* By provider donut */} {t("dashboard.byProvider")} ({ name: p.provider.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), value: p.count, color: formatColors[i % formatColors.length], }))} /> {/* Book metadata quality */} {t("dashboard.bookMetadata")}
0 ? `${Math.round((metadata.books_with_summary / overview.total_books) * 100)}%` : "0%"} color="hsl(198 78% 37%)" /> 0 ? `${Math.round((metadata.books_with_isbn / overview.total_books) * 100)}%` : "0%"} color="hsl(280 60% 50%)" />
{/* Libraries breakdown + Top series */}
{by_library.length > 0 && ( {t("dashboard.libraries")} ({ name: lib.library_name, read: lib.read_count, reading: lib.reading_count, unread: lib.unread_count, sizeLabel: formatBytes(lib.size_bytes), }))} labels={{ read: t("status.read"), reading: t("status.reading"), unread: t("status.unread"), books: t("dashboard.books"), }} /> )} {/* Top series */} {t("dashboard.popularSeries")} ({ name: s.series, value: s.book_count, subLabel: t("dashboard.readCount", { read: s.read_count, total: s.book_count }), }))} color="hsl(142 60% 45%)" />
{/* Additions line chart – full width */} {t("dashboard.booksAdded")} ({ label: formatChartLabel(m.month, period, locale), value: m.books_added }))} color="hsl(198 78% 37%)" /> {/* Jobs over time – multi-line chart */} {t("dashboard.jobsOverTime")} ({ label: formatChartLabel(j.label, period, locale), scan: j.scan, rebuild: j.rebuild, thumbnail: j.thumbnail, other: j.other, }))} lines={[ { key: "scan", label: t("dashboard.jobScan"), color: "hsl(198 78% 37%)" }, { key: "rebuild", label: t("dashboard.jobRebuild"), color: "hsl(142 60% 45%)" }, { key: "thumbnail", label: t("dashboard.jobThumbnail"), color: "hsl(45 93% 47%)" }, { key: "other", label: t("dashboard.jobOther"), color: "hsl(280 60% 50%)" }, ]} /> {/* Quick links */}
); } function StatCard({ icon, label, value, color }: { icon: string; label: string; value: string; color: string }) { const icons: Record = { book: , series: , library: , pages: , author: , size: , }; const colorClasses: Record = { primary: "bg-primary/10 text-primary", success: "bg-success/10 text-success", warning: "bg-warning/10 text-warning", }; return (
{icons[icon]}

{value}

{label}

); } function QuickLinks({ t }: { t: TranslateFunction }) { const links = [ { href: "/libraries", label: t("nav.libraries"), bg: "bg-primary/10", text: "text-primary", hoverBg: "group-hover:bg-primary", hoverText: "group-hover:text-primary-foreground", icon: }, { href: "/books", label: t("nav.books"), bg: "bg-success/10", text: "text-success", hoverBg: "group-hover:bg-success", hoverText: "group-hover:text-white", icon: }, { href: "/series", label: t("nav.series"), bg: "bg-warning/10", text: "text-warning", hoverBg: "group-hover:bg-warning", hoverText: "group-hover:text-white", icon: }, { href: "/jobs", label: t("nav.jobs"), bg: "bg-destructive/10", text: "text-destructive", hoverBg: "group-hover:bg-destructive", hoverText: "group-hover:text-destructive-foreground", icon: }, ]; return (
{links.map((l) => (
{l.icon}
{l.label} ))}
); }