@studious-lms/server 1.0.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 (103) hide show
  1. package/LICENSE.txt +8 -0
  2. package/README.md +143 -0
  3. package/dist/exportType.d.ts +9 -0
  4. package/dist/exportType.d.ts.map +1 -0
  5. package/dist/exportType.js +7 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +82 -0
  9. package/dist/lib/fileUpload.d.ts +38 -0
  10. package/dist/lib/fileUpload.d.ts.map +1 -0
  11. package/dist/lib/fileUpload.js +136 -0
  12. package/dist/lib/googleCloudStorage.d.ts +20 -0
  13. package/dist/lib/googleCloudStorage.d.ts.map +1 -0
  14. package/dist/lib/googleCloudStorage.js +88 -0
  15. package/dist/lib/prisma.d.ts +8 -0
  16. package/dist/lib/prisma.d.ts.map +1 -0
  17. package/dist/lib/prisma.js +11 -0
  18. package/dist/lib/thumbnailGenerator.d.ts +23 -0
  19. package/dist/lib/thumbnailGenerator.d.ts.map +1 -0
  20. package/dist/lib/thumbnailGenerator.js +180 -0
  21. package/dist/logger.d.ts +26 -0
  22. package/dist/logger.d.ts.map +1 -0
  23. package/dist/logger.js +135 -0
  24. package/dist/middleware/auth.d.ts +7 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +170 -0
  27. package/dist/middleware/logging.d.ts +2 -0
  28. package/dist/middleware/logging.d.ts.map +1 -0
  29. package/dist/middleware/logging.js +51 -0
  30. package/dist/routers/_app.d.ts +4369 -0
  31. package/dist/routers/_app.d.ts.map +1 -0
  32. package/dist/routers/_app.js +29 -0
  33. package/dist/routers/agenda.d.ts +66 -0
  34. package/dist/routers/agenda.d.ts.map +1 -0
  35. package/dist/routers/agenda.js +78 -0
  36. package/dist/routers/announcement.d.ts +79 -0
  37. package/dist/routers/announcement.d.ts.map +1 -0
  38. package/dist/routers/announcement.js +122 -0
  39. package/dist/routers/assignment.d.ts +1051 -0
  40. package/dist/routers/assignment.d.ts.map +1 -0
  41. package/dist/routers/assignment.js +1497 -0
  42. package/dist/routers/attendance.d.ts +99 -0
  43. package/dist/routers/attendance.d.ts.map +1 -0
  44. package/dist/routers/attendance.js +269 -0
  45. package/dist/routers/auth.d.ts +87 -0
  46. package/dist/routers/auth.d.ts.map +1 -0
  47. package/dist/routers/auth.js +255 -0
  48. package/dist/routers/class.d.ts +427 -0
  49. package/dist/routers/class.d.ts.map +1 -0
  50. package/dist/routers/class.js +693 -0
  51. package/dist/routers/event.d.ts +249 -0
  52. package/dist/routers/event.d.ts.map +1 -0
  53. package/dist/routers/event.js +467 -0
  54. package/dist/routers/file.d.ts +27 -0
  55. package/dist/routers/file.d.ts.map +1 -0
  56. package/dist/routers/file.js +88 -0
  57. package/dist/routers/section.d.ts +51 -0
  58. package/dist/routers/section.d.ts.map +1 -0
  59. package/dist/routers/section.js +123 -0
  60. package/dist/routers/user.d.ts +49 -0
  61. package/dist/routers/user.d.ts.map +1 -0
  62. package/dist/routers/user.js +74 -0
  63. package/dist/socket/handlers.d.ts +3 -0
  64. package/dist/socket/handlers.d.ts.map +1 -0
  65. package/dist/socket/handlers.js +130 -0
  66. package/dist/trpc.d.ts +147 -0
  67. package/dist/trpc.d.ts.map +1 -0
  68. package/dist/trpc.js +67 -0
  69. package/dist/types/trpc.d.ts +16 -0
  70. package/dist/types/trpc.d.ts.map +1 -0
  71. package/dist/types/trpc.js +2 -0
  72. package/dist/utils/email.d.ts +3 -0
  73. package/dist/utils/email.d.ts.map +1 -0
  74. package/dist/utils/email.js +16 -0
  75. package/dist/utils/generateInviteCode.d.ts +6 -0
  76. package/dist/utils/generateInviteCode.d.ts.map +1 -0
  77. package/dist/utils/generateInviteCode.js +11 -0
  78. package/dist/utils/logger.d.ts +27 -0
  79. package/dist/utils/logger.d.ts.map +1 -0
  80. package/dist/utils/logger.js +124 -0
  81. package/generated/prisma/client.d.ts +1 -0
  82. package/generated/prisma/client.js +4 -0
  83. package/generated/prisma/default.d.ts +1 -0
  84. package/generated/prisma/default.js +4 -0
  85. package/generated/prisma/edge.d.ts +1 -0
  86. package/generated/prisma/edge.js +389 -0
  87. package/generated/prisma/index-browser.js +375 -0
  88. package/generated/prisma/index.d.ts +34865 -0
  89. package/generated/prisma/index.js +410 -0
  90. package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  91. package/generated/prisma/package.json +140 -0
  92. package/generated/prisma/runtime/edge-esm.js +34 -0
  93. package/generated/prisma/runtime/edge.js +34 -0
  94. package/generated/prisma/runtime/index-browser.d.ts +370 -0
  95. package/generated/prisma/runtime/index-browser.js +16 -0
  96. package/generated/prisma/runtime/library.d.ts +3647 -0
  97. package/generated/prisma/runtime/library.js +146 -0
  98. package/generated/prisma/runtime/react-native.js +83 -0
  99. package/generated/prisma/runtime/wasm.js +35 -0
  100. package/generated/prisma/schema.prisma +304 -0
  101. package/generated/prisma/wasm.d.ts +1 -0
  102. package/generated/prisma/wasm.js +375 -0
  103. package/package.json +46 -0
