@studious-lms/server 1.21.0 → 1.23.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/agenda.d.ts +6 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +27 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +4 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +14 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/event.d.ts +11 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +19 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +4 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/gradebook.d.ts +180 -0
- package/dist/models/gradebook.d.ts.map +1 -0
- package/dist/models/gradebook.js +88 -0
- package/dist/models/gradebook.js.map +1 -0
- package/dist/models/labChat.d.ts +4 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +2 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +177 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +75 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +3 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +6 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/routers/_app.d.ts +1475 -73
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/admin.d.ts +164 -0
- package/dist/routers/admin.d.ts.map +1 -1
- package/dist/routers/admin.js +23 -4
- package/dist/routers/admin.js.map +1 -1
- package/dist/routers/agenda.d.ts +6 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +30 -3
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/class.d.ts +17 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/event.d.ts +4 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +66 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +111 -2
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/gradebook.d.ts +299 -0
- package/dist/routers/gradebook.d.ts.map +1 -0
- package/dist/routers/gradebook.js +107 -0
- package/dist/routers/gradebook.js.map +1 -0
- package/dist/routers/school.d.ts +118 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +32 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/section.d.ts +1 -1
- package/dist/routers/worksheet.d.ts +3 -1
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/services/adminInstitution.d.ts +51 -0
- package/dist/services/adminInstitution.d.ts.map +1 -1
- package/dist/services/adminInstitution.js +52 -2
- package/dist/services/adminInstitution.js.map +1 -1
- package/dist/services/adminMetrics.d.ts +17 -0
- package/dist/services/adminMetrics.d.ts.map +1 -0
- package/dist/services/adminMetrics.js +37 -0
- package/dist/services/adminMetrics.js.map +1 -0
- package/dist/services/adminRunJob.d.ts +9 -0
- package/dist/services/adminRunJob.d.ts.map +1 -1
- package/dist/services/adminRunJob.js +18 -2
- package/dist/services/adminRunJob.js.map +1 -1
- package/dist/services/adminUser.d.ts +67 -0
- package/dist/services/adminUser.d.ts.map +1 -0
- package/dist/services/adminUser.js +87 -0
- package/dist/services/adminUser.js.map +1 -0
- package/dist/services/agenda.d.ts +6 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +27 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/class.d.ts +17 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/event.d.ts +4 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/gradeCalculation.d.ts +70 -0
- package/dist/services/gradeCalculation.d.ts.map +1 -0
- package/dist/services/gradeCalculation.js +148 -0
- package/dist/services/gradeCalculation.js.map +1 -0
- package/dist/services/gradebook.d.ts +197 -0
- package/dist/services/gradebook.d.ts.map +1 -0
- package/dist/services/gradebook.js +263 -0
- package/dist/services/gradebook.js.map +1 -0
- package/dist/services/school.d.ts +98 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +116 -4
- package/dist/services/school.js.map +1 -1
- package/dist/services/worksheet.d.ts +2 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260708011629_add_gradebook_categories_columns_policies/migration.sql +78 -0
- package/prisma/migrations/20260723142314_make_file_annotation_submission_optional/migration.sql +2 -0
- package/prisma/schema.prisma +62 -2
- package/src/models/gradebook.ts +91 -0
- package/src/models/school.ts +89 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/admin.ts +28 -0
- package/src/routers/file.ts +128 -0
- package/src/routers/gradebook.ts +173 -0
- package/src/routers/school.ts +45 -0
- package/src/services/adminInstitution.ts +52 -0
- package/src/services/adminMetrics.ts +46 -0
- package/src/services/adminRunJob.ts +24 -0
- package/src/services/adminUser.ts +93 -0
- package/src/services/gradeCalculation.ts +253 -0
- package/src/services/gradebook.ts +383 -0
- package/src/services/school.ts +178 -1
- package/tests/unit/services/gradeCalculation.test.ts +206 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2860d786-c6b1-5d9a-b1bd-880c5bef0f51")}catch(e){}}();
|
|
3
|
+
import { TRPCError } from "@trpc/server";
|
|
4
|
+
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
import { isTeacherInClass } from "../models/class.js";
|
|
6
|
+
import { findGradeCategoriesByClassId, findGradeCategory, findCustomGradeColumnsByClassId, findCustomGradeColumn, findClassForGradebook, findClassGradePolicies, } from "../models/gradebook.js";
|
|
7
|
+
import { computeFinalGrade, resolveLetterGrade, } from "./gradeCalculation.js";
|
|
8
|
+
// ===== Categories =====
|
|
9
|
+
export async function listGradeCategories(classId) {
|
|
10
|
+
return findGradeCategoriesByClassId(classId);
|
|
11
|
+
}
|
|
12
|
+
export async function createGradeCategory(classId, data) {
|
|
13
|
+
return prisma.gradeCategory.create({
|
|
14
|
+
data: {
|
|
15
|
+
classId,
|
|
16
|
+
name: data.name,
|
|
17
|
+
weight: data.weight,
|
|
18
|
+
dropLowest: data.dropLowest ?? 0,
|
|
19
|
+
position: data.position ?? (await findGradeCategoriesByClassId(classId)).length,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function updateGradeCategory(categoryId, classId, data) {
|
|
24
|
+
const category = await findGradeCategory(categoryId, classId);
|
|
25
|
+
if (!category) {
|
|
26
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Category not found" });
|
|
27
|
+
}
|
|
28
|
+
return prisma.gradeCategory.update({
|
|
29
|
+
where: { id: categoryId },
|
|
30
|
+
data,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function deleteGradeCategory(categoryId, classId) {
|
|
34
|
+
const category = await findGradeCategory(categoryId, classId);
|
|
35
|
+
if (!category) {
|
|
36
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Category not found" });
|
|
37
|
+
}
|
|
38
|
+
return prisma.gradeCategory.delete({ where: { id: categoryId } });
|
|
39
|
+
}
|
|
40
|
+
export async function assignAssignmentCategory(classId, assignmentId, gradeCategoryId) {
|
|
41
|
+
if (gradeCategoryId) {
|
|
42
|
+
const category = await findGradeCategory(gradeCategoryId, classId);
|
|
43
|
+
if (!category) {
|
|
44
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Category not found" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return prisma.assignment.update({
|
|
48
|
+
where: { id: assignmentId, classId },
|
|
49
|
+
data: { gradeCategoryId },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// ===== Custom columns =====
|
|
53
|
+
export async function listCustomGradeColumns(classId) {
|
|
54
|
+
return findCustomGradeColumnsByClassId(classId);
|
|
55
|
+
}
|
|
56
|
+
export async function createCustomGradeColumn(classId, data) {
|
|
57
|
+
if (data.gradeCategoryId) {
|
|
58
|
+
const category = await findGradeCategory(data.gradeCategoryId, classId);
|
|
59
|
+
if (!category) {
|
|
60
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Category not found" });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return prisma.customGradeColumn.create({
|
|
64
|
+
data: {
|
|
65
|
+
classId,
|
|
66
|
+
name: data.name,
|
|
67
|
+
maxPoints: data.maxPoints,
|
|
68
|
+
gradeCategoryId: data.gradeCategoryId ?? null,
|
|
69
|
+
position: data.position ?? (await findCustomGradeColumnsByClassId(classId)).length,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function updateCustomGradeColumn(columnId, classId, data) {
|
|
74
|
+
const column = await findCustomGradeColumn(columnId, classId);
|
|
75
|
+
if (!column) {
|
|
76
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Column not found" });
|
|
77
|
+
}
|
|
78
|
+
if (data.gradeCategoryId) {
|
|
79
|
+
const category = await findGradeCategory(data.gradeCategoryId, classId);
|
|
80
|
+
if (!category) {
|
|
81
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Category not found" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return prisma.customGradeColumn.update({
|
|
85
|
+
where: { id: columnId },
|
|
86
|
+
data,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function deleteCustomGradeColumn(columnId, classId) {
|
|
90
|
+
const column = await findCustomGradeColumn(columnId, classId);
|
|
91
|
+
if (!column) {
|
|
92
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Column not found" });
|
|
93
|
+
}
|
|
94
|
+
return prisma.customGradeColumn.delete({ where: { id: columnId } });
|
|
95
|
+
}
|
|
96
|
+
export async function setCustomGradeEntry(classId, columnId, studentId, points, note) {
|
|
97
|
+
const column = await findCustomGradeColumn(columnId, classId);
|
|
98
|
+
if (!column) {
|
|
99
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Column not found" });
|
|
100
|
+
}
|
|
101
|
+
if (points === null) {
|
|
102
|
+
await prisma.customGradeEntry.deleteMany({
|
|
103
|
+
where: { columnId, studentId },
|
|
104
|
+
});
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return prisma.customGradeEntry.upsert({
|
|
108
|
+
where: { columnId_studentId: { columnId, studentId } },
|
|
109
|
+
create: { columnId, studentId, points, note: note ?? null },
|
|
110
|
+
update: { points, ...(note !== undefined ? { note } : {}) },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// ===== Policies =====
|
|
114
|
+
export async function getGradePolicies(classId) {
|
|
115
|
+
const policies = await findClassGradePolicies(classId);
|
|
116
|
+
if (!policies) {
|
|
117
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
118
|
+
}
|
|
119
|
+
return policies;
|
|
120
|
+
}
|
|
121
|
+
export async function updateGradePolicies(classId, data) {
|
|
122
|
+
return prisma.class.update({
|
|
123
|
+
where: { id: classId },
|
|
124
|
+
data,
|
|
125
|
+
select: {
|
|
126
|
+
id: true,
|
|
127
|
+
treatMissingAsZero: true,
|
|
128
|
+
gradeRoundingMode: true,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function getComputedGradebook(classId, userId) {
|
|
133
|
+
const [classData, categories, columns, isTeacher] = await Promise.all([
|
|
134
|
+
findClassForGradebook(classId),
|
|
135
|
+
findGradeCategoriesByClassId(classId),
|
|
136
|
+
findCustomGradeColumnsByClassId(classId),
|
|
137
|
+
isTeacherInClass(classId, userId),
|
|
138
|
+
]);
|
|
139
|
+
if (!classData) {
|
|
140
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
141
|
+
}
|
|
142
|
+
// Students only ever see their own row.
|
|
143
|
+
const students = isTeacher
|
|
144
|
+
? classData.students
|
|
145
|
+
: classData.students.filter((student) => student.id === userId);
|
|
146
|
+
const policies = {
|
|
147
|
+
treatMissingAsZero: classData.treatMissingAsZero,
|
|
148
|
+
roundingMode: classData.gradeRoundingMode,
|
|
149
|
+
};
|
|
150
|
+
const categoryInputs = categories.map((category) => ({
|
|
151
|
+
id: category.id,
|
|
152
|
+
name: category.name,
|
|
153
|
+
weight: category.weight,
|
|
154
|
+
dropLowest: category.dropLowest,
|
|
155
|
+
}));
|
|
156
|
+
const boundary = classData.gradingBoundaries[0] ?? null;
|
|
157
|
+
const rows = students.map((student) => {
|
|
158
|
+
const assignmentScores = {};
|
|
159
|
+
const scores = [];
|
|
160
|
+
for (const assignment of classData.assignments) {
|
|
161
|
+
const submission = assignment.submissions.find((item) => item.studentId === student.id);
|
|
162
|
+
const pointsEarned = submission?.gradeReceived ?? null;
|
|
163
|
+
assignmentScores[assignment.id] = pointsEarned;
|
|
164
|
+
scores.push({
|
|
165
|
+
categoryId: assignment.gradeCategoryId,
|
|
166
|
+
pointsEarned,
|
|
167
|
+
maxPoints: assignment.maxGrade ?? 0,
|
|
168
|
+
weight: assignment.weight,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const customScores = {};
|
|
172
|
+
for (const column of columns) {
|
|
173
|
+
const entry = column.entries.find((item) => item.studentId === student.id);
|
|
174
|
+
customScores[column.id] = {
|
|
175
|
+
points: entry?.points ?? null,
|
|
176
|
+
note: entry?.note ?? null,
|
|
177
|
+
};
|
|
178
|
+
scores.push({
|
|
179
|
+
categoryId: column.gradeCategoryId,
|
|
180
|
+
pointsEarned: entry?.points ?? null,
|
|
181
|
+
maxPoints: column.maxPoints,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const result = computeFinalGrade(policies, categoryInputs, scores);
|
|
185
|
+
const letterGrade = boundary && result.percentage !== null
|
|
186
|
+
? resolveLetterGrade(boundary.structured, result.percentage)
|
|
187
|
+
: null;
|
|
188
|
+
return {
|
|
189
|
+
studentId: student.id,
|
|
190
|
+
studentName: student.profile?.displayName || student.username,
|
|
191
|
+
assignmentScores,
|
|
192
|
+
customScores,
|
|
193
|
+
result,
|
|
194
|
+
letterGrade,
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
policies,
|
|
199
|
+
categories,
|
|
200
|
+
assignments: classData.assignments.map((assignment) => ({
|
|
201
|
+
id: assignment.id,
|
|
202
|
+
title: assignment.title,
|
|
203
|
+
maxGrade: assignment.maxGrade,
|
|
204
|
+
weight: assignment.weight,
|
|
205
|
+
gradeCategoryId: assignment.gradeCategoryId,
|
|
206
|
+
dueDate: assignment.dueDate,
|
|
207
|
+
})),
|
|
208
|
+
customColumns: columns.map((column) => ({
|
|
209
|
+
id: column.id,
|
|
210
|
+
name: column.name,
|
|
211
|
+
maxPoints: column.maxPoints,
|
|
212
|
+
gradeCategoryId: column.gradeCategoryId,
|
|
213
|
+
position: column.position,
|
|
214
|
+
})),
|
|
215
|
+
gradingBoundary: boundary,
|
|
216
|
+
students: rows,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// ===== CSV export =====
|
|
220
|
+
function escapeCsvCell(value) {
|
|
221
|
+
if (/[",\n\r]/.test(value)) {
|
|
222
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
export async function exportGradebookCsv(classId, userId) {
|
|
227
|
+
const gradebook = await getComputedGradebook(classId, userId);
|
|
228
|
+
const header = [
|
|
229
|
+
"Student",
|
|
230
|
+
...gradebook.assignments.map((assignment) => `${assignment.title} (/${assignment.maxGrade ?? 0})`),
|
|
231
|
+
...gradebook.customColumns.map((column) => `${column.name} (/${column.maxPoints})`),
|
|
232
|
+
...gradebook.categories.map((category) => `${category.name} Avg (%)`),
|
|
233
|
+
"Final (%)",
|
|
234
|
+
"Letter",
|
|
235
|
+
];
|
|
236
|
+
const lines = [header.map(escapeCsvCell).join(",")];
|
|
237
|
+
for (const row of gradebook.students) {
|
|
238
|
+
const categoryCells = gradebook.categories.map((category) => {
|
|
239
|
+
const average = row.result.categories.find((item) => item.categoryId === category.id)?.average;
|
|
240
|
+
return average === null || average === undefined
|
|
241
|
+
? ""
|
|
242
|
+
: average.toFixed(2);
|
|
243
|
+
});
|
|
244
|
+
const cells = [
|
|
245
|
+
row.studentName,
|
|
246
|
+
...gradebook.assignments.map((assignment) => {
|
|
247
|
+
const points = row.assignmentScores[assignment.id];
|
|
248
|
+
return points === null || points === undefined ? "" : String(points);
|
|
249
|
+
}),
|
|
250
|
+
...gradebook.customColumns.map((column) => {
|
|
251
|
+
const points = row.customScores[column.id]?.points;
|
|
252
|
+
return points === null || points === undefined ? "" : String(points);
|
|
253
|
+
}),
|
|
254
|
+
...categoryCells,
|
|
255
|
+
row.result.percentage === null ? "" : row.result.percentage.toFixed(2),
|
|
256
|
+
row.letterGrade ?? "",
|
|
257
|
+
];
|
|
258
|
+
lines.push(cells.map(escapeCsvCell).join(","));
|
|
259
|
+
}
|
|
260
|
+
return { csv: lines.join("\n") };
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=gradebook.js.map
|
|
263
|
+
//# debugId=2860d786-c6b1-5d9a-b1bd-880c5bef0f51
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradebook.js","sources":["services/gradebook.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Gradebook service – grade categories, custom columns/entries, class\n * grade-calculation policies, the computed gradebook, and CSV export.\n *\n * All grade math lives in `gradeCalculation.ts` (pure, unit-tested); this\n * service only assembles inputs and persists configuration.\n */\nimport { GradeRoundingMode as PrismaGradeRoundingMode } from \"@prisma/client\";\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { isTeacherInClass } from \"../models/class.js\";\nimport {\n findGradeCategoriesByClassId,\n findGradeCategory,\n findCustomGradeColumnsByClassId,\n findCustomGradeColumn,\n findClassForGradebook,\n findClassGradePolicies,\n} from \"../models/gradebook.js\";\nimport {\n computeFinalGrade,\n resolveLetterGrade,\n type FinalGradeResult,\n type GradeCategoryInput,\n type ScoreInput,\n} from \"./gradeCalculation.js\";\n\n// ===== Categories =====\n\nexport async function listGradeCategories(classId: string) {\n return findGradeCategoriesByClassId(classId);\n}\n\nexport async function createGradeCategory(\n classId: string,\n data: { name: string; weight: number; dropLowest?: number; position?: number }\n) {\n return prisma.gradeCategory.create({\n data: {\n classId,\n name: data.name,\n weight: data.weight,\n dropLowest: data.dropLowest ?? 0,\n position: data.position ?? (await findGradeCategoriesByClassId(classId)).length,\n },\n });\n}\n\nexport async function updateGradeCategory(\n categoryId: string,\n classId: string,\n data: {\n name?: string;\n weight?: number;\n dropLowest?: number;\n position?: number;\n }\n) {\n const category = await findGradeCategory(categoryId, classId);\n if (!category) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Category not found\" });\n }\n return prisma.gradeCategory.update({\n where: { id: categoryId },\n data,\n });\n}\n\nexport async function deleteGradeCategory(categoryId: string, classId: string) {\n const category = await findGradeCategory(categoryId, classId);\n if (!category) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Category not found\" });\n }\n return prisma.gradeCategory.delete({ where: { id: categoryId } });\n}\n\nexport async function assignAssignmentCategory(\n classId: string,\n assignmentId: string,\n gradeCategoryId: string | null\n) {\n if (gradeCategoryId) {\n const category = await findGradeCategory(gradeCategoryId, classId);\n if (!category) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Category not found\" });\n }\n }\n return prisma.assignment.update({\n where: { id: assignmentId, classId },\n data: { gradeCategoryId },\n });\n}\n\n// ===== Custom columns =====\n\nexport async function listCustomGradeColumns(classId: string) {\n return findCustomGradeColumnsByClassId(classId);\n}\n\nexport async function createCustomGradeColumn(\n classId: string,\n data: {\n name: string;\n maxPoints: number;\n gradeCategoryId?: string | null;\n position?: number;\n }\n) {\n if (data.gradeCategoryId) {\n const category = await findGradeCategory(data.gradeCategoryId, classId);\n if (!category) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Category not found\" });\n }\n }\n return prisma.customGradeColumn.create({\n data: {\n classId,\n name: data.name,\n maxPoints: data.maxPoints,\n gradeCategoryId: data.gradeCategoryId ?? null,\n position:\n data.position ?? (await findCustomGradeColumnsByClassId(classId)).length,\n },\n });\n}\n\nexport async function updateCustomGradeColumn(\n columnId: string,\n classId: string,\n data: {\n name?: string;\n maxPoints?: number;\n gradeCategoryId?: string | null;\n position?: number;\n }\n) {\n const column = await findCustomGradeColumn(columnId, classId);\n if (!column) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Column not found\" });\n }\n if (data.gradeCategoryId) {\n const category = await findGradeCategory(data.gradeCategoryId, classId);\n if (!category) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Category not found\" });\n }\n }\n return prisma.customGradeColumn.update({\n where: { id: columnId },\n data,\n });\n}\n\nexport async function deleteCustomGradeColumn(\n columnId: string,\n classId: string\n) {\n const column = await findCustomGradeColumn(columnId, classId);\n if (!column) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Column not found\" });\n }\n return prisma.customGradeColumn.delete({ where: { id: columnId } });\n}\n\nexport async function setCustomGradeEntry(\n classId: string,\n columnId: string,\n studentId: string,\n points: number | null,\n note?: string | null\n) {\n const column = await findCustomGradeColumn(columnId, classId);\n if (!column) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Column not found\" });\n }\n if (points === null) {\n await prisma.customGradeEntry.deleteMany({\n where: { columnId, studentId },\n });\n return null;\n }\n return prisma.customGradeEntry.upsert({\n where: { columnId_studentId: { columnId, studentId } },\n create: { columnId, studentId, points, note: note ?? null },\n update: { points, ...(note !== undefined ? { note } : {}) },\n });\n}\n\n// ===== Policies =====\n\nexport async function getGradePolicies(classId: string) {\n const policies = await findClassGradePolicies(classId);\n if (!policies) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Class not found\" });\n }\n return policies;\n}\n\nexport async function updateGradePolicies(\n classId: string,\n data: {\n treatMissingAsZero?: boolean;\n gradeRoundingMode?: PrismaGradeRoundingMode;\n }\n) {\n return prisma.class.update({\n where: { id: classId },\n data,\n select: {\n id: true,\n treatMissingAsZero: true,\n gradeRoundingMode: true,\n },\n });\n}\n\n// ===== Computed gradebook =====\n\nexport interface GradebookStudentRow {\n studentId: string;\n studentName: string;\n assignmentScores: Record<string, number | null>;\n customScores: Record<string, { points: number | null; note: string | null }>;\n result: FinalGradeResult;\n letterGrade: string | null;\n}\n\nexport async function getComputedGradebook(classId: string, userId: string) {\n const [classData, categories, columns, isTeacher] = await Promise.all([\n findClassForGradebook(classId),\n findGradeCategoriesByClassId(classId),\n findCustomGradeColumnsByClassId(classId),\n isTeacherInClass(classId, userId),\n ]);\n if (!classData) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Class not found\" });\n }\n\n // Students only ever see their own row.\n const students = isTeacher\n ? classData.students\n : classData.students.filter((student) => student.id === userId);\n\n const policies = {\n treatMissingAsZero: classData.treatMissingAsZero,\n roundingMode: classData.gradeRoundingMode,\n };\n const categoryInputs: GradeCategoryInput[] = categories.map((category) => ({\n id: category.id,\n name: category.name,\n weight: category.weight,\n dropLowest: category.dropLowest,\n }));\n const boundary = classData.gradingBoundaries[0] ?? null;\n\n const rows: GradebookStudentRow[] = students.map((student) => {\n const assignmentScores: Record<string, number | null> = {};\n const scores: ScoreInput[] = [];\n\n for (const assignment of classData.assignments) {\n const submission = assignment.submissions.find(\n (item) => item.studentId === student.id\n );\n const pointsEarned = submission?.gradeReceived ?? null;\n assignmentScores[assignment.id] = pointsEarned;\n scores.push({\n categoryId: assignment.gradeCategoryId,\n pointsEarned,\n maxPoints: assignment.maxGrade ?? 0,\n weight: assignment.weight,\n });\n }\n\n const customScores: GradebookStudentRow[\"customScores\"] = {};\n for (const column of columns) {\n const entry = column.entries.find(\n (item) => item.studentId === student.id\n );\n customScores[column.id] = {\n points: entry?.points ?? null,\n note: entry?.note ?? null,\n };\n scores.push({\n categoryId: column.gradeCategoryId,\n pointsEarned: entry?.points ?? null,\n maxPoints: column.maxPoints,\n });\n }\n\n const result = computeFinalGrade(policies, categoryInputs, scores);\n const letterGrade =\n boundary && result.percentage !== null\n ? resolveLetterGrade(boundary.structured, result.percentage)\n : null;\n\n return {\n studentId: student.id,\n studentName: student.profile?.displayName || student.username,\n assignmentScores,\n customScores,\n result,\n letterGrade,\n };\n });\n\n return {\n policies,\n categories,\n assignments: classData.assignments.map((assignment) => ({\n id: assignment.id,\n title: assignment.title,\n maxGrade: assignment.maxGrade,\n weight: assignment.weight,\n gradeCategoryId: assignment.gradeCategoryId,\n dueDate: assignment.dueDate,\n })),\n customColumns: columns.map((column) => ({\n id: column.id,\n name: column.name,\n maxPoints: column.maxPoints,\n gradeCategoryId: column.gradeCategoryId,\n position: column.position,\n })),\n gradingBoundary: boundary,\n students: rows,\n };\n}\n\n// ===== CSV export =====\n\nfunction escapeCsvCell(value: string): string {\n if (/[\",\\n\\r]/.test(value)) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n}\n\nexport async function exportGradebookCsv(classId: string, userId: string) {\n const gradebook = await getComputedGradebook(classId, userId);\n\n const header = [\n \"Student\",\n ...gradebook.assignments.map(\n (assignment) => `${assignment.title} (/${assignment.maxGrade ?? 0})`\n ),\n ...gradebook.customColumns.map(\n (column) => `${column.name} (/${column.maxPoints})`\n ),\n ...gradebook.categories.map((category) => `${category.name} Avg (%)`),\n \"Final (%)\",\n \"Letter\",\n ];\n\n const lines = [header.map(escapeCsvCell).join(\",\")];\n\n for (const row of gradebook.students) {\n const categoryCells = gradebook.categories.map((category) => {\n const average = row.result.categories.find(\n (item) => item.categoryId === category.id\n )?.average;\n return average === null || average === undefined\n ? \"\"\n : average.toFixed(2);\n });\n\n const cells = [\n row.studentName,\n ...gradebook.assignments.map((assignment) => {\n const points = row.assignmentScores[assignment.id];\n return points === null || points === undefined ? \"\" : String(points);\n }),\n ...gradebook.customColumns.map((column) => {\n const points = row.customScores[column.id]?.points;\n return points === null || points === undefined ? \"\" : String(points);\n }),\n ...categoryCells,\n row.result.percentage === null ? \"\" : row.result.percentage.toFixed(2),\n row.letterGrade ?? \"\",\n ];\n lines.push(cells.map(escapeCsvCell).join(\",\"));\n }\n\n return { csv: lines.join(\"\\n\") };\n}\n"],"names":[],"mappings":"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","debug_id":"2860d786-c6b1-5d9a-b1bd-880c5bef0f51"}
|
|
@@ -110,6 +110,23 @@ export declare function getAdminOverview(userId: string, schoolId: string): Prom
|
|
|
110
110
|
expiresAt: Date;
|
|
111
111
|
role: import(".prisma/client").$Enums.SchoolMembershipRole;
|
|
112
112
|
}[];
|
|
113
|
+
recentMembers: {
|
|
114
|
+
status: import(".prisma/client").$Enums.SchoolMembershipStatus;
|
|
115
|
+
user: {
|
|
116
|
+
id: string;
|
|
117
|
+
username: string;
|
|
118
|
+
email: string;
|
|
119
|
+
profile: {
|
|
120
|
+
profilePicture: string | null;
|
|
121
|
+
displayName: string | null;
|
|
122
|
+
profilePictureThumbnail: string | null;
|
|
123
|
+
} | null;
|
|
124
|
+
};
|
|
125
|
+
id: string;
|
|
126
|
+
createdAt: Date;
|
|
127
|
+
updatedAt: Date;
|
|
128
|
+
role: import(".prisma/client").$Enums.SchoolMembershipRole;
|
|
129
|
+
}[];
|
|
113
130
|
};
|
|
114
131
|
}>;
|
|
115
132
|
export declare function listClasses(userId: string, schoolId: string): Promise<{
|
|
@@ -144,6 +161,8 @@ export declare function listClasses(userId: string, schoolId: string): Promise<{
|
|
|
144
161
|
discussionsEnabled: boolean;
|
|
145
162
|
studentsCanCreateDiscussions: boolean;
|
|
146
163
|
studentsCanReply: boolean;
|
|
164
|
+
treatMissingAsZero: boolean;
|
|
165
|
+
gradeRoundingMode: import(".prisma/client").$Enums.GradeRoundingMode;
|
|
147
166
|
})[];
|
|
148
167
|
}>;
|
|
149
168
|
export declare function createSchoolClass(userId: string, input: {
|
|
@@ -191,6 +210,8 @@ export declare function createSchoolClass(userId: string, input: {
|
|
|
191
210
|
discussionsEnabled: boolean;
|
|
192
211
|
studentsCanCreateDiscussions: boolean;
|
|
193
212
|
studentsCanReply: boolean;
|
|
213
|
+
treatMissingAsZero: boolean;
|
|
214
|
+
gradeRoundingMode: import(".prisma/client").$Enums.GradeRoundingMode;
|
|
194
215
|
};
|
|
195
216
|
}>;
|
|
196
217
|
export declare function listMembers(userId: string, schoolId: string): Promise<{
|
|
@@ -242,6 +263,83 @@ export declare function updateMemberRole(userId: string, input: {
|
|
|
242
263
|
role: import(".prisma/client").$Enums.SchoolMembershipRole;
|
|
243
264
|
};
|
|
244
265
|
}>;
|
|
266
|
+
export declare function getMemberDetail(userId: string, input: {
|
|
267
|
+
schoolId: string;
|
|
268
|
+
memberUserId: string;
|
|
269
|
+
}): Promise<{
|
|
270
|
+
membership: {
|
|
271
|
+
status: import(".prisma/client").$Enums.SchoolMembershipStatus;
|
|
272
|
+
user: {
|
|
273
|
+
id: string;
|
|
274
|
+
username: string;
|
|
275
|
+
email: string;
|
|
276
|
+
profile: {
|
|
277
|
+
profilePicture: string | null;
|
|
278
|
+
displayName: string | null;
|
|
279
|
+
profilePictureThumbnail: string | null;
|
|
280
|
+
} | null;
|
|
281
|
+
};
|
|
282
|
+
id: string;
|
|
283
|
+
createdAt: Date;
|
|
284
|
+
updatedAt: Date;
|
|
285
|
+
role: import(".prisma/client").$Enums.SchoolMembershipRole;
|
|
286
|
+
};
|
|
287
|
+
teachingClasses: {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
section: string;
|
|
291
|
+
subject: string;
|
|
292
|
+
color: string | null;
|
|
293
|
+
teacherCount: number;
|
|
294
|
+
studentCount: number;
|
|
295
|
+
}[];
|
|
296
|
+
enrolledClasses: {
|
|
297
|
+
id: string;
|
|
298
|
+
name: string;
|
|
299
|
+
section: string;
|
|
300
|
+
subject: string;
|
|
301
|
+
color: string | null;
|
|
302
|
+
teacherCount: number;
|
|
303
|
+
studentCount: number;
|
|
304
|
+
}[];
|
|
305
|
+
}>;
|
|
306
|
+
export declare function bulkUpdateMemberRoles(userId: string, input: {
|
|
307
|
+
schoolId: string;
|
|
308
|
+
memberUserIds: string[];
|
|
309
|
+
role: SchoolMembershipRole;
|
|
310
|
+
}): Promise<{
|
|
311
|
+
updatedCount: number;
|
|
312
|
+
}>;
|
|
313
|
+
export declare function bulkRemoveMembers(userId: string, input: {
|
|
314
|
+
schoolId: string;
|
|
315
|
+
memberUserIds: string[];
|
|
316
|
+
}): Promise<{
|
|
317
|
+
removedCount: number;
|
|
318
|
+
}>;
|
|
319
|
+
export declare function deleteSchoolClass(userId: string, input: {
|
|
320
|
+
schoolId: string;
|
|
321
|
+
classId: string;
|
|
322
|
+
}): Promise<{
|
|
323
|
+
deletedClass: {
|
|
324
|
+
id: string;
|
|
325
|
+
};
|
|
326
|
+
}>;
|
|
327
|
+
export declare function updateClassMembers(userId: string, input: {
|
|
328
|
+
schoolId: string;
|
|
329
|
+
classId: string;
|
|
330
|
+
addTeacherIds?: string[];
|
|
331
|
+
removeTeacherIds?: string[];
|
|
332
|
+
addStudentIds?: string[];
|
|
333
|
+
removeStudentIds?: string[];
|
|
334
|
+
}): Promise<{
|
|
335
|
+
class: {
|
|
336
|
+
id: string;
|
|
337
|
+
_count: {
|
|
338
|
+
teachers: number;
|
|
339
|
+
students: number;
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
}>;
|
|
245
343
|
export declare function removeMember(userId: string, input: {
|
|
246
344
|
schoolId: string;
|
|
247
345
|
memberUserId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"school.d.ts","sourceRoot":"/","sources":["services/school.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,oBAAoB,EAAe,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"school.d.ts","sourceRoot":"/","sources":["services/school.ts"],"names":[],"mappings":"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"}
|
package/dist/services/school.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* School service - school admin workflow, memberships, and invites.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
5
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="70890b65-7dda-51a3-925f-31c75be34a33")}catch(e){}}();
|
|
6
6
|
import { SessionType } from "@prisma/client";
|
|
7
7
|
import { TRPCError } from "@trpc/server";
|
|
8
8
|
import { hash } from "bcryptjs";
|
|
@@ -14,7 +14,7 @@ import { generateUniqueUsername } from "../utils/generateUsername.js";
|
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
15
|
import { createSession, createUser, findUserByEmail, findUserById, findUserByUsername, } from "../models/auth.js";
|
|
16
16
|
import { upsertUserProfile } from "../models/user.js";
|
|
17
|
-
import { acceptSchoolInvite, countActiveAdmins, createSchoolInvite, createSchoolWithAdmin, findActiveSchoolAdminMembership, findPendingInviteByToken, findSchoolMembership, getSchoolOverview, listSchoolClasses, listSchoolInvites, listSchoolMembers, listUserSchoolMemberships, removeSchoolMembershipPreservingContent, revokeSchoolInvite, updateSchoolMembershipRole, updateSchoolRecord, upsertSchoolMembership, } from "../models/school.js";
|
|
17
|
+
import { acceptSchoolInvite, countActiveAdmins, createSchoolInvite, createSchoolWithAdmin, deleteSchoolClassRecord, findActiveSchoolAdminMembership, findPendingInviteByToken, findSchoolClass, findSchoolMembership, getSchoolMemberDetail, getSchoolOverview, listActiveSchoolMemberships, listMemberClasses, listRecentSchoolMembers, listSchoolClasses, listSchoolInvites, listSchoolMembers, listUserSchoolMemberships, removeSchoolMembershipPreservingContent, revokeSchoolInvite, updateSchoolClassMembers, updateSchoolMembershipRole, updateSchoolRecord, upsertSchoolMembership, } from "../models/school.js";
|
|
18
18
|
async function requireSchoolAdmin(schoolId, userId) {
|
|
19
19
|
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
20
20
|
if (!membership) {
|
|
@@ -83,7 +83,10 @@ export async function updateSchoolDetails(userId, input) {
|
|
|
83
83
|
}
|
|
84
84
|
export async function getAdminOverview(userId, schoolId) {
|
|
85
85
|
await requireSchoolAdmin(schoolId, userId);
|
|
86
|
-
const school = await
|
|
86
|
+
const [school, recentMembers] = await Promise.all([
|
|
87
|
+
getSchoolOverview(schoolId),
|
|
88
|
+
listRecentSchoolMembers(schoolId),
|
|
89
|
+
]);
|
|
87
90
|
if (!school) {
|
|
88
91
|
throw new TRPCError({ code: "NOT_FOUND", message: "School not found" });
|
|
89
92
|
}
|
|
@@ -104,6 +107,7 @@ export async function getAdminOverview(userId, schoolId) {
|
|
|
104
107
|
},
|
|
105
108
|
recentClasses: school.classes,
|
|
106
109
|
recentInvites: school.invites,
|
|
110
|
+
recentMembers,
|
|
107
111
|
},
|
|
108
112
|
};
|
|
109
113
|
}
|
|
@@ -176,6 +180,114 @@ export async function updateMemberRole(userId, input) {
|
|
|
176
180
|
const membership = await updateSchoolMembershipRole(input.schoolId, input.memberUserId, input.role);
|
|
177
181
|
return { membership };
|
|
178
182
|
}
|
|
183
|
+
export async function getMemberDetail(userId, input) {
|
|
184
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
185
|
+
const membership = await getSchoolMemberDetail(input.schoolId, input.memberUserId);
|
|
186
|
+
if (!membership) {
|
|
187
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
|
|
188
|
+
}
|
|
189
|
+
const [teachingClasses, enrolledClasses] = await listMemberClasses(input.schoolId, input.memberUserId);
|
|
190
|
+
const toClassSummary = (cls) => ({
|
|
191
|
+
id: cls.id,
|
|
192
|
+
name: cls.name,
|
|
193
|
+
section: cls.section,
|
|
194
|
+
subject: cls.subject,
|
|
195
|
+
color: cls.color,
|
|
196
|
+
teacherCount: cls._count.teachers,
|
|
197
|
+
studentCount: cls._count.students,
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
membership,
|
|
201
|
+
teachingClasses: teachingClasses.map(toClassSummary),
|
|
202
|
+
enrolledClasses: enrolledClasses.map(toClassSummary),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export async function bulkUpdateMemberRoles(userId, input) {
|
|
206
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
207
|
+
const memberships = await listActiveSchoolMemberships(input.schoolId, input.memberUserIds);
|
|
208
|
+
if (memberships.length === 0) {
|
|
209
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "No members found" });
|
|
210
|
+
}
|
|
211
|
+
if (input.role !== "ADMIN") {
|
|
212
|
+
const demotedAdmins = memberships.filter((m) => m.role === "ADMIN").length;
|
|
213
|
+
if (demotedAdmins > 0) {
|
|
214
|
+
const adminCount = await countActiveAdmins(input.schoolId);
|
|
215
|
+
if (adminCount - demotedAdmins < 1) {
|
|
216
|
+
throw new TRPCError({
|
|
217
|
+
code: "BAD_REQUEST",
|
|
218
|
+
message: "A school must have at least one active admin",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
await prisma.schoolMembership.updateMany({
|
|
224
|
+
where: {
|
|
225
|
+
schoolId: input.schoolId,
|
|
226
|
+
userId: { in: memberships.map((m) => m.userId) },
|
|
227
|
+
status: "ACTIVE",
|
|
228
|
+
},
|
|
229
|
+
data: { role: input.role },
|
|
230
|
+
});
|
|
231
|
+
return { updatedCount: memberships.length };
|
|
232
|
+
}
|
|
233
|
+
export async function bulkRemoveMembers(userId, input) {
|
|
234
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
235
|
+
const targetIds = input.memberUserIds.filter((id) => id !== userId);
|
|
236
|
+
const memberships = await listActiveSchoolMemberships(input.schoolId, targetIds);
|
|
237
|
+
if (memberships.length === 0) {
|
|
238
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "No members found" });
|
|
239
|
+
}
|
|
240
|
+
const removedAdmins = memberships.filter((m) => m.role === "ADMIN").length;
|
|
241
|
+
if (removedAdmins > 0) {
|
|
242
|
+
const adminCount = await countActiveAdmins(input.schoolId);
|
|
243
|
+
if (adminCount - removedAdmins < 1) {
|
|
244
|
+
throw new TRPCError({
|
|
245
|
+
code: "BAD_REQUEST",
|
|
246
|
+
message: "A school must have at least one active admin",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const membership of memberships) {
|
|
251
|
+
await removeSchoolMembershipPreservingContent(input.schoolId, membership.userId);
|
|
252
|
+
}
|
|
253
|
+
return { removedCount: memberships.length };
|
|
254
|
+
}
|
|
255
|
+
export async function deleteSchoolClass(userId, input) {
|
|
256
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
257
|
+
const cls = await findSchoolClass(input.schoolId, input.classId);
|
|
258
|
+
if (!cls) {
|
|
259
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
260
|
+
}
|
|
261
|
+
await deleteSchoolClassRecord(input.schoolId, input.classId);
|
|
262
|
+
return { deletedClass: { id: input.classId } };
|
|
263
|
+
}
|
|
264
|
+
export async function updateClassMembers(userId, input) {
|
|
265
|
+
await requireSchoolAdmin(input.schoolId, userId);
|
|
266
|
+
const cls = await findSchoolClass(input.schoolId, input.classId);
|
|
267
|
+
if (!cls) {
|
|
268
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
269
|
+
}
|
|
270
|
+
const addTeacherIds = input.addTeacherIds ?? [];
|
|
271
|
+
const addStudentIds = input.addStudentIds ?? [];
|
|
272
|
+
const addedIds = [...addTeacherIds, ...addStudentIds];
|
|
273
|
+
if (addedIds.length > 0) {
|
|
274
|
+
const memberships = await listActiveSchoolMemberships(input.schoolId, addedIds);
|
|
275
|
+
const activeIds = new Set(memberships.map((m) => m.userId));
|
|
276
|
+
if (addedIds.some((id) => !activeIds.has(id))) {
|
|
277
|
+
throw new TRPCError({
|
|
278
|
+
code: "BAD_REQUEST",
|
|
279
|
+
message: "All added users must be active members of this school",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const updated = await updateSchoolClassMembers(input.classId, {
|
|
284
|
+
addTeacherIds,
|
|
285
|
+
removeTeacherIds: input.removeTeacherIds ?? [],
|
|
286
|
+
addStudentIds,
|
|
287
|
+
removeStudentIds: input.removeStudentIds ?? [],
|
|
288
|
+
});
|
|
289
|
+
return { class: updated };
|
|
290
|
+
}
|
|
179
291
|
export async function removeMember(userId, input) {
|
|
180
292
|
await requireSchoolAdmin(input.schoolId, userId);
|
|
181
293
|
const current = await findSchoolMembership(input.schoolId, input.memberUserId);
|
|
@@ -753,4 +865,4 @@ export async function acceptInvite(userId, token) {
|
|
|
753
865
|
return { membership };
|
|
754
866
|
}
|
|
755
867
|
//# sourceMappingURL=school.js.map
|
|
756
|
-
//# debugId=
|
|
868
|
+
//# debugId=70890b65-7dda-51a3-925f-31c75be34a33
|