# GeoLynx Task System Plan

## Overview

A configurable workflow and task management system built into GeoLynx. Workflows are sets of sequenced, dependency-linked tasks that can be applied to projects (and optionally accounts or stocklists). All configuration is done in the admin area. Users interact with task instances on the item edit pages and via a personal task list.

---

## Core Concepts

| Term | Definition |
|---|---|
| **Task Template** | A reusable task definition — name, description, default SLA. Managed in admin. |
| **Workflow** | An ordered collection of task templates with dependency rules. e.g. "Fibre Build", "Wayleave Process". Managed in admin. |
| **Workflow Task** | A task template placed within a specific workflow, with an optional SLA override and sequence position. |
| **Dependency** | A prerequisite link between two workflow tasks. Task B cannot start until Task A reaches a unblocking status. |
| **Workflow Instance** | A workflow applied to a specific project/account/stocklist. Groups all resulting task instances together. |
| **Task Instance** | The live, operational record of a single task — has an assignee, status, ECD, SLA due date, and comment trail. |

---

## Features

- **Multiple workflows** — different workflow types for different project types (e.g. FTTP build vs. wayleave-only).
- **Dependency enforcement** — a task instance is not available to start until all its prerequisite task instances are in a unblocking/complete state.
- **Parallel tasks** — workflow tasks with no dependency between them become available simultaneously.
- **Blocking / on-hold** — a task instance can be placed on hold; an ad-hoc blocking task can be created to capture the unblocking work and assigned to another user.
- **SLA tracking** — each workflow task carries a configurable SLA (days). The SLA due date is calculated from when the task becomes available.
- **User-entered ECDs** — users can record their own estimated completion date alongside the system-calculated SLA date.
- **Comment trail** — multiple timestamped, user-attributed comments per task instance.
- **Audit trail** — all configuration and instance records capture created_by / created_at / modified_by / modified_at.
- **Admin-only configuration** — workflows, templates, statuses, and dependencies are managed exclusively in the admin area via the `admin_tasks` permission module.
- **Workflow graph designer** — a live Cytoscape.js DAG in the admin workflow builder shows tasks as nodes and dependencies as directed edges, updating in real-time as the workflow is configured. Prevents circular dependencies visually before server validation.
- **Live progress graph on entity pages** — when a workflow instance is active on a project/account/stocklist, a Cytoscape DAG shows every task node coloured by its current status. Clicking a node opens the task detail panel. Updates on every status change.
- **Team assignment** *(future)* — currently assigns to an individual user. Will extend to team assignment once the team system is built.

---

## Database Schema

### Configuration Layer (admin-managed)

#### `tasks.task_statuses`
Configurable set of statuses. Controls whether a status unblocks downstream tasks or terminates a task.

```sql
CREATE TABLE tasks.task_statuses (
    status_id      serial PRIMARY KEY,
    status_name    varchar(100) NOT NULL,
    status_colour  varchar(7),                    -- hex colour for UI badges
    is_blocking    boolean NOT NULL DEFAULT false, -- true = task is blocked/on hold
    is_terminal    boolean NOT NULL DEFAULT false, -- true = unblocks dependents (complete/cancelled)
    display_order  integer,
    is_active      boolean NOT NULL DEFAULT true,
    created_at     timestamp without time zone DEFAULT now(),
    created_by     bigint REFERENCES users.users(id)
);
```

Seed data:

| status_name | is_blocking | is_terminal | colour |
|---|---|---|---|
| Not Started | false | false | `#6c757d` |
| In Progress | false | false | `#0d6efd` |
| On Hold | true | false | `#ffc107` |
| Blocked | true | false | `#dc3545` |
| Complete | false | true | `#198754` |
| Cancelled | false | true | `#adb5bd` |

#### `tasks.task_templates` (enhanced from existing)
```sql
CREATE TABLE tasks.task_templates (
    template_id      serial PRIMARY KEY,
    name             varchar(255) NOT NULL,
    description      text,
    default_sla_days integer,
    is_active        boolean NOT NULL DEFAULT true,
    created_at       timestamp without time zone DEFAULT now(),
    created_by       bigint REFERENCES users.users(id),
    modified_at      timestamp without time zone DEFAULT now(),
    modified_by      bigint REFERENCES users.users(id)
);
```

