Program templates — code map
API (NestJS)
Base path: /organizations/:orgId/program-templates
| Route | Method | Service method |
|---|---|---|
POST / | create | Validates mode-specific shape, inserts header |
GET / | list | Filters: deliveryMode, programId, q, tags[] |
GET /:id | getById | Returns header + cells |
PATCH /:id | update | Header-only; mode/duration changes |
DELETE /:id | remove | Soft delete |
POST /:id/duplicate | duplicate | Header + cells deep copy with ” (copy)” suffix |
POST /:id/workouts | upsertCells | Replace full cell grid |
POST /:id/preview | preview | Dry-run apply, returns the would-be result |
POST /:id/apply | apply | Materialize into target |
POST /from-history | fromHistory | Save-as-template from existing materialized rows |
Controller: apps/api/src/program-templates/program-templates.controller.ts
Service: apps/api/src/program-templates/program-templates.service.ts (~1119 lines)
Module: apps/api/src/program-templates/program-templates.module.ts
DTOs (apps/api/src/program-templates/dto/)
create-program-template.dto.ts—CreateProgramTemplateDtoupdate-program-template.dto.ts—UpdateProgramTemplateDtoupsert-cells.dto.ts—UpsertProgramTemplateCellsDtoapply-template.dto.ts—ApplyProgramTemplateDto,PreviewProgramTemplateDto,ConflictModeschedule-slot.dto.ts—ScheduleSlotDto,ScheduleOccurrenceDtofrom-history.dto.ts—FromHistoryDto
Key service methods
requireCoach(orgId, clerkId)— role gate (owner/admin/coach).loadTemplateWithCells(orgId, id)— single fetch with cells, used by most read paths.assertModeMatches(template, dtoMode)— apply / preview safety check.applyToCoaching(template, cells, dto, orgId, authorUserId)— bulk insertsworkout_assignments. Conflict modes:skip(default),abort,overwrite.applyToSchedule(template, slots, dto, orgId, clerkId)— expands the slots, drops or replaces anything clashing with the live schedule, and writesclass_sessionsin one transaction. Same conflict modes as coaching;overwriteskips sessions that have bookings.buildPlannedSessions(template, slots, dto, orgId)— the expansion, shared by preview and apply so both see the same set.loadExistingSessions(classTypeIds, planned)— live sessions in the applied window plus their booking counts (two grouped queries, no correlated subquery).normalizeOccurrences(occurrences)— dedupes and (day, time)-sorts a slot on every write.buildCoachingPreview,buildSchedulePreview— mode-specific dry-run.readHistoryCells(orgId, dto)— normalize existing materialized rows back to cells (used byfrom-history’scoachingbranch; theschedulebranch derives slots inline infromHistory, per class type, viagetOrgTimezone+formatInTimeZone).assertSlotsInProgram(slots, orgId, programId)— every slot’s class type must belong toprogramId(not just the org); falls back to an org-only check whenprogramIdisnull(pre-backfill legacy row). Replaces the old org-onlyassertSlotsInOrg.assertProgramInOrg,assertLocationInOrg,assertMembershipInOrg,assertLibraryWorkoutInOrg— referential integrity helpers.
Pure modules (no DB, unit-tested)
session-conflicts.ts—findSessionConflicts(planned, existing)andgroupConflicts(...). The single definition of a schedule clash, shared by preview, apply and copy-week: same class type overlapping, and (flagspaceson) any overlap in the sameexclusivespace (kind: 'space'). Coach double-booking is not covered.addDays(date, n),toUtc(date, time, tz)— date math helpers (usesdate-fns-tzfromZonedTime).
Web (Next.js)
| Route | File |
|---|---|
| Templates list (per program) | apps/web/src/app/[lang]/(protected)/dashboard/programs/[id]/templates/page.tsx |
| Template detail | apps/web/src/app/[lang]/(protected)/dashboard/programs/[id]/templates/[templateId]/page.tsx |
| Cell editor | apps/web/src/app/[lang]/(protected)/dashboard/programs/[id]/templates/[templateId]/cells/[week]/[day]/edit/page.tsx |
Components
apps/web/src/components/shared/program-grid/— generic week × day grid used by templates and by workout-assignments coaching view (cell-shell.tsx,cell-menu.tsx,empty-cell-menu.tsx,note-prompt-dialog.tsx).apps/web/src/components/overview/schedule/save-week-as-template-dialog.tsx— “Save this week as a template” from the schedule’s setup menu (setup-timetable-menu.tsx). Postsfrom-historywithsource:'schedule'and the screen’sprogramId; no class-type picker — shows the program’s active class types as read-only chips instead. Driver: co-located.driver.tsx.- Apply / preview dialogs: search
components/overview/programming/(TODO: verify exact filenames — appears mixed across overview/programming and dashboard). apps/web/src/components/templates/program-templates-tab.tsx— per-program Templates tab. Knows the page’sprogramIdand threads it intonew-template-form.tsx,edit-template-dialog.tsx, and (through those)schedule-slots-editor.tsx, whose class-type picker takes a requiredprogramIdprop and only ever lists that program’s class types. Schedule rows are scoped bytemplate.programId; the tab falls back to scanningslots[].classTypeIdagainst the program’s own class types only for legacy rows withprogramId=null.apply-template-button.tsx(the schedule calendar’s “Apply template” entry point) uses the sameprogramId-first, slot-scan-fallback rule.
DB
libs/db/src/lib/schema/program-templates.ts:
program_templates— header rows.program_template_slots— schedule recurrence: one row per class type,occurrences[]as(day, time)pairs.program_template_workouts— cell rows (week × day × slot).
See data-model.md.
Shared
libs/shared/src/lib/schemas/program-template.schema.ts — Zod response shapes (TODO: verify exact exports). Delivery mode enum lives in workout.schema.ts as deliveryModeValues.
Tests
program-templates.program-scope.unit.spec.ts—create/findAllprogram scoping: a foreign-program slot is rejected, a missing/foreign-orgprogramIdis rejected,findAll’sprogramIdfilter.program-templates.from-history.unit.spec.ts—fromHistory’s schedule branch:programIdscope yields one slot per class type with the occurrences that ran (and skips a class type with none),sourceIdstill scopes to one class type, occurrences are read in the org timezone (not UTC), andprogramId/sourceIdare mutually required/exclusive.program-templates.occurrences.unit.spec.ts,program-templates.date.unit.spec.ts— pure helpers (normalizeOccurrences, date math).schedule-apply-conflicts.int.spec.ts— apply/preview against real Postgres (conflict detection, overwrite transaction).- The bulk apply logic is also covered indirectly via e2e flows in
apps/web/e2e/specs/coaching-grid.spec.tsandschedule-setup.spec.ts.