@studious-lms/server 1.2.27 → 1.2.28
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 +772 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +2 -0
- package/dist/routers/assignment.d.ts +16 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/auth.js +1 -1
- package/dist/routers/class.d.ts +8 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -0
- package/dist/routers/event.d.ts +2 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/worksheet.d.ts +362 -0
- package/dist/routers/worksheet.d.ts.map +1 -0
- package/dist/routers/worksheet.js +365 -0
- package/package.json +1 -1
- package/prisma/schema.prisma +70 -1
- package/src/routers/_app.ts +2 -0
- package/src/routers/auth.ts +1 -1
- package/src/routers/class.ts +3 -0
- package/src/routers/worksheet.ts +419 -0
|
@@ -0,0 +1,419 @@
|
|
|
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
|
+
markScheme: z.any().optional(), // JSON field
|
|
115
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
|
|
116
|
+
}))
|
|
117
|
+
.mutation(async ({ ctx, input }) => {
|
|
118
|
+
const { worksheetId, question, answer, markScheme, type } = input;
|
|
119
|
+
|
|
120
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
121
|
+
where: { id: worksheetId },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!worksheet) {
|
|
125
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const newQuestion = await prisma.worksheetQuestion.create({
|
|
129
|
+
data: {
|
|
130
|
+
worksheetId,
|
|
131
|
+
type,
|
|
132
|
+
question,
|
|
133
|
+
answer,
|
|
134
|
+
markScheme,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return newQuestion;
|
|
139
|
+
}),
|
|
140
|
+
updateQuestion: protectedProcedure
|
|
141
|
+
.input(z.object({
|
|
142
|
+
worksheetId: z.string(),
|
|
143
|
+
questionId: z.string(),
|
|
144
|
+
question: z.string().optional(),
|
|
145
|
+
answer: z.string().optional(),
|
|
146
|
+
markScheme: z.any().optional(), // JSON field
|
|
147
|
+
type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']).optional(),
|
|
148
|
+
}))
|
|
149
|
+
.mutation(async ({ ctx, input }) => {
|
|
150
|
+
const { worksheetId, questionId, question, answer, markScheme, type } = input;
|
|
151
|
+
|
|
152
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
153
|
+
where: { id: worksheetId },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!worksheet) {
|
|
157
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const updatedQuestion = await prisma.worksheetQuestion.update({
|
|
161
|
+
where: { id: questionId },
|
|
162
|
+
data: {
|
|
163
|
+
...(question !== undefined && { question }),
|
|
164
|
+
...(answer !== undefined && { answer }),
|
|
165
|
+
...(markScheme !== undefined && { markScheme }),
|
|
166
|
+
...(type !== undefined && { type }),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return updatedQuestion;
|
|
171
|
+
}),
|
|
172
|
+
deleteQuestion: protectedProcedure
|
|
173
|
+
.input(z.object({
|
|
174
|
+
worksheetId: z.string(),
|
|
175
|
+
questionId: z.string(),
|
|
176
|
+
}))
|
|
177
|
+
.mutation(async ({ ctx, input }) => {
|
|
178
|
+
const { worksheetId, questionId } = input;
|
|
179
|
+
|
|
180
|
+
const worksheet = await prisma.worksheet.findUnique({
|
|
181
|
+
where: { id: worksheetId },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!worksheet) {
|
|
185
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const deletedQuestion = await prisma.worksheetQuestion.delete({
|
|
189
|
+
where: { id: questionId },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return deletedQuestion;
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
getWorksheetSubmission: protectedProcedure
|
|
196
|
+
.input(z.object({
|
|
197
|
+
worksheetId: z.string(),
|
|
198
|
+
submissionId: z.string(),
|
|
199
|
+
}))
|
|
200
|
+
.query(async ({ ctx, input }) => {
|
|
201
|
+
const { worksheetId, submissionId } = input;
|
|
202
|
+
|
|
203
|
+
const submission = await prisma.submission.findUnique({
|
|
204
|
+
where: { id: submissionId },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!submission) {
|
|
208
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Submission not found' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
|
|
212
|
+
where: { submissionId },
|
|
213
|
+
include: {
|
|
214
|
+
responses: true,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return worksheetResponses;
|
|
219
|
+
}),
|
|
220
|
+
answerQuestion: protectedProcedure
|
|
221
|
+
.input(z.object({
|
|
222
|
+
worksheetResponseId: z.string(),
|
|
223
|
+
questionId: z.string(),
|
|
224
|
+
response: z.string(),
|
|
225
|
+
}))
|
|
226
|
+
.mutation(async ({ ctx, input }) => {
|
|
227
|
+
const { worksheetResponseId, questionId, response } = input;
|
|
228
|
+
|
|
229
|
+
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
230
|
+
where: { id: worksheetResponseId },
|
|
231
|
+
include: {
|
|
232
|
+
responses: {
|
|
233
|
+
where: { questionId },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!worksheetResponse) {
|
|
239
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if a response already exists for this question
|
|
243
|
+
const existingResponse = worksheetResponse.responses[0];
|
|
244
|
+
|
|
245
|
+
if (existingResponse) {
|
|
246
|
+
// Update existing response
|
|
247
|
+
await prisma.studentQuestionProgress.update({
|
|
248
|
+
where: { id: existingResponse.id },
|
|
249
|
+
data: { response },
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
// Create new response
|
|
253
|
+
await prisma.studentQuestionProgress.create({
|
|
254
|
+
data: {
|
|
255
|
+
studentId: worksheetResponse.studentId,
|
|
256
|
+
questionId,
|
|
257
|
+
response,
|
|
258
|
+
studentWorksheetResponseId: worksheetResponseId,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return the updated worksheet response with all responses
|
|
264
|
+
const updatedWorksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
265
|
+
where: { id: worksheetResponseId },
|
|
266
|
+
include: {
|
|
267
|
+
responses: true,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return updatedWorksheetResponse;
|
|
272
|
+
}),
|
|
273
|
+
submitWorksheet: protectedProcedure
|
|
274
|
+
.input(z.object({
|
|
275
|
+
worksheetResponseId: z.string(),
|
|
276
|
+
}))
|
|
277
|
+
.mutation(async ({ ctx, input }) => {
|
|
278
|
+
const { worksheetResponseId } = input;
|
|
279
|
+
|
|
280
|
+
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
281
|
+
where: { id: worksheetResponseId },
|
|
282
|
+
include: {
|
|
283
|
+
worksheet: {
|
|
284
|
+
include: {
|
|
285
|
+
questions: true,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
responses: true,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!worksheetResponse) {
|
|
293
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (worksheetResponse.submitted) {
|
|
297
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Mark worksheet as submitted
|
|
301
|
+
const submittedWorksheet = await prisma.studentWorksheetResponse.update({
|
|
302
|
+
where: { id: worksheetResponseId },
|
|
303
|
+
data: {
|
|
304
|
+
submitted: true,
|
|
305
|
+
submittedAt: new Date(),
|
|
306
|
+
},
|
|
307
|
+
include: {
|
|
308
|
+
responses: true,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// TODO: Implement AI grading here
|
|
313
|
+
// For now, we'll just mark all answers as pending review
|
|
314
|
+
// You could integrate with an AI service to auto-grade certain question types
|
|
315
|
+
|
|
316
|
+
return submittedWorksheet;
|
|
317
|
+
}),
|
|
318
|
+
|
|
319
|
+
// Get or create a student's worksheet response
|
|
320
|
+
getOrCreateWorksheetResponse: protectedProcedure
|
|
321
|
+
.input(z.object({
|
|
322
|
+
worksheetId: z.string(),
|
|
323
|
+
studentId: z.string(),
|
|
324
|
+
}))
|
|
325
|
+
.mutation(async ({ ctx, input }) => {
|
|
326
|
+
const { worksheetId, studentId } = input;
|
|
327
|
+
|
|
328
|
+
// Try to find existing response
|
|
329
|
+
let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
|
|
330
|
+
where: {
|
|
331
|
+
worksheetId,
|
|
332
|
+
studentId,
|
|
333
|
+
submitted: false, // Only get unsubmitted responses
|
|
334
|
+
},
|
|
335
|
+
include: {
|
|
336
|
+
responses: {
|
|
337
|
+
include: {
|
|
338
|
+
question: true,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Create new response if none exists
|
|
345
|
+
if (!worksheetResponse) {
|
|
346
|
+
worksheetResponse = await prisma.studentWorksheetResponse.create({
|
|
347
|
+
data: {
|
|
348
|
+
worksheetId,
|
|
349
|
+
studentId,
|
|
350
|
+
},
|
|
351
|
+
include: {
|
|
352
|
+
responses: {
|
|
353
|
+
include: {
|
|
354
|
+
question: true,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return worksheetResponse;
|
|
362
|
+
}),
|
|
363
|
+
|
|
364
|
+
// Get all student responses for a worksheet (teacher view)
|
|
365
|
+
getWorksheetResponses: protectedProcedure
|
|
366
|
+
.input(z.object({
|
|
367
|
+
worksheetId: z.string(),
|
|
368
|
+
}))
|
|
369
|
+
.query(async ({ ctx, input }) => {
|
|
370
|
+
const { worksheetId } = input;
|
|
371
|
+
|
|
372
|
+
const responses = await prisma.studentWorksheetResponse.findMany({
|
|
373
|
+
where: { worksheetId },
|
|
374
|
+
include: {
|
|
375
|
+
student: {
|
|
376
|
+
select: {
|
|
377
|
+
id: true,
|
|
378
|
+
username: true,
|
|
379
|
+
profile: {
|
|
380
|
+
select: {
|
|
381
|
+
displayName: true,
|
|
382
|
+
profilePicture: true,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
responses: {
|
|
388
|
+
include: {
|
|
389
|
+
question: true,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
orderBy: { submittedAt: 'desc' },
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return responses;
|
|
397
|
+
}),
|
|
398
|
+
|
|
399
|
+
// Grade a student's answer
|
|
400
|
+
gradeAnswer: protectedProcedure
|
|
401
|
+
.input(z.object({
|
|
402
|
+
responseId: z.string(), // StudentQuestionProgress ID
|
|
403
|
+
isCorrect: z.boolean(),
|
|
404
|
+
feedback: z.string().optional(),
|
|
405
|
+
}))
|
|
406
|
+
.mutation(async ({ ctx, input }) => {
|
|
407
|
+
const { responseId, isCorrect, feedback } = input;
|
|
408
|
+
|
|
409
|
+
const gradedResponse = await prisma.studentQuestionProgress.update({
|
|
410
|
+
where: { id: responseId },
|
|
411
|
+
data: {
|
|
412
|
+
isCorrect,
|
|
413
|
+
...(feedback !== undefined && { feedback }),
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return gradedResponse;
|
|
418
|
+
}),
|
|
419
|
+
});
|