@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,368 @@
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
+ options: z.any().optional(), // JSON field
99
+ markScheme: z.any().optional(), // JSON field
100
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']),
101
+ }))
102
+ .mutation(async ({ ctx, input }) => {
103
+ const { worksheetId, question, answer, options, markScheme, type } = input;
104
+ const worksheet = await prisma.worksheet.findUnique({
105
+ where: { id: worksheetId },
106
+ });
107
+ if (!worksheet) {
108
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
109
+ }
110
+ const newQuestion = await prisma.worksheetQuestion.create({
111
+ data: {
112
+ worksheetId,
113
+ type,
114
+ question,
115
+ answer,
116
+ options,
117
+ markScheme,
118
+ },
119
+ });
120
+ return newQuestion;
121
+ }),
122
+ updateQuestion: protectedProcedure
123
+ .input(z.object({
124
+ worksheetId: z.string(),
125
+ questionId: z.string(),
126
+ question: z.string().optional(),
127
+ answer: z.string().optional(),
128
+ options: z.any().optional(), // JSON field
129
+ markScheme: z.any().optional(), // JSON field
130
+ type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'LONG_ANSWER', 'MATH_EXPRESSION', 'ESSAY']).optional(),
131
+ }))
132
+ .mutation(async ({ ctx, input }) => {
133
+ const { worksheetId, questionId, question, answer, options, markScheme, type } = input;
134
+ const worksheet = await prisma.worksheet.findUnique({
135
+ where: { id: worksheetId },
136
+ });
137
+ if (!worksheet) {
138
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
139
+ }
140
+ const updatedQuestion = await prisma.worksheetQuestion.update({
141
+ where: { id: questionId },
142
+ data: {
143
+ ...(question !== undefined && { question }),
144
+ ...(answer !== undefined && { answer }),
145
+ ...(markScheme !== undefined && { markScheme }),
146
+ ...(type !== undefined && { type }),
147
+ },
148
+ });
149
+ return updatedQuestion;
150
+ }),
151
+ deleteQuestion: protectedProcedure
152
+ .input(z.object({
153
+ worksheetId: z.string(),
154
+ questionId: z.string(),
155
+ }))
156
+ .mutation(async ({ ctx, input }) => {
157
+ const { worksheetId, questionId } = input;
158
+ const worksheet = await prisma.worksheet.findUnique({
159
+ where: { id: worksheetId },
160
+ });
161
+ if (!worksheet) {
162
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet not found' });
163
+ }
164
+ const deletedQuestion = await prisma.worksheetQuestion.delete({
165
+ where: { id: questionId },
166
+ });
167
+ return deletedQuestion;
168
+ }),
169
+ getWorksheetSubmission: protectedProcedure
170
+ .input(z.object({
171
+ worksheetId: z.string(),
172
+ submissionId: z.string(),
173
+ }))
174
+ .query(async ({ ctx, input }) => {
175
+ const { worksheetId, submissionId } = input;
176
+ const submission = await prisma.submission.findUnique({
177
+ where: { id: submissionId },
178
+ });
179
+ if (!submission) {
180
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Submission not found' });
181
+ }
182
+ const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
183
+ where: { submissionId },
184
+ include: {
185
+ responses: true,
186
+ },
187
+ });
188
+ return worksheetResponses;
189
+ }),
190
+ answerQuestion: protectedProcedure
191
+ .input(z.object({
192
+ worksheetResponseId: z.string(),
193
+ questionId: z.string(),
194
+ response: z.string(),
195
+ }))
196
+ .mutation(async ({ ctx, input }) => {
197
+ const { worksheetResponseId, questionId, response } = input;
198
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
199
+ where: { id: worksheetResponseId },
200
+ include: {
201
+ responses: {
202
+ where: { questionId },
203
+ },
204
+ },
205
+ });
206
+ if (!worksheetResponse) {
207
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
208
+ }
209
+ // Check if a response already exists for this question
210
+ const existingResponse = worksheetResponse.responses[0];
211
+ if (existingResponse) {
212
+ // Update existing response
213
+ await prisma.studentQuestionProgress.update({
214
+ where: { id: existingResponse.id },
215
+ data: { response },
216
+ });
217
+ }
218
+ else {
219
+ // Create new response
220
+ await prisma.studentQuestionProgress.create({
221
+ data: {
222
+ studentId: worksheetResponse.studentId,
223
+ questionId,
224
+ response,
225
+ studentWorksheetResponseId: worksheetResponseId,
226
+ },
227
+ });
228
+ }
229
+ // Return the updated worksheet response with all responses
230
+ const updatedWorksheetResponse = await prisma.studentWorksheetResponse.findUnique({
231
+ where: { id: worksheetResponseId },
232
+ include: {
233
+ responses: true,
234
+ },
235
+ });
236
+ return updatedWorksheetResponse;
237
+ }),
238
+ submitWorksheet: protectedProcedure
239
+ .input(z.object({
240
+ worksheetResponseId: z.string(),
241
+ }))
242
+ .mutation(async ({ ctx, input }) => {
243
+ const { worksheetResponseId } = input;
244
+ const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
245
+ where: { id: worksheetResponseId },
246
+ include: {
247
+ worksheet: {
248
+ include: {
249
+ questions: true,
250
+ },
251
+ },
252
+ responses: true,
253
+ },
254
+ });
255
+ if (!worksheetResponse) {
256
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
257
+ }
258
+ if (worksheetResponse.submitted) {
259
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
260
+ }
261
+ // Mark worksheet as submitted
262
+ const submittedWorksheet = await prisma.studentWorksheetResponse.update({
263
+ where: { id: worksheetResponseId },
264
+ data: {
265
+ submitted: true,
266
+ submittedAt: new Date(),
267
+ },
268
+ include: {
269
+ responses: true,
270
+ },
271
+ });
272
+ // TODO: Implement AI grading here
273
+ // For now, we'll just mark all answers as pending review
274
+ // You could integrate with an AI service to auto-grade certain question types
275
+ return submittedWorksheet;
276
+ }),
277
+ // Get or create a student's worksheet response
278
+ getOrCreateWorksheetResponse: protectedProcedure
279
+ .input(z.object({
280
+ worksheetId: z.string(),
281
+ studentId: z.string(),
282
+ }))
283
+ .mutation(async ({ ctx, input }) => {
284
+ const { worksheetId, studentId } = input;
285
+ // Try to find existing response
286
+ let worksheetResponse = await prisma.studentWorksheetResponse.findFirst({
287
+ where: {
288
+ worksheetId,
289
+ studentId,
290
+ submitted: false, // Only get unsubmitted responses
291
+ },
292
+ include: {
293
+ responses: {
294
+ include: {
295
+ question: true,
296
+ },
297
+ },
298
+ },
299
+ });
300
+ // Create new response if none exists
301
+ if (!worksheetResponse) {
302
+ worksheetResponse = await prisma.studentWorksheetResponse.create({
303
+ data: {
304
+ worksheetId,
305
+ studentId,
306
+ },
307
+ include: {
308
+ responses: {
309
+ include: {
310
+ question: true,
311
+ },
312
+ },
313
+ },
314
+ });
315
+ }
316
+ return worksheetResponse;
317
+ }),
318
+ // Get all student responses for a worksheet (teacher view)
319
+ getWorksheetResponses: protectedProcedure
320
+ .input(z.object({
321
+ worksheetId: z.string(),
322
+ }))
323
+ .query(async ({ ctx, input }) => {
324
+ const { worksheetId } = input;
325
+ const responses = await prisma.studentWorksheetResponse.findMany({
326
+ where: { worksheetId },
327
+ include: {
328
+ student: {
329
+ select: {
330
+ id: true,
331
+ username: true,
332
+ profile: {
333
+ select: {
334
+ displayName: true,
335
+ profilePicture: true,
336
+ },
337
+ },
338
+ },
339
+ },
340
+ responses: {
341
+ include: {
342
+ question: true,
343
+ },
344
+ },
345
+ },
346
+ orderBy: { submittedAt: 'desc' },
347
+ });
348
+ return responses;
349
+ }),
350
+ // Grade a student's answer
351
+ gradeAnswer: protectedProcedure
352
+ .input(z.object({
353
+ responseId: z.string(), // StudentQuestionProgress ID
354
+ isCorrect: z.boolean(),
355
+ feedback: z.string().optional(),
356
+ }))
357
+ .mutation(async ({ ctx, input }) => {
358
+ const { responseId, isCorrect, feedback } = input;
359
+ const gradedResponse = await prisma.studentQuestionProgress.update({
360
+ where: { id: responseId },
361
+ data: {
362
+ isCorrect,
363
+ ...(feedback !== undefined && { feedback }),
364
+ },
365
+ });
366
+ return gradedResponse;
367
+ }),
368
+ });
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.29",
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,61 @@ 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
+ options Json? @default("{}")
509
+ markScheme Json? @default("{}")
510
+ createdAt DateTime @default(now())
511
+ updatedAt DateTime @updatedAt
512
+ studentResponses StudentQuestionProgress[]
513
+ }
514
+
515
+ model StudentWorksheetResponse {
516
+ id String @id @default(uuid())
517
+ studentId String
518
+ student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
519
+ worksheetId String
520
+ worksheet Worksheet @relation(fields: [worksheetId], references: [id], onDelete: Cascade)
521
+ responses StudentQuestionProgress[]
522
+ submission Submission? @relation(fields: [submissionId], references: [id], onDelete: Cascade)
523
+ submissionId String?
524
+ createdAt DateTime @default(now())
525
+ updatedAt DateTime @updatedAt
526
+ submittedAt DateTime?
527
+ submitted Boolean @default(false)
528
+ }
529
+
530
+ model StudentQuestionProgress {
531
+ id String @id @default(uuid())
532
+ studentId String
533
+ student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
534
+ questionId String
535
+ question WorksheetQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
536
+ response String
537
+ isCorrect Boolean @default(false)
538
+ feedback String?
539
+ createdAt DateTime @default(now())
540
+ updatedAt DateTime @updatedAt
541
+ studentWorksheetResponseId String?
542
+ studentWorksheetResponse StudentWorksheetResponse? @relation(fields: [studentWorksheetResponseId], references: [id], onDelete: Cascade)
543
+ }
474
544
 
475
545
  model SchoolDevelopementProgram {
476
546
  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
  },