@studious-lms/server 1.7.0 → 1.8.0

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.
Files changed (196) hide show
  1. package/dist/models/access.d.ts +21 -66
  2. package/dist/models/access.d.ts.map +1 -1
  3. package/dist/models/access.js +10 -26
  4. package/dist/models/access.js.map +1 -1
  5. package/dist/models/agenda.d.ts +2 -0
  6. package/dist/models/agenda.d.ts.map +1 -1
  7. package/dist/models/announcement.d.ts +2 -1
  8. package/dist/models/announcement.d.ts.map +1 -1
  9. package/dist/models/announcement.js +2 -3
  10. package/dist/models/announcement.js.map +1 -1
  11. package/dist/models/assignment.d.ts +7 -0
  12. package/dist/models/assignment.d.ts.map +1 -1
  13. package/dist/models/auth.d.ts +2 -4
  14. package/dist/models/auth.d.ts.map +1 -1
  15. package/dist/models/auth.js +2 -3
  16. package/dist/models/auth.js.map +1 -1
  17. package/dist/models/class.d.ts +32 -4
  18. package/dist/models/class.d.ts.map +1 -1
  19. package/dist/models/class.js +15 -2
  20. package/dist/models/class.js.map +1 -1
  21. package/dist/models/cloudStorage.d.ts +140 -0
  22. package/dist/models/cloudStorage.d.ts.map +1 -0
  23. package/dist/models/cloudStorage.js +175 -0
  24. package/dist/models/cloudStorage.js.map +1 -0
  25. package/dist/models/comment.d.ts +0 -24
  26. package/dist/models/comment.d.ts.map +1 -1
  27. package/dist/models/comment.js +2 -6
  28. package/dist/models/comment.js.map +1 -1
  29. package/dist/models/conversation.d.ts +22 -22
  30. package/dist/models/department.d.ts +154 -0
  31. package/dist/models/department.d.ts.map +1 -0
  32. package/dist/models/department.js +135 -0
  33. package/dist/models/department.js.map +1 -0
  34. package/dist/models/event.d.ts +7 -1
  35. package/dist/models/event.d.ts.map +1 -1
  36. package/dist/models/event.js +2 -5
  37. package/dist/models/event.js.map +1 -1
  38. package/dist/models/file.d.ts +25 -10
  39. package/dist/models/file.d.ts.map +1 -1
  40. package/dist/models/file.js +2 -5
  41. package/dist/models/file.js.map +1 -1
  42. package/dist/models/folder.d.ts +47 -0
  43. package/dist/models/folder.d.ts.map +1 -1
  44. package/dist/models/labChat.d.ts +5 -3
  45. package/dist/models/labChat.d.ts.map +1 -1
  46. package/dist/models/message.d.ts +46 -12
  47. package/dist/models/message.d.ts.map +1 -1
  48. package/dist/models/message.js +19 -10
  49. package/dist/models/message.js.map +1 -1
  50. package/dist/models/newtonChat.d.ts +2 -1
  51. package/dist/models/newtonChat.d.ts.map +1 -1
  52. package/dist/models/school.d.ts +45 -12
  53. package/dist/models/school.d.ts.map +1 -1
  54. package/dist/models/school.js +19 -2
  55. package/dist/models/school.js.map +1 -1
  56. package/dist/models/section.d.ts +1 -0
  57. package/dist/models/section.d.ts.map +1 -1
  58. package/dist/models/tag.d.ts +117 -0
  59. package/dist/models/tag.d.ts.map +1 -0
  60. package/dist/models/tag.js +113 -0
  61. package/dist/models/tag.js.map +1 -0
  62. package/dist/models/worksheet.d.ts +3 -0
  63. package/dist/models/worksheet.d.ts.map +1 -1
  64. package/dist/pipelines/aiLabChatContract.d.ts +8 -8
  65. package/dist/routers/_app.d.ts +3200 -1972
  66. package/dist/routers/_app.d.ts.map +1 -1
  67. package/dist/routers/_app.js +6 -2
  68. package/dist/routers/_app.js.map +1 -1
  69. package/dist/routers/agenda.d.ts +2 -0
  70. package/dist/routers/agenda.d.ts.map +1 -1
  71. package/dist/routers/announcement.d.ts.map +1 -1
  72. package/dist/routers/announcement.js +3 -5
  73. package/dist/routers/announcement.js.map +1 -1
  74. package/dist/routers/assignment.d.ts +18 -9
  75. package/dist/routers/assignment.d.ts.map +1 -1
  76. package/dist/routers/assignment.js +2 -3
  77. package/dist/routers/assignment.js.map +1 -1
  78. package/dist/routers/class.d.ts +87 -14
  79. package/dist/routers/class.d.ts.map +1 -1
  80. package/dist/routers/class.js +5 -2
  81. package/dist/routers/class.js.map +1 -1
  82. package/dist/routers/conversation.d.ts +5 -5
  83. package/dist/routers/department.d.ts +293 -0
  84. package/dist/routers/department.d.ts.map +1 -0
  85. package/dist/routers/department.js +93 -0
  86. package/dist/routers/department.js.map +1 -0
  87. package/dist/routers/event.d.ts +2 -1
  88. package/dist/routers/event.d.ts.map +1 -1
  89. package/dist/routers/file.d.ts +8 -6
  90. package/dist/routers/file.d.ts.map +1 -1
  91. package/dist/routers/file.js +26 -17
  92. package/dist/routers/file.js.map +1 -1
  93. package/dist/routers/folder.d.ts +139 -186
  94. package/dist/routers/folder.d.ts.map +1 -1
  95. package/dist/routers/folder.js +82 -90
  96. package/dist/routers/folder.js.map +1 -1
  97. package/dist/routers/labChat.d.ts +11 -2
  98. package/dist/routers/labChat.d.ts.map +1 -1
  99. package/dist/routers/message.d.ts +20 -2
  100. package/dist/routers/message.d.ts.map +1 -1
  101. package/dist/routers/newtonChat.d.ts +9 -0
  102. package/dist/routers/newtonChat.d.ts.map +1 -1
  103. package/dist/routers/school.d.ts +160 -17
  104. package/dist/routers/school.d.ts.map +1 -1
  105. package/dist/routers/school.js +46 -3
  106. package/dist/routers/school.js.map +1 -1
  107. package/dist/routers/tag.d.ts +119 -0
  108. package/dist/routers/tag.d.ts.map +1 -0
  109. package/dist/routers/tag.js +52 -0
  110. package/dist/routers/tag.js.map +1 -0
  111. package/dist/routers/worksheet.d.ts +1 -0
  112. package/dist/routers/worksheet.d.ts.map +1 -1
  113. package/dist/seedDatabase.d.ts +0 -1
  114. package/dist/seedDatabase.d.ts.map +1 -1
  115. package/dist/services/agenda.d.ts +2 -0
  116. package/dist/services/agenda.d.ts.map +1 -1
  117. package/dist/services/assignment.d.ts +18 -9
  118. package/dist/services/assignment.d.ts.map +1 -1
  119. package/dist/services/class.d.ts +87 -14
  120. package/dist/services/class.d.ts.map +1 -1
  121. package/dist/services/class.js +91 -4
  122. package/dist/services/class.js.map +1 -1
  123. package/dist/services/conversation.d.ts +5 -5
  124. package/dist/services/department.d.ts +200 -0
  125. package/dist/services/department.d.ts.map +1 -0
  126. package/dist/services/department.js +368 -0
  127. package/dist/services/department.js.map +1 -0
  128. package/dist/services/event.d.ts +2 -1
  129. package/dist/services/event.d.ts.map +1 -1
  130. package/dist/services/file.d.ts +85 -0
  131. package/dist/services/file.d.ts.map +1 -1
  132. package/dist/services/file.js +92 -2
  133. package/dist/services/file.js.map +1 -1
  134. package/dist/services/folder.d.ts +125 -149
  135. package/dist/services/folder.d.ts.map +1 -1
  136. package/dist/services/folder.js +258 -199
  137. package/dist/services/folder.js.map +1 -1
  138. package/dist/services/labChat.d.ts +11 -2
  139. package/dist/services/labChat.d.ts.map +1 -1
  140. package/dist/services/labChat.js +4 -3
  141. package/dist/services/labChat.js.map +1 -1
  142. package/dist/services/message.d.ts +20 -2
  143. package/dist/services/message.d.ts.map +1 -1
  144. package/dist/services/message.js +7 -23
  145. package/dist/services/message.js.map +1 -1
  146. package/dist/services/newtonChat.d.ts +9 -0
  147. package/dist/services/newtonChat.d.ts.map +1 -1
  148. package/dist/services/newtonChat.js +4 -3
  149. package/dist/services/newtonChat.js.map +1 -1
  150. package/dist/services/school.d.ts +142 -15
  151. package/dist/services/school.d.ts.map +1 -1
  152. package/dist/services/school.js +308 -54
  153. package/dist/services/school.js.map +1 -1
  154. package/dist/services/tag.d.ts +71 -0
  155. package/dist/services/tag.d.ts.map +1 -0
  156. package/dist/services/tag.js +178 -0
  157. package/dist/services/tag.js.map +1 -0
  158. package/dist/services/worksheet.d.ts +1 -0
  159. package/dist/services/worksheet.d.ts.map +1 -1
  160. package/dist/utils/aiUser.d.ts.map +1 -1
  161. package/dist/utils/aiUser.js +2 -3
  162. package/dist/utils/aiUser.js.map +1 -1
  163. package/package.json +1 -1
  164. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
  165. package/prisma/schema.prisma +119 -35
  166. package/src/models/access.ts +8 -24
  167. package/src/models/announcement.ts +0 -1
  168. package/src/models/auth.ts +0 -1
  169. package/src/models/class.ts +13 -0
  170. package/src/models/cloudStorage.ts +194 -0
  171. package/src/models/comment.ts +0 -4
  172. package/src/models/department.ts +159 -0
  173. package/src/models/event.ts +0 -3
  174. package/src/models/file.ts +0 -3
  175. package/src/models/message.ts +19 -5
  176. package/src/models/school.ts +26 -0
  177. package/src/models/tag.ts +140 -0
  178. package/src/routers/_app.ts +4 -0
  179. package/src/routers/announcement.ts +1 -3
  180. package/src/routers/assignment.ts +0 -1
  181. package/src/routers/class.ts +3 -0
  182. package/src/routers/department.ts +174 -0
  183. package/src/routers/file.ts +29 -26
  184. package/src/routers/folder.ts +100 -121
  185. package/src/routers/school.ts +83 -0
  186. package/src/routers/tag.ts +78 -0
  187. package/src/services/class.ts +108 -3
  188. package/src/services/department.ts +478 -0
  189. package/src/services/file.ts +100 -0
  190. package/src/services/folder.ts +313 -240
  191. package/src/services/labChat.ts +2 -1
  192. package/src/services/message.ts +5 -21
  193. package/src/services/newtonChat.ts +2 -1
  194. package/src/services/school.ts +369 -57
  195. package/src/services/tag.ts +229 -0
  196. package/src/utils/aiUser.ts +0 -1
