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:
@@ -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] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user