@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.
- package/dist/routers/_app.d.ts +302 -200
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +2 -0
- package/dist/routers/announcement.d.ts +8 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +124 -0
- package/dist/routers/comment.d.ts.map +1 -0
- package/dist/routers/comment.js +241 -0
- package/dist/routers/worksheet.d.ts +34 -109
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +79 -74
- package/package.json +1 -1
- package/prisma/schema.prisma +11 -4
- package/src/routers/_app.ts +2 -0
- package/src/routers/announcement.ts +3 -3
- package/src/routers/comment.ts +268 -0
- package/src/routers/worksheet.ts +80 -77
|
@@ -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
|
+
});
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -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
|
-
//
|
|
411
|
-
|
|
411
|
+
// Grade a student's answer
|
|
412
|
+
gradeAnswer: protectedProcedure
|
|
412
413
|
.input(z.object({
|
|
413
|
-
|
|
414
|
-
|
|
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 {
|
|
424
|
+
const { responseId, questionId, studentWorksheetResponseId, response, isCorrect, feedback, markschemeState, points } = input;
|
|
418
425
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
453
|
-
|
|
446
|
+
if (!worksheetResponse) {
|
|
447
|
+
throw new TRPCError({
|
|
448
|
+
code: 'NOT_FOUND',
|
|
449
|
+
message: 'Student worksheet response not found',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
454
452
|
|
|
455
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
|
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(),
|
|
494
|
-
|
|
495
|
-
feedback: z.string().optional(),
|
|
497
|
+
responseId: z.string(),
|
|
498
|
+
comment: z.string(),
|
|
496
499
|
}))
|
|
497
500
|
.mutation(async ({ ctx, input }) => {
|
|
498
|
-
const { responseId,
|
|
501
|
+
const { responseId, comment } = input;
|
|
499
502
|
|
|
500
|
-
const
|
|
501
|
-
where: { id: responseId },
|
|
503
|
+
const newComment = await prisma.comment.create({
|
|
502
504
|
data: {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
+
studentQuestionProgressId: responseId,
|
|
506
|
+
content: comment,
|
|
507
|
+
authorId: ctx.user!.id,
|
|
505
508
|
},
|
|
506
509
|
});
|
|
507
510
|
|
|
508
|
-
return
|
|
511
|
+
return newComment;
|
|
509
512
|
}),
|
|
510
513
|
});
|