@hed-hog/lms 0.0.305 → 0.0.306

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.
@@ -1,37 +1,37 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
- import {
3
- BadRequestException,
4
- ConflictException,
5
- Injectable,
6
- NotFoundException,
7
- } from '@nestjs/common';
8
- import { Prisma } from '@prisma/client';
9
- import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
10
- import { randomUUID } from 'node:crypto';
11
- import {
12
- CreateClassGroupDto,
13
- type ClassGroupSessionRecurrenceDto,
14
- type ClassGroupSessionTemplateDto,
15
- } from './dto/create-class-group.dto';
16
- import { CreateSessionDto, type SessionRecurrenceDto } from './dto/create-session.dto';
17
- import { UpdateClassGroupDto } from './dto/update-class-group.dto';
18
- import { UpdateSessionDto } from './dto/update-session.dto';
2
+ import {
3
+ BadRequestException,
4
+ ConflictException,
5
+ Injectable,
6
+ NotFoundException,
7
+ } from '@nestjs/common';
8
+ import { Prisma } from '@prisma/client';
9
+ import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
10
+ import { randomUUID } from 'node:crypto';
11
+ import {
12
+ CreateClassGroupDto,
13
+ type ClassGroupSessionRecurrenceDto,
14
+ type ClassGroupSessionTemplateDto,
15
+ } from './dto/create-class-group.dto';
16
+ import { CreateSessionDto, type SessionRecurrenceDto } from './dto/create-session.dto';
17
+ import { UpdateClassGroupDto } from './dto/update-class-group.dto';
18
+ import { UpdateSessionDto } from './dto/update-session.dto';
19
19
 
20
20
  type SessionScope = 'single' | 'series';
21
21
  type SessionRecurrenceDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
22
22
 
23
- type NormalizedSessionRecurrence = {
24
- frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
25
- interval: number;
26
- until: string;
27
- daysOfWeek?: SessionRecurrenceDay[];
28
- };
29
-
30
- type ClassSessionTemplateSummary = {
31
- title: string;
32
- isRecurring: boolean;
33
- recurrence?: NormalizedSessionRecurrence | null;
34
- };
23
+ type NormalizedSessionRecurrence = {
24
+ frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
25
+ interval: number;
26
+ until: string;
27
+ daysOfWeek?: SessionRecurrenceDay[];
28
+ };
29
+
30
+ type ClassSessionTemplateSummary = {
31
+ title: string;
32
+ isRecurring: boolean;
33
+ recurrence?: NormalizedSessionRecurrence | null;
34
+ };
35
35
 
36
36
  @Injectable()
