Skip to Content
FeaturesWorkFeature guide

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 an activityCategory and may optionally link to a ticketId.
  • CLIENT_BASEDclients → projects track hierarchy. Project tracks carry metadata (status, ETA, budget, hourly rate, contact, notes) plus milestones. Sessions may be flagged billable.
  • STORE — flat areas (all PROJECT tracks), a launch profile, milestones (with a milestoneKind and renewalDate), suppliers, operational tasks (+ recurring templates), inventory (products + stock events), staff shifts, and P&L (finance transactions linked by work_job_id). Sessions require a store activityCategory.

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

TypeTracksPlanningSession fields
SOFTWARE_ENGINEERINGFlat projects (PROJECT tracks)Tickets + checklist tasks per project; per-job people rosterRequired activityCategory; optional ticketId
CLIENT_BASEDClients (CLIENT) → projects (PROJECT)Project metadata + milestonesOptional billable
STOREFlat areas (PROJECT tracks)Area metadata + milestones (milestoneKind, renewalDate); suppliers, tasks, inventory, shifts, P&LRequired 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

EnumValuesUsed on
WorkJobTypeSOFTWARE_ENGINEERING, CLIENT_BASED, STOREJob shape
WorkTrackKindCLIENT, PROJECTTrack hierarchy
WorkTicketStatusBACKLOG, TODO, IN_PROGRESS, IN_REVIEW, BLOCKED, DONESE ticket lifecycle
WorkTicketPriorityLOW, MEDIUM, HIGH, URGENTSE ticket priority
WorkProjectStatusPROSPECT, PLANNING, ACTIVE, ON_HOLD, COMPLETEDTrack projectStatus
WorkActivityCategoryCODING, CODE_REVIEW, MEETING, PLANNING, LEARNING, DEBUGGING, OTHERSE session category
WorkStoreActivityCategoryPERMITS, LEASE, FIT_OUT, INVENTORY_SETUP, VENDOR, MARKETING, STAFFING, OTHERSTORE session category
WorkMilestoneKindPERMIT, VENDOR, MARKETING, LICENSE, OTHERMilestone classification
WorkStoreTaskStatusTODO, IN_PROGRESS, DONEStore ops task
WorkStoreTaskRecurrenceDAILY, WEEKLYStore task template
WorkStoreStockEventTypeRECEIVE, SALE, ADJUST, WASTEInventory 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

MethodPathDescription
GET / POST/work/jobsList 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}/tracksList / create tracks
PATCH / DELETE/work/tracks/{id}Update / delete a track
GET / POST/work/jobs/{jobId}/peopleList / create roster people (SE + STORE)
PATCH / DELETE/work/people/{id}Update / delete a person

Tickets (SOFTWARE_ENGINEERING)

MethodPathDescription
GET / POST/work/jobs/{jobId}/ticketsList (optional trackId) / create tickets
PATCH / DELETE/work/tickets/{id}Update / delete a ticket
GET / POST/work/tickets/{id}/tasksList / create checklist tasks
PATCH / DELETE/work/ticket-tasks/{id}Update / delete a checklist task
POST/work/jobs/{jobId}/tickets/import/previewMultipart upload preview (?source=)
POST/work/jobs/{jobId}/tickets/importCommit import
POST/work/jobs/{jobId}/tickets/import/undoUndo a prior import by historyId
GET/work/jobs/{jobId}/tickets/import/historyImport history

Milestones (CLIENT_BASED / STORE)

MethodPathDescription
GET / POST/work/tracks/{id}/milestonesList / create milestones on a PROJECT track
PATCH / DELETE/work/milestones/{id}Update / delete a milestone

Sessions & Stats

MethodPathDescription
GET / POST/work/jobs/{jobId}/sessionsList (?startDate=&endDate=) / create sessions
GET / PATCH / DELETE/work/sessions/{id}Get / update / delete a session
GET/work/sessions/{id}/timerGet-or-create the standalone timer for a session
POST/work/sessions/{id}/completeComplete a session (?actualEnd= required, ?notes=)
GET/work/statsAggregated time stats (?jobId=&startDate=&endDate=)

Store ops (STORE only — require jobType == STORE)

