@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.
@@ -0,0 +1,365 @@
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
+ export const worksheetRouter = createTRPCRouter({
6
+ // Get a single worksheet with all questions
7
+ getWorksheet: protectedProcedure
8
+ .input(z.object({
9
+ worksheetId: z.string(),
10
+ }))
11
+ .query(async ({ ctx, input }) => {
12
+ const { worksheetId } = input;
13
+ const worksheet = await prisma.worksheet.findUnique({
14
+ where: { id: worksheetId },
15
+ include: {
16
+ questions: {
17
+ orderBy: { createdAt: 'asc' },
18
+ },
19
+ class: true,
20
+ },
21
+ });
22
+ if (!worksheet) {
23
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
24
+ }
25
+ return worksheet;
26
+ }),
27
+ // List all worksheets for a class
28
+ listWorksheets: protectedProcedure
29
+ .input(z.object({
30
+ classId: z.string(),
31
+ }))
32
+ .query(async ({ ctx, input }) => {
33
+ const { classId } = input;
34
+ const worksheets = await prisma.worksheet.findMany({
35
+ where: { classId },
36
+ include: {
37
+ questions: {
38
+ select: { id: true },
39
+ },
40
+ },
41
+ orderBy: { createdAt: 'desc' },
42
+ });
43
+ return worksheets.map(worksheet => ({
44
+ ...worksheet,
45
+ questionCount: worksheet.questions.length,
46
+ }));
47
+ }),
48
+ // Update worksheet metadata
49
+ updateWorksheet: protectedProcedure
50
+ .input(z.object({
51
+ worksheetId: z.string(),
52
+ name: z.string().optional(),
53
+ }))
54
+ .mutation(async ({ ctx, input }) => {
55
+ const { worksheetId, name } = input;
56
+ const worksheet = await prisma.worksheet.update({
57
+ where: { id: worksheetId },
58
+ data: {
59
+ ...(name !== undefined && { name }),
60
+ },
61
+ });
62
+ return worksheet;
63
+ }),
64
+ // Delete a worksheet
65
+ deleteWorksheet: protectedProcedure
66
+ .input(z.object({
67
+ worksheetId: z.string(),
68
+ }))
69
+ .mutation(async ({ ctx, input }) => {
70
+ const { worksheetId } = input;
71
+ // This will cascade delete all questions and responses
72
+ const deletedWorksheet = await prisma.worksheet.delete({
73
+ where: { id: worksheetId },
74
+ });
75
+ return deletedWorksheet;
76
+ }),
77
+ create: protectedProcedure
78
+ .input(z.object({
79
+ classId: z.string(),
80
+ name: z.string(),
81
+ }))
82
+ .mutation(async ({ ctx, input }) => {
83
+ const { classId, name } = input;
84
+ // Create the worksheet
85
+ const worksheet = await prisma.worksheet.create({
86
+ data: {
87
+ name,
88
+ classId,
89
+ },
90
+ });
91
+ return worksheet;
92
+ }),
93
+ addQuestion: protectedProcedure
94
+ .input(z.object({
95
+ worksheetId: z.string(),
96
+ question: z.string(),
97
+ answer: z.string(),
98
+ markScheme: z.any().optional(), // JSON field
99
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
100
+ }))
101
+ .mutation(async ({ ctx, input }) => {
102
+ const { worksheetId, question, answer, markScheme, type } = input;
103
+ const worksheet = await prisma.worksheet.findUnique({
104
+ where: { id: worksheetId },
105
+ });
106
+ if (!worksheet) {
107
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
108
+ }
109
+ const newQuestion = await prisma.worksheetQuestion.create({
110
+ data: {
111
+ worksheetId,
112
+ type,
113
+ question,
114
+ answer,
115
+ markScheme,
116
+ },
117
+ });
118
+ return newQuestion;
119
+ }),
120
+ updateQuestion: protectedProcedure
121
+ .input(z.object({
122
+ worksheetId: z.string(),
123
+ questionId: z.string(),
124
+ question: z.string().optional(),
125
+ answer: z.string().optional(),
126
+ markScheme: z.any().optional(), // JSON field
127
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']).optional(),
128
+ }))
129
+ .mutation(async ({ ctx, input }) => {
130
+ const { worksheetId, questionId, question, answer, markScheme, type } = input;
131
+ const worksheet = await prisma.worksheet.findUnique({
132
+ where: { id: worksheetId },
133
+ });
134
+ if (!worksheet) {
135
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
136
+ }
137
+ const updatedQuestion = await prisma.worksheetQuestion.update({
138
+ where: { id: questionId },
139
+ data: {
140
+ ...(question !== undefined && { question }),
141
+ ...(answer !== undefined && { answer }),
142
+ ...(markScheme !== undefined && { markScheme }),
143
+ ...(type !== undefined && { type }),
144
+ },
145
+ });
146
+ return updatedQuestion;
147
+ }),
148
+ deleteQuestion: protectedProcedure
149
+ .input(z.object({
150
+ worksheetId: z.string(),
151
+ questionId: z.string(),
152
+ }))
153
+ .mutation(async ({ ctx, input }) => {
154
+ const { worksheetId, questionId } = input;
155
+ const worksheet = await prisma.worksheet.findUnique({
156
+ where: { id: worksheetId },
157
+ });
158
+ if (!worksheet) {
159
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
160
+ }
161
+ const deletedQuestion = await prisma.worksheetQuestion.delete({
162
+ where: { id: questionId },
163
+ });
164
+ return deletedQuestion;
165
+ }),
166
+ getWorksheetSubmission: protectedProcedure
167
+ .input(z.object({
168
+ worksheetId: z.string(),
169
+ submissionId: z.string(),
170
+ }))
171
+ .query(async ({ ctx, input }) => {
172
+ const { worksheetId, submissionId } = input;
173
+ const submission = await prisma.submission.findUnique({
174
+ where: { id: submissionId },
175
+ });
176
+ if (!submission) {
177
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Submission not found' });
178
+ }
179
+ const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
180
+ where: { submissionId },
181
+ include: {
182
+ responses: true,
183
+ },
184
+ });
185
+ return worksheetResponses;
186
+ }),
187
+ answerQuestion: protectedProcedure
188
+ .input(z.object({
189
+ worksheetResponseId: z.string(),
190
+ questionId: z.string(),
191
+ response: z.string(),
192
+ }))
193
+ .mutation(async ({ ctx, input }) => {
194
+ const { worksheetResponseId, questionId, response } = input;
195
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
196
+ where: { id: worksheetResponseId },
197
+ include: {
198
+ responses: {
199
+ where: { questionId },
200
+ },
201
+ },
202
+ });
203
+ if (!worksheetResponse) {
204
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
205
+ }
206
+ // Check if a response already exists for this question
207
+ const existingResponse = worksheetResponse.responses[0];
208
+ if (existingResponse) {
209
+ // Update existing response
210
+ await prisma.studentQuestionProgress.update({
211
+ where: { id: existingResponse.id },
212
+ data: { response },
213
+ });
214
+ }
215
+ else {
216
+ // Create new response
217
+ await prisma.studentQuestionProgress.create({
218
+ data: {
219
+ studentId: worksheetResponse.studentId,
220
+ questionId,
221
+ response,
222
+ studentWorksheetResponseId: worksheetResponseId,
223
+ },
224
+ });
225
+ }
226
+ // Return the updated worksheet response with all responses
227
+ const updatedWorksheetResponse = await prisma.studentWorksheetResponse.findUnique({
228
+ where: { id: worksheetResponseId },
229
+ include: {
230
+ responses: true,
231
+ },
232
+ });
233
+ return updatedWorksheetResponse;
234
+ }),
235
+ submitWorksheet: protectedProcedure
236
+ .input(z.object({
237
+ worksheetResponseId: z.string(),
238
+ }))
239
+ .mutation(async ({ ctx, input }) => {
240
+ const { worksheetResponseId } = input;
241
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
242
+ where: { id: worksheetResponseId },
243
+ include: {
244
+ worksheet: {
245
+ include: {
246
+ questions: true,
247
+ },
248
+ },
249
+ responses: true,
250
+ },
251
+ });
252
+ if (!worksheetResponse) {
253
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
254
+ }
255
+ if (worksheetResponse.submitted) {
256
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
257
+ }
258
+ // Mark worksheet as submitted
259
+ const submittedWorksheet = await prisma.studentWorksheetResponse.update({
260
+ where: { id: worksheetResponseId },
261
+ data: {
262
+ submitted: true,
263
+ submittedAt: new Date(),
264
+ },
265
+ include: {
266
+ responses: true,
267
+ },
268
+ });
269
+ // TODO: Implement AI grading here
270
+ // For now, we'll just mark all answers as pending review
271
+ // You could integrate with an AI service to auto-grade certain question types
272
+ return submittedWorksheet;
273
+ }),
274
+ // Get or create a student's worksheet response
275
+ getOrCreateWorksheetResponse: protectedProcedure
276
+ .input(z.object({
277
+ worksheetId: z.string(),
278
+ studentId: z.string(),
279
+ }))
280
+ .mutation(async ({ ctx, input }) => {
281
+ const { worksheetId, studentId } = input;
282
+ // Try to find existing response
283
+ let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
284
+ where: {
285
+ worksheetId,
286
+ studentId,
287
+ submitted: false, // Only get unsubmitted responses
288
+ },
289
+ include: {
290
+ responses: {
291
+ include: {
292
+ question: true,
293
+ },
294
+ },
295
+ },
296
+ });
297
+ // Create new response if none exists
298
+ if (!worksheetResponse) {
299
+ worksheetResponse = await prisma.studentWorksheetResponse.create({
300
+ data: {
301
+ worksheetId,
302
+ studentId,
303
+ },
304
+ include: {
305
+ responses: {
306
+ include: {
307
+ question: true,
308
+ },
309
+ },
310
+ },
311
+ });
312
+ }
313
+ return worksheetResponse;
314
+ }),
315
+ // Get all student responses for a worksheet (teacher view)
316
+ getWorksheetResponses: protectedProcedure
317
+ .input(z.object({
318
+ worksheetId: z.string(),
319
+ }))
320
+ .query(async ({ ctx, input }) => {
321
+ const { worksheetId } = input;
322
+ const responses = await prisma.studentWorksheetResponse.findMany({
323
+ where: { worksheetId },
324
+ include: {
325
+ student: {
326
+ select: {
327
+ id: true,
328
+ username: true,
329
+ profile: {
330
+ select: {
331
+ displayName: true,
332
+ profilePicture: true,
333
+ },
334
+ },
335
+ },
336
+ },
337
+ responses: {
338
+ include: {
339
+ question: true,
340
+ },
341
+ },
342
+ },
343
+ orderBy: { submittedAt: 'desc' },
344
+ });
345
+ return responses;
346
+ }),
347
+ // Grade a student's answer
348
+ gradeAnswer: protectedProcedure
349
+ .input(z.object({
350
+ responseId: z.string(), // StudentQuestionProgress ID
351
+ isCorrect: z.boolean(),
352
+ feedback: z.string().optional(),
353
+ }))
354
+ .mutation(async ({ ctx, input }) => {
355
+ const { responseId, isCorrect, feedback } = input;
356
+ const gradedResponse = await prisma.studentQuestionProgress.update({
357
+ where: { id: responseId },
358
+ data: {
359
+ isCorrect,
360
+ ...(feedback !== undefined && { feedback }),
361
+ },
362
+ });
363
+ return gradedResponse;
364
+ }),
365
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -42,6 +42,15 @@ enum UploadStatus {
42
42
  CANCELLED
43
43
  }