#### `tasks.workflows` (enhanced from existing)
```sql
CREATE TABLE tasks.workflows (
    workflow_id  serial PRIMARY KEY,
    name         varchar(255) NOT NULL,
    description  text,
    applies_to   varchar(20) NOT NULL DEFAULT 'project', -- 'project' | 'account' | 'stocklist'
    is_active    boolean NOT NULL DEFAULT true,
    created_at   timestamp without time zone DEFAULT now(),
    created_by   bigint REFERENCES users.users(id),
    modified_at  timestamp without time zone DEFAULT now(),
    modified_by  bigint REFERENCES users.users(id)
);
```

#### `tasks.workflow_tasks` (enhanced from existing)
```sql
CREATE TABLE tasks.workflow_tasks (
    workflow_task_id  serial PRIMARY KEY,
    workflow_id       integer NOT NULL REFERENCES tasks.workflows(workflow_id) ON DELETE CASCADE,
    template_id       integer NOT NULL REFERENCES tasks.task_templates(template_id),
    sequence_order    integer NOT NULL DEFAULT 0,
    custom_name       varchar(255),           -- overrides template name if set
    custom_description text,                  -- overrides template description if set
    sla_days          integer,                -- overrides template default_sla_days if set
    created_at        timestamp without time zone DEFAULT now(),
    created_by        bigint REFERENCES users.users(id),
    UNIQUE (workflow_id, template_id, sequence_order)
);
```

#### `tasks.task_dependencies` (fix schema reference from existing)
```sql
CREATE TABLE tasks.task_dependencies (
    dependency_id        serial PRIMARY KEY,
    workflow_task_id     integer NOT NULL REFERENCES tasks.workflow_tasks(workflow_task_id) ON DELETE CASCADE,
    prerequisite_task_id integer NOT NULL REFERENCES tasks.workflow_tasks(workflow_task_id) ON DELETE CASCADE,
    UNIQUE (workflow_task_id, prerequisite_task_id),
    CHECK (workflow_task_id != prerequisite_task_id)
);
```

---

### Instance Layer (operational)

#### `tasks.workflow_instances`
Created when a workflow is applied to an item. Groups all resulting task instances.

```sql
CREATE TABLE tasks.workflow_instances (
    workflow_instance_id  bigserial PRIMARY KEY,
    workflow_id           integer NOT NULL REFERENCES tasks.workflows(workflow_id),
    item_type             varchar(20) NOT NULL,  -- 'project' | 'account' | 'stocklist'
    item_id               integer NOT NULL,
    started_at            timestamp without time zone DEFAULT now(),
    started_by            bigint REFERENCES users.users(id),
    completed_at          timestamp without time zone,
    is_active             boolean NOT NULL DEFAULT true
);

CREATE INDEX idx_workflow_instances_item ON tasks.workflow_instances(item_type, item_id);
```

#### `tasks.task_instances`
One row per task per workflow run. The operational record.

```sql
CREATE TABLE tasks.task_instances (
    task_instance_id     bigserial PRIMARY KEY,
    workflow_instance_id bigint NOT NULL REFERENCES tasks.workflow_instances(workflow_instance_id) ON DELETE CASCADE,
    workflow_task_id     integer NOT NULL REFERENCES tasks.workflow_tasks(workflow_task_id),
    status_id            integer NOT NULL REFERENCES tasks.task_statuses(status_id),
    assigned_to          bigint REFERENCES users.users(id),
    available_at         timestamp without time zone,    -- set when all prerequisites reach is_terminal
    sla_due_date         timestamp without time zone,    -- available_at + sla_days
    ecd                  date,                           -- user-entered estimated completion date
    started_at           timestamp without time zone,
    completed_at         timestamp without time zone,
    blocking_task_id     bigint REFERENCES tasks.task_instances(task_instance_id), -- ad-hoc blocking task
    created_at           timestamp without time zone DEFAULT now(),
    created_by           bigint REFERENCES users.users(id),
    modified_at          timestamp without time zone DEFAULT now(),
    modified_by          bigint REFERENCES users.users(id)
);

CREATE INDEX idx_task_instances_workflow   ON tasks.task_instances(workflow_instance_id);
CREATE INDEX idx_task_instances_assigned   ON tasks.task_instances(assigned_to);
CREATE INDEX idx_task_instances_status     ON tasks.task_instances(status_id);
```

#### `tasks.task_instance_comments`
Append-only comment trail per task instance.