MethodPathDescription
GET / PUT/work/jobs/{jobId}/store-profileGet / upsert store profile
GET/work/jobs/{jobId}/store-pnlIncome/expense totals for linked transactions (?startDate=&endDate=)
GET / POST/work/jobs/{jobId}/store-suppliersList / create suppliers
PATCH / DELETE/work/store-suppliers/{id}Update / delete supplier
GET / POST/work/jobs/{jobId}/store-tasksList / create ops tasks
PATCH / DELETE/work/store-tasks/{id}Update / complete / delete task
GET / POST/work/jobs/{jobId}/store-task-templatesList / create templates
POST/work/jobs/{jobId}/store-task-templates/generateInstantiate tasks from templates (returns created count)
DELETE/work/store-task-templates/{id}Delete template
GET / POST/work/jobs/{jobId}/store-productsList / create products
PATCH / DELETE/work/store-products/{id}Update / delete product
GET / POST/work/store-products/{id}/stock-eventsStock history / add event
GET / POST/work/jobs/{jobId}/store-shiftsList / 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

FilePurpose
shared/.../models/WorkJob.ktWorkJob + WorkJobType enum
shared/.../models/WorkTrack.ktWorkTrack (project metadata) + WorkTrackKind
shared/.../models/WorkTicket.ktTicket, ticket task, draft/bulk-edit + status/priority/project-status enums
shared/.../models/WorkSession.ktWorkSession, WorkActivityCategory, WorkStats
shared/.../models/WorkMilestone.ktWorkMilestone (kind, dueDate, renewalDate)
shared/.../models/WorkStore.ktAll store models + WorkStoreActivityCategory, WorkMilestoneKind
shared/.../models/WorkPerson.ktPer-job roster person
shared/.../services/work/WorkService.ktShared Ktor HTTP client for all Work endpoints
shared/.../services/work/WorkSessionTimerRepository.ktLive session timer via WebSocket
shared/.../work/ui/WorkTimeLogScreen.ktShared time-log composable
composeApp/.../ui/viewmodels/WorkViewModel.ktDesktop WorkState + StateFlow, all Work actions
composeApp/.../ui/screens/WorkScreen.ktDesktop Work hub entry screen
composeApp/.../ui/screens/work/*Hub panels: WorkNavigation, WorkStorePanels, WorkTicketsPanel, WorkLogPanel, WorkTeamPanel, WorkOverviewContent, etc.
server/.../routing/WorkRouting.ktAll REST endpoints + per-type validation
server/.../service/WorkService.ktCore service (jobs, tracks, tickets, sessions, milestones, people) + WorkValidationException
server/.../service/WorkStoreService.ktStore ops + seedLaunchTemplate
server/.../service/work/import/WorkTicketImportService.ktSpreadsheet ticket import
server/.../database/entities/WorkStoreEntities.ktStore tables + store enums
server/.../database/entities/Work*.ktWorkJob, 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.kt covers 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 store activityCategory, project tracks not under a client for CLIENT_BASED.
  • Store seed: creating a STORE job with seedLaunchTemplate true should create the Permits/Fit-out/Inventory areas, permit milestones, and an empty profile; with it false, none.
  • Inventory: verify onHandQuantity math across RECEIVE (+), SALE/WASTE (−), and ADJUST (±) stock events.
  • Sessions/stats: complete with actualEnd populates durationMinutes; /work/stats aggregates 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-Platform header.

“Tickets are only available for software engineering jobs” / store API 400s

  • These are WorkValidationException responses. Tickets require jobType == SOFTWARE_ENGINEERING; all store-* routes require jobType == STORE. Confirm the job’s type before calling.

Milestone creation rejected

  • Milestones can only be created on PROJECT tracks of CLIENT_BASED or STORE jobs. Check the target track’s trackKind and the job type.

Store P&L shows zero

  • WorkStorePnL is computed from transactions whose work_job_id equals the store job. If no finance transactions are tagged to the job, totals are zero. Confirm Finance entries link via work_job_id and fall within the startDate/endDate range.

Session timer not ticking

  • The timer is created lazily on GET /work/sessions/{id}/timer. If the timer WebSocket is disconnected, WorkSessionTimerRepository receives no TimerUpdate events 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.
  • Finance — transactions and the work_job_id link 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