# Tasks: Board Engine

**Date:** 2026-05-30  
**Status:** Draft  
**Depends on:** `foundation-auth-rbac`, `foundation-design-system`, `projects-module`  
**Referenced by:** `tasks-detail-communication`, `time-management`

---

## Overview

The tasks board engine: multiple view layouts (Kanban, List, Timeline/Gantt), drag-and-drop, column management, filters with URL sync, and external task adapter imports. Task detail and real-time communication are in `tasks-detail-communication`.

---

## Task Status Model

Fully customizable per tenant. Defaults seeded on tenant creation:

```ts
const DEFAULT_STATUSES = ['BACKLOG', 'TODO', 'IN_PROGRESS', 'BLOCKED', 'REVIEW', 'TESTING', 'DEPLOY', 'DONE']
```

Statuses overridable per project. Ordered list; order persisted.

```sql
task_statuses (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  project_id UUID,              -- NULL = tenant-global default
  name TEXT NOT NULL,
  color TEXT NOT NULL,          -- CSS variable name or hex (stored as CSS var name)
  position INTEGER NOT NULL,
  is_terminal BOOLEAN DEFAULT false,  -- 'DONE'-type statuses
  created_at TIMESTAMPTZ
)
```

---

## Data Model

```sql
tasks (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  project_id UUID,
  status_id UUID NOT NULL,       -- references task_statuses
  title TEXT NOT NULL,
  description TEXT,              -- rich text (Tiptap JSON stored as JSONB)
  priority TEXT DEFAULT 'medium', -- 'low' | 'medium' | 'high' | 'urgent'
  assignee_id UUID,              -- references users
  reporter_id UUID NOT NULL,
  due_date DATE,
  estimated_hours NUMERIC(6,2),
  source TEXT DEFAULT 'manual',  -- 'manual' | 'email' | 'telegram' | 'trello' | ...
  external_id TEXT,              -- ID in external system
  position NUMERIC NOT NULL,     -- fractional indexing for column order
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
)

task_labels (
  task_id UUID NOT NULL,
  label TEXT NOT NULL,
  PRIMARY KEY (task_id, label)
)
```

### Fractional indexing for position

Card position within a column stored as `NUMERIC` with fractional values (e.g. 1.0, 2.0, 1.5 for between 1 and 2). Avoids re-numbering on drag. Rebalance when gap < 0.001.

---

## Views

### Kanban View

Columns = task statuses. Each column renders tasks sorted by `position`.

**Column behavior:**
- Column header: status name, count badge, "+ Add task" button
- Collapsed column: vertical label + count. Collapse state per column per user in `localStorage('board_columns_{userId}')`.
- Column reordering: drag column header. Order persisted to `user_preferences.board_column_order`.
- Card drag between columns: optimistic update → `PATCH /api/tasks/:id` with new `status_id` + `position`.
- Card drag within column: reorder via position update.

**Task card:**
- Title, priority badge, assignee avatar, due date (red if overdue), label chips, attachment count, comment count.
- Hover: quick actions (assign, set priority, set due date).

**Library:** `@dnd-kit/core` + `@dnd-kit/sortable` — lightweight, accessible, no jQuery.

#### Kanban Keyboard Accessibility (WCAG 2.1 SC 2.1.1)

Drag-and-drop must have a keyboard-operable alternative. dnd-kit provides `KeyboardSensor` — it must be configured:

```ts
import { KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'

const sensors = useSensors(
  useSensor(PointerSensor),
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
```

Keyboard interaction: Space/Enter to pick up, Arrow keys to move, Space/Enter to drop, Escape to cancel.

`DndContext` must set `announcements` for screen reader live region:

```ts
const announcements: Announcements = {
  onDragStart({ active }) { return `Picked up task ${active.data.current?.title}.` },
  onDragOver({ active, over }) {
    return over ? `Moving task ${active.data.current?.title} over column ${over.data.current?.status}.` : undefined
  },
  onDragEnd({ active, over }) {
    return over
      ? `Task ${active.data.current?.title} moved to column ${over.data.current?.status}.`
      : `Task ${active.data.current?.title} returned to original position.`
  },
  onDragCancel({ active }) { return `Drag cancelled for task ${active.data.current?.title}.` },
}
```

Each `SortableItem` (task card) gets `role="button"` and `aria-label="Drag to reorder: {title}"`. Column droppable areas get `aria-label="{status name} column — {count} tasks"`.

### List View

Table layout. Columns: Title, Status, Priority, Assignee, Project, Due date, Created.  
Sortable columns. Inline status/priority edit (popover select).  
Row click → task detail.

### Timeline View (Gantt)

Library: **dhtmlx Gantt** (referenced in Zync.txt). Renders tasks with start/due dates on a horizontal timeline.

Config:
- Group by: Project (default), Status, Assignee
- Zoom: Day, Week, Month
- Drag to reschedule: updates `due_date` via PATCH
- Read-only for tasks without dates (displayed in unscheduled column)

dhtmlx Gantt is GPLv2 for open-source or requires commercial license. Use the GPL build and note license requirement in project docs.

#### Timeline View — Keyboard Accessibility (WCAG 2.1 SC 2.1.1)

