Workout assignments
What this is
A workout assignment is a single dated cell on a user’s calendar: “Mia is assigned Fran on 2026-06-03, slot 0, kind=workout”. Assignments are the central join between workouts, users, programs, and dates. The domain owns three publish surfaces (personal / feed / schedule), the lazy-fork lifecycle (FIT-152), per-cell kinds (workout/rest/note, FIT-159), bulk preview/delete/publish over coaching programs, morning-of publish drip, push notifications on assign, the coach overview dashboard (KPIs + per-member sparkline), and the auto-completion side-effect when a result is logged. The assignments service exposes forkSnapshotIfNeeded, which is called by both workouts edit flows and workout-results result creation.
Who uses it
| Persona | Role |
|---|---|
| Coach | Assigns workouts via cell click, drag, or bulk apply; runs bulk operations; views compliance overview. |
| Owner | Same as coach + monitors getCoachOverview. |
| Member | Reads own assignments (my-week, today, :id); completes / skips. |
| Spotter agent | Creates assignments via the same controllers (typically assignPersonal). |
Persona impact
- Coach sees one calendar where program-driven assignments and ad-hoc personal assignments coexist. Bulk publish flips drafts to published with one click; bulk delete reasonably scopes to enrolled members.
- Member sees a stable per-day prescription that survives any post-assignment edit the coach makes to the library workout — the snapshot pointer freezes the prescription as of completion/edit time.
- Owner has a single roster view (
coach-overview) with traffic-light compliance and 7-day sparklines.
Capabilities
- Three modes of creation (FIT-74 / PRD §7.4):
POST /assignments/personal— assign a workout to N athletes for a date (also handleskind='rest'/'note').POST /assignments/feed— publish to a feed-mode track viaworkout_feed_posts.POST /assignments/schedule— add to class-schedule slots (createsclass_sessions+class_session_workouts).
- Legacy single-athlete create (
POST /assignments) for backwards compatibility. - Read endpoints —
GET /assignments?weekStart&userId|programId,GET /assignments/my-week,GET /assignments/today,GET /assignments/:id(week-independent single fetch with full snapshot payload). - Mutation endpoints —
PATCH /:id,DELETE /:id(soft),POST /:id/complete,POST /:id/skip,PATCH /publish(bulk explicit set). - Bulk filter-driven operations:
POST /assignments/bulk-preview— count + 10 samples for a filter.POST /assignments/bulk-delete— soft-delete every match, audit-logged under onebatch_id.POST /assignments/bulk-publish— publish every draft match.
- Coach overview —
GET /assignments/coach-overview— org-wide KPIs + roster with 7-day sparkline + traffic-light status (on_track/at_risk/overdue/quiet). - Lazy-fork —
forkSnapshotIfNeeded(assignmentId)is the single deep-copy entry point; called from workouts edit-from-cell and from workout-results result completion. UsesSELECT … FOR UPDATEfor concurrency. - Morning-of drip publish —
dto.drip === 'morning_of'onassignPersonalwritespublished=false+publish_at = date @ 05:00 UTC. A scheduled job flipspublished=trueat that moment (TODO: verify the publisher job lives elsewhere — search forpublish_at). - Auto-completion — when a result is created against the assignment id, the assignment auto-transitions
assigned → completedwithcompleted_at = now().
Relationship to other features
- workouts — assignments hold both
workout_id(library) andsnapshot_workout_id(per-athlete frozen copy). Workouts edit endpoints redirect writes viaforkSnapshotIfNeeded. - workout-results — result completion forks the snapshot and auto-completes the assignment.
- program-templates —
applyToCoachingmaterializes template cells as assignments in pointer state. - daily-programming — orthogonal: daily programming is class-type-based, assignments are user-based. A schedule-mode program populates daily_programming; coaching-mode uses assignments.
- scheduling/bookings —
assignSchedulewritesclass_sessionsrows that feed scheduling-bookings if it exists. TODO: verify cross-feature.
Current status
Shipped. Lazy-fork (FIT-152), per-cell kind (FIT-159), bulk operations, coach-overview dashboard, and morning-of drip are all live. Bulk preview/delete/publish observed at apps/api/src/workout-assignments/workout-assignments.service.ts and tested in workout-assignments.bulk.unit.spec.ts.
Known gaps / open Linear issues
- FIT-152 — lazy-fork. Shipped.
- FIT-159 — per-cell
kind. Shipped. - FIT-201 — bulk-filter scoping bug surfaced in prod. Fixed: bulk preview now intersects with
programEnrollmentsinstead ofworkoutAssignments.programIdso ad-hoc personal rows visible on the week grid are also covered (seeworkout-assignments.service.ts:1165). - Template-apply does not push notify; ad-hoc
assignPersonaldoes. TODO: verify whether this is intentional or a known gap. - Morning-of publisher job — the assignment carries
publish_at, but the worker that flipspublished=trueat that timestamp must exist elsewhere. TODO: verify the publisher service / cron. - Skip endpoint (
POST /:id/skip) — flips status toskipped. No undo endpoint surfaced. assignFeedwrites only viaworkout_feed_posts; it does not also create per-memberworkout_assignmentsrows — feed delivery is broadcast.