@studious-lms/server 1.7.0 → 1.9.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 (212) hide show
  1. package/dist/middleware/security.d.ts.map +1 -1
  2. package/dist/middleware/security.js +3 -3
  3. package/dist/middleware/security.js.map +1 -1
  4. package/dist/models/access.d.ts +21 -66
  5. package/dist/models/access.d.ts.map +1 -1
  6. package/dist/models/access.js +10 -26
  7. package/dist/models/access.js.map +1 -1
  8. package/dist/models/agenda.d.ts +4 -2
  9. package/dist/models/agenda.d.ts.map +1 -1
  10. package/dist/models/announcement.d.ts +4 -3
  11. package/dist/models/announcement.d.ts.map +1 -1
  12. package/dist/models/announcement.js +2 -3
  13. package/dist/models/announcement.js.map +1 -1
  14. package/dist/models/assignment.d.ts +40 -33
  15. package/dist/models/assignment.d.ts.map +1 -1
  16. package/dist/models/auth.d.ts +2 -4
  17. package/dist/models/auth.d.ts.map +1 -1
  18. package/dist/models/auth.js +2 -3
  19. package/dist/models/auth.js.map +1 -1
  20. package/dist/models/class.d.ts +34 -6
  21. package/dist/models/class.d.ts.map +1 -1
  22. package/dist/models/class.js +15 -2
  23. package/dist/models/class.js.map +1 -1
  24. package/dist/models/cloudStorage.d.ts +153 -0
  25. package/dist/models/cloudStorage.d.ts.map +1 -0
  26. package/dist/models/cloudStorage.js +295 -0
  27. package/dist/models/cloudStorage.js.map +1 -0
  28. package/dist/models/comment.d.ts +0 -24
  29. package/dist/models/comment.d.ts.map +1 -1
  30. package/dist/models/comment.js +2 -6
  31. package/dist/models/comment.js.map +1 -1
  32. package/dist/models/conversation.d.ts +22 -22
  33. package/dist/models/department.d.ts +154 -0
  34. package/dist/models/department.d.ts.map +1 -0
  35. package/dist/models/department.js +135 -0
  36. package/dist/models/department.js.map +1 -0
  37. package/dist/models/event.d.ts +10 -4
  38. package/dist/models/event.d.ts.map +1 -1
  39. package/dist/models/event.js +2 -5
  40. package/dist/models/event.js.map +1 -1
  41. package/dist/models/file.d.ts +52 -36
  42. package/dist/models/file.d.ts.map +1 -1
  43. package/dist/models/file.js +4 -42
  44. package/dist/models/file.js.map +1 -1
  45. package/dist/models/folder.d.ts +47 -0
  46. package/dist/models/folder.d.ts.map +1 -1
  47. package/dist/models/labChat.d.ts +11 -9
  48. package/dist/models/labChat.d.ts.map +1 -1
  49. package/dist/models/message.d.ts +46 -12
  50. package/dist/models/message.d.ts.map +1 -1
  51. package/dist/models/message.js +19 -10
  52. package/dist/models/message.js.map +1 -1
  53. package/dist/models/newtonChat.d.ts +2 -1
  54. package/dist/models/newtonChat.d.ts.map +1 -1
  55. package/dist/models/school.d.ts +45 -12
  56. package/dist/models/school.d.ts.map +1 -1
  57. package/dist/models/school.js +19 -2
  58. package/dist/models/school.js.map +1 -1
  59. package/dist/models/section.d.ts +2 -1
  60. package/dist/models/section.d.ts.map +1 -1
  61. package/dist/models/tag.d.ts +117 -0
  62. package/dist/models/tag.d.ts.map +1 -0
  63. package/dist/models/tag.js +113 -0
  64. package/dist/models/tag.js.map +1 -0
  65. package/dist/models/worksheet.d.ts +3 -0
  66. package/dist/models/worksheet.d.ts.map +1 -1
  67. package/dist/pipelines/aiLabChatContract.d.ts +8 -8
  68. package/dist/routers/_app.d.ts +6704 -5222
  69. package/dist/routers/_app.d.ts.map +1 -1
  70. package/dist/routers/_app.js +8 -2
  71. package/dist/routers/_app.js.map +1 -1
  72. package/dist/routers/agenda.d.ts +4 -2
  73. package/dist/routers/agenda.d.ts.map +1 -1
  74. package/dist/routers/announcement.d.ts.map +1 -1
  75. package/dist/routers/announcement.js +3 -5
  76. package/dist/routers/announcement.js.map +1 -1
  77. package/dist/routers/assignment.d.ts +49 -40
  78. package/dist/routers/assignment.d.ts.map +1 -1
  79. package/dist/routers/assignment.js +2 -3
  80. package/dist/routers/assignment.js.map +1 -1
  81. package/dist/routers/class.d.ts +92 -19
  82. package/dist/routers/class.d.ts.map +1 -1
  83. package/dist/routers/class.js +5 -2
  84. package/dist/routers/class.js.map +1 -1
  85. package/dist/routers/conversation.d.ts +5 -5
  86. package/dist/routers/department.d.ts +293 -0
  87. package/dist/routers/department.d.ts.map +1 -0
  88. package/dist/routers/department.js +93 -0
  89. package/dist/routers/department.js.map +1 -0
  90. package/dist/routers/event.d.ts +4 -3
  91. package/dist/routers/event.d.ts.map +1 -1
  92. package/dist/routers/file.d.ts +10 -8
  93. package/dist/routers/file.d.ts.map +1 -1
  94. package/dist/routers/file.js +26 -17
  95. package/dist/routers/file.js.map +1 -1
  96. package/dist/routers/folder.d.ts +163 -186
  97. package/dist/routers/folder.d.ts.map +1 -1
  98. package/dist/routers/folder.js +102 -88
  99. package/dist/routers/folder.js.map +1 -1
  100. package/dist/routers/labChat.d.ts +13 -4
  101. package/dist/routers/labChat.d.ts.map +1 -1
  102. package/dist/routers/message.d.ts +20 -2
  103. package/dist/routers/message.d.ts.map +1 -1
  104. package/dist/routers/newtonChat.d.ts +10 -1
  105. package/dist/routers/newtonChat.d.ts.map +1 -1
  106. package/dist/routers/permissions.d.ts +120 -0
  107. package/dist/routers/permissions.d.ts.map +1 -0
  108. package/dist/routers/permissions.js +50 -0
  109. package/dist/routers/permissions.js.map +1 -0
  110. package/dist/routers/school.d.ts +160 -17
  111. package/dist/routers/school.d.ts.map +1 -1
  112. package/dist/routers/school.js +46 -3
  113. package/dist/routers/school.js.map +1 -1
  114. package/dist/routers/tag.d.ts +119 -0
  115. package/dist/routers/tag.d.ts.map +1 -0
  116. package/dist/routers/tag.js +52 -0
  117. package/dist/routers/tag.js.map +1 -0
  118. package/dist/routers/worksheet.d.ts +1 -0
  119. package/dist/routers/worksheet.d.ts.map +1 -1
  120. package/dist/seedDatabase.d.ts +0 -1
  121. package/dist/seedDatabase.d.ts.map +1 -1
  122. package/dist/services/agenda.d.ts +4 -2
  123. package/dist/services/agenda.d.ts.map +1 -1
  124. package/dist/services/assignment.d.ts +49 -40
  125. package/dist/services/assignment.d.ts.map +1 -1
  126. package/dist/services/class.d.ts +92 -19
  127. package/dist/services/class.d.ts.map +1 -1
  128. package/dist/services/class.js +92 -5
  129. package/dist/services/class.js.map +1 -1
  130. package/dist/services/conversation.d.ts +5 -5
  131. package/dist/services/department.d.ts +200 -0
  132. package/dist/services/department.d.ts.map +1 -0
  133. package/dist/services/department.js +368 -0
  134. package/dist/services/department.js.map +1 -0
  135. package/dist/services/event.d.ts +4 -3
  136. package/dist/services/event.d.ts.map +1 -1
  137. package/dist/services/file.d.ts +105 -3
  138. package/dist/services/file.d.ts.map +1 -1
  139. package/dist/services/file.js +167 -41
  140. package/dist/services/file.js.map +1 -1
  141. package/dist/services/folder.d.ts +220 -149
  142. package/dist/services/folder.d.ts.map +1 -1
  143. package/dist/services/folder.js +326 -196
  144. package/dist/services/folder.js.map +1 -1
  145. package/dist/services/labChat.d.ts +13 -4
  146. package/dist/services/labChat.d.ts.map +1 -1
  147. package/dist/services/labChat.js +4 -3
  148. package/dist/services/labChat.js.map +1 -1
  149. package/dist/services/message.d.ts +20 -2
  150. package/dist/services/message.d.ts.map +1 -1
  151. package/dist/services/message.js +7 -23
  152. package/dist/services/message.js.map +1 -1
  153. package/dist/services/newtonChat.d.ts +9 -0
  154. package/dist/services/newtonChat.d.ts.map +1 -1
  155. package/dist/services/newtonChat.js +4 -3
  156. package/dist/services/newtonChat.js.map +1 -1
  157. package/dist/services/permissions.d.ts +188 -0
  158. package/dist/services/permissions.d.ts.map +1 -0
  159. package/dist/services/permissions.js +661 -0
  160. package/dist/services/permissions.js.map +1 -0
  161. package/dist/services/school.d.ts +142 -15
  162. package/dist/services/school.d.ts.map +1 -1
  163. package/dist/services/school.js +308 -54
  164. package/dist/services/school.js.map +1 -1
  165. package/dist/services/tag.d.ts +71 -0
  166. package/dist/services/tag.d.ts.map +1 -0
  167. package/dist/services/tag.js +178 -0
  168. package/dist/services/tag.js.map +1 -0
  169. package/dist/services/worksheet.d.ts +1 -0
  170. package/dist/services/worksheet.d.ts.map +1 -1
  171. package/dist/utils/aiUser.d.ts.map +1 -1
  172. package/dist/utils/aiUser.js +2 -3
  173. package/dist/utils/aiUser.js.map +1 -1
  174. package/package.json +1 -1
  175. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
  176. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
  177. package/prisma/schema.prisma +193 -35
  178. package/src/middleware/security.ts +1 -1
  179. package/src/models/access.ts +8 -24
  180. package/src/models/announcement.ts +0 -1
  181. package/src/models/auth.ts +0 -1
  182. package/src/models/class.ts +13 -0
  183. package/src/models/cloudStorage.ts +327 -0
  184. package/src/models/comment.ts +0 -4
  185. package/src/models/department.ts +159 -0
  186. package/src/models/event.ts +0 -3
  187. package/src/models/file.ts +2 -40
  188. package/src/models/message.ts +19 -5
  189. package/src/models/school.ts +26 -0
  190. package/src/models/tag.ts +140 -0
  191. package/src/routers/_app.ts +6 -0
  192. package/src/routers/announcement.ts +1 -3
  193. package/src/routers/assignment.ts +0 -1
  194. package/src/routers/class.ts +3 -0
  195. package/src/routers/department.ts +174 -0
  196. package/src/routers/file.ts +29 -26
  197. package/src/routers/folder.ts +127 -118
  198. package/src/routers/permissions.ts +71 -0
  199. package/src/routers/school.ts +83 -0
  200. package/src/routers/tag.ts +78 -0
  201. package/src/services/class.ts +109 -4
  202. package/src/services/department.ts +478 -0
  203. package/src/services/file.ts +172 -43
  204. package/src/services/folder.ts +399 -237
  205. package/src/services/labChat.ts +2 -1
  206. package/src/services/message.ts +5 -21
  207. package/src/services/newtonChat.ts +2 -1
  208. package/src/services/permissions.ts +778 -0
  209. package/src/services/school.ts +369 -57
  210. package/src/services/tag.ts +229 -0
  211. package/src/utils/aiUser.ts +0 -1
  212. package/tests/unit/services/permissions.test.ts +463 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Department service — admin CRUD, members, and template management for
