129 lines
4 KiB
TypeScript
129 lines
4 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import * as React from "react";
|
||
|
|
import { gateLabelForStatus } from "@tasks/shared/convoy-lifecycle";
|
||
|
|
import type { ConvoyStatus } from "@tasks/shared/convoy-lifecycle";
|
||
|
|
import { Loader2 } from "lucide-react";
|
||
|
|
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { api } from "@/lib/trpc";
|
||
|
|
|
||
|
|
type ConvoyCard = {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
status: ConvoyStatus;
|
||
|
|
content: { convoySlug: string; classification: string; successMetric: string } | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ConvoyKanban({
|
||
|
|
workspaceSlug,
|
||
|
|
selectedId,
|
||
|
|
onSelect,
|
||
|
|
}: {
|
||
|
|
workspaceSlug: string;
|
||
|
|
selectedId: string | null;
|
||
|
|
onSelect: (id: string) => void;
|
||
|
|
}) {
|
||
|
|
const kanbanQuery = api.convoys.kanban.useQuery(
|
||
|
|
{ workspace: workspaceSlug },
|
||
|
|
{ enabled: Boolean(workspaceSlug) },
|
||
|
|
);
|
||
|
|
|
||
|
|
if (kanbanQuery.isLoading) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
|
||
|
|
<Loader2 className="size-4 animate-spin" />
|
||
|
|
Loading kanban…
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns = kanbanQuery.data?.columns ?? [];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-0 flex-1 gap-3 overflow-x-auto p-4">
|
||
|
|
{columns.map((column) => (
|
||
|
|
<div
|
||
|
|
key={column.status}
|
||
|
|
className="flex w-56 shrink-0 flex-col rounded-lg border border-border bg-muted/20"
|
||
|
|
>
|
||
|
|
<div className="border-b border-border px-3 py-2">
|
||
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||
|
|
{column.label}
|
||
|
|
</p>
|
||
|
|
<p className="text-[10px] text-muted-foreground">{column.convoys.length}</p>
|
||
|
|
</div>
|
||
|
|
<ul className="flex flex-1 flex-col gap-2 overflow-y-auto p-2">
|
||
|
|
{column.convoys.map((convoy) => (
|
||
|
|
<li key={convoy.id}>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onSelect(convoy.id)}
|
||
|
|
className={cn(
|
||
|
|
"w-full rounded-md border border-border bg-background p-2 text-left text-sm transition-colors hover:bg-muted",
|
||
|
|
selectedId === convoy.id && "ring-2 ring-primary",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<p className="font-medium">{convoy.content?.convoySlug ?? convoy.title}</p>
|
||
|
|
{convoy.content?.classification && (
|
||
|
|
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
||
|
|
{convoy.content.classification}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function GateInbox({
|
||
|
|
workspaceSlug,
|
||
|
|
selectedId,
|
||
|
|
onSelect,
|
||
|
|
}: {
|
||
|
|
workspaceSlug: string;
|
||
|
|
selectedId: string | null;
|
||
|
|
onSelect: (id: string) => void;
|
||
|
|
}) {
|
||
|
|
const kanbanQuery = api.convoys.kanban.useQuery(
|
||
|
|
{ workspace: workspaceSlug },
|
||
|
|
{ enabled: Boolean(workspaceSlug) },
|
||
|
|
);
|
||
|
|
|
||
|
|
const inbox = kanbanQuery.data?.gateInbox ?? [];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="border-b border-border p-4">
|
||
|
|
<h2 className="text-sm font-semibold">Needs you</h2>
|
||
|
|
<p className="text-xs text-muted-foreground">Gate 1, 2, and 3 approvals</p>
|
||
|
|
{inbox.length === 0 ? (
|
||
|
|
<p className="mt-3 text-sm text-muted-foreground">No pending gates.</p>
|
||
|
|
) : (
|
||
|
|
<ul className="mt-3 space-y-2">
|
||
|
|
{inbox.map((convoy: ConvoyCard) => (
|
||
|
|
<li key={convoy.id}>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onSelect(convoy.id)}
|
||
|
|
className={cn(
|
||
|
|
"w-full rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-left text-sm hover:bg-amber-500/10",
|
||
|
|
selectedId === convoy.id && "ring-2 ring-amber-500",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<p className="font-medium">{convoy.content?.convoySlug ?? convoy.title}</p>
|
||
|
|
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||
|
|
{gateLabelForStatus(convoy.status)}
|
||
|
|
</p>
|
||
|
|
</button>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|