feat: add review bucket filtering and status indication in task board

This commit is contained in:
Abhimanyu Saharan
2026-02-07 01:05:12 +05:30
parent 5ffbcedfbe
commit ff68dd15d3
2 changed files with 91 additions and 2 deletions

View File

@@ -2,8 +2,11 @@ import { CalendarClock, UserCircle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type TaskStatus = "inbox" | "in_progress" | "review" | "done";
interface TaskCardProps { interface TaskCardProps {
title: string; title: string;
status?: TaskStatus;
priority?: string; priority?: string;
assignee?: string; assignee?: string;
due?: string; due?: string;
@@ -19,6 +22,7 @@ interface TaskCardProps {
export function TaskCard({ export function TaskCard({
title, title,
status,
priority, priority,
assignee, assignee,
due, due,
@@ -32,10 +36,13 @@ export function TaskCard({
onDragEnd, onDragEnd,
}: TaskCardProps) { }: TaskCardProps) {
const hasPendingApproval = approvalsPendingCount > 0; const hasPendingApproval = approvalsPendingCount > 0;
const needsLeadReview = status === "review" && !isBlocked && !hasPendingApproval;
const leftBarClassName = isBlocked const leftBarClassName = isBlocked
? "bg-rose-400" ? "bg-rose-400"
: hasPendingApproval : hasPendingApproval
? "bg-amber-400" ? "bg-amber-400"
: needsLeadReview
? "bg-indigo-400"
: null; : null;
const priorityBadge = (value?: string) => { const priorityBadge = (value?: string) => {
if (!value) return null; if (!value) return null;
@@ -61,6 +68,7 @@ export function TaskCard({
isDragging && "opacity-60 shadow-none", isDragging && "opacity-60 shadow-none",
hasPendingApproval && "border-amber-200 bg-amber-50/40", hasPendingApproval && "border-amber-200 bg-amber-50/40",
isBlocked && "border-rose-200 bg-rose-50/50", isBlocked && "border-rose-200 bg-rose-50/50",
needsLeadReview && "border-indigo-200 bg-indigo-50/30",
)} )}
draggable={draggable} draggable={draggable}
onDragStart={onDragStart} onDragStart={onDragStart}
@@ -98,6 +106,12 @@ export function TaskCard({
Approval needed · {approvalsPendingCount} Approval needed · {approvalsPendingCount}
</div> </div>
) : null} ) : null}
{needsLeadReview ? (
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-indigo-700">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Waiting for lead review
</div>
) : null}
</div> </div>
<div className="flex flex-col items-end gap-2"> <div className="flex flex-col items-end gap-2">
<span <span

View File

@@ -28,6 +28,8 @@ type TaskBoardProps = {
onTaskMove?: (taskId: string, status: TaskStatus) => void | Promise<void>; onTaskMove?: (taskId: string, status: TaskStatus) => void | Promise<void>;
}; };
type ReviewBucket = "all" | "approval_needed" | "waiting_lead" | "blocked";
const columns: Array<{ const columns: Array<{
title: string; title: string;
status: TaskStatus; status: TaskStatus;
@@ -99,6 +101,7 @@ export const TaskBoard = memo(function TaskBoard({
const [draggingId, setDraggingId] = useState<string | null>(null); const [draggingId, setDraggingId] = useState<string | null>(null);
const [activeColumn, setActiveColumn] = useState<TaskStatus | null>(null); const [activeColumn, setActiveColumn] = useState<TaskStatus | null>(null);
const [reviewBucket, setReviewBucket] = useState<ReviewBucket>("all");
const setCardRef = useCallback( const setCardRef = useCallback(
(taskId: string) => (node: HTMLDivElement | null) => { (taskId: string) => (node: HTMLDivElement | null) => {
@@ -310,6 +313,42 @@ export const TaskBoard = memo(function TaskBoard({
> >
{columns.map((column) => { {columns.map((column) => {
const columnTasks = grouped[column.status] ?? []; const columnTasks = grouped[column.status] ?? [];
const reviewCounts =
column.status === "review"
? columnTasks.reduce(
(acc, task) => {
if (task.is_blocked) {
acc.blocked += 1;
return acc;
}
if ((task.approvals_pending_count ?? 0) > 0) {
acc.approval_needed += 1;
return acc;
}
acc.waiting_lead += 1;
return acc;
},
{
all: columnTasks.length,
approval_needed: 0,
waiting_lead: 0,
blocked: 0,
},
)
: null;
const filteredTasks =
column.status === "review" && reviewBucket !== "all"
? columnTasks.filter((task) => {
if (reviewBucket === "blocked") return Boolean(task.is_blocked);
if (reviewBucket === "approval_needed")
return (task.approvals_pending_count ?? 0) > 0 && !task.is_blocked;
if (reviewBucket === "waiting_lead")
return !task.is_blocked && (task.approvals_pending_count ?? 0) === 0;
return true;
})
: columnTasks;
return ( return (
<div <div
key={column.title} key={column.title}
@@ -335,16 +374,52 @@ export const TaskBoard = memo(function TaskBoard({
column.badge, column.badge,
)} )}
> >
{columnTasks.length} {filteredTasks.length}
</span> </span>
</div> </div>
{column.status === "review" && reviewCounts ? (
<div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-slate-500">
{(
[
{ key: "all", label: "All", count: reviewCounts.all },
{
key: "approval_needed",
label: "Approval needed",
count: reviewCounts.approval_needed,
},
{
key: "waiting_lead",
label: "Lead review",
count: reviewCounts.waiting_lead,
},
{ key: "blocked", label: "Blocked", count: reviewCounts.blocked },
] as const
).map((option) => (
<button
key={option.key}
type="button"
onClick={() => setReviewBucket(option.key)}
className={cn(
"rounded-full border px-2.5 py-1 transition",
reviewBucket === option.key
? "border-slate-900 bg-slate-900 text-white"
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50",
)}
aria-pressed={reviewBucket === option.key}
>
{option.label} · {option.count}
</button>
))}
</div>
) : null}
</div> </div>
<div className="rounded-b-xl border border-t-0 border-slate-200 bg-white p-3"> <div className="rounded-b-xl border border-t-0 border-slate-200 bg-white p-3">
<div className="space-y-3"> <div className="space-y-3">
{columnTasks.map((task) => ( {filteredTasks.map((task) => (
<div key={task.id} ref={setCardRef(task.id)}> <div key={task.id} ref={setCardRef(task.id)}>
<TaskCard <TaskCard
title={task.title} title={task.title}
status={task.status}
priority={task.priority} priority={task.priority}
assignee={task.assignee ?? undefined} assignee={task.assignee ?? undefined}
due={formatDueDate(task.due_at)} due={formatDueDate(task.due_at)}