Files
leocrm/frontend/src/api/tasks.ts
T
Agent Zero a53dcc38d5
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(F.14): Unified Task System — F-TASK-MODEL/API/AGENT/WORK/UI/MIG/GOAL/TEST
- 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
2026-08-17 18:51:22 +02:00

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] });
},
});
}