@studious-lms/server 1.2.34 → 1.2.36
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 +362 -204
- 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 +163 -0
- package/dist/routers/comment.d.ts.map +1 -0
- package/dist/routers/comment.js +286 -0
- package/dist/routers/worksheet.d.ts +64 -111
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +94 -76
- 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 +314 -0
- package/src/routers/worksheet.ts +95 -79
|
@@ -0,0 +1,314 @@
|
|
|
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 commentSelect = {
|
|
7
|
+
replies: {
|
|
8
|
+
select: {
|
|
9
|
+
id: true,
|
|
10
|
+
content: true,
|
|
11
|
+
author: {
|
|
12
|
+
select: {
|
|
13
|
+
id: true,
|
|
14
|
+
username: true,
|
|
15
|
+
profile: {
|
|
16
|
+
select: {
|
|
17
|
+
displayName: true,
|
|
18
|
+
profilePicture: true,
|
|
19
|
+
profilePictureThumbnail: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
reactions: {
|
|
27
|
+
select: {
|
|
28
|
+
type: true,
|
|
29
|
+
user: {
|
|
30
|
+
select: {
|
|
31
|
+
id: true,
|
|
32
|
+
username: true,
|
|
33
|
+
profile: {
|
|
34
|
+
select: {
|
|
35
|
+
displayName: true,
|
|
36
|
+
profilePicture: true,
|
|
37
|
+
profilePictureThumbnail: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const commentRouter = createTRPCRouter({
|
|
47
|
+
get: protectedProcedure
|
|
48
|
+
.input(z.object({
|
|
49
|
+
id: z.string(),
|
|
50
|
+
}))
|
|
51
|
+
.query(async ({ ctx, input }) => {
|
|
52
|
+
const comment = await prisma.comment.findUnique({
|
|
53
|
+
where: { id: input.id },
|
|
54
|
+
select: {
|
|
55
|
+
replies: {
|
|
56
|
+
select: {
|
|
57
|
+
id: true,
|
|
58
|
+
content: true,
|
|
59
|
+
author: {
|
|
60
|
+
select: {
|
|
61
|
+
id: true,
|
|
62
|
+
username: true,
|
|
63
|
+
profile: {
|
|
64
|
+
select: {
|
|
65
|
+
displayName: true,
|
|
66
|
+
profilePicture: true,
|
|
67
|
+
profilePictureThumbnail: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
reactions: {
|
|
75
|
+
select: {
|
|
76
|
+
type: true,
|
|
77
|
+
user: {
|
|
78
|
+
select: {
|
|
79
|
+
id: true,
|
|
80
|
+
username: true,
|
|
81
|
+
profile: {
|
|
82
|
+
select: {
|
|
83
|
+
displayName: true,
|
|
84
|
+
profilePicture: true,
|
|
85
|
+
profilePictureThumbnail: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (!comment) {
|
|
95
|
+
throw new TRPCError({
|
|
96
|
+
code: "NOT_FOUND",
|
|
97
|
+
message: "Comment not found",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return comment;
|
|
101
|
+
}),
|
|
102
|
+
replyToComment: protectedProcedure
|
|
103
|
+
.input(z.object({
|
|
104
|
+
parentCommentId: z.string(),
|
|
105
|
+
content: z.string(),
|
|
106
|
+
}))
|
|
107
|
+
.mutation(async ({ ctx, input }) => {
|
|
108
|
+
const { parentCommentId, content } = input;
|
|
109
|
+
|
|
110
|
+
const newComment = await prisma.comment.create({
|
|
111
|
+
data: {
|
|
112
|
+
parentCommentId,
|
|
113
|
+
content,
|
|
114
|
+
authorId: ctx.user!.id,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return newComment;
|
|
119
|
+
}),
|
|
120
|
+
addReaction: protectedProcedure
|
|
121
|
+
.input(z.object({
|
|
122
|
+
id: z.string(),
|
|
123
|
+
type: z.enum(['THUMBSUP', 'CELEBRATE', 'CARE', 'HEART', 'IDEA', 'HAPPY']),
|
|
124
|
+
}))
|
|
125
|
+
.mutation(async ({ ctx, input }) => {
|
|
126
|
+
if (!ctx.user) {
|
|
127
|
+
throw new TRPCError({
|
|
128
|
+
code: "UNAUTHORIZED",
|
|
129
|
+
message: "User must be authenticated",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Exactly one of announcementId or commentId must be provided
|
|
134
|
+
const comment = await prisma.comment.findUnique({
|
|
135
|
+
where: { id: input.id },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const userId = ctx.user.id;
|
|
139
|
+
|
|
140
|
+
// Verify the announcement or comment exists and belongs to the class
|
|
141
|
+
if (comment) {
|
|
142
|
+
const announcement = await prisma.announcement.findFirst({
|
|
143
|
+
where: {
|
|
144
|
+
id: input.id,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Upsert reaction: update if exists, create if not
|
|
149
|
+
const reaction = await prisma.reaction.upsert({
|
|
150
|
+
where: {
|
|
151
|
+
userId_commentId: {
|
|
152
|
+
userId,
|
|
153
|
+
commentId: input.id,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
update: {
|
|
157
|
+
type: input.type,
|
|
158
|
+
},
|
|
159
|
+
create: {
|
|
160
|
+
type: input.type,
|
|
161
|
+
userId,
|
|
162
|
+
commentId: input.id,
|
|
163
|
+
},
|
|
164
|
+
include: {
|
|
165
|
+
user: {
|
|
166
|
+
select: {
|
|
167
|
+
id: true,
|
|
168
|
+
username: true,
|
|
169
|
+
profile: {
|
|
170
|
+
select: {
|
|
171
|
+
displayName: true,
|
|
172
|
+
profilePicture: true,
|
|
173
|
+
profilePictureThumbnail: true,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return { reaction };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new TRPCError({
|
|
185
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
186
|
+
message: "Unexpected error",
|
|
187
|
+
});
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
removeReaction: protectedProcedure
|
|
191
|
+
.input(z.object({
|
|
192
|
+
commentId: z.string().optional(),
|
|
193
|
+
}))
|
|
194
|
+
.mutation(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
|
+
const reaction = await prisma.reaction.findUnique({
|
|
213
|
+
where: {
|
|
214
|
+
userId_commentId: {
|
|
215
|
+
userId,
|
|
216
|
+
commentId: input.commentId,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!reaction) {
|
|
222
|
+
throw new TRPCError({
|
|
223
|
+
code: "NOT_FOUND",
|
|
224
|
+
message: "Reaction not found",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await prisma.reaction.delete({
|
|
229
|
+
where: { id: reaction.id },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return { success: true };
|
|
233
|
+
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
getReactions: protectedProcedure
|
|
237
|
+
.input(z.object({
|
|
238
|
+
commentId: z.string().optional(),
|
|
239
|
+
}))
|
|
240
|
+
.query(async ({ ctx, input }) => {
|
|
241
|
+
if (!ctx.user) {
|
|
242
|
+
throw new TRPCError({
|
|
243
|
+
code: "UNAUTHORIZED",
|
|
244
|
+
message: "User must be authenticated",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Exactly one of announcementId or commentId must be provided
|
|
249
|
+
if (!input.commentId) {
|
|
250
|
+
throw new TRPCError({
|
|
251
|
+
code: "BAD_REQUEST",
|
|
252
|
+
message: "Either announcementId or commentId must be provided",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const userId = ctx.user.id;
|
|
257
|
+
|
|
258
|
+
// Verify comment exists
|
|
259
|
+
const comment = await prisma.comment.findUnique({
|
|
260
|
+
where: { id: input.commentId },
|
|
261
|
+
include: {
|
|
262
|
+
announcement: {
|
|
263
|
+
select: {
|
|
264
|
+
classId: true,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!comment) {
|
|
271
|
+
throw new TRPCError({
|
|
272
|
+
code: "NOT_FOUND",
|
|
273
|
+
message: "Comment not found",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get reaction counts by type
|
|
278
|
+
const reactionCounts = await prisma.reaction.groupBy({
|
|
279
|
+
by: ['type'],
|
|
280
|
+
where: { commentId: input.commentId },
|
|
281
|
+
_count: { type: true },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Get current user's reaction
|
|
285
|
+
const userReaction = await prisma.reaction.findUnique({
|
|
286
|
+
where: {
|
|
287
|
+
userId_commentId: {
|
|
288
|
+
userId,
|
|
289
|
+
commentId: input.commentId,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Format counts
|
|
295
|
+
const counts = {
|
|
296
|
+
THUMBSUP: 0,
|
|
297
|
+
CELEBRATE: 0,
|
|
298
|
+
CARE: 0,
|
|
299
|
+
HEART: 0,
|
|
300
|
+
IDEA: 0,
|
|
301
|
+
HAPPY: 0,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
reactionCounts.forEach((item) => {
|
|
305
|
+
counts[item.type as keyof typeof counts] = item._count.type;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
counts,
|
|
310
|
+
userReaction: userReaction?.type || null,
|
|
311
|
+
total: reactionCounts.reduce((sum, item) => sum + item._count.type, 0),
|
|
312
|
+
};
|
|
313
|
+
}),
|
|
314
|
+
});
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
import { WorksheetQuestionType } from "@prisma/client";
|
|
6
|
+
import { commentSelect } from "./comment.js";
|
|
6
7
|
|
|
7
8
|
export const worksheetRouter = createTRPCRouter({
|
|
8
9
|
// Get a single worksheet with all questions
|
|
@@ -18,6 +19,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
18
19
|
include: {
|
|
19
20
|
questions: {
|
|
20
21
|
orderBy: { createdAt: 'asc' },
|
|
22
|
+
// select: { id: true, type: true, question: true, answer: true, points: true },
|
|
21
23
|
},
|
|
22
24
|
class: true,
|
|
23
25
|
},
|
|
@@ -281,7 +283,13 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
281
283
|
worksheetId
|
|
282
284
|
},
|
|
283
285
|
include: {
|
|
284
|
-
responses:
|
|
286
|
+
responses: {
|
|
287
|
+
include: {
|
|
288
|
+
comments: {
|
|
289
|
+
select: commentSelect,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
285
293
|
},
|
|
286
294
|
});
|
|
287
295
|
|
|
@@ -297,7 +305,13 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
297
305
|
studentId: submission.studentId,
|
|
298
306
|
},
|
|
299
307
|
include: {
|
|
300
|
-
responses:
|
|
308
|
+
responses: {
|
|
309
|
+
include: {
|
|
310
|
+
comments: {
|
|
311
|
+
select: commentSelect,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
301
315
|
},
|
|
302
316
|
});
|
|
303
317
|
|
|
@@ -407,104 +421,106 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
407
421
|
return submittedWorksheet;
|
|
408
422
|
}),
|
|
409
423
|
|
|
410
|
-
//
|
|
411
|
-
|
|
424
|
+
// Grade a student's answer
|
|
425
|
+
gradeAnswer: protectedProcedure
|
|
412
426
|
.input(z.object({
|
|
413
|
-
|
|
414
|
-
|
|
427
|
+
questionId: z.string(),
|
|
428
|
+
responseId: z.string().optional(), // StudentQuestionProgress ID (optional for upsert)
|
|
429
|
+
studentWorksheetResponseId: z.string(), // Required for linking to worksheet response
|
|
430
|
+
response: z.string().optional(), // The actual response text (needed if creating new)
|
|
431
|
+
isCorrect: z.boolean(),
|
|
432
|
+
feedback: z.string().optional(),
|
|
433
|
+
markschemeState: z.any().optional(),
|
|
434
|
+
points: z.number().optional(),
|
|
415
435
|
}))
|
|
416
436
|
.mutation(async ({ ctx, input }) => {
|
|
417
|
-
const {
|
|
437
|
+
const { responseId, questionId, studentWorksheetResponseId, response, isCorrect, feedback, markschemeState, points } = input;
|
|
418
438
|
|
|
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({
|
|
439
|
+
let gradedResponse;
|
|
440
|
+
|
|
441
|
+
if (responseId) {
|
|
442
|
+
// Update existing progress by ID
|
|
443
|
+
gradedResponse = await prisma.studentQuestionProgress.update({
|
|
444
|
+
where: { id: responseId },
|
|
438
445
|
data: {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
responses: {
|
|
444
|
-
include: {
|
|
445
|
-
question: true,
|
|
446
|
-
},
|
|
447
|
-
},
|
|
446
|
+
isCorrect,
|
|
447
|
+
...(feedback !== undefined && { feedback }),
|
|
448
|
+
...(markschemeState !== undefined && { markschemeState }),
|
|
449
|
+
...(points !== undefined && { points }),
|
|
448
450
|
},
|
|
449
451
|
});
|
|
450
|
-
}
|
|
452
|
+
} else {
|
|
453
|
+
// Get the studentId from the worksheet response
|
|
454
|
+
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
455
|
+
where: { id: studentWorksheetResponseId },
|
|
456
|
+
select: { studentId: true },
|
|
457
|
+
});
|
|
451
458
|
|
|
452
|
-
|
|
453
|
-
|
|
459
|
+
if (!worksheetResponse) {
|
|
460
|
+
throw new TRPCError({
|
|
461
|
+
code: 'NOT_FOUND',
|
|
462
|
+
message: 'Student worksheet response not found',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
454
465
|
|
|
455
|
-
|
|
456
|
-
getWorksheetResponse: protectedProcedure
|
|
457
|
-
.input(z.object({
|
|
458
|
-
worksheetId: z.string(),
|
|
459
|
-
}))
|
|
460
|
-
.query(async ({ ctx, input }) => {
|
|
461
|
-
const { worksheetId } = input;
|
|
466
|
+
const { studentId } = worksheetResponse;
|
|
462
467
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
username: true,
|
|
470
|
-
profile: {
|
|
471
|
-
select: {
|
|
472
|
-
displayName: true,
|
|
473
|
-
profilePicture: true,
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
},
|
|
468
|
+
// Upsert - find or create the progress record
|
|
469
|
+
const existing = await prisma.studentQuestionProgress.findFirst({
|
|
470
|
+
where: {
|
|
471
|
+
studentId,
|
|
472
|
+
questionId,
|
|
473
|
+
studentWorksheetResponseId,
|
|
477
474
|
},
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (existing) {
|
|
478
|
+
// Update existing
|
|
479
|
+
gradedResponse = await prisma.studentQuestionProgress.update({
|
|
480
|
+
where: { id: existing.id },
|
|
481
|
+
data: {
|
|
482
|
+
isCorrect,
|
|
483
|
+
...(response !== undefined && { response }),
|
|
484
|
+
...(feedback !== undefined && { feedback }),
|
|
485
|
+
...(markschemeState !== undefined && { markschemeState }),
|
|
486
|
+
...(points !== undefined && { points }),
|
|
481
487
|
},
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
// Create new
|
|
491
|
+
gradedResponse = await prisma.studentQuestionProgress.create({
|
|
492
|
+
data: {
|
|
493
|
+
studentId,
|
|
494
|
+
questionId,
|
|
495
|
+
studentWorksheetResponseId,
|
|
496
|
+
response: response || '',
|
|
497
|
+
isCorrect,
|
|
498
|
+
...(feedback !== undefined && { feedback }),
|
|
499
|
+
...(markschemeState !== undefined && { markschemeState }),
|
|
500
|
+
...(points !== undefined && { points: points || 0 }),
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
486
505
|
|
|
487
|
-
return
|
|
506
|
+
return gradedResponse;
|
|
488
507
|
}),
|
|
489
|
-
|
|
490
|
-
// Grade a student's answer
|
|
491
|
-
gradeAnswer: protectedProcedure
|
|
508
|
+
addComment: protectedProcedure
|
|
492
509
|
.input(z.object({
|
|
493
|
-
responseId: z.string(),
|
|
494
|
-
|
|
495
|
-
feedback: z.string().optional(),
|
|
510
|
+
responseId: z.string(),
|
|
511
|
+
comment: z.string(),
|
|
496
512
|
}))
|
|
497
513
|
.mutation(async ({ ctx, input }) => {
|
|
498
|
-
const { responseId,
|
|
514
|
+
const { responseId, comment } = input;
|
|
499
515
|
|
|
500
|
-
const
|
|
501
|
-
where: { id: responseId },
|
|
516
|
+
const newComment = await prisma.comment.create({
|
|
502
517
|
data: {
|
|
503
|
-
|
|
504
|
-
|
|
518
|
+
studentQuestionProgressId: responseId,
|
|
519
|
+
content: comment,
|
|
520
|
+
authorId: ctx.user!.id,
|
|
505
521
|
},
|
|
506
522
|
});
|
|
507
523
|
|
|
508
|
-
return
|
|
524
|
+
return newComment;
|
|
509
525
|
}),
|
|
510
526
|
});
|