@studious-lms/server 1.22.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.
@@ -23,17 +23,24 @@ import {
23
23
  countActiveAdmins,
24
24
  createSchoolInvite,
25
25
  createSchoolWithAdmin,
26
+ deleteSchoolClassRecord,
26
27
  findActiveSchoolAdminMembership,
27
28
  findPendingInviteByToken,
29
+ findSchoolClass,
28
30
  findSchoolMembership,
29
31
  getSchoolById,
32
+ getSchoolMemberDetail,
30
33
  getSchoolOverview,
34
+ listActiveSchoolMemberships,
35
+ listMemberClasses,
36
+ listRecentSchoolMembers,
31
37
  listSchoolClasses,
32
38
  listSchoolInvites,
33
39
  listSchoolMembers,
34
40
  listUserSchoolMemberships,
35
41
  removeSchoolMembershipPreservingContent,
36
42
  revokeSchoolInvite,
43
+ updateSchoolClassMembers,
37
44
  updateSchoolMembershipRole,
38
45
  updateSchoolRecord,
39
46
  upsertSchoolMembership,
@@ -133,7 +140,10 @@ export async function updateSchoolDetails(
133
140
 
134
141
  export async function getAdminOverview(userId: string, schoolId: string) {
135
142
  await requireSchoolAdmin(schoolId, userId);
136
- const school = await getSchoolOverview(schoolId);
143
+ const [school, recentMembers] = await Promise.all([
144
+ getSchoolOverview(schoolId),
145
+ listRecentSchoolMembers(schoolId),
146
+ ]);
137
147
  if (!school) {
138
148
  throw new TRPCError({ code: "NOT_FOUND", message: "School not found" });
139
149
  }
@@ -159,6 +169,7 @@ export async function getAdminOverview(userId: string, schoolId: string) {
159
169
  },
160
170
  recentClasses: school.classes,
161
171
  recentInvites: school.invites,
172
+ recentMembers,
162
173
  },
163
174
  };
164
175
  }
@@ -265,6 +276,172 @@ export async function updateMemberRole(
265
276
  return { membership };
266
277
  }
267
278
 
