Add lessons Phase 6: lesson plans with curriculum tracking
Structured lesson plans with nested sections and items per enrollment. Deep create in one request, one-active-per-enrollment constraint, auto-set startedDate/masteredDate on status transitions, progress % calculation (skipped items excluded). 8 new tests (84 total).
This commit is contained in:
43
packages/backend/src/db/migrations/0033_lesson_plans.sql
Normal file
43
packages/backend/src/db/migrations/0033_lesson_plans.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Phase 6: Lesson plans — structured curriculum per enrollment
|
||||
|
||||
CREATE TYPE "lesson_plan_item_status" AS ENUM ('not_started', 'in_progress', 'mastered', 'skipped');
|
||||
|
||||
CREATE TABLE "member_lesson_plan" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"member_id" uuid NOT NULL REFERENCES "member"("id"),
|
||||
"enrollment_id" uuid NOT NULL REFERENCES "enrollment"("id"),
|
||||
"created_by" uuid REFERENCES "user"("id"),
|
||||
"title" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"is_active" boolean NOT NULL DEFAULT true,
|
||||
"started_date" date,
|
||||
"completed_date" date,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE "lesson_plan_section" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"lesson_plan_id" uuid NOT NULL REFERENCES "member_lesson_plan"("id"),
|
||||
"title" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"sort_order" integer NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE "lesson_plan_item" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"section_id" uuid NOT NULL REFERENCES "lesson_plan_section"("id"),
|
||||
"title" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"status" lesson_plan_item_status NOT NULL DEFAULT 'not_started',
|
||||
"grading_scale_id" uuid REFERENCES "grading_scale"("id"),
|
||||
"current_grade_value" varchar(50),
|
||||
"target_grade_value" varchar(50),
|
||||
"started_date" date,
|
||||
"mastered_date" date,
|
||||
"notes" text,
|
||||
"sort_order" integer NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -232,6 +232,13 @@
|
||||
"when": 1774920000000,
|
||||
"tag": "0032_grading_scales",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1774930000000,
|
||||
"tag": "0033_lesson_plans",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -143,6 +143,61 @@ export const gradingScaleLevels = pgTable('grading_scale_level', {
|
||||
sortOrder: integer('sort_order').notNull(),
|
||||
})
|
||||
|
||||
export const lessonPlanItemStatusEnum = pgEnum('lesson_plan_item_status', [
|
||||
'not_started',
|
||||
'in_progress',
|
||||
'mastered',
|
||||
'skipped',
|
||||
])
|
||||
|
||||
export const memberLessonPlans = pgTable('member_lesson_plan', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
memberId: uuid('member_id')
|
||||
.notNull()
|
||||
.references(() => members.id),
|
||||
enrollmentId: uuid('enrollment_id')
|
||||
.notNull()
|
||||
.references(() => enrollments.id),
|
||||
createdBy: uuid('created_by').references(() => users.id),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
startedDate: date('started_date'),
|
||||
completedDate: date('completed_date'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const lessonPlanSections = pgTable('lesson_plan_section', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
lessonPlanId: uuid('lesson_plan_id')
|
||||
.notNull()
|
||||
.references(() => memberLessonPlans.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 lessonPlanItems = pgTable('lesson_plan_item', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
sectionId: uuid('section_id')
|
||||
.notNull()
|
||||
.references(() => lessonPlanSections.id),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
status: lessonPlanItemStatusEnum('status').notNull().default('not_started'),
|
||||
gradingScaleId: uuid('grading_scale_id').references(() => gradingScales.id),
|
||||
currentGradeValue: varchar('current_grade_value', { length: 50 }),
|
||||
targetGradeValue: varchar('target_grade_value', { length: 50 }),
|
||||
startedDate: date('started_date'),
|
||||
masteredDate: date('mastered_date'),
|
||||
notes: text('notes'),
|
||||
sortOrder: integer('sort_order').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
// --- Type exports ---
|
||||
|
||||
export type Instructor = typeof instructors.$inferSelect
|
||||
@@ -159,3 +214,9 @@ export type GradingScale = typeof gradingScales.$inferSelect
|
||||
export type GradingScaleInsert = typeof gradingScales.$inferInsert
|
||||
export type GradingScaleLevel = typeof gradingScaleLevels.$inferSelect
|
||||
export type GradingScaleLevelInsert = typeof gradingScaleLevels.$inferInsert
|
||||
export type MemberLessonPlan = typeof memberLessonPlans.$inferSelect
|
||||
export type MemberLessonPlanInsert = typeof memberLessonPlans.$inferInsert
|
||||
export type LessonPlanSection = typeof lessonPlanSections.$inferSelect
|
||||
export type LessonPlanSectionInsert = typeof lessonPlanSections.$inferInsert
|
||||
export type LessonPlanItem = typeof lessonPlanItems.$inferSelect
|
||||
export type LessonPlanItemInsert = typeof lessonPlanItems.$inferInsert
|
||||
|
||||
Reference in New Issue
Block a user