From a296ba4ce1065dced6b9b49bd13e8fe494f48d12 Mon Sep 17 00:00:00 2001 From: philipcheung Date: Sun, 13 Sep 2026 21:41:48 +0800 Subject: [PATCH] feat(admin): add getDashboardData query --- src/data/content.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/data/content.ts b/src/data/content.ts index 238cae3..1779de1 100644 --- a/src/data/content.ts +++ b/src/data/content.ts @@ -1,9 +1,10 @@ -import { and, asc, desc, eq, ne } from "drizzle-orm"; +import { and, asc, desc, eq, isNull, ne, sql } from "drizzle-orm"; import type { DrizzleD1Database } from "drizzle-orm/d1"; import { blogKeywords, cases, contentItems, + CONTENT_KINDS, posts, siteSettings, type BlogKeyword, @@ -71,6 +72,53 @@ export async function getKeywords(db: Db): Promise { .orderBy(asc(blogKeywords.createdAt), asc(blogKeywords.id)); } +export type DashboardData = { + posts: { published: number; draft: number }; + cases: { published: number; draft: number }; + content: { kind: ContentKind; published: number; draft: number }[]; + keywords: { pending: number; generated: number; skipped: number }; + recentPosts: Post[]; + postsWithoutCover: number; +}; + +/** 後台總覽用嘅統計(只讀 DB;AI secrets 由頁面層提供)。 */ +export async function getDashboardData(db: Db): Promise { + const [postRows, caseRows, itemRows, kwRows, recentPosts, noCover] = await Promise.all([ + db.select({ status: posts.status, n: sql`count(*)` }).from(posts).groupBy(posts.status), + db.select({ status: cases.status, n: sql`count(*)` }).from(cases).groupBy(cases.status), + db + .select({ kind: contentItems.kind, status: contentItems.status, n: sql`count(*)` }) + .from(contentItems) + .groupBy(contentItems.kind, contentItems.status), + db + .select({ status: blogKeywords.status, n: sql`count(*)` }) + .from(blogKeywords) + .groupBy(blogKeywords.status), + db.select().from(posts).orderBy(desc(posts.updatedAt)).limit(5), + db.select({ n: sql`count(*)` }).from(posts).where(isNull(posts.coverImage)), + ]); + + const pick = (rows: { status: T; n: number }[], status: T) => + Number(rows.find((r) => r.status === status)?.n ?? 0); + + return { + posts: { published: pick(postRows, "published"), draft: pick(postRows, "draft") }, + cases: { published: pick(caseRows, "published"), draft: pick(caseRows, "draft") }, + content: CONTENT_KINDS.map((kind) => ({ + kind, + published: Number(itemRows.find((r) => r.kind === kind && r.status === "published")?.n ?? 0), + draft: Number(itemRows.find((r) => r.kind === kind && r.status === "draft")?.n ?? 0), + })), + keywords: { + pending: pick(kwRows, "pending"), + generated: pick(kwRows, "generated"), + skipped: pick(kwRows, "skipped"), + }, + recentPosts, + postsWithoutCover: Number(noCover[0]?.n ?? 0), + }; +} + export async function getHomeData(db: Db): Promise { const [settings, services, features, steps, faqs, caseList] = await Promise.all([ getSettings(db),