Skip to content

feat(ui): project detail, task drawer and Kanban board (M4) - #5

Closed
gdarko wants to merge 4 commits into
feat/ui-projectsfrom
feat/ui-board
Closed

gdarko wants to merge 4 commits into
feat/ui-projectsfrom
feat/ui-board

Conversation

@gdarko

@gdarko gdarko commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Slice 2 of the module's UI (M4): the project detail page, the task drawer, the
task list and the Kanban board. Based on feat/ui-projects, which carries the
projects index this builds on.

What is here

Project detail projects/:id, with Overview, Tasks, Time and Members
rendered through <router-view> as registered children. The header shows the
name, identifier, status badge, the customer name looked up through the host
contact endpoint (falling back to #id), Edit through the existing project
modal and Archive/Restore. Overview renders the task counts, logged and
billable minutes, billable and unbilled amounts and a budget bar when the
project has one.

Board board, filtered by project and assignee, every column in one
request. Cards carry the name, number, project identifier, priority, assignee
initials, due date (red when overdue) and the billable flag. Drag uses
sortablejs directly; a "New task" button per column opens the drawer preset to
that column, and clicking a card opens it for editing.

Task drawer, shared by the board and every list: name, description,
project, read-only customer, status, assignee, priority, due date, estimate in
hours, billable and a rate override in major units, plus delete with a
confirm. Hours become minutes and major units become minor units on the way
out, and a 422 renders under its field.

Task list, a server-side BaseTable over the tasks endpoint, used by the
project Tasks tab and by an all-tasks page at tasks.

The projects index rows now link to the detail page and its header links to
the board.

Notes

  • A drop is undone in the DOM before the model changes, so Vue stays the only
    writer of the card's position. The neighbours in the target column become
    the before_id and after_id the move endpoint orders by, the returned
    board_position is applied, and a failed move puts both columns back with a
    notification.
  • Resolving the detail route by name matches the parent record alone, so the
    page settles on its Overview child when it is the target itself. Without
    that, a link straight to the project would render an empty body.
  • Only GET projects/{id} carries the totals, so every write on the detail
    page re-reads the project rather than keeping what the write returned.
  • New registrations, messages, api functions and types live in new files
    (registrations/board.ts, messages/board.ts, api/board.ts,
    types/task.ts, types/task-status.ts, types/board.ts,
    types/project-member.ts, types/time-entry.ts), so init.ts gains one
    import and one call and messages.ts is untouched. api.ts exports its
    base path, and support/format.ts gains three helpers at the end.
  • sortablejs is a runtime dependency bundled into dist/; it has no Vue
    dependency of its own. dist/init.js still destructures the host's Vue
    exactly once and imports nothing from "vue".

Checks

pnpm run lint, pnpm exec tsc --noEmit, pnpm run build,
validate-module and validate-package all pass, and dist/ is committed.

Driven in the running app: the projects index, the detail heading and its four
tabs, a task created through the drawer, a card dragged between columns and
reordered inside one (both persisted across a reload), attaching and detaching
a project member, archive and restore, and the all-tasks page. No console
errors.

https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E

The typed client wraps the task, task status, board, project member and time
entry endpoints over the axios instance the host injects, alongside the host
contact lookup the project header needs for a customer name. The base path
moves out of the projects client so both files build their URLs from one
constant.

Money stays in integer minor units and durations in minutes on the wire;
support/format gains the duration, initials and overdue helpers the board
and the project totals render with. sortablejs is a runtime dependency and
is bundled into dist/, which is how an npm dependency reaches an install.
The drawer is the one editor for a task, shared by the board and every list:
name, project, status, assignee, priority, due date, estimate, billable flag
and a rate override, with hours converted to minutes and major units to
minor units on the way out. A 422 renders under its field. The customer is
read-only, because a task with a project takes the project's.

The project id is the source of truth behind the project picker, so a page
that fixes the project and hides the picker still saves the task under it,
and editing a task whose project is not in the loaded list never clears it.

The list is a server-side BaseTable over the tasks endpoint with search,
status and assignee filters, and it tells its parent when a write lands so a
project's totals can catch up.
Three pages register through the host page contract, from their own
registration file so a later slice of the module never edits the same lines.
Their strings live in their own message bundle, which the host merges into
the module's namespace recursively.

The board reads every column in one request and drags with sortablejs. A
drop is undone in the DOM first, so the model stays the only writer, then
the neighbours in the target column become the before and after ids the move
endpoint orders by; the returned board position is applied and a failed move
puts both columns back. Each column opens the drawer preset to itself.

The project detail carries the header, the actions and four tabs rendered
through router-view: totals and a budget bar, the task list, a read-only
time table and the member editor. The tabs read the active route off the
host router, because a module bundle cannot call useRoute, and landing on
the page itself settles on the Overview child so the body is never empty.
Only the detail endpoint carries totals, so every write re-reads the project
rather than keeping what it returned.
dist/ ships with the package so an install never has to build. The bundle
still destructures the host's Vue once and imports nothing from "vue", and
carries sortablejs, which has no Vue dependency of its own.
@gdarko

gdarko commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #14, which carries this stack consolidated into three commits on top of main.

@gdarko gdarko closed this Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant