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
+134
View File
@@ -0,0 +1,134 @@
/**
* Tasks page tests — basic rendering and interaction.
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { TasksPage } from '@/pages/Tasks';
import * as tasksApi from '@/api/tasks';
// Mock i18n
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
// Mock toast
vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
}),
}));
// Mock UI components
vi.mock('@/components/ui/Input', () => ({
Input: ({ value, onChange, type, label, ...props }: any) => (
<div>
{label && <label>{label}</label>}
<input
data-testid={props['data-testid'] || 'input'}
type={type || 'text'}
value={value || ''}
onChange={onChange}
/>
</div>
),
}));
vi.mock('@/components/ui/Select', () => ({
Select: ({ value, onChange, options, label, ...props }: any) => (
<div>
{label && <label>{label}</label>}
<select data-testid={props['data-testid'] || 'select'} value={value || ''} onChange={onChange}>
{options?.map((o: any) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
),
}));
vi.mock('@/components/ui/Modal', () => ({
Modal: ({ open, onClose, title, children }: any) =>
open ? (
<div data-testid="modal" role="dialog">
<h2>{title}</h2>
<button onClick={onClose}>Close</button>
{children}
</div>
) : null,
}));
vi.mock('@/components/ui/Button', () => ({
Button: ({ children, onClick, disabled, ...props }: any) => (
<button onClick={onClick} disabled={disabled} data-testid={props['data-testid'] || 'button'}>
{children}
</button>
),
}));
vi.mock('@/components/ui/Badge', () => ({
Badge: ({ children, variant }: any) => <span data-testid="badge" data-variant={variant}>{children}</span>,
}));
vi.mock('@/components/ui/EmptyState', () => ({
EmptyState: ({ title, description }: any) => (
<div data-testid="empty-state">
<h3>{title}</h3>
<p>{description}</p>
</div>
),
}));
// Mock tasks API
vi.mock('@/api/tasks', () => ({
useTasks: vi.fn(() => ({
data: {
items: [
{
id: 'task-1',
title: 'Test Task',
description: 'Test description',
status: 'open',
priority: 'high',
due_date: '2025-12-31T10:00:00Z',
assigned_to: null,
contact_id: null,
created_by: null,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
],
total: 1,
page: 1,
page_size: 25,
},
isLoading: false,
})),
useCreateTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
useUpdateTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
useDeleteTask: vi.fn(() => ({ mutateAsync: vi.fn() })),
useUpdateTaskStatus: vi.fn(() => ({ mutateAsync: vi.fn() })),
}));
describe('TasksPage', () => {
it('renders the tasks page with header', () => {
render(<TasksPage />);
expect(screen.getByTestId('tasks-page')).toBeInTheDocument();
expect(screen.getByText('tasks.title')).toBeInTheDocument();
});
it('renders task items from API', () => {
render(<TasksPage />);
expect(screen.getByText('Test Task')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('opens create modal when create button is clicked', () => {
render(<TasksPage />);
const createBtn = screen.getByText('tasks.create');
fireEvent.click(createBtn);
expect(screen.getByTestId('modal')).toBeInTheDocument();
expect(screen.getByTestId('task-title-input')).toBeInTheDocument();
});
});