a53dcc38d5
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-TASK-MODEL: Extended Task model with polymorphic assignee/entity/creator, subtasks, dependencies, task_type, success_criteria, progress - F-TASK-API: Extended task routes with polymorphic filters, subtasks, dependencies, new lifecycle - F-TASK-AGENT: ai_tools.py (191 lines) — create_task, assign_task, update_task_status, decompose_goal tools - F-TASK-WORK: workstream.py — task_card and goal_card blocks in communication system - F-TASK-UI: TaskBoard.tsx, TaskDetail.tsx, GoalView.tsx frontend components - F-TASK-MIG: Migration 0124 — new columns, data migration for contact_id/assigned_to - F-TASK-GOAL: Progress aggregation, success criteria evaluation, parent status propagation - F-TASK-TEST: test_unified_tasks.py (414 lines) - i18n updates for task system
242 lines
7.8 KiB
TypeScript
242 lines
7.8 KiB
TypeScript
/**
|
|
* Tasks API hooks — CRUD, assign, status update, subtasks, dependencies, goals.
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
|
|
|
export type TaskStatus = 'open' | 'in_progress' | 'review' | 'blocked' | 'done' | 'cancelled';
|
|
export type TaskType = 'todo' | 'approval' | 'follow_up' | 'review' | 'goal' | 'milestone' | 'agent_subtask';
|
|
export type AssigneeType = 'user' | 'agent' | 'group';
|
|
export type CreatorType = 'user' | 'agent' | 'workflow' | 'system';
|
|
|
|
export interface Task {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
status: TaskStatus;
|
|
priority: 'low' | 'medium' | 'high' | 'urgent';
|
|
due_date: string | null;
|
|
assigned_to: string | null;
|
|
contact_id: string | null;
|
|
assignee_type: AssigneeType;
|
|
assignee_id: string | null;
|
|
entity_type: string | null;
|
|
entity_id: string | null;
|
|
creator_type: CreatorType;
|
|
creator_id: string | null;
|
|
parent_task_id: string | null;
|
|
depends_on: string[];
|
|
task_type: TaskType;
|
|
success_criteria: Record<string, unknown> | null;
|
|
target_date: string | null;
|
|
progress: number;
|
|
created_by: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface TaskListResponse {
|
|
items: Task[];
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
}
|
|
|
|
export interface TaskCreateInput {
|
|
title: string;
|
|
description?: string | null;
|
|
status?: TaskStatus;
|
|
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
|
due_date?: string | null;
|
|
assigned_to?: string | null;
|
|
contact_id?: string | null;
|
|
assignee_type?: AssigneeType;
|
|
assignee_id?: string | null;
|
|
entity_type?: string | null;
|
|
entity_id?: string | null;
|
|
creator_type?: CreatorType;
|
|
creator_id?: string | null;
|
|
parent_task_id?: string | null;
|
|
depends_on?: string[];
|
|
task_type?: TaskType;
|
|
success_criteria?: Record<string, unknown> | null;
|
|
target_date?: string | null;
|
|
progress?: number;
|
|
}
|
|
|
|
export interface TaskUpdateInput {
|
|
title?: string;
|
|
description?: string | null;
|
|
status?: TaskStatus;
|
|
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
|
due_date?: string | null;
|
|
assigned_to?: string | null;
|
|
contact_id?: string | null;
|
|
assignee_type?: AssigneeType;
|
|
assignee_id?: string | null;
|
|
entity_type?: string | null;
|
|
entity_id?: string | null;
|
|
creator_type?: CreatorType;
|
|
creator_id?: string | null;
|
|
parent_task_id?: string | null;
|
|
depends_on?: string[];
|
|
task_type?: TaskType;
|
|
success_criteria?: Record<string, unknown> | null;
|
|
target_date?: string | null;
|
|
progress?: number;
|
|
}
|
|
|
|
export interface TaskFilter {
|
|
status?: TaskStatus;
|
|
priority?: string;
|
|
assigned_to?: string;
|
|
contact_id?: string;
|
|
entity_type?: string;
|
|
entity_id?: string;
|
|
assignee_type?: AssigneeType;
|
|
assignee_id?: string;
|
|
parent_task_id?: string;
|
|
task_type?: TaskType;
|
|
search?: string;
|
|
}
|
|
|
|
export function useTasks(page = 1, pageSize = 25, filter?: TaskFilter) {
|
|
const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
|
|
if (filter?.status) params.set('status', filter.status);
|
|
if (filter?.priority) params.set('priority', filter.priority);
|
|
if (filter?.assigned_to) params.set('assigned_to', filter.assigned_to);
|
|
if (filter?.contact_id) params.set('contact_id', filter.contact_id);
|
|
if (filter?.entity_type) params.set('entity_type', filter.entity_type);
|
|
if (filter?.entity_id) params.set('entity_id', filter.entity_id);
|
|
if (filter?.assignee_type) params.set('assignee_type', filter.assignee_type);
|
|
if (filter?.assignee_id) params.set('assignee_id', filter.assignee_id);
|
|
if (filter?.parent_task_id) params.set('parent_task_id', filter.parent_task_id);
|
|
if (filter?.task_type) params.set('task_type', filter.task_type);
|
|
if (filter?.search) params.set('search', filter.search);
|
|
return useQuery({
|
|
queryKey: ['tasks', page, pageSize, filter],
|
|
queryFn: () => apiGet<TaskListResponse>(`/tasks?${params.toString()}`),
|
|
});
|
|
}
|
|
|
|
export function useTask(id?: string) {
|
|
return useQuery({
|
|
queryKey: ['tasks', id],
|
|
queryFn: () => apiGet<Task>(`/tasks/${id}`),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function useCreateTask() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: TaskCreateInput) => apiPost<Task>('/tasks', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateTask() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: TaskUpdateInput }) =>
|
|
apiPatch<Task>(`/tasks/${id}`, data),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteTask() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) => apiDelete(`/tasks/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAssignTask() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, assigneeType, assigneeId }: { id: string; assigneeType: AssigneeType; assigneeId: string }) =>
|
|
apiPost<Task>(`/tasks/${id}/assign`, { assignee_type: assigneeType, assignee_id: assigneeId }),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateTaskStatus() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, status }: { id: string; status: TaskStatus }) =>
|
|
apiPost<Task>(`/tasks/${id}/status`, { status }),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateSubtask() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ parentId, data }: { parentId: string; data: TaskCreateInput }) =>
|
|
apiPost<Task>(`/tasks/${parentId}/subtasks`, data),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.parentId] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useListSubtasks(parentId?: string) {
|
|
return useQuery({
|
|
queryKey: ['tasks', parentId, 'subtasks'],
|
|
queryFn: () => apiGet<Task[]>(`/tasks/${parentId}/subtasks`),
|
|
enabled: !!parentId,
|
|
});
|
|
}
|
|
|
|
export function useAddDependency() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, dependsOn }: { id: string; dependsOn: string }) =>
|
|
apiPost<Task>(`/tasks/${id}/dependencies`, { depends_on: dependsOn }),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRemoveDependency() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, dependsOn }: { id: string; dependsOn: string }) =>
|
|
apiDelete(`/tasks/${id}/dependencies/${dependsOn}`),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDecomposeGoal() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ goalId, subtasks }: { goalId: string; subtasks: TaskCreateInput[] }) =>
|
|
apiPost<{ goal: Task; subtasks: Task[] }>(`/tasks/${goalId}/decompose`, subtasks),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
queryClient.invalidateQueries({ queryKey: ['tasks', variables.goalId] });
|
|
},
|
|
});
|
|
}
|