Skip to content

Commit 9f50c84

Browse files
Ripwordsclaude
andcommitted
feat(dashboard): add insight charts to admin overview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d435c35 commit 9f50c84

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

apps/dashboard/app/pages/admin/index.vue

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,47 @@ const recentActivity = computed(() => overview.value?.recentEvents ?? [])
2525
const perProject = computed(() => overview.value?.perProject ?? [])
2626
const projectCount = computed(() => overview.value?.projects.total ?? 0)
2727
28+
// ── Chart data ───────────────────────────────────────────────────────────
29+
// Theme-aligned hex (charts need concrete colours, not Tailwind tokens).
30+
const C = {
31+
primary: "#6366f1",
32+
open: "#3b82f6",
33+
inProgress: "#f59e0b",
34+
resolved: "#22c55e",
35+
closed: "#94a3b8",
36+
} as const
37+
38+
const volume = computed(() => overview.value?.volume ?? [])
39+
const hasVolume = computed(() => volume.value.some((v) => v.count > 0))
40+
const volumeCategories = { count: { name: "Reports", color: C.primary } }
41+
// vue-chrts x-formatter receives the data-array index; map it back to a
42+
// short MM-DD label.
43+
const volumeXFormatter = (i: number): string => {
44+
const d = volume.value[i]?.date
45+
return d ? d.slice(5) : ""
46+
}
47+
48+
const statusCounts = computed(() => overview.value?.counts.byStatus)
49+
const STATUS_ORDER = ["open", "in_progress", "resolved", "closed"] as const
50+
const statusData = computed<number[]>(() => STATUS_ORDER.map((s) => statusCounts.value?.[s] ?? 0))
51+
const hasStatus = computed(() => statusData.value.some((n) => n > 0))
52+
const statusCategories = {
53+
Open: { name: "Open", color: C.open },
54+
"In progress": { name: "In progress", color: C.inProgress },
55+
Resolved: { name: "Resolved", color: C.resolved },
56+
Closed: { name: "Closed", color: C.closed },
57+
}
58+
59+
const topProjects = computed(() =>
60+
perProject.value
61+
.toSorted((a, b) => b.openCount - a.openCount)
62+
.slice(0, 8)
63+
.map((p) => ({ project: p.name, open: p.openCount })),
64+
)
65+
const hasTopProjects = computed(() => topProjects.value.some((p) => p.open > 0))
66+
const topProjectsCategories = { open: { name: "Open reports", color: C.open } }
67+
const topProjectsXFormatter = (i: number): string => topProjects.value[i]?.project ?? ""
68+
2869
const EVENT_LABEL: Record<string, string> = {
2970
status_changed: "changed status",
3071
priority_changed: "changed priority",
@@ -128,6 +169,66 @@ function describeEvent(e: AdminOverviewDTO["recentEvents"][number]): string {
128169
</NuxtLink>
129170
</div>
130171

172+
<!-- Insights -->
173+
<div v-if="projectCount > 0" class="space-y-4">
174+
<div class="rounded-xl border border-default bg-default">
175+
<div class="px-5 py-4 border-b border-default">
176+
<h2 class="text-sm font-semibold text-default tracking-tight">Reports over time</h2>
177+
<p class="mt-0.5 text-sm text-muted">Reports received per day, last 30 days.</p>
178+
</div>
179+
<div class="p-5">
180+
<AreaChart
181+
v-if="hasVolume"
182+
:data="volume"
183+
:height="240"
184+
:categories="volumeCategories"
185+
:x-formatter="volumeXFormatter"
186+
:x-num-ticks="6"
187+
/>
188+
<div v-else class="text-sm text-muted py-10 text-center">No reports yet.</div>
189+
</div>
190+
</div>
191+
192+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
193+
<div class="rounded-xl border border-default bg-default">
194+
<div class="px-5 py-4 border-b border-default">
195+
<h2 class="text-sm font-semibold text-default tracking-tight">Status distribution</h2>
196+
</div>
197+
<div class="p-5 flex justify-center">
198+
<DonutChart
199+
v-if="hasStatus"
200+
:data="statusData"
201+
:height="240"
202+
:categories="statusCategories"
203+
:radius="4"
204+
:arc-width="24"
205+
/>
206+
<div v-else class="text-sm text-muted py-10 text-center">No reports yet.</div>
207+
</div>
208+
</div>
209+
210+
<div class="rounded-xl border border-default bg-default">
211+
<div class="px-5 py-4 border-b border-default">
212+
<h2 class="text-sm font-semibold text-default tracking-tight">
213+
Top projects by open reports
214+
</h2>
215+
</div>
216+
<div class="p-5">
217+
<BarChart
218+
v-if="hasTopProjects"
219+
:data="topProjects"
220+
:height="240"
221+
:categories="topProjectsCategories"
222+
:y-axis="['open']"
223+
orientation="horizontal"
224+
:x-formatter="topProjectsXFormatter"
225+
/>
226+
<div v-else class="text-sm text-muted py-10 text-center">No open reports yet.</div>
227+
</div>
228+
</div>
229+
</div>
230+
</div>
231+
131232
<!-- Two-column: recent reports + activity -->
132233
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
133234
<div class="rounded-xl border border-default bg-default">

0 commit comments

Comments
 (0)