@studious-lms/server 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +4 -4
- package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1 -1
- package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +4 -4
- package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +4 -4
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +17 -14
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/marketplace.d.ts +37 -0
- package/dist/models/marketplace.d.ts.map +1 -1
- package/dist/models/marketplace.js +10 -2
- package/dist/models/marketplace.js.map +1 -1
- package/dist/models/module.d.ts +228 -0
- package/dist/models/module.d.ts.map +1 -0
- package/dist/models/module.js +69 -0
- package/dist/models/module.js.map +1 -0
- package/dist/models/moduleItem.d.ts +5 -1
- package/dist/models/moduleItem.d.ts.map +1 -1
- package/dist/models/moduleItem.js +19 -4
- package/dist/models/moduleItem.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +4 -2
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +768 -6
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/fees.d.ts +1 -1
- package/dist/routers/marketplace.d.ts +41 -0
- package/dist/routers/marketplace.d.ts.map +1 -1
- package/dist/routers/marketplace.js +13 -3
- package/dist/routers/marketplace.js.map +1 -1
- package/dist/routers/module.d.ts +345 -0
- package/dist/routers/module.d.ts.map +1 -0
- package/dist/routers/module.js +82 -0
- package/dist/routers/module.js.map +1 -0
- package/dist/routers/moduleItem.d.ts.map +1 -1
- package/dist/routers/moduleItem.js +7 -6
- package/dist/routers/moduleItem.js.map +1 -1
- package/dist/routers/parent.d.ts +2 -1
- package/dist/routers/parent.d.ts.map +1 -1
- package/dist/routers/school.d.ts +1 -1
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +14 -4
- package/dist/routers/school.js.map +1 -1
- package/dist/services/academicCalendar.d.ts.map +1 -1
- package/dist/services/academicCalendar.js +30 -2
- package/dist/services/academicCalendar.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +8 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +6 -2
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +8 -4
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/class.d.ts +14 -9
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +4 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/fees.d.ts.map +1 -1
- package/dist/services/fees.js +24 -4
- package/dist/services/fees.js.map +1 -1
- package/dist/services/feesAuthz.d.ts +2 -5
- package/dist/services/feesAuthz.d.ts.map +1 -1
- package/dist/services/feesAuthz.js +5 -8
- package/dist/services/feesAuthz.js.map +1 -1
- package/dist/services/marketplace.d.ts +36 -0
- package/dist/services/marketplace.d.ts.map +1 -1
- package/dist/services/marketplace.js +36 -6
- package/dist/services/marketplace.js.map +1 -1
- package/dist/services/module.d.ts +288 -0
- package/dist/services/module.d.ts.map +1 -0
- package/dist/services/module.js +518 -0
- package/dist/services/module.js.map +1 -0
- package/dist/services/moduleItem.d.ts.map +1 -1
- package/dist/services/moduleItem.js +23 -7
- package/dist/services/moduleItem.js.map +1 -1
- package/dist/services/parentPortal.d.ts +3 -5
- package/dist/services/parentPortal.d.ts.map +1 -1
- package/dist/services/parentPortal.js +29 -43
- package/dist/services/parentPortal.js.map +1 -1
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +52 -16
- package/dist/services/school.js.map +1 -1
- package/dist/services/studentImport.d.ts.map +1 -1
- package/dist/services/studentImport.js +38 -25
- package/dist/services/studentImport.js.map +1 -1
- package/dist/services/transcript.d.ts +2 -3
- package/dist/services/transcript.d.ts.map +1 -1
- package/dist/services/transcript.js +13 -25
- package/dist/services/transcript.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260708013000_add_modules/migration.sql +100 -0
- package/prisma/schema.prisma +93 -0
- package/scripts/import-nepal-cdc-curriculum.ts +29 -6
- package/src/models/attendance.ts +10 -12
- package/src/models/marketplace.ts +19 -0
- package/src/models/module.ts +77 -0
- package/src/models/moduleItem.ts +20 -2
- package/src/pipelines/gradeWorksheet.ts +6 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/marketplace.ts +16 -0
- package/src/routers/module.ts +118 -0
- package/src/routers/moduleItem.ts +6 -4
- package/src/routers/school.ts +16 -6
- package/src/services/academicCalendar.ts +28 -0
- package/src/services/assignment.ts +12 -1
- package/src/services/attendance.ts +7 -3
- package/src/services/class.ts +2 -2
- package/src/services/fees.ts +25 -2
- package/src/services/feesAuthz.ts +3 -7
- package/src/services/marketplace.ts +47 -3
- package/src/services/module.ts +683 -0
- package/src/services/moduleItem.ts +28 -4
- package/src/services/parentPortal.ts +26 -39
- package/src/services/school.ts +52 -15
- package/src/services/studentImport.ts +37 -25
- package/src/services/transcript.ts +11 -23
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.js","sources":["services/transcript.ts"],"sourceRoot":"/","sourcesContent":["/**\n * School-admin branded transcripts / progress reports (A4 — Nepal pilot).\n *\n * Aggregates per-class gradebook grades + grading boundaries + attendance %\n * into a pdf-lib PDF. Branding: school name + logo (letterhead optional when\n * present on School; schema does not include it yet — do not block).\n *\n * AcademicTerm (C1) is not modelled yet — accept `from`/`to` date range and/or\n * a free-text `schoolYear` string for the report header.\n *\n * Attendance is read via the existing present/late/absent M2M lists only\n * (same shaping as getStudentTimeline). Do not conflict with A1 Attendance\n * refactor — rate math lives in attendanceRate.ts.\n */\n\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { bucket } from \"../lib/googleCloudStorage.js\";\nimport {\n buildTranscriptPdf,\n isLevelBasedBoundary,\n type TranscriptClassRow,\n type TranscriptReportVariant,\n} from \"../lib/transcriptPdf.js\";\nimport {\n findActiveSchoolAdminMembership,\n getSchoolById,\n} from \"../models/school.js\";\nimport { computeAttendanceRate } from \"./attendanceRate.js\";\nimport { computeStudentGradeInClass } from \"./gradebook.js\";\n\nconst MAX_BULK = 50;\n\nexport interface GenerateTranscriptInput {\n schoolId: string;\n studentId: string;\n from?: Date;\n to?: Date;\n /**\n * Free-text school year label (e.g. \"2082 BS\", \"2025–2026\").\n * Prefer AcademicTerm (C1) once available.\n */\n schoolYear?: string;\n}\n\nasync function requireSchoolAdmin(schoolId: string, userId: string) {\n const membership = await findActiveSchoolAdminMembership(schoolId, userId);\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only school admins can generate transcripts\",\n });\n }\n return membership;\n}\n\nasync function loadFileBytes(\n path: string | null | undefined\n): Promise<Uint8Array | null> {\n if (!path) return null;\n try {\n const [bytes] = await bucket.file(path).download();\n return new Uint8Array(bytes);\n } catch {\n return null;\n }\n}\n\nfunction formatPeriodLabel(from?: Date, to?: Date): string | null {\n if (!from && !to) return null;\n const fmt = (d: Date) => d.toISOString().slice(0, 10);\n if (from && to) return `${fmt(from)} – ${fmt(to)}`;\n if (from) return `From ${fmt(from)}`;\n return `Through ${fmt(to!)}`;\n}\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, 48);\n}\n\nasync function fetchStudentAttendance(\n studentId: string,\n from?: Date,\n to?: Date\n) {\n const range = {\n ...(from ? { gte: from } : {}),\n ...(to ? { lte: to } : {}),\n };\n const hasRange = Object.keys(range).length > 0;\n\n // Same M2M present/late/absent read path as getStudentTimeline (A1-safe).\n const attendance = await prisma.attendance.findMany({\n where: {\n OR: [\n { present: { some: { id: studentId } } },\n { late: { some: { id: studentId } } },\n { absent: { some: { id: studentId } } },\n ],\n ...(hasRange ? { date: range } : {}),\n },\n select: {\n id: true,\n present: { where: { id: studentId }, select: { id: true } },\n late: { where: { id: studentId }, select: { id: true } },\n absent: { where: { id: studentId }, select: { id: true } },\n },\n });\n\n return attendance.map((record) => ({\n status: record.present.length\n ? (\"PRESENT\" as const)\n : record.late.length\n ? (\"LATE\" as const)\n : (\"ABSENT\" as const),\n }));\n}\n\nasync function buildClassRows(\n schoolId: string,\n studentId: string\n): Promise<{ rows: TranscriptClassRow[]; variant: TranscriptReportVariant }> {\n const classes = await prisma.class.findMany({\n where: {\n schoolId,\n students: { some: { id: studentId } },\n },\n select: {\n id: true,\n name: true,\n subject: true,\n section: true,\n },\n orderBy: [{ subject: \"asc\" }, { name: \"asc\" }],\n });\n\n const rows: TranscriptClassRow[] = [];\n let levelVotes = 0;\n let letterVotes = 0;\n\n for (const cls of classes) {\n const grade = await computeStudentGradeInClass(cls.id, studentId);\n if (!grade) continue;\n\n if (isLevelBasedBoundary(grade.gradingBoundary?.structured)) {\n levelVotes += 1;\n } else if (grade.gradingBoundary) {\n letterVotes += 1;\n }\n\n rows.push({\n className: cls.name,\n subject: cls.subject,\n section: cls.section,\n percentage: grade.result.percentage,\n gradeLabel: grade.letterGrade,\n categorySummaries: grade.result.categories.map((c) => ({\n name: c.name,\n average: c.average,\n })),\n });\n }\n\n const variant: TranscriptReportVariant =\n levelVotes > 0 && levelVotes >= letterVotes ? \"level\" : \"letter\";\n\n return { rows, variant };\n}\n\nexport async function generateTranscript(\n userId: string,\n input: GenerateTranscriptInput\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const school = await getSchoolById(input.schoolId);\n if (!school) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"School not found\" });\n }\n\n const membership = await prisma.schoolMembership.findFirst({\n where: {\n schoolId: input.schoolId,\n userId: input.studentId,\n status: \"ACTIVE\",\n role: \"STUDENT\",\n },\n select: {\n user: {\n select: {\n id: true,\n username: true,\n profile: { select: { displayName: true } },\n },\n },\n },\n });\n if (!membership) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Student is not an active student in this school\",\n });\n }\n\n const studentName =\n membership.user.profile?.displayName || membership.user.username;\n\n const [{ rows, variant }, attendanceStatuses, logoBytes] = await Promise.all([\n buildClassRows(input.schoolId, input.studentId),\n fetchStudentAttendance(input.studentId, input.from, input.to),\n loadFileBytes(school.logo?.path),\n ]);\n\n // Soft letterhead support when schema later adds it — never required.\n const schoolAny = school as {\n letterhead?: { path?: string | null; type?: string | null } | null;\n letterheadPath?: string | null;\n letterheadType?: string | null;\n };\n const letterheadPath =\n schoolAny.letterhead?.path ?? schoolAny.letterheadPath ?? null;\n const letterheadType =\n schoolAny.letterhead?.type ?? schoolAny.letterheadType ?? null;\n const letterheadBytes = await loadFileBytes(letterheadPath);\n\n const attendance = computeAttendanceRate(attendanceStatuses);\n const generatedAt = new Date();\n\n const pdfBytes = await buildTranscriptPdf({\n schoolName: school.name,\n studentName,\n studentUsername: membership.user.username,\n schoolYear: input.schoolYear ?? null,\n periodLabel: formatPeriodLabel(input.from, input.to),\n variant,\n classes: rows,\n attendance,\n logoBytes,\n logoContentType: school.logo?.type ?? null,\n letterheadBytes,\n letterheadContentType: letterheadType,\n generatedAt,\n });\n\n const yearSlug = input.schoolYear\n ? slugify(input.schoolYear)\n : generatedAt.toISOString().slice(0, 10);\n const fileName = `progress-report-${slugify(studentName) || membership.user.username}-${yearSlug}.pdf`;\n\n return {\n pdfBase64: Buffer.from(pdfBytes).toString(\"base64\"),\n fileName,\n variant,\n studentId: input.studentId,\n studentName,\n /** C1 blocker: AcademicTerm model does not exist yet. */\n academicTermSupported: false as const,\n attendance,\n classCount: rows.length,\n };\n}\n\nexport async function generateTranscripts(\n userId: string,\n input: {\n schoolId: string;\n studentIds: string[];\n from?: Date;\n to?: Date;\n schoolYear?: string;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const uniqueIds = [...new Set(input.studentIds)].slice(0, MAX_BULK);\n if (uniqueIds.length === 0) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Select at least one student\",\n });\n }\n\n type TranscriptResult = Awaited<ReturnType<typeof generateTranscript>>;\n const results: TranscriptResult[] = [];\n const errors: { studentId: string; message: string }[] = [];\n\n for (const studentId of uniqueIds) {\n try {\n const result = await generateTranscript(userId, {\n schoolId: input.schoolId,\n studentId,\n from: input.from,\n to: input.to,\n schoolYear: input.schoolYear,\n });\n results.push(result);\n } catch (error) {\n const message =\n error instanceof TRPCError\n ? error.message\n : error instanceof Error\n ? error.message\n : \"Failed to generate transcript\";\n errors.push({ studentId, message });\n }\n }\n\n return { transcripts: results, errors };\n}\n"],"names":[],"mappings":"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","debug_id":"c7eb65fe-e18f-5b5b-9897-ce16dc61c93d"}
|
|
1
|
+
{"version":3,"file":"transcript.js","sources":["services/transcript.ts"],"sourceRoot":"/","sourcesContent":["/**\n * School-admin branded transcripts / progress reports (A4 — Nepal pilot).\n *\n * Aggregates per-class gradebook grades + grading boundaries + attendance %\n * into a pdf-lib PDF. Branding: school name + logo (letterhead optional when\n * present on School; schema does not include it yet — do not block).\n *\n * AcademicTerm (C1) is not modelled yet — accept `from`/`to` date range and/or\n * a free-text `schoolYear` string for the report header.\n *\n * Attendance is read from AttendanceRecord (EXCUSED excluded from the rate\n * denominator) — rate math lives in attendanceRate.ts.\n */\n\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { bucket } from \"../lib/googleCloudStorage.js\";\nimport {\n buildTranscriptPdf,\n isLevelBasedBoundary,\n type TranscriptClassRow,\n type TranscriptReportVariant,\n} from \"../lib/transcriptPdf.js\";\nimport {\n findActiveSchoolAdminMembership,\n getSchoolById,\n} from \"../models/school.js\";\nimport { computeAttendanceRate } from \"./attendanceRate.js\";\nimport { computeStudentGradeInClass } from \"./gradebook.js\";\n\nconst MAX_BULK = 50;\n\nexport interface GenerateTranscriptInput {\n schoolId: string;\n studentId: string;\n from?: Date;\n to?: Date;\n /**\n * Free-text school year label (e.g. \"2082 BS\", \"2025–2026\").\n * Prefer AcademicTerm (C1) once available.\n */\n schoolYear?: string;\n}\n\nasync function requireSchoolAdmin(schoolId: string, userId: string) {\n const membership = await findActiveSchoolAdminMembership(schoolId, userId);\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only school admins can generate transcripts\",\n });\n }\n return membership;\n}\n\nasync function loadFileBytes(\n path: string | null | undefined\n): Promise<Uint8Array | null> {\n if (!path) return null;\n try {\n const [bytes] = await bucket.file(path).download();\n return new Uint8Array(bytes);\n } catch {\n return null;\n }\n}\n\nfunction formatPeriodLabel(from?: Date, to?: Date): string | null {\n if (!from && !to) return null;\n const fmt = (d: Date) => d.toISOString().slice(0, 10);\n if (from && to) return `${fmt(from)} – ${fmt(to)}`;\n if (from) return `From ${fmt(from)}`;\n return `Through ${fmt(to!)}`;\n}\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, 48);\n}\n\nasync function fetchStudentAttendance(\n studentId: string,\n from?: Date,\n to?: Date\n) {\n const range = {\n ...(from ? { gte: from } : {}),\n ...(to ? { lte: to } : {}),\n };\n const hasRange = Object.keys(range).length > 0;\n\n // Read AttendanceRecord so EXCUSED records are excluded from the rate\n // denominator (computeAttendanceRate ignores non-PRESENT/LATE/ABSENT).\n const records = await prisma.attendanceRecord.findMany({\n where: {\n studentId,\n status: { in: [\"PRESENT\", \"LATE\", \"ABSENT\"] },\n ...(hasRange ? { attendance: { date: range } } : {}),\n },\n select: { status: true },\n });\n\n return records.map((record) => ({\n status: record.status as \"PRESENT\" | \"LATE\" | \"ABSENT\",\n }));\n}\n\nasync function buildClassRows(\n schoolId: string,\n studentId: string\n): Promise<{ rows: TranscriptClassRow[]; variant: TranscriptReportVariant }> {\n const classes = await prisma.class.findMany({\n where: {\n schoolId,\n students: { some: { id: studentId } },\n },\n select: {\n id: true,\n name: true,\n subject: true,\n section: true,\n },\n orderBy: [{ subject: \"asc\" }, { name: \"asc\" }],\n });\n\n const rows: TranscriptClassRow[] = [];\n let levelVotes = 0;\n let letterVotes = 0;\n\n for (const cls of classes) {\n const grade = await computeStudentGradeInClass(cls.id, studentId);\n if (!grade) continue;\n\n if (isLevelBasedBoundary(grade.gradingBoundary?.structured)) {\n levelVotes += 1;\n } else if (grade.gradingBoundary) {\n letterVotes += 1;\n }\n\n rows.push({\n className: cls.name,\n subject: cls.subject,\n section: cls.section,\n percentage: grade.result.percentage,\n gradeLabel: grade.letterGrade,\n categorySummaries: grade.result.categories.map((c) => ({\n name: c.name,\n average: c.average,\n })),\n });\n }\n\n const variant: TranscriptReportVariant =\n levelVotes > 0 && levelVotes >= letterVotes ? \"level\" : \"letter\";\n\n return { rows, variant };\n}\n\nexport async function generateTranscript(\n userId: string,\n input: GenerateTranscriptInput\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const school = await getSchoolById(input.schoolId);\n if (!school) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"School not found\" });\n }\n\n const membership = await prisma.schoolMembership.findFirst({\n where: {\n schoolId: input.schoolId,\n userId: input.studentId,\n status: \"ACTIVE\",\n role: \"STUDENT\",\n },\n select: {\n user: {\n select: {\n id: true,\n username: true,\n profile: { select: { displayName: true } },\n },\n },\n },\n });\n if (!membership) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Student is not an active student in this school\",\n });\n }\n\n const studentName =\n membership.user.profile?.displayName || membership.user.username;\n\n const [{ rows, variant }, attendanceStatuses, logoBytes] = await Promise.all([\n buildClassRows(input.schoolId, input.studentId),\n fetchStudentAttendance(input.studentId, input.from, input.to),\n loadFileBytes(school.logo?.path),\n ]);\n\n // Soft letterhead support when schema later adds it — never required.\n const schoolAny = school as {\n letterhead?: { path?: string | null; type?: string | null } | null;\n letterheadPath?: string | null;\n letterheadType?: string | null;\n };\n const letterheadPath =\n schoolAny.letterhead?.path ?? schoolAny.letterheadPath ?? null;\n const letterheadType =\n schoolAny.letterhead?.type ?? schoolAny.letterheadType ?? null;\n const letterheadBytes = await loadFileBytes(letterheadPath);\n\n const attendance = computeAttendanceRate(attendanceStatuses);\n const generatedAt = new Date();\n\n const pdfBytes = await buildTranscriptPdf({\n schoolName: school.name,\n studentName,\n studentUsername: membership.user.username,\n schoolYear: input.schoolYear ?? null,\n periodLabel: formatPeriodLabel(input.from, input.to),\n variant,\n classes: rows,\n attendance,\n logoBytes,\n logoContentType: school.logo?.type ?? null,\n letterheadBytes,\n letterheadContentType: letterheadType,\n generatedAt,\n });\n\n const yearSlug = input.schoolYear\n ? slugify(input.schoolYear)\n : generatedAt.toISOString().slice(0, 10);\n const fileName = `progress-report-${slugify(studentName) || membership.user.username}-${yearSlug}.pdf`;\n\n return {\n pdfBase64: Buffer.from(pdfBytes).toString(\"base64\"),\n fileName,\n variant,\n studentId: input.studentId,\n studentName,\n /** C1 blocker: AcademicTerm model does not exist yet. */\n academicTermSupported: false as const,\n attendance,\n classCount: rows.length,\n };\n}\n\nexport async function generateTranscripts(\n userId: string,\n input: {\n schoolId: string;\n studentIds: string[];\n from?: Date;\n to?: Date;\n schoolYear?: string;\n }\n) {\n await requireSchoolAdmin(input.schoolId, userId);\n\n const uniqueIds = [...new Set(input.studentIds)].slice(0, MAX_BULK);\n if (uniqueIds.length === 0) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Select at least one student\",\n });\n }\n\n type TranscriptResult = Awaited<ReturnType<typeof generateTranscript>>;\n const results: TranscriptResult[] = [];\n const errors: { studentId: string; message: string }[] = [];\n\n for (const studentId of uniqueIds) {\n try {\n const result = await generateTranscript(userId, {\n schoolId: input.schoolId,\n studentId,\n from: input.from,\n to: input.to,\n schoolYear: input.schoolYear,\n });\n results.push(result);\n } catch (error) {\n const message =\n error instanceof TRPCError\n ? error.message\n : error instanceof Error\n ? error.message\n : \"Failed to generate transcript\";\n errors.push({ studentId, message });\n }\n }\n\n return { transcripts: results, errors };\n}\n"],"names":[],"mappings":"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","debug_id":"186437e2-1c14-52c2-94d5-09ccd8035936"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "CourseModuleItemType" AS ENUM ('ASSIGNMENT', 'WORKSHEET', 'FILE', 'ANNOUNCEMENT', 'DISCUSSION', 'EXTERNAL_URL', 'SUB_HEADER');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "ModuleItemRequirementType" AS ENUM ('VIEW', 'SUBMIT', 'SCORE_AT_LEAST');
|
|
6
|
+
|
|
7
|
+
-- CreateEnum
|
|
8
|
+
CREATE TYPE "ModuleItemProgressStatus" AS ENUM ('INCOMPLETE', 'COMPLETE');
|
|
9
|
+
|
|
10
|
+
-- CreateTable
|
|
11
|
+
CREATE TABLE "Module" (
|
|
12
|
+
"id" TEXT NOT NULL,
|
|
13
|
+
"classId" TEXT NOT NULL,
|
|
14
|
+
"title" TEXT NOT NULL,
|
|
15
|
+
"position" INTEGER NOT NULL,
|
|
16
|
+
"unlockAt" TIMESTAMP(3),
|
|
17
|
+
"requireSequential" BOOLEAN NOT NULL DEFAULT false,
|
|
18
|
+
"published" BOOLEAN NOT NULL DEFAULT false,
|
|
19
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
21
|
+
|
|
22
|
+
CONSTRAINT "Module_pkey" PRIMARY KEY ("id")
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- CreateTable
|
|
26
|
+
CREATE TABLE "ModulePrerequisite" (
|
|
27
|
+
"id" TEXT NOT NULL,
|
|
28
|
+
"moduleId" TEXT NOT NULL,
|
|
29
|
+
"prerequisiteId" TEXT NOT NULL,
|
|
30
|
+
|
|
31
|
+
CONSTRAINT "ModulePrerequisite_pkey" PRIMARY KEY ("id")
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- CreateTable
|
|
35
|
+
CREATE TABLE "CourseModuleItem" (
|
|
36
|
+
"id" TEXT NOT NULL,
|
|
37
|
+
"moduleId" TEXT NOT NULL,
|
|
38
|
+
"position" INTEGER NOT NULL,
|
|
39
|
+
"indent" INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
"itemType" "CourseModuleItemType" NOT NULL,
|
|
41
|
+
"itemId" TEXT,
|
|
42
|
+
"externalUrl" TEXT,
|
|
43
|
+
"title" TEXT,
|
|
44
|
+
"requirementType" "ModuleItemRequirementType",
|
|
45
|
+
"requiredScore" DOUBLE PRECISION,
|
|
46
|
+
|
|
47
|
+
CONSTRAINT "CourseModuleItem_pkey" PRIMARY KEY ("id")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- CreateTable
|
|
51
|
+
CREATE TABLE "ModuleItemProgress" (
|
|
52
|
+
"id" TEXT NOT NULL,
|
|
53
|
+
"moduleItemId" TEXT NOT NULL,
|
|
54
|
+
"studentId" TEXT NOT NULL,
|
|
55
|
+
"status" "ModuleItemProgressStatus" NOT NULL DEFAULT 'INCOMPLETE',
|
|
56
|
+
"completedAt" TIMESTAMP(3),
|
|
57
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
58
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
59
|
+
|
|
60
|
+
CONSTRAINT "ModuleItemProgress_pkey" PRIMARY KEY ("id")
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
-- CreateIndex
|
|
64
|
+
CREATE INDEX "Module_classId_position_idx" ON "Module"("classId", "position");
|
|
65
|
+
|
|
66
|
+
-- CreateIndex
|
|
67
|
+
CREATE INDEX "ModulePrerequisite_prerequisiteId_idx" ON "ModulePrerequisite"("prerequisiteId");
|
|
68
|
+
|
|
69
|
+
-- CreateIndex
|
|
70
|
+
CREATE UNIQUE INDEX "ModulePrerequisite_moduleId_prerequisiteId_key" ON "ModulePrerequisite"("moduleId", "prerequisiteId");
|
|
71
|
+
|
|
72
|
+
-- CreateIndex
|
|
73
|
+
CREATE INDEX "CourseModuleItem_moduleId_position_idx" ON "CourseModuleItem"("moduleId", "position");
|
|
74
|
+
|
|
75
|
+
-- CreateIndex
|
|
76
|
+
CREATE INDEX "CourseModuleItem_itemType_itemId_idx" ON "CourseModuleItem"("itemType", "itemId");
|
|
77
|
+
|
|
78
|
+
-- CreateIndex
|
|
79
|
+
CREATE INDEX "ModuleItemProgress_studentId_idx" ON "ModuleItemProgress"("studentId");
|
|
80
|
+
|
|
81
|
+
-- CreateIndex
|
|
82
|
+
CREATE UNIQUE INDEX "ModuleItemProgress_moduleItemId_studentId_key" ON "ModuleItemProgress"("moduleItemId", "studentId");
|
|
83
|
+
|
|
84
|
+
-- AddForeignKey
|
|
85
|
+
ALTER TABLE "Module" ADD CONSTRAINT "Module_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
86
|
+
|
|
87
|
+
-- AddForeignKey
|
|
88
|
+
ALTER TABLE "ModulePrerequisite" ADD CONSTRAINT "ModulePrerequisite_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
89
|
+
|
|
90
|
+
-- AddForeignKey
|
|
91
|
+
ALTER TABLE "ModulePrerequisite" ADD CONSTRAINT "ModulePrerequisite_prerequisiteId_fkey" FOREIGN KEY ("prerequisiteId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
92
|
+
|
|
93
|
+
-- AddForeignKey
|
|
94
|
+
ALTER TABLE "CourseModuleItem" ADD CONSTRAINT "CourseModuleItem_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
95
|
+
|
|
96
|
+
-- AddForeignKey
|
|
97
|
+
ALTER TABLE "ModuleItemProgress" ADD CONSTRAINT "ModuleItemProgress_moduleItemId_fkey" FOREIGN KEY ("moduleItemId") REFERENCES "CourseModuleItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
98
|
+
|
|
99
|
+
-- AddForeignKey
|
|
100
|
+
ALTER TABLE "ModuleItemProgress" ADD CONSTRAINT "ModuleItemProgress_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/prisma/schema.prisma
CHANGED
|
@@ -328,6 +328,7 @@ model User {
|
|
|
328
328
|
discussions Discussion[]
|
|
329
329
|
discussionReplies DiscussionReply[]
|
|
330
330
|
discussionReads DiscussionRead[]
|
|
331
|
+
moduleItemProgress ModuleItemProgress[]
|
|
331
332
|
customGradeEntries CustomGradeEntry[] @relation("UserCustomGradeEntries")
|
|
332
333
|
fileAnnotations FileAnnotation[] @relation("AuthoredFileAnnotations")
|
|
333
334
|
marketplaceListings MarketplaceListing[] @relation("MarketplaceListingAuthor")
|
|
@@ -428,6 +429,8 @@ model Class {
|
|
|
428
429
|
studentsCanCreateDiscussions Boolean @default(true)
|
|
429
430
|
studentsCanReply Boolean @default(true)
|
|
430
431
|
|
|
432
|
+
modules Module[]
|
|
433
|
+
|
|
431
434
|
/// Teacher-controlled ClassSidebar visibility. Keys match nav hrefs
|
|
432
435
|
/// without leading slash (e.g. "assignments", "quizzes"). Null = all defaults on.
|
|
433
436
|
sidebarFeatures Json?
|
|
@@ -1478,6 +1481,96 @@ model Permission {
|
|
|
1478
1481
|
@@index([schoolId])
|
|
1479
1482
|
}
|
|
1480
1483
|
|
|
1484
|
+
enum CourseModuleItemType {
|
|
1485
|
+
ASSIGNMENT
|
|
1486
|
+
WORKSHEET
|
|
1487
|
+
FILE
|
|
1488
|
+
ANNOUNCEMENT
|
|
1489
|
+
DISCUSSION
|
|
1490
|
+
EXTERNAL_URL
|
|
1491
|
+
SUB_HEADER
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
enum ModuleItemRequirementType {
|
|
1495
|
+
VIEW
|
|
1496
|
+
SUBMIT
|
|
1497
|
+
SCORE_AT_LEAST
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
enum ModuleItemProgressStatus {
|
|
1501
|
+
INCOMPLETE
|
|
1502
|
+
COMPLETE
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/// Canvas-style course module: an ordered group of content items with
|
|
1506
|
+
/// optional prerequisites, unlock date and sequential progression.
|
|
1507
|
+
model Module {
|
|
1508
|
+
id String @id @default(uuid())
|
|
1509
|
+
classId String
|
|
1510
|
+
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
1511
|
+
title String
|
|
1512
|
+
position Int
|
|
1513
|
+
unlockAt DateTime?
|
|
1514
|
+
requireSequential Boolean @default(false)
|
|
1515
|
+
published Boolean @default(false)
|
|
1516
|
+
createdAt DateTime @default(now())
|
|
1517
|
+
updatedAt DateTime @updatedAt
|
|
1518
|
+
|
|
1519
|
+
items CourseModuleItem[]
|
|
1520
|
+
prerequisites ModulePrerequisite[] @relation("ModulePrerequisites")
|
|
1521
|
+
requiredBy ModulePrerequisite[] @relation("ModuleRequiredBy")
|
|
1522
|
+
|
|
1523
|
+
@@index([classId, position])
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
model ModulePrerequisite {
|
|
1527
|
+
id String @id @default(uuid())
|
|
1528
|
+
moduleId String
|
|
1529
|
+
module Module @relation("ModulePrerequisites", fields: [moduleId], references: [id], onDelete: Cascade)
|
|
1530
|
+
prerequisiteId String
|
|
1531
|
+
prerequisite Module @relation("ModuleRequiredBy", fields: [prerequisiteId], references: [id], onDelete: Cascade)
|
|
1532
|
+
|
|
1533
|
+
@@unique([moduleId, prerequisiteId])
|
|
1534
|
+
@@index([prerequisiteId])
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
model CourseModuleItem {
|
|
1538
|
+
id String @id @default(uuid())
|
|
1539
|
+
moduleId String
|
|
1540
|
+
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
1541
|
+
position Int
|
|
1542
|
+
indent Int @default(0)
|
|
1543
|
+
itemType CourseModuleItemType
|
|
1544
|
+
/// ID of the referenced assignment / worksheet / file / announcement /
|
|
1545
|
+
/// discussion. Null for EXTERNAL_URL and SUB_HEADER items.
|
|
1546
|
+
itemId String?
|
|
1547
|
+
externalUrl String?
|
|
1548
|
+
/// Display title override; required for EXTERNAL_URL and SUB_HEADER.
|
|
1549
|
+
title String?
|
|
1550
|
+
requirementType ModuleItemRequirementType?
|
|
1551
|
+
requiredScore Float?
|
|
1552
|
+
|
|
1553
|
+
progress ModuleItemProgress[]
|
|
1554
|
+
|
|
1555
|
+
@@index([moduleId, position])
|
|
1556
|
+
@@index([itemType, itemId])
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
model ModuleItemProgress {
|
|
1560
|
+
id String @id @default(uuid())
|
|
1561
|
+
moduleItemId String
|
|
1562
|
+
moduleItem CourseModuleItem @relation(fields: [moduleItemId], references: [id], onDelete: Cascade)
|
|
1563
|
+
studentId String
|
|
1564
|
+
student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
|
1565
|
+
status ModuleItemProgressStatus @default(INCOMPLETE)
|
|
1566
|
+
completedAt DateTime?
|
|
1567
|
+
createdAt DateTime @default(now())
|
|
1568
|
+
updatedAt DateTime @updatedAt
|
|
1569
|
+
|
|
1570
|
+
@@unique([moduleItemId, studentId])
|
|
1571
|
+
@@index([studentId])
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1481
1574
|
// ---------------------------------------------------------------------------
|
|
1482
1575
|
// A2 — Monthly student fees + manual payment ledger (Nepal pilot)
|
|
1483
1576
|
// Billing period = FeeInvoice.year + FeeInvoice.month (no separate term table;
|
|
@@ -93,6 +93,12 @@ function parseArgs(argv: string[]) {
|
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function parseBool(value: string | boolean | undefined): boolean {
|
|
97
|
+
if (value === undefined) return false;
|
|
98
|
+
if (typeof value === "boolean") return value;
|
|
99
|
+
return !["false", "0", "no"].includes(value.trim().toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
function loadJson<T>(path: string): T {
|
|
97
103
|
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
98
104
|
}
|
|
@@ -174,13 +180,19 @@ async function main() {
|
|
|
174
180
|
process.exit(1);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
const dryRun =
|
|
178
|
-
const force =
|
|
179
|
-
const skipBoundary =
|
|
180
|
-
const
|
|
183
|
+
const dryRun = parseBool(args["dry-run"]);
|
|
184
|
+
const force = parseBool(args.force);
|
|
185
|
+
const skipBoundary = parseBool(args["skip-boundary"]);
|
|
186
|
+
const rawGrades = String(args.grades ?? "4,5")
|
|
181
187
|
.split(",")
|
|
182
|
-
.map((g) => Number(g.trim()))
|
|
183
|
-
|
|
188
|
+
.map((g) => Number(g.trim()));
|
|
189
|
+
const grades = rawGrades.filter((g): g is 4 | 5 => g === 4 || g === 5);
|
|
190
|
+
if (grades.length === 0 || grades.length !== rawGrades.length) {
|
|
191
|
+
console.error(
|
|
192
|
+
`Invalid --grades value: ${String(args.grades)}. Use 4, 5, or 4,5.`
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
184
196
|
const subjectFilter = args.subjects
|
|
185
197
|
? String(args.subjects)
|
|
186
198
|
.split(",")
|
|
@@ -219,6 +231,17 @@ async function main() {
|
|
|
219
231
|
loadJson<SubjectFile>(join(PACK_DIR, "subjects", f))
|
|
220
232
|
);
|
|
221
233
|
|
|
234
|
+
if (subjectFilter) {
|
|
235
|
+
const knownKeys = new Set(subjects.map((s) => s.key));
|
|
236
|
+
const unknown = subjectFilter.filter((key) => !knownKeys.has(key));
|
|
237
|
+
if (unknown.length > 0) {
|
|
238
|
+
console.error(
|
|
239
|
+
`Unknown --subjects value(s): ${unknown.join(", ")}. Known: ${[...knownKeys].join(", ")}`
|
|
240
|
+
);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
222
245
|
const units = subjects
|
|
223
246
|
.filter((s) => !subjectFilter || subjectFilter.includes(s.key))
|
|
224
247
|
.flatMap((s) => s.units)
|
package/src/models/attendance.ts
CHANGED
|
@@ -370,18 +370,16 @@ function buildRecordsFromLists(input: {
|
|
|
370
370
|
status: AttendanceStatus;
|
|
371
371
|
updatedById?: string | null;
|
|
372
372
|
}> = [];
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
records.push({ studentId, status
|
|
378
|
-
}
|
|
379
|
-
for (const studentId of input.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
for (const studentId of input.excusedIds)
|
|
383
|
-
records.push({ studentId, status: "EXCUSED", updatedById: input.updatedById });
|
|
384
|
-
}
|
|
373
|
+
const seen = new Set<string>();
|
|
374
|
+
const pushUnique = (studentId: string, status: AttendanceStatus) => {
|
|
375
|
+
if (seen.has(studentId)) return;
|
|
376
|
+
seen.add(studentId);
|
|
377
|
+
records.push({ studentId, status, updatedById: input.updatedById });
|
|
378
|
+
};
|
|
379
|
+
for (const studentId of input.presentIds) pushUnique(studentId, "PRESENT");
|
|
380
|
+
for (const studentId of input.lateIds) pushUnique(studentId, "LATE");
|
|
381
|
+
for (const studentId of input.absentIds) pushUnique(studentId, "ABSENT");
|
|
382
|
+
for (const studentId of input.excusedIds) pushUnique(studentId, "EXCUSED");
|
|
385
383
|
return records;
|
|
386
384
|
}
|
|
387
385
|
|
|
@@ -71,10 +71,13 @@ export async function searchMarketplaceListings(input: {
|
|
|
71
71
|
limit: number;
|
|
72
72
|
/** When true, include the caller's own hidden listings (for authors). */
|
|
73
73
|
authorId?: string;
|
|
74
|
+
/** When set, restrict the entire result set (including PUBLIC listings) to this school. */
|
|
75
|
+
scopeSchoolId?: string;
|
|
74
76
|
}) {
|
|
75
77
|
const search = input.search?.trim();
|
|
76
78
|
const where: Prisma.MarketplaceListingWhereInput = {
|
|
77
79
|
AND: [
|
|
80
|
+
...(input.scopeSchoolId ? [{ schoolId: input.scopeSchoolId }] : []),
|
|
78
81
|
{
|
|
79
82
|
OR: [
|
|
80
83
|
{ visibility: "PUBLIC", isHidden: false },
|
|
@@ -132,6 +135,22 @@ export async function searchMarketplaceListings(input: {
|
|
|
132
135
|
return { items: rows, nextCursor };
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
export async function updateMarketplaceListing(
|
|
139
|
+
id: string,
|
|
140
|
+
data: {
|
|
141
|
+
title?: string;
|
|
142
|
+
description?: string | null;
|
|
143
|
+
visibility?: MarketplaceVisibility;
|
|
144
|
+
tags?: string[];
|
|
145
|
+
}
|
|
146
|
+
) {
|
|
147
|
+
return prisma.marketplaceListing.update({
|
|
148
|
+
where: { id },
|
|
149
|
+
data,
|
|
150
|
+
include: marketplaceListingInclude,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
export async function incrementMarketplaceReportCount(id: string) {
|
|
136
155
|
return prisma.marketplaceListing.update({
|
|
137
156
|
where: { id },
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module model – Canvas-style class modules, items and student progress.
|
|
3
|
+
*/
|
|
4
|
+
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import { classTeacherOrSchoolAdminWhere } from "./access.js";
|
|
6
|
+
|
|
7
|
+
/** @returns Class if user is a teacher or school admin. */
|
|
8
|
+
export function findClassWithTeacher(classId: string, teacherId: string) {
|
|
9
|
+
return prisma.class.findFirst({
|
|
10
|
+
where: {
|
|
11
|
+
id: classId,
|
|
12
|
+
...classTeacherOrSchoolAdminWhere(teacherId),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const moduleInclude = {
|
|
18
|
+
items: {
|
|
19
|
+
orderBy: { position: "asc" as const },
|
|
20
|
+
},
|
|
21
|
+
prerequisites: {
|
|
22
|
+
select: { prerequisiteId: true },
|
|
23
|
+
},
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/** @returns All modules of a class ordered by position. */
|
|
27
|
+
export function findModulesByClassId(classId: string, publishedOnly = false) {
|
|
28
|
+
return prisma.module.findMany({
|
|
29
|
+
where: { classId, ...(publishedOnly && { published: true }) },
|
|
30
|
+
orderBy: { position: "asc" },
|
|
31
|
+
include: moduleInclude,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns Module by ID scoped to a class. */
|
|
36
|
+
export function findModuleByClassAndId(id: string, classId: string) {
|
|
37
|
+
return prisma.module.findFirst({
|
|
38
|
+
where: { id, classId },
|
|
39
|
+
include: moduleInclude,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @returns Module item with its module, scoped to a class. */
|
|
44
|
+
export function findModuleItemInClass(itemId: string, classId: string) {
|
|
45
|
+
return prisma.courseModuleItem.findFirst({
|
|
46
|
+
where: { id: itemId, module: { classId } },
|
|
47
|
+
include: { module: true },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @returns Progress rows of a student for all items of a class. */
|
|
52
|
+
export function findStudentProgressForClass(classId: string, studentId: string) {
|
|
53
|
+
return prisma.moduleItemProgress.findMany({
|
|
54
|
+
where: { studentId, moduleItem: { module: { classId } } },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Upsert a progress row for a student and item. */
|
|
59
|
+
export function upsertItemProgress(
|
|
60
|
+
moduleItemId: string,
|
|
61
|
+
studentId: string,
|
|
62
|
+
status: "INCOMPLETE" | "COMPLETE"
|
|
63
|
+
) {
|
|
64
|
+
return prisma.moduleItemProgress.upsert({
|
|
65
|
+
where: { moduleItemId_studentId: { moduleItemId, studentId } },
|
|
66
|
+
create: {
|
|
67
|
+
moduleItemId,
|
|
68
|
+
studentId,
|
|
69
|
+
status,
|
|
70
|
+
...(status === "COMPLETE" && { completedAt: new Date() }),
|
|
71
|
+
},
|
|
72
|
+
update:
|
|
73
|
+
status === "COMPLETE"
|
|
74
|
+
? { status, completedAt: new Date() }
|
|
75
|
+
: {},
|
|
76
|
+
});
|
|
77
|
+
}
|
package/src/models/moduleItem.ts
CHANGED
|
@@ -96,12 +96,30 @@ export function deleteModuleItem(id: string) {
|
|
|
96
96
|
return prisma.moduleItem.delete({ where: { id } });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function fileAccessibleToUserWhere(userId: string) {
|
|
100
|
+
return {
|
|
101
|
+
OR: [
|
|
102
|
+
{ userId },
|
|
103
|
+
{ folder: { personalOwnerId: userId } },
|
|
104
|
+
{ folder: { class: { is: classTeacherOrSchoolAdminWhere(userId) } } },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function findAccessibleFilesByIds(fileIds: string[], userId: string) {
|
|
110
|
+
return prisma.file.findMany({
|
|
111
|
+
where: { id: { in: fileIds }, ...fileAccessibleToUserWhere(userId) },
|
|
112
|
+
select: { id: true, type: true },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
export function attachFilesToModuleItem(
|
|
100
117
|
moduleItemId: string,
|
|
101
|
-
fileIds: string[]
|
|
118
|
+
fileIds: string[],
|
|
119
|
+
userId: string
|
|
102
120
|
) {
|
|
103
121
|
return prisma.file.updateMany({
|
|
104
|
-
where: { id: { in: fileIds } },
|
|
122
|
+
where: { id: { in: fileIds }, ...fileAccessibleToUserWhere(userId) },
|
|
105
123
|
data: { moduleItemId },
|
|
106
124
|
});
|
|
107
125
|
}
|
|
@@ -9,6 +9,7 @@ import z from "zod";
|
|
|
9
9
|
import { inference } from "../utils/inference.js";
|
|
10
10
|
import { getAIUserId } from "../utils/aiUser.js";
|
|
11
11
|
import { pusher, worksheetChannel } from "../lib/pusher.js";
|
|
12
|
+
import { syncWorksheetModuleProgress } from "../services/module.js";
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const removeAllPreviousAIComments = async (worksheetQuestionProgressId: string) => {
|
|
@@ -369,6 +370,11 @@ export const gradeWorksheetPipeline = async (
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
};
|
|
373
|
+
|
|
374
|
+
await syncWorksheetModuleProgress(
|
|
375
|
+
worksheetResponse.studentId,
|
|
376
|
+
worksheetResponse.worksheetId,
|
|
377
|
+
);
|
|
372
378
|
};
|
|
373
379
|
|
|
374
380
|
export const cancelGradePipeline = async (worksheetResponseId: string, worksheetQuestionProgressId: string) => {
|
package/src/routers/_app.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { permissionsRouter } from "./permissions.js";
|
|
|
29
29
|
import { discussionRouter } from "./discussion.js";
|
|
30
30
|
import { adminRouter } from "./admin.js";
|
|
31
31
|
import { embeddingRouter } from "./embedding.js";
|
|
32
|
+
import { moduleRouter } from "./module.js";
|
|
32
33
|
import { gradebookRouter } from "./gradebook.js";
|
|
33
34
|
import { feesRouter } from "./fees.js";
|
|
34
35
|
import { marketplaceRouter } from "./marketplace.js";
|
|
@@ -65,6 +66,7 @@ export const appRouter = createTRPCRouter({
|
|
|
65
66
|
discussion: discussionRouter,
|
|
66
67
|
admin: adminRouter,
|
|
67
68
|
embedding: embeddingRouter,
|
|
69
|
+
module: moduleRouter,
|
|
68
70
|
gradebook: gradebookRouter,
|
|
69
71
|
fees: feesRouter,
|
|
70
72
|
marketplace: marketplaceRouter,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
publishWorksheetListing,
|
|
16
16
|
reportMarketplaceListing,
|
|
17
17
|
unpublishMarketplaceListing,
|
|
18
|
+
updateMarketplaceListingDetails,
|
|
18
19
|
} from "../services/marketplace.js";
|
|
19
20
|
|
|
20
21
|
const listingTypeSchema = z.enum(["MARKSCHEME", "WORKSHEET", "FILE"]);
|
|
@@ -109,6 +110,21 @@ export const marketplaceRouter = createTRPCRouter({
|
|
|
109
110
|
importMarketplaceListing(ctx.user!.id, input)
|
|
110
111
|
),
|
|
111
112
|
|
|
113
|
+
/** Edit listing metadata (author or school admin). */
|
|
114
|
+
update: protectedProcedure
|
|
115
|
+
.input(
|
|
116
|
+
z.object({
|
|
117
|
+
listingId: z.string(),
|
|
118
|
+
title: z.string().trim().min(1).max(160).optional(),
|
|
119
|
+
description: z.string().trim().max(2000).nullable().optional(),
|
|
120
|
+
visibility: visibilitySchema.optional(),
|
|
121
|
+
tags: tagsSchema,
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
.mutation(({ ctx, input }) =>
|
|
125
|
+
updateMarketplaceListingDetails(ctx.user!.id, input)
|
|
126
|
+
),
|
|
127
|
+
|
|
112
128
|
report: protectedProcedure
|
|
113
129
|
.input(z.object({ listingId: z.string() }))
|
|
114
130
|
.mutation(({ ctx, input }) =>
|