```sql
CREATE TABLE tasks.task_instance_comments (
    comment_id       bigserial PRIMARY KEY,
    task_instance_id bigint NOT NULL REFERENCES tasks.task_instances(task_instance_id) ON DELETE CASCADE,
    comment_text     text NOT NULL,
    created_at       timestamp without time zone DEFAULT now(),
    created_by       bigint NOT NULL REFERENCES users.users(id)
);

CREATE INDEX idx_task_comments_instance ON tasks.task_instance_comments(task_instance_id);
```

---

## Dependency Logic

### Availability calculation
When a task instance's status changes to a terminal state (`is_terminal = true`):
1. Find all `task_dependencies` rows where `prerequisite_task_id = this workflow_task_id`
2. For each dependent workflow task, check whether **all** its prerequisites are now terminal
3. If yes, set `available_at = now()` and `sla_due_date = now() + sla_days` on that task instance
4. The first task(s) in a workflow (no prerequisites) have `available_at` set at workflow instance creation time

### On-hold / blocking
When a task instance is set to "On Hold" or "Blocked":
- A new ad-hoc `task_instance` can be created (not tied to a `workflow_task`) to represent the unblocking work
- The original task's `blocking_task_id` points to this ad-hoc task
- When the blocking task reaches a terminal status, the original task's status can be updated to resume

---

## Routing & File Conventions

Following the existing pattern in `global_functions.php::load_file()`.

### New routes

| Route (`?do=`) | Purpose |
|---|---|
| `admin_tasks` | Admin: manage workflows, templates, statuses, dependencies |
| `tasks` | User: personal task list (all tasks assigned to me) |

Tasks for a specific item are rendered as a tab within the existing edit pages (projectedit, accountedit, stocklistedit) — no separate route needed.

### New files

| Type | File | Purpose |
|---|---|---|
| PHP | `www/fn/task_admin_load.php` | Admin config read endpoints |
| PHP | `www/fn/task_admin_save.php` | Admin config write endpoints |
| PHP | `www/fn/task_load.php` | Instance read endpoints (task list, task detail) |
| PHP | `www/fn/task_save.php` | Instance write endpoints (create, update status, comment, assign) |
| HTML | `www/html/html_body_admin_tasks.php` | Admin UI shell |
| HTML | `www/html/html_body_tasks.php` | Personal task list UI shell |
| JS | `www/js/admin_tasks.js` | Admin config logic |
| JS | `www/js/tasks.js` | Personal task list logic |

Tasks tab on item edit pages is implemented inline in `project_edit_v2.js`, `account_edit_v2.js`, `stocklist_edit.js` — no new JS files needed for that.

### Permission module

Add `admin_tasks` to `role_permissions` module allowlist in `admin_save.php::rolePermissionSet()`.

---

## Admin UI (`?do=admin_tasks`)

Three tabs:

### Tab 1 — Statuses
- Tabulator table of `task_statuses`
- Inline edit: status_name, colour picker, is_blocking, is_terminal, display_order, is_active
- Add / deactivate actions

### Tab 2 — Task Templates
- Tabulator table of `task_templates`
- Columns: name, description, default_sla_days, is_active
- Inline edit; add / deactivate

### Tab 3 — Workflows
- List of workflows (left panel) — add / edit / deactivate
- Selecting a workflow opens a detail panel with three sub-sections:

**Task sequence** — ordered list of workflow tasks (drag to reorder, updates `sequence_order`):
- Columns: order, task name (from template or custom_name), SLA days (override), actions
- Add task: pick from template list, optionally override name/description/sla_days
- Remove task (only if no live task instances reference it)

**Dependencies** — checklist per workflow task:
- For each workflow task, a checklist of other tasks in the same workflow that must complete before it
- Writes to `task_dependencies`
- Prevent circular dependencies (validate server-side)
- Any change immediately refreshes the workflow graph below

**Workflow graph (Cytoscape.js DAG)**
- Rendered below the task sequence and dependency editors using Cytoscape.js with the `dagre` layout (both already loaded in the app)
- Nodes: each workflow task — displays `custom_name` or template name + SLA days
- Edges: directed arrows from prerequisite → dependent task
- Layout auto-updates whenever a task or dependency is added, removed, or reordered — no page reload needed
- Node colours are neutral in the designer (not status colours — no live data at this stage); use the workflow's brand colour or a fixed palette to distinguish parallel branches visually
- Read-only — editing is done via the task sequence and dependency panels above, not by dragging edges on the graph
- If a circular dependency is detected server-side on save, the graph highlights the offending edge in red

