@studious-lms/server 1.6.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.
- package/dist/models/access.d.ts +21 -66
- package/dist/models/access.d.ts.map +1 -1
- package/dist/models/access.js +10 -26
- package/dist/models/access.js.map +1 -1
- package/dist/models/agenda.d.ts +2 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -1
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +2 -3
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +7 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +2 -4
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +2 -3
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +32 -4
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +15 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +140 -0
- package/dist/models/cloudStorage.d.ts.map +1 -0
- package/dist/models/cloudStorage.js +175 -0
- package/dist/models/cloudStorage.js.map +1 -0
- package/dist/models/comment.d.ts +0 -24
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +2 -6
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +38 -22
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +50 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/department.d.ts +154 -0
- package/dist/models/department.d.ts.map +1 -0
- package/dist/models/department.js +135 -0
- package/dist/models/department.js.map +1 -0
- package/dist/models/event.d.ts +7 -1
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +2 -5
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +25 -10
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +2 -5
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +47 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +5 -3
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +46 -12
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/message.js +19 -10
- package/dist/models/message.js.map +1 -1
- package/dist/models/newtonChat.d.ts +2 -1
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +45 -12
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +19 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +1 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +117 -0
- package/dist/models/tag.d.ts.map +1 -0
- package/dist/models/tag.js +113 -0
- package/dist/models/tag.js.map +1 -0
- package/dist/models/worksheet.d.ts +3 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +8 -8
- package/dist/routers/_app.d.ts +3556 -2298
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +6 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +2 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +3 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +18 -9
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +2 -3
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +87 -14
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +5 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/conversation.d.ts +20 -5
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +6 -3
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/department.d.ts +293 -0
- package/dist/routers/department.d.ts.map +1 -0
- package/dist/routers/department.js +93 -0
- package/dist/routers/department.js.map +1 -0
- package/dist/routers/event.d.ts +2 -1
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +8 -6
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +26 -17
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/folder.d.ts +139 -186
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +82 -90
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +11 -2
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +20 -2
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +9 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/school.d.ts +160 -17
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +46 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/tag.d.ts +119 -0
- package/dist/routers/tag.d.ts.map +1 -0
- package/dist/routers/tag.js +52 -0
- package/dist/routers/tag.js.map +1 -0
- package/dist/routers/worksheet.d.ts +1 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts +0 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +2 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +18 -9
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +4 -4
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +87 -14
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +91 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/conversation.d.ts +21 -5
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +22 -3
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/department.d.ts +200 -0
- package/dist/services/department.d.ts.map +1 -0
- package/dist/services/department.js +368 -0
- package/dist/services/department.js.map +1 -0
- package/dist/services/event.d.ts +2 -1
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +85 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +92 -2
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +125 -149
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +258 -199
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +11 -2
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +8 -7
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +20 -2
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +7 -23
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +9 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +7 -6
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/school.d.ts +142 -15
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +308 -54
- package/dist/services/school.js.map +1 -1
- package/dist/services/tag.d.ts +71 -0
- package/dist/services/tag.d.ts.map +1 -0
- package/dist/services/tag.js +178 -0
- package/dist/services/tag.js.map +1 -0
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +2 -3
- package/dist/utils/aiUser.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
- package/prisma/schema.prisma +119 -35
- package/src/models/access.ts +8 -24
- package/src/models/announcement.ts +0 -1
- package/src/models/auth.ts +0 -1
- package/src/models/class.ts +13 -0
- package/src/models/cloudStorage.ts +194 -0
- package/src/models/comment.ts +0 -4
- package/src/models/conversation.ts +54 -0
- package/src/models/department.ts +159 -0
- package/src/models/event.ts +0 -3
- package/src/models/file.ts +0 -3
- package/src/models/message.ts +19 -5
- package/src/models/school.ts +26 -0
- package/src/models/tag.ts +140 -0
- package/src/routers/_app.ts +4 -0
- package/src/routers/announcement.ts +1 -3
- package/src/routers/assignment.ts +0 -1
- package/src/routers/class.ts +3 -0
- package/src/routers/conversation.ts +7 -0
- package/src/routers/department.ts +174 -0
- package/src/routers/file.ts +29 -26
- package/src/routers/folder.ts +100 -121
- package/src/routers/school.ts +83 -0
- package/src/routers/tag.ts +78 -0
- package/src/services/assignment.ts +2 -3
- package/src/services/class.ts +108 -3
- package/src/services/conversation.ts +30 -0
- package/src/services/department.ts +478 -0
- package/src/services/file.ts +100 -0
- package/src/services/folder.ts +313 -240
- package/src/services/labChat.ts +7 -6
- package/src/services/message.ts +5 -21
- package/src/services/newtonChat.ts +5 -5
- package/src/services/school.ts +369 -57
- package/src/services/tag.ts +229 -0
- package/src/utils/aiUser.ts +0 -1
- package/tests/integration/routers/conversation.test.ts +121 -2
package/src/services/class.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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 {
|
|
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 })) } }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
countUnreadMessages,
|
|
9
9
|
countUnreadMentions,
|
|
10
10
|
findUserByUsername,
|
|
11
|
+
searchInstitutionConversationMembers,
|
|
11
12
|
findDmConversationsByUserId,
|
|
12
13
|
findUsersByIdsOrUsernames,
|
|
13
14
|
createConversation,
|
|
@@ -17,6 +18,11 @@ import {
|
|
|
17
18
|
deleteConversationMember,
|
|
18
19
|
} from "../models/conversation.js";
|
|
19
20
|
|
|
21
|
+
/** Maximum number of member suggestions returned for conversation search. */
|
|
22
|
+
const MAX_MEMBER_SEARCH_RESULTS = 10;
|
|
23
|
+
/** Minimum non-whitespace characters required before running member search. */
|
|
24
|
+
const MIN_MEMBER_SEARCH_QUERY_LENGTH = 2;
|
|
25
|
+
|
|
20
26
|
/** List user's conversations with unread and mention counts. */
|
|
21
27
|
export async function listConversations(userId: string) {
|
|
22
28
|
const conversations = await findConversationsByUserId(userId);
|
|
@@ -89,6 +95,30 @@ export async function hideConversation(userId: string, conversationId: string) {
|
|
|
89
95
|
return { success: true };
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Search active school peers that can be selected for a new conversation.
|
|
100
|
+
*
|
|
101
|
+
* Blank or one-character input returns an empty list instead of searching all
|
|
102
|
+
* institution members, which keeps typeahead callers from issuing broad result
|
|
103
|
+
* queries.
|
|
104
|
+
*/
|
|
105
|
+
export async function searchConversationMembers(userId: string, query: string) {
|
|
106
|
+
const trimmedQuery = query.trim();
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!trimmedQuery ||
|
|
110
|
+
trimmedQuery.length < MIN_MEMBER_SEARCH_QUERY_LENGTH
|
|
111
|
+
) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return searchInstitutionConversationMembers(
|
|
116
|
+
userId,
|
|
117
|
+
trimmedQuery,
|
|
118
|
+
MAX_MEMBER_SEARCH_RESULTS
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
/** Create a DM or group conversation with the given members. */
|
|
93
123
|
export async function createConversationRecord(
|
|
94
124
|
userId: string,
|
|
@@ -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
|
+
}
|