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:
Ryan Moon
2026-03-30 09:29:03 -05:00
parent 93405af3b2
commit 73360cd478
8 changed files with 587 additions and 3 deletions

View File

@@ -10,8 +10,11 @@ import {
EnrollmentCreateSchema,
EnrollmentUpdateSchema,
EnrollmentStatusUpdateSchema,
LessonSessionStatusUpdateSchema,
LessonSessionNotesSchema,
LessonSessionUpdateSchema,
} from '@lunarfront/shared/schemas'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService } from '../../services/lesson.service.js'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService } from '../../services/lesson.service.js'
export const lessonRoutes: FastifyPluginAsync = async (app) => {
// --- Instructors ---
@@ -206,4 +209,68 @@ export const lessonRoutes: FastifyPluginAsync = async (app) => {
if (!enrollment) return reply.status(404).send({ error: { message: 'Enrollment not found', statusCode: 404 } })
return reply.send(enrollment)
})
app.post('/enrollments/:id/generate-sessions', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const query = request.query as Record<string, string | undefined>
const weeks = query.weeks ? Number(query.weeks) : 4
const sessions = await LessonSessionService.generateSessions(app.db, id, weeks)
return reply.send({ generated: sessions.length, sessions })
})
// --- Lesson Sessions ---
app.get('/lesson-sessions', { 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,
instructorId: query.instructorId,
status: query.status?.split(',').filter(Boolean),
dateFrom: query.dateFrom,
dateTo: query.dateTo,
}
const result = await LessonSessionService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/lesson-sessions/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const session = await LessonSessionService.getById(app.db, id)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
app.patch('/lesson-sessions/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const session = await LessonSessionService.update(app.db, id, parsed.data)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
app.post('/lesson-sessions/:id/status', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionStatusUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const session = await LessonSessionService.updateStatus(app.db, id, parsed.data.status)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
app.post('/lesson-sessions/:id/notes', { preHandler: [app.authenticate, app.requirePermission('lessons.edit')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = LessonSessionNotesSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const session = await LessonSessionService.updateNotes(app.db, id, parsed.data)
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
}