feat(adminsite): staff operations dashboard
Four counts, each one work somebody has to do today: failed injections, licences expiring inside 14 days, past-due subscriptions, and purchases unlinked for more than 48 hours. No totals and no revenue -- nothing that cannot be acted on. Every row links straight to the thing that needs doing. An empty queue says "nothing to do here" rather than rendering a bare zero, so a quiet dashboard reads as quiet rather than broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { API_BASE, NotConnected, api } from "@/lib/api";
|
||||
import { NotConnectedPanel } from "@/components/NotConnected";
|
||||
import { Queue } from "@/components/Queue";
|
||||
import { daysRemaining } from "@/lib/format";
|
||||
|
||||
const HOURS_48 = 48 * 3600_000;
|
||||
|
||||
export default function StaffDashboard() {
|
||||
const injection = useQuery({ queryKey: ["injection"], queryFn: api.staff.injectionHealth });
|
||||
const expiring = useQuery({
|
||||
queryKey: ["instances", "expiring"],
|
||||
queryFn: () => api.staff.instances({ expiring: "true" }),
|
||||
});
|
||||
const pastDue = useQuery({
|
||||
queryKey: ["subs", "past_due"],
|
||||
queryFn: () => api.staff.subscriptions("past_due"),
|
||||
});
|
||||
const unlinked = useQuery({
|
||||
queryKey: ["instances", "awaiting_link"],
|
||||
queryFn: () => api.staff.instances({ status: "awaiting_link" }),
|
||||
});
|
||||
|
||||
if (injection.error instanceof NotConnected) return <NotConnectedPanel url={API_BASE} />;
|
||||
|
||||
const stale = (unlinked.data ?? []).filter(
|
||||
(i) => Date.now() - new Date(i.created_at).getTime() > HOURS_48,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<h1 className="text-3xl">Operations</h1>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Queue
|
||||
title="Failed injections"
|
||||
tone="expired"
|
||||
count={injection.data?.count ?? 0}
|
||||
items={(injection.data?.failed ?? []).slice(0, 4).map((i) => ({
|
||||
label: i.name || i.instance_id,
|
||||
href: `/staff/instances/${i.instance_id}`,
|
||||
meta: i.inject_failed_at
|
||||
? new Date(i.inject_failed_at).toISOString().slice(11, 16)
|
||||
: "",
|
||||
}))}
|
||||
/>
|
||||
<Queue
|
||||
title="Expiring ≤ 14 days"
|
||||
tone="warn"
|
||||
count={expiring.data?.length ?? 0}
|
||||
items={(expiring.data ?? []).slice(0, 4).map((i) => ({
|
||||
label: i.name || i.instance_id,
|
||||
href: `/staff/instances/${i.instance_id}`,
|
||||
meta: i.tier ?? "",
|
||||
}))}
|
||||
/>
|
||||
<Queue
|
||||
title="Past due"
|
||||
tone="expired"
|
||||
count={pastDue.data?.length ?? 0}
|
||||
items={(pastDue.data ?? []).slice(0, 4).map((s) => ({
|
||||
label: s.instance_id || s.account_id,
|
||||
href: `/staff/accounts/${s.account_id}`,
|
||||
meta: `${daysRemaining(s.current_period_end)}d`,
|
||||
}))}
|
||||
/>
|
||||
<Queue
|
||||
title="Unlinked > 48h"
|
||||
tone="accent"
|
||||
count={stale.length}
|
||||
items={stale.slice(0, 4).map((i) => ({
|
||||
label: i.name || i.instance_id,
|
||||
href: `/staff/accounts/${i.account_id}`,
|
||||
meta: `${Math.floor((Date.now() - new Date(i.created_at).getTime()) / 86_400_000)}d`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user