TaskSection.tsx8.7 KBView on GitHub 'use client';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { SectionHeading } from '@/modules/conversations/components/SectionHeading';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { ConversationUserTask as UserTask } from '@/modules/crm/types';
import { taskOutputLabel } from '@/modules/userTasks/utils/task-output';
import { Mail, ExternalLink, ChevronDown, Filter } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useCedarStore } from '@/modules/store';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { useState } from 'react';
interface TaskSectionProps {
tasks: UserTask[];
}
type TaskFilter = 'todo' | 'done' | 'all';
export function TaskSection({ tasks }: TaskSectionProps) {
const selectThreadId = useCedarStore((state) => state.selectThreadId);
const setIsThreadOpen = useCedarStore((state) => state.setIsThreadOpen);
const [statusFilter, setStatusFilter] = useState<TaskFilter>('todo');
if (!tasks || tasks.length === 0) {
return (
<div className="rounded-lg p-3">
<div className="mb-2">
<SectionHeading>Tasks</SectionHeading>
</div>
<div className="text-muted-foreground rounded-sm border border-dashed p-3 text-center text-xs">
No tasks for this conversation
</div>
</div>
);
}
// Filter tasks based on selected filter
const filteredTasks = tasks.filter((t) => {
if (statusFilter === 'all') return true;
return t.status === statusFilter;
});
const handleOpenThread = (task: UserTask) => {
const actionData = task.taskActionData;
if (actionData?.channel === 'email' && actionData.threadId) {
selectThreadId(actionData.threadId);
setIsThreadOpen(true);
}
};
const getTaskTypeAndIcon = (task: UserTask) => {
// Check if it has a draft ready using new schema
const actionData = task.taskActionData;
const hasDraft = actionData?.channel === 'email' && actionData.draftId;
if (hasDraft) {
return {
type: 'Follow-up drafted',
icon: <Mail className="h-3 w-3 fill-green-800 dark:fill-green-200" />,
hasDraft: true,
};
}
// Use task label
return {
type: task.taskType || 'Task',
icon: <Mail className="h-3 w-3 fill-current" />,
hasDraft: false,
};
};
const getTaskDescription = (task: UserTask) => {
return task.description || 'No description';
};
const renderTask = (task: UserTask) => {
const actionData = task.taskActionData;
const hasThread = actionData?.channel === 'email' && actionData.threadId;
const { type: taskType, icon: taskIcon, hasDraft } = getTaskTypeAndIcon(task);
const description = getTaskDescription(task);
return (
<div
key=[redacted]
className={cn(
'space-y-2 rounded-md border p-2.5 text-xs transition-colors',
task.status === 'todo'
? 'border-orange-200 bg-orange-50/50 dark:border-orange-900/50 dark:bg-orange-950/20'
: task.status === 'done'
? 'border-blue-200 bg-blue-50/50 dark:border-blue-900/50 dark:bg-blue-950/20'
: 'border-gray-200 bg-gray-50/50 dark:border-gray-800 dark:bg-gray-900/20',
)}
>
{/* Row 1: Task Type Badge + Type Badge + External Link */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5">
<Badge
variant="outline"
className={cn(
'shrink-0 gap-1.5 rounded-sm',
hasDraft
? 'border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700 dark:bg-orange-900 dark:text-orange-200 hover:bg-orange-200 dark:hover:bg-orange-900/80'
: 'bg-primary/10 text-primary border-primary/20 hover:bg-primary/20 dark:hover:bg-primary/15',
)}
>
{taskIcon}
<span className="truncate">{taskType}</span>
</Badge>
{/* Task Output Badge */}
<Badge variant="secondary" className="rounded-sm text-[10px]">
{taskOutputLabel(task)}
</Badge>
{/* Status Badge */}
<Badge
variant="outline"
className={cn(
'rounded-sm text-[10px]',
task.status === 'todo'
? 'border-green-300 bg-green-50 text-green-700 dark:border-green-700 dark:bg-green-900/20 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/30'
: task.status === 'done'
? 'border-blue-300 bg-blue-50 text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/30'
: 'border-gray-300 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-800/20 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800/30',
)}
>
{task.status === 'todo' ? 'To Do' : task.status === 'done' ? 'Done' : task.status}
</Badge>
</div>
{hasThread && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-6 gap-1 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
handleOpenThread(task);
}}
>
<ExternalLink className="h-3 w-3" />
Open
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Open thread</p>
</TooltipContent>
</Tooltip>
)}
</div>
{/* Row 2: Task Description */}
<div className="text-muted-foreground text-xs leading-relaxed">{description}</div>
</div>
);
};
const getFilterLabel = (filter: TaskFilter): string => {
switch (filter) {
case 'todo':
return 'To Do';
case 'done':
return 'Done';
case 'all':
return 'All';
}
};
const getFilterCount = (filter: TaskFilter): number => {
switch (filter) {
case 'todo':
return tasks.filter((t) => t.status === 'todo').length;
case 'done':
return tasks.filter((t) => t.status === 'done').length;
case 'all':
return tasks.length;
}
};
return (
<div className="rounded-lg p-3">
{/* Header with Filter */}
<div className="mb-3 flex items-center justify-between">
<SectionHeading>Tasks ({getFilterCount(statusFilter)})</SectionHeading>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 gap-1.5 text-xs">
<Filter className="h-3 w-3" />
{getFilterLabel(statusFilter)}
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setStatusFilter('todo')}>
<div className="flex w-full items-center justify-between gap-2">
<span>To Do</span>
<Badge variant="secondary" className="ml-2 rounded-sm text-[10px]">
{getFilterCount('todo')}
</Badge>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setStatusFilter('done')}>
<div className="flex w-full items-center justify-between gap-2">
<span>Done</span>
<Badge variant="secondary" className="ml-2 rounded-sm text-[10px]">
{getFilterCount('done')}
</Badge>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setStatusFilter('all')}>
<div className="flex w-full items-center justify-between gap-2">
<span>All</span>
<Badge variant="secondary" className="ml-2 rounded-sm text-[10px]">
{tasks.length}
</Badge>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Tasks List */}
{filteredTasks.length > 0 ? (
<div className="space-y-2">{filteredTasks.map(renderTask)}</div>
) : (
<div className="text-muted-foreground rounded-sm border border-dashed p-3 text-center text-xs">
No {statusFilter !== 'all' ? getFilterLabel(statusFilter).toLowerCase() : ''} tasks
</div>
)}
</div>
);
}