3
+ * department-level rubrics and grading boundaries.
4
+ */
5
+ import { TRPCError } from "@trpc/server";
6
+ import { prisma } from "../lib/prisma.js";
7
+ import {
8
+ addDepartmentMembers,
9
+ createDepartment,
10
+ deleteDepartment,
11
+ findDepartmentMembership,
12
+ getDepartment,
13
+ listDepartmentsForSchool,
14
+ listDepartmentsForUser,
15
+ removeDepartmentMember,
16
+ updateDepartment,
17
+ } from "../models/department.js";
18
+ import {
19
+ findActiveSchoolAdminMembership,
20
+ findSchoolMembership,
21
+ } from "../models/school.js";
22
+
23
+ async function requireAdmin(schoolId: string, userId: string) {
24
+ const membership = await findActiveSchoolAdminMembership(schoolId, userId);
25
+ if (!membership) {
26
+ throw new TRPCError({
27
+ code: "FORBIDDEN",
28
+ message: "Only school admins can manage departments",
29
+ });
30
+ }
31
+ }
32
+
33
+ async function requireMember(schoolId: string, userId: string) {
34
+ const membership = await findSchoolMembership(schoolId, userId);
35
+ if (!membership || membership.status !== "ACTIVE") {
36
+ throw new TRPCError({
37
+ code: "FORBIDDEN",
38
+ message: "You aren't a member of this institution",
39
+ });
40
+ }
41
+ }
42
+
43
+ export async function listDepartments(
44
+ userId: string,
45
+ schoolId: string
46
+ ) {
47
+ await requireMember(schoolId, userId);
48
+ return listDepartmentsForSchool(schoolId);
49
+ }
50
+
51
+ export async function listMyDepartments(
52
+ userId: string,
53
+ schoolId: string
54
+ ) {
55
+ await requireMember(schoolId, userId);
56
+ return listDepartmentsForUser(userId, schoolId);
57
+ }
58
+
59
+ export async function getDepartmentDetails(
60
+ userId: string,
61
+ departmentId: string
62
+ ) {
63
+ const department = await getDepartment(departmentId);
64
+ if (!department) {
65
+ throw new TRPCError({
66
+ code: "NOT_FOUND",
67
+ message: "Department not found",
68
+ });
69
+ }
70
+ await requireMember(department.schoolId, userId);
71
+ return department;
72
+ }
73
+
74
+ export async function createDepartmentRecord(
75
+ userId: string,
76
+ input: {
77
+ schoolId: string;
78
+ name: string;
79
+ description?: string;
80
+ color?: string;
81
+ }
82
+ ) {
83
+ await requireAdmin(input.schoolId, userId);
84
+ return createDepartment(input);
85
+ }
86
+
87
+ export async function updateDepartmentRecord(
88
+ userId: string,
89
+ input: {
90
+ departmentId: string;
91
+ name?: string;
92
+ description?: string | null;
93
+ color?: string;
94
+ }
95
+ ) {
96
+ const department = await getDepartment(input.departmentId);
97
+ if (!department) {
98
+ throw new TRPCError({
99
+ code: "NOT_FOUND",
100
+ message: "Department not found",
101
+ });
102
+ }
103
+ await requireAdmin(department.schoolId, userId);
104
+ return updateDepartment(input.departmentId, {
105
+ name: input.name,
106
+ description: input.description,
107
+ color: input.color,
108
+ });
109
+ }
110
+
111
+ export async function deleteDepartmentRecord(
112
+ userId: string,
113
+ departmentId: string
114
+ ) {
115
+ const department = await getDepartment(departmentId);
116
+ if (!department) {
117
+ throw new TRPCError({
118
+ code: "NOT_FOUND",
119
+ message: "Department not found",
120
+ });
121
+ }
122
+ await requireAdmin(department.schoolId, userId);
123
+ await deleteDepartment(departmentId);
124
+ return { success: true };
125
+ }
126
+
127
+ export async function addMembersToDepartment(
128
+ userId: string,
129
+ input: { departmentId: string; userIds: string[] }
130
+ ) {
131
+ const department = await getDepartment(input.departmentId);
132
+ if (!department) {
133
+ throw new TRPCError({
134
+ code: "NOT_FOUND",
135
+ message: "Department not found",
136
+ });
137
+ }
138
+ await requireAdmin(department.schoolId, userId);
139
+
140
+ // Members must already be in the institution.
141
+ const memberships = await prisma.schoolMembership.findMany({
142
+ where: {
143
+ schoolId: department.schoolId,
144
+ userId: { in: input.userIds },
145
+ status: "ACTIVE",
146
+ },
147
+ select: { userId: true },
148
+ });
149
+ const validIds = new Set(memberships.map((m) => m.userId));
150
+ const validUserIds = input.userIds.filter((id) => validIds.has(id));
151
+
152
+ if (validUserIds.length === 0) {
153
+ throw new TRPCError({
154
+ code: "BAD_REQUEST",
155
+ message: "None of the supplied users are institution members",
156
+ });
157
+ }
158
+ return addDepartmentMembers(input.departmentId, validUserIds);
159
+ }
160
+
161
+ export async function removeMemberFromDepartment(
162
+ userId: string,
163
+ input: { departmentId: string; memberId: string }
164
+ ) {
165
+ const department = await getDepartment(input.departmentId);
166
+ if (!department) {
167
+ throw new TRPCError({
168
+ code: "NOT_FOUND",
169
+ message: "Department not found",
170
+ });
171
+ }
172
+ await requireAdmin(department.schoolId, userId);
173
+ return removeDepartmentMember(input.departmentId, input.memberId);
174
+ }
175
+
176
+ /** Department template authoring (rubrics + boundaries). */
177
+ export async function createRubricTemplate(
178
+ userId: string,
179
+ input: { departmentId: string; name: string; structured: string }
180
+ ) {
181
+ const department = await getDepartment(input.departmentId);
182
+ if (!department) {
183
+ throw new TRPCError({
184
+ code: "NOT_FOUND",
185
+ message: "Department not found",
186
+ });
187
+ }
188
+ await requireAdmin(department.schoolId, userId);
189
+ return prisma.markScheme.create({
190
+ data: {
191
+ name: input.name,
192
+ structured: input.structured.replace(/\\n/g, "\n"),
193
+ department: { connect: { id: input.departmentId } },
194
+ },
195
+ select: { id: true, name: true, structured: true, departmentId: true },
196
+ });
197
+ }
198
+
199
+ export async function updateRubricTemplate(
200
+ userId: string,
201
+ input: {
202
+ templateId: string;
203
+ name?: string;
204
+ structured?: string;
205
+ }
206
+ ) {
207
+ const template = await prisma.markScheme.findUnique({
208
+ where: { id: input.templateId },
209
+ select: { departmentId: true, department: { select: { schoolId: true } } },
210
+ });
211
+ if (!template?.departmentId || !template.department) {
212
+ throw new TRPCError({
213
+ code: "NOT_FOUND",
214
+ message: "Template not found",
215
+ });
216
+ }
217
+ await requireAdmin(template.department.schoolId, userId);
218
+ return prisma.markScheme.update({
219
+ where: { id: input.templateId },
220
+ data: {
221
+ ...(input.name !== undefined ? { name: input.name } : {}),
222
+ ...(input.structured !== undefined
223
+ ? { structured: input.structured.replace(/\\n/g, "\n") }
224
+ : {}),
225
+ },
226
+ });
227
+ }
228
+
229
+ export async function deleteRubricTemplate(
230
+ userId: string,
231
+ templateId: string
232
+ ) {
233
+ const template = await prisma.markScheme.findUnique({
234
+ where: { id: templateId },
235
+ select: { departmentId: true, department: { select: { schoolId: true } } },
236
+ });
237
+ if (!template?.departmentId || !template.department) {
238
+ throw new TRPCError({
239
+ code: "NOT_FOUND",
240
+ message: "Template not found",
241
+ });
242
+ }
243
+ await requireAdmin(template.department.schoolId, userId);
244
+ return prisma.markScheme.delete({ where: { id: templateId } });
245
+ }
246
+
247
+ export async function createBoundaryTemplate(
248
+ userId: string,
249
+ input: { departmentId: string; name: string; structured: string }
250
+ ) {
251
+ const department = await getDepartment(input.departmentId);
252
+ if (!department) {
253
+ throw new TRPCError({
254
+ code: "NOT_FOUND",
255
+ message: "Department not found",
256
+ });
257
+ }
258
+ await requireAdmin(department.schoolId, userId);
259
+ return prisma.gradingBoundary.create({
260
+ data: {
261
+ name: input.name,
262
+ structured: input.structured.replace(/\\n/g, "\n"),
263
+ department: { connect: { id: input.departmentId } },
264
+ },
265
+ select: { id: true, name: true, structured: true, departmentId: true },
266
+ });
267
+ }
268
+
269
+ export async function updateBoundaryTemplate(
270
+ userId: string,
271
+ input: {
272
+ templateId: string;
273
+ name?: string;
274
+ structured?: string;
275
+ }
276
+ ) {
277
+ const template = await prisma.gradingBoundary.findUnique({
278
+ where: { id: input.templateId },
279
+ select: { departmentId: true, department: { select: { schoolId: true } } },
280
+ });
281
+ if (!template?.departmentId || !template.department) {
282
+ throw new TRPCError({
283
+ code: "NOT_FOUND",
284
+ message: "Template not found",
285
+ });
286
+ }
287
+ await requireAdmin(template.department.schoolId, userId);
288
+ return prisma.gradingBoundary.update({
289
+ where: { id: input.templateId },
290
+ data: {
291
+ ...(input.name !== undefined ? { name: input.name } : {}),
292
+ ...(input.structured !== undefined
293
+ ? { structured: input.structured.replace(/\\n/g, "\n") }
294
+ : {}),
295
+ },
296
+ });
297
+ }
298
+
299
+ export async function deleteBoundaryTemplate(
300
+ userId: string,
301
+ templateId: string
302
+ ) {
303
+ const template = await prisma.gradingBoundary.findUnique({
304
+ where: { id: templateId },
305
+ select: { departmentId: true, department: { select: { schoolId: true } } },
306
+ });
307
+ if (!template?.departmentId || !template.department) {
308
+ throw new TRPCError({
309
+ code: "NOT_FOUND",
310
+ message: "Template not found",
311
+ });
312
+ }
313
+ await requireAdmin(template.department.schoolId, userId);
314
+ return prisma.gradingBoundary.delete({ where: { id: templateId } });
315
+ }
316
+
317
+ /**
318
+ * Apply a department template to a class by *copying* the structured payload
319
+ * into a class-scoped record. Teacher-only (the class teacher).
320
+ */
321
+ export async function applyRubricTemplate(
322
+ userId: string,
323
+ input: { classId: string; templateId: string }
324
+ ) {
325
+ const cls = await prisma.class.findFirst({
326
+ where: {
327
+ id: input.classId,
328
+ OR: [
329
+ { teachers: { some: { id: userId } } },
330
+ {
331
+ school: {
332
+ memberships: {
333
+ some: { userId, role: "ADMIN", status: "ACTIVE" },
334
+ },
335
+ },
336
+ },
337
+ ],
338
+ },
339
+ select: { id: true, departmentId: true },
340
+ });
341
+ if (!cls) {
342
+ throw new TRPCError({
343
+ code: "FORBIDDEN",
344
+ message: "You don't have permission to apply templates to this class",
345
+ });
346
+ }
347
+
348
+ const template = await prisma.markScheme.findUnique({
349
+ where: { id: input.templateId },
350
+ select: { id: true, structured: true, departmentId: true, name: true },
351
+ });
352
+ if (!template?.departmentId) {
353
+ throw new TRPCError({
354
+ code: "NOT_FOUND",
355
+ message: "Template not found",
356
+ });
357
+ }
358
+ if (cls.departmentId && cls.departmentId !== template.departmentId) {
359
+ throw new TRPCError({
360
+ code: "FORBIDDEN",
361
+ message:
362
+ "This template belongs to a different department than the class",
363
+ });
364
+ }
365
+
366
+ return prisma.markScheme.create({
367
+ data: {
368
+ class: { connect: { id: input.classId } },
369
+ structured: template.structured,
370
+ name: template.name,
371
+ },
372
+ });
373
+ }
374
+
375
+ export async function applyBoundaryTemplate(
376
+ userId: string,
377
+ input: { classId: string; templateId: string }
378
+ ) {
379
+ const cls = await prisma.class.findFirst({
380
+ where: {
381
+ id: input.classId,
382
+ OR: [
383
+ { teachers: { some: { id: userId } } },
384
+ {
385
+ school: {
386
+ memberships: {
387
+ some: { userId, role: "ADMIN", status: "ACTIVE" },
388
+ },
389
+ },
390
+ },
391
+ ],
392
+ },
393
+ select: { id: true, departmentId: true },
394
+ });
395
+ if (!cls) {
396
+ throw new TRPCError({
397
+ code: "FORBIDDEN",
398
+ message: "You don't have permission to apply templates to this class",
399
+ });
400
+ }
401
+
402
+ const template = await prisma.gradingBoundary.findUnique({
403
+ where: { id: input.templateId },
404
+ select: { id: true, structured: true, departmentId: true, name: true },
405
+ });
406
+ if (!template?.departmentId) {
407
+ throw new TRPCError({
408
+ code: "NOT_FOUND",
409
+ message: "Template not found",
410
+ });
411
+ }
412
+ if (cls.departmentId && cls.departmentId !== template.departmentId) {
413
+ throw new TRPCError({
414
+ code: "FORBIDDEN",
415
+ message:
416
+ "This template belongs to a different department than the class",
417
+ });
418
+ }
419
+
420
+ return prisma.gradingBoundary.create({
421
+ data: {
422
+ class: { connect: { id: input.classId } },
423
+ structured: template.structured,
424
+ name: template.name,
425
+ },
426
+ });
427
+ }
428
+
429
+ /** Helper used by the auto-tag flow on class creation. */
430
+ export async function setClassDepartment(
431
+ userId: string,
432
+ input: { classId: string; departmentId: string | null }
433
+ ) {
434
+ const cls = await prisma.class.findFirst({
435
+ where: {
436
+ id: input.classId,
437
+ OR: [
438
+ { teachers: { some: { id: userId } } },
439
+ {
440
+ school: {
441
+ memberships: {
442
+ some: { userId, role: "ADMIN", status: "ACTIVE" },
443
+ },
444
+ },
445
+ },
446
+ ],
447
+ },
448
+ select: { id: true, schoolId: true },
449
+ });
450
+ if (!cls?.schoolId) {
451
+ throw new TRPCError({
452
+ code: "BAD_REQUEST",
453
+ message: "Class is not affiliated with an institution",
454
+ });
455
+ }
456
+
457
+ if (input.departmentId) {
458
+ const dept = await getDepartment(input.departmentId);
459
+ if (!dept || dept.schoolId !== cls.schoolId) {
460
+ throw new TRPCError({
461
+ code: "BAD_REQUEST",
462
+ message: "Department does not belong to this institution",
463
+ });
464
+ }
465
+ }
466
+ return prisma.class.update({
467
+ where: { id: input.classId },
468
+ data: { departmentId: input.departmentId },
469
+ select: { id: true, departmentId: true },
470
+ });
471
+ }
472
+
473
+ export async function checkDepartmentMember(
474
+ userId: string,
475
+ departmentId: string
476
+ ) {
477
+ return Boolean(await findDepartmentMembership(departmentId, userId));
478
+ }