import crypto from "node:crypto"; import { and, asc, desc, eq, inArray } from "drizzle-orm"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { db } from "@/db"; import { sessions, taskInstances, taskTemplates, users } from "@/db/schema"; import { SESSION_COOKIE, getSessionUser } from "@/lib/auth"; import { daysAgo, todayStr } from "@/lib/dates"; import { ensureDay } from "@/lib/tasks"; import { streakOf } from "@/lib/streak"; import { ParentDashboard, type DevicesData, type OverviewData, type TemplatesData } from "./dashboard"; const SHANGHAI = { timeZone: "Asia/Shanghai" } as const; const fmtDateTime = (d: Date) => d.toLocaleString("zh-CN", { ...SHANGHAI, hour12: false }).replace(/\//g, "-"); export default async function ParentPage() { const parent = await getSessionUser(); if (!parent) redirect("/login"); if (parent.role !== "parent") redirect("/kid"); const kids = await db .select({ id: users.id, name: users.displayName }) .from(users) .where(and(eq(users.familyId, parent.familyId), eq(users.role, "child"))) .orderBy(asc(users.sortOrder)); const today = todayStr(); const weekDates = Array.from({ length: 7 }, (_, i) => daysAgo(i)); // 今天在前 // ===== 今日概览 ===== const overview: OverviewData = { kids: [] }; for (const kid of kids) { await ensureDay(kid.id, today); // 家长先看时也要生成今天的快照 const [streak, rows] = await Promise.all([ streakOf(kid.id), db .select({ id: taskInstances.id, date: taskInstances.date, title: taskInstances.title, status: taskInstances.status, isMakeup: taskInstances.isMakeup, sortOrder: taskInstances.sortOrder, }) .from(taskInstances) .where(and(eq(taskInstances.childUserId, kid.id), inArray(taskInstances.date, weekDates))) .orderBy(desc(taskInstances.date), asc(taskInstances.sortOrder)), ]); const week = weekDates.map((date) => ({ date, isToday: date === today, tasks: rows.filter((r) => r.date === date), })); const todayTasks = week.find((w) => w.isToday)?.tasks ?? []; overview.kids.push({ id: kid.id, name: kid.name, streak, today: { total: todayTasks.length, done: todayTasks.filter((t) => t.status === "done").length, }, week, }); } // ===== 周模板 ===== const templateRows = kids.length ? await db .select() .from(taskTemplates) .where(inArray(taskTemplates.childUserId, kids.map((k) => k.id))) .orderBy(asc(taskTemplates.childUserId), asc(taskTemplates.dow), asc(taskTemplates.sortOrder)) : []; const templates: TemplatesData = { kids: kids.map((kid) => ({ id: kid.id, name: kid.name, items: templateRows .filter((t) => t.childUserId === kid.id) .map((t) => ({ id: t.id, dow: t.dow, title: t.title, requiresPhoto: t.requiresPhoto, })), })), }; // ===== 设备会话 ===== const token = (await cookies()).get(SESSION_COOKIE)?.value; const tokenHash = token ? crypto.createHash("sha256").update(token).digest("hex") : null; const sessionRows = await db .select({ id: sessions.id, tokenHash: sessions.tokenHash, deviceLabel: sessions.deviceLabel, createdAt: sessions.createdAt, expiresAt: sessions.expiresAt, owner: users.displayName, role: users.role, }) .from(sessions) .innerJoin(users, eq(users.id, sessions.userId)) .where(eq(users.familyId, parent.familyId)) .orderBy(desc(sessions.createdAt)); const devices: DevicesData = { rows: sessionRows.map((s) => ({ id: s.id, owner: s.role === "parent" ? `${s.owner}(家长)` : s.owner, deviceLabel: s.deviceLabel, createdAt: fmtDateTime(s.createdAt), expiresAt: fmtDateTime(s.expiresAt), isCurrent: s.tokenHash === tokenHash, })), }; return ; }