@@ -0,0 +1,1497 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assignmentRouter = void 0;
4
+ const zod_1 = require("zod");
5
+ const trpc_1 = require("../trpc");
6
+ const server_1 = require("@trpc/server");
7
+ const prisma_1 = require("@lib/prisma");
8
+ const fileUpload_1 = require("@lib/fileUpload");
9
+ const googleCloudStorage_1 = require("@lib/googleCloudStorage");
10
+ const fileSchema = zod_1.z.object({
11
+ name: zod_1.z.string(),
12
+ type: zod_1.z.string(),
13
+ size: zod_1.z.number(),
14
+ data: zod_1.z.string(), // base64 encoded file data
15
+ });
16
+ const createAssignmentSchema = zod_1.z.object({
17
+ classId: zod_1.z.string(),
18
+ title: zod_1.z.string(),
19
+ instructions: zod_1.z.string(),
20
+ dueDate: zod_1.z.string(),
21
+ files: zod_1.z.array(fileSchema).optional(),
22
+ maxGrade: zod_1.z.number().optional(),
23
+ graded: zod_1.z.boolean().optional(),
24
+ weight: zod_1.z.number().optional(),
25
+ sectionId: zod_1.z.string().optional(),
26
+ type: zod_1.z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']).optional(),
27
+ markSchemeId: zod_1.z.string().optional(),
28
+ gradingBoundaryId: zod_1.z.string().optional(),
29
+ });
30
+ const updateAssignmentSchema = zod_1.z.object({
31
+ classId: zod_1.z.string(),
32
+ id: zod_1.z.string(),
33
+ title: zod_1.z.string().optional(),
34
+ instructions: zod_1.z.string().optional(),
35
+ dueDate: zod_1.z.string().optional(),
36
+ files: zod_1.z.array(fileSchema).optional(),
37
+ removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
38
+ maxGrade: zod_1.z.number().optional(),
39
+ graded: zod_1.z.boolean().optional(),
40
+ weight: zod_1.z.number().optional(),
41
+ sectionId: zod_1.z.string().nullable().optional(),
42
+ type: zod_1.z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']).optional(),
43
+ });
44
+ const deleteAssignmentSchema = zod_1.z.object({
45
+ id: zod_1.z.string(),
46
+ classId: zod_1.z.string(),
47
+ });
48
+ const getAssignmentSchema = zod_1.z.object({
49
+ id: zod_1.z.string(),
50
+ classId: zod_1.z.string(),
51
+ });
52
+ const submissionSchema = zod_1.z.object({
53
+ assignmentId: zod_1.z.string(),
54
+ classId: zod_1.z.string(),
55
+ submissionId: zod_1.z.string(),
56
+ submit: zod_1.z.boolean().optional(),
57
+ newAttachments: zod_1.z.array(fileSchema).optional(),
58
+ removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
59
+ });
60
+ const updateSubmissionSchema = zod_1.z.object({
61
+ assignmentId: zod_1.z.string(),
62
+ classId: zod_1.z.string(),
63
+ submissionId: zod_1.z.string(),
64
+ return: zod_1.z.boolean().optional(),
65
+ gradeReceived: zod_1.z.number().nullable().optional(),
66
+ newAttachments: zod_1.z.array(fileSchema).optional(),
67
+ removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
68
+ rubricGrades: zod_1.z.array(zod_1.z.object({
69
+ criteriaId: zod_1.z.string(),
70
+ selectedLevelId: zod_1.z.string(),
71
+ points: zod_1.z.number(),
72
+ comments: zod_1.z.string(),
73
+ })).optional(),
74
+ });
75
+ exports.assignmentRouter = (0, trpc_1.createTRPCRouter)({
76
+ create: trpc_1.protectedProcedure
77
+ .input(createAssignmentSchema)
78
+ .mutation(async ({ ctx, input }) => {
79
+ const { classId, title, instructions, dueDate, files, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId } = input;
80
+ if (!ctx.user) {
81
+ throw new server_1.TRPCError({
82
+ code: "UNAUTHORIZED",
83
+ message: "User must be authenticated",
84
+ });
85
+ }
86
+ // Get all students in the class
87
+ const classData = await prisma_1.prisma.class.findUnique({
88
+ where: { id: classId },
89
+ include: {
90
+ students: {
91
+ select: { id: true }
92
+ }
93
+ }
94
+ });
95
+ if (!classData) {
96
+ throw new server_1.TRPCError({
97
+ code: "NOT_FOUND",
98
+ message: "Class not found",
99
+ });
100
+ }
101
+ const rubric = await prisma_1.prisma.markScheme.findUnique({
102
+ where: { id: markSchemeId },
103
+ select: {
104
+ structured: true,
105
+ }
106
+ });
107
+ const parsedRubric = JSON.parse(rubric?.structured || "{}");
108
+ // Calculate max grade from rubric criteria levels
109
+ const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
110
+ const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
111
+ return acc + maxPoints;
112
+ }, 0);
113
+ // Create assignment with submissions for all students
114
+ const assignment = await prisma_1.prisma.assignment.create({
115
+ data: {
116
+ title,
117
+ instructions,
118
+ dueDate: new Date(dueDate),
119
+ maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
120
+ graded,
121
+ weight,
122
+ type,
123
+ class: {
124
+ connect: { id: classId }
125
+ },
126
+ ...(sectionId && {
127
+ section: {
128
+ connect: { id: sectionId }
129
+ }
130
+ }),
131
+ ...(markSchemeId && {
132
+ markScheme: {
133
+ connect: { id: markSchemeId }
134
+ }
135
+ }),
136
+ ...(gradingBoundaryId && {
137
+ gradingBoundary: {
138
+ connect: { id: gradingBoundaryId }
139
+ }
140
+ }),
141
+ submissions: {
142
+ create: classData.students.map((student) => ({
143
+ student: {
144
+ connect: { id: student.id }
145
+ }
146
+ }))
147
+ },
148
+ teacher: {
149
+ connect: { id: ctx.user.id }
150
+ }
151
+ },
152
+ select: {
153
+ id: true,
154
+ title: true,
155
+ instructions: true,
156
+ dueDate: true,
157
+ maxGrade: true,
158
+ graded: true,
159
+ weight: true,
160
+ type: true,
161
+ attachments: {
162
+ select: {
163
+ id: true,
164
+ name: true,
165
+ type: true,
166
+ }
167
+ },
168
+ section: {
169
+ select: {
170
+ id: true,
171
+ name: true
172
+ }
173
+ },
174
+ teacher: {
175
+ select: {
176
+ id: true,
177
+ username: true
178
+ }
179
+ },
180
+ class: {
181
+ select: {
182
+ id: true,
183
+ name: true
184
+ }
185
+ }
186
+ }
187
+ });
188
+ // Upload files if provided
189
+ let uploadedFiles = [];
190
+ if (files && files.length > 0) {
191
+ // Store files in a class and assignment specific directory
192
+ uploadedFiles = await (0, fileUpload_1.uploadFiles)(files, ctx.user.id, `class/${classId}/assignment/${assignment.id}`);
193
+ }
194
+ // Update assignment with new file attachments
195
+ if (uploadedFiles.length > 0) {
196
+ await prisma_1.prisma.assignment.update({
197
+ where: { id: assignment.id },
198
+ data: {
199
+ attachments: {
200
+ create: uploadedFiles.map(file => ({
201
+ name: file.name,
202
+ type: file.type,
203
+ size: file.size,
204
+ path: file.path,
205
+ ...(file.thumbnailId && {
206
+ thumbnail: {
207
+ connect: { id: file.thumbnailId }
208
+ }
209
+ })
210
+ }))
211
+ }
212
+ }
213
+ });
214
+ }
215
+ return assignment;
216
+ }),
217
+ update: trpc_1.protectedProcedure
218
+ .input(updateAssignmentSchema)
219
+ .mutation(async ({ ctx, input }) => {
220
+ const { id, title, instructions, dueDate, files, maxGrade, graded, weight, sectionId, type } = input;
221
+ if (!ctx.user) {
222
+ throw new server_1.TRPCError({
223
+ code: "UNAUTHORIZED",
224
+ message: "User must be authenticated",
225
+ });
226
+ }
227
+ // Get the assignment with current attachments
228
+ const assignment = await prisma_1.prisma.assignment.findFirst({
229
+ where: {
230
+ id,
231
+ teacherId: ctx.user.id,
232
+ },
233
+ include: {
234
+ attachments: {
235
+ select: {
236
+ id: true,
237
+ name: true,
238
+ type: true,
239
+ thumbnail: {
240
+ select: {
241
+ path: true
242
+ }
243
+ }
244
+ },
245
+ },
246
+ class: {
247
+ select: {
248
+ id: true,
249
+ name: true
250
+ }
251
+ },
252
+ },
253
+ });
254
+ if (!assignment) {
255
+ throw new server_1.TRPCError({
256
+ code: "NOT_FOUND",
257
+ message: "Assignment not found",
258
+ });
259
+ }
260
+ // Upload new files if provided
261
+ let uploadedFiles = [];
262
+ if (files && files.length > 0) {
263
+ // Store files in a class and assignment specific directory
264
+ uploadedFiles = await (0, fileUpload_1.uploadFiles)(files, ctx.user.id, `class/${assignment.classId}/assignment/${id}`);
265
+ }
266
+ // Update assignment
267
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
268
+ where: { id },
269
+ data: {
270
+ ...(title && { title }),
271
+ ...(instructions && { instructions }),
272
+ ...(dueDate && { dueDate: new Date(dueDate) }),
273
+ ...(maxGrade && { maxGrade }),
274
+ ...(graded !== undefined && { graded }),
275
+ ...(weight && { weight }),
276
+ ...(type && { type }),
277
+ ...(sectionId !== undefined && {
278
+ section: sectionId ? {
279
+ connect: { id: sectionId }
280
+ } : {
281
+ disconnect: true
282
+ }
283
+ }),
284
+ ...(uploadedFiles.length > 0 && {
285
+ attachments: {
286
+ create: uploadedFiles.map(file => ({
287
+ name: file.name,
288
+ type: file.type,
289
+ size: file.size,
290
+ path: file.path,
291
+ ...(file.thumbnailId && {
292
+ thumbnail: {
293
+ connect: { id: file.thumbnailId }
294
+ }
295
+ })
296
+ }))
297
+ }
298
+ }),
299
+ ...(input.removedAttachments && input.removedAttachments.length > 0 && {
300
+ attachments: {
301
+ deleteMany: {
302
+ id: { in: input.removedAttachments }
303
+ }
304
+ }
305
+ }),
306
+ },
307
+ select: {
308
+ id: true,
309
+ title: true,
310
+ instructions: true,
311
+ dueDate: true,
312
+ maxGrade: true,
313
+ graded: true,
314
+ weight: true,
315
+ type: true,
316
+ createdAt: true,
317
+ submissions: {
318
+ select: {
319
+ student: {
320
+ select: {
321
+ id: true,
322
+ username: true
323
+ }
324
+ }
325
+ }
326
+ },
327
+ attachments: {
328
+ select: {
329
+ id: true,
330
+ name: true,
331
+ type: true,
332
+ thumbnail: true
333
+ }
334
+ },
335
+ section: true,
336
+ teacher: true,
337
+ class: true
338
+ }
339
+ });
340
+ if (assignment.markSchemeId) {
341
+ const rubric = await prisma_1.prisma.markScheme.findUnique({
342
+ where: { id: assignment.markSchemeId },
343
+ select: {
344
+ structured: true,
345
+ }
346
+ });
347
+ const parsedRubric = JSON.parse(rubric?.structured || "{}");
348
+ const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
349
+ const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
350
+ return acc + maxPoints;
351
+ }, 0);
352
+ await prisma_1.prisma.assignment.update({
353
+ where: { id },
354
+ data: {
355
+ maxGrade: computedMaxGrade,
356
+ }
357
+ });
358
+ }
359
+ return updatedAssignment;
360
+ }),
361
+ delete: trpc_1.protectedProcedure
362
+ .input(deleteAssignmentSchema)
363
+ .mutation(async ({ ctx, input }) => {
364
+ const { id, classId } = input;
365
+ if (!ctx.user) {
366
+ throw new server_1.TRPCError({
367
+ code: "UNAUTHORIZED",
368
+ message: "User must be authenticated",
369
+ });
370
+ }
371
+ // Get the assignment with all related files
372
+ const assignment = await prisma_1.prisma.assignment.findFirst({
373
+ where: {
374
+ id,
375
+ teacherId: ctx.user.id,
376
+ },
377
+ include: {
378
+ attachments: {
379
+ include: {
380
+ thumbnail: true
381
+ }
382
+ },
383
+ submissions: {
384
+ include: {
385
+ attachments: {
386
+ include: {
387
+ thumbnail: true
388
+ }
389
+ },
390
+ annotations: {
391
+ include: {
392
+ thumbnail: true
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ });
399
+ if (!assignment) {
400
+ throw new server_1.TRPCError({
401
+ code: "NOT_FOUND",
402
+ message: "Assignment not found",
403
+ });
404
+ }
405
+ // Delete all files from storage
406
+ const filesToDelete = [
407
+ ...assignment.attachments,
408
+ ...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
409
+ ];
410
+ // Delete files from storage
411
+ await Promise.all(filesToDelete.map(async (file) => {
412
+ try {
413
+ // Delete the main file
414
+ await (0, googleCloudStorage_1.deleteFile)(file.path);
415
+ // Delete thumbnail if it exists
416
+ if (file.thumbnail) {
417
+ await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
418
+ }
419
+ }
420
+ catch (error) {
421
+ console.warn(`Failed to delete file ${file.path}:`, error);
422
+ }
423
+ }));
424
+ // Delete the assignment (this will cascade delete all related records)
425
+ await prisma_1.prisma.assignment.delete({
426
+ where: { id },
427
+ });
428
+ return {
429
+ id,
430
+ };
431
+ }),
432
+ get: trpc_1.protectedProcedure
433
+ .input(getAssignmentSchema)
434
+ .query(async ({ ctx, input }) => {
435
+ const { id, classId } = input;
436
+ if (!ctx.user) {
437
+ throw new server_1.TRPCError({
438
+ code: "UNAUTHORIZED",
439
+ message: "User must be authenticated",
440
+ });
441
+ }
442
+ const assignment = await prisma_1.prisma.assignment.findUnique({
443
+ where: {
444
+ id,
445
+ // classId,
446
+ },
447
+ include: {
448
+ submissions: {
449
+ select: {
450
+ student: {
451
+ select: {
452
+ id: true,
453
+ username: true
454
+ }
455
+ }
456
+ }
457
+ },
458
+ attachments: {
459
+ select: {
460
+ id: true,
461
+ name: true,
462
+ type: true,
463
+ size: true,
464
+ path: true,
465
+ thumbnailId: true,
466
+ }
467
+ },
468
+ section: {
469
+ select: {
470
+ id: true,
471
+ name: true,
472
+ }
473
+ },
474
+ teacher: {
475
+ select: {
476
+ id: true,
477
+ username: true
478
+ }
479
+ },
480
+ class: {
481
+ select: {
482
+ id: true,
483
+ name: true
484
+ }
485
+ },
486
+ eventAttached: {
487
+ select: {
488
+ id: true,
489
+ name: true,
490
+ startTime: true,
491
+ endTime: true,
492
+ location: true,
493
+ remarks: true,
494
+ }
495
+ },
496
+ markScheme: {
497
+ select: {
498
+ id: true,
499
+ structured: true,
500
+ }
501
+ },
502
+ gradingBoundary: {
503
+ select: {
504
+ id: true,
505
+ structured: true,
506
+ }
507
+ }
508
+ }
509
+ });
510
+ if (!assignment) {
511
+ throw new server_1.TRPCError({
512
+ code: "NOT_FOUND",
513
+ message: "Assignment not found",
514
+ });
515
+ }
516
+ const sections = await prisma_1.prisma.section.findMany({
517
+ where: {
518
+ classId: assignment.classId,
519
+ },
520
+ select: {
521
+ id: true,
522
+ name: true,
523
+ },
524
+ });
525
+ return { ...assignment, sections };
526
+ }),
527
+ getSubmission: trpc_1.protectedClassMemberProcedure
528
+ .input(zod_1.z.object({
529
+ assignmentId: zod_1.z.string(),
530
+ classId: zod_1.z.string(),
531
+ }))
532
+ .query(async ({ ctx, input }) => {
533
+ if (!ctx.user) {
534
+ throw new server_1.TRPCError({
535
+ code: "UNAUTHORIZED",
536
+ message: "User must be authenticated",
537
+ });
538
+ }
539
+ const { assignmentId } = input;
540
+ const submission = await prisma_1.prisma.submission.findFirst({
541
+ where: {
542
+ assignmentId,
543
+ studentId: ctx.user.id,
544
+ },
545
+ include: {
546
+ attachments: true,
547
+ student: {
548
+ select: {
549
+ id: true,
550
+ username: true,
551
+ },
552
+ },
553
+ assignment: {
554
+ include: {
555
+ class: true,
556
+ markScheme: {
557
+ select: {
558
+ id: true,
559
+ structured: true,
560
+ }
561
+ },
562
+ gradingBoundary: {
563
+ select: {
564
+ id: true,
565
+ structured: true,
566
+ }
567
+ }
568
+ },
569
+ },
570
+ annotations: true,
571
+ },
572
+ });
573
+ if (!submission) {
574
+ // Create a new submission if it doesn't exist
575
+ return await prisma_1.prisma.submission.create({
576
+ data: {
577
+ assignment: {
578
+ connect: { id: assignmentId },
579
+ },
580
+ student: {
581
+ connect: { id: ctx.user.id },
582
+ },
583
+ },
584
+ include: {
585
+ attachments: true,
586
+ annotations: true,
587
+ student: {
588
+ select: {
589
+ id: true,
590
+ username: true,
591
+ },
592
+ },
593
+ assignment: {
594
+ include: {
595
+ class: true,
596
+ markScheme: {
597
+ select: {
598
+ id: true,
599
+ structured: true,
600
+ }
601
+ },
602
+ gradingBoundary: {
603
+ select: {
604
+ id: true,
605
+ structured: true,
606
+ }
607
+ }
608
+ },
609
+ },
610
+ },
611
+ });
612
+ }
613
+ return {
614
+ ...submission,
615
+ late: submission.assignment.dueDate < new Date(),
616
+ };
617
+ }),
618
+ getSubmissionById: trpc_1.protectedTeacherProcedure
619
+ .input(zod_1.z.object({
620
+ submissionId: zod_1.z.string(),
621
+ classId: zod_1.z.string(),
622
+ }))
623
+ .query(async ({ ctx, input }) => {
624
+ if (!ctx.user) {
625
+ throw new server_1.TRPCError({
626
+ code: "UNAUTHORIZED",
627
+ message: "User must be authenticated",
628
+ });
629
+ }
630
+ const { submissionId, classId } = input;
631
+ const submission = await prisma_1.prisma.submission.findFirst({
632
+ where: {
633
+ id: submissionId,
634
+ assignment: {
635
+ classId,
636
+ class: {
637
+ teachers: {
638
+ some: {
639
+ id: ctx.user.id
640
+ }
641
+ }
642
+ }
643
+ },
644
+ },
645
+ include: {
646
+ attachments: true,
647
+ annotations: true,
648
+ student: {
649
+ select: {
650
+ id: true,
651
+ username: true,
652
+ },
653
+ },
654
+ assignment: {
655
+ include: {
656
+ class: true,
657
+ markScheme: {
658
+ select: {
659
+ id: true,
660
+ structured: true,
661
+ }
662
+ },
663
+ gradingBoundary: {
664
+ select: {
665
+ id: true,
666
+ structured: true,
667
+ }
668
+ }
669
+ },
670
+ },
671
+ },
672
+ });
673
+ if (!submission) {
674
+ throw new server_1.TRPCError({
675
+ code: "NOT_FOUND",
676
+ message: "Submission not found",
677
+ });
678
+ }
679
+ return {
680
+ ...submission,
681
+ late: submission.assignment.dueDate < new Date(),
682
+ };
683
+ }),
684
+ updateSubmission: trpc_1.protectedClassMemberProcedure
685
+ .input(submissionSchema)
686
+ .mutation(async ({ ctx, input }) => {
687
+ if (!ctx.user) {
688
+ throw new server_1.TRPCError({
689
+ code: "UNAUTHORIZED",
690
+ message: "User must be authenticated",
691
+ });
692
+ }
693
+ const { submissionId, submit, newAttachments, removedAttachments } = input;
694
+ const submission = await prisma_1.prisma.submission.findFirst({
695
+ where: {
696
+ id: submissionId,
697
+ OR: [
698
+ {
699
+ student: {
700
+ id: ctx.user.id,
701
+ },
702
+ },
703
+ {
704
+ assignment: {
705
+ class: {
706
+ teachers: {
707
+ some: {
708
+ id: ctx.user.id,
709
+ },
710
+ },
711
+ },
712
+ },
713
+ },
714
+ ],
715
+ },
716
+ include: {
717
+ attachments: {
718
+ include: {
719
+ thumbnail: true
720
+ }
721
+ },
722
+ assignment: {
723
+ include: {
724
+ class: true,
725
+ markScheme: {
726
+ select: {
727
+ id: true,
728
+ structured: true,
729
+ }
730
+ },
731
+ gradingBoundary: {
732
+ select: {
733
+ id: true,
734
+ structured: true,
735
+ }
736
+ }
737
+ },
738
+ },
739
+ },
740
+ });
741
+ if (!submission) {
742
+ throw new server_1.TRPCError({
743
+ code: "NOT_FOUND",
744
+ message: "Submission not found",
745
+ });
746
+ }
747
+ if (submit !== undefined) {
748
+ // Toggle submission status
749
+ return await prisma_1.prisma.submission.update({
750
+ where: { id: submission.id },
751
+ data: {
752
+ submitted: !submission.submitted,
753
+ submittedAt: new Date(),
754
+ },
755
+ include: {
756
+ attachments: true,
757
+ student: {
758
+ select: {
759
+ id: true,
760
+ username: true,
761
+ },
762
+ },
763
+ assignment: {
764
+ include: {
765
+ class: true,
766
+ markScheme: {
767
+ select: {
768
+ id: true,
769
+ structured: true,
770
+ }
771
+ },
772
+ gradingBoundary: {
773
+ select: {
774
+ id: true,
775
+ structured: true,
776
+ }
777
+ }
778
+ },
779
+ },
780
+ },
781
+ });
782
+ }
783
+ let uploadedFiles = [];
784
+ if (newAttachments && newAttachments.length > 0) {
785
+ // Store files in a class and assignment specific directory
786
+ uploadedFiles = await (0, fileUpload_1.uploadFiles)(newAttachments, ctx.user.id, `class/${submission.assignment.classId}/assignment/${submission.assignmentId}/submission/${submission.id}`);
787
+ }
788
+ // Update submission with new file attachments
789
+ if (uploadedFiles.length > 0) {
790
+ await prisma_1.prisma.submission.update({
791
+ where: { id: submission.id },
792
+ data: {
793
+ attachments: {
794
+ create: uploadedFiles.map(file => ({
795
+ name: file.name,
796
+ type: file.type,
797
+ size: file.size,
798
+ path: file.path,
799
+ ...(file.thumbnailId && {
800
+ thumbnail: {
801
+ connect: { id: file.thumbnailId }
802
+ }
803
+ })
804
+ }))
805
+ }
806
+ }
807
+ });
808
+ }
809
+ // Delete removed attachments if any
810
+ if (removedAttachments && removedAttachments.length > 0) {
811
+ const filesToDelete = submission.attachments.filter((file) => removedAttachments.includes(file.id));
812
+ // Delete files from storage
813
+ await Promise.all(filesToDelete.map(async (file) => {
814
+ try {
815
+ // Delete the main file
816
+ await (0, googleCloudStorage_1.deleteFile)(file.path);
817
+ // Delete thumbnail if it exists
818
+ if (file.thumbnail?.path) {
819
+ await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
820
+ }
821
+ }
822
+ catch (error) {
823
+ console.warn(`Failed to delete file ${file.path}:`, error);
824
+ }
825
+ }));
826
+ }
827
+ // Update submission with attachments
828
+ return await prisma_1.prisma.submission.update({
829
+ where: { id: submission.id },
830
+ data: {
831
+ ...(removedAttachments && removedAttachments.length > 0 && {
832
+ attachments: {
833
+ deleteMany: {
834
+ id: { in: removedAttachments },
835
+ },
836
+ },
837
+ }),
838
+ },
839
+ include: {
840
+ attachments: {
841
+ include: {
842
+ thumbnail: true
843
+ }
844
+ },
845
+ student: {
846
+ select: {
847
+ id: true,
848
+ username: true,
849
+ },
850
+ },
851
+ assignment: {
852
+ include: {
853
+ class: true,
854
+ markScheme: {
855
+ select: {
856
+ id: true,
857
+ structured: true,
858
+ }
859
+ },
860
+ gradingBoundary: {
861
+ select: {
862
+ id: true,
863
+ structured: true,
864
+ }
865
+ }
866
+ },
867
+ },
868
+ },
869
+ });
870
+ }),
871
+ getSubmissions: trpc_1.protectedTeacherProcedure
872
+ .input(zod_1.z.object({
873
+ assignmentId: zod_1.z.string(),
874
+ classId: zod_1.z.string(),
875
+ }))
876
+ .query(async ({ ctx, input }) => {
877
+ if (!ctx.user) {
878
+ throw new server_1.TRPCError({
879
+ code: "UNAUTHORIZED",
880
+ message: "User must be authenticated",
881
+ });
882
+ }
883
+ const { assignmentId } = input;
884
+ const submissions = await prisma_1.prisma.submission.findMany({
885
+ where: {
886
+ assignment: {
887
+ id: assignmentId,
888
+ class: {
889
+ teachers: {
890
+ some: { id: ctx.user.id },
891
+ },
892
+ },
893
+ },
894
+ },
895
+ include: {
896
+ attachments: {
897
+ include: {
898
+ thumbnail: true
899
+ }
900
+ },
901
+ student: {
902
+ select: {
903
+ id: true,
904
+ username: true,
905
+ },
906
+ },
907
+ assignment: {
908
+ include: {
909
+ class: true,
910
+ markScheme: {
911
+ select: {
912
+ id: true,
913
+ structured: true,
914
+ }
915
+ },
916
+ gradingBoundary: {
917
+ select: {
918
+ id: true,
919
+ structured: true,
920
+ }
921
+ }
922
+ },
923
+ },
924
+ },
925
+ });
926
+ return submissions.map(submission => ({
927
+ ...submission,
928
+ late: submission.assignment.dueDate < new Date(),
929
+ }));
930
+ }),
931
+ updateSubmissionAsTeacher: trpc_1.protectedTeacherProcedure
932
+ .input(updateSubmissionSchema)
933
+ .mutation(async ({ ctx, input }) => {
934
+ if (!ctx.user) {
935
+ throw new server_1.TRPCError({
936
+ code: "UNAUTHORIZED",
937
+ message: "User must be authenticated",
938
+ });
939
+ }
940
+ const { submissionId, return: returnSubmission, gradeReceived, newAttachments, removedAttachments, rubricGrades } = input;
941
+ const submission = await prisma_1.prisma.submission.findFirst({
942
+ where: {
943
+ id: submissionId,
944
+ assignment: {
945
+ class: {
946
+ teachers: {
947
+ some: { id: ctx.user.id },
948
+ },
949
+ },
950
+ },
951
+ },
952
+ include: {
953
+ attachments: {
954
+ include: {
955
+ thumbnail: true
956
+ }
957
+ },
958
+ annotations: {
959
+ include: {
960
+ thumbnail: true
961
+ }
962
+ },
963
+ assignment: {
964
+ include: {
965
+ class: true,
966
+ markScheme: {
967
+ select: {
968
+ id: true,
969
+ structured: true,
970
+ }
971
+ },
972
+ gradingBoundary: {
973
+ select: {
974
+ id: true,
975
+ structured: true,
976
+ }
977
+ }
978
+ },
979
+ },
980
+ },
981
+ });
982
+ if (!submission) {
983
+ throw new server_1.TRPCError({
984
+ code: "NOT_FOUND",
985
+ message: "Submission not found",
986
+ });
987
+ }
988
+ if (returnSubmission !== undefined) {
989
+ // Toggle return status
990
+ return await prisma_1.prisma.submission.update({
991
+ where: { id: submissionId },
992
+ data: {
993
+ returned: !submission.returned,
994
+ },
995
+ include: {
996
+ attachments: true,
997
+ student: {
998
+ select: {
999
+ id: true,
1000
+ username: true,
1001
+ },
1002
+ },
1003
+ assignment: {
1004
+ include: {
1005
+ class: true,
1006
+ markScheme: {
1007
+ select: {
1008
+ id: true,
1009
+ structured: true,
1010
+ }
1011
+ },
1012
+ gradingBoundary: {
1013
+ select: {
1014
+ id: true,
1015
+ structured: true,
1016
+ }
1017
+ }
1018
+ },
1019
+ },
1020
+ },
1021
+ });
1022
+ }
1023
+ let uploadedFiles = [];
1024
+ if (newAttachments && newAttachments.length > 0) {
1025
+ // Store files in a class and assignment specific directory
1026
+ uploadedFiles = await (0, fileUpload_1.uploadFiles)(newAttachments, ctx.user.id, `class/${submission.assignment.classId}/assignment/${submission.assignmentId}/submission/${submission.id}/annotations`);
1027
+ }
1028
+ // Update submission with new file attachments
1029
+ if (uploadedFiles.length > 0) {
1030
+ await prisma_1.prisma.submission.update({
1031
+ where: { id: submission.id },
1032
+ data: {
1033
+ annotations: {
1034
+ create: uploadedFiles.map(file => ({
1035
+ name: file.name,
1036
+ type: file.type,
1037
+ size: file.size,
1038
+ path: file.path,
1039
+ ...(file.thumbnailId && {
1040
+ thumbnail: {
1041
+ connect: { id: file.thumbnailId }
1042
+ }
1043
+ })
1044
+ }))
1045
+ }
1046
+ }
1047
+ });
1048
+ }
1049
+ // Delete removed attachments if any
1050
+ if (removedAttachments && removedAttachments.length > 0) {
1051
+ const filesToDelete = submission.annotations.filter((file) => removedAttachments.includes(file.id));
1052
+ // Delete files from storage
1053
+ await Promise.all(filesToDelete.map(async (file) => {
1054
+ try {
1055
+ // Delete the main file
1056
+ await (0, googleCloudStorage_1.deleteFile)(file.path);
1057
+ // Delete thumbnail if it exists
1058
+ if (file.thumbnail?.path) {
1059
+ await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
1060
+ }
1061
+ }
1062
+ catch (error) {
1063
+ console.warn(`Failed to delete file ${file.path}:`, error);
1064
+ }
1065
+ }));
1066
+ }
1067
+ // Update submission with grade and attachments
1068
+ return await prisma_1.prisma.submission.update({
1069
+ where: { id: submissionId },
1070
+ data: {
1071
+ ...(gradeReceived !== undefined && { gradeReceived }),
1072
+ ...(rubricGrades && { rubricState: JSON.stringify(rubricGrades) }),
1073
+ ...(removedAttachments && removedAttachments.length > 0 && {
1074
+ annotations: {
1075
+ deleteMany: {
1076
+ id: { in: removedAttachments },
1077
+ },
1078
+ },
1079
+ }),
1080
+ },
1081
+ include: {
1082
+ attachments: {
1083
+ include: {
1084
+ thumbnail: true
1085
+ }
1086
+ },
1087
+ annotations: {
1088
+ include: {
1089
+ thumbnail: true
1090
+ }
1091
+ },
1092
+ student: {
1093
+ select: {
1094
+ id: true,
1095
+ username: true,
1096
+ },
1097
+ },
1098
+ assignment: {
1099
+ include: {
1100
+ class: true,
1101
+ markScheme: {
1102
+ select: {
1103
+ id: true,
1104
+ structured: true,
1105
+ }
1106
+ },
1107
+ gradingBoundary: {
1108
+ select: {
1109
+ id: true,
1110
+ structured: true,
1111
+ }
1112
+ }
1113
+ },
1114
+ },
1115
+ },
1116
+ });
1117
+ }),
1118
+ attachToEvent: trpc_1.protectedTeacherProcedure
1119
+ .input(zod_1.z.object({
1120
+ assignmentId: zod_1.z.string(),
1121
+ eventId: zod_1.z.string(),
1122
+ }))
1123
+ .mutation(async ({ ctx, input }) => {
1124
+ if (!ctx.user) {
1125
+ throw new server_1.TRPCError({
1126
+ code: "UNAUTHORIZED",
1127
+ message: "User must be authenticated",
1128
+ });
1129
+ }
1130
+ const { assignmentId, eventId } = input;
1131
+ // Check if assignment exists and user is a teacher of the class
1132
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1133
+ where: {
1134
+ id: assignmentId,
1135
+ class: {
1136
+ teachers: {
1137
+ some: { id: ctx.user.id },
1138
+ },
1139
+ },
1140
+ },
1141
+ include: {
1142
+ class: true,
1143
+ },
1144
+ });
1145
+ if (!assignment) {
1146
+ throw new server_1.TRPCError({
1147
+ code: "NOT_FOUND",
1148
+ message: "Assignment not found or you are not authorized",
1149
+ });
1150
+ }
1151
+ // Check if event exists and belongs to the same class
1152
+ const event = await prisma_1.prisma.event.findFirst({
1153
+ where: {
1154
+ id: eventId,
1155
+ classId: assignment.classId,
1156
+ },
1157
+ });
1158
+ if (!event) {
1159
+ throw new server_1.TRPCError({
1160
+ code: "NOT_FOUND",
1161
+ message: "Event not found or does not belong to the same class",
1162
+ });
1163
+ }
1164
+ // Attach assignment to event
1165
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
1166
+ where: { id: assignmentId },
1167
+ data: {
1168
+ eventAttached: {
1169
+ connect: { id: eventId }
1170
+ }
1171
+ },
1172
+ include: {
1173
+ attachments: {
1174
+ select: {
1175
+ id: true,
1176
+ name: true,
1177
+ type: true,
1178
+ }
1179
+ },
1180
+ section: {
1181
+ select: {
1182
+ id: true,
1183
+ name: true
1184
+ }
1185
+ },
1186
+ teacher: {
1187
+ select: {
1188
+ id: true,
1189
+ username: true
1190
+ }
1191
+ },
1192
+ eventAttached: {
1193
+ select: {
1194
+ id: true,
1195
+ name: true,
1196
+ startTime: true,
1197
+ endTime: true,
1198
+ }
1199
+ }
1200
+ }
1201
+ });
1202
+ return { assignment: updatedAssignment };
1203
+ }),
1204
+ detachEvent: trpc_1.protectedTeacherProcedure
1205
+ .input(zod_1.z.object({
1206
+ assignmentId: zod_1.z.string(),
1207
+ }))
1208
+ .mutation(async ({ ctx, input }) => {
1209
+ if (!ctx.user) {
1210
+ throw new server_1.TRPCError({
1211
+ code: "UNAUTHORIZED",
1212
+ message: "User must be authenticated",
1213
+ });
1214
+ }
1215
+ const { assignmentId } = input;
1216
+ // Check if assignment exists and user is a teacher of the class
1217
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1218
+ where: {
1219
+ id: assignmentId,
1220
+ class: {
1221
+ teachers: {
1222
+ some: { id: ctx.user.id },
1223
+ },
1224
+ },
1225
+ },
1226
+ });
1227
+ if (!assignment) {
1228
+ throw new server_1.TRPCError({
1229
+ code: "NOT_FOUND",
1230
+ message: "Assignment not found or you are not authorized",
1231
+ });
1232
+ }
1233
+ // Detach assignment from event
1234
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
1235
+ where: { id: assignmentId },
1236
+ data: {
1237
+ eventAttached: {
1238
+ disconnect: true
1239
+ }
1240
+ },
1241
+ include: {
1242
+ attachments: {
1243
+ select: {
1244
+ id: true,
1245
+ name: true,
1246
+ type: true,
1247
+ }
1248
+ },
1249
+ section: {
1250
+ select: {
1251
+ id: true,
1252
+ name: true
1253
+ }
1254
+ },
1255
+ teacher: {
1256
+ select: {
1257
+ id: true,
1258
+ username: true
1259
+ }
1260
+ },
1261
+ eventAttached: {
1262
+ select: {
1263
+ id: true,
1264
+ name: true,
1265
+ startTime: true,
1266
+ endTime: true,
1267
+ }
1268
+ }
1269
+ }
1270
+ });
1271
+ return { assignment: updatedAssignment };
1272
+ }),
1273
+ getAvailableEvents: trpc_1.protectedTeacherProcedure
1274
+ .input(zod_1.z.object({
1275
+ assignmentId: zod_1.z.string(),
1276
+ }))
1277
+ .query(async ({ ctx, input }) => {
1278
+ if (!ctx.user) {
1279
+ throw new server_1.TRPCError({
1280
+ code: "UNAUTHORIZED",
1281
+ message: "User must be authenticated",
1282
+ });
1283
+ }
1284
+ const { assignmentId } = input;
1285
+ // Get the assignment to find the class
1286
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1287
+ where: {
1288
+ id: assignmentId,
1289
+ class: {
1290
+ teachers: {
1291
+ some: { id: ctx.user.id },
1292
+ },
1293
+ },
1294
+ },
1295
+ select: { classId: true }
1296
+ });
1297
+ if (!assignment) {
1298
+ throw new server_1.TRPCError({
1299
+ code: "NOT_FOUND",
1300
+ message: "Assignment not found or you are not authorized",
1301
+ });
1302
+ }
1303
+ // Get all events for the class that don't already have this assignment attached
1304
+ const events = await prisma_1.prisma.event.findMany({
1305
+ where: {
1306
+ classId: assignment.classId,
1307
+ assignmentsAttached: {
1308
+ none: {
1309
+ id: assignmentId
1310
+ }
1311
+ }
1312
+ },
1313
+ select: {
1314
+ id: true,
1315
+ name: true,
1316
+ startTime: true,
1317
+ endTime: true,
1318
+ location: true,
1319
+ remarks: true,
1320
+ },
1321
+ orderBy: {
1322
+ startTime: 'asc'
1323
+ }
1324
+ });
1325
+ return { events };
1326
+ }),
1327
+ dueToday: trpc_1.protectedProcedure
1328
+ .query(async ({ ctx }) => {
1329
+ if (!ctx.user) {
1330
+ throw new server_1.TRPCError({
1331
+ code: "UNAUTHORIZED",
1332
+ message: "User must be authenticated",
1333
+ });
1334
+ }
1335
+ const assignments = await prisma_1.prisma.assignment.findMany({
1336
+ where: {
1337
+ dueDate: {
1338
+ equals: new Date(),
1339
+ },
1340
+ },
1341
+ select: {
1342
+ id: true,
1343
+ title: true,
1344
+ dueDate: true,
1345
+ type: true,
1346
+ graded: true,
1347
+ maxGrade: true,
1348
+ class: {
1349
+ select: {
1350
+ id: true,
1351
+ name: true,
1352
+ }
1353
+ }
1354
+ }
1355
+ });
1356
+ return assignments.map(assignment => ({
1357
+ ...assignment,
1358
+ dueDate: assignment.dueDate.toISOString(),
1359
+ }));
1360
+ }),
1361
+ attachMarkScheme: trpc_1.protectedTeacherProcedure
1362
+ .input(zod_1.z.object({
1363
+ assignmentId: zod_1.z.string(),
1364
+ markSchemeId: zod_1.z.string().nullable(),
1365
+ }))
1366
+ .mutation(async ({ ctx, input }) => {
1367
+ const { assignmentId, markSchemeId } = input;
1368
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1369
+ where: {
1370
+ id: assignmentId,
1371
+ },
1372
+ });
1373
+ if (!assignment) {
1374
+ throw new server_1.TRPCError({
1375
+ code: "NOT_FOUND",
1376
+ message: "Assignment not found",
1377
+ });
1378
+ }
1379
+ // If markSchemeId is provided, verify it exists
1380
+ if (markSchemeId) {
1381
+ const markScheme = await prisma_1.prisma.markScheme.findFirst({
1382
+ where: {
1383
+ id: markSchemeId,
1384
+ },
1385
+ });
1386
+ if (!markScheme) {
1387
+ throw new server_1.TRPCError({
1388
+ code: "NOT_FOUND",
1389
+ message: "Mark scheme not found",
1390
+ });
1391
+ }
1392
+ }
1393
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
1394
+ where: { id: assignmentId },
1395
+ data: {
1396
+ markScheme: markSchemeId ? {
1397
+ connect: { id: markSchemeId },
1398
+ } : {
1399
+ disconnect: true,
1400
+ },
1401
+ },
1402
+ include: {
1403
+ attachments: true,
1404
+ section: true,
1405
+ teacher: true,
1406
+ eventAttached: true,
1407
+ markScheme: true,
1408
+ },
1409
+ });
1410
+ return updatedAssignment;
1411
+ }),
1412
+ detachMarkScheme: trpc_1.protectedTeacherProcedure
1413
+ .input(zod_1.z.object({
1414
+ assignmentId: zod_1.z.string(),
1415
+ }))
1416
+ .mutation(async ({ ctx, input }) => {
1417
+ const { assignmentId } = input;
1418
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1419
+ where: {
1420
+ id: assignmentId,
1421
+ },
1422
+ });
1423
+ if (!assignment) {
1424
+ throw new server_1.TRPCError({
1425
+ code: "NOT_FOUND",
1426
+ message: "Assignment not found",
1427
+ });
1428
+ }
1429
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
1430
+ where: { id: assignmentId },
1431
+ data: {
1432
+ markScheme: {
1433
+ disconnect: true,
1434
+ },
1435
+ },
1436
+ include: {
1437
+ attachments: true,
1438
+ section: true,
1439
+ teacher: true,
1440
+ eventAttached: true,
1441
+ markScheme: true,
1442
+ },
1443
+ });
1444
+ return updatedAssignment;
1445
+ }),
1446
+ attachGradingBoundary: trpc_1.protectedTeacherProcedure
1447
+ .input(zod_1.z.object({
1448
+ assignmentId: zod_1.z.string(),
1449
+ gradingBoundaryId: zod_1.z.string().nullable(),
1450
+ }))
1451
+ .mutation(async ({ ctx, input }) => {
1452
+ const { assignmentId, gradingBoundaryId } = input;
1453
+ const assignment = await prisma_1.prisma.assignment.findFirst({
1454
+ where: {
1455
+ id: assignmentId,
1456
+ },
1457
+ });
1458
+ if (!assignment) {
1459
+ throw new server_1.TRPCError({
1460
+ code: "NOT_FOUND",
1461
+ message: "Assignment not found",
1462
+ });
1463
+ }
1464
+ // If gradingBoundaryId is provided, verify it exists
1465
+ if (gradingBoundaryId) {
1466
+ const gradingBoundary = await prisma_1.prisma.gradingBoundary.findFirst({
1467
+ where: {
1468
+ id: gradingBoundaryId,
1469
+ },
1470
+ });
1471
+ if (!gradingBoundary) {
1472
+ throw new server_1.TRPCError({
1473
+ code: "NOT_FOUND",
1474
+ message: "Grading boundary not found",
1475
+ });
1476
+ }
1477
+ }
1478
+ const updatedAssignment = await prisma_1.prisma.assignment.update({
1479
+ where: { id: assignmentId },
1480
+ data: {
1481
+ gradingBoundary: gradingBoundaryId ? {
1482
+ connect: { id: gradingBoundaryId },
1483
+ } : {
1484
+ disconnect: true,
1485
+ },
1486
+ },
1487
+ include: {
1488
+ attachments: true,
1489
+ section: true,
1490
+ teacher: true,
1491
+ eventAttached: true,
1492
+ gradingBoundary: true,
1493
+ },
1494
+ });
1495
+ return updatedAssignment;
1496
+ }),
1497
+ });