feat(5.21): Tasks Plugin — activities with status, priority, due dates, ARQ reminders

- New builtin plugin app/plugins/builtins/tasks/ with full CRUD
- Task model with TenantMixin (title, description, status, priority, due_date, assigned_to, contact_id)
- Routes: GET/POST /tasks, GET/PATCH/DELETE /tasks/{id}, POST /tasks/{id}/assign, POST /tasks/{id}/status
- All routes RBAC-protected (tasks:read, tasks:write, tasks:delete)
- ARQ cron job tasks_due_reminder (daily 8:00) sends notifications for due tasks
- Migration 0001_initial.sql creates tasks table with indexes
- Frontend: Tasks.tsx page with list, filter, create/edit modal, detail modal
- Frontend: api/tasks.ts with React Query hooks
- Route /tasks in routes/index.tsx, sidebar entry via plugin manifest
- i18n keys for nav.tasks and tasks.* in de.json and en.json
- Tests: test_tasks.py (11 tests) + Tasks.test.tsx (3 tests)
- Registered TasksPlugin in conftest.py and worker.py
This commit is contained in:
Agent Zero
2026-07-23 23:41:34 +02:00
parent bdad91a649
commit 2c9e74776e
17 changed files with 1536 additions and 2 deletions
+132
View File
@@ -0,0 +1,132 @@
/**
* Tasks API hooks — CRUD, assign, status update.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
export interface Task {
id: string;
title: string;
description: string | null;
status: 'open' | 'in_progress' | 'done';
priority: 'low' | 'medium' | 'high' | 'urgent';
due_date: string | null;
assigned_to: string | null;
contact_id: string | null;
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?: string;
priority?: string;
due_date?: string | null;
assigned_to?: string | null;
contact_id?: string | null;
}
export interface TaskUpdateInput {
title?: string;
description?: string | null;
status?: string;
priority?: string;
due_date?: string | null;
assigned_to?: string | null;
contact_id?: string | null;
}
export interface TaskFilter {
status?: string;
priority?: string;
assigned_to?: string;
contact_id?: string;
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?.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, assignedTo }: { id: string; assignedTo: string }) =>
apiPost<Task>(`/tasks/${id}/assign`, { assigned_to: assignedTo }),
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: string }) =>
apiPost<Task>(`/tasks/${id}/status`, { status }),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
},
});
}