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:
@@ -1,6 +1,6 @@
|
||||
import { eq, and, ne, count, gte, lte, inArray, type Column, type SQL } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { instructors, lessonTypes, scheduleSlots, enrollments, lessonSessions } from '../db/schema/lessons.js'
|
||||
import { instructors, lessonTypes, scheduleSlots, enrollments, lessonSessions, gradingScales, gradingScaleLevels } from '../db/schema/lessons.js'
|
||||
import type {
|
||||
InstructorCreateInput,
|
||||
InstructorUpdateInput,
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
EnrollmentUpdateInput,
|
||||
LessonSessionNotesInput,
|
||||
LessonSessionUpdateInput,
|
||||
GradingScaleCreateInput,
|
||||
GradingScaleUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
@@ -577,3 +579,132 @@ export const LessonSessionService = {
|
||||
return session ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const GradingScaleService = {
|
||||
async create(db: PostgresJsDatabase<any>, input: GradingScaleCreateInput, createdBy?: string) {
|
||||
// If setting as default, unset any existing default
|
||||
if (input.isDefault) {
|
||||
await db
|
||||
.update(gradingScales)
|
||||
.set({ isDefault: false, updatedAt: new Date() })
|
||||
.where(eq(gradingScales.isDefault, true))
|
||||
}
|
||||
|
||||
const [scale] = await db
|
||||
.insert(gradingScales)
|
||||
.values({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
isDefault: input.isDefault,
|
||||
createdBy,
|
||||
})
|
||||
.returning()
|
||||
|
||||
const levels = await db
|
||||
.insert(gradingScaleLevels)
|
||||
.values(
|
||||
input.levels.map((l) => ({
|
||||
gradingScaleId: scale.id,
|
||||
value: l.value,
|
||||
label: l.label,
|
||||
numericValue: l.numericValue,
|
||||
colorHex: l.colorHex,
|
||||
sortOrder: l.sortOrder,
|
||||
})),
|
||||
)
|
||||
.returning()
|
||||
|
||||
return { ...scale, levels }
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [scale] = await db
|
||||
.select()
|
||||
.from(gradingScales)
|
||||
.where(eq(gradingScales.id, id))
|
||||
.limit(1)
|
||||
if (!scale) return null
|
||||
|
||||
const levels = await db
|
||||
.select()
|
||||
.from(gradingScaleLevels)
|
||||
.where(eq(gradingScaleLevels.gradingScaleId, id))
|
||||
.orderBy(gradingScaleLevels.sortOrder)
|
||||
|
||||
return { ...scale, levels }
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(gradingScales.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [gradingScales.name])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: gradingScales.name,
|
||||
created_at: gradingScales.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(gradingScales).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, gradingScales.name)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(gradingScales).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async listAll(db: PostgresJsDatabase<any>) {
|
||||
const scales = await db
|
||||
.select()
|
||||
.from(gradingScales)
|
||||
.where(eq(gradingScales.isActive, true))
|
||||
.orderBy(gradingScales.name)
|
||||
|
||||
// Fetch levels for all scales in one query
|
||||
if (scales.length === 0) return []
|
||||
const allLevels = await db
|
||||
.select()
|
||||
.from(gradingScaleLevels)
|
||||
.where(inArray(gradingScaleLevels.gradingScaleId, scales.map((s) => s.id)))
|
||||
.orderBy(gradingScaleLevels.sortOrder)
|
||||
|
||||
const levelsByScale = new Map<string, typeof allLevels>()
|
||||
for (const level of allLevels) {
|
||||
const existing = levelsByScale.get(level.gradingScaleId) ?? []
|
||||
existing.push(level)
|
||||
levelsByScale.set(level.gradingScaleId, existing)
|
||||
}
|
||||
|
||||
return scales.map((s) => ({ ...s, levels: levelsByScale.get(s.id) ?? [] }))
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: GradingScaleUpdateInput) {
|
||||
if (input.isDefault) {
|
||||
await db
|
||||
.update(gradingScales)
|
||||
.set({ isDefault: false, updatedAt: new Date() })
|
||||
.where(and(eq(gradingScales.isDefault, true), ne(gradingScales.id, id)))
|
||||
}
|
||||
|
||||
const [scale] = await db
|
||||
.update(gradingScales)
|
||||
.set({ ...input, updatedAt: new Date() })
|
||||
.where(eq(gradingScales.id, id))
|
||||
.returning()
|
||||
return scale ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [scale] = await db
|
||||
.update(gradingScales)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(eq(gradingScales.id, id))
|
||||
.returning()
|
||||
return scale ?? null
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user