Work Feature Guide
Guide to the Work module — professional work tracking across multiple jobs, each with a type-specific track hierarchy, time-logging sessions, and job-type-specific planning (tickets, milestones, and full store operations).
Overview
The Work module tracks professional work across multiple jobs. Every job has a jobType that determines its entire shape — track hierarchy, planning model, and session fields differ per type:
SOFTWARE_ENGINEERING— flat projects as tracks, a per-job people roster, and tickets (status, priority, ETA, assignees, checklist tasks) on each project. Sessions require anactivityCategoryand may optionally link to aticketId.CLIENT_BASED— clients → projects track hierarchy. Project tracks carry metadata (status, ETA, budget, hourly rate, contact, notes) plus milestones. Sessions may be flaggedbillable.STORE— flat areas (allPROJECTtracks), a launch profile, milestones (with amilestoneKindandrenewalDate), suppliers, operational tasks (+ recurring templates), inventory (products + stock events), staff shifts, and P&L (finance transactions linked bywork_job_id). Sessions require a storeactivityCategory.
Creating a STORE job with seedLaunchTemplate: true (the default for new store jobs) seeds three areas (Permits, Fit-out, Inventory), permit milestones, and an empty store profile.
Unlike the Android-style feature modules (e.g. Habits, with *_domain/*_data/*_presentation Gradle modules), Work has no separate feature module. It lives in three places: the Ktor server (entities, services, routing), shared KMP (serializable models + an HTTP WorkService + a session-timer repository), and a desktop ViewModel + Compose UI. There is no Android/iOS presentation for Work today.
Architecture
WorkViewModel holds a single WorkState exposed as a StateFlow; it talks to the shared WorkService for REST and to WorkSessionTimerRepository for live timer ticks over the timer WebSocket.
Job Types
| Type | Tracks | Planning | Session fields |
|---|---|---|---|
SOFTWARE_ENGINEERING | Flat projects (PROJECT tracks) | Tickets + checklist tasks per project; per-job people roster | Required activityCategory; optional ticketId |
CLIENT_BASED | Clients (CLIENT) → projects (PROJECT) | Project metadata + milestones | Optional billable |
STORE | Flat areas (PROJECT tracks) | Area metadata + milestones (milestoneKind, renewalDate); suppliers, tasks, inventory, shifts, P&L | Required store activityCategory |
Server-side validation enforces these shapes: tickets only on SOFTWARE_ENGINEERING jobs, project tracks must sit under a client track for CLIENT_BASED, milestones on PROJECT tracks for CLIENT_BASED/STORE, all store APIs require jobType == STORE, and store sessions require a store activity category. Violations throw WorkService.WorkValidationException → HTTP 400.
Data Models
Store P&L is not a table — WorkStorePnL is computed from transactions rows whose optional work_job_id FK points at the store job.
Enums
| Enum | Values | Used on |
|---|---|---|
WorkJobType | SOFTWARE_ENGINEERING, CLIENT_BASED, STORE | Job shape |
WorkTrackKind | CLIENT, PROJECT | Track hierarchy |
WorkTicketStatus | BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, BLOCKED, DONE | SE ticket lifecycle |
WorkTicketPriority | LOW, MEDIUM, HIGH, URGENT | SE ticket priority |
WorkProjectStatus | PROSPECT, PLANNING, ACTIVE, ON_HOLD, COMPLETED | Track projectStatus |
WorkActivityCategory | CODING, CODE_REVIEW, MEETING, PLANNING, LEARNING, DEBUGGING, OTHER | SE session category |
WorkStoreActivityCategory | PERMITS, LEASE, FIT_OUT, INVENTORY_SETUP, VENDOR, MARKETING, STAFFING, OTHER | STORE session category |
WorkMilestoneKind | PERMIT, VENDOR, MARKETING, LICENSE, OTHER | Milestone classification |
WorkStoreTaskStatus | TODO, IN_PROGRESS, DONE | Store ops task |
WorkStoreTaskRecurrence | DAILY, WEEKLY | Store task template |
WorkStoreStockEventType | RECEIVE, SALE, ADJUST, WASTE | Inventory stock event |
activity_category is persisted as a varchar on work_sessions (it spans both the SE and store enums), not a single DB enum.
API Endpoints
All routes are under /api/v1/work and require auth. The WORK feature flag must be enabled for the requesting client platform (via the X-Platform header). Calendar/date fields on the wire use dd/MM/yyyy (startDate, endDate, etaDate, dueDate, renewalDate, etc.).
Jobs, Tracks, People
| Method | Path | Description |
|---|---|---|
GET / POST | /work/jobs | List jobs / create job (body may include seedLaunchTemplate, default true for STORE) |
GET / PATCH / DELETE | /work/jobs/{jobId} | Get / update / delete a job |
GET / POST | /work/jobs/{jobId}/tracks | List / create tracks |
PATCH / DELETE | /work/tracks/{id} | Update / delete a track |
GET / POST | /work/jobs/{jobId}/people | List / create roster people (SE + STORE) |
PATCH / DELETE | /work/people/{id} | Update / delete a person |
Tickets (SOFTWARE_ENGINEERING)
| Method | Path | Description |
|---|---|---|
GET / POST | /work/jobs/{jobId}/tickets | List (optional trackId) / create tickets |
PATCH / DELETE | /work/tickets/{id} | Update / delete a ticket |
GET / POST | /work/tickets/{id}/tasks | List / create checklist tasks |
PATCH / DELETE | /work/ticket-tasks/{id} | Update / delete a checklist task |
POST | /work/jobs/{jobId}/tickets/import/preview | Multipart upload preview (?source=) |
POST | /work/jobs/{jobId}/tickets/import | Commit import |
POST | /work/jobs/{jobId}/tickets/import/undo | Undo a prior import by historyId |
GET | /work/jobs/{jobId}/tickets/import/history | Import history |
Milestones (CLIENT_BASED / STORE)
| Method | Path | Description |
|---|---|---|
GET / POST | /work/tracks/{id}/milestones | List / create milestones on a PROJECT track |
PATCH / DELETE | /work/milestones/{id} | Update / delete a milestone |
Sessions & Stats
| Method | Path | Description |
|---|---|---|
GET / POST | /work/jobs/{jobId}/sessions | List (?startDate=&endDate=) / create sessions |
GET / PATCH / DELETE | /work/sessions/{id} | Get / update / delete a session |
GET | /work/sessions/{id}/timer | Get-or-create the standalone timer for a session |
POST | /work/sessions/{id}/complete | Complete a session (?actualEnd= required, ?notes=) |
GET | /work/stats | Aggregated time stats (?jobId=&startDate=&endDate=) |
Store ops (STORE only — require jobType == STORE)
| Method | Path | Description |
|---|---|---|
GET / PUT | /work/jobs/{jobId}/store-profile | Get / upsert store profile |
GET | /work/jobs/{jobId}/store-pnl | Income/expense totals for linked transactions (?startDate=&endDate=) |
GET / POST | /work/jobs/{jobId}/store-suppliers | List / create suppliers |
PATCH / DELETE | /work/store-suppliers/{id} | Update / delete supplier |
GET / POST | /work/jobs/{jobId}/store-tasks | List / create ops tasks |
PATCH / DELETE | /work/store-tasks/{id} | Update / complete / delete task |
GET / POST | /work/jobs/{jobId}/store-task-templates | List / create templates |
POST | /work/jobs/{jobId}/store-task-templates/generate | Instantiate tasks from templates (returns created count) |
DELETE | /work/store-task-templates/{id} | Delete template |
GET / POST | /work/jobs/{jobId}/store-products | List / create products |
PATCH / DELETE | /work/store-products/{id} | Update / delete product |
GET / POST | /work/store-products/{id}/stock-events | Stock history / add event |
GET / POST | /work/jobs/{jobId}/store-shifts | List / create shifts |
PATCH / DELETE | /work/store-shifts/{id} | End shift / delete shift |
State Machine
WorkTicketStatus is a free enum on the wire — the server does not enforce transition order, so any status may be set via PATCH /work/tickets/{id}. The diagram shows the intended workflow; the columns of the SE ticket board map directly to these values.
Client Behavior
Each session gets a backing standalone timer created lazily by GET /work/sessions/{id}/timer. WorkSessionTimerRepository maps timer IDs to session IDs and forwards WebSocket TimerUpdate events as sessionTimerUpdates, so the desktop UI shows live elapsed time without polling.
Key Files
| File | Purpose |
|---|---|
shared/.../models/WorkJob.kt | WorkJob + WorkJobType enum |
shared/.../models/WorkTrack.kt | WorkTrack (project metadata) + WorkTrackKind |
shared/.../models/WorkTicket.kt | Ticket, ticket task, draft/bulk-edit + status/priority/project-status enums |
shared/.../models/WorkSession.kt | WorkSession, WorkActivityCategory, WorkStats |
shared/.../models/WorkMilestone.kt | WorkMilestone (kind, dueDate, renewalDate) |
shared/.../models/WorkStore.kt | All store models + WorkStoreActivityCategory, WorkMilestoneKind |
shared/.../models/WorkPerson.kt | Per-job roster person |
shared/.../services/work/WorkService.kt | Shared Ktor HTTP client for all Work endpoints |
shared/.../services/work/WorkSessionTimerRepository.kt | Live session timer via WebSocket |
shared/.../work/ui/WorkTimeLogScreen.kt | Shared time-log composable |
composeApp/.../ui/viewmodels/WorkViewModel.kt | Desktop WorkState + StateFlow, all Work actions |
composeApp/.../ui/screens/WorkScreen.kt | Desktop Work hub entry screen |
composeApp/.../ui/screens/work/* | Hub panels: WorkNavigation, WorkStorePanels, WorkTicketsPanel, WorkLogPanel, WorkTeamPanel, WorkOverviewContent, etc. |
server/.../routing/WorkRouting.kt | All REST endpoints + per-type validation |
server/.../service/WorkService.kt | Core service (jobs, tracks, tickets, sessions, milestones, people) + WorkValidationException |
server/.../service/WorkStoreService.kt | Store ops + seedLaunchTemplate |
server/.../service/work/import/WorkTicketImportService.kt | Spreadsheet ticket import |
server/.../database/entities/WorkStoreEntities.kt | Store tables + store enums |
server/.../database/entities/Work*.kt | WorkJob, WorkTrack, WorkTicket, WorkSession, WorkPerson entities |
Migrations: V10__work_tracking.sql, V12__work_tickets_and_project_management.sql, V13__work_store_phase1.sql, V14__work_store_operations.sql, V15__work_store_inventory_and_finance.sql.
Testing
- Server (import):
server/.../service/work/import/WorkTicketImportServiceTest.ktcovers spreadsheet preview/commit parsing. - Validation: assert
WorkValidationException→ 400 on cross-type misuse — tickets on a non-SE job, store APIs on a non-STORE job, store sessions without a storeactivityCategory, project tracks not under a client forCLIENT_BASED. - Store seed: creating a STORE job with
seedLaunchTemplatetrue should create the Permits/Fit-out/Inventory areas, permit milestones, and an empty profile; with it false, none. - Inventory: verify
onHandQuantitymath acrossRECEIVE(+),SALE/WASTE(−), andADJUST(±) stock events. - Sessions/stats:
completewithactualEndpopulatesdurationMinutes;/work/statsaggregates by track and category and splits billable vs non-billable minutes.
Troubleshooting
Work sidebar entry / endpoints not appearing
- The WORK feature flag must be enabled for the client platform. Locally:
UPDATE feature_flags SET enabled = true WHERE feature_key = 'WORK' AND platform = 'DESKTOP'; - Ensure OTER is also enabled for that platform, and that the request sends the correct
X-Platformheader.
“Tickets are only available for software engineering jobs” / store API 400s
- These are
WorkValidationExceptionresponses. Tickets requirejobType == SOFTWARE_ENGINEERING; allstore-*routes requirejobType == STORE. Confirm the job’s type before calling.
Milestone creation rejected
- Milestones can only be created on
PROJECTtracks ofCLIENT_BASEDorSTOREjobs. Check the target track’strackKindand the job type.
Store P&L shows zero
WorkStorePnLis computed fromtransactionswhosework_job_idequals the store job. If no finance transactions are tagged to the job, totals are zero. Confirm Finance entries link viawork_job_idand fall within thestartDate/endDaterange.
Session timer not ticking
- The timer is created lazily on
GET /work/sessions/{id}/timer. If the timer WebSocket is disconnected,WorkSessionTimerRepositoryreceives noTimerUpdateevents and the UI elapsed time stays frozen — verify the WebSocket connection and that the user is authenticated (TokenStorage).
Dates parsing incorrectly
- All calendar fields use
dd/MM/yyyy. Sending ISO (yyyy-MM-dd) will fail server-side parsing.
Related Docs
- Finance — transactions and the
work_job_idlink that powers store P&L - Timers — standalone timer model and WebSocket updates reused by work sessions
- Habits — reference for the standard feature-module layout Work intentionally diverges from
- Domain & models — shared domain model reference
- UI & UX — desktop hub navigation patterns
- Testing & QA — testing strategy