@@ -2,175 +2,154 @@ import { z } from "zod";
2
2
  import {
3
3
  createTRPCRouter,
4
4
  protectedProcedure,
5
- protectedClassMemberProcedure,
6
- protectedTeacherProcedure,
7
5
  } from "../trpc.js";
8
6
  import {
7
+ browseFolder,
8
+ confirmFolderUpload,
9
9
  createFolderRecord,
10
+ deleteFolder,
10
11
  getFolder,
11
- getChildFolders,
12
- getFolderChildren,
12
+ getFolderParents,
13
+ getFolderUploadUrls,
13
14
  getRootFolder,
14
- uploadFilesToFolder,
15
- deleteFolderRecord,
15
+ listUserScopes,
16
16
  moveFolder,
17
17
  updateFolderRecord,
18
- getFolderParents,
19
- getFolderUploadUrls,
20
18
  } from "../services/folder.js";
21
19
 
22
- const createFolderSchema = z.object({
23
- name: z.string(),
24
- parentFolderId: z.string().optional(),
25
- color: z.string().optional(),
26
- });
27
-
28
20
  const directFileSchema = z.object({
29
21
  name: z.string(),
30
22
  type: z.string(),
31
23
  size: z.number(),
32
24
  });
33
25
 
34
- const uploadFilesToFolderSchema = z.object({
35
- folderId: z.string(),
36
- files: z.array(directFileSchema),
37
- });
38
-
39
- const getRootFolderSchema = z.object({
40
- classId: z.string(),
41
- });
26
+ /**
27
+ * Cloud scope discriminator. Folders live under one of:
28
+ * - CLASS: a class shared-files area
29
+ * - SCHOOL_STORAGE: institution-wide files
30
+ * - PERSONAL: a single user's storage
31
+ *
32
+ * All folder operations identify the location they apply to with either a
33
+ * `folderId` (for an existing folder) or a `scope` (when no folder exists yet,
34
+ * e.g. browsing a scope root). There is no `classId` shorthand — use
35
+ * `{ scope: { kind: "CLASS", classId } }` instead.
36
+ */
37
+ const cloudScopeSchema = z.discriminatedUnion("kind", [
38
+ z.object({ kind: z.literal("CLASS"), classId: z.string() }),
39
+ z.object({ kind: z.literal("SCHOOL_STORAGE"), schoolId: z.string() }),
40
+ z.object({ kind: z.literal("PERSONAL"), userId: z.string() }),
41
+ ]);
42
42
 
