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

@@ -218,6 +218,46 @@ export const lessonPlanItems = pgTable('lesson_plan_item', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
// --- Lesson Plan Templates ---
export const skillLevelEnum = pgEnum('skill_level', ['beginner', 'intermediate', 'advanced', 'all_levels'])
export const lessonPlanTemplates = pgTable('lesson_plan_template', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
instrument: varchar('instrument', { length: 100 }),
skillLevel: skillLevelEnum('skill_level').notNull().default('all_levels'),
createdBy: uuid('created_by').references(() => users.id),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export const lessonPlanTemplateSections = pgTable('lesson_plan_template_section', {
id: uuid('id').primaryKey().defaultRandom(),
templateId: uuid('template_id')
.notNull()
.references(() => lessonPlanTemplates.id),
title: varchar('title', { length: 255 }).notNull(),
description: text('description'),
sortOrder: integer('sort_order').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const lessonPlanTemplateItems = pgTable('lesson_plan_template_item', {
id: uuid('id').primaryKey().defaultRandom(),
sectionId: uuid('section_id')
.notNull()
.references(() => lessonPlanTemplateSections.id),
title: varchar('title', { length: 255 }).notNull(),
description: text('description'),
gradingScaleId: uuid('grading_scale_id').references(() => gradingScales.id),
targetGradeValue: varchar('target_grade_value', { length: 50 }),
sortOrder: integer('sort_order').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const lessonPlanItemGradeHistory = pgTable('lesson_plan_item_grade_history', {
id: uuid('id').primaryKey().defaultRandom(),
lessonPlanItemId: uuid('lesson_plan_item_id')
@@ -268,6 +308,12 @@ export type LessonPlanSection = typeof lessonPlanSections.$inferSelect
export type LessonPlanSectionInsert = typeof lessonPlanSections.$inferInsert
export type LessonPlanItem = typeof lessonPlanItems.$inferSelect
export type LessonPlanItemInsert = typeof lessonPlanItems.$inferInsert
export type LessonPlanTemplate = typeof lessonPlanTemplates.$inferSelect
export type LessonPlanTemplateInsert = typeof lessonPlanTemplates.$inferInsert
export type LessonPlanTemplateSection = typeof lessonPlanTemplateSections.$inferSelect
export type LessonPlanTemplateSectionInsert = typeof lessonPlanTemplateSections.$inferInsert
export type LessonPlanTemplateItem = typeof lessonPlanTemplateItems.$inferSelect
export type LessonPlanTemplateItemInsert = typeof lessonPlanTemplateItems.$inferInsert
export type LessonPlanItemGradeHistory = typeof lessonPlanItemGradeHistory.$inferSelect
export type LessonPlanItemGradeHistoryInsert = typeof lessonPlanItemGradeHistory.$inferInsert
export type LessonSessionPlanItem = typeof lessonSessionPlanItems.$inferSelect