diff --git a/apps/api/src/stats.rs b/apps/api/src/stats.rs index 9c58b8d..f8187d1 100644 --- a/apps/api/src/stats.rs +++ b/apps/api/src/stats.rs @@ -106,6 +106,7 @@ pub struct RecentlyReadItem { pub struct MonthlyReading { pub month: String, pub books_read: i64, + pub pages_read: i64, } #[derive(Serialize, ToSchema)] @@ -113,6 +114,7 @@ pub struct UserMonthlyReading { pub month: String, pub username: String, pub books_read: i64, + pub pages_read: i64, } #[derive(Serialize, ToSchema)] @@ -516,11 +518,14 @@ pub async fn get_stats( r#" SELECT TO_CHAR(d.dt, 'YYYY-MM-DD') AS month, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series(CURRENT_DATE - INTERVAL '6 days', CURRENT_DATE, '1 day') AS d(dt) LEFT JOIN ( - SELECT brp.last_read_at::date AS dt, COUNT(*) AS books_read + SELECT brp.last_read_at::date AS dt, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= CURRENT_DATE - INTERVAL '6 days' AND ($1::uuid IS NULL OR brp.user_id = $1) @@ -538,15 +543,18 @@ pub async fn get_stats( r#" SELECT TO_CHAR(d.dt, 'YYYY-MM-DD') AS month, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series( DATE_TRUNC('week', NOW() - INTERVAL '2 months'), DATE_TRUNC('week', NOW()), '1 week' ) AS d(dt) LEFT JOIN ( - SELECT DATE_TRUNC('week', brp.last_read_at) AS dt, COUNT(*) AS books_read + SELECT DATE_TRUNC('week', brp.last_read_at) AS dt, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= DATE_TRUNC('week', NOW() - INTERVAL '2 months') AND ($1::uuid IS NULL OR brp.user_id = $1) @@ -564,15 +572,18 @@ pub async fn get_stats( r#" SELECT TO_CHAR(d.dt, 'YYYY-MM') AS month, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series( DATE_TRUNC('month', NOW()) - INTERVAL '11 months', DATE_TRUNC('month', NOW()), '1 month' ) AS d(dt) LEFT JOIN ( - SELECT DATE_TRUNC('month', brp.last_read_at) AS dt, COUNT(*) AS books_read + SELECT DATE_TRUNC('month', brp.last_read_at) AS dt, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= DATE_TRUNC('month', NOW()) - INTERVAL '11 months' AND ($1::uuid IS NULL OR brp.user_id = $1) @@ -592,6 +603,7 @@ pub async fn get_stats( .map(|r| MonthlyReading { month: r.get::, _>("month").unwrap_or_default(), books_read: r.get("books_read"), + pages_read: r.get("pages_read"), }) .collect(); @@ -603,12 +615,15 @@ pub async fn get_stats( SELECT TO_CHAR(d.dt, 'YYYY-MM-DD') AS month, u.username, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series(CURRENT_DATE - INTERVAL '6 days', CURRENT_DATE, '1 day') AS d(dt) CROSS JOIN users u LEFT JOIN ( - SELECT brp.last_read_at::date AS dt, brp.user_id, COUNT(*) AS books_read + SELECT brp.last_read_at::date AS dt, brp.user_id, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= CURRENT_DATE - INTERVAL '6 days' GROUP BY brp.last_read_at::date, brp.user_id @@ -625,7 +640,8 @@ pub async fn get_stats( SELECT TO_CHAR(d.dt, 'YYYY-MM-DD') AS month, u.username, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series( DATE_TRUNC('week', NOW() - INTERVAL '2 months'), DATE_TRUNC('week', NOW()), @@ -633,8 +649,10 @@ pub async fn get_stats( ) AS d(dt) CROSS JOIN users u LEFT JOIN ( - SELECT DATE_TRUNC('week', brp.last_read_at) AS dt, brp.user_id, COUNT(*) AS books_read + SELECT DATE_TRUNC('week', brp.last_read_at) AS dt, brp.user_id, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= DATE_TRUNC('week', NOW() - INTERVAL '2 months') GROUP BY DATE_TRUNC('week', brp.last_read_at), brp.user_id @@ -651,7 +669,8 @@ pub async fn get_stats( SELECT TO_CHAR(d.dt, 'YYYY-MM') AS month, u.username, - COALESCE(cnt.books_read, 0) AS books_read + COALESCE(cnt.books_read, 0) AS books_read, + COALESCE(cnt.pages_read, 0) AS pages_read FROM generate_series( DATE_TRUNC('month', NOW()) - INTERVAL '11 months', DATE_TRUNC('month', NOW()), @@ -659,8 +678,10 @@ pub async fn get_stats( ) AS d(dt) CROSS JOIN users u LEFT JOIN ( - SELECT DATE_TRUNC('month', brp.last_read_at) AS dt, brp.user_id, COUNT(*) AS books_read + SELECT DATE_TRUNC('month', brp.last_read_at) AS dt, brp.user_id, COUNT(*) AS books_read, + COALESCE(SUM(b.page_count), 0)::BIGINT AS pages_read FROM book_reading_progress brp + JOIN books b ON b.id = brp.book_id WHERE brp.status = 'read' AND brp.last_read_at >= DATE_TRUNC('month', NOW()) - INTERVAL '11 months' GROUP BY DATE_TRUNC('month', brp.last_read_at), brp.user_id @@ -679,6 +700,7 @@ pub async fn get_stats( month: r.get::, _>("month").unwrap_or_default(), username: r.get("username"), books_read: r.get("books_read"), + pages_read: r.get("pages_read"), }) .collect(); diff --git a/apps/backoffice/app/(app)/page.tsx b/apps/backoffice/app/(app)/page.tsx index 621da50..1a46e7e 100644 --- a/apps/backoffice/app/(app)/page.tsx +++ b/apps/backoffice/app/(app)/page.tsx @@ -3,6 +3,7 @@ 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 { MetricToggle } from "@/app/components/MetricToggle"; import { CurrentlyReadingList, RecentlyReadList } from "@/app/components/ReadingUserFilter"; import Link from "next/link"; import { getServerTranslations } from "@/lib/i18n/server"; @@ -67,6 +68,7 @@ export default async function DashboardPage({ const searchParamsAwaited = await searchParams; const rawPeriod = searchParamsAwaited.period; const period = rawPeriod === "day" ? "day" as const : rawPeriod === "week" ? "week" as const : "month" as const; + const metric = searchParamsAwaited.metric === "pages" ? "pages" as const : "books" as const; const { t, locale } = await getServerTranslations(); let stats: StatsResponse | null = null; @@ -179,7 +181,10 @@ export default async function DashboardPage({ {t("dashboard.readingActivity")} - +
+ + +
{(() => { @@ -187,12 +192,13 @@ export default async function DashboardPage({ "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 dataKey = metric === "pages" ? "pages_read" : "books_read"; 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 }))} + data={reading_over_time.map((m) => ({ label: formatChartLabel(m.month, period, locale), value: m[dataKey] }))} color="hsl(142 60% 45%)" /> ); @@ -202,7 +208,7 @@ export default async function DashboardPage({ 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; + byMonth.get(row.month)![row.username] = row[dataKey]; } const chartData = [...byMonth.values()]; const lines = usernames.map((u, i) => ({ diff --git a/apps/backoffice/app/components/MetricToggle.tsx b/apps/backoffice/app/components/MetricToggle.tsx new file mode 100644 index 0000000..b389a8e --- /dev/null +++ b/apps/backoffice/app/components/MetricToggle.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; + +type Metric = "books" | "pages"; + +export function MetricToggle({ + labels, +}: { + labels: { books: string; pages: string }; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + const raw = searchParams.get("metric"); + const current: Metric = raw === "pages" ? "pages" : "books"; + + function setMetric(metric: Metric) { + const params = new URLSearchParams(searchParams.toString()); + if (metric === "books") { + params.delete("metric"); + } else { + params.set("metric", metric); + } + const qs = params.toString(); + router.push(qs ? `?${qs}` : "/", { scroll: false }); + } + + const options: Metric[] = ["books", "pages"]; + + return ( +
+ {options.map((m) => ( + + ))} +
+ ); +} diff --git a/apps/backoffice/lib/api.ts b/apps/backoffice/lib/api.ts index 15b8ad4..18eaeb8 100644 --- a/apps/backoffice/lib/api.ts +++ b/apps/backoffice/lib/api.ts @@ -699,12 +699,14 @@ export type RecentlyReadItem = { export type MonthlyReading = { month: string; books_read: number; + pages_read: number; }; export type UserMonthlyReading = { month: string; username: string; books_read: number; + pages_read: number; }; export type JobTimePoint = { diff --git a/apps/backoffice/lib/i18n/en.ts b/apps/backoffice/lib/i18n/en.ts index fe6ddcd..d9ea2a5 100644 --- a/apps/backoffice/lib/i18n/en.ts +++ b/apps/backoffice/lib/i18n/en.ts @@ -80,6 +80,8 @@ const en: Record = { "dashboard.periodDay": "Day", "dashboard.periodWeek": "Week", "dashboard.periodMonth": "Month", + "dashboard.metricBooks": "Books", + "dashboard.metricPages": "Pages", "dashboard.popularSeries": "Popular series", "dashboard.noSeries": "No series yet", "dashboard.unknown": "Unknown", diff --git a/apps/backoffice/lib/i18n/fr.ts b/apps/backoffice/lib/i18n/fr.ts index 0e99751..0d02292 100644 --- a/apps/backoffice/lib/i18n/fr.ts +++ b/apps/backoffice/lib/i18n/fr.ts @@ -78,6 +78,8 @@ const fr = { "dashboard.periodDay": "Jour", "dashboard.periodWeek": "Semaine", "dashboard.periodMonth": "Mois", + "dashboard.metricBooks": "Livres", + "dashboard.metricPages": "Pages", "dashboard.popularSeries": "Séries populaires", "dashboard.noSeries": "Aucune série pour le moment", "dashboard.unknown": "Inconnu",