279
+ export async function getMemberDetail(
280
+ userId: string,
281
+ input: { schoolId: string; memberUserId: string }
282
+ ) {
283
+ await requireSchoolAdmin(input.schoolId, userId);
284
+ const membership = await getSchoolMemberDetail(
285
+ input.schoolId,
286
+ input.memberUserId
287
+ );
288
+ if (!membership) {
289
+ throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
290
+ }
291
+
292
+ const [teachingClasses, enrolledClasses] = await listMemberClasses(
293
+ input.schoolId,
294
+ input.memberUserId
295
+ );
296
+
297
+ const toClassSummary = (cls: (typeof teachingClasses)[number]) => ({
298
+ id: cls.id,
299
+ name: cls.name,
300
+ section: cls.section,
301
+ subject: cls.subject,
302
+ color: cls.color,
303
+ teacherCount: cls._count.teachers,
304
+ studentCount: cls._count.students,
305
+ });
306
+
307
+ return {
308
+ membership,
309
+ teachingClasses: teachingClasses.map(toClassSummary),
310
+ enrolledClasses: enrolledClasses.map(toClassSummary),
311
+ };
312
+ }
313
+
314
+ export async function bulkUpdateMemberRoles(
315
+ userId: string,
316
+ input: {
317
+ schoolId: string;
318
+ memberUserIds: string[];
319
+ role: SchoolMembershipRole;
320
+ }
321
+ ) {
322
+ await requireSchoolAdmin(input.schoolId, userId);
323
+ const memberships = await listActiveSchoolMemberships(
324
+ input.schoolId,
325
+ input.memberUserIds
326
+ );
327
+ if (memberships.length === 0) {
328
+ throw new TRPCError({ code: "NOT_FOUND", message: "No members found" });
329
+ }
330
+
331
+ if (input.role !== "ADMIN") {
332
+ const demotedAdmins = memberships.filter((m) => m.role === "ADMIN").length;
333
+ if (demotedAdmins > 0) {
334
+ const adminCount = await countActiveAdmins(input.schoolId);
335
+ if (adminCount - demotedAdmins < 1) {
336
+ throw new TRPCError({
337
+ code: "BAD_REQUEST",
338
+ message: "A school must have at least one active admin",
339
+ });
340
+ }
341
+ }
342
+ }
343
+
344
+ await prisma.schoolMembership.updateMany({
345
+ where: {
346
+ schoolId: input.schoolId,
347
+ userId: { in: memberships.map((m) => m.userId) },
348
+ status: "ACTIVE",
349
+ },
350
+ data: { role: input.role },
351
+ });
352
+ return { updatedCount: memberships.length };
353
+ }
354
+
355
+ export async function bulkRemoveMembers(
356
+ userId: string,
357
+ input: { schoolId: string; memberUserIds: string[] }
358
+ ) {
359
+ await requireSchoolAdmin(input.schoolId, userId);
360
+ const targetIds = input.memberUserIds.filter((id) => id !== userId);
361
+ const memberships = await listActiveSchoolMemberships(
362
+ input.schoolId,
363
+ targetIds
364
+ );
365
+ if (memberships.length === 0) {
366
+ throw new TRPCError({ code: "NOT_FOUND", message: "No members found" });
367
+ }
368
+
369
+ const removedAdmins = memberships.filter((m) => m.role === "ADMIN").length;
370
+ if (removedAdmins > 0) {
371
+ const adminCount = await countActiveAdmins(input.schoolId);
372
+ if (adminCount - removedAdmins < 1) {
373
+ throw new TRPCError({
374
+ code: "BAD_REQUEST",
375
+ message: "A school must have at least one active admin",
376
+ });
377
+ }
378
+ }
379
+
380
+ for (const membership of memberships) {
381
+ await removeSchoolMembershipPreservingContent(
382
+ input.schoolId,
383
+ membership.userId
384
+ );
385
+ }
386
+ return { removedCount: memberships.length };
387
+ }
388
+
389
+ export async function deleteSchoolClass(
390
+ userId: string,
391
+ input: { schoolId: string; classId: string }
392
+ ) {
393
+ await requireSchoolAdmin(input.schoolId, userId);
394
+ const cls = await findSchoolClass(input.schoolId, input.classId);
395
+ if (!cls) {
396
+ throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
397
+ }
398
+ await deleteSchoolClassRecord(input.schoolId, input.classId);
399
+ return { deletedClass: { id: input.classId } };
400
+ }
401
+
402
+ export async function updateClassMembers(
403
+ userId: string,
404
+ input: {
405
+ schoolId: string;
406
+ classId: string;
407
+ addTeacherIds?: string[];
408
+ removeTeacherIds?: string[];
409
+ addStudentIds?: string[];
410
+ removeStudentIds?: string[];
411
+ }
412
+ ) {
413
+ await requireSchoolAdmin(input.schoolId, userId);
414
+ const cls = await findSchoolClass(input.schoolId, input.classId);
415
+ if (!cls) {
416
+ throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
417
+ }
418
+
419
+ const addTeacherIds = input.addTeacherIds ?? [];
420
+ const addStudentIds = input.addStudentIds ?? [];
421
+ const addedIds = [...addTeacherIds, ...addStudentIds];
422
+ if (addedIds.length > 0) {
423
+ const memberships = await listActiveSchoolMemberships(
424
+ input.schoolId,
425
+ addedIds
426
+ );
427
+ const activeIds = new Set(memberships.map((m) => m.userId));
428
+ if (addedIds.some((id) => !activeIds.has(id))) {
429
+ throw new TRPCError({
430
+ code: "BAD_REQUEST",
431
+ message: "All added users must be active members of this school",
432
+ });
433
+ }
434
+ }
435
+
436
+ const updated = await updateSchoolClassMembers(input.classId, {
437
+ addTeacherIds,
438
+ removeTeacherIds: input.removeTeacherIds ?? [],
439
+ addStudentIds,
440
+ removeStudentIds: input.removeStudentIds ?? [],
441
+ });
442
+ return { class: updated };
443
+ }
444
+
268
445
  export async function removeMember(
269
446
  userId: string,
270
447
  input: { schoolId: string; memberUserId: string }