UMBRA/frontend/src/components/dashboard/TrackedProjectsWidget.tsx
Kyle Pope c67567e186 Resolve remaining QA suggestions: shared constants, query tuning, cleanup
- Extract duplicate statusColors/statusLabels to projects/constants.ts
- Add staleTime + select to sidebar tracked projects query to reduce
  refetches and narrow data to only id/name
- Gate TrackedProjectsWidget query on settings being loaded
- Remove unnecessary from_attributes on TrackedTaskResponse schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 03:10:48 +08:00

111 lines
4.1 KiB
TypeScript

import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { format, parseISO, isPast, isToday } from 'date-fns';
import { FolderKanban } from 'lucide-react';
import api from '@/lib/api';
import type { TrackedTask } from '@/types';
import { useSettings } from '@/hooks/useSettings';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
const priorityColors: Record<string, string> = {
high: 'bg-red-400',
medium: 'bg-yellow-400',
low: 'bg-green-400',
none: 'bg-gray-400',
};
const statusBadgeColors: Record<string, string> = {
pending: 'bg-gray-500/10 text-gray-400',
in_progress: 'bg-purple-500/10 text-purple-400',
blocked: 'bg-red-500/10 text-red-400',
review: 'bg-yellow-500/10 text-yellow-400',
on_hold: 'bg-orange-500/10 text-orange-400',
};
const statusLabels: Record<string, string> = {
pending: 'Pending',
in_progress: 'Active',
blocked: 'Blocked',
review: 'Review',
on_hold: 'Hold',
};
export default function TrackedProjectsWidget() {
const navigate = useNavigate();
const { settings } = useSettings();
const days = settings?.upcoming_days || 7;
const { data: tasks } = useQuery({
queryKey: ['tracked-tasks', days],
queryFn: async () => {
const { data } = await api.get<TrackedTask[]>(`/projects/tracked-tasks?days=${days}`);
return data;
},
enabled: !!settings,
});
if (!tasks || tasks.length === 0) return null;
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-purple-500/10">
<FolderKanban className="h-4 w-4 text-purple-400" />
</div>
Tracked Projects
</CardTitle>
<span className="text-xs text-muted-foreground">{days} days</span>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="max-h-[400px] -mr-2 pr-2">
<div className="space-y-0.5">
{tasks.map((task) => {
const dueDate = parseISO(task.due_date);
const overdue = isPast(dueDate) && !isToday(dueDate);
return (
<div
key={task.id}
className="flex items-center gap-2 py-1.5 px-2 rounded-md hover:bg-card-elevated transition-colors duration-150 cursor-pointer"
onClick={() => navigate(`/projects/${task.project_id}`)}
>
<div className={cn('w-1.5 h-1.5 rounded-full shrink-0', priorityColors[task.priority] || 'bg-gray-400')} />
<div className="flex-1 min-w-0">
<span className="text-sm font-medium truncate block">
{task.parent_task_title && (
<span className="text-muted-foreground font-normal"> </span>
)}
{task.title}
</span>
{task.parent_task_title && (
<span className="text-[11px] text-muted-foreground truncate block">{task.parent_task_title}</span>
)}
</div>
<span className="text-[11px] text-muted-foreground truncate shrink-0 max-w-[5rem]">{task.project_name}</span>
<span className={cn(
'text-xs shrink-0 whitespace-nowrap tabular-nums',
overdue ? 'text-red-400' : isToday(dueDate) ? 'text-accent' : 'text-muted-foreground'
)}>
{isToday(dueDate) ? 'Today' : format(dueDate, 'MMM d')}
</span>
<span className={cn(
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0',
statusBadgeColors[task.status] || 'bg-gray-500/10 text-gray-400'
)}>
{statusLabels[task.status] || task.status}
</span>
</div>
);
})}
</div>
</ScrollArea>
</CardContent>
</Card>
);
}