Add lessons Phase 5: grading scales with nested levels

Custom grading scales with ordered levels (value, label, numeric score,
color). Supports one-default-per-store constraint, deep create with
nested levels, lookup endpoint for dropdowns, and search/pagination.
12 new tests (76 total lessons tests).
This commit is contained in:
Ryan Moon
2026-03-30 09:36:48 -05:00
parent 73360cd478
commit 31f661ff4f
8 changed files with 396 additions and 2 deletions

View File

@@ -13,8 +13,10 @@ import {
LessonSessionStatusUpdateSchema,
LessonSessionNotesSchema,
LessonSessionUpdateSchema,
GradingScaleCreateSchema,
GradingScaleUpdateSchema,
} from '@lunarfront/shared/schemas'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService } from '../../services/lesson.service.js'
import { InstructorService, LessonTypeService, ScheduleSlotService, EnrollmentService, LessonSessionService, GradingScaleService } from '../../services/lesson.service.js'
export const lessonRoutes: FastifyPluginAsync = async (app) => {
// --- Instructors ---
@@ -273,4 +275,51 @@ export const lessonRoutes: FastifyPluginAsync = async (app) => {
if (!session) return reply.status(404).send({ error: { message: 'Lesson session not found', statusCode: 404 } })
return reply.send(session)
})
// --- Grading Scales ---
app.post('/grading-scales', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const parsed = GradingScaleCreateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const scale = await GradingScaleService.create(app.db, parsed.data, request.user.id)
return reply.status(201).send(scale)
})
app.get('/grading-scales', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await GradingScaleService.list(app.db, params)
return reply.send(result)
})
app.get('/grading-scales/all', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (_request, reply) => {
const scales = await GradingScaleService.listAll(app.db)
return reply.send(scales)
})
app.get('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const scale = await GradingScaleService.getById(app.db, id)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
app.patch('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const parsed = GradingScaleUpdateSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const scale = await GradingScaleService.update(app.db, id, parsed.data)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
app.delete('/grading-scales/:id', { preHandler: [app.authenticate, app.requirePermission('lessons.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const scale = await GradingScaleService.delete(app.db, id)
if (!scale) return reply.status(404).send({ error: { message: 'Grading scale not found', statusCode: 404 } })
return reply.send(scale)
})
}