Skip to content

Commit 8719c9b

Browse files
Ripwordsclaude
andcommitted
feat(ui): /admin overview page with tiles, activity, per-project list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b17e400 commit 8719c9b

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<script setup lang="ts">
2+
import type { AdminOverviewDTO } from "@reprojs/shared"
3+
import AppEmptyState from "~/components/common/app-empty-state.vue"
4+
import { priorityColor, relativeTime } from "~/composables/use-report-format"
5+
6+
definePageMeta({ middleware: "admin-only" })
7+
useHead({ title: "Admin overview" })
8+
9+
const { data: overview } = await useApi<AdminOverviewDTO>("/api/admin/overview")
10+
11+
const metrics = computed(() => {
12+
const o = overview.value
13+
if (!o) return null
14+
return {
15+
open: o.counts.byStatus.open ?? 0,
16+
newThisWeek: o.counts.last7Days,
17+
total: o.counts.total,
18+
projects: o.projects.total,
19+
projectsWithGithub: o.projects.withGithub,
20+
}
21+
})
22+
23+
const recentReports = computed(() => overview.value?.recentReports ?? [])
24+
const recentActivity = computed(() => overview.value?.recentEvents ?? [])
25+
const perProject = computed(() => overview.value?.perProject ?? [])
26+
const projectCount = computed(() => overview.value?.projects.total ?? 0)
27+
28+
const EVENT_LABEL: Record<string, string> = {
29+
status_changed: "changed status",
30+
priority_changed: "changed priority",
31+
assignee_changed: "reassigned",
32+
tag_added: "added a tag",
33+
tag_removed: "removed a tag",
34+
github_unlinked: "unlinked GitHub issue",
35+
}
36+
37+
function describeEvent(e: AdminOverviewDTO["recentEvents"][number]): string {
38+
const label = EVENT_LABEL[e.kind] ?? e.kind
39+
return `${label} on "${e.reportTitle}" in ${e.projectName}`
40+
}
41+
</script>
42+
43+
<template>
44+
<div class="space-y-8">
45+
<!-- Page header -->
46+
<header class="flex items-end justify-between gap-4">
47+
<div>
48+
<div class="text-xs font-medium uppercase tracking-[0.18em] text-muted">Admin</div>
49+
<h1 class="mt-1 text-3xl font-semibold text-default tracking-tight">Overview</h1>
50+
<p class="mt-1.5 text-sm text-muted">
51+
Snapshot of incoming reports, health, and recent team activity across all projects.
52+
</p>
53+
</div>
54+
<UButton
55+
to="/"
56+
label="View all projects"
57+
trailing-icon="i-heroicons-arrow-right"
58+
color="primary"
59+
size="md"
60+
/>
61+
</header>
62+
63+
<!-- Metric tiles -->
64+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
65+
<div class="relative overflow-hidden rounded-xl border border-default bg-default p-5">
66+
<div
67+
class="flex items-center justify-center size-8 rounded-lg bg-primary/10 text-primary ring-1 ring-primary/15"
68+
>
69+
<UIcon name="i-heroicons-inbox" class="size-4" />
70+
</div>
71+
<div class="mt-4 text-xs font-medium uppercase tracking-wider text-muted">Open reports</div>
72+
<div class="mt-1 text-3xl font-semibold text-default tracking-tight tabular-nums">
73+
{{ metrics?.open ?? 0 }}
74+
</div>
75+
</div>
76+
77+
<div class="relative overflow-hidden rounded-xl border border-default bg-default p-5">
78+
<div class="flex items-center justify-center size-8 rounded-lg bg-muted text-muted">
79+
<UIcon name="i-heroicons-sparkles" class="size-4" />
80+
</div>
81+
<div class="mt-4 text-xs font-medium uppercase tracking-wider text-muted">
82+
New · last 7 days
83+
</div>
84+
<div class="mt-1 text-3xl font-semibold text-default tracking-tight tabular-nums">
85+
{{ metrics?.newThisWeek ?? 0 }}
86+
</div>
87+
</div>
88+
89+
<div class="relative overflow-hidden rounded-xl border border-default bg-default p-5">
90+
<div class="flex items-center justify-center size-8 rounded-lg bg-muted text-muted">
91+
<UIcon name="i-heroicons-chart-bar" class="size-4" />
92+
</div>
93+
<div class="mt-4 text-xs font-medium uppercase tracking-wider text-muted">
94+
Total reports
95+
</div>
96+
<div class="mt-1 text-3xl font-semibold text-default tracking-tight tabular-nums">
97+
{{ metrics?.total ?? 0 }}
98+
</div>
99+
</div>
100+
101+
<NuxtLink
102+
to="/"
103+
class="group relative overflow-hidden rounded-xl border border-default bg-default p-5 transition hover:border-primary/40 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_-12px_rgba(0,0,0,0.14)]"
104+
>
105+
<div class="flex items-center justify-between">
106+
<div class="flex items-center justify-center size-8 rounded-lg bg-muted text-muted">
107+
<UIcon name="i-heroicons-squares-2x2" class="size-4" />
108+
</div>
109+
<UIcon
110+
name="i-heroicons-arrow-up-right"
111+
class="size-3.5 text-muted opacity-0 group-hover:opacity-100 transition"
112+
/>
113+
</div>
114+
<div class="mt-4 text-xs font-medium uppercase tracking-wider text-muted">Projects</div>
115+
<div class="mt-1 text-3xl font-semibold text-default tracking-tight tabular-nums">
116+
{{ metrics?.projects ?? 0 }}
117+
</div>
118+
<div class="mt-1 text-xs text-muted">
119+
{{ metrics?.projectsWithGithub ?? 0 }} connected to GitHub
120+
</div>
121+
</NuxtLink>
122+
</div>
123+
124+
<!-- Two-column: recent reports + activity -->
125+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
126+
<div class="rounded-xl border border-default bg-default">
127+
<div class="flex items-center justify-between px-5 py-4 border-b border-default">
128+
<h2 class="text-sm font-semibold text-default tracking-tight">Recent reports</h2>
129+
</div>
130+
<div
131+
v-if="!recentReports || recentReports.length === 0"
132+
class="text-sm text-muted py-10 text-center"
133+
>
134+
No reports yet.
135+
</div>
136+
<ul v-else class="divide-y divide-default">
137+
<li v-for="r in recentReports" :key="r.id">
138+
<NuxtLink
139+
:to="`/projects/${r.projectId}/reports/${r.id}`"
140+
class="flex items-center gap-3 px-5 py-3 text-sm transition-colors hover:bg-elevated/50"
141+
>
142+
<span
143+
class="shrink-0 text-[10px] font-medium uppercase tracking-wider px-1.5 py-0.5 rounded bg-elevated text-muted"
144+
>
145+
{{ r.projectName }}
146+
</span>
147+
<UBadge
148+
:label="r.priority"
149+
:color="priorityColor(r.priority)"
150+
variant="soft"
151+
size="sm"
152+
class="capitalize shrink-0"
153+
/>
154+
<span class="flex-1 min-w-0 truncate text-default">{{ r.title }}</span>
155+
<span class="text-xs text-muted whitespace-nowrap tabular-nums">
156+
{{ relativeTime(r.receivedAt) }}
157+
</span>
158+
</NuxtLink>
159+
</li>
160+
</ul>
161+
</div>
162+
163+
<div class="rounded-xl border border-default bg-default">
164+
<div class="px-5 py-4 border-b border-default">
165+
<h2 class="text-sm font-semibold text-default tracking-tight">Activity</h2>
166+
</div>
167+
<div
168+
v-if="!recentActivity || recentActivity.length === 0"
169+
class="text-sm text-muted py-10 text-center"
170+
>
171+
No activity yet.
172+
</div>
173+
<ul v-else class="px-5 py-4 space-y-3.5">
174+
<li v-for="e in recentActivity" :key="e.id" class="flex items-start gap-3 text-sm">
175+
<span
176+
class="shrink-0 mt-1.5 inline-block size-1.5 rounded-full bg-primary/60"
177+
aria-hidden="true"
178+
/>
179+
<div class="flex-1 min-w-0">
180+
<span class="text-default font-medium">
181+
{{ e.actor?.name ?? e.actor?.email ?? "System" }}
182+
</span>
183+
<span class="text-muted"> {{ describeEvent(e) }}</span>
184+
<div class="mt-0.5 text-xs text-muted tabular-nums">
185+
{{ relativeTime(e.createdAt) }}
186+
</div>
187+
</div>
188+
</li>
189+
</ul>
190+
</div>
191+
</div>
192+
193+
<!-- Per-project breakdown -->
194+
<div v-if="perProject.length > 0" class="rounded-xl border border-default bg-default">
195+
<div class="px-5 py-4 border-b border-default">
196+
<h2 class="text-sm font-semibold text-default tracking-tight">Projects</h2>
197+
</div>
198+
<ul class="divide-y divide-default">
199+
<li v-for="p in perProject" :key="p.id">
200+
<NuxtLink
201+
:to="`/projects/${p.id}`"
202+
class="flex items-center gap-4 px-5 py-3 text-sm transition-colors hover:bg-elevated/50"
203+
>
204+
<span class="flex-1 min-w-0 truncate font-medium text-default">{{ p.name }}</span>
205+
<span class="text-xs text-muted tabular-nums shrink-0">
206+
<span class="font-semibold text-default">{{ p.openCount }}</span> open
207+
</span>
208+
<span class="text-xs text-muted tabular-nums shrink-0">
209+
{{ p.newLast7Count }} new · 7d
210+
</span>
211+
<span class="text-xs text-muted tabular-nums shrink-0"> {{ p.totalCount }} total </span>
212+
<UIcon name="i-heroicons-chevron-right" class="size-4 text-muted shrink-0" />
213+
</NuxtLink>
214+
</li>
215+
</ul>
216+
</div>
217+
218+
<!-- Empty state when the install has no projects at all -->
219+
<AppEmptyState
220+
v-if="projectCount === 0"
221+
variant="gradient"
222+
icon="i-heroicons-squares-plus"
223+
title="Create your first project"
224+
description="Once you spin up a project, it'll appear here with open-report counts and recent activity."
225+
action-label="New project"
226+
action-to="/"
227+
/>
228+
</div>
229+
</template>

0 commit comments

Comments
 (0)