43
43
  export const folderRouter = createTRPCRouter({
44
- create: protectedTeacherProcedure
45
- .input(createFolderSchema)
46
- .mutation(async ({ ctx, input }) => {
47
- if (!ctx.user) {
48
- throw new Error("You must be logged in to create a folder");
49
- }
50
- return createFolderRecord(ctx.user.id, {
51
- classId: input.classId,
52
- name: input.name,
53
- parentFolderId: input.parentFolderId,
54
- color: input.color,
55
- });
56
- }),
57
-
58
- get: protectedClassMemberProcedure
59
- .input(z.object({ folderId: z.string(), classId: z.string() }))
60
- .query(({ input }) => getFolder(input.folderId)),
61
-
62
- getChildFolders: protectedClassMemberProcedure
63
- .input(z.object({ classId: z.string() }))
64
- .query(({ input }) => getChildFolders(input.classId)),
44
+ /** Browse a folder (or scope root) with breadcrumbs and access flag. */
45
+ browse: protectedProcedure
46
+ .input(
47
+ z.union([
48
+ z.object({ folderId: z.string() }),
49
+ z.object({ scope: cloudScopeSchema }),
50
+ ])
51
+ )
52
+ .query(({ ctx, input }) => browseFolder(ctx.user!.id, input)),
65
53
 
66
- getFolderChildren: protectedClassMemberProcedure
67
- .input(z.object({ folderId: z.string(), classId: z.string() }))
68
- .query(({ input }) =>
69
- getFolderChildren(input.folderId, input.classId)
70
- ),
54
+ /** What scopes can this user pick from in the cloud-storage UI? */
55
+ listScopes: protectedProcedure.query(({ ctx }) =>
56
+ listUserScopes(ctx.user!.id)
57
+ ),
71
58
 
72
- getRootFolder: protectedClassMemberProcedure
73
- .input(getRootFolderSchema)
74
- .query(({ input }) => getRootFolder(input.classId)),
59
+ /** Root folder for any scope (no folderId). */
60
+ getRootFolder: protectedProcedure
61
+ .input(z.object({ scope: cloudScopeSchema }))
62
+ .query(({ ctx, input }) => getRootFolder(ctx.user!.id, input.scope)),
75
63
 
76
- uploadFiles: protectedTeacherProcedure
77
- .input(uploadFilesToFolderSchema)
78
- .mutation(async ({ ctx, input }) => {
79
- if (!ctx.user) {
80
- throw new Error("You must be logged in to upload files");
81
- }
82
- return uploadFilesToFolder(
83
- ctx.user.id,
84
- input.classId,
85
- input.folderId,
86
- input.files
87
- );
88
- }),
64
+ get: protectedProcedure
65
+ .input(z.object({ folderId: z.string() }))
66
+ .query(({ ctx, input }) => getFolder(ctx.user!.id, input.folderId)),
89
67
 
90
- delete: protectedTeacherProcedure
91
- .input(z.object({ classId: z.string(), folderId: z.string() }))
92
- .mutation(async ({ ctx, input }) => {
93
- if (!ctx.user) {
94
- throw new Error("You must be logged in to delete folders");
95
- }
96
- return deleteFolderRecord(ctx.user.id, input.classId, input.folderId);
97
- }),
68
+ getParents: protectedProcedure
69
+ .input(z.object({ folderId: z.string() }))
70
+ .query(({ ctx, input }) => getFolderParents(ctx.user!.id, input.folderId)),
98
71
 
99
- move: protectedTeacherProcedure
72
+ create: protectedProcedure
100
73
  .input(
101
74
  z.object({
102
- folderId: z.string(),
103
- targetParentFolderId: z.string(),
104
- classId: z.string(),
75
+ name: z.string().min(1),
76
+ parentFolderId: z.string().nullable().optional(),
77
+ color: z.string().optional(),
78
+ scope: cloudScopeSchema.optional(),
105
79
  })
106
80
  )
107
- .mutation(async ({ ctx, input }) => {
108
- if (!ctx.user) {
109
- throw new Error("You must be logged in to move folders");
110
- }
111
- return moveFolder(
112
- ctx.user.id,
113
- input.folderId,
114
- input.targetParentFolderId,
115
- input.classId
116
- );
117
- }),
81
+ .mutation(({ ctx, input }) =>
82
+ createFolderRecord(ctx.user!.id, {
83
+ name: input.name,
84
+ parentFolderId: input.parentFolderId ?? undefined,
85
+ color: input.color,
86
+ scope: input.scope,
87
+ })
88
+ ),
118
89
 
119
- update: protectedTeacherProcedure
90
+ update: protectedProcedure
120
91
  .input(
121
92
  z.object({
122
93
  folderId: z.string(),
123
- name: z.string(),
94
+ name: z.string().optional(),
124
95
  color: z.string().optional(),
125
- classId: z.string(),
126
96
  })
127
97
  )
128
- .mutation(({ input }) =>
129
- updateFolderRecord(
130
- input.folderId,
131
- input.name,
132
- input.color,
133
- input.classId
134
- )
98
+ .mutation(({ ctx, input }) =>
99
+ updateFolderRecord(ctx.user!.id, {
100
+ folderId: input.folderId,
101
+ name: input.name,
102
+ color: input.color,
103
+ })
135
104
  ),
136
105
 
137
- getParents: protectedProcedure
106
+ delete: protectedProcedure
138
107
  .input(z.object({ folderId: z.string() }))
139
- .query(({ input }) => getFolderParents(input.folderId)),
108
+ .mutation(({ ctx, input }) => deleteFolder(ctx.user!.id, input.folderId)),
109
+
110
+ move: protectedProcedure
111
+ .input(
112
+ z.object({
113
+ folderId: z.string(),
114
+ targetParentFolderId: z.string(),
115
+ })
116
+ )
117
+ .mutation(({ ctx, input }) =>
118
+ moveFolder(ctx.user!.id, {
119
+ folderId: input.folderId,
120
+ targetParentFolderId: input.targetParentFolderId,
121
+ })
122
+ ),
140
123
 
141
- getFolderUploadUrls: protectedTeacherProcedure
124
+ /** Get signed upload URLs for direct-to-GCS upload. */
125
+ getFolderUploadUrls: protectedProcedure
142
126
  .input(
143
127
  z.object({
144
- classId: z.string(),
145
128
  folderId: z.string(),
146
129
  files: z.array(directFileSchema),
147
130
  })
148
131
  )
149
- .mutation(async ({ ctx, input }) => {
150
- if (!ctx.user) {
151
- throw new Error("You must be logged in to upload files");
152
- }
153
- return getFolderUploadUrls(
154
- ctx.user.id,
155
- input.classId,
156
- input.folderId,
157
- input.files
158
- );
159
- }),
132
+ .mutation(({ ctx, input }) =>
133
+ getFolderUploadUrls(ctx.user!.id, {
134
+ folderId: input.folderId,
135
+ files: input.files,
136
+ })
137
+ ),
160
138
 
161
- confirmFolderUpload: protectedTeacherProcedure
139
+ /** Acknowledge a direct upload completed (or failed). */
140
+ confirmFolderUpload: protectedProcedure
162
141
  .input(
163
142
  z.object({
164
143
  fileId: z.string(),
165
144
  uploadSuccess: z.boolean(),
145
+ errorMessage: z.string().optional(),
166
146
  })
167
147
  )
168
- .mutation(async ({ ctx, input }) => {
169
- if (!ctx.user) {
170
- throw new Error("You must be logged in to confirm uploads");
171
- }
172
- const { confirmDirectUpload } = await import("../lib/fileUpload.js");
173
- await confirmDirectUpload(input.fileId, input.uploadSuccess);
174
- return { success: true };
175
- }),
148
+ .mutation(({ ctx, input }) =>
149
+ confirmFolderUpload(ctx.user!.id, {
150
+ fileId: input.fileId,
151
+ uploadSuccess: input.uploadSuccess,
152
+ errorMessage: input.errorMessage,
153
+ })
154
+ ),
176
155
  });
@@ -7,18 +7,24 @@ import {
7
7
  import {
8
8
  acceptInvite,
9
9
  addMember,
10
+ confirmSchoolAssetUpload,
10
11
  createInvite,
11
12
  createSchoolClass,
12
13
  createSchoolRecord,
13
14
  getAdminOverview,
14
15
  getCurrentSchool,
15
16
  getInvite,
17
+ getSchoolAssetUploadUrl,
18
+ getStudentTimeline,
16
19
  listClasses,
17
20
  listInvites,
21
+ listMemberIds,
18
22
  listMembers,
19
23
  removeMember,
20
24
  revokeInvite,
25
+ searchMembers,
21
26
  updateMemberRole,
27
+ updateSchoolDetails,
22
28
  } from "../services/school.js";
23
29
 
24
30
  const schoolRoleSchema = z.enum(["ADMIN", "TEACHER", "STUDENT"]);
@@ -38,12 +44,88 @@ export const schoolRouter = createTRPCRouter({
38
44
  .input(
39
45
  z.object({
40
46
  name: z.string().min(1),
47
+ description: z.string().optional(),
41
48
  subdomain: z.string().min(1).optional(),
42
49
  logoId: z.string().optional(),
43
50
  })
44
51
  )
45
52
  .mutation(({ ctx, input }) => createSchoolRecord(ctx.user!.id, input)),
46
53
 
54
+ update: protectedProcedure
55
+ .input(
56
+ z.object({
57
+ schoolId: z.string(),
58
+ name: z.string().min(1).optional(),
59
+ description: z.string().nullable().optional(),
60
+ subdomain: z.string().nullable().optional(),
61
+ logoId: z.string().nullable().optional(),
62
+ })
63
+ )
64
+ .mutation(({ ctx, input }) => updateSchoolDetails(ctx.user!.id, input)),
65
+
66
+ searchMembers: protectedProcedure
67
+ .input(
68
+ z.object({
69
+ schoolId: z.string(),
70
+ query: z.string(),
71
+ limit: z.number().min(1).max(50).optional(),
72
+ role: schoolRoleSchema.optional(),
73
+ })
74
+ )
75
+ .query(({ ctx, input }) =>
76
+ searchMembers(
77
+ ctx.user!.id,
78
+ input.schoolId,
79
+ input.query,
80
+ input.limit,
81
+ input.role
82
+ )
83
+ ),
84
+
85
+ listMemberIds: protectedProcedure
86
+ .input(schoolIdSchema)
87
+ .query(({ ctx, input }) => listMemberIds(ctx.user!.id, input.schoolId)),
88
+
89
+ getAssetUploadUrl: protectedProcedure
90
+ .input(
91
+ z.object({
92
+ fileName: z.string(),
93
+ fileType: z.string(),
94
+ fileSize: z.number(),
95
+ })
96
+ )
97
+ .mutation(({ ctx, input }) =>
98
+ getSchoolAssetUploadUrl(ctx.user!.id, input)
99
+ ),
100
+
101
+ confirmAssetUpload: protectedProcedure
102
+ .input(
103
+ z.object({
104
+ fileId: z.string(),
105
+ uploadSuccess: z.boolean(),
106
+ })
107
+ )
108
+ .mutation(({ ctx, input }) =>
109
+ confirmSchoolAssetUpload(
110
+ ctx.user!.id,
111
+ input.fileId,
112
+ input.uploadSuccess
113
+ )
114
+ ),
115
+
116
+ getStudentTimeline: protectedProcedure
117
+ .input(
118
+ z.object({
119
+ schoolId: z.string(),
120
+ studentId: z.string(),
121
+ from: z.date().optional(),
122
+ to: z.date().optional(),
123
+ })
124
+ )
125
+ .query(({ ctx, input }) =>
126
+ getStudentTimeline(ctx.user!.id, input)
127
+ ),
128
+
47
129
  getAdminOverview: protectedProcedure
48
130
  .input(schoolIdSchema)
49
131
  .query(({ ctx, input }) => getAdminOverview(ctx.user!.id, input.schoolId)),
@@ -60,6 +142,7 @@ export const schoolRouter = createTRPCRouter({
60
142
  section: z.string(),
61
143
  subject: z.string(),
62
144
  color: z.string().optional(),
145
+ departmentId: z.string().optional(),
63
146
  teachers: z.array(z.string()).optional(),
64
147
  students: z.array(z.string()).optional(),
65
148
  })
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
+ import {
4
+ attachTag,
5
+ createTagRecord,
6
+ deleteTagRecord,
7
+ detachTag,
8
+ getAttachedTags,
9
+ listTags,
10
+ updateTagRecord,
11
+ } from "../services/tag.js";
12
+
13
+ const tagTargetSchema = z.enum(["USER", "CLASS", "RUBRIC", "BOUNDARY"]);
14
+
15
+ export const tagRouter = createTRPCRouter({
16
+ list: protectedProcedure
17
+ .input(
18
+ z.object({
19
+ schoolId: z.string(),
20
+ departmentId: z.string().nullable().optional(),
21
+ })
22
+ )
23
+ .query(({ ctx, input }) => listTags(ctx.user!.id, input)),
24
+
25
+ create: protectedProcedure
26
+ .input(
27
+ z.object({
28
+ schoolId: z.string(),
29
+ departmentId: z.string().nullable().optional(),
30
+ name: z.string().min(1),
31
+ color: z.string().optional(),
32
+ })
33
+ )
34
+ .mutation(({ ctx, input }) => createTagRecord(ctx.user!.id, input)),
35
+
36
+ update: protectedProcedure
37
+ .input(
38
+ z.object({
39
+ tagId: z.string(),
40
+ name: z.string().min(1).optional(),
41
+ color: z.string().optional(),
42
+ departmentId: z.string().nullable().optional(),
43
+ })
44
+ )
45
+ .mutation(({ ctx, input }) => updateTagRecord(ctx.user!.id, input)),
46
+
47
+ delete: protectedProcedure
48
+ .input(z.object({ tagId: z.string() }))
49
+ .mutation(({ ctx, input }) =>
50
+ deleteTagRecord(ctx.user!.id, input.tagId)
51
+ ),
52
+
53
+ attach: protectedProcedure
54
+ .input(
55
+ z.object({
56
+ tagId: z.string(),
57
+ target: tagTargetSchema,
58
+ targetId: z.string(),
59
+ })
60
+ )
61
+ .mutation(({ ctx, input }) => attachTag(ctx.user!.id, input)),
62
+
63
+ detach: protectedProcedure
64
+ .input(
65
+ z.object({
66
+ tagId: z.string(),
67
+ target: tagTargetSchema,
68
+ targetId: z.string(),
69
+ })
70
+ )
71
+ .mutation(({ ctx, input }) => detachTag(ctx.user!.id, input)),
72
+
73
+ getAttached: protectedProcedure
74
+ .input(
75
+ z.object({ target: tagTargetSchema, targetId: z.string() })
76
+ )
77
+ .query(({ ctx, input }) => getAttachedTags(ctx.user!.id, input)),
78
+ });
@@ -71,6 +71,9 @@ export async function getAllClasses(userId: string) {
71
71
  assignments: cls.assignments,
72
72
  members: [...cls.students, ...cls.teachers],
73
73
  color: cls.color,
74
+ schoolId: cls.schoolId ?? null,
75
+ school: cls.school ?? null,
76
+ department: cls.department ?? null,
74
77
  })),
75
78
  studentInClass: studentClasses.map((cls) => ({
76
79
  id: cls.id,
@@ -81,6 +84,9 @@ export async function getAllClasses(userId: string) {
81
84
  assignments: cls.assignments,
82
85
  members: [...cls.students, ...cls.teachers],
83
86
  color: cls.color,
87
+ schoolId: cls.schoolId ?? null,
88
+ school: cls.school ?? null,
89
+ department: cls.department ?? null,
84
90
  })),
85
91
  };
86
92
  }
@@ -112,20 +118,87 @@ export async function getClass(userId: string, classId: string) {
112
118
  })),
