126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useMemo } from "react";
|
||
|
|
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { api } from "@/lib/trpc";
|
||
|
|
|
||
|
|
export interface OverviewViewProps {
|
||
|
|
workspaceId?: string;
|
||
|
|
spaceId?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatUpdatedAt(value: Date | string): string {
|
||
|
|
const date = value instanceof Date ? value : new Date(value);
|
||
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
||
|
|
return date.toLocaleString();
|
||
|
|
}
|
||
|
|
|
||
|
|
export function OverviewView({ workspaceId, spaceId }: OverviewViewProps) {
|
||
|
|
const spaceQuery = api.objects.getById.useQuery(
|
||
|
|
{ id: spaceId! },
|
||
|
|
{ enabled: Boolean(spaceId) },
|
||
|
|
);
|
||
|
|
|
||
|
|
const childrenQuery = api.objects.list.useQuery(
|
||
|
|
{
|
||
|
|
workspaceId: workspaceId!,
|
||
|
|
parentId: spaceId ?? undefined,
|
||
|
|
limit: 200,
|
||
|
|
},
|
||
|
|
{ enabled: Boolean(workspaceId) },
|
||
|
|
);
|
||
|
|
|
||
|
|
const statusCounts = useMemo(() => {
|
||
|
|
const counts = { open: 0, in_progress: 0, done: 0 };
|
||
|
|
for (const row of childrenQuery.data?.objects ?? []) {
|
||
|
|
const s = row.status;
|
||
|
|
if (s === "in_progress") counts.in_progress += 1;
|
||
|
|
else if (s === "done") counts.done += 1;
|
||
|
|
else counts.open += 1;
|
||
|
|
}
|
||
|
|
return counts;
|
||
|
|
}, [childrenQuery.data?.objects]);
|
||
|
|
|
||
|
|
const recentChildren = useMemo(() => {
|
||
|
|
const rows = childrenQuery.data?.objects ?? [];
|
||
|
|
return [...rows]
|
||
|
|
.sort((a, b) => {
|
||
|
|
const ta = new Date(a.updatedAt).getTime();
|
||
|
|
const tb = new Date(b.updatedAt).getTime();
|
||
|
|
return tb - ta;
|
||
|
|
})
|
||
|
|
.slice(0, 10);
|
||
|
|
}, [childrenQuery.data?.objects]);
|
||
|
|
|
||
|
|
const isLoading = spaceQuery.isLoading || childrenQuery.isLoading;
|
||
|
|
const children = childrenQuery.data?.objects ?? [];
|
||
|
|
const hasChildren = children.length > 0;
|
||
|
|
const space = spaceQuery.data as { title: string } | undefined;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="h-full overflow-auto p-6">
|
||
|
|
{spaceQuery.isLoading ? (
|
||
|
|
<div className="text-sm text-muted-foreground">Loading space…</div>
|
||
|
|
) : space ? (
|
||
|
|
<h1 className="text-2xl font-semibold tracking-tight">{space.title}</h1>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
<div className="mt-8 space-y-8">
|
||
|
|
<section>
|
||
|
|
<h2 className="text-sm font-semibold">By status</h2>
|
||
|
|
<div className="mt-3 grid grid-cols-3 gap-4">
|
||
|
|
{(
|
||
|
|
[
|
||
|
|
{ key: "open" as const, label: "Open" },
|
||
|
|
{ key: "in_progress" as const, label: "In progress" },
|
||
|
|
{ key: "done" as const, label: "Done" },
|
||
|
|
] as const
|
||
|
|
).map(({ key, label }) => (
|
||
|
|
<div
|
||
|
|
key={key}
|
||
|
|
className="rounded-lg border border-border bg-card p-4 shadow-sm"
|
||
|
|
>
|
||
|
|
<div className="text-sm text-muted-foreground">{label}</div>
|
||
|
|
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
||
|
|
{isLoading ? "—" : statusCounts[key]}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2 className="text-sm font-semibold">Recent activity</h2>
|
||
|
|
{childrenQuery.isLoading ? (
|
||
|
|
<p className="mt-3 text-sm text-muted-foreground">Loading…</p>
|
||
|
|
) : !hasChildren ? (
|
||
|
|
<div className="mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-10 text-center text-sm text-muted-foreground">
|
||
|
|
No items in this space yet.
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card shadow-sm">
|
||
|
|
{recentChildren.map((obj) => (
|
||
|
|
<li
|
||
|
|
key={obj.id}
|
||
|
|
className="flex items-center justify-between gap-3 px-4 py-3"
|
||
|
|
>
|
||
|
|
<span className="min-w-0 truncate font-medium">
|
||
|
|
{obj.title}
|
||
|
|
</span>
|
||
|
|
<div className="flex shrink-0 items-center gap-2">
|
||
|
|
<Badge variant="outline">{obj.type}</Badge>
|
||
|
|
<span className="text-xs text-muted-foreground tabular-nums">
|
||
|
|
{formatUpdatedAt(obj.updatedAt)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|