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:
Ryan Moon
2026-03-30 09:40:41 -05:00
parent 31f661ff4f
commit aae5a022a8
8 changed files with 661 additions and 2 deletions

View File

@@ -15,8 +15,11 @@ import {
LessonSessionUpdateSchema,
GradingScaleCreateSchema,
GradingScaleUpdateSchema,
LessonPlanCreateSchema,
LessonPlanUpdateSchema,
LessonPlanItemUpdateSchema,
} from '@lunarfront/shared/schemas'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService, GradingScaleService } from '../../services/lesson.service.js'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService, GradingScaleService, LessonPlanService, LessonPlanItemService } from '../../services/lesson.service.js'
export const lessonRoutes: FastifyPluginAsync = async (app) => {
// --- Instructors ---
@@ -322,4 +325,58 @@ export const lessonRoutes: FastifyPluginAsync = async (app) => {
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
// --- Lesson Plans ---
app.post('/lesson-plans', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const parsed = LessonPlanCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const plan = await LessonPlanService.create(app.db, parsed.data, request.user.id)
return reply.status(201).send(plan)
})
app.get('/lesson-plans', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const query = request.query as Record<string, string | undefined>
const params = PaginationSchema.parse(query)
const filters = {
enrollmentId: query.enrollmentId,
memberId: query.memberId,
isActive: query.isActive === 'true' ? true : query.isActive === 'false' ? false : undefined,
}
const result = await LessonPlanService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/lesson-plans/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const plan = await LessonPlanService.getById(app.db, id)
if (!plan) return reply.status(404).send({ error: { message: 'Lesson plan not found', statusCode: 404 } })
return reply.send(plan)
})
app.patch('/lesson-plans/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonPlanUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const plan = await LessonPlanService.update(app.db, id, parsed.data)
if (!plan) return reply.status(404).send({ error: { message: 'Lesson plan not found', statusCode: 404 } })
return reply.send(plan)
})
// --- Lesson Plan Items ---
app.patch('/lesson-plan-items/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonPlanItemUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await LessonPlanItemService.update(app.db, id, parsed.data)
if (!item) return reply.status(404).send({ error: { message: 'Lesson plan item not found', statusCode: 404 } })
return reply.send(item)
})
}