113
119
  };
114
120
 
115
- const sections = await findSectionsByClassId(classId);
121
+ const [sections, affiliation] = await Promise.all([
122
+ findSectionsByClassId(classId),
123
+ prisma.class.findUnique({
124
+ where: { id: classId },
125
+ select: {
126
+ schoolId: true,
127
+ departmentId: true,
128
+ school: {
129
+ select: {
130
+ id: true,
131
+ name: true,
132
+ description: true,
133
+ logo: { select: { id: true, path: true, type: true } },
134
+ },
135
+ },
136
+ department: {
137
+ select: {
138
+ id: true,
139
+ name: true,
140
+ color: true,
141
+ description: true,
142
+ },
143
+ },
144
+ tags: {
145
+ select: { id: true, name: true, color: true, departmentId: true },
146
+ },
147
+ },
148
+ }),
149
+ ]);
116
150
 
117
151
  return {
118
152
  class: {
119
153
  ...formattedClassData,
120
154
  sections,
155
+ affiliation: affiliation
156
+ ? {
157
+ school: affiliation.school,
158
+ department: affiliation.department,
159
+ tags: affiliation.tags,
160
+ }
161
+ : null,
121
162
  },
122
163
  };
123
164
  }
124
165
 
125
166
  export async function updateClass(
126
167
  classId: string,
127
- data: { name?: string; section?: string; subject?: string }
168
+ data: {
169
+ name?: string;
170
+ section?: string;
171
+ subject?: string;
172
+ color?: string;
173
+ departmentId?: string | null;
174
+ }
128
175
  ) {
176
+ if (data.departmentId !== undefined) {
177
+ // Department must belong to the class's institution.
178
+ const cls = await prisma.class.findUnique({
179
+ where: { id: classId },
180
+ select: { schoolId: true },
181
+ });
182
+ if (!cls?.schoolId && data.departmentId) {
183
+ throw new TRPCError({
184
+ code: "BAD_REQUEST",
185
+ message: "Personal classes can't have a department",
186
+ });
187
+ }
188
+ if (data.departmentId && cls?.schoolId) {
189
+ const dept = await prisma.department.findUnique({
190
+ where: { id: data.departmentId },
191
+ select: { schoolId: true },
192
+ });
193
+ if (!dept || dept.schoolId !== cls.schoolId) {
194
+ throw new TRPCError({
195
+ code: "BAD_REQUEST",
196
+ message: "Department does not belong to this institution",
197
+ });
198
+ }
199
+ }
200
+ }
201
+
129
202
  const updatedClass = await prisma.class.update({
130
203
  where: { id: classId },
131
204
  data,
@@ -134,6 +207,8 @@ export async function updateClass(
134
207
  name: true,
135
208
  section: true,
136
209
  subject: true,
210
+ color: true,
211
+ departmentId: true,
137
212
  },
138
213
  });
139
214
  return { updatedClass };
@@ -149,9 +224,19 @@ export async function createClass(
149
224
  subject: string;
150
225
  color?: string;
151
226
  schoolId?: string;
227
+ departmentId?: string;
152
228
  }
153
229
  ) {
154
- const { students, teachers, name, section, subject, color, schoolId } = input;
230
+ const {
231
+ students,
232
+ teachers,
233
+ name,
234
+ section,
235
+ subject,
236
+ color,
237
+ schoolId,
238
+ departmentId,
239
+ } = input;
155
240
 
156
241
  if (schoolId) {
157
242
  const membership = await prisma.schoolMembership.findFirst({
@@ -174,6 +259,25 @@ export async function createClass(
174
259
  }
175
260
  }
176
261
 
262
+ if (departmentId) {
263
+ if (!schoolId) {
264
+ throw new TRPCError({
265
+ code: "BAD_REQUEST",
266
+ message: "Personal classes can't have a department",
267
+ });
268
+ }
269
+ const dept = await prisma.department.findUnique({
270
+ where: { id: departmentId },
271
+ select: { schoolId: true },
272
+ });
273
+ if (!dept || dept.schoolId !== schoolId) {
274
+ throw new TRPCError({
275
+ code: "BAD_REQUEST",
276
+ message: "Department does not belong to this institution",
277
+ });
278
+ }
279
+ }
280
+
177
281
  const teacherIds = teachers?.length ? teachers : [userId];
178
282
  const studentIds = students?.length ? students : [];
179
283
 
@@ -188,6 +292,7 @@ export async function createClass(
188
292
  subject,
189
293
  color,
190
294
  schoolId,
295
+ departmentId,
191
296
  teachers: { connect: teacherIds.map((id) => ({ id })) },
192
297
  ...(studentIds.length
193
298
  ? { students: { connect: studentIds.map((id) => ({ id })) } }