@studious-lms/server 1.20.0 → 1.22.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.
Files changed (117) hide show
  1. package/Dockerfile +5 -2
  2. package/dist/models/agenda.d.ts +6 -0
  3. package/dist/models/agenda.d.ts.map +1 -1
  4. package/dist/models/announcement.d.ts +2 -0
  5. package/dist/models/announcement.d.ts.map +1 -1
  6. package/dist/models/assignment.d.ts +27 -0
  7. package/dist/models/assignment.d.ts.map +1 -1
  8. package/dist/models/auth.d.ts +4 -0
  9. package/dist/models/auth.d.ts.map +1 -1
  10. package/dist/models/class.d.ts +14 -0
  11. package/dist/models/class.d.ts.map +1 -1
  12. package/dist/models/event.d.ts +11 -0
  13. package/dist/models/event.d.ts.map +1 -1
  14. package/dist/models/file.d.ts +19 -0
  15. package/dist/models/file.d.ts.map +1 -1
  16. package/dist/models/folder.d.ts +4 -0
  17. package/dist/models/folder.d.ts.map +1 -1
  18. package/dist/models/gradebook.d.ts +180 -0
  19. package/dist/models/gradebook.d.ts.map +1 -0
  20. package/dist/models/gradebook.js +88 -0
  21. package/dist/models/gradebook.js.map +1 -0
  22. package/dist/models/labChat.d.ts +4 -0
  23. package/dist/models/labChat.d.ts.map +1 -1
  24. package/dist/models/newtonChat.d.ts +2 -0
  25. package/dist/models/newtonChat.d.ts.map +1 -1
  26. package/dist/models/school.d.ts +2 -0
  27. package/dist/models/school.d.ts.map +1 -1
  28. package/dist/models/section.d.ts +3 -0
  29. package/dist/models/section.d.ts.map +1 -1
  30. package/dist/models/worksheet.d.ts +6 -0
  31. package/dist/models/worksheet.d.ts.map +1 -1
  32. package/dist/routers/_app.d.ts +1359 -37
  33. package/dist/routers/_app.d.ts.map +1 -1
  34. package/dist/routers/_app.js +4 -2
  35. package/dist/routers/_app.js.map +1 -1
  36. package/dist/routers/admin.d.ts +164 -0
  37. package/dist/routers/admin.d.ts.map +1 -1
  38. package/dist/routers/admin.js +23 -4
  39. package/dist/routers/admin.js.map +1 -1
  40. package/dist/routers/agenda.d.ts +6 -0
  41. package/dist/routers/agenda.d.ts.map +1 -1
  42. package/dist/routers/assignment.d.ts +102 -1
  43. package/dist/routers/assignment.d.ts.map +1 -1
  44. package/dist/routers/assignment.js +180 -2
  45. package/dist/routers/assignment.js.map +1 -1
  46. package/dist/routers/class.d.ts +17 -0
  47. package/dist/routers/class.d.ts.map +1 -1
  48. package/dist/routers/event.d.ts +4 -0
  49. package/dist/routers/event.d.ts.map +1 -1
  50. package/dist/routers/file.d.ts +66 -0
  51. package/dist/routers/file.d.ts.map +1 -1
  52. package/dist/routers/file.js +111 -2
  53. package/dist/routers/file.js.map +1 -1
  54. package/dist/routers/gradebook.d.ts +299 -0
  55. package/dist/routers/gradebook.d.ts.map +1 -0
  56. package/dist/routers/gradebook.js +107 -0
  57. package/dist/routers/gradebook.js.map +1 -0
  58. package/dist/routers/school.d.ts +4 -0
  59. package/dist/routers/school.d.ts.map +1 -1
  60. package/dist/routers/section.d.ts +1 -1
  61. package/dist/routers/worksheet.d.ts +3 -1
  62. package/dist/routers/worksheet.d.ts.map +1 -1
  63. package/dist/services/adminInstitution.d.ts +51 -0
  64. package/dist/services/adminInstitution.d.ts.map +1 -1
  65. package/dist/services/adminInstitution.js +52 -2
  66. package/dist/services/adminInstitution.js.map +1 -1
  67. package/dist/services/adminMetrics.d.ts +17 -0
  68. package/dist/services/adminMetrics.d.ts.map +1 -0
  69. package/dist/services/adminMetrics.js +37 -0
  70. package/dist/services/adminMetrics.js.map +1 -0
  71. package/dist/services/adminRunJob.d.ts +9 -0
  72. package/dist/services/adminRunJob.d.ts.map +1 -1
  73. package/dist/services/adminRunJob.js +18 -2
  74. package/dist/services/adminRunJob.js.map +1 -1
  75. package/dist/services/adminUser.d.ts +67 -0
  76. package/dist/services/adminUser.d.ts.map +1 -0
  77. package/dist/services/adminUser.js +87 -0
  78. package/dist/services/adminUser.js.map +1 -0
  79. package/dist/services/agenda.d.ts +6 -0
  80. package/dist/services/agenda.d.ts.map +1 -1
  81. package/dist/services/assignment.d.ts +27 -0
  82. package/dist/services/assignment.d.ts.map +1 -1
  83. package/dist/services/class.d.ts +17 -0
  84. package/dist/services/class.d.ts.map +1 -1
  85. package/dist/services/event.d.ts +4 -0
  86. package/dist/services/event.d.ts.map +1 -1
  87. package/dist/services/gradeCalculation.d.ts +70 -0
  88. package/dist/services/gradeCalculation.d.ts.map +1 -0
  89. package/dist/services/gradeCalculation.js +148 -0
  90. package/dist/services/gradeCalculation.js.map +1 -0
  91. package/dist/services/gradebook.d.ts +197 -0
  92. package/dist/services/gradebook.d.ts.map +1 -0
  93. package/dist/services/gradebook.js +263 -0
  94. package/dist/services/gradebook.js.map +1 -0
  95. package/dist/services/school.d.ts +4 -0
  96. package/dist/services/school.d.ts.map +1 -1
  97. package/dist/services/worksheet.d.ts +2 -0
  98. package/dist/services/worksheet.d.ts.map +1 -1
  99. package/docker-entrypoint.sh +7 -0
  100. package/package.json +1 -1
  101. package/prisma/migrations/20260708011456_add_file_annotations/migration.sql +35 -0
  102. package/prisma/migrations/20260708011629_add_gradebook_categories_columns_policies/migration.sql +78 -0
  103. package/prisma/migrations/20260723142314_make_file_annotation_submission_optional/migration.sql +2 -0
  104. package/prisma/schema.prisma +130 -38
  105. package/src/models/gradebook.ts +91 -0
  106. package/src/routers/_app.ts +2 -0
  107. package/src/routers/admin.ts +28 -0
  108. package/src/routers/assignment.ts +204 -0
  109. package/src/routers/file.ts +128 -0
  110. package/src/routers/gradebook.ts +173 -0
  111. package/src/services/adminInstitution.ts +52 -0
  112. package/src/services/adminMetrics.ts +46 -0
  113. package/src/services/adminRunJob.ts +24 -0
  114. package/src/services/adminUser.ts +93 -0
  115. package/src/services/gradeCalculation.ts +253 -0
  116. package/src/services/gradebook.ts +383 -0
  117. package/tests/unit/services/gradeCalculation.test.ts +206 -0
