Add lessons Phase 4: lesson sessions with hybrid calendar generation
Individual lesson occurrences generated from schedule slot patterns. Idempotent session generation with configurable rolling window. Post-lesson notes workflow with auto-set notesCompletedAt. Status tracking (scheduled/attended/missed/makeup/cancelled) and date/time filtering. 13 new tests (64 total lessons tests).
This commit is contained in:
22
packages/backend/src/db/migrations/0031_lesson_sessions.sql
Normal file
22
packages/backend/src/db/migrations/0031_lesson_sessions.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Phase 4: Lesson sessions — individual lesson occurrences
|
||||
|
||||
CREATE TYPE "lesson_session_status" AS ENUM ('scheduled', 'attended', 'missed', 'makeup', 'cancelled');
|
||||
|
||||
CREATE TABLE "lesson_session" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"enrollment_id" uuid NOT NULL REFERENCES "enrollment"("id"),
|
||||
"scheduled_date" date NOT NULL,
|
||||
"scheduled_time" time NOT NULL,
|
||||
"actual_start_time" time,
|
||||
"actual_end_time" time,
|
||||
"status" lesson_session_status NOT NULL DEFAULT 'scheduled',
|
||||
"instructor_notes" text,
|
||||
"member_notes" text,
|
||||
"homework_assigned" text,
|
||||
"next_lesson_goals" text,
|
||||
"topics_covered" text[],
|
||||
"makeup_for_session_id" uuid,
|
||||
"notes_completed_at" timestamptz,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -218,6 +218,13 @@
|
||||
"when": 1774900000000,
|
||||
"tag": "0030_enrollments",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "7",
|
||||
"when": 1774910000000,
|
||||
"tag": "0031_lesson_sessions",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -91,6 +91,35 @@ export const enrollments = pgTable('enrollment', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const lessonSessionStatusEnum = pgEnum('lesson_session_status', [
|
||||
'scheduled',
|
||||
'attended',
|
||||
'missed',
|
||||
'makeup',
|
||||
'cancelled',
|
||||
])
|
||||
|
||||
export const lessonSessions = pgTable('lesson_session', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
enrollmentId: uuid('enrollment_id')
|
||||
.notNull()
|
||||
.references(() => enrollments.id),
|
||||
scheduledDate: date('scheduled_date').notNull(),
|
||||
scheduledTime: time('scheduled_time').notNull(),
|
||||
actualStartTime: time('actual_start_time'),
|
||||
actualEndTime: time('actual_end_time'),
|
||||
status: lessonSessionStatusEnum('status').notNull().default('scheduled'),
|
||||
instructorNotes: text('instructor_notes'),
|
||||
memberNotes: text('member_notes'),
|
||||
homeworkAssigned: text('homework_assigned'),
|
||||
nextLessonGoals: text('next_lesson_goals'),
|
||||
topicsCovered: text('topics_covered').array(),
|
||||
makeupForSessionId: uuid('makeup_for_session_id'),
|
||||
notesCompletedAt: timestamp('notes_completed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
// --- Type exports ---
|
||||
|
||||
export type Instructor = typeof instructors.$inferSelect
|
||||
@@ -101,3 +130,5 @@ export type ScheduleSlot = typeof scheduleSlots.$inferSelect
|
||||
export type ScheduleSlotInsert = typeof scheduleSlots.$inferInsert
|
||||
export type Enrollment = typeof enrollments.$inferSelect
|
||||
export type EnrollmentInsert = typeof enrollments.$inferInsert
|
||||
export type LessonSession = typeof lessonSessions.$inferSelect
|
||||
export type LessonSessionInsert = typeof lessonSessions.$inferInsert
|
||||
|
||||
Reference in New Issue
Block a user