@studious-lms/server 1.2.33 → 1.2.35

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.
@@ -523,7 +523,7 @@ export const announcementRouter = createTRPCRouter({
523
523
 
524
524
  // If replying to a comment, verify parent comment exists and belongs to the same announcement
525
525
  if (input.parentCommentId) {
526
- const parentComment = await prisma.announcementComment.findFirst({
526
+ const parentComment = await prisma.comment.findFirst({
527
527
  where: {
528
528
  id: input.parentCommentId,
529
529
  announcementId: input.announcementId,
@@ -538,7 +538,7 @@ export const announcementRouter = createTRPCRouter({
538
538
  }
539
539
  }
540
540
 
541
- const comment = await prisma.announcementComment.create({
541
+ const comment = await prisma.comment.create({
542
542
  data: {
543
543
  content: input.content,
544
544
  author: {
@@ -586,7 +586,7 @@ export const announcementRouter = createTRPCRouter({
586
586
  });
587
587
  }
588
588
 
589
- const comment = await prisma.announcementComment.findUnique({
589
+ const comment = await prisma.comment.findUnique({
590
590
  where: { id: input.id },
591
591
  });
592
592
 
@@ -605,7 +605,7 @@ export const announcementRouter = createTRPCRouter({
605
605
  });
606
606
  }
607
607
 
608
- const updatedComment = await prisma.announcementComment.update({
608
+ const updatedComment = await prisma.comment.update({
609
609
  where: { id: input.id },
610
610
  data: {
611
611
  content: input.content,
@@ -642,7 +642,7 @@ export const announcementRouter = createTRPCRouter({
642
642
  });
643
643
  }
644
644
 
645
- const comment = await prisma.announcementComment.findUnique({
645
+ const comment = await prisma.comment.findUnique({
646
646
  where: { id: input.id },
647
647
  include: {
648
648
  announcement: {
@@ -667,7 +667,7 @@ export const announcementRouter = createTRPCRouter({
667
667
  // Only the author or a class teacher can delete comments
668
668
  const userId = ctx.user.id;
669
669
  const isAuthor = comment.authorId === userId;
670
- const isClassTeacher = comment.announcement.class.teachers.some(
670
+ const isClassTeacher = comment.announcement!.class.teachers.some(
671
671
  (teacher) => teacher.id === userId
672
672
  );
673
673
 
@@ -678,7 +678,7 @@ export const announcementRouter = createTRPCRouter({
678
678
  });
679
679
  }
680
680
 
681
- await prisma.announcementComment.delete({
681
+ await prisma.comment.delete({
682
682
  where: { id: input.id },
683
683
  });
684
684
 
@@ -707,7 +707,7 @@ export const announcementRouter = createTRPCRouter({
707
707
  }
708
708
 
709
709
  // Get all top-level comments (no parent)
710
- const comments = await prisma.announcementComment.findMany({
710
+ const comments = await prisma.comment.findMany({
711
711
  where: {
712
712
  announcementId: input.announcementId,
713
713
  parentCommentId: null,
@@ -840,7 +840,7 @@ export const announcementRouter = createTRPCRouter({
840
840
  return { reaction };
841
841
  } else if (input.commentId) {
842
842
  // Verify comment exists and get its announcement to check class
843
- const comment = await prisma.announcementComment.findUnique({
843
+ const comment = await prisma.comment.findUnique({
844
844
  where: { id: input.commentId },
845
845
  include: {
846
846
  announcement: {
@@ -858,7 +858,7 @@ export const announcementRouter = createTRPCRouter({
858
858
  });
859
859
  }
860
860
 
861
- if (comment.announcement.classId !== input.classId) {
861
+ if (comment.announcement!.classId !== input.classId) {
862
862
  throw new TRPCError({
863
863
  code: "FORBIDDEN",
864
864
  message: "Comment does not belong to this class",
@@ -1060,7 +1060,7 @@ export const announcementRouter = createTRPCRouter({
1060
1060
  };
1061
1061
  } else if (input.commentId) {
1062
1062
  // Verify comment exists
1063
- const comment = await prisma.announcementComment.findUnique({
1063
+ const comment = await prisma.comment.findUnique({
1064
1064
  where: { id: input.commentId },
1065
1065
  include: {
1066
1066
  announcement: {
@@ -1078,7 +1078,7 @@ export const announcementRouter = createTRPCRouter({
1078
1078
  });
1079
1079
  }
1080
1080
 
1081
- if (comment.announcement.classId !== input.classId) {
1081
+ if (comment.announcement!.classId !== input.classId) {
1082
1082
  throw new TRPCError({
1083
1083
  code: "FORBIDDEN",
1084
1084
  message: "Comment does not belong to this class",
@@ -0,0 +1,268 @@
1
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { z } from "zod";
3
+ import { prisma } from "../lib/prisma.js";
4
+ import { TRPCError } from "@trpc/server";
5
+
6
+ export const commentRouter = createTRPCRouter({
7
+ get: protectedProcedure
8
+ .input(z.object({
9
+ id: z.string(),
10
+ }))
11
+ .query(async ({ ctx, input }) => {
12
+ const comment = await prisma.comment.findUnique({
13
+ where: { id: input.id },
14
+ select: {
15
+ replies: {
16
+ select: {
17
+ id: true,
18
+ content: true,
19
+ author: {
20
+ select: {
21
+ id: true,
22
+ username: true,
23
+ profile: {
24
+ select: {
25
+ displayName: true,
26
+ profilePicture: true,
27
+ profilePictureThumbnail: true,
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ reactions: {
35
+ select: {
36
+ type: true,
37
+ user: {
38
+ select: {
39
+ id: true,
40
+ username: true,
41
+ profile: {
42
+ select: {
43
+ displayName: true,
44
+ profilePicture: true,
45
+ profilePictureThumbnail: true,
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ }
53
+ });
54
+ return comment;
55
+ }),
56
+ replyToComment: protectedProcedure
57
+ .input(z.object({
58
+ parentCommentId: z.string(),
59
+ content: z.string(),
60
+ }))
61
+ .mutation(async ({ ctx, input }) => {
62
+ const { parentCommentId, content } = input;
63
+
64
+ const newComment = await prisma.comment.create({
65
+ data: {
66
+ parentCommentId,
67
+ content,
68
+ authorId: ctx.user!.id,
69
+ },
70
+ });
71
+
72
+ return newComment;
73
+ }),
74
+ addReaction: protectedProcedure
75
+ .input(z.object({
76
+ id: z.string(),
77
+ type: z.enum(['THUMBSUP', 'CELEBRATE', 'CARE', 'HEART', 'IDEA', 'HAPPY']),
78
+ }))
79
+ .mutation(async ({ ctx, input }) => {
80
+ if (!ctx.user) {
81
+ throw new TRPCError({
82
+ code: "UNAUTHORIZED",
83
+ message: "User must be authenticated",
84
+ });
85
+ }
86
+
87
+ // Exactly one of announcementId or commentId must be provided
88
+ const comment = await prisma.comment.findUnique({
89
+ where: { id: input.id },
90
+ });
91
+
92
+ const userId = ctx.user.id;
93
+
94
+ // Verify the announcement or comment exists and belongs to the class
95
+ if (comment) {
96
+ const announcement = await prisma.announcement.findFirst({
97
+ where: {
98
+ id: input.id,
99
+ },
100
+ });
101
+
102
+ // Upsert reaction: update if exists, create if not
103
+ const reaction = await prisma.reaction.upsert({
104
+ where: {
105
+ userId_commentId: {
106
+ userId,
107
+ commentId: input.id,
108
+ },
109
+ },
110
+ update: {
111
+ type: input.type,
112
+ },
113
+ create: {
114
+ type: input.type,
115
+ userId,
116
+ commentId: input.id,
117
+ },
118
+ include: {
119
+ user: {
120
+ select: {
121
+ id: true,
122
+ username: true,
123
+ profile: {
124
+ select: {
125
+ displayName: true,
126
+ profilePicture: true,
127
+ profilePictureThumbnail: true,
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ });
134
+
135
+ return { reaction };
136
+ }
137
+
138
+ throw new TRPCError({
139
+ code: "INTERNAL_SERVER_ERROR",
140
+ message: "Unexpected error",
141
+ });
142
+ }),
143
+
144
+ removeReaction: protectedProcedure
145
+ .input(z.object({
146
+ commentId: z.string().optional(),
147
+ }))
148
+ .mutation(async ({ ctx, input }) => {
149
+ if (!ctx.user) {
150
+ throw new TRPCError({
151
+ code: "UNAUTHORIZED",
152
+ message: "User must be authenticated",
153
+ });
154
+ }
155
+
156
+ // Exactly one of announcementId or commentId must be provided
157
+ if (!input.commentId) {
158
+ throw new TRPCError({
159
+ code: "BAD_REQUEST",
160
+ message: "Either announcementId or commentId must be provided",
161
+ });
162
+ }
163
+
164
+ const userId = ctx.user.id;
165
+
166
+ const reaction = await prisma.reaction.findUnique({
167
+ where: {
168
+ userId_commentId: {
169
+ userId,
170
+ commentId: input.commentId,
171
+ },
172
+ },
173
+ });
174
+
175
+ if (!reaction) {
176
+ throw new TRPCError({
177
+ code: "NOT_FOUND",
178
+ message: "Reaction not found",
179
+ });
180
+ }
181
+
182
+ await prisma.reaction.delete({
183
+ where: { id: reaction.id },
184
+ });
185
+
186
+ return { success: true };
187
+
188
+ }),
189
+
190
+ getReactions: protectedProcedure
191
+ .input(z.object({
192
+ commentId: z.string().optional(),
193
+ }))
194
+ .query(async ({ ctx, input }) => {
195
+ if (!ctx.user) {
196
+ throw new TRPCError({
197
+ code: "UNAUTHORIZED",
198
+ message: "User must be authenticated",
199
+ });
200
+ }
201
+
202
+ // Exactly one of announcementId or commentId must be provided
203
+ if (!input.commentId) {
204
+ throw new TRPCError({
205
+ code: "BAD_REQUEST",
206
+ message: "Either announcementId or commentId must be provided",
207
+ });
208
+ }
209
+
210
+ const userId = ctx.user.id;
211
+
212
+ // Verify comment exists
213
+ const comment = await prisma.comment.findUnique({
214
+ where: { id: input.commentId },
215
+ include: {
216
+ announcement: {
217
+ select: {
218
+ classId: true,
219
+ },
220
+ },
221
+ },
222
+ });
223
+
224
+ if (!comment) {
225
+ throw new TRPCError({
226
+ code: "NOT_FOUND",
227
+ message: "Comment not found",
228
+ });
229
+ }
230
+
231
+ // Get reaction counts by type
232
+ const reactionCounts = await prisma.reaction.groupBy({
233
+ by: ['type'],
234
+ where: { commentId: input.commentId },
235
+ _count: { type: true },
236
+ });
237
+
238
+ // Get current user's reaction
239
+ const userReaction = await prisma.reaction.findUnique({
240
+ where: {
241
+ userId_commentId: {
242
+ userId,
243
+ commentId: input.commentId,
244
+ },
245
+ },
246
+ });
247
+
248
+ // Format counts
249
+ const counts = {
250
+ THUMBSUP: 0,
251
+ CELEBRATE: 0,
252
+ CARE: 0,
253
+ HEART: 0,
254
+ IDEA: 0,
255
+ HAPPY: 0,
256
+ };
257
+
258
+ reactionCounts.forEach((item) => {
259
+ counts[item.type as keyof typeof counts] = item._count.type;
260
+ });
261
+
262
+ return {
263
+ counts,
264
+ userReaction: userReaction?.type || null,
265
+ total: reactionCounts.reduce((sum, item) => sum + item._count.type, 0),
266
+ };
267
+ }),
268
+ });
@@ -1,7 +1,7 @@
1
1
  import { TRPCError } from "@trpc/server";
2
- import { createTRPCRouter, protectedProcedure } from "src/trpc";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { z } from "zod";
4
- import { prisma } from "src/lib/prisma";
4
+ import { prisma } from "../lib/prisma.js";
5
5
  import { WorksheetQuestionType } from "@prisma/client";
6
6
 
7
7
  export const worksheetRouter = createTRPCRouter({
@@ -18,6 +18,7 @@ export const worksheetRouter = createTRPCRouter({
18
18
  include: {
19
19
  questions: {
20
20
  orderBy: { createdAt: 'asc' },
21
+ // select: { id: true, type: true, question: true, answer: true, points: true },
21
22
  },
22
23
  class: true,
23
24
  },
@@ -112,12 +113,13 @@ export const worksheetRouter = createTRPCRouter({
112
113
  worksheetId: z.string(),
113
114
  question: z.string(),
114
115
  answer: z.string(),
116
+ points: z.number().optional(),
115
117
  options: z.any().optional(), // JSON field
116
118
  markScheme: z.any().optional(), // JSON field
117
119
  type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
118
120
  }))
119
121
  .mutation(async ({ ctx, input }) => {
120
- const { worksheetId, question, answer, options, markScheme, type } = input;
122
+ const { worksheetId, question, points, answer, options, markScheme, type } = input;
121
123
 
122
124
  const worksheet = await prisma.worksheet.findUnique({
123
125
  where: { id: worksheetId },
@@ -131,6 +133,7 @@ export const worksheetRouter = createTRPCRouter({
131
133
  data: {
132
134
  worksheetId,
133
135
  type,
136
+ points,
134
137
  question,
135
138
  answer,
136
139
  options,
@@ -201,12 +204,13 @@ export const worksheetRouter = createTRPCRouter({
201
204
  questionId: z.string(),
202
205
  question: z.string().optional(),
203
206
  answer: z.string().optional(),
207
+ points: z.number().optional(),
204
208
  options: z.any().optional(), // JSON field
205
209
  markScheme: z.any().optional(), // JSON field
206
210
  type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']).optional(),
207
211
  }))
208
212
  .mutation(async ({ ctx, input }) => {
209
- const { worksheetId, questionId, question, answer, options, markScheme, type } = input;
213
+ const { worksheetId, questionId, points, question, answer, options, markScheme, type } = input;
210
214
 
211
215
  const worksheet = await prisma.worksheet.findUnique({
212
216
  where: { id: worksheetId },
@@ -224,6 +228,7 @@ export const worksheetRouter = createTRPCRouter({
224
228
  ...(markScheme !== undefined && { markScheme }),
225
229
  ...(type !== undefined && { type }),
226
230
  ...(options !== undefined && { options }),
231
+ ...(points !== undefined && { points }),
227
232
  },
228
233
  });
229
234
 
@@ -269,7 +274,7 @@ export const worksheetRouter = createTRPCRouter({
269
274
  }
270
275
 
271
276
  // Find or create worksheet response for this submission
272
- const worksheetResponses = await prisma.$transaction(async (tx) => {
277
+ const worksheetResponse = await prisma.$transaction(async (tx) => {
273
278
  // First check if a response exists
274
279
  const existing = await tx.studentWorksheetResponse.findFirst({
275
280
  where: {
@@ -300,7 +305,9 @@ export const worksheetRouter = createTRPCRouter({
300
305
  return created;
301
306
  });
302
307
 
303
- return worksheetResponses;
308
+
309
+ console.log(worksheetResponse);
310
+ return worksheetResponse;
304
311
  }),
305
312
  answerQuestion: protectedProcedure
306
313
  .input(z.object({
@@ -401,104 +408,106 @@ export const worksheetRouter = createTRPCRouter({
401
408
  return submittedWorksheet;
402
409
  }),
403
410
 
404
- // Get or create a student's worksheet response
405
- getOrCreateWorksheetResponse: protectedProcedure
411
+ // Grade a student's answer
412
+ gradeAnswer: protectedProcedure
406
413
  .input(z.object({
407
- worksheetId: z.string(),
408
- studentId: z.string(),
414
+ questionId: z.string(),
415
+ responseId: z.string().optional(), // StudentQuestionProgress ID (optional for upsert)
416
+ studentWorksheetResponseId: z.string(), // Required for linking to worksheet response
417
+ response: z.string().optional(), // The actual response text (needed if creating new)
418
+ isCorrect: z.boolean(),
419
+ feedback: z.string().optional(),
420
+ markschemeState: z.any().optional(),
421
+ points: z.number().optional(),
409
422
  }))
410
423
  .mutation(async ({ ctx, input }) => {
411
- const { worksheetId, studentId } = input;
412
-
413
- // Try to find existing response
414
- let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
415
- where: {
416
- worksheetId,
417
- studentId,
418
- submitted: false, // Only get unsubmitted responses
419
- },
420
- include: {
421
- responses: {
422
- include: {
423
- question: true,
424
- },
425
- },
426
- },
427
- });
424
+ const { responseId, questionId, studentWorksheetResponseId, response, isCorrect, feedback, markschemeState, points } = input;
428
425
 
429
- // Create new response if none exists
430
- if (!worksheetResponse) {
431
- worksheetResponse = await prisma.studentWorksheetResponse.create({
426
+ let gradedResponse;
427
+
428
+ if (responseId) {
429
+ // Update existing progress by ID
430
+ gradedResponse = await prisma.studentQuestionProgress.update({
431
+ where: { id: responseId },
432
432
  data: {
433
- worksheetId,
434
- studentId,
435
- },
436
- include: {
437
- responses: {
438
- include: {
439
- question: true,
440
- },
441
- },
433
+ isCorrect,
434
+ ...(feedback !== undefined && { feedback }),
435
+ ...(markschemeState !== undefined && { markschemeState }),
436
+ ...(points !== undefined && { points }),
442
437
  },
443
438
  });
444
- }
439
+ } else {
440
+ // Get the studentId from the worksheet response
441
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
442
+ where: { id: studentWorksheetResponseId },
443
+ select: { studentId: true },
444
+ });
445
445
 
446
- return worksheetResponse;
447
- }),
446
+ if (!worksheetResponse) {
447
+ throw new TRPCError({
448
+ code: 'NOT_FOUND',
449
+ message: 'Student worksheet response not found',
450
+ });
451
+ }
448
452
 
449
- // Get all student responses for a worksheet (teacher view)
450
- getWorksheetResponse: protectedProcedure
451
- .input(z.object({
452
- worksheetId: z.string(),
453
- }))
454
- .query(async ({ ctx, input }) => {
455
- const { worksheetId } = input;
453
+ const { studentId } = worksheetResponse;
456
454
 
457
- const responses = await prisma.studentWorksheetResponse.findFirst({
458
- where: { worksheetId },
459
- include: {
460
- student: {
461
- select: {
462
- id: true,
463
- username: true,
464
- profile: {
465
- select: {
466
- displayName: true,
467
- profilePicture: true,
468
- },
469
- },
470
- },
455
+ // Upsert - find or create the progress record
456
+ const existing = await prisma.studentQuestionProgress.findFirst({
457
+ where: {
458
+ studentId,
459
+ questionId,
460
+ studentWorksheetResponseId,
471
461
  },
472
- responses: {
473
- include: {
474
- question: true,
462
+ });
463
+
464
+ if (existing) {
465
+ // Update existing
466
+ gradedResponse = await prisma.studentQuestionProgress.update({
467
+ where: { id: existing.id },
468
+ data: {
469
+ isCorrect,
470
+ ...(response !== undefined && { response }),
471
+ ...(feedback !== undefined && { feedback }),
472
+ ...(markschemeState !== undefined && { markschemeState }),
473
+ ...(points !== undefined && { points }),
475
474
  },
476
- },
477
- },
478
- orderBy: { submittedAt: 'desc' },
479
- });
475
+ });
476
+ } else {
477
+ // Create new
478
+ gradedResponse = await prisma.studentQuestionProgress.create({
479
+ data: {
480
+ studentId,
481
+ questionId,
482
+ studentWorksheetResponseId,
483
+ response: response || '',
484
+ isCorrect,
485
+ ...(feedback !== undefined && { feedback }),
486
+ ...(markschemeState !== undefined && { markschemeState }),
487
+ ...(points !== undefined && { points: points || 0 }),
488
+ },
489
+ });
490
+ }
491
+ }
480
492
 
481
- return responses;
493
+ return gradedResponse;
482
494
  }),
483
-
484
- // Grade a student's answer
485
- gradeAnswer: protectedProcedure
495
+ addComment: protectedProcedure
486
496
  .input(z.object({
487
- responseId: z.string(), // StudentQuestionProgress ID
488
- isCorrect: z.boolean(),
489
- feedback: z.string().optional(),
497
+ responseId: z.string(),
498
+ comment: z.string(),
490
499
  }))
491
500
  .mutation(async ({ ctx, input }) => {
492
- const { responseId, isCorrect, feedback } = input;
501
+ const { responseId, comment } = input;
493
502
 
494
- const gradedResponse = await prisma.studentQuestionProgress.update({
495
- where: { id: responseId },
503
+ const newComment = await prisma.comment.create({
496
504
  data: {
497
- isCorrect,
498
- ...(feedback !== undefined && { feedback }),
505
+ studentQuestionProgressId: responseId,
506
+ content: comment,
507
+ authorId: ctx.user!.id,
499
508
  },
500
509
  });
501
510
 
502
- return gradedResponse;
511
+ return newComment;
503
512
  }),
504
513
  });