37
37
  export class ClassGroupService {
@@ -133,7 +133,7 @@ export class ClassGroupService {
133
133
  const pageSize = Math.max(Number(params.pageSize) || 12, 1);
134
134
  const skip = (page - 1) * pageSize;
135
135
 
136
- const where: any = {};
136
+ const where: any = {};
137
137
 
138
138
  const status = this.normalizeStatus(params.status);
139
139
  if (status) {
@@ -230,33 +230,33 @@ export class ClassGroupService {
230
230
  };
231
231
  }
232
232
 
233
- async getById(id: number) {
234
- const item = await this.prisma.course_class_group.findUnique({
235
- where: { id },
236
- include: this.getClassGroupInclude(),
237
- });
238
-
239
- if (!item) {
240
- return null;
241
- }
242
-
243
- const sessionTemplateSummary = await this.getClassSessionTemplateSummary(id);
244
-
245
- return {
246
- ...this.mapClassGroup(item),
247
- sessionTitle: sessionTemplateSummary?.title ?? null,
248
- sessionRecurrenceSummary: sessionTemplateSummary
249
- ? {
250
- ...(sessionTemplateSummary.recurrence ?? {}),
251
- isRecurring: sessionTemplateSummary.isRecurring,
252
- }
253
- : null,
254
- };
255
- }
256
-
257
- async create(dto: CreateClassGroupDto) {
258
- await this.assertCourseExists(dto.courseId);
259
- if (dto.instructorId) {
233
+ async getById(id: number) {
234
+ const item = await this.prisma.course_class_group.findUnique({
235
+ where: { id },
236
+ include: this.getClassGroupInclude(),
237
+ });
238
+
239
+ if (!item) {
240
+ return null;
241
+ }
242
+
243
+ const sessionTemplateSummary = await this.getClassSessionTemplateSummary(id);
244
+
245
+ return {
246
+ ...this.mapClassGroup(item),
247
+ sessionTitle: sessionTemplateSummary?.title ?? null,
248
+ sessionRecurrenceSummary: sessionTemplateSummary
249
+ ? {
250
+ ...(sessionTemplateSummary.recurrence ?? {}),
251
+ isRecurring: sessionTemplateSummary.isRecurring,
252
+ }
253
+ : null,
254
+ };
255
+ }
256
+
257
+ async create(dto: CreateClassGroupDto) {
258
+ await this.assertCourseExists(dto.courseId);
259
+ if (dto.instructorId) {
260
260
  await this.assertInstructorExists(dto.instructorId);
261
261
  }
262
262
  this.assertDatesAndTimes({
@@ -264,68 +264,68 @@ export class ClassGroupService {
264
264
  endDate: dto.endDate,
265
265
  startTime: dto.startTime,
266
266
  endTime: dto.endTime,
267
- });
268
-
269
- try {
270
- const created = await this.prisma.$transaction(async (tx) => {
271
- const createdClass = await tx.course_class_group.create({
272
- data: {
273
- code: dto.code,
274
- course_id: dto.courseId,
275
- instructor_id: dto.instructorId,
276
- title: dto.title,
277
- description: dto.description,
278
- delivery_mode: dto.deliveryMode,
279
- status: dto.status ?? 'open',
280
- start_date: new Date(dto.startDate),
281
- end_date: dto.endDate ? new Date(dto.endDate) : null,
282
- start_time: dto.startTime,
283
- end_time: dto.endTime,
284
- week_days: dto.weekDays,
285
- capacity: dto.capacity ?? 30,
286
- location: dto.location,
287
- virtual_room_url: dto.virtualRoomUrl,
288
- },
289
- include: this.getClassGroupInclude(),
290
- });
291
-
292
- if (dto.sessionTemplate) {
293
- await this.syncClassSessionsFromTemplate(
294
- tx,
295
- createdClass.id,
296
- createdClass,
297
- dto.sessionTemplate,
298
- );
299
- }
300
-
301
- return createdClass;
302
- });
303
-
304
- return this.mapClassGroup(created);
305
- } catch (error) {
306
- this.handlePrismaError(error);
267
+ });
268
+
269
+ try {
270
+ const created = await this.prisma.$transaction(async (tx) => {
271
+ const createdClass = await tx.course_class_group.create({
272
+ data: {
273
+ code: dto.code,
274
+ course_id: dto.courseId,
275
+ instructor_id: dto.instructorId,
276
+ title: dto.title,
277
+ description: dto.description,
278
+ delivery_mode: dto.deliveryMode,
279
+ status: dto.status ?? 'open',
280
+ start_date: new Date(dto.startDate),
281
+ end_date: dto.endDate ? new Date(dto.endDate) : null,
282
+ start_time: dto.startTime,
283
+ end_time: dto.endTime,
284
+ week_days: dto.weekDays,
285
+ capacity: dto.capacity ?? 30,
286
+ location: dto.location,
287
+ virtual_room_url: dto.virtualRoomUrl,
288
+ },
289
+ include: this.getClassGroupInclude(),
290
+ });
291
+
292
+ if (dto.sessionTemplate) {
293
+ await this.syncClassSessionsFromTemplate(
294
+ tx,
295
+ createdClass.id,
296
+ createdClass,
297
+ dto.sessionTemplate,
298
+ );
299
+ }
300
+
301
+ return createdClass;
302
+ });
303
+
304
+ return this.mapClassGroup(created);
305
+ } catch (error) {
306
+ this.handlePrismaError(error);
307
307
  }
308
308
  }
309
309
 
310
310
  async update(id: number, dto: UpdateClassGroupDto) {
311
311
  const existing = await this.prisma.course_class_group.findUnique({
312
312
  where: { id },
313
- select: {
314
- id: true,
315
- course_id: true,
316
- code: true,
317
- title: true,
318
- description: true,
319
- delivery_mode: true,
320
- instructor_id: true,
321
- start_date: true,
322
- end_date: true,
323
- start_time: true,
324
- end_time: true,
325
- location: true,
326
- virtual_room_url: true,
327
- },
328
- });
313
+ select: {
314
+ id: true,
315
+ course_id: true,
316
+ code: true,
317
+ title: true,
318
+ description: true,
319
+ delivery_mode: true,
320
+ instructor_id: true,
321
+ start_date: true,
322
+ end_date: true,
323
+ start_time: true,
324
+ end_time: true,
325
+ location: true,
326
+ virtual_room_url: true,
327
+ },
328
+ });
329
329
 
330
330
  if (!existing) {
331
331
  throw new NotFoundException('Class not found');
@@ -351,198 +351,198 @@ export class ClassGroupService {
351
351
 
352
352
  const status =
353
353
  dto.status !== undefined ? this.normalizeStatus(dto.status) : undefined;
354
- const deliveryMode =
355
- dto.deliveryMode !== undefined
356
- ? this.normalizeDeliveryMode(dto.deliveryMode)
357
- : undefined;
358
-
359
- try {
360
- const updated = await this.prisma.$transaction(async (tx) => {
361
- const updatedClass = await tx.course_class_group.update({
362
- where: { id },
363
- data: {
364
- ...(dto.code !== undefined && { code: dto.code }),
365
- ...(dto.courseId !== undefined && { course_id: dto.courseId }),
366
- ...(dto.instructorId !== undefined && {
367
- instructor_id: dto.instructorId,
368
- }),
369
- ...(dto.title !== undefined && { title: dto.title }),
370
- ...(dto.description !== undefined && { description: dto.description }),
371
- ...(deliveryMode !== undefined && { delivery_mode: deliveryMode }),
372
- ...(status !== undefined && { status }),
373
- ...(dto.startDate !== undefined && { start_date: new Date(dto.startDate) }),
374
- ...(dto.endDate !== undefined && {
375
- end_date: dto.endDate ? new Date(dto.endDate) : null,
376
- }),
377
- ...(dto.startTime !== undefined && { start_time: dto.startTime }),
378
- ...(dto.endTime !== undefined && { end_time: dto.endTime }),
379
- ...(dto.weekDays !== undefined && { week_days: dto.weekDays }),
380
- ...(dto.capacity !== undefined && { capacity: dto.capacity }),
381
- ...(dto.location !== undefined && { location: dto.location }),
382
- ...(dto.virtualRoomUrl !== undefined && {
383
- virtual_room_url: dto.virtualRoomUrl,
384
- }),
385
- },
386
- include: this.getClassGroupInclude(),
387
- });
388
-
389
- if (dto.sessionTemplate) {
390
- await this.syncClassSessionsFromTemplate(
391
- tx,
392
- id,
393
- updatedClass,
394
- dto.sessionTemplate,
395
- { futureOnly: true },
396
- );
397
- }
398
-
399
- return updatedClass;
400
- });
401
-
402
- return this.mapClassGroup(updated);
403
- } catch (error) {
404
- this.handlePrismaError(error);
354
+ const deliveryMode =
355
+ dto.deliveryMode !== undefined
356
+ ? this.normalizeDeliveryMode(dto.deliveryMode)
357
+ : undefined;
358
+
359
+ try {
360
+ const updated = await this.prisma.$transaction(async (tx) => {
361
+ const updatedClass = await tx.course_class_group.update({
362
+ where: { id },
363
+ data: {
364
+ ...(dto.code !== undefined && { code: dto.code }),
365
+ ...(dto.courseId !== undefined && { course_id: dto.courseId }),
366
+ ...(dto.instructorId !== undefined && {
367
+ instructor_id: dto.instructorId,
368
+ }),
369
+ ...(dto.title !== undefined && { title: dto.title }),
370
+ ...(dto.description !== undefined && { description: dto.description }),
371
+ ...(deliveryMode !== undefined && { delivery_mode: deliveryMode }),
372
+ ...(status !== undefined && { status }),
373
+ ...(dto.startDate !== undefined && { start_date: new Date(dto.startDate) }),
374
+ ...(dto.endDate !== undefined && {
375
+ end_date: dto.endDate ? new Date(dto.endDate) : null,
376
+ }),
377
+ ...(dto.startTime !== undefined && { start_time: dto.startTime }),
378
+ ...(dto.endTime !== undefined && { end_time: dto.endTime }),
379
+ ...(dto.weekDays !== undefined && { week_days: dto.weekDays }),
380
+ ...(dto.capacity !== undefined && { capacity: dto.capacity }),
381
+ ...(dto.location !== undefined && { location: dto.location }),
382
+ ...(dto.virtualRoomUrl !== undefined && {
383
+ virtual_room_url: dto.virtualRoomUrl,
384
+ }),
385
+ },
386
+ include: this.getClassGroupInclude(),
387
+ });
388
+
389
+ if (dto.sessionTemplate) {
390
+ await this.syncClassSessionsFromTemplate(
391
+ tx,
392
+ id,
393
+ updatedClass,
394
+ dto.sessionTemplate,
395
+ { futureOnly: true },
396
+ );
397
+ }
398
+
399
+ return updatedClass;
400
+ });
401
+
402
+ return this.mapClassGroup(updated);
403
+ } catch (error) {
404
+ this.handlePrismaError(error);
405
+ }
406
+ }
407
+
408
+ async remove(id: number) {
409
+ await this.prisma.course_class_group.delete({ where: { id } });
410
+ return { success: true };
411
+ }
412
+
413
+ private async getClassSessionTemplateSummary(
414
+ classGroupId: number,
415
+ ): Promise<ClassSessionTemplateSummary | null> {
416
+ const today = this.getSessionMutationFloorDate();
417
+ const session =
418
+ (await this.prisma.course_class_session.findFirst({
419
+ where: {
420
+ course_class_group_id: classGroupId,
421
+ session_date: { gte: today },
422
+ status: { notIn: ['completed', 'cancelled'] },
423
+ },
424
+ orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
425
+ select: {
426
+ title: true,
427
+ recurrence_rule: true,
428
+ recurrence_id: true,
429
+ },
430
+ })) ??
431
+ (await this.prisma.course_class_session.findFirst({
432
+ where: { course_class_group_id: classGroupId },
433
+ orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
434
+ select: {
435
+ title: true,
436
+ recurrence_rule: true,
437
+ recurrence_id: true,
438
+ },
439
+ }));
440
+
441
+ if (!session) {
442
+ return null;
443
+ }
444
+
445
+ return {
446
+ title: session.title,
447
+ recurrence: this.parseRecurrenceRule(session.recurrence_rule),
448
+ isRecurring: Boolean(session.recurrence_id),
449
+ };
450
+ }
451
+
452
+ private getSessionMutationFloorDate() {
453
+ return this.normalizeSessionDate(new Date());
454
+ }
455
+
456
+ private buildClassSessionDto(
457
+ classGroup: any,
458
+ sessionTemplate: ClassGroupSessionTemplateDto,
459
+ ): CreateSessionDto {
460
+ if (!classGroup.start_date) {
461
+ throw new BadRequestException('Class start date is required to generate sessions');
462
+ }
463
+
464
+ if (!classGroup.start_time || !classGroup.end_time) {
465
+ throw new BadRequestException('Class start and end times are required to generate sessions');
405
466
  }
467
+
468
+ const deliveryMode = classGroup.delivery_mode;
469
+ const isOnline = deliveryMode === 'online';
470
+ const isHybrid = deliveryMode === 'hybrid';
471
+
472
+ return {
473
+ title: sessionTemplate.title.trim(),
474
+ description: sessionTemplate.description ?? classGroup.description ?? undefined,
475
+ sessionDate: this.toDateKey(this.normalizeSessionDate(classGroup.start_date)),
476
+ startTime: classGroup.start_time,
477
+ endTime: classGroup.end_time,
478
+ location:
479
+ isOnline && !isHybrid
480
+ ? undefined
481
+ : sessionTemplate.location ?? classGroup.location ?? undefined,
482
+ meetingUrl:
483
+ !isOnline && !isHybrid
484
+ ? undefined
485
+ : sessionTemplate.meetingUrl ?? classGroup.virtual_room_url ?? undefined,
486
+ color: sessionTemplate.color ?? undefined,
487
+ status: 'scheduled',
488
+ instructorId: classGroup.instructor_id ?? undefined,
489
+ recurrence: sessionTemplate.recurrence
490
+ ? this.toSessionRecurrenceDto(sessionTemplate.recurrence)
491
+ : undefined,
492
+ };
493
+ }
494
+
495
+ private toSessionRecurrenceDto(
496
+ recurrence: ClassGroupSessionRecurrenceDto,
497
+ ): SessionRecurrenceDto {
498
+ return {
499
+ frequency: recurrence.frequency,
500
+ interval: recurrence.interval,
501
+ until: recurrence.until,
502
+ daysOfWeek: recurrence.daysOfWeek,
503
+ };
406
504
  }
407
505
 
408
- async remove(id: number) {
409
- await this.prisma.course_class_group.delete({ where: { id } });
410
- return { success: true };
411
- }
412
-
413
- private async getClassSessionTemplateSummary(
414
- classGroupId: number,
415
- ): Promise<ClassSessionTemplateSummary | null> {
416
- const today = this.getSessionMutationFloorDate();
417
- const session =
418
- (await this.prisma.course_class_session.findFirst({
419
- where: {
420
- course_class_group_id: classGroupId,
421
- session_date: { gte: today },
422
- status: { notIn: ['completed', 'cancelled'] },
423
- },
424
- orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
425
- select: {
426
- title: true,
427
- recurrence_rule: true,
428
- recurrence_id: true,
429
- },
430
- })) ??
431
- (await this.prisma.course_class_session.findFirst({
432
- where: { course_class_group_id: classGroupId },
433
- orderBy: [{ session_date: 'asc' }, { occurrence_index: 'asc' }],
434
- select: {
435
- title: true,
436
- recurrence_rule: true,
437
- recurrence_id: true,
438
- },
439
- }));
440
-
441
- if (!session) {
442
- return null;
443
- }
444
-
445
- return {
446
- title: session.title,
447
- recurrence: this.parseRecurrenceRule(session.recurrence_rule),
448
- isRecurring: Boolean(session.recurrence_id),
449
- };
450
- }
451
-
452
- private getSessionMutationFloorDate() {
453
- return this.normalizeSessionDate(new Date());
454
- }
455
-
456
- private buildClassSessionDto(
457
- classGroup: any,
458
- sessionTemplate: ClassGroupSessionTemplateDto,
459
- ): CreateSessionDto {
460
- if (!classGroup.start_date) {
461
- throw new BadRequestException('Class start date is required to generate sessions');
462
- }
463
-
464
- if (!classGroup.start_time || !classGroup.end_time) {
465
- throw new BadRequestException('Class start and end times are required to generate sessions');
466
- }
467
-
468
- const deliveryMode = classGroup.delivery_mode;
469
- const isOnline = deliveryMode === 'online';
470
- const isHybrid = deliveryMode === 'hybrid';
471
-
472
- return {
473
- title: sessionTemplate.title.trim(),
474
- description: sessionTemplate.description ?? classGroup.description ?? undefined,
475
- sessionDate: this.toDateKey(this.normalizeSessionDate(classGroup.start_date)),
476
- startTime: classGroup.start_time,
477
- endTime: classGroup.end_time,
478
- location:
479
- isOnline && !isHybrid
480
- ? undefined
481
- : sessionTemplate.location ?? classGroup.location ?? undefined,
482
- meetingUrl:
483
- !isOnline && !isHybrid
484
- ? undefined
485
- : sessionTemplate.meetingUrl ?? classGroup.virtual_room_url ?? undefined,
486
- color: sessionTemplate.color ?? undefined,
487
- status: 'scheduled',
488
- instructorId: classGroup.instructor_id ?? undefined,
489
- recurrence: sessionTemplate.recurrence
490
- ? this.toSessionRecurrenceDto(sessionTemplate.recurrence)
491
- : undefined,
492
- };
493
- }
494
-
495
- private toSessionRecurrenceDto(
496
- recurrence: ClassGroupSessionRecurrenceDto,
497
- ): SessionRecurrenceDto {
498
- return {
499
- frequency: recurrence.frequency,
500
- interval: recurrence.interval,
501
- until: recurrence.until,
502
- daysOfWeek: recurrence.daysOfWeek,
503
- };
504
- }
505
-
506
- private async syncClassSessionsFromTemplate(
507
- tx: Prisma.TransactionClient,
508
- classGroupId: number,
509
- classGroup: any,
510
- sessionTemplate: ClassGroupSessionTemplateDto,
511
- options?: { futureOnly?: boolean },
512
- ) {
513
- const createDto = this.buildClassSessionDto(classGroup, sessionTemplate);
514
- const mutableWhere: any = {
515
- course_class_group_id: classGroupId,
516
- };
517
-
518
- if (options?.futureOnly) {
519
- mutableWhere.session_date = { gte: this.getSessionMutationFloorDate() };
520
- mutableWhere.status = { notIn: ['completed', 'cancelled'] };
521
- }
522
-
523
- const mutableSessions = await tx.course_class_session.findMany({
524
- where: mutableWhere,
525
- select: { id: true },
526
- });
527
-
528
- if (mutableSessions.length > 0) {
529
- const sessionIds = mutableSessions.map((session) => session.id);
530
-
531
- await tx.course_class_attendance.deleteMany({
532
- where: { course_class_session_id: { in: sessionIds } },
533
- });
534
- await tx.course_class_session_instructor.deleteMany({
535
- where: { course_class_session_id: { in: sessionIds } },
536
- });
537
- await tx.course_class_session.deleteMany({
538
- where: { id: { in: sessionIds } },
539
- });
540
- }
541
-
542
- await this.createSessionSeriesInTransaction(tx, classGroupId, createDto, {
543
- minDate: options?.futureOnly ? this.getSessionMutationFloorDate() : undefined,
544
- });
545
- }
506
+ private async syncClassSessionsFromTemplate(
507
+ tx: Prisma.TransactionClient,
508
+ classGroupId: number,
509
+ classGroup: any,
510
+ sessionTemplate: ClassGroupSessionTemplateDto,
511
+ options?: { futureOnly?: boolean },
512
+ ) {
513
+ const createDto = this.buildClassSessionDto(classGroup, sessionTemplate);
514
+ const mutableWhere: any = {
515
+ course_class_group_id: classGroupId,
516
+ };
517
+
518
+ if (options?.futureOnly) {
519
+ mutableWhere.session_date = { gte: this.getSessionMutationFloorDate() };
520
+ mutableWhere.status = { notIn: ['completed', 'cancelled'] };
521
+ }
522
+
523
+ const mutableSessions = await tx.course_class_session.findMany({
524
+ where: mutableWhere,
525
+ select: { id: true },
526
+ });
527
+
528
+ if (mutableSessions.length > 0) {
529
+ const sessionIds = mutableSessions.map((session) => session.id);
530
+
531
+ await tx.course_class_attendance.deleteMany({
532
+ where: { course_class_session_id: { in: sessionIds } },
533
+ });
534
+ await tx.course_class_session_instructor.deleteMany({
535
+ where: { course_class_session_id: { in: sessionIds } },
536
+ });
537
+ await tx.course_class_session.deleteMany({
538
+ where: { id: { in: sessionIds } },
539
+ });
540
+ }
541
+
542
+ await this.createSessionSeriesInTransaction(tx, classGroupId, createDto, {
543
+ minDate: options?.futureOnly ? this.getSessionMutationFloorDate() : undefined,
544
+ });
545
+ }
546
546
 
547
547
  private mapClassGroup(item: any) {
548
548
  const instructorName =
@@ -1148,117 +1148,117 @@ export class ClassGroupService {
1148
1148
  return sessions.map((s) => this.mapSession(s));
1149
1149
  }
1150
1150
 
1151
- async createSession(classGroupId: number, dto: CreateSessionDto) {
1152
- const group = await this.prisma.course_class_group.findUnique({
1153
- where: { id: classGroupId },
1154
- select: { id: true },
1155
- });
1151
+ async createSession(classGroupId: number, dto: CreateSessionDto) {
1152
+ const group = await this.prisma.course_class_group.findUnique({
1153
+ where: { id: classGroupId },
1154
+ select: { id: true },
1155
+ });
1156
1156
  if (!group) throw new NotFoundException('Class not found');
1157
1157
 
1158
- if (dto.instructorId) {
1159
- await this.assertInstructorExists(dto.instructorId);
1160
- }
1161
-
1162
- const created = await this.prisma.$transaction((tx) =>
1163
- this.createSessionSeriesInTransaction(tx, classGroupId, dto),
1164
- );
1165
-
1166
- if (!created) {
1167
- throw new BadRequestException('No sessions were generated for the provided recurrence');
1168
- }
1169
-
1170
- return this.mapSession(created);
1171
- }
1172
-
1173
- private async createSessionSeriesInTransaction(
1174
- tx: Prisma.TransactionClient,
1175
- classGroupId: number,
1176
- dto: CreateSessionDto,
1177
- options?: { minDate?: Date },
1178
- ) {
1179
- const normalizedRecurrence = dto.recurrence
1180
- ? this.normalizeSessionRecurrence(dto.recurrence, dto.sessionDate)
1181
- : null;
1182
- const fullOccurrenceDates = normalizedRecurrence
1183
- ? this.buildRecurringSessionDates(dto.sessionDate, normalizedRecurrence)
1184
- : [this.toSessionDate(dto.sessionDate)];
1185
- const occurrenceDates = options?.minDate
1186
- ? fullOccurrenceDates.filter(
1187
- (date) =>
1188
- this.normalizeSessionDate(date).getTime() >=
1189
- this.normalizeSessionDate(options.minDate!).getTime(),
1190
- )
1191
- : fullOccurrenceDates;
1192
-
1193
- if (occurrenceDates.length === 0) {
1194
- return null;
1195
- }
1196
-
1197
- const recurrenceId = normalizedRecurrence ? randomUUID() : null;
1198
- const recurrenceRule = normalizedRecurrence
1199
- ? JSON.stringify(normalizedRecurrence)
1200
- : null;
1201
-
1202
- const first = await tx.course_class_session.create({
1203
- data: {
1204
- course_class_group_id: classGroupId,
1205
- title: dto.title,
1206
- description: dto.description ?? null,
1207
- session_date: occurrenceDates[0],
1208
- start_time: dto.startTime,
1209
- end_time: dto.endTime,
1210
- location: dto.location ?? null,
1211
- meeting_url: dto.meetingUrl ?? null,
1212
- color: dto.color ?? null,
1213
- status: dto.status ?? 'scheduled',
1214
- recurrence_id: recurrenceId,
1215
- recurrence_rule: recurrenceRule,
1216
- occurrence_index: normalizedRecurrence ? 0 : null,
1217
- is_exception: false,
1218
- ...(dto.instructorId && {
1219
- course_class_session_instructor: {
1220
- create: {
1221
- instructor_id: dto.instructorId,
1222
- role: 'lead',
1223
- },
1224
- },
1225
- }),
1226
- },
1227
- include: this.getSessionInclude(),
1228
- });
1229
-
1230
- for (let index = 1; index < occurrenceDates.length; index += 1) {
1231
- await tx.course_class_session.create({
1232
- data: {
1233
- course_class_group_id: classGroupId,
1234
- title: dto.title,
1235
- description: dto.description ?? null,
1236
- session_date: occurrenceDates[index],
1237
- start_time: dto.startTime,
1238
- end_time: dto.endTime,
1239
- location: dto.location ?? null,
1240
- meeting_url: dto.meetingUrl ?? null,
1241
- color: dto.color ?? null,
1242
- status: dto.status ?? 'scheduled',
1243
- recurrence_id: recurrenceId,
1244
- recurrence_rule: recurrenceRule,
1245
- occurrence_index: normalizedRecurrence ? index : null,
1246
- is_exception: false,
1247
- parent_session_id: first.id,
1248
- ...(dto.instructorId && {
1249
- course_class_session_instructor: {
1250
- create: {
1251
- instructor_id: dto.instructorId,
1252
- role: 'lead',
1253
- },
1254
- },
1255
- }),
1256
- },
1257
- });
1258
- }
1259
-
1260
- return first;
1261
- }
1158
+ if (dto.instructorId) {
1159
+ await this.assertInstructorExists(dto.instructorId);
1160
+ }
1161
+
1162
+ const created = await this.prisma.$transaction((tx) =>
1163
+ this.createSessionSeriesInTransaction(tx, classGroupId, dto),
1164
+ );
1165
+
1166
+ if (!created) {
1167
+ throw new BadRequestException('No sessions were generated for the provided recurrence');
1168
+ }
1169
+
1170
+ return this.mapSession(created);
1171
+ }
1172
+
1173
+ private async createSessionSeriesInTransaction(
1174
+ tx: Prisma.TransactionClient,
1175
+ classGroupId: number,
1176
+ dto: CreateSessionDto,
1177
+ options?: { minDate?: Date },
1178
+ ) {
1179
+ const normalizedRecurrence = dto.recurrence
1180
+ ? this.normalizeSessionRecurrence(dto.recurrence, dto.sessionDate)
1181
+ : null;
1182
+ const fullOccurrenceDates = normalizedRecurrence
1183
+ ? this.buildRecurringSessionDates(dto.sessionDate, normalizedRecurrence)
1184
+ : [this.toSessionDate(dto.sessionDate)];
1185
+ const occurrenceDates = options?.minDate
1186
+ ? fullOccurrenceDates.filter(
1187
+ (date) =>
1188
+ this.normalizeSessionDate(date).getTime() >=
1189
+ this.normalizeSessionDate(options.minDate!).getTime(),
1190
+ )
1191
+ : fullOccurrenceDates;
1192
+
1193
+ if (occurrenceDates.length === 0) {
1194
+ return null;
1195
+ }
1196
+
1197
+ const recurrenceId = normalizedRecurrence ? randomUUID() : null;
1198
+ const recurrenceRule = normalizedRecurrence
1199
+ ? JSON.stringify(normalizedRecurrence)
1200
+ : null;
1201
+
1202
+ const first = await tx.course_class_session.create({
1203
+ data: {
1204
+ course_class_group_id: classGroupId,
1205
+ title: dto.title,
1206
+ description: dto.description ?? null,
1207
+ session_date: occurrenceDates[0],
1208
+ start_time: dto.startTime,
1209
+ end_time: dto.endTime,
1210
+ location: dto.location ?? null,
1211
+ meeting_url: dto.meetingUrl ?? null,
1212
+ color: dto.color ?? null,
1213
+ status: dto.status ?? 'scheduled',
1214
+ recurrence_id: recurrenceId,
1215
+ recurrence_rule: recurrenceRule,
1216
+ occurrence_index: normalizedRecurrence ? 0 : null,
1217
+ is_exception: false,
1218
+ ...(dto.instructorId && {
1219
+ course_class_session_instructor: {
1220
+ create: {
1221
+ instructor_id: dto.instructorId,
1222
+ role: 'lead',
1223
+ },
1224
+ },
1225
+ }),
1226
+ },
1227
+ include: this.getSessionInclude(),
1228
+ });
1229
+
1230
+ for (let index = 1; index < occurrenceDates.length; index += 1) {
1231
+ await tx.course_class_session.create({
1232
+ data: {
1233
+ course_class_group_id: classGroupId,
1234
+ title: dto.title,
1235
+ description: dto.description ?? null,
1236
+ session_date: occurrenceDates[index],
1237
+ start_time: dto.startTime,
1238
+ end_time: dto.endTime,
1239
+ location: dto.location ?? null,
1240
+ meeting_url: dto.meetingUrl ?? null,
1241
+ color: dto.color ?? null,
1242
+ status: dto.status ?? 'scheduled',
1243
+ recurrence_id: recurrenceId,
1244
+ recurrence_rule: recurrenceRule,
1245
+ occurrence_index: normalizedRecurrence ? index : null,
1246
+ is_exception: false,
1247
+ parent_session_id: first.id,
1248
+ ...(dto.instructorId && {
1249
+ course_class_session_instructor: {
1250
+ create: {
1251
+ instructor_id: dto.instructorId,
1252
+ role: 'lead',
1253
+ },
1254
+ },
1255
+ }),
1256
+ },
1257
+ });
1258
+ }
1259
+
1260
+ return first;
1261
+ }
1262
1262
 
1263
1263
  async updateSession(
1264
1264
  classGroupId: number,
@@ -1791,8 +1791,8 @@ export class ClassGroupService {
1791
1791
 
1792
1792
  private handlePrismaError(error: unknown): never {
1793
1793
  if (
1794
- error instanceof PrismaClientKnownRequestError &&
1795
- error.code === 'P2002'
1794
+ error instanceof PrismaClientKnownRequestError &&
1795
+ error.code === 'P2002'
1796
1796
  ) {
1797
1797
  throw new ConflictException('Class code already exists');
1798
1798
  }