---

## Backend Endpoints

### `task_admin_load.php`

| Mode | Returns |
|---|---|
| `statuses_list` | All `task_statuses` |
| `templates_list` | All `task_templates` |
| `workflows_list` | All `workflows` |
| `workflow_detail` | Single workflow with its `workflow_tasks` and `task_dependencies` |

### `task_admin_save.php`

| Mode | Action |
|---|---|
| `status_add` / `status_update` / `status_toggle_active` | CRUD on `task_statuses` |
| `template_add` / `template_update` / `template_toggle_active` | CRUD on `task_templates` |
| `workflow_add` / `workflow_update` / `workflow_toggle_active` | CRUD on `workflows` |
| `workflow_task_add` / `workflow_task_update` / `workflow_task_remove` | Manage tasks within a workflow |
| `workflow_tasks_reorder` | Bulk update `sequence_order` |
| `dependency_set` | Replace all dependencies for a workflow task |

### `task_load.php`

| Mode | Returns |
|---|---|
| `my_tasks` | All open task instances assigned to current user, with item name and workflow name |
| `item_tasks` | All workflow instances + task instances for a given `item_type` / `item_id` |
| `task_detail` | Single task instance with comments |
| `available_workflows` | Workflows where `applies_to` matches a given item type and `is_active = true` |
| `workflow_graph_data` | Nodes + edges for a workflow definition (admin designer) or a live workflow instance (entity progress view) — see Visualisations section |

### `task_save.php`

| Mode | Action |
|---|---|
| `workflow_start` | Create `workflow_instance` + all `task_instances`; set `available_at` on tasks with no prerequisites |
| `task_update_status` | Update `status_id` on a task instance; trigger availability check on dependents |
| `task_assign` | Set `assigned_to` on a task instance |
| `task_set_ecd` | Set `ecd` on a task instance |
| `task_comment_add` | Insert into `task_instance_comments` |
| `blocking_task_create` | Create ad-hoc task instance and link to `blocking_task_id` |
| `workflow_cancel` | Set all non-terminal task instances to Cancelled; mark workflow instance inactive |

---

## Operational UI

### Tasks tab on item edit pages (project/account/stocklist)

Rendered inside the existing edit page. Shows:

1. **Active workflows** — one card per `workflow_instance`, showing overall % complete (terminal tasks / total tasks)
2. **Workflow progress graph** — Cytoscape.js DAG showing the live state of the selected workflow instance (see Visualisations section). Renders immediately below the workflow selector; updates on every status change without a page reload.
3. **Task list** — Tabulator table of task instances for the selected workflow:
   - Columns: Task name, Assigned to, Status (badge), Available, SLA due date, ECD, days overdue
   - Locked rows for tasks that are not yet available (prerequisites not met) — visually greyed out
   - Click a row to open task detail panel (also reachable by clicking the corresponding graph node)
4. **Task detail panel** — status update dropdown, assign user, set ECD, comment thread
5. **Start workflow button** — opens a modal to select a workflow and confirm

### Personal task list (`?do=tasks`)

Tabulator table of all task instances assigned to the current user where status is not terminal:
- Columns: Item (linked), Workflow, Task name, Status, SLA due date, ECD, Days overdue
- Sortable/filterable
- Click row → navigates to the item edit page (tasks tab pre-selected)

### Dashboard widget *(future)*
Count of open tasks assigned to current user + overdue count.

---

## Visualisations

Both graph views use **Cytoscape.js** with the **`cytoscape-dagre`** layout — already loaded globally in `index.php`. No additional library dependencies are required.

---

### 1 — Workflow Graph Designer (admin)

**Where:** Tab 3 of `?do=admin_tasks`, rendered below the task sequence and dependency editors.

**Data source:** `task_load.php` mode `workflow_graph_data` with `context=designer&workflow_id=X`. Returns nodes (one per `workflow_task`) and edges (one per `task_dependency`).

**Node label:** `custom_name` if set, otherwise `task_templates.name`. Sub-label: SLA days (e.g. `7d`).

**Node colour:** Fixed neutral palette — not status colours. Use a single brand colour (e.g. `#0d6efd`) for all nodes; distinguish parallel branches only through graph layout, not colour.

