- 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>
111 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|