@@ -0,0 +1,78 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "GradeRoundingMode" AS ENUM ('NONE', 'HALF_UP', 'FLOOR', 'CEILING');
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "Assignment" ADD COLUMN "gradeCategoryId" TEXT;
6
+
7
+ -- AlterTable
8
+ ALTER TABLE "Class" ADD COLUMN "gradeRoundingMode" "GradeRoundingMode" NOT NULL DEFAULT 'NONE',
9
+ ADD COLUMN "treatMissingAsZero" BOOLEAN NOT NULL DEFAULT false;
10
+
11
+ -- CreateTable
12
+ CREATE TABLE "GradeCategory" (
13
+ "id" TEXT NOT NULL,
14
+ "classId" TEXT NOT NULL,
15
+ "name" TEXT NOT NULL,
16
+ "weight" DOUBLE PRECISION NOT NULL,
17
+ "dropLowest" INTEGER NOT NULL DEFAULT 0,
18
+ "position" INTEGER NOT NULL DEFAULT 0,
19
+
20
+ CONSTRAINT "GradeCategory_pkey" PRIMARY KEY ("id")
21
+ );
22
+
23
+ -- CreateTable
24
+ CREATE TABLE "CustomGradeColumn" (
25
+ "id" TEXT NOT NULL,
26
+ "classId" TEXT NOT NULL,
27
+ "gradeCategoryId" TEXT,
28
+ "name" TEXT NOT NULL,
29
+ "maxPoints" DOUBLE PRECISION NOT NULL,
30
+ "position" INTEGER NOT NULL DEFAULT 0,
31
+
32
+ CONSTRAINT "CustomGradeColumn_pkey" PRIMARY KEY ("id")
33
+ );
34
+
35
+ -- CreateTable
36
+ CREATE TABLE "CustomGradeEntry" (
37
+ "id" TEXT NOT NULL,
38
+ "columnId" TEXT NOT NULL,
39
+ "studentId" TEXT NOT NULL,
40
+ "points" DOUBLE PRECISION NOT NULL,
41
+ "note" TEXT,
42
+
43
+ CONSTRAINT "CustomGradeEntry_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateIndex
47
+ CREATE INDEX "GradeCategory_classId_idx" ON "GradeCategory"("classId");
48
+
49
+ -- CreateIndex
50
+ CREATE INDEX "CustomGradeColumn_classId_idx" ON "CustomGradeColumn"("classId");
51
+
52
+ -- CreateIndex
53
+ CREATE INDEX "CustomGradeColumn_gradeCategoryId_idx" ON "CustomGradeColumn"("gradeCategoryId");
54
+
55
+ -- CreateIndex
56
+ CREATE INDEX "CustomGradeEntry_studentId_idx" ON "CustomGradeEntry"("studentId");
57
+
58
+ -- CreateIndex
59
+ CREATE UNIQUE INDEX "CustomGradeEntry_columnId_studentId_key" ON "CustomGradeEntry"("columnId", "studentId");
60
+
61
+ -- AddForeignKey
62
+ ALTER TABLE "GradeCategory" ADD CONSTRAINT "GradeCategory_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
63
+
64
+ -- AddForeignKey
65
+ ALTER TABLE "CustomGradeColumn" ADD CONSTRAINT "CustomGradeColumn_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
66
+
67
+ -- AddForeignKey
68
+ ALTER TABLE "CustomGradeColumn" ADD CONSTRAINT "CustomGradeColumn_gradeCategoryId_fkey" FOREIGN KEY ("gradeCategoryId") REFERENCES "GradeCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
69
+
70
+ -- AddForeignKey
71
+ ALTER TABLE "CustomGradeEntry" ADD CONSTRAINT "CustomGradeEntry_columnId_fkey" FOREIGN KEY ("columnId") REFERENCES "CustomGradeColumn"("id") ON DELETE CASCADE ON UPDATE CASCADE;
72
+
73
+ -- AddForeignKey
74
+ ALTER TABLE "CustomGradeEntry" ADD CONSTRAINT "CustomGradeEntry_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
75
+
76
+ -- AddForeignKey
77
+ ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_gradeCategoryId_fkey" FOREIGN KEY ("gradeCategoryId") REFERENCES "GradeCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
78
+
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE "FileAnnotation" ALTER COLUMN "submissionId" DROP NOT NULL;
@@ -128,14 +128,14 @@ model School {
128
128
  createdAt DateTime @default(now())
129
129
  updatedAt DateTime @updatedAt
130
130
 
131
- users User[]
132
- memberships SchoolMembership[]
133
- invites SchoolInvite[]
134
- classes Class[]
135
- departments Department[]
136
- tags Tag[]
137
- storageRoot Folder? @relation("SchoolStorageRoot")
138
- permissions Permission[] @relation("SchoolPermissions")
131
+ users User[]
132
+ memberships SchoolMembership[]
133
+ invites SchoolInvite[]
134
+ classes Class[]
135
+ departments Department[]
136
+ tags Tag[]
137
+ storageRoot Folder? @relation("SchoolStorageRoot")
138
+ permissions Permission[] @relation("SchoolPermissions")
139
139
  }
140
140
 
141
141
  model Department {
@@ -175,13 +175,13 @@ model Tag {
175
175
  createdAt DateTime @default(now())
176
176
  updatedAt DateTime @updatedAt
177
177
 
178
- school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
179
- department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
180
- users User[] @relation("UserTags")
181
- classes Class[] @relation("ClassTags")
182
- rubrics MarkScheme[] @relation("MarkSchemeTags")
183
- boundaries GradingBoundary[] @relation("GradingBoundaryTags")
184
- permissions Permission[] @relation("TagPermissions")
178
+ school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
179
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
180
+ users User[] @relation("UserTags")
181
+ classes Class[] @relation("ClassTags")
182
+ rubrics MarkScheme[] @relation("MarkSchemeTags")
183
+ boundaries GradingBoundary[] @relation("GradingBoundaryTags")
184
+ permissions Permission[] @relation("TagPermissions")
185
185
 
186
186
  @@unique([schoolId, name])
187
187
  @@index([schoolId])
@@ -197,7 +197,7 @@ model User {
197
197
  verified Boolean @default(false)
198
198
 
199
199
  termsAcceptedAt DateTime?
200
- termsVersion String?
200
+ termsVersion String?
201
201
 
202
202
  teacherIn Class[] @relation("UserTeacherToClass")
203
203
  studentIn Class[] @relation("UserStudentToClass")
@@ -241,6 +241,8 @@ model User {
241
241
  discussions Discussion[]
242
242
  discussionReplies DiscussionReply[]
243
243
  discussionReads DiscussionRead[]
244
+ customGradeEntries CustomGradeEntry[] @relation("UserCustomGradeEntries")
245
+ fileAnnotations FileAnnotation[] @relation("AuthoredFileAnnotations")
244
246
  }
245
247
 
246
248
  model SchoolMembership {
@@ -330,14 +332,71 @@ model Class {
330
332
  classTags ClassTag[]
331
333
  embeddings Embedding[]
332
334
 
333
- discussionsEnabled Boolean @default(true)
334
- studentsCanCreateDiscussions Boolean @default(true)
335
- studentsCanReply Boolean @default(true)
335
+ discussionsEnabled Boolean @default(true)
336
+ studentsCanCreateDiscussions Boolean @default(true)
337
+ studentsCanReply Boolean @default(true)
338
+
339
+ gradeCategories GradeCategory[]
340
+ customGradeColumns CustomGradeColumn[]
341
+
342
+ treatMissingAsZero Boolean @default(false)
343
+ gradeRoundingMode GradeRoundingMode @default(NONE)
336
344
 
337
345
  @@index([schoolId])
338
346
  @@index([departmentId])
339
347
  }
340
348
 
349
+ enum GradeRoundingMode {
350
+ NONE
351
+ HALF_UP
352
+ FLOOR
353
+ CEILING
354
+ }
355
+
356
+ model GradeCategory {
357
+ id String @id @default(uuid())
358
+ classId String
359
+ class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
360
+ name String
361
+ weight Float
362
+ dropLowest Int @default(0)
363
+ position Int @default(0)
364
+
365
+ assignments Assignment[]
366
+ customColumns CustomGradeColumn[]
367
+
368
+ @@index([classId])
369
+ }
370
+
371
+ model CustomGradeColumn {
372
+ id String @id @default(uuid())
373
+ classId String
374
+ class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
375
+ gradeCategoryId String?
376
+ gradeCategory GradeCategory? @relation(fields: [gradeCategoryId], references: [id], onDelete: SetNull)
377
+ name String
378
+ maxPoints Float
379
+ position Int @default(0)
380
+
381
+ entries CustomGradeEntry[]
382
+
383
+ @@index([classId])
384
+ @@index([gradeCategoryId])
385
+ }
386
+
387
+ model CustomGradeEntry {
388
+ id String @id @default(uuid())
389
+ columnId String
390
+ column CustomGradeColumn @relation(fields: [columnId], references: [id], onDelete: Cascade)
391
+ studentId String
392
+ student User @relation("UserCustomGradeEntries", fields: [studentId], references: [id], onDelete: Cascade)
393
+ points Float
394
+ note String?
395
+
396
+ @@unique([columnId, studentId])
397
+ @@index([studentId])
398
+ }
399
+
341
400
  model MarkScheme {
342
401
  id String @id @default(uuid())
343
402
  classId String?
@@ -454,6 +513,35 @@ model File {
454
513
 
455
514
  schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
456
515
  schoolDevelopementProgramId String?
516
+
517
+ fileAnnotations FileAnnotation[] @relation("FileAnnotations")
518
+ }
519
+
520
+ enum FileAnnotationType {
521
+ HIGHLIGHT
522
+ INK
523
+ COMMENT
524
+ TEXT
525
+ }
526
+
527
+ model FileAnnotation {
528
+ id String @id @default(uuid())
529
+ file File @relation("FileAnnotations", fields: [fileId], references: [id], onDelete: Cascade)
530
+ fileId String
531
+ submission Submission? @relation("SubmissionFileAnnotations", fields: [submissionId], references: [id], onDelete: Cascade)
532
+ submissionId String?
533
+ author User @relation("AuthoredFileAnnotations", fields: [authorId], references: [id], onDelete: Cascade)
534
+ authorId String
535
+ page Int
536
+ type FileAnnotationType
537
+ geometry Json
538
+ text String?
539
+ color String?
540
+ createdAt DateTime @default(now())
541
+ updatedAt DateTime @updatedAt
542
+
543
+ @@index([fileId])
544
+ @@index([submissionId])
457
545
  }
458
546
 
459
547
  model Assignment {
@@ -493,6 +581,8 @@ model Assignment {
493
581
  order Int?
494
582
  gradingBoundary GradingBoundary? @relation(fields: [gradingBoundaryId], references: [id], onDelete: Cascade)
495
583
  gradingBoundaryId String?
584
+ gradeCategory GradeCategory? @relation(fields: [gradeCategoryId], references: [id], onDelete: SetNull)
585
+ gradeCategoryId String?
496
586
  worksheets Worksheet[] @relation("AssignmentWorksheets")
497
587
  targetedBySubmissions Submission[] @relation("SubmissionTargetedAssignment")
498
588
  }
@@ -607,19 +697,19 @@ model DiscussionRead {
607
697
  }
608
698
 
609
699
  model Reaction {
610
- id String @id @default(uuid())
611
- type ReactionType
612
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
613
- userId String
614
- announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
615
- announcementId String?
616
- comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
617
- commentId String?
700
+ id String @id @default(uuid())
701
+ type ReactionType
702
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
703
+ userId String
704
+ announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
705
+ announcementId String?
706
+ comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
707
+ commentId String?
618
708
  discussion Discussion? @relation(fields: [discussionId], references: [id], onDelete: Cascade)
619
709
  discussionId String?
620
710
  discussionReply DiscussionReply? @relation(fields: [discussionReplyId], references: [id], onDelete: Cascade)
621
711
  discussionReplyId String?
622
- createdAt DateTime @default(now())
712
+ createdAt DateTime @default(now())
623
713
 
624
714
  @@unique([userId, announcementId])
625
715
  @@unique([userId, commentId])
@@ -661,6 +751,8 @@ model Submission {
661
751
  returned Boolean? @default(false)
662
752
  newtonTutors NewtonChat[]
663
753
 
754
+ fileAnnotations FileAnnotation[] @relation("SubmissionFileAnnotations")
755
+
664
756
  @@index([studentId, recommendationState])
665
757
  }
666
758
 
@@ -890,11 +982,11 @@ model Worksheet {
890
982
  questions WorksheetQuestion[]
891
983
  studentWorksheetResponses StudentWorksheetResponse[]
892
984
 
893
- runJobId String?
894
- runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
985
+ runJobId String?
986
+ runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
895
987
 
896
- status JobStatus? @default(NOT_STARTED)
897
- tags ClassTag[] @relation("WorksheetToTag")
988
+ status JobStatus? @default(NOT_STARTED)
989
+ tags ClassTag[] @relation("WorksheetToTag")
898
990
 
899
991
  @@unique([classId, sourceUrl])
900
992
  }
@@ -1011,14 +1103,14 @@ enum EmbeddingSourceType {
1011
1103
  }
1012
1104
 
1013
1105
  model Embedding {
1014
- id String @id @default(uuid())
1106
+ id String @id @default(uuid())
1015
1107
  classId String
1016
1108
  sourceType EmbeddingSourceType
1017
1109
  sourceId String
1018
1110
  content String
1019
1111
  vector Unsupported("vector(1536)")
1020
- metadata Json? @default("{}")
1021
- createdAt DateTime @default(now())
1112
+ metadata Json? @default("{}")
1113
+ createdAt DateTime @default(now())
1022
1114
 
1023
1115
  class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
1024
1116
 
@@ -1062,7 +1154,7 @@ model RunJob {
1062
1154
  /// most jobs touch zero or one of these.
1063
1155
  messages Message[] @relation("MessageRunJob")
1064
1156
  studentQuestionProgresses StudentQuestionProgress[] @relation("StudentQuestionProgressRunJob")
1065
- worksheet Worksheet[] @relation("WorksheetRunJob")
1157
+ worksheet Worksheet[] @relation("WorksheetRunJob")
1066
1158
 
1067
1159
  @@index([userId, type, status])
1068
1160
  @@index([classId, status])
@@ -1098,9 +1190,9 @@ enum PermissionLevel {
1098
1190
  /// `schoolId` is set, matching `principalType`. CLASS, CLASS_TEACHERS,
1099
1191
  /// CLASS_STUDENTS all share `classId`.
1100
1192
  model Permission {
1101
- id String @id @default(uuid())
1193
+ id String @id @default(uuid())
1102
1194
  folderId String
1103
- folder Folder @relation("FolderPermissions", fields: [folderId], references: [id], onDelete: Cascade)
1195
+ folder Folder @relation("FolderPermissions", fields: [folderId], references: [id], onDelete: Cascade)
1104
1196
 
1105
1197
  principalType PermissionPrincipalType
1106
1198
  userId String?
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Gradebook model – grade categories, custom gradebook columns/entries,
3
+ * and the queries backing the computed gradebook.
4
+ */
5
+
6
+ import { prisma } from "../lib/prisma.js";
7
+
8
+ /** @returns Grade categories for a class, ordered by position. */
9
+ export function findGradeCategoriesByClassId(classId: string) {
10
+ return prisma.gradeCategory.findMany({
11
+ where: { classId },
12
+ orderBy: { position: "asc" },
13
+ });
14
+ }
15
+
16
+ /** @returns A grade category scoped to a class. */
17
+ export function findGradeCategory(categoryId: string, classId: string) {
18
+ return prisma.gradeCategory.findFirst({
19
+ where: { id: categoryId, classId },
20
+ });
21
+ }
22
+
23
+ /** @returns Custom gradebook columns for a class with their entries. */
24
+ export function findCustomGradeColumnsByClassId(classId: string) {
25
+ return prisma.customGradeColumn.findMany({
26
+ where: { classId },
27
+ orderBy: { position: "asc" },
28
+ include: {
29
+ entries: {
30
+ select: { id: true, studentId: true, points: true, note: true },
31
+ },
32
+ },
33
+ });
34
+ }
35
+
36
+ /** @returns A custom gradebook column scoped to a class. */
37
+ export function findCustomGradeColumn(columnId: string, classId: string) {
38
+ return prisma.customGradeColumn.findFirst({
39
+ where: { id: columnId, classId },
40
+ });
41
+ }
42
+
43
+ /** @returns Class data needed to compute the gradebook. */
44
+ export function findClassForGradebook(classId: string) {
45
+ return prisma.class.findUnique({
46
+ where: { id: classId },
47
+ select: {
48
+ id: true,
49
+ treatMissingAsZero: true,
50
+ gradeRoundingMode: true,
51
+ students: {
52
+ select: {
53
+ id: true,
54
+ username: true,
55
+ profile: { select: { displayName: true } },
56
+ },
57
+ },
58
+ gradingBoundaries: {
59
+ take: 1,
60
+ select: { id: true, name: true, structured: true },
61
+ },
62
+ assignments: {
63
+ where: { graded: true, template: false, inProgress: false },
64
+ orderBy: [{ order: "asc" }, { dueDate: "asc" }],
65
+ select: {
66
+ id: true,
67
+ title: true,
68
+ maxGrade: true,
69
+ weight: true,
70
+ gradeCategoryId: true,
71
+ dueDate: true,
72
+ submissions: {
73
+ select: { studentId: true, gradeReceived: true },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ });
79
+ }
80
+
81
+ /** @returns Grade policy fields for a class. */
82
+ export function findClassGradePolicies(classId: string) {
83
+ return prisma.class.findUnique({
84
+ where: { id: classId },
85
+ select: {
86
+ id: true,
87
+ treatMissingAsZero: true,
88
+ gradeRoundingMode: true,
89
+ },
90
+ });
91
+ }
@@ -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 { gradebookRouter } from "./gradebook.js";
32
33
 
33
34
  export const appRouter = createTRPCRouter({
34
35
  class: classRouter,
@@ -59,6 +60,7 @@ export const appRouter = createTRPCRouter({
59
60
  discussion: discussionRouter,
60
61
  admin: adminRouter,
61
62
  embedding: embeddingRouter,
63
+ gradebook: gradebookRouter,
62
64
  });
63
65
 
64
66
  // Export type router type definition
@@ -4,15 +4,21 @@ import { createTRPCRouter, adminProcedure } from '../trpc.js';
4
4
  import {
5
5
  getRunJobForAdmin,
6
6
  listRunJobsForAdmin,
7
+ retryRunJobForAdmin,
7
8
  } from '../services/adminRunJob.js';
8
9
  import {
10
+ getSchoolForAdmin,
9
11
  listDepartmentsForAdmin,
10
12
  listEarlyAccessRequestsForAdmin,
11
13
  listSchoolDevelopmentProgramsForAdmin,
12
14
  listSchoolsForAdmin,
13
15
  } from '../services/adminInstitution.js';
16
+ import { getOverviewMetricsForAdmin } from '../services/adminMetrics.js';
17
+ import { getUserForAdmin, searchUsersForAdmin } from '../services/adminUser.js';
14
18
 
15
19
  export const adminRouter = createTRPCRouter({
20
+ getOverviewMetrics: adminProcedure.query(() => getOverviewMetricsForAdmin()),
21
+
16
22
  listRunJobs: adminProcedure
17
23
  .input(
18
24
  z.object({
@@ -28,6 +34,14 @@ export const adminRouter = createTRPCRouter({
28
34
  .input(z.object({ id: z.string().uuid() }))
29
35
  .query(({ input }) => getRunJobForAdmin(input.id)),
30
36
 
37
+ retryRunJob: adminProcedure
38
+ .input(z.object({ id: z.string().uuid() }))
39
+ .mutation(({ input }) => retryRunJobForAdmin(input.id)),
40
+
41
+ getSchool: adminProcedure
42
+ .input(z.object({ id: z.string().uuid() }))
43
+ .query(({ input }) => getSchoolForAdmin(input.id)),
44
+
31
45
  listSchools: adminProcedure
32
46
  .input(
33
47
  z.object({
@@ -67,4 +81,18 @@ export const adminRouter = createTRPCRouter({
67
81
  }),
68
82
  )
69
83
  .query(({ input }) => listEarlyAccessRequestsForAdmin(input)),
84
+
85
+ searchUsers: adminProcedure
86
+ .input(
87
+ z.object({
88
+ page: z.number().int().min(0).default(0),
89
+ pageSize: z.number().int().min(1).max(100).default(25),
90
+ query: z.string().optional(),
91
+ }),
92
+ )
93
+ .query(({ input }) => searchUsersForAdmin(input)),
94
+
95
+ getUser: adminProcedure
96
+ .input(z.object({ id: z.string().uuid() }))
97
+ .query(({ input }) => getUserForAdmin(input.id)),
70
98
  });