**Edges:** Directed arrows from prerequisite → dependent task.

**Interactivity:**
- Read-only — no drag-to-connect. Editing is done via the sequence and dependency panels above.
- Graph re-renders (via `cy.json()` + layout re-run) whenever a task or dependency is added, removed, or reordered — no page reload.
- If the server returns a `circular_dependency` error on a `dependency_set` save, the offending edge is highlighted red (`line-color: #dc3545`) until the graph is next refreshed.

**Layout:** `dagre` with `rankDir: 'LR'` (left-to-right flow) so the sequence reads naturally from start to finish.

---

### 2 — Live Progress Graph (entity pages)

**Where:** Tasks tab on `projectedit`, `accountedit`, and `stocklistedit` — rendered above the task list Tabulator table, inside the active workflow card.

**Data source:** `task_load.php` mode `workflow_graph_data` with `context=instance&workflow_instance_id=X`. Returns the same node/edge structure as the designer, but each node also carries:
- `status_id`, `status_name`, `status_colour` (from `task_statuses`)
- `is_available` (boolean — `available_at IS NOT NULL`)
- `task_instance_id` (for click-to-open)

**Node colour:** `task_statuses.status_colour` for available tasks. Unavailable tasks (prerequisites not met) render with a greyed border and reduced opacity (`opacity: 0.4`) to signal they cannot be acted on yet.

**Node badge:** Small status label inside or below the node shape.

**Interactivity:**
- Clicking a node fires `openTaskDetail(task_instance_id)` — same handler as clicking the corresponding Tabulator row.
- Graph re-renders after every `task_update_status` response so the colour changes are reflected immediately.
- Hovering a node shows a tooltip: task name + SLA due date (if set).

**Layout:** `dagre` with `rankDir: 'LR'`. Graph is read-only — no node dragging.

---

### `workflow_graph_data` response shape

```json
{
  "nodes": [
    {
      "data": {
        "id": "wt_12",
        "label": "Survey",
        "sub_label": "7d",
        "status_colour": "#198754",
        "status_name": "Complete",
        "is_available": true,
        "task_instance_id": 45
      }
    }
  ],
  "edges": [
    {
      "data": {
        "id": "dep_8",
        "source": "wt_11",
        "target": "wt_12"
      }
    }
  ]
}
```

For the designer context, `status_colour`, `status_name`, `is_available`, and `task_instance_id` are omitted (or `null`).

---

## Availability Check — Server-Side Function

A reusable PHP function in `global_functions.php`:

```php
function updateTaskAvailability($dbh, $workflowInstanceId)
```

Called after any `task_update_status` save. Logic:
1. Load all task instances for the workflow instance with their prerequisite links
2. For each task instance where `available_at IS NULL`:
   - Check all prerequisite task instances — are all in a terminal status?
   - If yes: set `available_at = now()`, calculate `sla_due_date = now() + sla_days`
3. Check if all task instances are terminal → mark `workflow_instances.completed_at`

---

## Build Order

1. **DDL** — run `task_workflow_system.sql` (complete schema replacing the draft)
2. **Admin: statuses** — `task_admin_load.php` + `task_admin_save.php` (statuses modes) + Tab 1 UI
3. **Admin: templates** — extend endpoints + Tab 2 UI
4. **Admin: workflows + tasks** — extend endpoints + Tab 3 UI (sequence, no dependencies yet)
5. **Admin: dependencies + designer graph** — dependency UI + `dependency_set` endpoint + circular dependency validation + `workflow_graph_data` (designer context) + Cytoscape DAG in Tab 3
6. **Instance creation** — `workflow_start` endpoint + `updateTaskAvailability()` helper
7. **Tasks tab on projectedit** — `item_tasks`, `task_update_status`, `task_assign`, `task_set_ecd`, `task_comment_add` + `workflow_graph_data` (instance context) + live progress Cytoscape graph above task list
8. **Blocking task flow** — `blocking_task_create` endpoint + UI to create/link blocking tasks
9. **Personal task list** — `?do=tasks` page + `my_tasks` endpoint
10. **Account and stocklist tabs** — re-use tasks tab pattern from step 7 (progress graph included)
11. **Extend admin_area permission** — add `admin_tasks` to `rolePermissionSet` allowlist + page gate in `index.php`
