|
1 | 1 | import Link from "next/link"; |
2 | 2 | import { fetchQuery } from "convex/nextjs"; |
| 3 | +import { |
| 4 | + ArrowRightIcon, |
| 5 | + CheckCircle2Icon, |
| 6 | + FileCheck2Icon, |
| 7 | + FolderKanbanIcon, |
| 8 | + ListChecksIcon, |
| 9 | + PackageCheckIcon, |
| 10 | + SparklesIcon, |
| 11 | +} from "lucide-react"; |
3 | 12 | import { api } from "@/convex/_generated/api"; |
4 | 13 | import { Badge } from "@/components/ui/badge"; |
5 | 14 | import { buttonVariants } from "@/components/ui/button"; |
6 | | -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; |
| 15 | +import { |
| 16 | + CompactPanel, |
| 17 | + SectionKicker, |
| 18 | + StatusDot, |
| 19 | + WorkflowBoard, |
| 20 | + WorkflowStateStrip, |
| 21 | +} from "@/components/app/workflow-primitives"; |
7 | 22 | import { getAppContext } from "@/lib/server/auth"; |
8 | | -import { cn } from "@/lib/utils"; |
| 23 | +import { cn, formatTimestamp } from "@/lib/utils"; |
9 | 24 |
|
10 | 25 | export default async function AppOverviewPage() { |
11 | 26 | const context = await getAppContext(); |
12 | | - const projects = await fetchQuery( |
13 | | - api.projects.listProjects, |
| 27 | + const summary = await fetchQuery( |
| 28 | + api.projects.dashboardSummary, |
14 | 29 | { |
15 | 30 | workspaceId: context.activeWorkspace.id, |
16 | 31 | organizationId: context.activeWorkspace.organizationId, |
17 | 32 | }, |
18 | 33 | { token: context.auth.accessToken }, |
19 | 34 | ); |
20 | 35 |
|
| 36 | + const metrics = [ |
| 37 | + { |
| 38 | + label: "Projects", |
| 39 | + value: summary.projectCount, |
| 40 | + icon: FolderKanbanIcon, |
| 41 | + }, |
| 42 | + { |
| 43 | + label: "Intakes started", |
| 44 | + value: summary.intakeCount, |
| 45 | + icon: ListChecksIcon, |
| 46 | + }, |
| 47 | + { |
| 48 | + label: "Pending review", |
| 49 | + value: summary.pendingReviewCount, |
| 50 | + icon: SparklesIcon, |
| 51 | + }, |
| 52 | + { |
| 53 | + label: "Approved outputs", |
| 54 | + value: summary.approvedOutputCount, |
| 55 | + icon: FileCheck2Icon, |
| 56 | + }, |
| 57 | + ]; |
| 58 | + const flowCards = [ |
| 59 | + { |
| 60 | + label: "Project dashboard", |
| 61 | + detail: "Create and track owner projects.", |
| 62 | + icon: FolderKanbanIcon, |
| 63 | + href: `/w/${context.activeWorkspace.slug}/projects`, |
| 64 | + }, |
| 65 | + { |
| 66 | + label: "Project intake", |
| 67 | + detail: "Capture scope, budget, timing, and constraints.", |
| 68 | + icon: ListChecksIcon, |
| 69 | + }, |
| 70 | + { |
| 71 | + label: "Delivery recommendation", |
| 72 | + detail: "Compare method fit and review decision drivers.", |
| 73 | + icon: SparklesIcon, |
| 74 | + }, |
| 75 | + { |
| 76 | + label: "Procurement package", |
| 77 | + detail: "Generate, edit, review, approve, and export.", |
| 78 | + icon: PackageCheckIcon, |
| 79 | + }, |
| 80 | + ]; |
| 81 | + |
21 | 82 | return ( |
22 | | - <div className="flex flex-col gap-6"> |
23 | | - <section className="page-surface grid gap-6 p-6 lg:grid-cols-[1.25fr_0.75fr]"> |
24 | | - <div className="space-y-4"> |
25 | | - <Badge variant="secondary">Authenticated app shell</Badge> |
26 | | - <div className="space-y-2"> |
27 | | - <h1 className="text-3xl font-semibold tracking-tight"> |
28 | | - {context.viewer.firstName ?? context.viewer.name ?? "Welcome back"} |
29 | | - </h1> |
30 | | - <p className="max-w-2xl text-sm leading-6 text-muted-foreground"> |
31 | | - Your active organization is mirrored from WorkOS into Convex as a workspace. The starter keeps tenancy in WorkOS and reserves Convex for app-specific data. |
32 | | - </p> |
33 | | - </div> |
34 | | - <div className="flex flex-col gap-3 sm:flex-row"> |
| 83 | + <div className="flex flex-col gap-5"> |
| 84 | + <WorkflowBoard className="p-5 md:p-6"> |
| 85 | + <div className="grid gap-5 lg:grid-cols-[0.95fr_1.4fr] lg:items-end"> |
| 86 | + <div className="space-y-4"> |
| 87 | + <div className="flex flex-wrap items-center gap-3"> |
| 88 | + <SectionKicker>Variation 1</SectionKicker> |
| 89 | + <Badge variant="secondary">App flow & core states</Badge> |
| 90 | + </div> |
| 91 | + <div> |
| 92 | + <h1 className="font-heading text-5xl font-bold uppercase leading-[0.92] tracking-wide md:text-6xl"> |
| 93 | + Capital Project Launchpad |
| 94 | + </h1> |
| 95 | + <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground"> |
| 96 | + {context.activeWorkspace.name} moves project setup through delivery recommendation, procurement drafting, review, approval, and export. |
| 97 | + </p> |
| 98 | + </div> |
35 | 99 | <Link |
36 | | - href={`/w/${context.activeWorkspace.slug}`} |
| 100 | + href={`/w/${context.activeWorkspace.slug}/projects`} |
37 | 101 | className={cn(buttonVariants())} |
38 | 102 | > |
39 | | - Open workspace |
40 | | - </Link> |
41 | | - <Link |
42 | | - href="/settings/profile" |
43 | | - className={cn(buttonVariants({ variant: "outline" }))} |
44 | | - > |
45 | | - Open profile widget |
| 103 | + Open project board |
| 104 | + <ArrowRightIcon data-icon="inline-end" /> |
46 | 105 | </Link> |
47 | 106 | </div> |
| 107 | + |
| 108 | + <div className="grid gap-3 md:grid-cols-2"> |
| 109 | + {flowCards.map(({ label, detail, icon: Icon, href }, index) => { |
| 110 | + const body = ( |
| 111 | + <> |
| 112 | + <div className="flex items-center justify-between gap-3"> |
| 113 | + <span className="flex size-7 items-center justify-center rounded-full bg-[color:var(--brand-orange)] font-mono text-xs font-semibold text-white"> |
| 114 | + {index + 1} |
| 115 | + </span> |
| 116 | + <Icon className="size-4 text-[color:var(--brand-navy)]" /> |
| 117 | + </div> |
| 118 | + <div> |
| 119 | + <div className="font-semibold">{label}</div> |
| 120 | + <div className="mt-1 text-xs leading-5 text-muted-foreground"> |
| 121 | + {detail} |
| 122 | + </div> |
| 123 | + </div> |
| 124 | + </> |
| 125 | + ); |
| 126 | + |
| 127 | + return href ? ( |
| 128 | + <Link |
| 129 | + key={label} |
| 130 | + href={href} |
| 131 | + className="flex min-h-32 flex-col justify-between rounded-md border border-border bg-card/80 p-3 text-sm hover:bg-card" |
| 132 | + > |
| 133 | + {body} |
| 134 | + </Link> |
| 135 | + ) : ( |
| 136 | + <div |
| 137 | + key={label} |
| 138 | + className="flex min-h-32 flex-col justify-between rounded-md border border-border bg-card/80 p-3 text-sm" |
| 139 | + > |
| 140 | + {body} |
| 141 | + </div> |
| 142 | + ); |
| 143 | + })} |
| 144 | + </div> |
48 | 145 | </div> |
49 | | - <Card className="border-border/70 bg-background/70"> |
50 | | - <CardHeader> |
51 | | - <CardTitle>Current tenant</CardTitle> |
52 | | - <CardDescription> |
53 | | - Role-aware navigation comes from WorkOS session claims. |
54 | | - </CardDescription> |
55 | | - </CardHeader> |
56 | | - <CardContent className="space-y-3 text-sm text-muted-foreground"> |
57 | | - <div className="flex items-center justify-between"> |
58 | | - <span>Workspace</span> |
59 | | - <span className="font-medium text-foreground"> |
60 | | - {context.activeWorkspace.name} |
61 | | - </span> |
62 | | - </div> |
63 | | - <div className="flex items-center justify-between"> |
64 | | - <span>Role</span> |
65 | | - <Badge variant="outline">{context.role}</Badge> |
66 | | - </div> |
67 | | - <div className="flex items-center justify-between"> |
68 | | - <span>Projects</span> |
69 | | - <span className="font-medium text-foreground">{projects.length}</span> |
| 146 | + </WorkflowBoard> |
| 147 | + |
| 148 | + <WorkflowStateStrip |
| 149 | + states={[ |
| 150 | + { label: "Draft generated", detail: `${summary.intakeCount} intakes started`, status: "draft" }, |
| 151 | + { label: "Review complete", detail: `${summary.pendingReviewCount} pending review`, status: "reviewed" }, |
| 152 | + { label: "Approved / locked", detail: `${summary.approvedOutputCount} export-ready`, status: "approved", locked: true }, |
| 153 | + ]} |
| 154 | + /> |
| 155 | + |
| 156 | + <section className="grid gap-3 md:grid-cols-4"> |
| 157 | + {metrics.map(({ label, value, icon: Icon }) => ( |
| 158 | + <CompactPanel key={label} className="min-h-28"> |
| 159 | + <div className="flex items-start justify-between gap-3"> |
| 160 | + <div> |
| 161 | + <div className="text-sm text-muted-foreground">{label}</div> |
| 162 | + <div className="mt-3 font-heading text-4xl font-bold tracking-wide"> |
| 163 | + {value} |
| 164 | + </div> |
| 165 | + </div> |
| 166 | + <div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground"> |
| 167 | + <Icon /> |
| 168 | + </div> |
70 | 169 | </div> |
71 | | - </CardContent> |
72 | | - </Card> |
| 170 | + </CompactPanel> |
| 171 | + ))} |
73 | 172 | </section> |
74 | 173 |
|
75 | | - <section className="grid gap-4 md:grid-cols-3"> |
76 | | - <Card className="page-surface"> |
77 | | - <CardHeader> |
78 | | - <CardTitle>Realtime sample data</CardTitle> |
79 | | - <CardDescription> |
80 | | - `/w/[slug]` hydrates a Convex project panel with typed mutations. |
81 | | - </CardDescription> |
82 | | - </CardHeader> |
83 | | - <CardContent className="text-3xl font-semibold">{projects.length}</CardContent> |
84 | | - </Card> |
85 | | - <Card className="page-surface"> |
86 | | - <CardHeader> |
87 | | - <CardTitle>Accessible workspaces</CardTitle> |
88 | | - <CardDescription> |
89 | | - The workspace switcher uses WorkOS memberships and session switching. |
90 | | - </CardDescription> |
91 | | - </CardHeader> |
92 | | - <CardContent className="text-3xl font-semibold"> |
93 | | - {context.workspaces.length} |
94 | | - </CardContent> |
95 | | - </Card> |
96 | | - <Card className="page-surface"> |
97 | | - <CardHeader> |
98 | | - <CardTitle>Admin controls</CardTitle> |
99 | | - <CardDescription> |
100 | | - Widgets are loaded on demand and token issuance stays server-side. |
101 | | - </CardDescription> |
102 | | - </CardHeader> |
103 | | - <CardContent className="text-3xl font-semibold"> |
104 | | - {context.role === "owner" || context.role === "admin" ? "Enabled" : "Member"} |
105 | | - </CardContent> |
106 | | - </Card> |
107 | | - </section> |
| 174 | + <CompactPanel> |
| 175 | + <div className="mb-3 flex items-center justify-between gap-3"> |
| 176 | + <div> |
| 177 | + <SectionKicker>Generation log</SectionKicker> |
| 178 | + <h2 className="mt-1 font-heading text-xl font-bold uppercase tracking-wide"> |
| 179 | + Recent system activity |
| 180 | + </h2> |
| 181 | + </div> |
| 182 | + <Badge variant="outline">{summary.recentEvents.length}</Badge> |
| 183 | + </div> |
| 184 | + <div className="grid gap-2"> |
| 185 | + {summary.recentEvents.length === 0 ? ( |
| 186 | + <div className="flex items-center gap-3 rounded-md border border-dashed border-border bg-background/60 p-4 text-sm text-muted-foreground"> |
| 187 | + <CheckCircle2Icon className="size-4 text-[color:var(--brand-teal)]" /> |
| 188 | + <span>No generation events yet.</span> |
| 189 | + </div> |
| 190 | + ) : ( |
| 191 | + summary.recentEvents.map((event) => ( |
| 192 | + <div |
| 193 | + key={event._id} |
| 194 | + className="flex flex-col gap-2 rounded-md border border-border bg-background/70 p-3 text-sm md:flex-row md:items-center" |
| 195 | + > |
| 196 | + <StatusDot status={event.status} className="font-medium capitalize" /> |
| 197 | + <div className="font-medium capitalize">{event.promptType}</div> |
| 198 | + <div className="text-muted-foreground md:ml-auto"> |
| 199 | + {formatTimestamp(event.createdAt)} |
| 200 | + </div> |
| 201 | + </div> |
| 202 | + )) |
| 203 | + )} |
| 204 | + </div> |
| 205 | + </CompactPanel> |
108 | 206 | </div> |
109 | 207 | ); |
110 | 208 | } |
0 commit comments