44
44
 
45
+ enum WorksheetQuestionType {
46
+ MULTIPLE_CHOICE
47
+ TRUE_FALSE
48
+ SHORT_ANSWER
49
+ LONG_ANSWER
50
+ MATH_EXPRESSION
51
+ ESSAY
52
+ }
53
+
45
54
  enum ReactionType {
46
55
  THUMBSUP
47
56
  CELEBRATE
@@ -85,6 +94,8 @@ model User {
85
94
  announcements Announcement[]
86
95
  notificationsSent Notification[] @relation("SentNotifications")
87
96
  notificationsReceived Notification[] @relation("ReceivedNotifications")
97
+ studentQuestionProgress StudentQuestionProgress[]
98
+ studentWorksheetResponses StudentWorksheetResponse[]
88
99
 
89
100
  presentAttendance Attendance[] @relation("PresentAttendance")
90
101
  lateAttendance Attendance[] @relation("LateAttendance")
@@ -132,6 +143,7 @@ model Class {
132
143
  events Event[]
133
144
  sections Section[]
134
145
  sessions Session[]
146
+ worksheets Worksheet[]
135
147
  students User[] @relation("UserStudentToClass")
136
148
  teachers User[] @relation("UserTeacherToClass")
137
149
  markSchemes MarkScheme[] @relation("ClassToMarkScheme")
@@ -255,6 +267,8 @@ model Assignment {
255
267
  order Int?
256
268
  gradingBoundary GradingBoundary? @relation(fields: [gradingBoundaryId], references: [id], onDelete: Cascade)
257
269
  gradingBoundaryId String?
270
+ worksheet Worksheet? @relation(fields: [worksheetId], references: [id], onDelete: Cascade)
271
+ worksheetId String?
258
272
  }
259
273
 
260
274
 
@@ -318,7 +332,8 @@ model Submission {
318
332
 
319
333
  attachments File[] @relation("SubmissionFile")
320
334
  annotations File[] @relation("SubmissionAnnotations")
321
-
335
+ worksheetResponses StudentWorksheetResponse[]
336
+
322
337
  gradeReceived Int?
323
338
 
324
339
  rubricState String?
@@ -471,6 +486,60 @@ model Mention {
471
486
  @@unique([messageId, userId])
472
487
  }
473
488
 
489
+ model Worksheet {
490
+ id String @id @default(uuid())
491
+ name String
492
+ createdAt DateTime @default(now())
493
+ updatedAt DateTime @updatedAt
494
+ classId String
495
+ class Class? @relation(fields: [classId], references: [id], onDelete: Cascade)
496
+ assignments Assignment[]
497
+ questions WorksheetQuestion[]
498
+ studentWorksheetResponses StudentWorksheetResponse[]
499
+ }
500
+
501
+ model WorksheetQuestion {
502
+ id String @id @default(uuid())
503
+ worksheetId String
504
+ worksheet Worksheet @relation(fields: [worksheetId], references: [id], onDelete: Cascade)
505
+ type WorksheetQuestionType
506
+ question String
507
+ answer String
508
+ markScheme Json? @default("{}")
509
+ createdAt DateTime @default(now())
510
+ updatedAt DateTime @updatedAt
511
+ studentResponses StudentQuestionProgress[]
512
+ }
513
+
514
+ model StudentWorksheetResponse {
515
+ id String @id @default(uuid())
516
+ studentId String
517
+ student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
518
+ worksheetId String
519
+ worksheet Worksheet @relation(fields: [worksheetId], references: [id], onDelete: Cascade)
520
+ responses StudentQuestionProgress[]
521
+ submission Submission? @relation(fields: [submissionId], references: [id], onDelete: Cascade)
522
+ submissionId String?
523
+ createdAt DateTime @default(now())
524
+ updatedAt DateTime @updatedAt
525
+ submittedAt DateTime?
526
+ submitted Boolean @default(false)
527
+ }
528
+
529
+ model StudentQuestionProgress {
530
+ id String @id @default(uuid())
531
+ studentId String
532
+ student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
533
+ questionId String
534
+ question WorksheetQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
535
+ response String
536
+ isCorrect Boolean @default(false)
537
+ feedback String?
538
+ createdAt DateTime @default(now())
539
+ updatedAt DateTime @updatedAt
540
+ studentWorksheetResponseId String?
541
+ studentWorksheetResponse StudentWorksheetResponse? @relation(fields: [studentWorksheetResponseId], references: [id], onDelete: Cascade)
542
+ }
474
543
 
475
544
  model SchoolDevelopementProgram {
476
545
  id String @id
@@ -17,6 +17,7 @@ import { conversationRouter } from "./conversation.js";
17
17
  import { messageRouter } from "./message.js";
18
18
  import { labChatRouter } from "./labChat.js";
19
19
  import { marketingRouter } from "./marketing.js";
20
+ import { worksheetRouter } from "./worksheet.js";
20
21
 
21
22
  export const appRouter = createTRPCRouter({
22
23
  class: classRouter,
@@ -35,6 +36,7 @@ export const appRouter = createTRPCRouter({
35
36
  message: messageRouter,
36
37
  labChat: labChatRouter,
37
38
  marketing: marketingRouter,
39
+ worksheet: worksheetRouter,
38
40
  });
39
41
 
40
42
  // Export type router type definition
@@ -142,7 +142,7 @@ export const authRouter = createTRPCRouter({
142
142
  if (!user) {
143
143
  throw new TRPCError({
144
144
  code: "UNAUTHORIZED",
145
- message: "user doesn't exists.",
145
+ message: "Invalid username or password",
146
146
  });
147
147
  }
148
148
 
@@ -195,6 +195,9 @@ export const classRouter = createTRPCRouter({
195
195
  studentId: true,
196
196
  id: true,
197
197
  submitted: true,
198
+ gradeReceived: true,
199
+ rubricState: true,
200
+ teacherComments: true,
198
201
  returned: true,
199
202
  submittedAt: true,
200
203
  },