From d397cd9d7a4ba93f149a39075a37fcd4edb2a2be Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Tue, 24 Mar 2026 21:14:52 +0100 Subject: [PATCH 01/13] Add openspec structure files from template - Merge openspec/README.md: keep Planix goal section, add artifact progression diagram, workflow steps, and full commands table - Add openspec/ROADMAP.md for feature tracking - Add openspec/architecture/README.md with ADR format guide - Add openspec/specs/README.md with feature spec format and lifecycle --- openspec/README.md | 59 +++++++++++++++++++++++---- openspec/ROADMAP.md | 35 ++++++++++++++++ openspec/architecture/README.md | 66 ++++++++++++++++++++++++++++++ openspec/specs/README.md | 72 +++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 openspec/ROADMAP.md create mode 100644 openspec/architecture/README.md create mode 100644 openspec/specs/README.md diff --git a/openspec/README.md b/openspec/README.md index 27271a9..cfbfe2f 100644 --- a/openspec/README.md +++ b/openspec/README.md @@ -1,6 +1,6 @@ # Planix — OpenSpec -This folder contains the configuration and specifications for Planix. +This folder contains feature specifications, architectural decisions, and implementation specs for Planix. ## Goal @@ -10,13 +10,56 @@ Planix is a Kanban-based project and task management app for Nextcloud, built as | File / Folder | Purpose | |---|---| -| `app-config.json` | Core app configuration — all choices from `/opsx:app-create` and `/opsx:app-explore` | -| `config.yaml` | OpenSpec project config — rules, context, standards | -| `specs/` | Feature specifications | -| `changes/` | In-progress and archived OpenSpec changes | +| `app-config.json` | App identity, configuration, and tracked decisions — written by `/opsx:app-explore` | +| `config.yaml` | OpenSpec CLI project configuration — context and rules | +| `specs/` | Feature specs — what the app should do (input for OpenSpec changes) | +| `architecture/` | App-specific Architectural Decision Records (ADRs) | +| `changes/` | Individual change directories, each with a full set of specification artifacts (created on first change) | + +> If `app-config.json` has `"requiresOpenRegister": true`, install [OpenRegister](https://github.com/ConductionNL/openregister) before enabling this app. Planix requires OpenRegister as its data storage layer. + +## Artifact Progression + +Each change in `changes/` moves through these artifacts: + +``` +proposal.md ──► specs/ ──► design.md ──► tasks.md ──► plan.json + │ + ▼ + GitHub Issues + │ + ▼ + implementation + │ + ▼ + review.md + │ + ▼ + archive/ +``` + +## Workflow + +1. **Explore** — Use `/opsx:app-explore planix` to think through goals, architecture, and features; captures decisions into `app-config.json` +2. **Plan** — When a feature spec reaches `planned` status, use `/opsx:ff` to create a change spec +3. **Implement** — Use `/opsx:apply` to implement the tasks +4. **Verify** — Use `/opsx:verify` to check implementation matches the spec +5. **Archive** — Use `/opsx:archive` to move completed changes to `changes/archive/` ## Commands -- `/opsx:app-explore planix` — Think through and update app configuration interactively -- `/opsx:app-apply planix` — Apply `app-config.json` changes to the actual app files -- `/opsx:ff {feature-name}` — Implement a planned feature from `specs/` +| Command | Purpose | +|---------|---------| +| `/opsx:app-design` | Full upfront design — architecture, features, wireframes (optional pre-step) | +| `/opsx:app-create` | Bootstrap a new app or onboard an existing repo | +| `/opsx:app-explore planix` | Think through goals, architecture, and features; updates `app-config.json` | +| `/opsx:app-apply planix` | Apply `app-config.json` decisions to actual app files | +| `/opsx:app-verify planix` | Audit app files against `app-config.json` (read-only) | +| `/opsx:explore` | Investigate a problem or idea before starting a change (no output) | +| `/opsx:ff {name}` | Create all artifacts for a new change at once | +| `/opsx:new {name}` | Start a new change (step-by-step) | +| `/opsx:continue` | Generate the next artifact in the sequence | +| `/opsx:plan-to-issues` | Convert tasks.md into plan.json and GitHub Issues | +| `/opsx:apply` | Implement tasks from a change | +| `/opsx:verify` | Verify implementation matches the spec | +| `/opsx:archive` | Archive a completed change | diff --git a/openspec/ROADMAP.md b/openspec/ROADMAP.md new file mode 100644 index 0000000..1b20004 --- /dev/null +++ b/openspec/ROADMAP.md @@ -0,0 +1,35 @@ +# Roadmap + +This document tracks the planned development of Planix. + +Features are defined in [`openspec/specs/`](specs/). When a feature reaches `planned` status during an `/opsx:app-explore` session, it is listed here and an OpenSpec change is created with `/opsx:ff`. + +## Status Overview + +| Feature | Status | Priority | OpenSpec Change | +|---------|--------|----------|----------------| +| _(no features defined yet — use `/opsx:app-explore planix` to start)_ | — | — | — | + +## Phases + +### Phase 1 — Foundation + +_Define the core features needed for a working app. These are the minimum set that make the app useful._ + +### Phase 2 — Enhancement + +_Add features that improve the experience, extend functionality, and cover more use cases._ + +### Phase 3 — Polish + +_Performance, accessibility improvements, full localization, and hardening for production._ + +--- + +## How This Works + +1. Run `/opsx:app-explore planix` to define features in `openspec/specs/` +2. When a feature is `planned`, add it to the table above +3. Run `/opsx:ff {feature-name}` to create the implementation spec +4. Update the **OpenSpec Change** column with a link to the change directory +5. When all changes for a feature are done, mark the feature `done` diff --git a/openspec/architecture/README.md b/openspec/architecture/README.md new file mode 100644 index 0000000..70553e7 --- /dev/null +++ b/openspec/architecture/README.md @@ -0,0 +1,66 @@ +# Architectural Decision Records + +This folder contains app-specific Architectural Decision Records (ADRs) for Planix. + +ADRs document significant design decisions, their context, the reasoning behind them, and the alternatives that were considered. They provide a historical record of why Planix is built the way it is. + +> **Note:** Organisation-wide ADRs (ADR-001 through ADR-015) live in `apps-extra/.claude/openspec/architecture/` and apply to all Conduction apps. Only create an app-specific ADR here when the decision is **unique to Planix** and not already covered by an org-wide ADR. + +ADRs are created and refined during `/opsx:app-explore planix` sessions. + +## Naming Convention + +Files are named `adr-{NNN}-{slug}.md` with sequential numbering: + +- `adr-001-example-decision.md` +- `adr-002-another-decision.md` + +## File Format + +```markdown +# ADR-{NNN}: {Title} + +**Status**: proposed | accepted | deprecated | superseded by [ADR-XXX] + +**Date**: YYYY-MM-DD + +## Context + +What situation or problem prompted this decision? What constraints exist? + +## Decision + +What was decided? + +## Consequences + +**Positive:** +- ... + +**Negative / trade-offs:** +- ... + +## Alternatives Considered + +| Option | Reason not chosen | +|--------|------------------| +| ... | ... | +``` + +## Status Values + +| Status | Meaning | +|--------|---------| +| `proposed` | Being discussed — not yet in effect | +| `accepted` | Agreed and in effect | +| `deprecated` | No longer applies (but kept for history) | +| `superseded` | Replaced by a newer ADR (reference the new one) | + +## When to Write an ADR + +Write an ADR whenever you make a significant decision that: +- Is hard to reverse +- Affects multiple parts of the codebase +- Would surprise future developers if they didn't know the reasoning +- Involves a meaningful trade-off +- Is specific to Planix (not already covered by an org-wide ADR) diff --git a/openspec/specs/README.md b/openspec/specs/README.md new file mode 100644 index 0000000..ff384e1 --- /dev/null +++ b/openspec/specs/README.md @@ -0,0 +1,72 @@ +# Feature Specs + +Feature specs define what Planix should do — they are the input for OpenSpec changes when you are ready to build. + +Specs are created and refined during `/opsx:app-explore planix` sessions. + +## Feature Lifecycle + +``` +idea ──► planned ──► in-progress ──► done + │ │ + │ use /opsx:ff + │ to create a + │ change spec + │ +still fuzzy, +needs more thinking +``` + +| Status | Meaning | +|--------|---------| +| `idea` | Concept noted, not yet ready to spec out — keep exploring | +| `planned` | User stories and acceptance criteria defined — **ready for `/opsx:ff`** | +| `in-progress` | One or more OpenSpec changes have been created from this feature | +| `done` | All associated OpenSpec changes have been archived | + +## Spec Format + +Each feature spec lives at `openspec/specs/{feature-name}/spec.md`: + +```markdown +# {Feature Name} Specification + +**Status**: idea | planned | in-progress | done + +**OpenSpec changes:** _(links to openspec/changes/ directories when in-progress or done)_ + +## Purpose + +What this feature does and why it matters to users. + +## Requirements + +### Requirement: {Requirement Name} +The system MUST/SHOULD/MAY {requirement statement}. + +#### Scenario: {Scenario Name} +- GIVEN {precondition} +- WHEN {action} +- THEN the system {MUST/SHOULD} {expected outcome} + +## User Stories + +- As a [role], I want to [action] so that [outcome] + +## Acceptance Criteria + +- [ ] ... +- [ ] ... + +## Notes + +Open questions, constraints, dependencies, related ADRs. +``` + +> For `idea` status, a lightweight spec (Purpose + User Stories + Acceptance Criteria) is fine. Fill in Requirements/Scenarios when moving to `planned`. + +## Important Notes + +- A single feature can result in **multiple OpenSpec changes** — break large features into independently deployable slices +- Features are maintained at the concept level here; implementation details live in `openspec/changes/` +- Once a feature moves to `in-progress`, link to the OpenSpec change directories in the `OpenSpec changes` field From af75cbdd5df6d9fe60929593d390ac7db698c5cc Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Thu, 26 Mar 2026 16:07:34 +0100 Subject: [PATCH 02/13] =?UTF-8?q?Update=20docs=20and=20openspec=20specs=20?= =?UTF-8?q?=E2=80=94=20tasks,=20time-tracking,=20features,=20design=20refe?= =?UTF-8?q?rences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- docs/ARCHITECTURE.md | 3 +- docs/DESIGN-REFERENCES.md | 53 ++++++++++++++ docs/FEATURES.md | 126 +++++++++++++++++--------------- openspec/specs/tasks.md | 9 +++ openspec/specs/time-tracking.md | 7 ++ 5 files changed, 138 insertions(+), 60 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f54daee..65cca1f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -114,7 +114,8 @@ A task is the core unit of work in Planix. Tasks belong to a project, can be pla | `description` | string | `DESCRIPTION` | `schema:description` | — | No | — | | `status` | enum | `STATUS` | `schema:actionStatus` | `status` | Yes | `open` | | `priority` | enum: low, normal, high, urgent | `PRIORITY` (1-9) | — | — | No | `normal` | -| `project` | reference | `RELATED-TO` (parent project) | — | `zaakUuid` (optional) | No | — | +| `project` | reference | `RELATED-TO` (parent project) | — | — | No | — | +| `zaakUuid` | string (UUID) | — | — | Procest case UUID (cross-app bridge) | No | null | | `column` | reference | — | — | — | No | null (backlog) | | `columnOrder` | integer | — | `schema:position` | — | No | 0 | | `assignedTo` | string (user UID) | `ATTENDEE` | `schema:agent` | `toegewezenAanGebruikersnaam` | No | — | diff --git a/docs/DESIGN-REFERENCES.md b/docs/DESIGN-REFERENCES.md index c986ff6..2d02c1d 100644 --- a/docs/DESIGN-REFERENCES.md +++ b/docs/DESIGN-REFERENCES.md @@ -389,6 +389,59 @@ CnSettingsSection (name="OpenRegister Setup", ...) **Note**: Uses `NcAppSettingsDialog` (NOT `NcDialog`). Triggered from the `?` / gear icon in the Planix top navigation bar. See `openspec/specs/nextcloud-app/spec.md` for the authoritative pattern. +### 3.9 Timesheet View + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ PLANIX › My Timesheet │ +├────────────────────────────────────────────────────────────────────┤ +│ [This week ▾] Mar 24 – Mar 30, 2026 Total: 14h 30m │ +├────────────────────────────────────────────────────────────────────┤ +│ │ +│ 📅 Monday, Mar 24 2h 45m │ +│ ──────────────────────────────────────────────────────────────── │ +│ Fix auth token expiry bug API Gateway 0h 45m [✎] [✕] │ +│ Write deployment checklist Infra Migration 2h 00m [✎] [✕] │ +│ │ +│ 📅 Tuesday, Mar 25 4h 00m │ +│ ──────────────────────────────────────────────────────────────── │ +│ Fix auth token expiry bug API Gateway 1h 30m [✎] [✕] │ +│ Migrate to PostgreSQL pool API Gateway 2h 30m [✎] [✕] │ +│ │ +│ 📅 Wednesday, Mar 26 3h 45m │ +│ ──────────────────────────────────────────────────────────────── │ +│ Review PR #42 — rate limiting API Gateway 0h 45m [✎] [✕] │ +│ Add CSRF token validation API Gateway 3h 00m [✎] [✕] │ +│ │ +│ 📅 Thursday, Mar 27 · 2h 30m │ 📅 Friday, Mar 28 · 1h 30m │ +│ ──────────────────────────────┤──────────────────────────────── │ +│ Pagination for /list 1h 00m │ Write OpenAPI 3.0 spec 1h 30m │ +│ Update error format 1h 30m │ │ +│ │ +├────────────────────────────────────────────────────────────────────┤ +│ Week total: 14h 30m [+ Log time] │ +└────────────────────────────────────────────────────────────────────┘ +``` + +**Component hierarchy**: +``` +CnListViewLayout (title="My Timesheet") +├─ date range selector (This week / Last week / This month / Custom) +├─ week total badge +├─ CnDataTable (grouped by date) +│ ├─ date group header (date label + daily total) +│ └─ rows: task title (link) | project badge | duration | [edit] [delete] +└─ week total footer + [+ Log time] CTA +``` + +**Key UX patterns** (sourced from Leantime, OpenProject, Harvest): +- Date grouped rows with daily subtotals — scan work patterns at a glance +- Inline edit and delete per row — correct mistakes without navigating away +- Task title is a clickable link → task detail view (back returns to timesheet) +- Week view with mini day columns for at-a-glance density when days are sparse +- Weekly total prominently displayed in header and footer +- "Log time" CTA always visible — encourages consistent logging + --- ## 4. Updated Feature Counts (after design review) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index d845788..05aa090 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -22,9 +22,9 @@ Nextcloud Deck is the only native Nextcloud kanban app — and it is fundamental | Name | GitHub ★ | Positioning | Key Features | Weaknesses | |------|----------|------------|-------------|------------| -| **Plane** | 38.6k | Linear alternative, GitHub-native | Kanban, list, calendar views; cycles (sprints); GitHub/GitLab sync; modules/epics; issue templates | No time tracking, no Gantt, no backlog management, SaaS-first | +| **Plane** | 40k+ | Linear alternative, GitHub-native | Kanban, list, calendar views; cycles (sprints); GitHub/GitLab sync; epics (archivable); project subscribers; workspace-level kanban/calendar; Plane AI (web search, chart generation); Slack routing; Jira import | No time tracking, no Gantt, SaaS-first | | **Taiga** | ~10k | Agile teams, Scrum+Kanban | Scrum boards, backlog, burndown charts, epics, user stories, wiki, swimlanes, WIP limits | No GitHub PR integration, complex UI, no time tracking | -| **Vikunja** | 3.6k | Self-hosted flexible | Kanban, list, Gantt, table views; recurring tasks; email notifications | No sprint/cycle, no GitHub integration, no time tracking, smaller community | +| **Vikunja** | 5k+ (1.0 stable Jan 2026) | Self-hosted flexible | Kanban, list, Gantt (overhauled v2.2), table views; recurring tasks; task duplication; email notifications | No sprint/cycle, no GitHub integration, no time tracking | | **WeKan** | 14.6k | Trello alternative | Kanban boards, automation rules, swimlanes, 70+ languages, Trello import | Kanban-only, no agile features, no time tracking, Meteor stack | | **Kanboard** | 9.5k | Minimalist kanban | WIP limits, query language, LDAP, GitHub webhooks | Kanban-only, minimal by design, no time tracking, no backlog | | **Leantime** | — | Goal-driven PM | Kanban, Gantt, time tracking, time blocking, whiteboard, sprints, neuro-inclusive | Limited GitHub integration, smaller community, complex for small teams | @@ -89,6 +89,10 @@ No dedicated Dutch government task management tools were identified. OpenProject | Column color coding | **MVP** | Visual workflow clarity | | Swimlanes (group cards by assignee or priority) | **V1** | Workload visibility | | Board filter (by assignee, label, priority) | **MVP** | Focus on relevant work | +| View toggle on board (kanban ↔ list) | **MVP** | Users need a dense list view alongside kanban for large projects (Linear, Plane, Jira pattern) | +| Task card hover quick-actions (assign, set due date, change status) | **MVP** | Assign/update without opening detail — Jira, Asana, Trello pattern | +| Task count per column (shown in column header) | **MVP** | Instant awareness of column load; present in every kanban tool | +| Overdue task highlight (red border/badge on card) | **MVP** | Urgency signal visible without opening task — Jira, Linear, Asana pattern | | Collapsed columns | **V1** | Space management | | Blocked task indicators | **V1** | Dependency visibility | | Card quick-edit (inline title/status change) | **V1** | Speed of use | @@ -304,14 +308,14 @@ No dedicated Dutch government task management tools were identified. OpenProject | Risk | Severity | Mitigation | |------|---------|------------| | Nextcloud Deck owns the kanban mindshare in the NC ecosystem | High | Differentiate on time tracking + backlog + dev integration — Deck explicitly excludes these | -| Plane (38.6k ★) moves faster than we can | Medium | Focus on Nextcloud-native features that Plane will never build; don't compete on Plane's turf | +| Plane (40k+ ★) moves faster than we can | Medium | Focus on Nextcloud-native features that Plane will never build; don't compete on Plane's turf | | Small initial team → scope creep | Medium | MVP is strictly kanban + backlog + time tracking; defer everything else | | Drag-and-drop kanban is UX-complex in Vue 2 | Medium | Use a proven drag library (vue-draggable/SortableJS); budget time for polish | | OpenRegister performance at scale (many tasks) | Medium | Lean on OpenRegister's pagination and indexing; document pagination patterns early | ## 6. Recommended Feature Set Summary -### MVP (40 features) +### MVP (44 features) Flow-based kanban with backlog and time tracking for dev/IT teams on Nextcloud. Covers the gap left by Nextcloud Deck. 1. Task CRUD (title, description, status, priority) @@ -338,64 +342,68 @@ Flow-based kanban with backlog and time tracking for dev/IT teams on Nextcloud. 22. Task card anatomy (title, assignee, due date, labels, priority) 23. Column color coding 24. Board filter (by assignee, label, priority) -25. Backlog view (tasks without a column) -26. Drag task from backlog to board column -27. Backlog sorting (by priority, due date, created date) -28. Backlog search and filter -29. Personal dashboard (landing page with KPI cards) -30. My Work view (tasks assigned to me, across all projects) -31. Overdue task list -32. Tasks due this week -33. Recently updated tasks -34. Notes/comments on tasks (ICommentsManager) -35. File attachments on tasks (CnObjectSidebar) -36. Activity stream on task (Audit Trail tab) -37. Shared project access (multi-user) -38. Project progress (tasks done / total) -39. Procest bridge (case → project/task) -40. NcAppSettingsDialog (notify_assigned, notify_due_reminder, default_view) - -### V1 (25 additional features, continuing from 40) +25. View toggle on board (kanban ↔ list) +26. Task card hover quick-actions (assign, due date, status) +27. Task count in column header +28. Overdue task highlight (red border) on card +29. Backlog view (tasks without a column) +30. Drag task from backlog to board column +31. Backlog sorting (by priority, due date, created date) +32. Backlog search and filter +33. Personal dashboard (landing page with KPI cards) +34. My Work view (tasks assigned to me, across all projects) +35. Overdue task list +36. Tasks due this week +37. Recently updated tasks +38. Notes/comments on tasks (ICommentsManager) +39. File attachments on tasks (CnObjectSidebar) +40. Activity stream on task (Audit Trail tab) +41. Shared project access (multi-user) +42. Project progress (tasks done / total) +43. Procest bridge (case → project/task) +44. NcAppSettingsDialog (notify_assigned, notify_due_reminder, default_view) + +### V1 (25 additional features, continuing from 44) More collaboration, reporting, dev integrations, and advanced kanban. -41. Sub-tasks (one level deep) -42. Task dependencies (blocks / is-blocked-by) -43. Recurring tasks -44. Project milestones -45. Project templates -46. Swimlanes (group by assignee or priority) -47. Collapsed columns -48. Blocked task indicators -49. Card quick-edit (inline title/status change) -50. Bulk select and move tasks from backlog -51. Backlog item ordering (manual drag-and-drop rank) -52. Backlog statistics (count, overdue, unassigned) -53. Project time report (estimated vs logged) -54. Team timesheet (admin, all users, export CSV) -55. Timer (start/stop, auto-log) -56. Time tracking export (CSV) -57. Cumulative flow diagram -58. Team workload report (tasks per user) -59. Throughput chart (tasks/week) -60. @mention users in comments -61. Talk integration (per-task conversation) -62. Activity feed on dashboard -63. CalDAV/VTODO export (sync to Nextcloud Tasks) -64. GitHub/GitLab sync (via OpenConnector) -65. Import from Nextcloud Deck - -### Enterprise (10 additional features, continuing from 65) +45. Sub-tasks (one level deep) +46. Task dependencies (blocks / is-blocked-by) +47. Recurring tasks +48. Project milestones +49. Project templates +50. Swimlanes (group by assignee or priority) +51. Collapsed columns +52. Blocked task indicators +53. Card quick-edit (inline title/status change) +54. Bulk select and move tasks from backlog +55. Backlog item ordering (manual drag-and-drop rank) +56. Backlog statistics (count, overdue, unassigned) +57. Project time report (estimated vs logged) +58. Team timesheet (admin, all users, export CSV) +59. Timer (start/stop, auto-log) +60. Time tracking export (CSV) +61. Cumulative flow diagram +62. Team workload report (tasks per user) +63. Throughput chart (tasks/week) +64. @mention users in comments +65. Talk integration (per-task conversation) +66. Activity feed on dashboard +67. CalDAV/VTODO export (sync to Nextcloud Tasks) +68. GitHub/GitLab sync (via OpenConnector) +69. Import from Nextcloud Deck + +### Enterprise (10 additional features, continuing from 69) Governance, advanced analytics, and custom workflows. -66. Task templates -67. Custom task fields -68. Project portfolios (cross-project grouping) -69. Role-based project permissions (viewer/editor/admin) -70. Cycle time tracking (column entry to exit) -71. Overtime / budget alerts -72. Task completion gamification -73. Webhook outgoing (on task events) -74. Import from CSV -75. Advanced admin controls (max projects per user, role restrictions) +70. Task templates +71. Custom task fields +72. Project portfolios (cross-project grouping) +73. Role-based project permissions (viewer/editor/admin) +74. Cycle time tracking (column entry to exit) +75. Overtime / budget alerts +76. Task completion gamification +77. Webhook outgoing (on task events) +78. Import from CSV +79. Advanced admin controls (max projects per user, role restrictions) diff --git a/openspec/specs/tasks.md b/openspec/specs/tasks.md index 85181c7..8a4b479 100644 --- a/openspec/specs/tasks.md +++ b/openspec/specs/tasks.md @@ -69,6 +69,13 @@ The system MUST allow authenticated users to create, read, update, and delete ta - THEN the system MUST show only urgent tasks on the board - AND non-urgent tasks MUST be visually hidden or faded +#### Scenario: Assignment notification sent +- GIVEN UserA is a member of a project +- WHEN UserB assigns a task to UserA +- THEN the system MUST create a Nextcloud notification for UserA with subject `task_assigned` +- AND the notification MUST only be sent if UserA has `notify_assigned = true` in their user settings +- AND UserA MUST NOT receive a notification if they assigned the task to themselves + ## User Stories - As a team member, I want to create a task with a title and description so that I can capture work that needs to be done @@ -88,6 +95,8 @@ The system MUST allow authenticated users to create, read, update, and delete ta - [ ] Board filter by priority, assignee, and label works without page reload - [ ] Tasks show assignee avatar, due date, priority color, and label chips on kanban cards - [ ] Overdue tasks (dueDate < today, status != done) are highlighted in red on cards +- [ ] Assigning a task to another user triggers a `task_assigned` notification (respects `notify_assigned` user setting) +- [ ] A user assigning a task to themselves does NOT trigger an assignment notification ## Notes diff --git a/openspec/specs/time-tracking.md b/openspec/specs/time-tracking.md index 2933f51..fe91018 100644 --- a/openspec/specs/time-tracking.md +++ b/openspec/specs/time-tracking.md @@ -88,6 +88,12 @@ The system MUST provide a timesheet view showing the current user's time entries - THEN the system MUST filter entries to the selected range - AND the total for the range MUST be displayed +#### Scenario: Navigate to task from timesheet +- GIVEN the timesheet shows a time entry row +- WHEN the user clicks the task title in the timesheet row +- THEN the system MUST navigate to the task detail view +- AND the browser back button MUST return to the timesheet at the same scroll position and date filter + ## User Stories - As a developer, I want to log the time I spent on a task so that the team has accurate capacity data @@ -110,6 +116,7 @@ The system MUST provide a timesheet view showing the current user's time entries - [ ] Timesheet can be filtered by date range - [ ] Users can edit and delete their own time entries - [ ] Admins (V1) can view and export all users' time entries per project +- [ ] Clicking a task title in the timesheet navigates to the task detail; back button returns to timesheet ## Notes From 9c330ea8679d824e856653ce3b2445e8217a2efc Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Thu, 26 Mar 2026 16:09:41 +0100 Subject: [PATCH 03/13] Extend Nextcloud compatibility to v34, update openspec config Made-with: Cursor --- appinfo/info.xml | 2 +- openspec/app-config.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index e8b09ed..48fae09 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -52,7 +52,7 @@ Vrij en open source onder de EUPL-1.2-licentie. https://raw.githubusercontent.com/ConductionNL/planix/main/img/app-store.svg - + diff --git a/openspec/app-config.json b/openspec/app-config.json index 1154df0..1f1875d 100644 --- a/openspec/app-config.json +++ b/openspec/app-config.json @@ -17,9 +17,9 @@ }, "cicd": { "phpVersions": ["8.3", "8.4"], - "nextcloudRefs": ["stable31", "stable32"], + "nextcloudRefs": ["stable31", "stable32", "stable33"], "enableNewman": false }, "createdAt": "2026-03-24", - "updatedAt": "2026-03-24" + "updatedAt": "2026-03-26" } From f9426a92aaadb0f1e023b7c04d5d27f2e78ae781 Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Thu, 26 Mar 2026 20:10:05 +0100 Subject: [PATCH 04/13] feat(openspec): expand specs with ADRs and detailed feature documentation --- .github/workflows/code-quality.yml | 4 +- appinfo/info.xml | 2 +- .../adr-001-vtodo-primary-standard.md | 38 ++++++++++ .../architecture/adr-002-flow-based-kanban.md | 38 ++++++++++ .../architecture/adr-003-procest-bridge.md | 44 ++++++++++++ .../adr-004-time-tracking-scope.md | 35 ++++++++++ openspec/specs/admin-user-settings.md | 26 ++++++- openspec/specs/dashboard-my-work.md | 28 +++++++- openspec/specs/kanban-board.md | 55 ++++++++++++++- openspec/specs/procest-integration.md | 69 ++++++++++++++----- openspec/specs/projects.md | 55 ++++++++++++++- openspec/specs/tasks.md | 56 ++++++++++++++- openspec/specs/time-tracking.md | 47 ++++++++++--- 13 files changed, 457 insertions(+), 40 deletions(-) create mode 100644 openspec/architecture/adr-001-vtodo-primary-standard.md create mode 100644 openspec/architecture/adr-002-flow-based-kanban.md create mode 100644 openspec/architecture/adr-003-procest-bridge.md create mode 100644 openspec/architecture/adr-004-time-tracking-scope.md diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index db6aef8..c1e0b49 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -14,13 +14,13 @@ jobs: app-name: planix php-version: "8.3" php-test-versions: '["8.3", "8.4"]' - nextcloud-test-refs: '["stable31", "stable32"]' + nextcloud-test-refs: '["stable31", "stable32", "stable33"]' enable-psalm: true enable-phpstan: true enable-phpmetrics: true enable-frontend: true enable-eslint: true enable-phpunit: true - enable-newman: true + enable-newman: false additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"main"}]' enable-sbom: true diff --git a/appinfo/info.xml b/appinfo/info.xml index 48fae09..69b47c5 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -35,7 +35,7 @@ Vrij en open source onder de EUPL-1.2-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. ]]> 0.1.0 - agpl + eupl Conduction Planix diff --git a/openspec/architecture/adr-001-vtodo-primary-standard.md b/openspec/architecture/adr-001-vtodo-primary-standard.md new file mode 100644 index 0000000..e502b02 --- /dev/null +++ b/openspec/architecture/adr-001-vtodo-primary-standard.md @@ -0,0 +1,38 @@ +# ADR-001: VTODO as Primary Task Standard + +**Status**: accepted + +**Date**: 2026-03-26 + +## Context + +Planix stores task data in OpenRegister. We needed a field reference for task properties (title, status, priority, due date, assignee, etc.) that is mature, widely understood, and allows future interoperability with calendar/task systems including Nextcloud's own Tasks app. + +We evaluated 8 standards: iCalendar VTODO, Schema.org Action, OpenProject Work Packages API, Nextcloud CalDAV VTODO, GitHub Issues API, VNG InterneTaak, BPMN 2.0 UserTask, and W3C PROV-O. + +## Decision + +iCalendar VTODO (RFC 5545) is the primary field reference for the Task entity. Schema.org Action provides semantic type annotations. VNG InterneTaak is an API mapping layer only — not a storage model. + +> **Data storage uses international standards. Dutch government standards are an API mapping layer.** + +## Consequences + +**Positive:** +- Task properties (SUMMARY, DESCRIPTION, DTSTART, DUE, STATUS, PRIORITY, PERCENT-COMPLETE, ATTENDEE, CATEGORIES, RELATED-TO) map to a ratified RFC +- Compatible with Nextcloud Tasks app (CalDAV/VTODO) via `calendarEventUid` reference field +- Schema.org annotations make tasks machine-readable / JSON-LD compatible +- Dutch government interoperability via VNG mapping layer, without coupling storage model to Dutch-only standards + +**Negative / trade-offs:** +- VTODO does not cover project/board concepts — supplemented with Schema.org ItemList (boards) and DefinedTerm (columns/labels) +- VTODO property names (DTSTART, ATTENDEE) are not used verbatim in JSON — we use camelCase equivalents (startDate, assignedTo) + +## Alternatives Considered + +| Option | Reason not chosen | +|--------|------------------| +| BPMN 2.0 UserTask | Too process-oriented; no kanban fit | +| VNG InterneTaak as primary | Dutch-only standard; excludes non-government use cases | +| GitHub Issues API as primary | No time tracking fields, no percent-complete | +| W3C PROV-O | Audit trail reference only, not a task model | diff --git a/openspec/architecture/adr-002-flow-based-kanban.md b/openspec/architecture/adr-002-flow-based-kanban.md new file mode 100644 index 0000000..0ac455d --- /dev/null +++ b/openspec/architecture/adr-002-flow-based-kanban.md @@ -0,0 +1,38 @@ +# ADR-002: Flow-Based Kanban (No Sprints) + +**Status**: accepted + +**Date**: 2026-03-26 + +## Context + +Planix is positioned for developer and IT teams who manage continuous work. Two models exist for kanban-based project management: + +- **Flow-based (continuous)**: work items move through columns at their own pace; no time-boxed iterations +- **Sprint-based (Scrum)**: work is planned into 2-week sprints with velocity tracking, burndown charts, and sprint reviews + +Competitors like Jira support both; Linear and Plane are flow-based only. Nextcloud Deck is flow-based but lacks backlog and WIP limits. + +## Decision + +Planix is flow-based only. Projects have one persistent kanban board with configurable columns. There are no sprints, sprint planning, velocity charts, or burndown charts in any tier (MVP, V1, or Enterprise). A backlog is implicit — tasks not assigned to a board column live there. + +## Consequences + +**Positive:** +- Simpler mental model — no sprint ceremonies or planning overhead +- Matches how most small dev/IT teams actually work +- Aligns with Plane and Linear positioning (modern, developer-first) +- Less UI surface area; faster to build and maintain +- Cumulative flow diagrams (V1) replace burndown as the primary flow metric + +**Negative / trade-offs:** +- Scrum teams requiring sprint planning and velocity tracking are excluded +- No roadmap/milestone grouping at MVP — teams with release planning needs must use labels or project naming conventions as a workaround + +## Alternatives Considered + +| Option | Reason not chosen | +|--------|------------------| +| Sprint support in V1 | Adds significant complexity (Sprint entity, task-sprint assignment, velocity calculation) for a minority use case; better served by a dedicated Scrum tool | +| Optional sprint mode per project | Doubles the UI surface and creates two divergent user mental models within one app | diff --git a/openspec/architecture/adr-003-procest-bridge.md b/openspec/architecture/adr-003-procest-bridge.md new file mode 100644 index 0000000..6f3b609 --- /dev/null +++ b/openspec/architecture/adr-003-procest-bridge.md @@ -0,0 +1,44 @@ +# ADR-003: Procest Bridge via Schema Fields (Loose Coupling) + +**Status**: accepted + +**Date**: 2026-03-26 + +## Context + +Planix's sister app Procest handles case management (ZGW-aligned). A common workflow is: a case in Procest generates one or more tasks that need to be tracked on a kanban board in Planix. + +Three integration approaches were considered: + +1. **Tight coupling** — Planix calls the Procest API to fetch/create tasks +2. **Loose coupling** — Tasks carry optional metadata fields that reference a Procest case; no direct API calls between apps +3. **Separate bridge service** — a dedicated integration layer translates between the two apps + +## Decision + +Loose coupling via schema fields. The Task entity has two optional fields: + +- `caseReference` (string) — human-readable case identifier +- `zaakUuid` (UUID) — machine-readable ZGW case UUID + +Planix does not call Procest APIs in MVP. Procest creates tasks in Planix via OpenRegister directly, populating these fields. Planix displays them as read-only metadata on the task detail view. + +## Consequences + +**Positive:** +- No circular dependency between apps at runtime +- Planix works fully without Procest installed +- Tasks remain valid OpenRegister objects regardless of case status +- `zaakUuid` enables future ZGW API mapping without changing the data model + +**Negative / trade-offs:** +- No real-time status sync between Procest case and Planix task in MVP +- Planix cannot initiate case creation in Procest +- `caseReference` is a display-only string — no validation against Procest data + +## Alternatives Considered + +| Option | Reason not chosen | +|--------|------------------| +| Tight API coupling | Creates runtime dependency; if Procest is down, Planix task operations are affected | +| Separate bridge service | Over-engineering for MVP; adds infrastructure complexity before the integration pattern is proven | diff --git a/openspec/architecture/adr-004-time-tracking-scope.md b/openspec/architecture/adr-004-time-tracking-scope.md new file mode 100644 index 0000000..983ba5d --- /dev/null +++ b/openspec/architecture/adr-004-time-tracking-scope.md @@ -0,0 +1,35 @@ +# ADR-004: Time Tracking Scope — Manual Only in MVP + +**Status**: accepted + +**Date**: 2026-03-26 + +## Context + +Time tracking is a key differentiator for Planix vs. competitors (Plane, Taiga, Nextcloud Deck — none have time tracking). However, time tracking can be implemented at three levels of complexity: + +1. **Manual logging only** — user enters duration + date after the fact +2. **Live timers** — start/stop timer on a task; auto-creates a time entry +3. **External integrations** — sync with Toggl, Harvest, etc. + +## Decision + +MVP includes manual time logging only (TimeEntry entity with task, user, duration in minutes, date, and optional description). Live timers are deferred to V1. External integrations are Enterprise tier. + +## Consequences + +**Positive:** +- Manual logging covers the primary use case (reporting hours per task) with minimal UI complexity +- TimeEntry data model is identical whether entries are created manually or via a timer — V1 timer feature adds UI only, not schema changes +- Faster MVP delivery; time tracking UI is a form, not a real-time widget + +**Negative / trade-offs:** +- Users who prefer live timers must track time externally and log manually in MVP +- No automatic time capture — relies on user discipline to log accurately + +## Alternatives Considered + +| Option | Reason not chosen | +|--------|------------------| +| Timers in MVP | Adds a persistent UI widget (active timer indicator in navigation/header), background state management, and edge cases (multiple active timers, browser close mid-timer) that significantly increase MVP scope | +| Skip time tracking entirely until V1 | Time tracking is the primary competitive differentiator vs. Plane and Nextcloud Deck; even manual logging establishes the feature and data model in MVP | diff --git a/openspec/specs/admin-user-settings.md b/openspec/specs/admin-user-settings.md index 2ac33bb..db95482 100644 --- a/openspec/specs/admin-user-settings.md +++ b/openspec/specs/admin-user-settings.md @@ -1,6 +1,6 @@ # Admin & User Settings Specification -**Status**: idea +**Status**: planned **Standards**: Nextcloud OCP\IAppConfig (admin), OCP\IConfig (user), NcAppSettingsDialog (user), CnSettingsSection + CnVersionInfoCard (@conduction/nextcloud-vue) **Feature tier**: MVP @@ -41,12 +41,24 @@ No OpenRegister entities. Settings are stored in Nextcloud's native config stora ### Requirement: Admin Settings Page [MVP] The system MUST provide an admin settings page under Nextcloud Administration → Planix. +#### Scenario: Admin access only +- GIVEN a regular (non-admin) Nextcloud user +- WHEN they attempt to access the Planix admin settings URL directly +- THEN Nextcloud MUST return a 403 Forbidden response +- AND the admin settings section MUST NOT appear in the user's Settings navigation + #### Scenario: View admin settings - GIVEN a Nextcloud admin opens Administration → Planix - THEN the system MUST render a CnVersionInfoCard as the first section (app name, version, update status) - AND the system MUST show a CnSettingsSection for "Default Project Configuration" - AND the section MUST show the current `default_columns` value as an editable list +#### Scenario: CnVersionInfoCard — update available +- GIVEN a newer version of Planix is available in the Nextcloud App Store +- WHEN the admin views the Planix admin settings +- THEN CnVersionInfoCard MUST display the current version and an "Update available" indicator +- AND the indicator MUST link to the Nextcloud App Store entry for Planix + #### Scenario: Configure default columns - GIVEN the admin is in the "Default Project Configuration" section - WHEN the admin adds, removes, or reorders column names @@ -82,6 +94,12 @@ The system MUST provide a user settings dialog via NcAppSettingsDialog, accessib - THEN the system MUST save `default_view = kanban` via OCP\IConfig - AND the next time the user opens a project, the board view MUST be shown by default +#### Scenario: Settings persist across sessions +- GIVEN a user has set `notify_assigned = false` and `default_view = kanban` +- WHEN the user closes Planix and returns in a new browser session +- THEN the notification toggle MUST still be off +- AND the default view MUST still be "Kanban" + ## User Stories - As an admin, I want to configure the default column set so that new projects start with our team's standard workflow @@ -89,17 +107,21 @@ The system MUST provide a user settings dialog via NcAppSettingsDialog, accessib - As a user, I want to control which notifications I receive so that I'm not overwhelmed by alerts - As a user, I want to choose my default view so that Planix opens in the mode I use most - As an admin, I want to initialize the OpenRegister schemas from the settings page so that I can set up the app without CLI access +- As an admin, I want to be notified of available updates in the settings page so that I can keep the app current +- As a user, I want my preferences to survive a browser restart so that I don't have to configure Planix each time ## Acceptance Criteria - [ ] Admin settings page appears under Nextcloud Administration with link "Planix" +- [ ] Non-admin users cannot access the admin settings page (403 response; section not shown in navigation) - [ ] First section is CnVersionInfoCard showing app name, version, and update status +- [ ] CnVersionInfoCard shows an "Update available" indicator and App Store link when a newer version exists - [ ] Admin can configure default columns via an editable ordered list - [ ] Admin can trigger OpenRegister initialization from the settings page - [ ] NcAppSettingsDialog opens from the Planix navigation — uses NcAppSettingsDialog NOT NcDialog - [ ] Dialog contains notification toggles: task assigned (default on), due date reminder (default on) - [ ] Dialog contains display preferences: default view selector -- [ ] All settings persist across sessions (stored via IAppConfig / IConfig) +- [ ] All settings persist across browser sessions (stored via IAppConfig / IConfig) - [ ] Notification settings are respected by NotificationService (SUBJECT_SETTING_MAP pattern) - [ ] Settings page is accessible (WCAG AA) and uses NL Design System CSS variables diff --git a/openspec/specs/dashboard-my-work.md b/openspec/specs/dashboard-my-work.md index fc72be6..e7e0e00 100644 --- a/openspec/specs/dashboard-my-work.md +++ b/openspec/specs/dashboard-my-work.md @@ -1,6 +1,6 @@ # Dashboard & My Work Specification -**Status**: idea +**Status**: planned **Standards**: Schema.org Action/PlanAction (task aggregation), Nextcloud Dashboard API (OCP\Dashboard\IWidget) **Feature tier**: MVP @@ -73,6 +73,24 @@ The system MUST provide a "My Work" view showing all tasks assigned to the curre - WHEN the user opens My Work - THEN the system MUST show a CnEmptyState with message "No tasks assigned to you" and a "Browse projects" action +### Requirement: Dashboard Empty State [MVP] +The system MUST guide new users who have no projects or tasks yet. + +#### Scenario: Dashboard — new user with no projects +- GIVEN a user is authenticated and is not a member of any project +- WHEN the user opens the Planix dashboard +- THEN the system MUST show a CnEmptyState in place of the recent projects and due-this-week sections +- AND the message MUST read "No projects yet" +- AND a "Create project" button MUST be shown (navigates to the new project form) +- AND KPI cards MUST all show 0 (not be hidden) + +#### Scenario: Dashboard — member of projects but no assigned tasks +- GIVEN a user is a member of projects but has no tasks assigned to them +- WHEN the user opens the Planix dashboard +- THEN KPI cards MUST show 0 for Open, Overdue, In Progress, and Completed Today +- AND the "Due this week" section MUST show a CnEmptyState: "No tasks due this week" +- AND recent projects MUST still render normally + ## User Stories - As a developer, I want to see all my tasks in one place when I open Planix so that I can prioritize my day @@ -81,20 +99,24 @@ The system MUST provide a "My Work" view showing all tasks assigned to the curre - As a user, I want to see my recent projects at a glance so that I can navigate quickly to active work - As a user, I want KPI cards on the dashboard so that I can understand my work state without scrolling - As a developer, I want to update task status directly from My Work so that I don't have to open each task +- As a new user, I want a helpful empty state when I have no projects yet so that I know how to get started +- As a user, I want clicking a KPI card to take me to My Work filtered to that category so that I can act on it immediately ## Acceptance Criteria - [ ] Dashboard is the default route (`/`) and loads on Planix open - [ ] Dashboard shows 4 KPI cards: Open, Overdue, In Progress, Completed Today -- [ ] KPI cards are clickable and navigate to My Work with the corresponding filter applied +- [ ] Each KPI card is clickable and navigates to My Work with the corresponding filter pre-applied - [ ] Dashboard shows the 5 most recently active projects with progress bars - [ ] Dashboard shows tasks due within 7 days, sorted by due date ascending +- [ ] "Due this week" section shows CnEmptyState when no tasks are due this week +- [ ] New user with no projects sees a CnEmptyState with a "Create project" button; KPI cards show 0 - [ ] My Work groups tasks into Overdue, Due this week, Everything else - [ ] Within each group, tasks are sorted by priority (urgent → high → normal → low) - [ ] Tasks in My Work show: project name (badge), title, due date, status indicator, priority dot - [ ] Status can be updated inline from My Work without full navigation - [ ] Clicking a task title in My Work navigates to task detail; back button returns to My Work -- [ ] Empty My Work state shows CnEmptyState with helpful action +- [ ] Empty My Work state shows CnEmptyState with "Browse projects" action ## Notes diff --git a/openspec/specs/kanban-board.md b/openspec/specs/kanban-board.md index 425afb9..9fee6e9 100644 --- a/openspec/specs/kanban-board.md +++ b/openspec/specs/kanban-board.md @@ -1,6 +1,6 @@ # Kanban Board Specification -**Status**: idea +**Status**: planned **Standards**: Schema.org ItemList (board), DefinedTerm (column), Kanban Guide (kanban.university) **Feature tier**: MVP @@ -70,6 +70,47 @@ The system MUST render a kanban board with columns and task cards for each proje - THEN the system MUST assign the task to that column - AND the task MUST disappear from the backlog panel +#### Scenario: Delete column with tasks +- GIVEN a column contains one or more tasks +- WHEN a user deletes the column from project settings +- THEN the system MUST show a dialog: "This column contains {N} tasks. Move them to the backlog, or move to another column?" +- AND the system MUST NOT delete the column until the user confirms a destination for the tasks +- AND if "Move to backlog" is chosen, all tasks MUST have their `column` cleared +- AND if "Move to column" is chosen, all tasks MUST be reassigned to the selected column + +#### Scenario: Empty board — no columns yet +- GIVEN a project has no columns (e.g., default columns were cleared by admin before project was created) +- WHEN a member opens the board view +- THEN the system MUST show a CnEmptyState with message "No columns yet" +- AND a project creator or admin MUST see an "Add column" button to create the first column + +#### Scenario: Empty board — no tasks yet +- GIVEN a project has columns but no tasks +- WHEN a member opens the board view +- THEN each empty column MUST show a CnEmptyState with a "+ Add task" button +- AND clicking "+ Add task" in a column MUST open the task creation form with that column pre-selected + +### Requirement: View Toggle — Kanban and List [MVP] +The system MUST allow users to switch between kanban (card) view and list (table) view for a project's tasks. + +#### Scenario: Switch to list view +- GIVEN a user is on the kanban board view +- WHEN the user clicks the list view toggle button +- THEN the system MUST render tasks as a sortable flat list (title, assignee, due date, status, priority, labels) +- AND the selected view MUST persist in the URL hash so the page reloads in the same view + +#### Scenario: Switch back to kanban view +- GIVEN a user is in the list view +- WHEN the user clicks the kanban view toggle button +- THEN the system MUST render the kanban board with columns and cards +- AND any active filter MUST remain applied in the new view + +#### Scenario: Update task from list view +- GIVEN a user is in list view +- WHEN the user clicks a task row +- THEN the system MUST navigate to the task detail view (CnDetailPage) +- AND the browser back button MUST return to list view (not kanban view) + ## User Stories - As a developer, I want to see all project tasks as cards on a board so that I understand the current state of work at a glance @@ -77,6 +118,9 @@ The system MUST render a kanban board with columns and task cards for each proje - As a project lead, I want to set WIP limits on columns so that the team doesn't overload any single stage - As a user, I want to filter the board by my name so that I can see only my tasks - As an admin, I want to add and reorder columns so that the board matches our team's workflow +- As a user, I want to switch between kanban and list view so that I can choose the layout that suits my current focus +- As a project creator, I want guidance when my board has no columns yet so that I know how to get started +- As a user, I want a safe prompt when deleting a column that contains tasks so that I don't accidentally lose work ## Acceptance Criteria @@ -87,9 +131,14 @@ The system MUST render a kanban board with columns and task cards for each proje - [ ] Filter by assignee, label, and priority works without full page reload - [ ] Filter state is reflected in the URL hash (shareable) - [ ] Columns can be created, renamed, reordered, and deleted via project settings -- [ ] Deleting a column with tasks prompts: "Move tasks to backlog" or "Move to another column" +- [ ] Deleting a column with tasks prompts the user to move tasks to backlog or another column before deleting - [ ] Board is keyboard-navigable (WCAG AA) -- [ ] Empty columns show a clear "Empty" state with a "+ Add task" button +- [ ] Empty columns show a CnEmptyState with a "+ Add task" button; clicking pre-selects that column in the task form +- [ ] A board with no columns shows a CnEmptyState with an "Add column" button (visible to creator/admin) +- [ ] View toggle (kanban ↔ list) is available in the project toolbar; selected view persists in the URL hash +- [ ] List view shows tasks as a sortable table: title, assignee, due date, status, priority, labels +- [ ] Switching views preserves active filters +- [ ] Clicking a task in list view navigates to task detail; back button returns to list view ## Notes diff --git a/openspec/specs/procest-integration.md b/openspec/specs/procest-integration.md index 3da7694..8831766 100644 --- a/openspec/specs/procest-integration.md +++ b/openspec/specs/procest-integration.md @@ -1,6 +1,6 @@ # Procest Integration Specification -**Status**: idea +**Status**: planned **Standards**: VNG ZGW InterneTaak, Schema.org Action, OpenRegister object references **Feature tier**: MVP (caseReference field); V1 (full bridge API) @@ -34,6 +34,10 @@ See [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) for cross-app relationship dia ## Requirements +--- + +## MVP Requirements + ### Requirement: Case Reference on Project [MVP] The system MUST support a `caseReference` field on Project to link it to a Procest case. @@ -43,12 +47,37 @@ The system MUST support a `caseReference` field on Project to link it to a Proce - THEN the project MUST display a "Case: {caseNumber}" badge - AND the project detail MUST show a link to the Procest case +#### Scenario: Manually set caseReference via project edit form +- GIVEN a user is editing a project +- WHEN they enter a Procest case UUID in the "Case reference" field and save +- THEN the system MUST store `caseReference` on the project +- AND the case badge MUST appear in the project list and detail immediately + +### Requirement: Task Case Link [MVP] +The system MUST support a `zaakUuid` field on Task to link an individual task to a Procest case. + #### Scenario: Individual task linked to a case - GIVEN a task has `zaakUuid` set to a Procest case UUID - WHEN the task is displayed -- THEN the task detail MUST show a link to the Procest case +- THEN the task detail MUST show a read-only "Case" field with a link to the Procest case - AND the task MUST appear in the Procest case's task list (via Procest's cross-app query) +#### Scenario: Manually set zaakUuid via task edit form +- GIVEN a user is editing a task +- WHEN they enter a Procest case UUID in the "Case UUID" field and save +- THEN the system MUST store `zaakUuid` on the task +- AND the case link MUST appear in the task detail immediately + +#### Scenario: Bridge disabled — fields still displayed +- GIVEN the Procest bridge toggle is disabled in admin settings (or not yet configured) +- WHEN a task with `zaakUuid` is marked done +- THEN Planix MUST NOT send any request to Procest +- AND the `caseReference` and `zaakUuid` fields MUST still be stored and displayed in the UI as read-only metadata + +--- + +## V1 Requirements + ### Requirement: Procest Bridge — Create Project from Case [V1] When the Procest bridge is enabled, the system MUST allow Procest to create a Planix project for a case via API. @@ -65,11 +94,12 @@ When the Procest bridge is enabled, the system MUST allow Procest to create a Pl - THEN Planix MUST send a PATCH to the Procest case tasks API to mark the InterneTaak as afgehandeld - AND Planix MUST log the mirroring event in the task's audit trail -#### Scenario: Procest bridge disabled -- GIVEN the Procest bridge is disabled in admin settings -- WHEN a task with `zaakUuid` is marked done -- THEN Planix MUST NOT send any request to Procest -- AND the `caseReference` field MUST still be stored and displayed in the UI +#### Scenario: Procest unreachable — graceful degradation +- GIVEN the Procest bridge is enabled and a task with `zaakUuid` is marked done +- WHEN the Procest API is unreachable +- THEN the task MUST still be updated to `done` in Planix (task update MUST NOT fail) +- AND Planix MUST log a warning with the failed mirroring attempt +- AND the user MUST NOT see an error related to Procest #### Scenario: Bridge API authentication - GIVEN Procest sends a bridge request to Planix @@ -78,22 +108,27 @@ When the Procest bridge is enabled, the system MUST allow Procest to create a Pl ## User Stories -- As a case handler in Procest, I want a Planix kanban board automatically created for my case so that I can track implementation tasks visually +- As a case handler in Procest, I want a Planix kanban board automatically created for my case so that I can track implementation tasks visually (V1) - As a team member, I want tasks linked to a case to show the case reference so that I know the business context -- As a case manager, I want task completions in Planix to mirror back to the case status so that Procest stays up to date +- As a case manager, I want task completions in Planix to mirror back to the case status so that Procest stays up to date (V1) - As a Planix user, I want to see a link to the Procest case from a task or project so that I can navigate to the case without searching +- As a user, I want to manually link a task or project to a Procest case so that I can bridge cases without the full API bridge enabled +- As a user, I want Planix to remain fully functional even when Procest is unreachable so that my task updates are never blocked ## Acceptance Criteria -- [ ] `caseReference` field exists on Project entity and is stored/displayed correctly -- [ ] `zaakUuid` field exists on Task entity +**MVP:** +- [ ] `caseReference` field exists on Project entity; can be set manually via project edit form +- [ ] `zaakUuid` field exists on Task entity; can be set manually via task edit form - [ ] Projects with `caseReference` show a case badge in the project list and detail -- [ ] Task detail shows a link to the Procest case when `zaakUuid` is set -- [ ] Bridge toggle in admin settings enables/disables cross-app API calls -- [ ] (V1) Procest can create a Planix project via the bridge API (POST `/planix/api/bridge/project`) -- [ ] (V1) Task completion triggers a mirroring update to Procest when bridge is enabled -- [ ] (V1) Bridge API authenticates via shared token -- [ ] Bridge errors are logged and do not crash the Planix task update flow (graceful degradation) +- [ ] Task detail shows a read-only case link when `zaakUuid` is set +- [ ] When bridge is disabled, `caseReference` and `zaakUuid` fields are still stored and displayed; no Procest API calls are made + +**V1:** +- [ ] Procest can create a Planix project via the bridge API (POST `/planix/api/bridge/project`) +- [ ] Task completion with `zaakUuid` set triggers a mirroring PATCH to Procest when bridge is enabled +- [ ] If Procest is unreachable during task completion, the task update succeeds and a warning is logged (graceful degradation) +- [ ] Bridge API authenticates via shared token; unauthenticated requests return 401 ## Notes diff --git a/openspec/specs/projects.md b/openspec/specs/projects.md index 9665bfb..da901c2 100644 --- a/openspec/specs/projects.md +++ b/openspec/specs/projects.md @@ -1,6 +1,6 @@ # Projects Specification -**Status**: idea +**Status**: planned **Standards**: Schema.org CreativeWork, iCalendar VTODO (parent container reference) **Feature tier**: MVP @@ -64,6 +64,49 @@ The system MUST allow authenticated users to create, read, update, and archive p - THEN the system MUST show a centered NcEmptyContent (no sidebar, no navigation) with an appropriate message - AND admin users MUST see an "Install OpenRegister" button linking to the Nextcloud App Store +#### Scenario: Edit project metadata +- GIVEN a project creator or admin is viewing a project +- WHEN they edit the title, description, color, or icon and save +- THEN the system MUST update the project in OpenRegister +- AND the new title, color, and icon MUST be reflected in the sidebar and project list immediately + +### Requirement: Member Management [MVP] +The system MUST allow project creators and admins to add and remove members after project creation. + +#### Scenario: Add a member +- GIVEN a project creator opens the project settings +- WHEN they search for a Nextcloud user and click "Add member" +- THEN the system MUST add the user to the project's `members` array +- AND the user MUST immediately be able to access the project board and tasks + +#### Scenario: Remove a member +- GIVEN a project has members [UserA, UserB] and UserB has tasks assigned +- WHEN the project creator removes UserB +- THEN the system MUST remove UserB from `members` +- AND UserB MUST no longer appear in the project list or board +- AND tasks assigned to UserB MUST remain assigned to them (not auto-reassigned) +- AND a warning MUST be shown: "UserB has N assigned tasks in this project" + +#### Scenario: Leave a project +- GIVEN a non-creator project member is viewing a project +- WHEN the member clicks "Leave project" +- THEN the system MUST remove them from `members` +- AND if the user is the last member, the system MUST warn: "You are the last member. Leave anyway?" + +### Requirement: Project Deletion [MVP] +The system MUST allow admins and project creators to permanently delete a project. + +#### Scenario: Delete a project +- GIVEN a project exists with tasks +- WHEN a Nextcloud admin or the project creator clicks "Delete project" and confirms +- THEN the system MUST delete the project, all its tasks, all linked TimeEntries, and all columns +- AND the project MUST be removed from all members' project lists immediately + +#### Scenario: Delete confirmation with task count +- GIVEN a project has tasks +- WHEN the user initiates project deletion +- THEN the confirmation dialog MUST state: "This will permanently delete {N} tasks and all their time entries. This cannot be undone." + ## User Stories - As a team lead, I want to create a project so that I can group related tasks @@ -71,6 +114,10 @@ The system MUST allow authenticated users to create, read, update, and archive p - As a user, I want to see a list of my active projects so that I can navigate between them quickly - As an admin, I want to archive completed projects so that the project list stays manageable - As a user bridging Procest, I want a Planix project automatically created from a case so that I can track case-related tasks on a kanban board +- As a project creator, I want to update the project title, color, and icon so that it stays recognizable as scope evolves +- As a project creator, I want to remove a member who has left the team, with a warning about their assigned tasks +- As a team member, I want to leave a project I no longer contribute to so that my project list stays relevant +- As an admin, I want to permanently delete a project and all its data so that stale projects don't clutter the system ## Acceptance Criteria @@ -83,6 +130,12 @@ The system MUST allow authenticated users to create, read, update, and archive p - [ ] Projects are color-coded with the chosen hex color in the sidebar and list - [ ] The OpenRegister dependency check shows NcEmptyContent when OpenRegister is absent - [ ] `caseReference` links the project back to its Procest case (if applicable) +- [ ] Project title, description, color, and icon can be edited; changes reflect immediately in sidebar and list +- [ ] Members can be added by searching Nextcloud users; added members gain immediate board access +- [ ] Removing a member shows a warning if they have assigned tasks; tasks remain assigned after removal +- [ ] A member can leave a project via "Leave project"; last-member warning shown before confirming +- [ ] Deleting a project requires admin or creator permission and a confirmation dialog stating task/entry count +- [ ] Project deletion cascades to all tasks, columns, and TimeEntries ## Notes diff --git a/openspec/specs/tasks.md b/openspec/specs/tasks.md index 8a4b479..6348d15 100644 --- a/openspec/specs/tasks.md +++ b/openspec/specs/tasks.md @@ -1,6 +1,6 @@ # Tasks Specification -**Status**: idea +**Status**: planned **Standards**: iCalendar VTODO (RFC 5545), Schema.org Action/PlanAction, VNG InterneTaak **Feature tier**: MVP @@ -76,6 +76,50 @@ The system MUST allow authenticated users to create, read, update, and delete ta - AND the notification MUST only be sent if UserA has `notify_assigned = true` in their user settings - AND UserA MUST NOT receive a notification if they assigned the task to themselves +#### Scenario: Delete a task +- GIVEN a task exists in a project +- WHEN the task creator, project creator, or a Nextcloud admin clicks "Delete task" and confirms the dialog +- THEN the system MUST delete the task and all linked TimeEntries +- AND the task MUST be removed from the board and backlog immediately for all users + +#### Scenario: Delete a task with sub-tasks (V1 guard) +- GIVEN a task has one or more sub-tasks (V1 feature) +- WHEN a user attempts to delete the parent task +- THEN the system MUST prompt: "This task has sub-tasks. Delete all sub-tasks too, or move them to backlog?" +- AND the system MUST NOT silently orphan sub-tasks + +#### Scenario: Move task to another project +- GIVEN a task belongs to Project A +- WHEN a project member edits the task and selects Project B as the new project +- THEN the system MUST update the task's `project` reference +- AND the task's `column` assignment MUST be cleared (task moves to Project B's backlog) +- AND the task MUST no longer appear in Project A's board or backlog + +### Requirement: Task Search [MVP] +The system MUST allow users to search tasks within a project. + +#### Scenario: Search tasks by title +- GIVEN a project board or backlog is open +- WHEN the user types in the search field +- THEN the system MUST filter visible tasks to those matching the search term in `title` or `description` +- AND the filter MUST apply without page reload +- AND the search MUST be case-insensitive + +### Requirement: Bulk Task Operations [MVP] +The system MUST allow users to update multiple tasks at once from the backlog view. + +#### Scenario: Bulk status update +- GIVEN multiple tasks are selected in the backlog view +- WHEN the user selects "Change status" from the bulk actions bar +- THEN the system MUST update all selected tasks to the chosen status +- AND a toast notification MUST confirm "N tasks updated" + +#### Scenario: Bulk assignee update +- GIVEN multiple tasks are selected in the backlog view +- WHEN the user selects "Assign to" from the bulk actions bar and picks a user +- THEN the system MUST set `assignedTo` on all selected tasks +- AND assignment notifications MUST be sent per each task's normal notification rules + ## User Stories - As a team member, I want to create a task with a title and description so that I can capture work that needs to be done @@ -84,6 +128,10 @@ The system MUST allow authenticated users to create, read, update, and delete ta - As a user, I want to see tasks I created and tasks assigned to me so that I know my responsibilities - As a project manager, I want to filter board tasks by assignee so that I can review each person's workload - As a team member, I want to set a time estimate on a task so that I can plan capacity +- As a user, I want to delete a task I no longer need so that the board stays clean +- As a team lead, I want to move a task to a different project so that it is tracked in the right place +- As a user, I want to search for tasks by title so that I can find specific work items quickly +- As a project manager, I want to bulk-update assignee or status on multiple tasks so that I can reorganize work efficiently ## Acceptance Criteria @@ -97,6 +145,12 @@ The system MUST allow authenticated users to create, read, update, and delete ta - [ ] Overdue tasks (dueDate < today, status != done) are highlighted in red on cards - [ ] Assigning a task to another user triggers a `task_assigned` notification (respects `notify_assigned` user setting) - [ ] A user assigning a task to themselves does NOT trigger an assignment notification +- [ ] Deleting a task requires a confirmation dialog and removes all linked TimeEntries +- [ ] Only the task creator, project creator, or NC admin can delete a task +- [ ] Moving a task to another project clears its column assignment (task lands in new project's backlog) +- [ ] Task search filters by title and description, case-insensitive, without page reload +- [ ] Bulk status update applies to all selected tasks and confirms with a toast +- [ ] Bulk assignee update applies to all selected tasks and respects per-user notification settings ## Notes diff --git a/openspec/specs/time-tracking.md b/openspec/specs/time-tracking.md index fe91018..8a02a82 100644 --- a/openspec/specs/time-tracking.md +++ b/openspec/specs/time-tracking.md @@ -1,6 +1,6 @@ # Time Tracking Specification -**Status**: idea +**Status**: planned **Standards**: Schema.org QuantitativeValue, iCalendar ESTIMATED-DURATION (RFC 7986), OpenProject spentTime model **Feature tier**: MVP @@ -43,6 +43,24 @@ The system MUST allow users to set a time estimate on a task. - THEN the system MUST store `estimatedDuration` in minutes on the task - AND the task card on the kanban board MUST display the estimate (e.g., "2h 30m") +#### Scenario: Estimate input — accepted formats +- GIVEN a user is entering a time estimate +- WHEN the user types any of: "2h 30m", "150m", "1.5h", "90", "2h" +- THEN the system MUST parse the value and store it as an integer number of minutes +- AND the stored value MUST be displayed back in human-readable format (e.g., 90 → "1h 30m") + +#### Scenario: Estimate input — invalid format +- GIVEN a user is entering a time estimate +- WHEN the user types an unparseable value (e.g., "lots", "-5", "0") +- THEN the system MUST show an inline validation error +- AND the system MUST NOT save the estimate until a valid value is entered + +#### Scenario: Logged vs estimated progress +- GIVEN a task has `estimatedDuration` = 180 minutes and total logged time = 90 minutes +- WHEN the user views the task detail +- THEN the system MUST display a progress indicator showing "1h 30m / 3h" +- AND if logged time exceeds the estimate, the indicator MUST turn red and show the overage (e.g., "3h 30m / 3h") + ### Requirement: Log Time [MVP] The system MUST allow users to log time spent on a task. @@ -71,6 +89,12 @@ The system MUST allow users to log time spent on a task. - THEN the system MUST remove the entry - AND the task's total logged time MUST recalculate +#### Scenario: User cannot edit or delete another user's time entry +- GIVEN UserA has a time entry on a task +- WHEN UserB (not an admin) views the same task detail +- THEN UserB MUST NOT see edit or delete controls on UserA's time entries +- AND a direct API call from UserB to edit UserA's entry MUST return 403 Forbidden + ### Requirement: Personal Timesheet [MVP] The system MUST provide a timesheet view showing the current user's time entries grouped by date. @@ -101,22 +125,25 @@ The system MUST provide a timesheet view showing the current user's time entries - As a project lead, I want to see estimated vs actual time per task so that I can improve future estimates - As a user, I want to add multiple time logs per task so that I can track time across multiple work sessions - As a team member, I want to see my total hours for the week so that I can track my workload +- As a user, I want the time estimate input to accept natural formats like "2h 30m" so that I don't have to do mental arithmetic +- As a user, I want to see a progress bar when my logged time approaches or exceeds the estimate so that I can flag scope creep ## Acceptance Criteria -- [ ] Time estimate can be set on any task (input accepts "1h 30m", "90m", "1.5h" formats) +- [ ] Time estimate can be set on any task; input accepts "2h 30m", "150m", "1.5h", "90", "2h" formats +- [ ] Invalid or zero estimate input shows an inline validation error and does not save - [ ] Estimated duration is stored in minutes and displayed in human-readable format on task card and detail - [ ] "Log time" button is accessible from the task detail view - [ ] A time entry requires at minimum a duration and a date - [ ] Multiple time entries can be added to the same task -- [ ] Total logged time is computed from all entries and displayed on the task -- [ ] Logged time vs estimated time shows a progress indicator (e.g., "1h 30m / 3h") -- [ ] Timesheet view shows all entries by the current user grouped by date -- [ ] Timesheet shows daily totals and weekly total -- [ ] Timesheet can be filtered by date range -- [ ] Users can edit and delete their own time entries -- [ ] Admins (V1) can view and export all users' time entries per project -- [ ] Clicking a task title in the timesheet navigates to the task detail; back button returns to timesheet +- [ ] Total logged time is computed from all TimeEntries and displayed on the task detail +- [ ] Logged vs estimated progress indicator shows "Xh Ym / Zh" and turns red when logged exceeds estimate +- [ ] Users can edit and delete only their own time entries; other users' entries show no edit/delete controls +- [ ] A direct API call to edit another user's entry returns 403 Forbidden +- [ ] Timesheet view shows all entries by the current user grouped by date (newest first) +- [ ] Timesheet shows daily totals and a total for the selected date range +- [ ] Timesheet can be filtered by date range (presets: this week, last week, custom) +- [ ] Clicking a task title in the timesheet navigates to the task detail; back button returns to timesheet at same scroll and filter state ## Notes From 1ea3671181451ee482dfce0cf253fdfe21da7ccc Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Thu, 26 Mar 2026 21:55:33 +0100 Subject: [PATCH 05/13] docs: expand app description with positioning vs Nextcloud Deck and Jira --- README.md | 2 +- appinfo/info.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b20572..1ca2aa6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ --- -Planix is a Kanban-based project and task management app for Nextcloud, built as a thin client on OpenRegister. It manages projects, tasks, kanban boards with WIP limits, backlogs, and time entries for internal dev and IT teams. +Planix is a Kanban-based project and task management app for Nextcloud, built as a thin client on OpenRegister. It manages projects, tasks, kanban boards with WIP limits, backlogs, and time entries — giving internal dev and IT teams a focused workflow tool built directly into their Nextcloud environment. Unlike Nextcloud Deck (which lacks backlog management, time tracking, and WIP limits), Planix closes the gap between Deck's simplicity and Jira's complexity. > **Pre-wired for [OpenRegister](https://github.com/ConductionNL/openregister)** — all data is stored as OpenRegister objects. If your app needs OpenRegister, install it first. If not, remove the dependency from `appinfo/info.xml` and `openspec/app-config.json`. diff --git a/appinfo/info.xml b/appinfo/info.xml index 69b47c5..ea167ee 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,7 +6,7 @@ Planix Flow-based kanban project and task management for Nextcloud dev and IT teams Flow-gebaseerd kanban project- en taakbeheer voor Nextcloud dev- en IT-teams - Date: Thu, 2 Apr 2026 17:05:12 +0200 Subject: [PATCH 06/13] feat(openspec): add 8 MVP change artifacts and resolve architecture questions - Resolve all 6 open research questions in ARCHITECTURE.md: CalDAV one-way export (V1), 1-level sub-tasks, configurable Procest bridge (Procest UI decides), per-task-only time tracking, GitHub sync via OpenConnector (V1), soft WIP limits - Update ADR-003 (Procest bridge) and ADR-004 (time tracking scope) - Create 8 OpenSpec changes with full artifacts (proposal, design, specs, tasks): register-schemas, projects, tasks, kanban-board, time-tracking, dashboard-my-work, admin-user-settings, procest-integration - Update all 7 feature specs to in-progress status with change links - Add conduction schema symlink (openspec/schemas/conduction) - Sync app description in info.xml and README.md with app-config.json goal --- README.md | 14 +- docs/ARCHITECTURE.md | 16 +- docs/FEATURES.md | 2 +- .../architecture/adr-003-procest-bridge.md | 2 + .../adr-004-time-tracking-scope.md | 2 + .../admin-user-settings/.openspec.yaml | 2 + .../changes/admin-user-settings/design.md | 363 ++++++++++++++++ .../changes/admin-user-settings/proposal.md | 72 ++++ .../specs/admin-user-settings/spec.md | 300 +++++++++++++ openspec/changes/admin-user-settings/tasks.md | 301 +++++++++++++ .../changes/dashboard-my-work/.openspec.yaml | 2 + openspec/changes/dashboard-my-work/design.md | 287 ++++++++++++ .../changes/dashboard-my-work/proposal.md | 81 ++++ .../specs/dashboard-my-work/spec.md | 238 ++++++++++ openspec/changes/dashboard-my-work/tasks.md | 251 +++++++++++ openspec/changes/kanban-board/.openspec.yaml | 2 + openspec/changes/kanban-board/design.md | 380 ++++++++++++++++ openspec/changes/kanban-board/proposal.md | 84 ++++ .../kanban-board/specs/kanban-board/spec.md | 341 +++++++++++++++ openspec/changes/kanban-board/tasks.md | 407 ++++++++++++++++++ .../procest-integration/.openspec.yaml | 2 + .../changes/procest-integration/design.md | 218 ++++++++++ .../changes/procest-integration/proposal.md | 94 ++++ .../specs/procest-integration/spec.md | 182 ++++++++ openspec/changes/procest-integration/tasks.md | 214 +++++++++ openspec/changes/projects/.openspec.yaml | 2 + openspec/changes/projects/design.md | 221 ++++++++++ openspec/changes/projects/proposal.md | 71 +++ .../changes/projects/specs/projects/spec.md | 186 ++++++++ openspec/changes/projects/tasks.md | 218 ++++++++++ .../changes/register-schemas/.openspec.yaml | 2 + openspec/changes/register-schemas/design.md | 347 +++++++++++++++ openspec/changes/register-schemas/proposal.md | 68 +++ .../specs/register-schemas/spec.md | 174 ++++++++ openspec/changes/register-schemas/tasks.md | 60 +++ openspec/changes/tasks/.openspec.yaml | 2 + openspec/changes/tasks/design.md | 383 ++++++++++++++++ openspec/changes/tasks/proposal.md | 84 ++++ openspec/changes/tasks/specs/tasks/spec.md | 307 +++++++++++++ openspec/changes/tasks/tasks.md | 356 +++++++++++++++ openspec/changes/time-tracking/.openspec.yaml | 2 + openspec/changes/time-tracking/design.md | 163 +++++++ openspec/changes/time-tracking/proposal.md | 70 +++ .../time-tracking/specs/time-tracking/spec.md | 354 +++++++++++++++ openspec/changes/time-tracking/tasks.md | 291 +++++++++++++ openspec/config.yaml | 2 +- openspec/schemas/conduction | 1 + openspec/specs/admin-user-settings.md | 5 +- openspec/specs/dashboard-my-work.md | 5 +- openspec/specs/kanban-board.md | 5 +- openspec/specs/procest-integration.md | 5 +- openspec/specs/projects.md | 5 +- openspec/specs/tasks.md | 5 +- openspec/specs/time-tracking.md | 5 +- project.md | 2 +- 55 files changed, 7229 insertions(+), 29 deletions(-) create mode 100644 openspec/changes/admin-user-settings/.openspec.yaml create mode 100644 openspec/changes/admin-user-settings/design.md create mode 100644 openspec/changes/admin-user-settings/proposal.md create mode 100644 openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md create mode 100644 openspec/changes/admin-user-settings/tasks.md create mode 100644 openspec/changes/dashboard-my-work/.openspec.yaml create mode 100644 openspec/changes/dashboard-my-work/design.md create mode 100644 openspec/changes/dashboard-my-work/proposal.md create mode 100644 openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md create mode 100644 openspec/changes/dashboard-my-work/tasks.md create mode 100644 openspec/changes/kanban-board/.openspec.yaml create mode 100644 openspec/changes/kanban-board/design.md create mode 100644 openspec/changes/kanban-board/proposal.md create mode 100644 openspec/changes/kanban-board/specs/kanban-board/spec.md create mode 100644 openspec/changes/kanban-board/tasks.md create mode 100644 openspec/changes/procest-integration/.openspec.yaml create mode 100644 openspec/changes/procest-integration/design.md create mode 100644 openspec/changes/procest-integration/proposal.md create mode 100644 openspec/changes/procest-integration/specs/procest-integration/spec.md create mode 100644 openspec/changes/procest-integration/tasks.md create mode 100644 openspec/changes/projects/.openspec.yaml create mode 100644 openspec/changes/projects/design.md create mode 100644 openspec/changes/projects/proposal.md create mode 100644 openspec/changes/projects/specs/projects/spec.md create mode 100644 openspec/changes/projects/tasks.md create mode 100644 openspec/changes/register-schemas/.openspec.yaml create mode 100644 openspec/changes/register-schemas/design.md create mode 100644 openspec/changes/register-schemas/proposal.md create mode 100644 openspec/changes/register-schemas/specs/register-schemas/spec.md create mode 100644 openspec/changes/register-schemas/tasks.md create mode 100644 openspec/changes/tasks/.openspec.yaml create mode 100644 openspec/changes/tasks/design.md create mode 100644 openspec/changes/tasks/proposal.md create mode 100644 openspec/changes/tasks/specs/tasks/spec.md create mode 100644 openspec/changes/tasks/tasks.md create mode 100644 openspec/changes/time-tracking/.openspec.yaml create mode 100644 openspec/changes/time-tracking/design.md create mode 100644 openspec/changes/time-tracking/proposal.md create mode 100644 openspec/changes/time-tracking/specs/time-tracking/spec.md create mode 100644 openspec/changes/time-tracking/tasks.md create mode 120000 openspec/schemas/conduction diff --git a/README.md b/README.md index 1ca2aa6..7a040d1 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,19 @@ graph TD A --> E[Nextcloud Search] ``` -_Update this diagram during `/app-explore` sessions as the architecture evolves._ +See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full architecture breakdown. ### Data Model -| Object | Description | -|--------|-------------| -| _(define your data objects here)_ | — | +| Object | Schema.org Type | Description | +|--------|----------------|-------------| +| Task | `schema:Action` / `schema:PlanAction` | Core unit of work — title, description, assignee, due date, priority, status, estimates | +| Project | `schema:CreativeWork` | Container for tasks and kanban board — teams, members, metadata | +| Column | `schema:DefinedTerm` | Kanban board column — configurable stages with WIP limits | +| TimeEntry | `schema:QuantitativeValue` | Effort log — task, user, duration (minutes), date, description | +| Label | `schema:DefinedTerm` | Cross-project tag — name, color, description | -_Data model is defined using OpenRegister schemas. See [`openspec/specs/`](openspec/specs/) for feature-level design decisions and [`openspec/architecture/`](openspec/architecture/) for architectural decisions._ +Data model is defined using OpenRegister schemas. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full entity definitions and standards mapping, [`openspec/specs/`](openspec/specs/) for feature-level requirements, and [`openspec/architecture/`](openspec/architecture/) for architectural decisions. ### Directory Structure diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 65cca1f..049c944 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -405,19 +405,21 @@ Schemas MUST be defined in `lib/Settings/planix_register.json` using OpenAPI 3.0 The configuration is imported via `ConfigurationService::importFromApp()` in the repair step. -## 5. Open Research Questions +## 5. Resolved Research Questions -1. **CalDAV VTODO sync** — Should Planix offer two-way sync with the Nextcloud Tasks app (CalDAV)? The Tasks app already supports VTODO. A sync would let tasks appear in mobile Calendar apps. Current decision: store `calendarEventUid` field and implement one-way export in V1. +All research questions below have been resolved. Decisions are recorded in the app-specific ADRs under [`openspec/architecture/`](../openspec/architecture/). -2. **Sub-task depth** — OpenProject supports unlimited hierarchy; GitHub Issues has no hierarchy. One level of sub-tasks (task → sub-task) is planned. Should we support epic → task → sub-task (three levels)? Current decision: one level only in MVP. +1. **CalDAV VTODO sync** — **One-way export to Nextcloud Tasks app in V1.** The `calendarEventUid` field on Task stores the VTODO UID. Planix writes tasks to CalDAV; changes made in the Tasks app are not synced back. Two-way sync was rejected due to data model mismatch (Tasks app has no concept of projects, columns, or WIP limits). -3. **Procest task bridge** — When a Procest case creates tasks in Planix, who owns the project? Does each case get its own Planix project, or do case tasks appear in an existing project? Current decision: a dedicated Planix project per case (with `caseReference` linking back). +2. **Sub-task depth** — **One level only (task → sub-task), all tiers.** Projects serve the "epic" grouping role. No formal Epic entity. This matches Linear and Plane (1 level + containers) and avoids Jira's 3-level complexity. -4. **Time tracking scope** — Should time entries be per-task only, or also per-project (overhead, meetings)? Current decision: per-task only in MVP. Overhead tracking in V1. +3. **Procest task bridge** — **Configurable — Procest UI decides.** Procest's UI presents a project picker when creating tasks for a case. It may create a new project (with `caseReference`) or add tasks to an existing project (with `zaakUuid` on each task). Planix has no routing mechanism — it reads whatever Procest wrote to OpenRegister. See ADR-003. -5. **GitHub/GitLab sync** — Dev teams want tasks linked to commits and PRs. Should Planix natively sync with GitHub Issues or GitLab Issues? Current decision: out of scope for MVP; targeted for V1 via OpenConnector. +4. **Time tracking scope** — **Per-task only, forever.** `TimeEntry.task` is always required. Overhead work (meetings, planning) is tracked as tasks — not as project-level time entries. This keeps the data model simple and queryable. No special cases needed. See ADR-004. -6. **WIP limit enforcement** — Should WIP limits be hard (block task drag) or soft (visual warning only)? Current decision: soft limits with prominent visual warning (industry consensus: hard limits cause friction). +5. **GitHub/GitLab sync** — **Via OpenConnector in V1.** Planix owns no GitHub/GitLab API code. OpenConnector handles the external API mapping (GitHub Issues ↔ Planix tasks). This avoids duplicating integration logic across Conduction apps. + +6. **WIP limit enforcement** — **Soft limits with visual warning.** Column header turns orange/red when over the WIP limit; counter shows e.g. `4/3`. Drag is never blocked. Industry consensus: hard limits cause friction and workarounds (Jira, Kanboard both use soft limits). ## 6. References diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 05aa090..d826b3d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -12,7 +12,7 @@ Nextcloud Deck is the only native Nextcloud kanban app — and it is fundamental | Name | Status | Key Features | Gaps | |------|--------|-------------|------| -| **Nextcloud Deck** | Bundled, active (v1.17, Feb 2026) | Kanban boards, cards, labels, file attachment, mobile apps, Circles sharing | No backlog, no time tracking, no GitHub sync, no WIP limits, 6500+ DB queries per board, no multi-view | +| **Nextcloud Deck** | Bundled, active | Kanban boards, cards, labels, file attachment, mobile apps, Circles sharing | No backlog, no time tracking, no GitHub sync, no WIP limits, 6500+ DB queries per board, no multi-view | | **Nextcloud Tasks** | Bundled, active | CalDAV/VTODO task sync, due dates, priorities, sub-tasks | No project grouping, no kanban board, no time tracking, no team collaboration | | **Nextcloud Deck Extended** | Community, low activity | Minor Deck extensions | Unmaintained, Deck fork approach | diff --git a/openspec/architecture/adr-003-procest-bridge.md b/openspec/architecture/adr-003-procest-bridge.md index 6f3b609..9ceb82c 100644 --- a/openspec/architecture/adr-003-procest-bridge.md +++ b/openspec/architecture/adr-003-procest-bridge.md @@ -23,6 +23,8 @@ Loose coupling via schema fields. The Task entity has two optional fields: Planix does not call Procest APIs in MVP. Procest creates tasks in Planix via OpenRegister directly, populating these fields. Planix displays them as read-only metadata on the task detail view. +**Project ownership is configurable — Procest's UI decides.** When creating tasks for a case, Procest's UI presents a project picker. The user can create a new Planix project (with `caseReference` linking back to the case) or add tasks to an existing project (with `zaakUuid` on each task). Planix has no routing or default-project mechanism — it reads whatever Procest wrote to OpenRegister. + ## Consequences **Positive:** diff --git a/openspec/architecture/adr-004-time-tracking-scope.md b/openspec/architecture/adr-004-time-tracking-scope.md index 983ba5d..9a29ddc 100644 --- a/openspec/architecture/adr-004-time-tracking-scope.md +++ b/openspec/architecture/adr-004-time-tracking-scope.md @@ -16,6 +16,8 @@ Time tracking is a key differentiator for Planix vs. competitors (Plane, Taiga, MVP includes manual time logging only (TimeEntry entity with task, user, duration in minutes, date, and optional description). Live timers are deferred to V1. External integrations are Enterprise tier. +**Time entries are per-task only, in all tiers.** `TimeEntry.task` is always required — there are no project-level time entries. Overhead work (meetings, planning, standups) is tracked as tasks. This keeps the data model simple and queryable with no special cases. + ## Consequences **Positive:** diff --git a/openspec/changes/admin-user-settings/.openspec.yaml b/openspec/changes/admin-user-settings/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/admin-user-settings/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/admin-user-settings/design.md b/openspec/changes/admin-user-settings/design.md new file mode 100644 index 0000000..441dea4 --- /dev/null +++ b/openspec/changes/admin-user-settings/design.md @@ -0,0 +1,363 @@ +# Design: admin-user-settings + +**Change ID:** admin-user-settings +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Context + +Planix is a thin-client Nextcloud app backed by OpenRegister. It has no custom database tables. Admin settings (`IAppConfig`) and user settings (`IConfig`) are stored in Nextcloud's native configuration storage, which both PHP and frontend can read/write through the `SettingsController`. + +The existing codebase has two stub entry points: +- `lib/Settings/AdminSettings.php` — registered with Nextcloud's settings infrastructure; renders a template that is currently empty. +- `src/views/settings/UserSettings.vue` — imported in the Vue app but renders nothing. + +This change implements both stubs fully, adds a `SettingsController` that exposes read/write endpoints for both admin and user settings, and integrates the user settings keys into `NotificationService`. + +--- + +## Goals + +- Admin settings page: `CnVersionInfoCard`, editable default columns list, OpenRegister register initialization button. +- User settings dialog: `NcAppSettingsDialog`, notification toggles, default view selector. +- Pinia settings store: single source of truth for both admin and user settings in the frontend. +- `ColumnListEditor` component: ordered, drag-to-reorder list for column name strings. +- Backend endpoints: `GET /settings/admin`, `GET /settings/user`, `PUT /settings/user`, `POST /settings/admin/register-init`. +- NotificationService: align `SUBJECT_SETTING_MAP` keys with user settings keys. +- Full i18n coverage (en + nl). + +## Non-Goals + +- Procest bridge settings (`procest_bridge_enabled`, `procest_base_url`) — V1 feature; section placeholder may be rendered but fields are non-functional. +- `allow_project_creation` access control setting — V1. +- `notify_overdue`, `notify_commented`, `notify_status_changed`, `items_per_page` user settings — V1; keys may be declared in `SUBJECT_SETTING_MAP` but toggles are not shown in MVP dialog. +- OAuth/SAML authentication settings — outside scope. +- Custom PHP admin settings UI using Nextcloud's legacy `IAdmin` interface — using Vue SPA rendered in the template instead. + +--- + +## Decisions + +### Decision 1: Admin settings use a Vue component rendered inside the Nextcloud admin template + +**Options considered:** +1. Pure PHP template with HTML form elements (legacy Nextcloud pattern). +2. Vue SPA rendered inside the PHP template via a mount point `
` (chosen). + +**Rationale:** `CnVersionInfoCard` and `CnSettingsSection` are Vue components from `@conduction/nextcloud-vue`. Rendering them requires a Vue mount point. The PHP `AdminSettings.php` provides the initial data (version, update status, current settings values) as JSON on the page's `data-*` attributes so the Vue component can hydrate without an extra network request. + +The PHP template registers a dedicated admin settings entry script (`admin-settings.js`) that mounts `AdminSettings.vue` onto the `#planix-admin-settings` div. + +### Decision 2: User settings use NcAppSettingsDialog, not NcDialog + +**Options considered:** +1. `NcDialog` — generic modal. +2. `NcAppSettingsDialog` — Nextcloud's purpose-built settings dialog with sectioned navigation (chosen). + +**Rationale:** `NcAppSettingsDialog` provides a two-pane layout with a sidebar section list and content area. This matches the established Nextcloud UX pattern for per-app user settings (Deck, Talk, Calendar all use this). It also renders consistently across desktop and mobile viewports. Using `NcDialog` would require reimplementing the section navigation. + +The dialog is opened from a gear icon `NcAppNavigationItem` at the bottom of the Planix navigation sidebar (existing slot in `MainMenu.vue`). + +### Decision 3: Settings store handles both admin and user settings in one Pinia store + +**Options considered:** +1. Two separate stores: `useAdminSettingsStore` and `useUserSettingsStore`. +2. One store `useSettingsStore` with namespaced getters/actions (chosen). + +**Rationale:** Both settings types share the same controller (`SettingsController`) and the same fetch-on-mount / save-on-change pattern. A single store reduces boilerplate and makes it easy to read both in a single mount hook. State is namespaced internally (`adminSettings`, `userSettings`). + +### Decision 4: Column list editor uses SortableJS via vue-draggable-plus (or HTML5 DnD fallback) + +**Options considered:** +1. `vue-draggable-plus` (SortableJS wrapper, used in other Conduction apps) — chosen if already in `package.json`. +2. Plain HTML5 `draggable` attribute with `ondragstart`/`ondrop` (fallback if vue-draggable-plus is not available). + +**Rationale:** Drag-to-reorder is a critical UX requirement for the column list. `vue-draggable-plus` provides a declarative Vue 3 wrapper around SortableJS with built-in accessibility. If not available, the HTML5 DnD fallback is acceptable for MVP given the admin-only audience. The component exposes a `modelValue` prop (array of strings) and emits `update:modelValue` on change, making it a standard v-model component. + +The component also supports keyboard-accessible reordering (move up/down buttons) alongside drag-and-drop, satisfying WCAG AA requirement for keyboard accessibility. + +### Decision 5: Register initialization is an admin-only synchronous endpoint + +**Options considered:** +1. Asynchronous: endpoint triggers a background job, admin polls for status. +2. Synchronous: endpoint calls `ConfigurationService::importFromApp()` directly and returns success/error (chosen for MVP). + +**Rationale:** OpenRegister register initialization is a one-time operation that completes in under 2 seconds in typical environments. A synchronous endpoint keeps the implementation simple. The frontend shows a spinner while the request is in flight. If the operation takes longer in edge cases (large schema files, slow disk), the standard Nextcloud request timeout (30 s) is sufficient. + +The endpoint is protected by `OCP\AppFramework\Middleware\Security\SecurityMiddleware` admin check via the `@AdminRequired` annotation on the controller action. + +### Decision 6: User settings are read once on dialog open, not reactive + +**Options considered:** +1. Reactive: settings subscribe to a server-sent event or poll every N seconds. +2. Read-once: settings are fetched when the dialog opens; each toggle saves immediately via PUT (chosen). + +**Rationale:** User settings do not change from another session during the dialog's open state. Read-once on open + save-on-change is simpler, faster, and consistent with how all other Nextcloud settings dialogs work (Deck, Calendar, Talk). Each toggle calls `PUT /settings/user` with the changed key/value pair; the store updates optimistically. + +### Decision 7: NotificationService SUBJECT_SETTING_MAP keys use `notify_` prefix (matching user settings keys directly) + +**Options considered:** +1. Map notification subjects to arbitrarily named setting keys. +2. Map notification subjects to user setting keys with `notify_` prefix, matching the `IConfig` key names exactly (chosen). + +**Rationale:** This makes the relationship explicit and reduces the chance of mismatch. The `SUBJECT_SETTING_MAP` in `NotificationService` maps: + +| Notification subject | IConfig user key | Default | +|---------------------|-----------------|---------| +| `task_assigned` | `notify_assigned` | `true` | +| `task_due_soon` | `notify_due_reminder` | `true` | +| `task_overdue` (V1) | `notify_overdue` | `true` | +| `task_commented` (V1) | `notify_commented` | `true` | +| `task_status_changed` (V1) | `notify_status_changed` | `false` | + +Note: The `tasks` change used `notify_task_assigned` and `notify_task_due_soon` as key names in its design. This change adopts the shorter form (`notify_assigned`, `notify_due_reminder`) as defined in the base spec (`openspec/specs/admin-user-settings.md`). The `tasks` change's `NotificationService` must be updated to use these canonical key names. + +--- + +## Component Architecture + +``` +src/ + views/settings/ + AdminSettings.vue # Vue component mounted in admin template + UserSettings.vue # NcAppSettingsDialog (replaces empty placeholder) + components/settings/ + ColumnListEditor.vue # Drag-to-reorder column name list editor + store/ + settings.js # Pinia store — useSettingsStore + +lib/ + Settings/ + AdminSettings.php # Modified — add page data (version, update, settings) + Controller/ + SettingsController.php # Modified — add userIndex, userUpdate, adminRegisterInit + Service/ + NotificationService.php # Modified — align SUBJECT_SETTING_MAP keys + +templates/ + admin-settings.php # Modified — add mount point, load admin-settings.js + +appinfo/ + routes.php # Modified — add user settings + register init routes + assets.php (or webpack.config)# Modified — add admin-settings.js entry point + +src/navigation/ + MainMenu.vue # Modified — gear icon opens UserSettings.vue +``` + +--- + +## Backend: SettingsController Endpoints + +| Method | URL | Auth | Description | +|--------|-----|------|-------------| +| `GET` | `/planix/settings/admin` | Admin | Read all admin settings | +| `PUT` | `/planix/settings/admin` | Admin | Update admin settings (full or partial) | +| `POST` | `/planix/settings/admin/register-init` | Admin | Trigger `ConfigurationService::importFromApp()` | +| `GET` | `/planix/settings/user` | Any authenticated | Read current user's settings | +| `PUT` | `/planix/settings/user` | Any authenticated | Update one or more of current user's settings | + +Admin settings read response schema: +```json +{ + "default_columns": ["To Do", "In Progress", "Review", "Done"], + "allow_project_creation": "all", + "procest_bridge_enabled": false, + "procest_base_url": "", + "register_initialized": true, + "app_version": "0.1.0", + "update_available": false, + "update_version": null +} +``` + +User settings read response schema: +```json +{ + "notify_assigned": true, + "notify_due_reminder": true, + "notify_overdue": true, + "notify_commented": true, + "notify_status_changed": false, + "default_view": "my-work", + "items_per_page": 25 +} +``` + +--- + +## Pinia Store: `useSettingsStore` + +```js +// src/store/settings.js +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useSettingsStore = defineStore('settings', () => { + // State + const adminSettings = ref({}) + const userSettings = ref({}) + const adminLoading = ref(false) + const userLoading = ref(false) + const error = ref(null) + + // Actions + async function fetchAdminSettings() { /* GET /planix/settings/admin */ } + async function updateAdminSettings(data) { /* PUT /planix/settings/admin */ } + async function initRegister() { /* POST /planix/settings/admin/register-init */ } + async function fetchUserSettings() { /* GET /planix/settings/user */ } + async function updateUserSetting(key, value) { + // Optimistic update: set locally first, then PUT; revert on failure + const prev = userSettings.value[key] + userSettings.value[key] = value + try { + await /* PUT /planix/settings/user with { [key]: value } */ + } catch (e) { + userSettings.value[key] = prev + throw e + } + } + + return { + adminSettings, userSettings, adminLoading, userLoading, error, + fetchAdminSettings, updateAdminSettings, initRegister, + fetchUserSettings, updateUserSetting, + } +}) +``` + +--- + +## AdminSettings.vue Layout + +``` +┌─────────────────────────────────────────────────────┐ +│ CnVersionInfoCard │ +│ App name: Planix | Version: 0.1.0 | [Update avail] │ +├─────────────────────────────────────────────────────┤ +│ CnSettingsSection: Default Project Configuration │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ ColumnListEditor │ │ +│ │ [≡] To Do [✕] │ │ +│ │ [≡] In Progress [✕] │ │ +│ │ [≡] Review [✕] │ │ +│ │ [≡] Done [✕] │ │ +│ │ [+ Add column] │ │ +│ └─────────────────────────────────────────────┘ │ +│ [Save changes] │ +├─────────────────────────────────────────────────────┤ +│ CnSettingsSection: Register Setup │ +│ Status: ✓ Initialized / ✗ Not initialized │ +│ [Initialize register] (spinner when in progress) │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## UserSettings.vue Layout (NcAppSettingsDialog) + +``` +┌──────────────────────────────────────────────────────┐ +│ Planix Settings [Close] │ +├──────────┬───────────────────────────────────────────┤ +│ Sections │ Content area │ +│ │ │ +│ Notific. │ Notifications │ +│ Display │ ────────────────────────────────────── │ +│ │ [toggle] Notify when a task is │ +│ │ assigned to me │ +│ │ │ +│ │ [toggle] Notify 1 day before a │ +│ │ task's due date │ +└──────────┴───────────────────────────────────────────┘ +``` + +Second section (Display): +``` +│ Display +│ ────────────────────────────────────── +│ Default view: [my-work ▼] +│ Options: My Work | Kanban | Backlog +``` + +--- + +## ColumnListEditor Component Anatomy + +``` +props: { + modelValue: { type: Array, required: true }, // string[] + disabled: { type: Boolean, default: false }, +} +emits: ['update:modelValue'] +``` + +Each item row: +- Drag handle icon (`≡`) — cursor: grab +- Text input (column name, min 1 char) +- Remove button (`✕`) — disabled if only 1 item remains +- Up/Down keyboard buttons for accessibility + +Add column: appends `''` (empty) to the list; focuses the new input. +Save is triggered by the parent (`AdminSettings.vue` "Save changes" button), not per-keystroke. + +--- + +## PHP: AdminSettings.php Data Injection + +```php +class AdminSettings implements ISettings { + public function getForm(): TemplateResponse { + $appVersion = \OCP\App::getAppVersion('planix'); + // Check for updates via IAppManager or app store API (cached) + $updateInfo = $this->appManager->getAppInfo('planix'); + $registerInitialized = $this->configurationService->isInitialized(); + + return new TemplateResponse('planix', 'admin-settings', [ + 'appVersion' => $appVersion, + 'updateAvailable' => $updateInfo['update_available'] ?? false, + 'updateVersion' => $updateInfo['update_version'] ?? null, + 'registerInitialized'=> $registerInitialized, + ]); + } +} +``` + +The Vue `AdminSettings.vue` component reads these values from `data-*` attributes on the mount div and supplements with a `GET /settings/admin` call for the editable settings values. + +--- + +## i18n String Inventory + +| Key context | Example string | +|-------------|----------------| +| Admin page title | `Planix Settings` | +| Version card | `Version {version}`, `Update available: {version}`, `Up to date` | +| Default columns section | `Default Project Configuration`, `Default columns for new projects` | +| Column editor | `Add column`, `Remove column`, `Move up`, `Move down`, `Column name` | +| Column editor validation | `Column name cannot be empty` | +| Save button | `Save changes`, `Saving…`, `Changes saved` | +| Register setup section | `Register Setup`, `Register initialized`, `Register not initialized`, `Initialize register`, `Initializing…`, `Register initialized successfully`, `Failed to initialize register` | +| User dialog title | `Planix Settings` | +| Notifications section | `Notifications` | +| Notify assigned toggle | `Notify me when a task is assigned to me` | +| Notify due reminder toggle | `Notify me 1 day before a task's due date` | +| Display section | `Display` | +| Default view label | `Default view` | +| Default view options | `My Work`, `Kanban`, `Backlog` | +| Save success | `Settings saved` | +| Save error | `Failed to save settings` | + +--- + +## Risks and Trade-offs + +| Risk | Likelihood | Mitigation | +|------|-----------|-----------| +| `NotificationService` key mismatch with `tasks` change | Medium | Canonical key names defined here; `tasks` change must adopt them. Document in both PRs. | +| Vue mount inside PHP admin template conflicts with NC CSP | Low | Use `TemplateResponse` (not raw HTML output); NC admin templates already support script includes. | +| `CnVersionInfoCard` update-check hits app store on every page load | Low | Cache update check result in `ICache` (TTL: 1 hour); serve cached value in `AdminSettings.php`. | +| Drag-to-reorder not keyboard accessible by default | Medium | `ColumnListEditor` must include Up/Down buttons alongside drag handles. | +| `ConfigurationService::importFromApp()` called multiple times (double-click) | Low | Disable "Initialize register" button after first click; re-enable only on error. | diff --git a/openspec/changes/admin-user-settings/proposal.md b/openspec/changes/admin-user-settings/proposal.md new file mode 100644 index 0000000..9966363 --- /dev/null +++ b/openspec/changes/admin-user-settings/proposal.md @@ -0,0 +1,72 @@ +# Change Proposal: admin-user-settings + +**Change ID:** admin-user-settings +**Status:** proposed +**Created:** 2026-04-02 +**Author:** Conduction Development Team + +--- + +## Why + +Planix already has stub entry points for both admin and user settings (`lib/Settings/AdminSettings.php` exists; `src/views/settings/UserSettings.vue` is an empty placeholder), but neither is functional. Administrators have no way to configure app-level defaults (such as the default column set for new projects), verify the OpenRegister initialization status, or see the current app version from the Nextcloud administration interface. End users have no way to personalise their notification preferences or choose a default project view — meaning all notification emails are sent unconditionally and every project always opens in the system-default view regardless of user preference. + +The `tasks` change introduces `NotificationService` with a `SUBJECT_SETTING_MAP` pattern that is designed to check user settings before sending a notification. Without this change, those setting keys always resolve to the default (`true`) because no user can toggle them off. The `admin-user-settings` change closes both gaps: it makes the admin settings page genuinely useful and gives users a settings dialog that actually persists and influences app behaviour. + +--- + +## What Changes + +Implement the admin settings page and user settings dialog for Planix: + +1. **Admin settings page** — enhance the existing `AdminSettings.php` template to render `CnVersionInfoCard` (app name, current version, update-available indicator), a "Default Project Configuration" section with an editable ordered column list (`default_columns`), and a "Register Setup" section showing OpenRegister initialization status with an "Initialize register" button. Settings stored via `IAppConfig`. +2. **User settings dialog** — implement the currently empty `UserSettings.vue` as an `NcAppSettingsDialog` (not `NcDialog`) opened from the navigation gear icon. Dialog contains a Notification section (toggles for `notify_assigned`, `notify_due_reminder`) and a Display section (`default_view` selector). Settings stored via `OCP\IConfig`. +3. **SettingsController enhancements** — add user settings read/write endpoints (`GET /settings/user`, `PUT /settings/user`) alongside the existing admin endpoints. +4. **NotificationService integration** — align `SUBJECT_SETTING_MAP` keys in `NotificationService` with the user settings keys defined here (`notify_assigned`, `notify_due_reminder`, etc.) so toggling a preference immediately controls notification delivery. +5. **Column list editor** — new `ColumnListEditor.vue` component for the admin settings page; supports add, remove, and drag-to-reorder operations on a list of column name strings. +6. **Register initialization flow** — "Register Setup" section calls `ConfigurationService::importFromApp()` via a new admin-only endpoint; shows spinner during import and success/error feedback. +7. **i18n** — all new strings added to `l10n/en.json` and `l10n/nl.json`. + +--- + +## Capabilities + +### Modified Capabilities + +- **`admin-user-settings`** — implementing the full admin and user settings layer defined in `openspec/specs/admin-user-settings.md`. This change brings the capability from stub/placeholder state to fully functional: admin settings page with version card, column configuration, and register initialization; user settings dialog with notification toggles and view preference; backend endpoints; NotificationService integration. + +No new capabilities are introduced. The `admin-user-settings` capability was declared in the spec; this change completes the implementation. + +--- + +## Impact + +### Files Changed + +| File | Change | +|------|--------| +| `lib/Settings/AdminSettings.php` | Modified — add template data: version, update status, default_columns, register init status | +| `templates/admin-settings.php` | Modified — render `CnVersionInfoCard`, column list editor, register setup section | +| `lib/Controller/SettingsController.php` | Modified — add `userIndex`, `userUpdate` actions; add `adminRegisterInit` action | +| `appinfo/routes.php` | Modified — add user settings routes; add register init route | +| `src/views/settings/UserSettings.vue` | Modified — implement full `NcAppSettingsDialog` with notification and display sections | +| `src/views/settings/AdminSettings.vue` | New — Vue component rendering `CnVersionInfoCard` + `CnSettingsSection` groups | +| `src/components/settings/ColumnListEditor.vue` | New — drag-to-reorder ordered column name list editor | +| `src/store/settings.js` | New — Pinia store for user and admin settings (read/write via SettingsController) | +| `src/navigation/MainMenu.vue` | Modified — wire gear icon to open `UserSettings.vue` dialog | +| `lib/Service/NotificationService.php` | Modified — align `SUBJECT_SETTING_MAP` keys with user settings keys | +| `l10n/en.json` | Modified — add all settings-related translation strings | +| `l10n/nl.json` | Modified — add Dutch translations for all settings strings | + +### Risk + +Low-to-medium. Admin settings template modifications are additive. User settings dialog is a net-new implementation of an empty placeholder. The `SettingsController` enhancement adds actions without removing existing ones. The highest-risk step is aligning `NotificationService::SUBJECT_SETTING_MAP` keys with the user settings keys — if the key names diverge from what `tasks` change established, notifications will silently fail or fire unconditionally. + +The `ColumnListEditor` component uses drag-and-drop (Vue Draggable or similar). If `@conduction/nextcloud-vue` does not export a list-reorder primitive, a local component using the HTML5 Drag-and-Drop API must be used instead. + +### Dependencies + +- `register-schemas` must be applied first (OpenRegister register and schemas must exist for the init status check). +- `tasks` change should be applied first or in parallel (to align `NotificationService::SUBJECT_SETTING_MAP` keys). If applied before `tasks`, the map is declared here and the `tasks` change imports it. +- `@conduction/nextcloud-vue` must export `CnVersionInfoCard`, `CnSettingsSection`, `NcAppSettingsDialog` (or this change wraps `NcAppSettingsDialog` directly from `@nextcloud/vue`). +- OpenRegister `ConfigurationService` must expose `importFromApp()` (confirmed in `register-schemas` spec). diff --git a/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md b/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md new file mode 100644 index 0000000..fd11868 --- /dev/null +++ b/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md @@ -0,0 +1,300 @@ +# Delta Spec: admin-user-settings + +**Capability:** admin-user-settings +**Change ID:** admin-user-settings +**Delta type:** implementation +**Base spec:** [openspec/specs/admin-user-settings.md](../../../../specs/admin-user-settings.md) +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Summary + +This delta captures implementation-specific requirements added when building the admin settings page and user settings dialog. The base spec (`openspec/specs/admin-user-settings.md`) defines all business requirements, scenarios, user stories, and acceptance criteria. The delta below documents: + +1. Vue component patterns required by the implementation architecture (admin template mount strategy, `NcAppSettingsDialog` layout). +2. `ColumnListEditor` component contract (drag-to-reorder, keyboard accessibility, v-model interface). +3. `CnVersionInfoCard` integration requirements (data injection, update-check caching). +4. Register initialization flow details (synchronous endpoint, double-click guard). +5. `NotificationService` `SUBJECT_SETTING_MAP` key alignment. +6. Pinia settings store patterns (optimistic update, error rollback). +7. Loading and error state requirements. +8. i18n requirements. + +All base spec requirements are implemented as-is. No base spec requirement is modified or removed. + +--- + +## ADDED Requirements + +### Requirement: CnVersionInfoCard Integration [MVP] + +The admin settings page MUST use `CnVersionInfoCard` from `@conduction/nextcloud-vue` as the first rendered section. + +#### Scenario: Version card renders current version +- GIVEN a Nextcloud admin opens Administration → Planix +- WHEN `AdminSettings.vue` mounts +- THEN `CnVersionInfoCard` MUST be the first visible element +- AND it MUST display the app name ("Planix") and the current installed version (read from `data-app-version` attribute injected by `AdminSettings.php`) +- AND the version MUST match the value returned by `\OCP\App::getAppVersion('planix')` + +#### Scenario: Version card — update available +- GIVEN the PHP `AdminSettings.php` detects a newer version in the app store (cached, TTL 1 hour) +- WHEN the admin page renders +- THEN `CnVersionInfoCard` MUST receive `updateAvailable: true` and `updateVersion: "{newVersion}"` +- AND the card MUST display an "Update available" indicator linking to the Nextcloud App Store entry for Planix +- AND the update check result MUST be served from the `ICache` layer to avoid a live app store HTTP call on every page load + +#### Scenario: Version card — up to date +- GIVEN no newer version is available +- WHEN the admin page renders +- THEN `CnVersionInfoCard` MUST show an "Up to date" indicator (no update link) + +--- + +### Requirement: Admin Settings Vue Mount [MVP] + +The admin settings page MUST render the Vue `AdminSettings.vue` component inside the PHP template. + +#### Scenario: Vue component mounts in admin template +- GIVEN a Nextcloud admin navigates to Administration → Planix +- WHEN the PHP template `templates/admin-settings.php` is rendered +- THEN the template MUST include a `
` mount point +- AND the template MUST load the `admin-settings.js` webpack entry point +- AND `AdminSettings.vue` MUST mount onto `#planix-admin-settings` and read initial values from the `data-*` attributes (no additional network request for static values) + +#### Scenario: Admin settings fetch editable values on mount +- GIVEN `AdminSettings.vue` has mounted +- WHEN the component's `onMounted` hook runs +- THEN the component MUST call `settingsStore.fetchAdminSettings()` to retrieve `default_columns` and other editable settings via `GET /planix/settings/admin` +- AND a loading skeleton MUST be shown until the fetch resolves + +--- + +### Requirement: Column List Editor Component [MVP] + +The `ColumnListEditor.vue` component MUST provide an accessible, ordered, editable list of column name strings. + +#### Scenario: Render column list +- GIVEN `ColumnListEditor` receives `modelValue: ["To Do", "In Progress", "Review", "Done"]` +- WHEN the component renders +- THEN each column name MUST appear as a row with: a drag handle icon, an editable text input, and a remove button +- AND the rows MUST be rendered in the order provided by `modelValue` + +#### Scenario: Add a column +- GIVEN the `ColumnListEditor` is rendered +- WHEN the admin clicks "Add column" +- THEN a new empty text input row MUST be appended to the list +- AND focus MUST move to the new input automatically +- AND `update:modelValue` MUST be emitted with the new array including the empty string + +#### Scenario: Remove a column +- GIVEN the list has 2 or more columns +- WHEN the admin clicks the remove button on a row +- THEN that row MUST be removed from the list +- AND `update:modelValue` MUST be emitted with the updated array +- AND the remove button MUST be disabled (visually and functionally) when only 1 column remains + +#### Scenario: Reorder via drag-and-drop +- GIVEN the list has at least 2 columns +- WHEN the admin drags a row to a new position using the drag handle +- THEN the list order MUST update immediately (optimistic, no save button needed for visual update) +- AND `update:modelValue` MUST be emitted with the reordered array + +#### Scenario: Reorder via keyboard (WCAG AA) +- GIVEN the list has at least 2 columns +- WHEN the admin focuses a row and clicks the "Move up" or "Move down" button +- THEN the row MUST move one position in the indicated direction +- AND `update:modelValue` MUST be emitted with the reordered array +- AND focus MUST follow the moved row to maintain keyboard navigation context + +#### Scenario: Empty column name validation +- GIVEN the admin clears a column name input (leaving it empty) +- WHEN the "Save changes" button is clicked in the parent `AdminSettings.vue` +- THEN the parent MUST validate that no column name is empty +- AND if validation fails, an inline error MUST appear: `t('planix', 'Column name cannot be empty')` +- AND the save request MUST NOT be sent + +--- + +### Requirement: Register Initialization Flow [MVP] + +#### Scenario: Register already initialized +- GIVEN OpenRegister has already been initialized for Planix (detected via `ConfigurationService::isInitialized()`) +- WHEN the admin settings page renders +- THEN the "Register Setup" section MUST show a green checkmark and the text "Register initialized" +- AND the "Initialize register" button MUST NOT be shown (or shown as disabled with label "Already initialized") + +#### Scenario: Register not initialized +- GIVEN OpenRegister is NOT initialized for Planix +- WHEN the admin settings page renders +- THEN the "Register Setup" section MUST show a warning indicator and the text "Register not initialized" +- AND an "Initialize register" button MUST be visible and enabled + +#### Scenario: Trigger initialization +- GIVEN the "Initialize register" button is enabled +- WHEN the admin clicks it +- THEN the button MUST immediately become disabled and show a spinner with label "Initializing…" +- AND the frontend MUST call `settingsStore.initRegister()` which POSTs to `/planix/settings/admin/register-init` +- AND the PHP endpoint MUST call `ConfigurationService::importFromApp()` synchronously +- AND on success, the section status MUST update to "Register initialized" +- AND a success toast MUST be shown: `t('planix', 'Register initialized successfully')` + +#### Scenario: Initialization failure +- GIVEN the "Initialize register" request fails (API error or `importFromApp()` throws) +- WHEN the store catches the error +- THEN the button MUST re-enable with its original label +- AND an error toast MUST be shown: `t('planix', 'Failed to initialize register')` +- AND the section status indicator MUST remain in the "not initialized" state + +--- + +### Requirement: NcAppSettingsDialog Layout [MVP] + +The user settings dialog MUST use `NcAppSettingsDialog` and be opened from the Planix navigation gear icon. + +#### Scenario: Open user settings dialog from gear icon +- GIVEN a user is using Planix +- WHEN the user clicks the gear icon in the Planix navigation sidebar (bottom navigation item) +- THEN `UserSettings.vue` MUST open as an `NcAppSettingsDialog` +- AND the dialog MUST show two sections in its sidebar: "Notifications" and "Display" +- AND the first section ("Notifications") MUST be selected by default + +#### Scenario: Dialog navigation — switch section +- GIVEN the user settings dialog is open on the "Notifications" section +- WHEN the user clicks "Display" in the dialog's section sidebar +- THEN the content area MUST transition to show the Display section content +- AND the "Display" section item MUST be highlighted as active + +#### Scenario: Load user settings on open +- GIVEN the user settings dialog is opened +- WHEN `UserSettings.vue` mounts +- THEN `settingsStore.fetchUserSettings()` MUST be called +- AND while loading, each toggle and selector MUST render in a loading/skeleton state +- AND after loading, each control MUST reflect the current saved value from `IConfig` + +--- + +### Requirement: Notification Toggles [MVP] + +#### Scenario: Display notification toggles +- GIVEN the user settings dialog is open on the "Notifications" section +- WHEN the section content renders +- THEN the following toggles MUST be present: + - "Notify me when a task is assigned to me" (`notify_assigned`, default: on) + - "Notify me 1 day before a task's due date" (`notify_due_reminder`, default: on) +- AND each toggle MUST be an `NcCheckboxRadioSwitch` (toggle mode) from `@nextcloud/vue` +- AND each toggle's state MUST reflect the value returned by `fetchUserSettings()` + +#### Scenario: Toggle notification preference — save immediately +- GIVEN the "Notifications" section is rendered with toggles +- WHEN the user clicks a toggle to change its state +- THEN `settingsStore.updateUserSetting(key, value)` MUST be called immediately (no separate Save button for toggles) +- AND the toggle MUST update optimistically (change is visible before server confirms) +- AND on success, a brief confirmation indicator (or no feedback for toggle, per NC convention) is shown +- AND on failure, the toggle MUST revert to its previous state and an error toast MUST appear + +#### Scenario: Notification service respects user preference +- GIVEN a user has toggled `notify_assigned` to off +- WHEN user B assigns a task to this user +- THEN `NotificationService::notify('task_assigned', ...)` MUST check the user's `notify_assigned` IConfig value +- AND MUST find `false` (or string `'no'`) +- AND MUST NOT create or send the notification +- AND the assignment MUST still succeed + +--- + +### Requirement: Default View Selector [MVP] + +#### Scenario: Display default view selector +- GIVEN the user settings dialog is open on the "Display" section +- WHEN the section content renders +- THEN a "Default view" label and a dropdown/radio selector MUST be present +- AND the three options MUST be: "My Work" (value: `my-work`), "Kanban" (value: `kanban`), "Backlog" (value: `backlog`) +- AND the selector MUST reflect the value returned by `fetchUserSettings()` (default: `my-work`) + +#### Scenario: Change default view +- GIVEN the "Display" section is rendered +- WHEN the user selects "Kanban" from the default view selector +- THEN `settingsStore.updateUserSetting('default_view', 'kanban')` MUST be called immediately +- AND the next time the user opens a project (without a saved route), Planix MUST navigate to the Kanban view +- AND the setting MUST persist in `OCP\IConfig` across browser sessions + +--- + +### Requirement: Settings Persistence and Backend [MVP] + +#### Scenario: Admin settings persist via IAppConfig +- GIVEN the admin changes the default columns list and clicks "Save changes" +- WHEN `settingsStore.updateAdminSettings({ default_columns: [...] })` calls `PUT /planix/settings/admin` +- THEN the backend MUST call `IAppConfig::setValueArray('planix', 'default_columns', [...])` (or `setValueString` with JSON-encoded value) +- AND subsequent reads via `GET /planix/settings/admin` MUST return the updated value +- AND new projects created after the save MUST use the updated column set + +#### Scenario: User settings persist via IConfig +- GIVEN a user toggles `notify_assigned` to off +- WHEN `PUT /planix/settings/user` is called with `{ notify_assigned: false }` +- THEN the backend MUST call `IConfig::setUserValue($uid, 'planix', 'notify_assigned', 'no')` +- AND subsequent calls to `GET /planix/settings/user` MUST return `notify_assigned: false` +- AND the toggle MUST still be off after the user closes and reopens the dialog + +#### Scenario: User settings survive browser restart +- GIVEN a user has set `notify_assigned = false` and `default_view = kanban` +- WHEN the user closes the browser, clears session cookies, and returns to Planix +- THEN `GET /planix/settings/user` MUST still return `notify_assigned: false` and `default_view: "kanban"` (stored server-side in `IConfig`, not in browser storage) + +--- + +### Requirement: Admin Access Control [MVP] + +#### Scenario: Admin endpoint blocked for regular users +- GIVEN a regular (non-admin) Nextcloud user +- WHEN they call `GET /planix/settings/admin`, `PUT /planix/settings/admin`, or `POST /planix/settings/admin/register-init` +- THEN Nextcloud MUST return HTTP 403 Forbidden +- AND the admin settings link MUST NOT appear in the user's Nextcloud settings navigation + +#### Scenario: User settings accessible to all authenticated users +- GIVEN any authenticated Nextcloud user +- WHEN they call `GET /planix/settings/user` or `PUT /planix/settings/user` +- THEN the request MUST succeed (200) +- AND the response MUST only contain settings for the calling user (uid from session) + +--- + +### Requirement: i18n Coverage [MVP] + +#### Scenario: All user-visible strings use t() +- GIVEN any Vue component or PHP file in this change +- WHEN it contains a string visible to the end user +- THEN the string MUST be wrapped in `t('planix', '...')` (Vue) or `$this->l10n->t('...')` (PHP) +- AND the key MUST be present in both `l10n/en.json` and `l10n/nl.json` +- AND NO English text MUST appear as a hardcoded string in templates or PHP output + +#### Scenario: Dutch translation completeness +- GIVEN the `l10n/nl.json` file +- WHEN checked against `l10n/en.json` +- THEN every key present in `en.json` introduced by this change MUST also be present in `nl.json` +- AND all Dutch translations MUST be human-readable Dutch (no English placeholders, no machine-translation artifacts) + +--- + +### Requirement: Loading and Error States [MVP] + +#### Scenario: Admin settings loading +- GIVEN `AdminSettings.vue` is mounted +- WHEN `fetchAdminSettings()` is in progress +- THEN the `ColumnListEditor` area MUST show a skeleton loading state +- AND the "Save changes" button MUST be disabled until loading completes + +#### Scenario: User settings loading +- GIVEN the `UserSettings.vue` dialog has opened +- WHEN `fetchUserSettings()` is in progress +- THEN all toggles and selectors MUST show a loading/disabled state +- AND no stale values MUST be shown (no flash of default values before server values load) + +#### Scenario: Save failure — optimistic rollback +- GIVEN the user changes a setting (toggle or selector) +- WHEN `updateUserSetting()` applies the change optimistically AND the API call fails +- THEN the control MUST revert to its previous state +- AND an error toast MUST be shown: `t('planix', 'Failed to save settings')` diff --git a/openspec/changes/admin-user-settings/tasks.md b/openspec/changes/admin-user-settings/tasks.md new file mode 100644 index 0000000..ded14c0 --- /dev/null +++ b/openspec/changes/admin-user-settings/tasks.md @@ -0,0 +1,301 @@ +# Tasks: admin-user-settings + +**Change ID:** admin-user-settings +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Implementation Tasks + +### Task 1: Setup and Prerequisites +- **spec_ref**: `openspec/specs/admin-user-settings.md` +- **files**: `src/store/settings.js`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN the developer inspects `@conduction/nextcloud-vue` WHEN checking exports THEN `CnVersionInfoCard` and `CnSettingsSection` are available + - GIVEN the developer checks `@nextcloud/vue` WHEN checking exports THEN `NcAppSettingsDialog` and `NcCheckboxRadioSwitch` are available + - GIVEN the developer verifies dependencies WHEN checking `lib/Settings/AdminSettings.php` THEN the file exists and is registered in `lib/AppInfo/Application.php` + - GIVEN the developer verifies WHEN checking `src/views/settings/UserSettings.vue` THEN the file exists (even if empty placeholder) +- [ ] Confirm `@conduction/nextcloud-vue` exports: `CnVersionInfoCard`, `CnSettingsSection` +- [ ] Confirm `@nextcloud/vue` exports: `NcAppSettingsDialog`, `NcCheckboxRadioSwitch` +- [ ] Confirm `lib/Settings/AdminSettings.php` is registered in `Application.php` +- [ ] Create directory `src/components/settings/` if not present +- [ ] Create `src/store/settings.js` stub with empty state and no-op actions + +--- + +### Task 2: SettingsController — Admin Endpoints +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-settings-persistence-and-backend` +- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN an admin calls `GET /planix/settings/admin` WHEN the controller handles the request THEN a JSON response with `default_columns`, `register_initialized`, `app_version`, `update_available`, `update_version` is returned + - GIVEN an admin calls `PUT /planix/settings/admin` with `{ default_columns: ["A","B"] }` WHEN the controller handles the request THEN `IAppConfig::setValueString('planix', 'default_columns', '["A","B"]')` is called and HTTP 200 is returned + - GIVEN a non-admin user calls any `/planix/settings/admin` endpoint WHEN Nextcloud processes the request THEN HTTP 403 is returned (enforced by `@AdminRequired` annotation) +- [ ] Add `adminIndex(): JSONResponse` action to `SettingsController` (or create it if missing) + - Read `default_columns` from `IAppConfig` (default: `["To Do","In Progress","Review","Done"]`) + - Read `app_version` via `\OCP\App::getAppVersion('planix')` + - Read `register_initialized` from `ConfigurationService::isInitialized()` + - Read `update_available` and `update_version` from `IAppManager` / app info (cached) + - Annotate with `@AdminRequired` +- [ ] Add `adminUpdate(array $settings): JSONResponse` action + - Accept `default_columns` (JSON-decode, validate is array of strings) + - Store via `IAppConfig::setValueString` + - Annotate with `@AdminRequired` +- [ ] Add routes to `appinfo/routes.php`: + - `['name' => 'settings#adminIndex', 'url' => '/settings/admin', 'verb' => 'GET']` + - `['name' => 'settings#adminUpdate', 'url' => '/settings/admin', 'verb' => 'PUT']` +- [ ] Run `composer check:strict` +- [ ] Test + +--- + +### Task 3: SettingsController — User Endpoints +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-settings-persistence-and-backend` +- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN any authenticated user calls `GET /planix/settings/user` WHEN the controller handles the request THEN a JSON response with all user setting keys and their values is returned for the calling user only + - GIVEN a user calls `PUT /planix/settings/user` with `{ notify_assigned: false }` WHEN the controller handles the request THEN `IConfig::setUserValue($uid, 'planix', 'notify_assigned', 'no')` is called and HTTP 200 is returned + - GIVEN user A calls `PUT /planix/settings/user` WHEN another user B reads their settings THEN user B's settings are unaffected +- [ ] Add `userIndex(): JSONResponse` action to `SettingsController` + - Read all user settings from `IConfig::getUserValue($uid, 'planix', $key, $default)` + - Return boolean values as PHP booleans (not strings) in JSON + - No admin annotation required +- [ ] Add `userUpdate(array $settings): JSONResponse` action + - Accept any subset of user setting keys; ignore unknown keys + - Store booleans as `'yes'`/`'no'` strings via `IConfig::setUserValue` + - Store `default_view` as string value directly +- [ ] Add routes to `appinfo/routes.php`: + - `['name' => 'settings#userIndex', 'url' => '/settings/user', 'verb' => 'GET']` + - `['name' => 'settings#userUpdate', 'url' => '/settings/user', 'verb' => 'PUT']` +- [ ] Run `composer check:strict` +- [ ] Test + +--- + +### Task 4: SettingsController — Register Init Endpoint +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-register-initialization-flow` +- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN an admin calls `POST /planix/settings/admin/register-init` WHEN the controller handles the request THEN `ConfigurationService::importFromApp()` is called and HTTP 200 with `{ success: true }` is returned + - GIVEN `importFromApp()` throws an exception WHEN the controller handles the error THEN HTTP 500 with `{ success: false, message: "..." }` is returned + - GIVEN a non-admin user calls this endpoint WHEN Nextcloud processes the request THEN HTTP 403 is returned +- [ ] Add `adminRegisterInit(): JSONResponse` action to `SettingsController` + - Inject `ConfigurationService` via constructor + - Call `$this->configurationService->importFromApp()` in try/catch + - Return `JSONResponse(['success' => true])` on success + - Return `JSONResponse(['success' => false, 'message' => $e->getMessage()], 500)` on failure + - Annotate with `@AdminRequired` +- [ ] Add route: `['name' => 'settings#adminRegisterInit', 'url' => '/settings/admin/register-init', 'verb' => 'POST']` +- [ ] Run `composer check:strict` +- [ ] Test + +--- + +### Task 5: Pinia Settings Store +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-loading-and-error-states` +- **files**: `src/store/settings.js` +- **acceptance_criteria**: + - GIVEN `useSettingsStore()` is called WHEN the store is initialized THEN it exposes: `adminSettings`, `userSettings`, `adminLoading`, `userLoading`, `error` + - GIVEN `fetchUserSettings()` is called WHEN the API responds THEN `userSettings` is populated with the server values + - GIVEN `updateUserSetting('notify_assigned', false)` is called WHEN the API call is in progress THEN `userSettings.notify_assigned` immediately reflects `false` (optimistic update) + - GIVEN `updateUserSetting` is called and the API call fails WHEN the store catches the error THEN the key reverts to its previous value + - GIVEN `initRegister()` is called WHEN the API call succeeds THEN `adminSettings.register_initialized` is set to `true` +- [ ] Implement `fetchAdminSettings()` — GET `/planix/settings/admin`, set `adminSettings`, handle loading/error states +- [ ] Implement `updateAdminSettings(data)` — PUT `/planix/settings/admin`; update `adminSettings` on success +- [ ] Implement `initRegister()` — POST `/planix/settings/admin/register-init`; set `adminSettings.register_initialized = true` on success +- [ ] Implement `fetchUserSettings()` — GET `/planix/settings/user`, set `userSettings`, handle loading/error +- [ ] Implement `updateUserSetting(key, value)` — optimistic update + PUT `/planix/settings/user`; revert on failure +- [ ] Test + +--- + +### Task 6: AdminSettings.php — Page Data Injection +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-cnversioninfocard-integration` +- **files**: `lib/Settings/AdminSettings.php`, `templates/admin-settings.php` +- **acceptance_criteria**: + - GIVEN an admin navigates to Administration → Planix WHEN the PHP template renders THEN the HTML contains `
` + - GIVEN a newer version exists in the app store WHEN `AdminSettings.php` builds the template data THEN `update_available` is `true` and `update_version` is the new version string (served from ICache, TTL 1 hour) + - GIVEN the app store check fails WHEN the template renders THEN `update_available` defaults to `false` gracefully +- [ ] Modify `lib/Settings/AdminSettings.php`: + - Inject `IAppManager`, `ICacheFactory`, `ConfigurationService` via constructor + - Read `appVersion` via `\OCP\App::getAppVersion('planix')` + - Check update status via `IAppManager::getAppInfo()` or app store endpoint; cache result for 1 hour + - Read `registerInitialized` via `ConfigurationService::isInitialized()` + - Pass all values to `TemplateResponse` +- [ ] Modify `templates/admin-settings.php`: + - Output `
` + - Include `admin-settings.js` script via `\OCP\Util::addScript('planix', 'admin-settings')` +- [ ] Run `composer check:strict` +- [ ] Test + +--- + +### Task 7: AdminSettings.vue — Vue Component +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-admin-settings-vue-mount` +- **files**: `src/views/settings/AdminSettings.vue`, webpack config / `appinfo/assets.php` +- **acceptance_criteria**: + - GIVEN `AdminSettings.vue` mounts onto `#planix-admin-settings` WHEN the component initializes THEN it reads `data-app-version`, `data-update-available`, `data-register-initialized` from the mount div and passes them to sub-components + - GIVEN `fetchAdminSettings()` is loading WHEN the component renders the column editor section THEN a skeleton loading state is shown + - GIVEN settings are loaded WHEN the admin changes a column and clicks "Save changes" THEN `updateAdminSettings` is called; a "Saving…" state appears on the button; on success the button reverts to "Save changes" +- [ ] Create `src/views/settings/AdminSettings.vue` + - On `onMounted`: read `data-*` from mount div; call `settingsStore.fetchAdminSettings()` + - Render `CnVersionInfoCard` with version and update props + - Render `CnSettingsSection` "Default Project Configuration" containing `ColumnListEditor` + - Render `CnSettingsSection` "Register Setup" with init status and button + - "Save changes" button calls `updateAdminSettings({ default_columns: columns.value })` +- [ ] Add webpack entry `admin-settings.js` that imports and mounts `AdminSettings.vue` +- [ ] Register the entry point so `\OCP\Util::addScript('planix', 'admin-settings')` resolves correctly +- [ ] Test + +--- + +### Task 8: ColumnListEditor Component +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-column-list-editor-component` +- **files**: `src/components/settings/ColumnListEditor.vue` +- **acceptance_criteria**: + - GIVEN `modelValue: ["To Do", "In Progress", "Done"]` WHEN `ColumnListEditor` renders THEN 3 rows appear with correct labels and a drag handle, editable text input, and remove button on each + - GIVEN the admin drags row 2 above row 1 WHEN the drag completes THEN `update:modelValue` is emitted with the reordered array + - GIVEN the admin clicks "Move up" on row 2 WHEN the action completes THEN `update:modelValue` is emitted with row 2 in position 1; focus follows the moved row + - GIVEN only 1 row remains WHEN the component renders THEN the remove button is disabled + - GIVEN an empty column name exists WHEN the parent validates before saving THEN the parent can check `modelValue.some(v => v.trim() === '')` and show an error +- [ ] Create `src/components/settings/ColumnListEditor.vue` + - Props: `{ modelValue: Array, disabled: Boolean }` + - Emit: `['update:modelValue']` + - Use `vue-draggable-plus` (SortableJS wrapper) if available; otherwise HTML5 DnD API + - Each row: drag handle (`IconDragVertical`), ``, Move Up button, Move Down button, Remove button + - "Add column" button appends empty string and focuses the new input + - Remove button disabled when `modelValue.length <= 1` + - Move Up disabled for first row; Move Down disabled for last row +- [ ] Keyboard accessibility: Move Up/Down buttons must be focusable and operable via Enter/Space +- [ ] Use CSS variables for colors; no hardcoded color values +- [ ] Test + +--- + +### Task 9: UserSettings.vue — NcAppSettingsDialog +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-ncappsettingsdialog-layout` +- **files**: `src/views/settings/UserSettings.vue` +- **acceptance_criteria**: + - GIVEN the gear icon is clicked in Planix navigation WHEN `UserSettings.vue` is opened THEN it renders as an `NcAppSettingsDialog` (NOT `NcDialog`) + - GIVEN the dialog opens WHEN `onMounted` runs THEN `settingsStore.fetchUserSettings()` is called; toggles show loading state until resolved + - GIVEN settings are loaded WHEN the dialog renders THEN the "Notifications" section shows two toggles with correct initial values; "Display" section shows the default view selector with correct initial value + - GIVEN the user switches to the "Display" section WHEN the section nav item is clicked THEN the content area transitions to the Display content +- [ ] Implement `src/views/settings/UserSettings.vue` (replaces empty placeholder) + - Use `NcAppSettingsDialog` with `sections` prop: `[{ id: 'notifications', name: t('planix', 'Notifications') }, { id: 'display', name: t('planix', 'Display') }]` + - On `onMounted`: call `settingsStore.fetchUserSettings()` + - Render Notifications section: two `NcCheckboxRadioSwitch` (type="switch") for `notify_assigned` and `notify_due_reminder` + - Render Display section: label + `NcSelect` or `NcCheckboxRadioSwitch` (type="radio") for `default_view` + - Each control binds to `settingsStore.userSettings[key]` and calls `settingsStore.updateUserSetting(key, value)` on change +- [ ] Wire gear icon in `MainMenu.vue` to open the dialog (v-model or direct `open` call) +- [ ] Test + +--- + +### Task 10: Wire Gear Icon in MainMenu.vue +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-ncappsettingsdialog-layout` +- **files**: `src/navigation/MainMenu.vue` +- **acceptance_criteria**: + - GIVEN the user is in Planix WHEN they look at the bottom of the navigation sidebar THEN a gear icon (`NcAppNavigationItem` or `NcButton` with gear icon) is visible + - GIVEN the user clicks the gear icon WHEN the click handler fires THEN `showUserSettings.value = true` causes `UserSettings.vue` to render and open +- [ ] Add a gear icon navigation item at the bottom of `MainMenu.vue` using the `#footer` slot or equivalent +- [ ] Add `const showUserSettings = ref(false)` and toggle on gear icon click +- [ ] Include `` +- [ ] Test + +--- + +### Task 11: NotificationService — Align SUBJECT_SETTING_MAP Keys +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-notification-toggles` +- **files**: `lib/Service/NotificationService.php` +- **acceptance_criteria**: + - GIVEN `NotificationService::SUBJECT_SETTING_MAP` is inspected WHEN the map is read THEN `'task_assigned'` maps to `'notify_assigned'` (not `'notify_task_assigned'`) + - GIVEN `NotificationService::SUBJECT_SETTING_MAP` is inspected WHEN the map is read THEN `'task_due_soon'` maps to `'notify_due_reminder'` + - GIVEN user B has `notify_assigned = 'no'` in `IConfig` WHEN `notify('task_assigned', ..., userBUid)` is called THEN no notification is sent + - GIVEN user B has `notify_due_reminder = 'no'` WHEN `notify('task_due_soon', ..., userBUid)` is called THEN no notification is sent +- [ ] Update `SUBJECT_SETTING_MAP` in `lib/Service/NotificationService.php`: + ```php + private const SUBJECT_SETTING_MAP = [ + 'task_assigned' => 'notify_assigned', + 'task_due_soon' => 'notify_due_reminder', + // V1 — declared but not triggered in MVP: + 'task_overdue' => 'notify_overdue', + 'task_commented' => 'notify_commented', + 'task_status_changed' => 'notify_status_changed', + ]; + ``` +- [ ] Verify default value logic: `IConfig::getUserValue($uid, 'planix', 'notify_assigned', 'yes')` returns `'yes'` for users who have never toggled the setting (correct default-on behaviour) +- [ ] Run `composer check:strict` +- [ ] Test + +--- + +### Task 12: i18n — English Strings +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-i18n-coverage` +- **files**: `l10n/en.json` +- **acceptance_criteria**: + - GIVEN the `l10n/en.json` file WHEN inspected THEN all strings listed in the i18n inventory in `design.md` are present as keys + - GIVEN any Vue template or PHP file in this change WHEN all user-visible strings are checked THEN each uses `t('planix', '...')` / `$this->l10n->t('...')` and the key exists in `en.json` +- [ ] Add all admin and user settings strings to `l10n/en.json` (see i18n inventory in `design.md`) +- [ ] Verify no hardcoded English strings remain in any new or modified component or PHP file +- [ ] Test + +--- + +### Task 13: i18n — Dutch Translations +- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-i18n-coverage` +- **files**: `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN the `l10n/nl.json` file WHEN compared to `l10n/en.json` THEN every key added by this change in `en.json` also exists in `nl.json` + - GIVEN the Dutch translations WHEN reviewed THEN they are natural Dutch (not literal translations or English placeholders) +- [ ] Add Dutch translations for all settings strings to `l10n/nl.json` +- [ ] Key translations: + - `Planix Settings` → `Planix instellingen` + - `Default Project Configuration` → `Standaard projectconfiguratie` + - `Default columns for new projects` → `Standaard kolommen voor nieuwe projecten` + - `Add column` → `Kolom toevoegen` + - `Remove column` → `Kolom verwijderen` + - `Move up` → `Omhoog verplaatsen` + - `Move down` → `Omlaag verplaatsen` + - `Save changes` → `Wijzigingen opslaan` + - `Register Setup` → `Register instellen` + - `Register initialized` → `Register geïnitialiseerd` + - `Register not initialized` → `Register niet geïnitialiseerd` + - `Initialize register` → `Register initialiseren` + - `Notifications` → `Meldingen` + - `Notify me when a task is assigned to me` → `Stuur een melding wanneer een taak aan mij is toegewezen` + - `Notify me 1 day before a task's due date` → `Stuur een melding 1 dag voor de vervaldatum van een taak` + - `Display` → `Weergave` + - `Default view` → `Standaardweergave` + - `My Work` → `Mijn werk` + - `Settings saved` → `Instellingen opgeslagen` + - `Failed to save settings` → `Instellingen konden niet worden opgeslagen` +- [ ] Test + +--- + +## Verification +- [ ] All tasks checked off +- [ ] Manual testing against acceptance criteria +- [ ] Admin settings page visible under Nextcloud Administration → Planix (admin account only) +- [ ] User settings dialog opens from gear icon in Planix navigation +- [ ] Default columns change persists after page reload +- [ ] Register initialization button works on a clean install +- [ ] Notification toggles save and are respected by NotificationService + +## Tests (company-wide ADR-009) +- [ ] PHPUnit unit tests for `SettingsController::adminIndex` (returns all expected keys, 403 for non-admin) +- [ ] PHPUnit unit tests for `SettingsController::userIndex` and `userUpdate` (returns user-scoped values, boolean conversion) +- [ ] PHPUnit unit tests for `SettingsController::adminRegisterInit` (success + exception handling, 403 for non-admin) +- [ ] PHPUnit unit tests for `NotificationService` with updated `SUBJECT_SETTING_MAP` key names (preference check, self-notification guard) +- [ ] Browser tests (Playwright MCP) for admin settings page: version card renders, column list editable, save persists +- [ ] Browser tests (Playwright MCP) for user settings dialog: opens from gear icon, toggle saves, default view selector saves +- [ ] Browser tests (Playwright MCP) for register init: button shows spinner, success state updates after init +- [ ] Browser tests (Playwright MCP) for settings persistence: reload and verify saved values are still shown +- [ ] All tests pass + +## Documentation (company-wide ADR-010) +- [ ] Feature documentation updated (admin settings and user settings sections in `docs/`) +- [ ] Screenshot captured: admin settings page with version card and column editor; user settings dialog open on Notifications section + +## i18n (company-wide ADR-005) +- [ ] Dutch and English translation strings added (Tasks 12 and 13) diff --git a/openspec/changes/dashboard-my-work/.openspec.yaml b/openspec/changes/dashboard-my-work/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/dashboard-my-work/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/dashboard-my-work/design.md b/openspec/changes/dashboard-my-work/design.md new file mode 100644 index 0000000..71eb253 --- /dev/null +++ b/openspec/changes/dashboard-my-work/design.md @@ -0,0 +1,287 @@ +# Design: dashboard-my-work + +**Change ID:** dashboard-my-work +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Context + +The `tasks` change built task CRUD, the task store, `TaskCard`, and `TaskStatusBadge`. The `projects` change built the project store and project list. This change builds the personal aggregation layer on top of those foundations — the dashboard and My Work views. + +Planix is a thin client: all data lives in OpenRegister and is queried via `useObjectStore('planix', 'task')` and `useObjectStore('planix', 'project')`. The dashboard performs at most 3 parallel API calls on load and derives all KPI counts client-side. No new PHP services or database tables are needed. + +--- + +## Goals + +- Dashboard landing page (`/`) with 4 KPI cards, recent projects, and due-this-week tasks. +- KPI cards that navigate to My Work with a filter pre-applied. +- My Work view (`/my-work`) with three urgency groups, priority sort, and filter URL integration. +- Inline status update on My Work task rows (no full navigation). +- CnEmptyState for all empty states (no projects, no tasks due, no assigned tasks). +- Full i18n coverage (en + nl). + +## Non-Goals + +- Activity feed on dashboard (V1 — requires Nextcloud Activity API integration). +- Nextcloud Dashboard widget (`OCP\Dashboard\IWidget`) surfacing overdue count (V1). +- Sub-task display in My Work (tasks change handles sub-tasks as V1). +- Reporting or analytics (separate future change). +- Custom notification rules for dashboard data (no new PHP services in this change). + +--- + +## Decisions + +### Decision 1: Dashboard performs 2 parallel API calls (tasks + projects) + +**Options considered:** +1. 3 parallel calls (tasks, projects, due-this-week tasks separately). +2. 2 parallel calls — derive due-this-week client-side from the task list (chosen). + +**Rationale:** The due-this-week list is a subset of all assigned tasks. Eliminating the 3rd call simplifies the fetch logic. The 2 calls are: +1. `useObjectStore('planix', 'task').getObjects({ assignedTo: currentUser })` — all tasks assigned to current user (used for KPIs, My Work grouping, AND due-this-week derivation). +2. `useObjectStore('planix', 'project').getObjects({ members: currentUser })` — all projects the user is a member of (for recent projects). + +Using `Promise.all` makes both calls fire simultaneously. The dashboard shows a skeleton loading state until both resolve. Due-this-week tasks are filtered client-side: `tasks.filter(t => t.dueDate && t.dueDate <= today+7 && t.status !== 'done')`. + +**Note on project progress bars:** The progress bar on recent projects shows all tasks in the project (done/total), not just the current user's tasks. This requires fetching task counts per project — either via a lightweight count query per project or by fetching all project tasks in a 3rd call. The implementer should choose the most efficient approach based on OpenRegister's query capabilities. + +### Decision 2: KPI counts are computed client-side from the task list + +**Options considered:** +1. A dedicated API call per KPI (4 calls, each returning a count). +2. Derive all KPIs from the single task list returned by call 1 (chosen). + +**Rationale:** Once call 1 returns all tasks assigned to the current user, KPI counts are simple array filters: +- **Open tasks**: `tasks.filter(t => ['open', 'in_progress'].includes(t.status)).length` +- **Overdue**: `tasks.filter(t => t.dueDate < today && t.status !== 'done' && t.status !== 'cancelled').length` +- **In progress**: `tasks.filter(t => t.status === 'in_progress').length` +- **Completed today**: `tasks.filter(t => t.completedAt && isToday(t.completedAt)).length` + +No additional API calls are needed. This avoids 4 extra round-trips and keeps all data consistent (same snapshot). + +### Decision 3: KpiCard is a pure display component — navigation handled by parent + +**Options considered:** +1. `KpiCard` navigates internally using `useRouter`. +2. `KpiCard` emits `click` event; `DashboardView` handles navigation (chosen). + +**Rationale:** `KpiCard` may be reused in different contexts (e.g., future reporting widget). Keeping navigation in the parent makes the component portable and testable in isolation. The parent (`DashboardView`) calls `router.push({ name: 'MyWork', query: { filter: filterValue } })` on the emitted `click` event. + +Stable prop interface: +```js +props: { + label: { type: String, required: true }, + count: { type: Number, required: true }, + icon: { type: String, required: true }, // icon component name + color: { type: String, required: true }, // CSS variable name e.g. '--color-error' + filterValue: { type: String, required: true } // passed back in click event +} +``` + +### Decision 4: My Work groups are computed client-side from a single task fetch + +My Work fetches all tasks assigned to the current user (same call as the dashboard's call 1) and groups them into three buckets: + +| Group | Condition | Header color | +|-------|-----------|--------------| +| **Overdue** | `dueDate < today && status !== 'done' && status !== 'cancelled'` | `--color-error` | +| **Due this week** | `dueDate >= today && dueDate <= today+7 && status !== 'done' && status !== 'cancelled'` | `--color-warning` | +| **Everything else** | all other non-done, non-cancelled tasks | default | + +Within each group, tasks are sorted by priority: +```js +const PRIORITY_ORDER = { urgent: 0, high: 1, normal: 2, low: 3 } +group.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]) +``` + +Done and cancelled tasks are excluded from My Work entirely. If the user wants to see completed tasks, they navigate to the full task list (`/tasks`). + +### Decision 5: Filter URL params control My Work initial state + +KPI cards on the dashboard navigate to `/my-work?filter=`. The `MyWorkView` reads the `filter` query param on mount and scrolls to / highlights the relevant group: + +| Filter value | Behaviour | +|-------------|-----------| +| `overdue` | Scroll to Overdue group, briefly highlight | +| `in_progress` | Scroll to Everything Else group, filter to `in_progress` status within it | +| `open` | Show all three groups (default) | +| `completed_today` | Show a fourth ephemeral group "Completed Today" at the top (read-only, status `done` + `completedAt` is today) | + +The filter param is not persistent — it only controls the initial scroll/highlight on load. The user can then freely browse all groups. + +### Decision 6: Inline status update uses a dropdown overlay, not a dialog + +**Options considered:** +1. Navigate to task detail to change status. +2. Inline `NcSelect` dropdown on the status cell of `MyWorkTaskRow` (chosen). + +**Rationale:** The My Work view's primary value is momentum — users should be able to mark tasks done, move them in-progress, or block them without losing context. A dropdown on the status indicator is the fastest interaction pattern and is consistent with how `TaskMetaSidebar` works in the task detail view. The dropdown calls `tasksStore.updateStatus(taskId, newStatus)` directly. On success, the task row updates reactively (or disappears from the group if the new status moves it to done/cancelled). + +### Decision 7: Recent projects list shows at most 5, sorted by updatedAt desc + +The project store returns all projects the user is a member of. `DashboardRecentProjects` takes the top 5 by `updatedAt` descending. Progress bar calculation: +```js +const done = tasks.filter(t => t.project === project.id && t.status === 'done').length +const total = tasks.filter(t => t.project === project.id).length +const progress = total > 0 ? Math.round((done / total) * 100) : 0 +``` + +Task counts come from the task list already fetched in call 1 (no extra API call). If the user is not a member of any project, `DashboardRecentProjects` is replaced by `CnEmptyState`. + +### Decision 8: CnEmptyState is used for all empty states + +All three empty states use the `CnEmptyState` component from `@conduction/nextcloud-vue`: + +| Location | Title | Action | +|----------|-------|--------| +| Dashboard — no projects | "No projects yet" | "Create project" → `/projects/new` | +| Dashboard — no tasks due this week | "No tasks due this week" | none (informational only) | +| My Work — no assigned tasks | "No tasks assigned to you" | "Browse projects" → `/projects` | + +The dashboard KPI cards always render (showing 0) even when there are no projects or tasks. + +### Decision 9: No new store is created — reuse existing stores + +`DashboardView` and `MyWorkView` import and use `useTasksStore` (from the `tasks` change) and `useProjectsStore` (from the `projects` change) directly. No new Pinia store is created for the dashboard. Dashboard-specific computed state (KPI counts, grouped tasks, recent projects) is defined as local `computed` refs inside the view component using Vue Composition API. + +--- + +## Component Architecture + +``` +src/ + views/ + DashboardView.vue # / — landing page, 3 parallel fetches + MyWorkView.vue # /my-work — grouped task list, filter URL + components/ + KpiCard.vue # Reusable KPI card (label, count, icon, color) + DashboardRecentProjects.vue # Recent 5 projects with progress bars + DashboardDueThisWeek.vue # Due-this-week task list with date chips + MyWorkTaskRow.vue # Single task row with inline status dropdown + router/ + index.js # Modified — add / and /my-work routes + navigation/ + MainMenu.vue # Modified — add My Work nav entry + +appinfo/ + routes.php # Modified — add /my-work catch-all +``` + +--- + +## Dashboard Layout + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ KPI Cards (4 columns, responsive → 2 on tablet → 1 on mobile) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Open │ │ Overdue │ │In Progress│ │Completed │ │ +│ │ 12 │ │ 3 │ │ 5 │ │ Today: 2 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +├──────────────────────────────────────────┬───────────────────────────┤ +│ Recent Projects (left, ~60% width) │ Due This Week (right) │ +│ ┌──────────────────────────────────┐ │ ┌─────────────────────┐ │ +│ │ [icon] Project Alpha ████░ 70% │ │ │ Task A — Project X │ │ +│ │ [icon] Project Beta ██░░░ 40% │ │ │ Due today │ │ +│ │ ... │ │ │ Task B — Project Y │ │ +│ └──────────────────────────────────┘ │ │ Due in 3 days │ │ +│ │ └─────────────────────┘ │ +└──────────────────────────────────────────┴───────────────────────────┘ +``` + +KPI card colors (CSS variables): +- Open tasks: `--color-primary-element` +- Overdue: `--color-error` +- In progress: `--color-warning` +- Completed today: `--color-success` + +--- + +## My Work Layout + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ My Work [filter dropdown] │ +├──────────────────────────────────────────────────────────────────────┤ +│ ▼ Overdue (red) │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ [!] Fix login bug [Backend] Overdue: Jan 30 [●] In Progress │ │ +│ │ [!] Update docs [Frontend] Overdue: Feb 1 [●] Open │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ ▼ Due This Week (amber) │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ [↑] Write tests [Backend] Due Feb 5 [●] Open │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ ▼ Everything Else │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ [↓] Refactor store [Backend] No due date [●] Open │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +`MyWorkTaskRow` anatomy (left to right): +1. Priority dot (color-coded: urgent=red, high=amber, normal=none, low=blue) +2. Task title (clickable → `/tasks/:id`, truncated to 1 line) +3. Project badge (`NcBadge` with project color) +4. Due date chip (neutral / amber today / red overdue) +5. Status dropdown (`NcSelect` with `TaskStatusBadge` option slots) + +--- + +## Vue Router Routes + +| Path | Name | Component | Notes | +|------|------|-----------|-------| +| `/` | `Dashboard` | `DashboardView` | Default route — replaces any previous root redirect | +| `/my-work` | `MyWork` | `MyWorkView` | Reads `?filter` query param | + +--- + +## PHP Routing + +```php +// appinfo/routes.php additions +['name' => 'page#my_work', 'url' => '/my-work', 'verb' => 'GET'], +``` + +The root `/` route is already served by the existing `page#index` catch-all. + +--- + +## i18n String Inventory + +| Key context | Example string | +|-------------|----------------| +| Navigation | `My Work` | +| Dashboard title | `Dashboard` | +| KPI labels | `Open Tasks`, `Overdue`, `In Progress`, `Completed Today` | +| Recent projects header | `Recent Projects` | +| Recent projects progress | `{done} of {total} tasks done` | +| Due this week header | `Due This Week` | +| Due date chips | `Due today`, `Due tomorrow`, `Due {date}`, `Overdue: {date}` | +| Empty — no projects | `No projects yet` | +| Empty — no tasks due | `No tasks due this week` | +| Empty — no assigned | `No tasks assigned to you` | +| Empty actions | `Create project`, `Browse projects` | +| My Work title | `My Work` | +| My Work groups | `Overdue`, `Due This Week`, `Everything Else`, `Completed Today` | +| My Work row — no due date | `No due date` | +| Status labels | `Open`, `In Progress`, `Blocked`, `Done`, `Cancelled` | +| Priority labels | `Urgent`, `High`, `Normal`, `Low` | +| Error states | `Failed to load dashboard data`, `Failed to update task status` | +| Loading state | `Loading your work…` | + +--- + +## Risks and Trade-offs + +| Risk | Likelihood | Mitigation | +|------|-----------|-----------| +| 3 parallel API calls on dashboard load are slow for users in many projects | Low | Skeleton loading state hides the delay. If call 1 returns quickly, KPIs and My Work are usable before calls 2 and 3 resolve. | +| `completed_today` filter group relies on client-side `completedAt` date comparison | Low | `completedAt` is set by the `tasks` change store action to `new Date().toISOString()`. Timezone drift is acceptable at MVP. | +| Inline status update in My Work causes task to disappear from the group it was in | Intentional | Task reactively moves to the correct group or disappears (if done/cancelled). This is the expected UX — it mirrors how kanban columns work. Show a brief undo toast in V1. | +| Dashboard root route (`/`) may conflict with an existing redirect | Low | Check current `router/index.js` before applying. The `projects` change likely has a root redirect that this change replaces with the Dashboard component. | diff --git a/openspec/changes/dashboard-my-work/proposal.md b/openspec/changes/dashboard-my-work/proposal.md new file mode 100644 index 0000000..d41d1b2 --- /dev/null +++ b/openspec/changes/dashboard-my-work/proposal.md @@ -0,0 +1,81 @@ +# Change Proposal: dashboard-my-work + +**Change ID:** dashboard-my-work +**Status:** proposed +**Created:** 2026-04-02 +**Author:** Conduction Development Team + +--- + +## Why + +Planix now has projects, tasks, and a kanban board — but there is no personal landing page. When a user opens the app, they have no immediate overview of their own work state. Every user must manually navigate to a project and search for their tasks, making it hard to prioritise daily work or spot overdue items at a glance. + +This change delivers two personal views that transform Planix into a daily-driver tool: + +1. **Personal Dashboard** — the default landing page (`/`) showing KPI cards (open tasks, overdue, in progress, completed today), the 5 most recently active projects the user is a member of, and tasks due within 7 days. No new entities are needed; the dashboard is a frontend aggregation over existing Task and Project data. +2. **My Work view** (`/my-work`) — a priority-sorted, urgency-grouped list of all tasks assigned to the current user, with an inline status update control so users can act on tasks without navigating away. + +Both views are personal and user-scoped. No new backend entities or PHP controllers are required beyond SPA route registrations. The dashboard is deliberately minimal (3 parallel API calls) to keep load times fast even for users who are members of many projects. + +--- + +## What Changes + +Adds the Vue frontend and routing needed to: + +1. **Dashboard view** — route `/` renders `DashboardView.vue`. On mount, 3 parallel API calls fetch: tasks assigned to current user, projects the user is a member of, and tasks with `dueDate` within 7 days. The view renders 4 KPI cards and two list sections (recent projects, due this week). +2. **KPI card component** — `KpiCard.vue` — reusable card showing a label, count, icon, and color. Clickable; emits a filter value that the parent uses to navigate to `/my-work?filter=`. +3. **Recent projects list** — `DashboardRecentProjects.vue` — shows the 5 most recently active projects the user is in (sorted by `updatedAt` desc), with title, color/icon, task count, and a progress bar (done/total). +4. **Due this week section** — `DashboardDueThisWeek.vue` — lists tasks with `dueDate` within the next 7 days, sorted by due date ascending, with project badge and due date chip. +5. **My Work view** — route `/my-work` renders `MyWorkView.vue`. Fetches tasks assigned to current user. Groups them into Overdue (red header), Due this week, and Everything else. Within each group, sorts by priority (urgent → high → normal → low). +6. **My Work task row** — `MyWorkTaskRow.vue` — shows task title (clickable → `/tasks/:id`), project badge, due date chip, priority dot, and an inline status dropdown. Status can be updated without navigating away. +7. **Empty states** — `CnEmptyState` used for: no-projects dashboard, no-tasks-due-this-week section, empty My Work list. +8. **Filter URL integration** — My Work reads `?filter` query param on mount and applies it (overdue, in_progress, open, completed_today). KPI cards write the filter param on navigate. +9. **Vue Router routes** — `/` (Dashboard) and `/my-work` (My Work) added. +10. **Navigation wiring** — "My Work" entry added to `MainMenu.vue`; Dashboard is already the root path. +11. **i18n strings** — all user-visible strings added to `l10n/en.json` and `l10n/nl.json`. + +--- + +## Capabilities + +### Modified Capabilities + +- **`dashboard-my-work`** — implementing the personal dashboard and My Work views defined in `openspec/specs/dashboard-my-work.md`. This change brings the capability from spec-only to fully implemented: KPI cards, recent projects with progress bars, due-this-week task list, My Work grouping/sorting, inline status update, filter URL integration, and all empty states. + +No new capabilities are introduced. The `dashboard-my-work` capability was declared in the spec and depends on the `tasks` and `projects` capabilities already implemented. + +--- + +## Impact + +### Files Changed + +| File | Change | +|------|--------| +| `src/views/DashboardView.vue` | New — dashboard landing page with 3 parallel fetches, KPI cards, recent projects, due this week | +| `src/views/MyWorkView.vue` | New — My Work view with grouping, sorting, filter URL integration | +| `src/components/KpiCard.vue` | New — reusable KPI card: label, count, icon, color, clickable | +| `src/components/DashboardRecentProjects.vue` | New — recent projects list with progress bars | +| `src/components/DashboardDueThisWeek.vue` | New — due-this-week task list with date chips | +| `src/components/MyWorkTaskRow.vue` | New — single task row in My Work with inline status dropdown | +| `src/router/index.js` | Modified — add `/` (Dashboard) and `/my-work` routes | +| `src/navigation/MainMenu.vue` | Modified — add My Work navigation entry | +| `appinfo/routes.php` | Modified — add SPA catch-all for `/my-work` | +| `l10n/en.json` | Modified — add all dashboard and My Work translation strings | +| `l10n/nl.json` | Modified — add Dutch translations for all dashboard/My Work strings | + +### Risk + +Low. This change is entirely additive — no existing components or views are modified (except routing and navigation). The dashboard is a pure frontend aggregation over existing API endpoints. No new PHP services are introduced. The only PHP change is adding a catch-all route for `/my-work` in `appinfo/routes.php`. + +The inline status update in My Work reuses the existing `tasksStore.updateStatus()` action from the `tasks` change — no new store logic is needed. The `tasks` change must be applied before this one. + +### Dependencies + +- `register-schemas` must be applied first (Task and Project schemas must exist in OpenRegister). +- `projects` change must be applied first (project store and `useObjectStore('planix', 'project')` must be in place). +- `tasks` change must be applied first (`useTasksStore`, `TaskStatusBadge`, and `updateStatus` must exist; `TaskCard` is available as a reference but not directly reused here — `MyWorkTaskRow` is a custom row layout). +- `@conduction/nextcloud-vue` must export `CnEmptyState`, `useObjectStore`, `CnDetailPage` (already declared in `package.json`). +- OpenRegister `^v0.2.10` (already declared). diff --git a/openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md b/openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md new file mode 100644 index 0000000..30b1569 --- /dev/null +++ b/openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md @@ -0,0 +1,238 @@ +# Delta Spec: dashboard-my-work + +**Capability:** dashboard-my-work +**Change ID:** dashboard-my-work +**Delta type:** implementation +**Base spec:** [openspec/specs/dashboard-my-work.md](../../../../specs/dashboard-my-work.md) +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Summary + +This delta captures implementation-specific requirements added when building the Dashboard and My Work views. The base spec (`openspec/specs/dashboard-my-work.md`) defines all business requirements, scenarios, user stories, and acceptance criteria. The delta below documents: + +1. KPI card component anatomy and reuse contract. +2. Data fetching strategy (3 parallel calls, client-side KPI derivation). +3. My Work grouping and sorting logic. +4. Filter URL parameter integration. +5. CnEmptyState patterns for all empty states. +6. Inline status update interaction pattern. +7. Responsive layout constraints. +8. i18n requirements. +9. Constraints introduced by the thin-client + `useObjectStore` architecture. + +All base spec requirements are implemented as-is. No base spec requirement is modified or removed. + +--- + +## ADDED Requirements + +### Requirement: KPI Card Component [MVP] + +A reusable `KpiCard.vue` component MUST be created for the dashboard KPI section. + +#### Scenario: KPI card renders correctly +- GIVEN the dashboard loads and task data is available +- WHEN `KpiCard` is rendered with a label, count, icon, color, and filterValue +- THEN the card MUST display the label, count (as a large number), and icon +- AND the card border or background accent MUST use the provided CSS variable color +- AND the card MUST be keyboard-focusable and show a focus ring + +#### Scenario: KPI card click navigates to My Work with filter +- GIVEN the user clicks a KPI card +- WHEN the `click` event fires +- THEN the parent `DashboardView` MUST navigate to `/my-work?filter=` +- AND the My Work view MUST apply the filter immediately on mount + +#### Scenario: KPI card loading state +- GIVEN the dashboard is loading (API calls in flight) +- WHEN `KpiCard` is rendered +- THEN it MUST show a skeleton loader in place of the count +- AND the card MUST NOT be clickable during loading + +--- + +### Requirement: Data Fetching Strategy [MVP] + +The dashboard MUST use 3 parallel API calls via `Promise.all` on component mount. + +#### Scenario: Parallel fetch on dashboard mount +- GIVEN the user navigates to `/` +- WHEN `DashboardView` mounts +- THEN the component MUST fire all 3 API calls simultaneously using `Promise.all`: + 1. `useObjectStore('planix', 'task').getObjects({ assignedTo: currentUser })` — all assigned tasks + 2. `useObjectStore('planix', 'project').getObjects({ members: currentUser })` — all member projects + 3. `useObjectStore('planix', 'task').getObjects({ assignedTo: currentUser, dueDateBefore: sevenDaysFromNow })` — tasks due this week +- AND a skeleton loading state MUST be shown until all 3 calls resolve +- AND if any call fails, an error banner MUST appear with "Failed to load dashboard data" and a Retry button + +#### Scenario: KPI counts derived client-side +- GIVEN call 1 has resolved with a list of tasks +- WHEN KPI counts are computed +- THEN the following client-side filters MUST be applied to the task list: + - Open tasks: `status in ['open', 'in_progress']` + - Overdue: `dueDate < today AND status NOT IN ['done', 'cancelled']` + - In progress: `status === 'in_progress'` + - Completed today: `completedAt exists AND isToday(completedAt)` +- AND these counts MUST update reactively if a task status is updated inline (e.g., from My Work) + +--- + +### Requirement: My Work List Layout [MVP] + +The `MyWorkView` MUST group and render tasks in a specific layout. + +#### Scenario: My Work groups and sort order +- GIVEN the user opens My Work +- WHEN task data loads +- THEN tasks MUST be divided into three groups in this order: + 1. **Overdue** — `dueDate < today AND status NOT IN ['done', 'cancelled']` + 2. **Due This Week** — `dueDate >= today AND dueDate <= today+7 AND status NOT IN ['done', 'cancelled']` + 3. **Everything Else** — all remaining non-done, non-cancelled tasks +- AND within each group tasks MUST be sorted by priority: urgent (0) → high (1) → normal (2) → low (3) +- AND done and cancelled tasks MUST NOT appear in any group + +#### Scenario: My Work task row fields +- GIVEN a task row in My Work +- WHEN `MyWorkTaskRow` renders +- THEN it MUST show (left to right): priority dot, task title (clickable), project badge, due date chip, status indicator +- AND the priority dot MUST use CSS variables: urgent=`--color-error`, high=`--color-warning`, normal=transparent, low=`--color-info` +- AND the project badge MUST show the project color defined in the project object +- AND the due date chip MUST follow the same overdue/today/future logic as `TaskCard` from the `tasks` change + +#### Scenario: Group headers +- GIVEN My Work is rendered with tasks in multiple groups +- WHEN the view renders +- THEN each group header MUST show the group name, task count, and a collapse/expand toggle +- AND the Overdue group header MUST use `--color-error` for its label color +- AND the Due This Week group header MUST use `--color-warning` for its label color +- AND an empty group MUST be hidden (not rendered as an empty section) + +--- + +### Requirement: Status Update Inline [MVP] + +The My Work view MUST allow status updates without full navigation. + +#### Scenario: Inline status dropdown +- GIVEN a task row in My Work +- WHEN the user clicks the status indicator chip +- THEN a dropdown MUST appear with all available status options: Open, In Progress, Blocked, Done, Cancelled +- AND each option MUST display the `TaskStatusBadge` styling for that status +- AND selecting a status MUST call `tasksStore.updateStatus(taskId, newStatus)` immediately +- AND the row MUST update reactively after the store action resolves + +#### Scenario: Task disappears from group after status update +- GIVEN the user marks an Overdue task as Done +- WHEN `updateStatus` resolves successfully +- THEN the task MUST be removed from the Overdue group reactively +- AND if the group becomes empty, the group section MUST be hidden +- AND a brief success indicator (toast or row highlight) MUST confirm the update + +#### Scenario: Status update error handling +- GIVEN the user selects a new status +- WHEN the `updateStatus` store action fails (network error) +- THEN the row MUST revert to the previous status +- AND an error toast MUST appear: "Failed to update task status" + +--- + +### Requirement: Filter URL Integration [MVP] + +My Work MUST read and apply filter query parameters from the URL. + +#### Scenario: Apply filter from KPI card navigation +- GIVEN the user clicks the "Overdue" KPI card on the dashboard +- WHEN the router navigates to `/my-work?filter=overdue` +- THEN the My Work view MUST scroll to the Overdue group on mount +- AND the Overdue group MUST be briefly highlighted (CSS animation, 2 seconds) + +#### Scenario: Filter param for in_progress +- GIVEN the user clicks the "In Progress" KPI card +- WHEN the router navigates to `/my-work?filter=in_progress` +- THEN the My Work view MUST scroll to the Everything Else group +- AND within that group, tasks with `status === 'in_progress'` MUST be visually highlighted + +#### Scenario: Filter param for completed_today +- GIVEN the user clicks the "Completed Today" KPI card +- WHEN the router navigates to `/my-work?filter=completed_today` +- THEN the My Work view MUST render an additional ephemeral group at the top: "Completed Today" +- AND this group MUST show tasks with `status === 'done'` AND `isToday(completedAt)` +- AND this group MUST be read-only (no inline status update available) + +#### Scenario: No filter param +- GIVEN the user navigates to `/my-work` without a `?filter` param +- WHEN the view mounts +- THEN all three standard groups MUST render normally with no scroll or highlight behavior + +--- + +### Requirement: Empty States [MVP] + +All empty states MUST use the `CnEmptyState` component from `@conduction/nextcloud-vue`. + +#### Scenario: Dashboard no-projects empty state +- GIVEN the user is authenticated and is not a member of any project +- WHEN `DashboardRecentProjects` renders +- THEN it MUST render `CnEmptyState` with: + - Title: `t('planix', 'No projects yet')` + - Description: `t('planix', 'Create your first project to get started')` + - Action button: `t('planix', 'Create project')` — navigates to `/projects/new` +- AND the KPI cards MUST still render with all counts at 0 + +#### Scenario: Dashboard due-this-week empty state +- GIVEN the user has no tasks due within 7 days +- WHEN `DashboardDueThisWeek` renders +- THEN it MUST render `CnEmptyState` with: + - Title: `t('planix', 'No tasks due this week')` + - No action button +- AND the Recent Projects section MUST still render normally + +#### Scenario: My Work empty state +- GIVEN the user has no tasks assigned to them +- WHEN `MyWorkView` renders +- THEN it MUST render `CnEmptyState` with: + - Title: `t('planix', 'No tasks assigned to you')` + - Description: `t('planix', 'Tasks assigned to you will appear here')` + - Action button: `t('planix', 'Browse projects')` — navigates to `/projects` + +--- + +### Requirement: Responsive Layout [MVP] + +The dashboard MUST be usable on tablet-sized screens (minimum 768 px wide). + +#### Scenario: KPI cards responsive grid +- GIVEN the viewport is >= 1024 px +- WHEN the dashboard renders +- THEN KPI cards MUST be displayed in a 4-column grid +- GIVEN the viewport is 768–1023 px +- THEN KPI cards MUST be displayed in a 2-column grid +- GIVEN the viewport is < 768 px +- THEN KPI cards MUST be displayed in a 1-column stack + +#### Scenario: Two-column dashboard layout +- GIVEN the viewport is >= 1024 px +- WHEN the dashboard renders the main content area +- THEN Recent Projects and Due This Week MUST be displayed side by side (approximately 60/40 split) +- GIVEN the viewport is < 1024 px +- THEN they MUST stack vertically (Recent Projects above Due This Week) + +--- + +### Requirement: i18n Coverage [MVP] + +All user-visible strings in Dashboard and My Work MUST be internationalised. + +#### Scenario: English strings present +- GIVEN the `l10n/en.json` file +- WHEN inspected +- THEN all strings listed in the i18n inventory in `design.md` MUST be present as keys +- AND every Vue template string MUST use `t('planix', '...')` — no hardcoded English strings + +#### Scenario: Dutch translations present +- GIVEN the `l10n/nl.json` file +- WHEN compared to `l10n/en.json` +- THEN every key added by this change in `en.json` MUST also exist in `nl.json` diff --git a/openspec/changes/dashboard-my-work/tasks.md b/openspec/changes/dashboard-my-work/tasks.md new file mode 100644 index 0000000..b72b120 --- /dev/null +++ b/openspec/changes/dashboard-my-work/tasks.md @@ -0,0 +1,251 @@ +# Tasks: dashboard-my-work + +**Change ID:** dashboard-my-work +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Implementation Tasks + +### Task 1: Setup and Prerequisites +- **spec_ref**: `openspec/specs/dashboard-my-work.md` +- **files**: `src/router/index.js`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN a fresh Planix install WHEN the developer checks the app THEN the `register-schemas`, `projects`, and `tasks` changes are already applied + - GIVEN the developer inspects `@conduction/nextcloud-vue` WHEN checking exports THEN `CnEmptyState`, `useObjectStore`, `CnDetailPage` are all available + - GIVEN the developer inspects `src/store/` THEN `useTasksStore` and `useProjectsStore` are importable +- [ ] Verify `register-schemas`, `projects`, and `tasks` changes are applied +- [ ] Confirm `@conduction/nextcloud-vue` exports: `CnEmptyState`, `useObjectStore` +- [ ] Confirm `useTasksStore` and `useProjectsStore` are available from `src/store/` +- [ ] Confirm `src/components/` directory exists (from `tasks` change) + +--- + +### Task 2: KpiCard Component +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-kpi-card-component` +- **files**: `src/components/KpiCard.vue` +- **acceptance_criteria**: + - GIVEN a `KpiCard` with label "Overdue", count 3, icon `AlertCircle`, color `--color-error`, filterValue `overdue` WHEN rendered THEN it shows "Overdue", "3", and a red accent + - GIVEN the dashboard is loading WHEN `KpiCard` renders with `:loading="true"` THEN a skeleton replaces the count number and the card is not clickable + - GIVEN the user clicks the card WHEN not loading THEN a `click` event is emitted with `filterValue` and the parent navigates to `/my-work?filter=overdue` + - GIVEN a keyboard user focuses the card and presses Enter THEN the same click behavior is triggered +- [ ] Create `src/components/KpiCard.vue` with props `{ label: String, count: Number, icon: String, color: String, filterValue: String, loading: Boolean }` +- [ ] Render label (small uppercase), count (large bold number), icon (NcIconSvgWrapper or slot) +- [ ] Apply `color` CSS variable as left border or card accent using inline style binding +- [ ] Implement skeleton loading state (hide count, show `NcLoadingIcon` or shimmer div) +- [ ] Emit `click` event with `filterValue`; bind `@keyup.enter` to same handler +- [ ] Ensure keyboard focus ring using Nextcloud focus styles (`focus-visible`) +- [ ] Test + +--- + +### Task 3: DashboardView — Data Fetching and KPI Cards +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-data-fetching-strategy` +- **files**: `src/views/DashboardView.vue` +- **acceptance_criteria**: + - GIVEN `DashboardView` mounts WHEN the component initialises THEN 3 API calls fire simultaneously via `Promise.all` + - GIVEN all 3 calls resolve WHEN the view renders THEN KPI cards show correct counts (Open, Overdue, In Progress, Completed Today) + - GIVEN one call fails WHEN `Promise.allSettled` catches it THEN an error banner appears with "Failed to load dashboard data" and a Retry button + - GIVEN a user inline-updates a task status in My Work WHEN they navigate back to the dashboard THEN KPI counts are re-fetched on next mount +- [ ] Create `src/views/DashboardView.vue` +- [ ] On `onMounted`, fire all 3 calls with `Promise.allSettled` (not `Promise.all` — to handle partial failures gracefully) +- [ ] Derive KPI counts as `computed` refs from the resolved task list +- [ ] Render 4 `KpiCard` components with correct label, count, icon, color, filterValue props +- [ ] Handle KPI card `click` event: `router.push({ name: 'MyWork', query: { filter: filterValue } })` +- [ ] Show error banner if any fetch fails; Retry button re-fires all 3 calls +- [ ] Show skeleton loading state while any call is pending +- [ ] Test + +--- + +### Task 4: DashboardRecentProjects Component +- **spec_ref**: `openspec/specs/dashboard-my-work.md#scenario-recent-projects-list` +- **files**: `src/components/DashboardRecentProjects.vue` +- **acceptance_criteria**: + - GIVEN the user is a member of 8 projects WHEN `DashboardRecentProjects` renders THEN only the 5 most recently active (by `updatedAt` desc) are shown + - GIVEN a project has 10 tasks, 7 done WHEN the component renders THEN the progress bar shows 70% fill and label "7 of 10 tasks done" + - GIVEN the user is not a member of any project WHEN the component renders THEN `CnEmptyState` appears with title "No projects yet" and a "Create project" button + - GIVEN a project entry WHEN clicked THEN the router navigates to `/projects/:id` +- [ ] Create `src/components/DashboardRecentProjects.vue` with props `{ projects: Array, tasks: Array }` +- [ ] Sort projects by `updatedAt` descending, take first 5 +- [ ] For each project, compute `done` count and `total` count from the passed `tasks` array (filtered by `project === project.id`) +- [ ] Render progress bar using `NcProgressBar` or a styled `
` with width computed from `(done/total)*100` +- [ ] Show project icon/color, title, task count as clickable row +- [ ] Show `CnEmptyState` when `projects.length === 0` +- [ ] Test + +--- + +### Task 5: DashboardDueThisWeek Component +- **spec_ref**: `openspec/specs/dashboard-my-work.md#scenario-tasks-due-this-week` +- **files**: `src/components/DashboardDueThisWeek.vue` +- **acceptance_criteria**: + - GIVEN tasks due in the next 7 days WHEN `DashboardDueThisWeek` renders THEN tasks appear sorted by `dueDate` ascending + - GIVEN a task with `dueDate === today` WHEN the component renders THEN the due date chip uses `--color-warning` and shows "Due today" + - GIVEN a task with `dueDate === yesterday` and `status !== 'done'` WHEN the component renders THEN the due date chip uses `--color-error` and shows "Overdue: {date}" + - GIVEN there are no tasks due this week WHEN the component renders THEN `CnEmptyState` appears with title "No tasks due this week" + - GIVEN the user clicks a task title WHEN navigating THEN the router navigates to `/tasks/:id` +- [ ] Create `src/components/DashboardDueThisWeek.vue` with prop `{ tasks: Array }` +- [ ] Sort tasks by `dueDate` ascending +- [ ] Render each task as a row: title (clickable → `/tasks/:id`), project badge, due date chip +- [ ] Implement due date chip logic: future (neutral), today (`--color-warning`), overdue (`--color-error`) +- [ ] Show `CnEmptyState` when `tasks.length === 0` +- [ ] Test + +--- + +### Task 6: MyWorkView — Layout and Grouping Logic +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-my-work-list-layout` +- **files**: `src/views/MyWorkView.vue` +- **acceptance_criteria**: + - GIVEN the user opens `/my-work` WHEN the component mounts THEN all tasks assigned to the current user are fetched via `useTasksStore` + - GIVEN tasks load WHEN the view renders THEN tasks appear in 3 groups: Overdue (red header), Due This Week (amber header), Everything Else + - GIVEN the Overdue group has 0 tasks WHEN the view renders THEN the Overdue section is hidden entirely + - GIVEN tasks within a group WHEN rendered THEN they are sorted by priority: urgent → high → normal → low + - GIVEN done and cancelled tasks WHEN grouping is applied THEN they MUST NOT appear in any group +- [ ] Create `src/views/MyWorkView.vue` +- [ ] On `onMounted`, call `useTasksStore().fetchTasks({ assignedTo: currentUser })` +- [ ] Define `computed` grouping: `overdueGroup`, `dueThisWeekGroup`, `everythingElseGroup` +- [ ] Sort each group by priority using `PRIORITY_ORDER = { urgent: 0, high: 1, normal: 2, low: 3 }` +- [ ] Render each group with a section header (name, count, collapse toggle) +- [ ] Apply `--color-error` to Overdue header, `--color-warning` to Due This Week header +- [ ] Hide empty groups (don't render the section at all) +- [ ] Render `MyWorkTaskRow` for each task in each group +- [ ] Show `CnEmptyState` when all three groups are empty +- [ ] Test + +--- + +### Task 7: MyWorkTaskRow Component +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-my-work-list-layout` +- **files**: `src/components/MyWorkTaskRow.vue` +- **acceptance_criteria**: + - GIVEN a task row WHEN rendered THEN it shows (left to right): priority dot, task title, project badge, due date chip, status indicator + - GIVEN `task.priority === 'urgent'` WHEN the priority dot renders THEN it uses `--color-error` + - GIVEN the user clicks the task title WHEN navigating THEN the router navigates to `/tasks/:id` + - GIVEN the user clicks the status indicator WHEN the dropdown opens THEN all 5 status options are shown with `TaskStatusBadge` styling + - GIVEN the user selects a new status WHEN `updateStatus` resolves THEN the row updates reactively; if status is done/cancelled the row disappears from the group +- [ ] Create `src/components/MyWorkTaskRow.vue` with prop `{ task: Object }` +- [ ] Priority dot: a small circle (`12 px`) with background CSS variable based on priority +- [ ] Task title: `NcButton` variant `tertiary` (no border, text-style) that navigates to `/tasks/:id` +- [ ] Project badge: `NcBadge` with project color and name (look up project from `useProjectsStore` by `task.project`) +- [ ] Due date chip: reuse the same date-chip logic as `DashboardDueThisWeek` +- [ ] Status indicator: `NcSelect` (or `NcActions` + items) showing current status; on change, call `tasksStore.updateStatus(task.id, newStatus)` +- [ ] Import `TaskStatusBadge` from `src/components/TaskStatusBadge.vue` for status option rendering +- [ ] Show error toast on `updateStatus` failure and revert status reactively +- [ ] Test + +--- + +### Task 8: Status Update Inline — Error Handling and Reactive Updates +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-status-update-inline` +- **files**: `src/components/MyWorkTaskRow.vue`, `src/views/MyWorkView.vue` +- **acceptance_criteria**: + - GIVEN the user updates status to `done` WHEN the store action resolves THEN the task is removed from the current group reactively (no page reload) + - GIVEN the network fails during `updateStatus` WHEN the promise rejects THEN the status indicator reverts to the previous value and an error toast appears + - GIVEN a loading state during `updateStatus` WHEN the dropdown is open THEN a spinner is shown on the selected option and other options are disabled +- [ ] Add local `updating` ref to `MyWorkTaskRow` — tracks in-progress status update +- [ ] On `updateStatus` start: set `updating = true`, disable dropdown options +- [ ] On `updateStatus` success: `updating = false`, let parent `computed` groups reactively re-filter +- [ ] On `updateStatus` failure: `updating = false`, revert `task.status` to previous value, show toast via `showError(t('planix', 'Failed to update task status'))` +- [ ] Ensure `MyWorkView` group computeds are reactive to task store changes (use `storeToRefs` or watch) +- [ ] Test + +--- + +### Task 9: Filter URL Integration +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-filter-url-integration` +- **files**: `src/views/MyWorkView.vue` +- **acceptance_criteria**: + - GIVEN the user navigates to `/my-work?filter=overdue` WHEN the component mounts THEN after data loads, the view scrolls to the Overdue group and the header briefly highlights + - GIVEN the user navigates to `/my-work?filter=completed_today` WHEN the component mounts THEN a "Completed Today" group appears at the top showing done tasks with `isToday(completedAt)` + - GIVEN the user navigates to `/my-work` (no filter) WHEN the component mounts THEN no scroll or highlight behaviour occurs + - GIVEN the user has already landed on My Work and navigates to the KPI card again WHEN the filter changes THEN `watchEffect` on the route query detects the change and re-applies the scroll/highlight +- [ ] Read `route.query.filter` using `useRoute()` in `MyWorkView` +- [ ] After data loads, use `nextTick` + `el.scrollIntoView({ behavior: 'smooth' })` to scroll to the target group +- [ ] Apply a CSS animation class (e.g., `--highlight-pulse`) to the group header for 2 seconds, then remove it +- [ ] For `filter === 'completed_today'`: compute `completedTodayGroup` from tasks with `status === 'done' && isToday(completedAt)`; render at top of groups as read-only (no status dropdown) +- [ ] For `filter === 'in_progress'`: scroll to "Everything Else" group, highlight rows with `status === 'in_progress'` +- [ ] Use `watch(() => route.query.filter, ...)` to re-apply on param change +- [ ] Test + +--- + +### Task 10: Navigation and Routing +- **spec_ref**: `openspec/specs/dashboard-my-work.md` +- **files**: `src/router/index.js`, `src/navigation/MainMenu.vue`, `appinfo/routes.php` +- **acceptance_criteria**: + - GIVEN the user is in Planix WHEN they look at the main navigation THEN a "My Work" entry is visible and clicking it navigates to `/my-work` + - GIVEN the user opens Planix WHEN Vue Router resolves the root path `/` THEN `DashboardView.vue` is rendered + - GIVEN the user navigates to `/my-work` in the browser directly WHEN Nextcloud serves the request THEN the SPA shell is returned and Vue Router takes over +- [ ] Add route to `src/router/index.js`: + - `{ path: '/', name: 'Dashboard', component: () => import('../views/DashboardView.vue') }` (replace any existing root redirect) + - `{ path: '/my-work', name: 'MyWork', component: () => import('../views/MyWorkView.vue') }` +- [ ] Add My Work nav entry to `src/navigation/MainMenu.vue` (`NcAppNavigationItem`, icon: `AccountClockOutline`, `:to="{ name: 'MyWork' }"`) +- [ ] Add PHP route to `appinfo/routes.php`: + - `['name' => 'page#my_work', 'url' => '/my-work', 'verb' => 'GET']` +- [ ] Test + +--- + +### Task 11: i18n — English Strings +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-i18n-coverage` +- **files**: `l10n/en.json` +- **acceptance_criteria**: + - GIVEN the `l10n/en.json` file WHEN inspected THEN all strings listed in the i18n inventory in `design.md` are present as keys + - GIVEN any Vue template in this change WHEN all user-visible strings are checked THEN each uses `t('planix', '...')` and the key exists in `en.json` +- [ ] Add all dashboard and My Work strings to `l10n/en.json` (see i18n inventory in `design.md`) +- [ ] Strings include: navigation, dashboard title, KPI labels, section headers, due date chips, empty state messages and actions, My Work groups, status/priority labels, error and loading strings +- [ ] Verify no hardcoded English strings remain in any new component +- [ ] Test + +--- + +### Task 12: i18n — Dutch Translations +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md#requirement-i18n-coverage` +- **files**: `l10n/nl.json` +- **acceptance_criteria**: + - GIVEN `l10n/nl.json` WHEN compared to `l10n/en.json` THEN every key added by this change in `en.json` also exists in `nl.json` + - GIVEN Dutch translations WHEN reviewed THEN they are natural Dutch (not literal or placeholder translations) +- [ ] Add Dutch translations for all dashboard/My Work strings to `l10n/nl.json` +- [ ] Key translations: `My Work` → `Mijn Werk`, `Dashboard` → `Dashboard`, `Open Tasks` → `Openstaande taken`, `Overdue` → `Verlopen`, `In Progress` → `In uitvoering`, `Completed Today` → `Vandaag afgerond`, `Recent Projects` → `Recente projecten`, `Due This Week` → `Deze week`, `No projects yet` → `Nog geen projecten`, `Create project` → `Project aanmaken`, `No tasks due this week` → `Geen taken deze week`, `No tasks assigned to you` → `Geen taken aan jou toegewezen`, `Browse projects` → `Projecten bekijken`, `Everything Else` → `Overige taken`, `Loading your work…` → `Jouw werk laden…` +- [ ] Test + +--- + +### Task 13: Testing — Unit and Browser Tests +- **spec_ref**: `openspec/changes/dashboard-my-work/specs/dashboard-my-work/spec.md` +- **files**: `src/components/__tests__/`, `src/views/__tests__/` +- **acceptance_criteria**: + - GIVEN the `KpiCard` component WHEN rendered with all props THEN label, count, and color accent are present in the DOM + - GIVEN `DashboardView` WHEN all 3 API calls resolve THEN KPI counts are computed correctly for each scenario (all zero, mixed, all high) + - GIVEN `MyWorkView` WHEN tasks with mixed due dates and priorities load THEN groups are in the correct order with correct tasks in each + - GIVEN a browser session navigating to `/my-work?filter=overdue` THEN the Overdue group is scrolled into view +- [ ] Browser tests (Playwright MCP) for Dashboard: load with tasks, load empty (no projects), load empty (no tasks due this week), KPI card click navigates to My Work +- [ ] Browser tests (Playwright MCP) for My Work: load with mixed tasks, groups correct, priority sort correct, inline status update (happy path), inline status update (error reverts) +- [ ] Browser tests (Playwright MCP) for filter URL integration: `/my-work?filter=overdue` scrolls and highlights, `?filter=completed_today` shows ephemeral group +- [ ] Browser tests (Playwright MCP) for empty states: no-projects dashboard, no-tasks-due section, empty My Work +- [ ] All tests pass + +--- + +## Verification +- [ ] All tasks checked off +- [ ] Manual testing against acceptance criteria + +## Tests (company-wide ADR-009) +- [ ] Browser tests (Playwright MCP) for Dashboard happy path (tasks + projects loaded) +- [ ] Browser tests (Playwright MCP) for Dashboard empty states (no projects, no tasks due) +- [ ] Browser tests (Playwright MCP) for KPI card navigation (each card → My Work with correct filter) +- [ ] Browser tests (Playwright MCP) for My Work view (grouping, priority sort, empty state) +- [ ] Browser tests (Playwright MCP) for inline status update (success, error revert) +- [ ] Browser tests (Playwright MCP) for filter URL params (overdue, completed_today, in_progress, no filter) +- [ ] All tests pass + +## Documentation (company-wide ADR-010) +- [ ] Feature documentation updated (dashboard and My Work sections in `docs/`) +- [ ] Screenshots captured: dashboard with KPI cards and recent projects, My Work with all three groups, inline status update dropdown, empty states + +## i18n (company-wide ADR-005) +- [ ] English and Dutch translation strings added (Tasks 11 and 12) diff --git a/openspec/changes/kanban-board/.openspec.yaml b/openspec/changes/kanban-board/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/kanban-board/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/kanban-board/design.md b/openspec/changes/kanban-board/design.md new file mode 100644 index 0000000..1d5eee3 --- /dev/null +++ b/openspec/changes/kanban-board/design.md @@ -0,0 +1,380 @@ +# Design: kanban-board + +**Change ID:** kanban-board +**Status:** draft +**Created:** 2026-04-02 + +--- + +## Context + +The `tasks` change delivered the `TaskCard` component, the task store, and the flat backlog list. The `projects` change installed placeholder views for `ProjectBoard.vue` and `ProjectBacklog.vue`. This change implements the kanban board on top of those foundations. + +Planix is a thin client: all data operations go through the OpenRegister REST API via `useObjectStore`. There are no new PHP controllers in this change — all board state (columns, task positions) is read and written through the existing OpenRegister object API from the Vue frontend. PHP routing already handles the `/projects/:id` SPA catch-all (installed by the `projects` change). + +--- + +## Goals + +- Render a kanban board with columns (ordered, horizontally scrollable) and `TaskCard` instances per column. +- Drag-and-drop task cards between columns: optimistic update → PATCH task in OpenRegister → revert on failure. +- WIP limit visual warning (orange/red column header + tooltip) — soft limit, not blocking. +- Board filter bar (Assignee, Label, Priority) with URL query string sync for shareable views. +- View toggle (kanban ↔ list) persisted in URL hash; filters preserved across switch. +- Column management: create, rename (+ WIP limit), reorder (drag column headers), delete with task migration dialog. +- Backlog panel: collapsible left sidebar showing `column: null` tasks; drag-to-column moves tasks onto the board. +- Keyboard navigation: Tab through columns, arrow keys within column, Enter to open task detail (WCAG AA). +- Full i18n coverage (en + nl). + +## Non-Goals + +- TaskCard component definition (delivered by `tasks` change — reused here). +- Task CRUD / task detail view (delivered by `tasks` change). +- Project creation / settings (delivered by `projects` change). +- Swimlanes, collapsed columns, card quick-edit, blocked indicators (all V1 — declared in spec notes). +- Sub-task tree on the board (V1). +- PHP notification triggers from board drag events (V1 — drag to `done`-type column triggers status change but no separate notification). + +--- + +## Decisions + +### Decision 1: Drag-drop library — `vuedraggable` (Vue 2 wrapper for SortableJS) + +**Options considered:** +1. Vanilla SortableJS with manual Vue integration. +2. `vuedraggable` (official Vue 2 wrapper for SortableJS) — chosen. +3. `@dnd-kit/core` (React-first, Vue port immature). +4. `vue-smooth-dnd` (unmaintained since 2021). + +**Rationale:** `vuedraggable` is the standard Vue 2 drag-and-drop solution (>5M weekly npm downloads). It wraps SortableJS and exposes Vue-idiomatic `v-model` binding plus `@change` events, making optimistic state management straightforward. It handles touch events, cross-list dragging, and drag handles natively. The package is actively maintained and used across dozens of Nextcloud apps. + +Package added: `vuedraggable` (`vuedraggable@^2.x` for Vue 2 compatibility). + +### Decision 2: Optimistic UI pattern — clone → apply → PATCH → revert + +**Options considered:** +1. Wait for API confirmation before moving the card (no optimism). +2. Apply immediately, revert on failure, show toast — chosen. + +**Rationale:** Waiting for the API makes the board feel sluggish. The standard UX expectation for kanban boards is that card drops are instant. The implementation pattern is: + +``` +1. User drops card into target column +2. Clone pre-move state (source column tasks, target column tasks) +3. Apply new positions to local Pinia state immediately +4. Call PATCH task: { column: targetColumnId, columnOrder: newIndex } +5a. On success: discard clone, update task in store +5b. On failure: restore cloned state, show toast "Failed to move task — try again" +``` + +The rollback restores both the task's column reference and its `columnOrder` within the column. A toast notification (using `NcToast` or equivalent) is shown on failure so the user knows the action did not persist. + +### Decision 3: Column store — separate `useColumnsStore` wrapping `useObjectStore('planix', 'column')` + +**Options considered:** +1. Fetch columns inside the project store. +2. Dedicated `useColumnsStore` in `src/store/columns.js` — chosen. + +**Rationale:** Columns are a first-class entity with their own CRUD and reorder logic. Keeping them in a dedicated store mirrors the `useTasksStore` pattern from the `tasks` change and keeps each store focused. The column store is project-scoped: `fetchColumns(projectId)` filters by `project === projectId`. Reorder is handled by PATCHing the `order` field on each column in sequence. + +The store exposes: +```js +// src/store/columns.js +{ + columns, // ref([]) — ordered list for current project + loading, + error, + fetchColumns(projectId), + createColumn(data), + updateColumn(id, data), + deleteColumn(id), + reorderColumns(orderedIds), // PATCH order on all columns + moveTasksToBacklog(columnId), + moveTasksToColumn(fromId, toId), +} +``` + +### Decision 4: Board layout — CSS Grid columns with overflow-x scroll + +**Options considered:** +1. Flexbox row with fixed column width. +2. CSS Grid with `grid-auto-flow: column` and `overflow-x: auto` — chosen. + +**Rationale:** CSS Grid with `grid-template-columns: repeat(auto-fill, 280px)` (or explicit per-column) gives precise column widths while allowing horizontal scroll when columns exceed viewport. This is the standard pattern used by Trello and Linear. Column width is 280 px (a widely-accepted kanban column width). Columns do not wrap — horizontal scroll is always `overflow-x: auto` on the board container. + +The backlog panel, when open, sits as a fixed-width left sidebar (240 px) inside the board container, reducing the available width for columns. + +### Decision 5: WIP limit UI — CSS class on column header + +**Options considered:** +1. Computed property returning an inline style object. +2. CSS classes `wip-warning` (orange) and `wip-exceeded` (red) applied to the column header — chosen. + +**Rationale:** CSS classes are more maintainable and allow overriding via NL Design tokens. The thresholds: +- `wip-warning`: task count equals `wipLimit` (at the limit, amber) +- `wip-exceeded`: task count exceeds `wipLimit` (over the limit, red) + +The column header renders a `` tooltip (`NcTooltip`) with text "WIP limit ({limit}) exceeded — {count} tasks in column". The tooltip is always present when a limit is set but only becomes visually prominent at `wip-warning` or `wip-exceeded`. + +Colours use CSS variables only: `wip-warning` → `var(--color-warning)`, `wip-exceeded` → `var(--color-error)`. + +### Decision 6: Board filter — URL query params, client-side application + +**Options considered:** +1. Server-side filter: re-fetch from OpenRegister on each filter change. +2. Client-side filter after initial full board fetch — chosen. + +**Rationale:** The board already fetches all tasks for the project on mount. Applying filters client-side is instantaneous (no spinner) and allows the "dim non-matching cards" UX pattern (cards remain visible but faded, so the user can still see the full board context). The filter state is synced to URL query params (`?assignee=uid&priority=high&label=bug`) so filtered views are shareable. + +Filter application logic in `KanbanBoard.vue`: +```js +const filteredTaskIds = computed(() => { + // Returns a Set of task IDs matching all active filters + // KanbanColumn checks: isVisible(task) = filteredTaskIds.has(task.id) || !hasActiveFilters +}) +``` + +Non-matching cards receive class `task-card--dimmed` (opacity 0.35) rather than being removed from the DOM, keeping drag-and-drop targets intact. + +### Decision 7: View toggle — URL hash (`#view=kanban` / `#view=list`) + +**Options considered:** +1. Separate route paths (`/projects/:id` and `/projects/:id/list`). +2. URL hash on the same route — chosen. + +**Rationale:** The view toggle does not change the logical resource (the project board). A hash avoids adding new Vue Router routes while persisting the selected view across page reloads. Query params are reserved for filters. The `ViewToggle` component reads `window.location.hash` on mount and sets it on toggle. If no hash is present, the kanban view is the default. + +### Decision 8: Backlog panel — collapsible left sidebar + +**Options considered:** +1. Separate route `/projects/:id/backlog` only (no panel). +2. Collapsible panel inside the board view — chosen. + +**Rationale:** The spec requires drag-from-backlog-to-board. This is only possible if both surfaces are visible simultaneously. The backlog panel is a collapsible `