Add Phase 7: grade history and session-plan item linking
- New tables: lesson_plan_item_grade_history (append-only), lesson_session_plan_item - Grading an item updates current_grade_value and creates immutable history record - Grading a not_started item auto-transitions it to in_progress - Linking items to a session also auto-transitions not_started items - Link operation is idempotent — re-linking same items produces no duplicates - Endpoints: POST/GET /lesson-plan-items/:id/grades, GET /lesson-plan-items/:id/grade-history - Endpoints: POST/GET /lesson-sessions/:id/plan-items - 8 new integration 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, instructorBlockedDates, storeClosures, gradingScales, gradingScaleLevels, memberLessonPlans, lessonPlanSections, lessonPlanItems } from '../db/schema/lessons.js'
|
||||
import { instructors, lessonTypes, scheduleSlots, enrollments, lessonSessions, instructorBlockedDates, storeClosures, gradingScales, gradingScaleLevels, memberLessonPlans, lessonPlanSections, lessonPlanItems, lessonPlanItemGradeHistory, lessonSessionPlanItems } from '../db/schema/lessons.js'
|
||||
import type {
|
||||
InstructorCreateInput,
|
||||
InstructorUpdateInput,
|
||||
@@ -17,6 +17,8 @@ import type {
|
||||
LessonPlanCreateInput,
|
||||
LessonPlanUpdateInput,
|
||||
LessonPlanItemUpdateInput,
|
||||
GradeCreateInput,
|
||||
SessionPlanItemsInput,
|
||||
InstructorBlockedDateCreateInput,
|
||||
StoreClosureCreateInput,
|
||||
PaginationInput,
|
||||
@@ -957,6 +959,105 @@ export const LessonPlanService = {
|
||||
},
|
||||
}
|
||||
|
||||
export const GradeHistoryService = {
|
||||
async create(db: PostgresJsDatabase<any>, lessonPlanItemId: string, input: GradeCreateInput, gradedBy?: string) {
|
||||
// Get the item to check current status and validate grading scale
|
||||
const [item] = await db
|
||||
.select()
|
||||
.from(lessonPlanItems)
|
||||
.where(eq(lessonPlanItems.id, lessonPlanItemId))
|
||||
.limit(1)
|
||||
if (!item) return null
|
||||
|
||||
// Create the grade history record
|
||||
const [record] = await db
|
||||
.insert(lessonPlanItemGradeHistory)
|
||||
.values({
|
||||
lessonPlanItemId,
|
||||
gradingScaleId: input.gradingScaleId,
|
||||
gradeValue: input.gradeValue,
|
||||
gradedBy,
|
||||
sessionId: input.sessionId,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returning()
|
||||
|
||||
// Update the item's current grade and auto-transition status
|
||||
const updates: Record<string, unknown> = {
|
||||
currentGradeValue: input.gradeValue,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
if (input.gradingScaleId) updates.gradingScaleId = input.gradingScaleId
|
||||
if (item.status === 'not_started') {
|
||||
updates.status = 'in_progress'
|
||||
updates.startedDate = new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
const [updatedItem] = await db
|
||||
.update(lessonPlanItems)
|
||||
.set(updates)
|
||||
.where(eq(lessonPlanItems.id, lessonPlanItemId))
|
||||
.returning()
|
||||
|
||||
return { record, item: updatedItem }
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, lessonPlanItemId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(lessonPlanItemGradeHistory)
|
||||
.where(eq(lessonPlanItemGradeHistory.lessonPlanItemId, lessonPlanItemId))
|
||||
.orderBy(lessonPlanItemGradeHistory.createdAt)
|
||||
},
|
||||
}
|
||||
|
||||
export const SessionPlanItemService = {
|
||||
async linkItems(db: PostgresJsDatabase<any>, sessionId: string, input: SessionPlanItemsInput) {
|
||||
// Verify session exists
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(lessonSessions)
|
||||
.where(eq(lessonSessions.id, sessionId))
|
||||
.limit(1)
|
||||
if (!session) return null
|
||||
|
||||
// Find already-linked item ids to avoid duplicates
|
||||
const existing = await db
|
||||
.select({ lessonPlanItemId: lessonSessionPlanItems.lessonPlanItemId })
|
||||
.from(lessonSessionPlanItems)
|
||||
.where(eq(lessonSessionPlanItems.sessionId, sessionId))
|
||||
const existingIds = new Set(existing.map((r) => r.lessonPlanItemId))
|
||||
|
||||
const newIds = input.lessonPlanItemIds.filter((id) => !existingIds.has(id))
|
||||
if (newIds.length === 0) return []
|
||||
|
||||
// Auto-transition any not_started items to in_progress
|
||||
if (newIds.length > 0) {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
await db
|
||||
.update(lessonPlanItems)
|
||||
.set({ status: 'in_progress', startedDate: today, updatedAt: new Date() })
|
||||
.where(
|
||||
and(
|
||||
inArray(lessonPlanItems.id, newIds),
|
||||
eq(lessonPlanItems.status, 'not_started'),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const rows = newIds.map((id) => ({ sessionId, lessonPlanItemId: id }))
|
||||
const created = await db.insert(lessonSessionPlanItems).values(rows).returning()
|
||||
return created
|
||||
},
|
||||
|
||||
async listForSession(db: PostgresJsDatabase<any>, sessionId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(lessonSessionPlanItems)
|
||||
.where(eq(lessonSessionPlanItems.sessionId, sessionId))
|
||||
},
|
||||
}
|
||||
|
||||
export const InstructorBlockedDateService = {
|
||||
async create(db: PostgresJsDatabase<any>, instructorId: string, input: InstructorBlockedDateCreateInput) {
|
||||
if (input.startDate > input.endDate) {
|
||||
|
||||
Reference in New Issue
Block a user