@studious-lms/server 1.2.27 → 1.2.29

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,422 @@
1
+ import { TRPCError } from "@trpc/server";
2
+ import { createTRPCRouter, protectedProcedure } from "src/trpc";
3
+ import { z } from "zod";
4
+ import { prisma } from "src/lib/prisma";
5
+
6
+ export const worksheetRouter = createTRPCRouter({
7
+ // Get a single worksheet with all questions
8
+ getWorksheet: protectedProcedure
9
+ .input(z.object({
10
+ worksheetId: z.string(),
11
+ }))
12
+ .query(async ({ ctx, input }) => {
13
+ const { worksheetId } = input;
14
+
15
+ const worksheet = await prisma.worksheet.findUnique({
16
+ where: { id: worksheetId },
17
+ include: {
18
+ questions: {
19
+ orderBy: { createdAt: 'asc' },
20
+ },
21
+ class: true,
22
+ },
23
+ });
24
+
25
+ if (!worksheet) {
26
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
27
+ }
28
+
29
+ return worksheet;
30
+ }),
31
+
32
+ // List all worksheets for a class
33
+ listWorksheets: protectedProcedure
34
+ .input(z.object({
35
+ classId: z.string(),
36
+ }))
37
+ .query(async ({ ctx, input }) => {
38
+ const { classId } = input;
39
+
40
+ const worksheets = await prisma.worksheet.findMany({
41
+ where: { classId },
42
+ include: {
43
+ questions: {
44
+ select: { id: true },
45
+ },
46
+ },
47
+ orderBy: { createdAt: 'desc' },
48
+ });
49
+
50
+ return worksheets.map(worksheet => ({
51
+ ...worksheet,
52
+ questionCount: worksheet.questions.length,
53
+ }));
54
+ }),
55
+
56
+ // Update worksheet metadata
57
+ updateWorksheet: protectedProcedure
58
+ .input(z.object({
59
+ worksheetId: z.string(),
60
+ name: z.string().optional(),
61
+ }))
62
+ .mutation(async ({ ctx, input }) => {
63
+ const { worksheetId, name } = input;
64
+
65
+ const worksheet = await prisma.worksheet.update({
66
+ where: { id: worksheetId },
67
+ data: {
68
+ ...(name !== undefined && { name }),
69
+ },
70
+ });
71
+
72
+ return worksheet;
73
+ }),
74
+
75
+ // Delete a worksheet
76
+ deleteWorksheet: protectedProcedure
77
+ .input(z.object({
78
+ worksheetId: z.string(),
79
+ }))
80
+ .mutation(async ({ ctx, input }) => {
81
+ const { worksheetId } = input;
82
+
83
+ // This will cascade delete all questions and responses
84
+ const deletedWorksheet = await prisma.worksheet.delete({
85
+ where: { id: worksheetId },
86
+ });
87
+
88
+ return deletedWorksheet;
89
+ }),
90
+
91
+ create: protectedProcedure
92
+ .input(z.object({
93
+ classId: z.string(),
94
+ name: z.string(),
95
+ }))
96
+ .mutation(async ({ ctx, input }) => {
97
+ const { classId, name } = input;
98
+
99
+ // Create the worksheet
100
+ const worksheet = await prisma.worksheet.create({
101
+ data: {
102
+ name,
103
+ classId,
104
+ },
105
+ });
106
+
107
+ return worksheet;
108
+ }),
109
+ addQuestion: protectedProcedure
110
+ .input(z.object({
111
+ worksheetId: z.string(),
112
+ question: z.string(),
113
+ answer: z.string(),
114
+ options: z.any().optional(), // JSON field
115
+ markScheme: z.any().optional(), // JSON field
116
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
117
+ }))
118
+ .mutation(async ({ ctx, input }) => {
119
+ const { worksheetId, question, answer, options, markScheme, type } = input;
120
+
121
+ const worksheet = await prisma.worksheet.findUnique({
122
+ where: { id: worksheetId },
123
+ });
124
+
125
+ if (!worksheet) {
126
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
127
+ }
128
+
129
+ const newQuestion = await prisma.worksheetQuestion.create({
130
+ data: {
131
+ worksheetId,
132
+ type,
133
+ question,
134
+ answer,
135
+ options,
136
+ markScheme,
137
+ },
138
+ });
139
+
140
+ return newQuestion;
141
+ }),
142
+ updateQuestion: protectedProcedure
143
+ .input(z.object({
144
+ worksheetId: z.string(),
145
+ questionId: z.string(),
146
+ question: z.string().optional(),
147
+ answer: z.string().optional(),
148
+ options: z.any().optional(), // JSON field
149
+ markScheme: z.any().optional(), // JSON field
150
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']).optional(),
151
+ }))
152
+ .mutation(async ({ ctx, input }) => {
153
+ const { worksheetId, questionId, question, answer, options, markScheme, type } = input;
154
+
155
+ const worksheet = await prisma.worksheet.findUnique({
156
+ where: { id: worksheetId },
157
+ });
158
+
159
+ if (!worksheet) {
160
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
161
+ }
162
+
163
+ const updatedQuestion = await prisma.worksheetQuestion.update({
164
+ where: { id: questionId },
165
+ data: {
166
+ ...(question !== undefined && { question }),
167
+ ...(answer !== undefined && { answer }),
168
+ ...(markScheme !== undefined && { markScheme }),
169
+ ...(type !== undefined && { type }),
170
+ },
171
+ });
172
+
173
+ return updatedQuestion;
174
+ }),
175
+ deleteQuestion: protectedProcedure
176
+ .input(z.object({
177
+ worksheetId: z.string(),
178
+ questionId: z.string(),
179
+ }))
180
+ .mutation(async ({ ctx, input }) => {
181
+ const { worksheetId, questionId } = input;
182
+
183
+ const worksheet = await prisma.worksheet.findUnique({
184
+ where: { id: worksheetId },
185
+ });
186
+
187
+ if (!worksheet) {
188
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
189
+ }
190
+
191
+ const deletedQuestion = await prisma.worksheetQuestion.delete({
192
+ where: { id: questionId },
193
+ });
194
+
195
+ return deletedQuestion;
196
+ }),
197
+
198
+ getWorksheetSubmission: protectedProcedure
199
+ .input(z.object({
200
+ worksheetId: z.string(),
201
+ submissionId: z.string(),
202
+ }))
203
+ .query(async ({ ctx, input }) => {
204
+ const { worksheetId, submissionId } = input;
205
+
206
+ const submission = await prisma.submission.findUnique({
207
+ where: { id: submissionId },
208
+ });
209
+
210
+ if (!submission) {
211
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Submission not found' });
212
+ }
213
+
214
+ const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
215
+ where: { submissionId },
216
+ include: {
217
+ responses: true,
218
+ },
219
+ });
220
+
221
+ return worksheetResponses;
222
+ }),
223
+ answerQuestion: protectedProcedure
224
+ .input(z.object({
225
+ worksheetResponseId: z.string(),
226
+ questionId: z.string(),
227
+ response: z.string(),
228
+ }))
229
+ .mutation(async ({ ctx, input }) => {
230
+ const { worksheetResponseId, questionId, response } = input;
231
+
232
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
233
+ where: { id: worksheetResponseId },
234
+ include: {
235
+ responses: {
236
+ where: { questionId },
237
+ },
238
+ },
239
+ });
240
+
241
+ if (!worksheetResponse) {
242
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
243
+ }
244
+
245
+ // Check if a response already exists for this question
246
+ const existingResponse = worksheetResponse.responses[0];
247
+
248
+ if (existingResponse) {
249
+ // Update existing response
250
+ await prisma.studentQuestionProgress.update({
251
+ where: { id: existingResponse.id },
252
+ data: { response },
253
+ });
254
+ } else {
255
+ // Create new response
256
+ await prisma.studentQuestionProgress.create({
257
+ data: {
258
+ studentId: worksheetResponse.studentId,
259
+ questionId,
260
+ response,
261
+ studentWorksheetResponseId: worksheetResponseId,
262
+ },
263
+ });
264
+ }
265
+
266
+ // Return the updated worksheet response with all responses
267
+ const updatedWorksheetResponse = await prisma.studentWorksheetResponse.findUnique({
268
+ where: { id: worksheetResponseId },
269
+ include: {
270
+ responses: true,
271
+ },
272
+ });
273
+
274
+ return updatedWorksheetResponse;
275
+ }),
276
+ submitWorksheet: protectedProcedure
277
+ .input(z.object({
278
+ worksheetResponseId: z.string(),
279
+ }))
280
+ .mutation(async ({ ctx, input }) => {
281
+ const { worksheetResponseId } = input;
282
+
283
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
284
+ where: { id: worksheetResponseId },
285
+ include: {
286
+ worksheet: {
287
+ include: {
288
+ questions: true,
289
+ },
290
+ },
291
+ responses: true,
292
+ },
293
+ });
294
+
295
+ if (!worksheetResponse) {
296
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
297
+ }
298
+
299
+ if (worksheetResponse.submitted) {
300
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
301
+ }
302
+
303
+ // Mark worksheet as submitted
304
+ const submittedWorksheet = await prisma.studentWorksheetResponse.update({
305
+ where: { id: worksheetResponseId },
306
+ data: {
307
+ submitted: true,
308
+ submittedAt: new Date(),
309
+ },
310
+ include: {
311
+ responses: true,
312
+ },
313
+ });
314
+
315
+ // TODO: Implement AI grading here
316
+ // For now, we'll just mark all answers as pending review
317
+ // You could integrate with an AI service to auto-grade certain question types
318
+
319
+ return submittedWorksheet;
320
+ }),
321
+
322
+ // Get or create a student's worksheet response
323
+ getOrCreateWorksheetResponse: protectedProcedure
324
+ .input(z.object({
325
+ worksheetId: z.string(),
326
+ studentId: z.string(),
327
+ }))
328
+ .mutation(async ({ ctx, input }) => {
329
+ const { worksheetId, studentId } = input;
330
+
331
+ // Try to find existing response
332
+ let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
333
+ where: {
334
+ worksheetId,
335
+ studentId,
336
+ submitted: false, // Only get unsubmitted responses
337
+ },
338
+ include: {
339
+ responses: {
340
+ include: {
341
+ question: true,
342
+ },
343
+ },
344
+ },
345
+ });
346
+
347
+ // Create new response if none exists
348
+ if (!worksheetResponse) {
349
+ worksheetResponse = await prisma.studentWorksheetResponse.create({
350
+ data: {
351
+ worksheetId,
352
+ studentId,
353
+ },
354
+ include: {
355
+ responses: {
356
+ include: {
357
+ question: true,
358
+ },
359
+ },
360
+ },
361
+ });
362
+ }
363
+
364
+ return worksheetResponse;
365
+ }),
366
+
367
+ // Get all student responses for a worksheet (teacher view)
368
+ getWorksheetResponses: protectedProcedure
369
+ .input(z.object({
370
+ worksheetId: z.string(),
371
+ }))
372
+ .query(async ({ ctx, input }) => {
373
+ const { worksheetId } = input;
374
+
375
+ const responses = await prisma.studentWorksheetResponse.findMany({
376
+ where: { worksheetId },
377
+ include: {
378
+ student: {
379
+ select: {
380
+ id: true,
381
+ username: true,
382
+ profile: {
383
+ select: {
384
+ displayName: true,
385
+ profilePicture: true,
386
+ },
387
+ },
388
+ },
389
+ },
390
+ responses: {
391
+ include: {
392
+ question: true,
393
+ },
394
+ },
395
+ },
396
+ orderBy: { submittedAt: 'desc' },
397
+ });
398
+
399
+ return responses;
400
+ }),
401
+
402
+ // Grade a student's answer
403
+ gradeAnswer: protectedProcedure
404
+ .input(z.object({
405
+ responseId: z.string(), // StudentQuestionProgress ID
406
+ isCorrect: z.boolean(),
407
+ feedback: z.string().optional(),
408
+ }))
409
+ .mutation(async ({ ctx, input }) => {
410
+ const { responseId, isCorrect, feedback } = input;
411
+
412
+ const gradedResponse = await prisma.studentQuestionProgress.update({
413
+ where: { id: responseId },
414
+ data: {
415
+ isCorrect,
416
+ ...(feedback !== undefined && { feedback }),
417
+ },
418
+ });
419
+
420
+ return gradedResponse;
421
+ }),
422
+ });