@studious-lms/server 1.2.34 → 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.
@@ -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
+ });
@@ -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
  },
@@ -407,104 +408,106 @@ export const worksheetRouter = createTRPCRouter({
407
408
  return submittedWorksheet;
408
409
  }),
409
410
 
410
- // Get or create a student's worksheet response
411
- getOrCreateWorksheetResponse: protectedProcedure
411
+ // Grade a student's answer
412
+ gradeAnswer: protectedProcedure
412
413
  .input(z.object({
413
- worksheetId: z.string(),
414
- 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(),
415
422
  }))
416
423
  .mutation(async ({ ctx, input }) => {
417
- const { worksheetId, studentId } = input;
424
+ const { responseId, questionId, studentWorksheetResponseId, response, isCorrect, feedback, markschemeState, points } = input;
418
425
 
419
- // Try to find existing response
420
- let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
421
- where: {
422
- worksheetId,
423
- studentId,
424
- submitted: false, // Only get unsubmitted responses
425
- },
426
- include: {
427
- responses: {
428
- include: {
429
- question: true,
430
- },
431
- },
432
- },
433
- });
434
-
435
- // Create new response if none exists
436
- if (!worksheetResponse) {
437
- 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 },
438
432
  data: {
439
- worksheetId,
440
- studentId,
441
- },
442
- include: {
443
- responses: {
444
- include: {
445
- question: true,
446
- },
447
- },
433
+ isCorrect,
434
+ ...(feedback !== undefined && { feedback }),
435
+ ...(markschemeState !== undefined && { markschemeState }),
436
+ ...(points !== undefined && { points }),
448
437
  },
449
438
  });
450
- }
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
+ });
451
445
 
452
- return worksheetResponse;
453
- }),
446
+ if (!worksheetResponse) {
447
+ throw new TRPCError({
448
+ code: 'NOT_FOUND',
449
+ message: 'Student worksheet response not found',
450
+ });
451
+ }
454
452
 
455
- // Get all student responses for a worksheet (teacher view)
456
- getWorksheetResponse: protectedProcedure
457
- .input(z.object({
458
- worksheetId: z.string(),
459
- }))
460
- .query(async ({ ctx, input }) => {
461
- const { worksheetId } = input;
453
+ const { studentId } = worksheetResponse;
462
454
 
463
- const responses = await prisma.studentWorksheetResponse.findFirst({
464
- where: { worksheetId },
465
- include: {
466
- student: {
467
- select: {
468
- id: true,
469
- username: true,
470
- profile: {
471
- select: {
472
- displayName: true,
473
- profilePicture: true,
474
- },
475
- },
476
- },
455
+ // Upsert - find or create the progress record
456
+ const existing = await prisma.studentQuestionProgress.findFirst({
457
+ where: {
458
+ studentId,
459
+ questionId,
460
+ studentWorksheetResponseId,
477
461
  },
478
- responses: {
479
- include: {
480
- 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 }),
481
474
  },
482
- },
483
- },
484
- orderBy: { submittedAt: 'desc' },
485
- });
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
+ }
486
492
 
487
- return responses;
493
+ return gradedResponse;
488
494
  }),
489
-
490
- // Grade a student's answer
491
- gradeAnswer: protectedProcedure
495
+ addComment: protectedProcedure
492
496
  .input(z.object({
493
- responseId: z.string(), // StudentQuestionProgress ID
494
- isCorrect: z.boolean(),
495
- feedback: z.string().optional(),
497
+ responseId: z.string(),
498
+ comment: z.string(),
496
499
  }))
497
500
  .mutation(async ({ ctx, input }) => {
498
- const { responseId, isCorrect, feedback } = input;
501
+ const { responseId, comment } = input;
499
502
 
500
- const gradedResponse = await prisma.studentQuestionProgress.update({
501
- where: { id: responseId },
503
+ const newComment = await prisma.comment.create({
502
504
  data: {
503
- isCorrect,
504
- ...(feedback !== undefined && { feedback }),
505
+ studentQuestionProgressId: responseId,
506
+ content: comment,
507
+ authorId: ctx.user!.id,
505
508
  },
506
509
  });
507
510
 
508
- return gradedResponse;
511
+ return newComment;
509
512
  }),
510
513
  });