The Kanban view uses dnd-kit `KeyboardSensor`. The Timeline view must have equivalent keyboard coverage. No interaction may be drag-only.

**Gantt bar focus and activation:**
- Bars: focusable via Tab; `role="button"` `aria-label="Task '{title}': {startDate} to {endDate}"`
- `Left`/`Right Arrow` (while bar is focused): extend/shrink task duration by 1 day
- `Enter`/`Space`: open task detail panel

**Row reordering:**
- Same `KeyboardSensor` pattern as Kanban: `Space` to pick up, `Arrow` keys to reorder rows, `Space`/`Enter` to drop, `Escape` to cancel
- `DndContext` `announcements` prop required: `"Picked up task {title}"`, `"Moved {title} to row {n}"`, `"Dropped {title}"`

**Date grid scroll:**
- `Left`/`Right Arrow` on the timeline grid container (when no bar is focused): scrolls the visible date window
- Focus management must prevent conflict between bar resize and grid scroll — only the focused element receives arrows

**Screen reader summary:**
- Visually-hidden `<caption>` on the Gantt table: `"{n} tasks, spanning {startDate} to {endDate}"`

**Invariant:** every Gantt resize and reorder operation must have a keyboard equivalent.

### Board Preferences (Zustand store)

```ts
interface BoardPreferences {
  view: 'kanban' | 'list' | 'timeline'
  columnOrder: Record<string, number>      // statusId → display position
  collapsedColumns: Record<string, boolean>
}
// Persisted: zustand-persist to localStorage
```

---

## Filters

Applied to all views. State synced to URL search params.

| Filter | URL param | Type |
|--------|-----------|------|
| Project | `project` | UUID |
| Status | `status` | comma-separated status IDs |
| Priority | `priority` | comma-separated values |
| Assignee | `assignee` | UUID |
| Source | `source` | comma-separated |
| Due date | `due_from`, `due_to` | ISO dates |
| Labels | `labels` | comma-separated |
| Search | `q` | text |

Active filter chips rendered below the toolbar. Each chip: label + × to remove. "Reset filters" clears all.

Filter state is URL-native: bookmarkable, shareable, back-button safe.

---

## External Adapter Imports

Adapters import tasks from external systems. All adapters implement:

```ts
interface TaskAdapter {
  id: string
  name: string
  fetchTasks(credentials: AdapterCredentials): Promise<ExternalTask[]>
  mapToTask(external: ExternalTask, tenantId: string, projectId: string): Partial<Task>
  pushUpdate?(task: Task, credentials: AdapterCredentials): Promise<void>  // 2-way sync
}
```

Adapters: Trello, Asana, Jira, Monday, ClickUp, Slack.

Sync schedule: configurable per-tenant (default: every 2h). Triggered by Cloudflare Cron → `/api/cron/tasks-sync` (CRON_SECRET protected). Manual "sync now" from settings.

Sync result logged to `integration_sync_logs` (see `settings-module`).

External task `source` field + `external_id` prevent duplicate imports.

### Auto-create from inbound messages

When enabled in this board's settings (the auto-create rules are configured per board, in the board settings panel owned by this spec — not a standalone `/settings` route):

| Inbound source | Config | Behavior |
|---------------|--------|---------|
| Email | Configured email → tenant inbox | Subject = task title, body = description |
| Telegram | Bot message | Message text = task title |
| Slack | DM to bot or channel mention | Message text = task title |
| WhatsApp | Enterprise only | Message = task title |

Route: inbound messages from `system-communications-notifications` queue → task creation if `auto_create_tickets_from.X = true`.

---

## Permissions

| Action | Required permission |
|--------|-------------------|
| View tasks | `tasks:read` |
| Create task | `tasks:write` |
| Edit own tasks | `tasks:write` |
| Edit any task | `tasks:write` |
| Delete task | `tasks:delete` |
| Assign task | `tasks:assign` |

---

## API Endpoints

```
GET    /api/tasks                         → list (filterable, paginated)
POST   /api/tasks                         → create
PATCH  /api/tasks/:id                     → update (status, position, assignee, etc.)
DELETE /api/tasks/:id                     → delete
GET    /api/tasks/statuses                → list statuses for tenant
POST   /api/tasks/statuses                → create status
PATCH  /api/tasks/statuses/:id            → update status
DELETE /api/tasks/statuses/:id            → delete (reassign tasks first)
PATCH  /api/tasks/statuses/reorder        → update positions
POST   /api/tasks/bulk                    → bulk status update
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| DnD library | @dnd-kit | Accessible, no jQuery, Sortable + Draggable compose cleanly |
| Card position | Fractional numeric | Efficient reorder without renumbering; Postgres NUMERIC precision sufficient |
| Gantt library | dhtmlx Gantt | Directly referenced in Zync.txt; feature-complete |
| Column order | User preference (localStorage + Zustand) | Per-user board layout, not global |
| Filter state | URL search params | Bookmarkable, shareable, back-nav safe |
| 2-way sync | Adapter `pushUpdate` optional | Not all adapters support it; Trello/Jira do, Slack doesn't |
