diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx index 5264e1f..8fa2c90 100644 --- a/apps/web/app/dashboard/page.tsx +++ b/apps/web/app/dashboard/page.tsx @@ -1,19 +1,35 @@ +import Link from "next/link" +import { + BotIcon, + ClipboardListIcon, + FileTextIcon, + NotebookTextIcon, + ScrollTextIcon, +} from "lucide-react" + import { BrandLogo } from "@/components/brand-logo" import { CreateCompanyDialog } from "@/components/dashboard/create-company-dialog" import { DashboardShell } from "@/components/dashboard/shell" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { findReviewReader } from "@/lib/api/review-reader" import { getDashboardContext } from "@/lib/dashboard/companies" +import { isDoneSummaryUnread } from "@/lib/dashboard/note-read-state" import { prisma } from "@/lib/prisma" - import { DashboardSummary } from "./dashboard-summary" type DashboardPageProps = { searchParams: Promise<{ company?: string; createCompany?: string }> } -export default async function DashboardPage({ searchParams }: DashboardPageProps) { +export default async function DashboardPage({ + searchParams, +}: DashboardPageProps) { const { company, createCompany } = await searchParams - const { session, companies, activeCompany } = await getDashboardContext(company) - const summary = activeCompany ? await getDashboardSummary(activeCompany.id) : null + const { session, companies, activeCompany } = + await getDashboardContext(company) + const summary = activeCompany + ? await getDashboardSummary(activeCompany.id) + : null return ( - + {!activeCompany ? ( @@ -43,29 +63,190 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps ) : null} - + ) } -async function getDashboardSummary(companyId: string) { - const [agents, projects, tasks] = await Promise.all([ +function OperatorShortcuts({ + companyId, + summary, +}: { + companyId: string | null + summary: DashboardSummaryData | null +}) { + const href = (path = "") => dashboardHref(companyId, path) + const projectHref = summary?.currentProject + ? href(`/projects/${summary.currentProject.id}`) + : href("/projects") + const emptyState = !summary + ? "Create a company to unlock agent setup, projects, notes, and audit history." + : summary.projects === 0 + ? "Create a project to give agents a task board and a place to report progress." + : summary.agents === 0 + ? "Add agents so work can be assigned and reviewed from the dashboard." + : null + + const shortcuts = [ + { + title: "Setup docs", + description: "Review token, agent ID, and operator runbook guidance.", + href: href("/docs"), + icon: FileTextIcon, + }, + { + title: summary?.currentProject + ? summary.currentProject.name + : "Projects & tasks", + description: summary?.currentProject + ? "Open the current project task board." + : "Create or open a project task board.", + href: projectHref, + icon: ClipboardListIcon, + }, + { + title: "Notes to review", + description: `${summary?.unreadNotes ?? 0} unread done ${ + (summary?.unreadNotes ?? 0) === 1 ? "summary" : "summaries" + } for main review.`, + href: href("/notes"), + icon: NotebookTextIcon, + }, + { + title: "Agent office", + description: `${summary?.agents ?? 0} configured ${ + (summary?.agents ?? 0) === 1 ? "agent" : "agents" + } in this company.`, + href: href("/office"), + icon: BotIcon, + }, + { + title: "Audit log", + description: "Inspect recent operator and agent changes.", + href: href("/audit-logs"), + icon: ScrollTextIcon, + }, + ] + + return ( + + + + + Next actions + Operator shortcuts + + {summary?.currentProject ? ( + + Current project: {summary.currentProject.name} + + ) : null} + + + + {emptyState ? ( + + {emptyState} + + ) : null} + + {shortcuts.map((shortcut) => { + const Icon = shortcut.icon + return ( + + + + + + {shortcut.title} + + + {shortcut.description} + + + ) + })} + + + + ) +} + +function dashboardHref(companyId: string | null, path = "") { + const search = companyId ? `?company=${companyId}` : "" + return `/dashboard${path}${search}` +} + +type DashboardSummaryData = { + agents: number + projects: number + tasks: Partial> + unreadNotes: number + currentProject: { id: string; name: string } | null +} + +async function getDashboardSummary( + companyId: string +): Promise { + const reviewReader = await findReviewReader(companyId) + const [agents, projects, currentProject, tasks, notes] = await Promise.all([ prisma.agent.count({ where: { companyId } }), prisma.project.count({ where: { companyId } }), + prisma.project.findFirst({ + where: { companyId }, + orderBy: { name: "asc" }, + select: { id: true, name: true }, + }), prisma.task.groupBy({ by: ["status"], - where: { project: { companyId } }, + where: { archivedAt: null, project: { companyId } }, _count: { _all: true }, }), + prisma.task.findMany({ + where: { + archivedAt: null, + note: { not: null }, + status: "done", + project: { companyId }, + ...(reviewReader ? {} : { id: "__no_review_reader__" }), + }, + select: { + summaryUpdatedAt: true, + readMarkers: { + where: { + status: "done", + ...(reviewReader ? { agentId: reviewReader.id } : { agentId: "__no_review_reader__" }), + }, + select: { readAt: true }, + }, + }, + }), ]) - - const taskCounts = Object.fromEntries(tasks.map((task) => [task.status, task._count._all])) + const taskCounts = Object.fromEntries( + tasks.map((task) => [task.status, task._count._all]) + ) + const unreadNotes = notes.filter((task) => { + const readAt = task.readMarkers[0]?.readAt + return isDoneSummaryUnread({ + readAt, + summaryUpdatedAt: task.summaryUpdatedAt, + }) + }).length return { agents, projects, tasks: taskCounts, + unreadNotes, + currentProject, } }
Next actions
+ Current project: {summary.currentProject.name} +