Add Phase 8: lesson plan templates with deep-copy instantiation

- New tables: lesson_plan_template, lesson_plan_template_section, lesson_plan_template_item
- skill_level enum: beginner, intermediate, advanced, all_levels
- Templates are reusable curriculum definitions independent of any member/enrollment
- POST /lesson-plan-templates/:id/create-plan deep-copies the template into a member plan
- Instantiation uses template name as default plan title, accepts custom title override
- Instantiation deactivates any existing active plan on the enrollment (one-active rule)
- Plan items are independent copies — renaming the template does not affect existing plans
- 11 new integration tests
This commit is contained in:
Ryan Moon
2026-03-30 10:37:30 -05:00
parent 2cc8f24535
commit 7680a73d88
8 changed files with 623 additions and 2 deletions

View File

@@ -200,6 +200,51 @@ export const LessonPlanItemUpdateSchema = z.object({
})
export type LessonPlanItemUpdateInput = z.infer<typeof LessonPlanItemUpdateSchema>
// --- Lesson Plan Template schemas ---
export const SkillLevel = z.enum(['beginner', 'intermediate', 'advanced', 'all_levels'])
export type SkillLevel = z.infer<typeof SkillLevel>
const TemplateItemInput = z.object({
title: z.string().min(1).max(255),
description: opt(z.string()),
gradingScaleId: opt(z.string().uuid()),
targetGradeValue: opt(z.string().max(50)),
sortOrder: z.coerce.number().int(),
})
const TemplateSectionInput = z.object({
title: z.string().min(1).max(255),
description: opt(z.string()),
sortOrder: z.coerce.number().int(),
items: z.array(TemplateItemInput).default([]),
})
export const LessonPlanTemplateCreateSchema = z.object({
name: z.string().min(1).max(255),
description: opt(z.string()),
instrument: opt(z.string().max(100)),
skillLevel: SkillLevel.default('all_levels'),
sections: z.array(TemplateSectionInput).default([]),
})
export type LessonPlanTemplateCreateInput = z.infer<typeof LessonPlanTemplateCreateSchema>
export const LessonPlanTemplateUpdateSchema = z.object({
name: z.string().min(1).max(255).optional(),
description: opt(z.string()),
instrument: opt(z.string().max(100)),
skillLevel: SkillLevel.optional(),
isActive: z.boolean().optional(),
})
export type LessonPlanTemplateUpdateInput = z.infer<typeof LessonPlanTemplateUpdateSchema>
export const TemplateInstantiateSchema = z.object({
memberId: z.string().uuid(),
enrollmentId: z.string().uuid(),
title: opt(z.string().min(1).max(255)),
})
export type TemplateInstantiateInput = z.infer<typeof TemplateInstantiateSchema>
// --- Grade History schemas ---
export const GradeCreateSchema = z.object({