diff --git a/src/Schedule/schedule.schema.ts b/src/Schedule/schedule.schema.ts index a2fc07f..de488a2 100644 --- a/src/Schedule/schedule.schema.ts +++ b/src/Schedule/schedule.schema.ts @@ -1,84 +1,93 @@ -import { Inject, Injectable, Logger } from '@nestjs/common' -import { CenterStatus, ScheduleDateStatus, ScheduleStatus } from '@prisma/client' -import { Role } from '@prisma/client' -import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' -import { DateTimeUtils } from 'src/common/utils/datetime.utils' -import { AppConfigService } from '../AppConfig/appconfig.service' -import { Builder } from '../Graphql/graphql.builder' -import { PrismaService } from '../Prisma/prisma.service' -import { ScheduleConfigType } from './schedule' -import { ScheduleService } from './schedule.service' +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { + CenterStatus, + ScheduleDateStatus, + ScheduleStatus, +} from "@prisma/client"; +import { Role } from "@prisma/client"; +import { + Pothos, + PothosRef, + PothosSchema, + SchemaBuilderToken, +} from "@smatch-corp/nestjs-pothos"; +import { DateTimeUtils } from "src/common/utils/datetime.utils"; +import { AppConfigService } from "../AppConfig/appconfig.service"; +import { Builder } from "../Graphql/graphql.builder"; +import { PrismaService } from "../Prisma/prisma.service"; +import { ScheduleConfigType } from "./schedule"; +import { ScheduleService } from "./schedule.service"; @Injectable() export class ScheduleSchema extends PothosSchema { constructor( @Inject(SchemaBuilderToken) private readonly builder: Builder, private readonly prisma: PrismaService, private readonly scheduleService: ScheduleService, - private readonly appConfigService: AppConfigService, + private readonly appConfigService: AppConfigService ) { - super() + super(); } @PothosRef() schedule() { - return this.builder.prismaObject('Schedule', { - description: 'A schedule in the system.', + return this.builder.prismaObject("Schedule", { + description: "A schedule in the system.", fields: (t) => ({ - id: t.exposeID('id', { - description: 'The ID of the schedule.', + id: t.exposeID("id", { + description: "The ID of the schedule.", }), - customerId: t.exposeID('customerId', { - description: 'The ID of the customer the schedule belongs to.', + customerId: t.exposeID("customerId", { + description: "The ID of the customer the schedule belongs to.", nullable: true, }), - managedServiceId: t.exposeID('managedServiceId', { - description: 'The ID of the managed service the schedule belongs to.', + managedServiceId: t.exposeID("managedServiceId", { + description: "The ID of the managed service the schedule belongs to.", nullable: false, }), - orderId: t.exposeID('orderId', { - description: 'The ID of the order the schedule belongs to.', + orderId: t.exposeID("orderId", { + description: "The ID of the order the schedule belongs to.", nullable: true, }), - order: t.relation('Order', { - description: 'The order that belongs to orderId.', + order: t.relation("Order", { + description: "The order that belongs to orderId.", nullable: true, }), - scheduleStart: t.expose('scheduleStart', { - type: 'DateTime', + scheduleStart: t.expose("scheduleStart", { + type: "DateTime", nullable: false, }), - scheduleEnd: t.expose('scheduleEnd', { - type: 'DateTime', + scheduleEnd: t.expose("scheduleEnd", { + type: "DateTime", nullable: false, }), - slots: t.exposeIntList('slots', { + slots: t.exposeIntList("slots", { nullable: false, }), - daysOfWeek: t.exposeIntList('daysOfWeek', { + daysOfWeek: t.exposeIntList("daysOfWeek", { nullable: false, }), - dates: t.relation('dates', { - description: 'The dates of the schedule.', + dates: t.relation("dates", { + description: "The dates of the schedule.", }), - status: t.expose('status', { + status: t.expose("status", { type: ScheduleStatus, nullable: false, }), - managedService: t.relation('managedService', { - description: 'The managed service the schedule belongs to.', + managedService: t.relation("managedService", { + description: "The managed service the schedule belongs to.", nullable: false, }), - personalMilestone: t.relation('personalMilestone', { - description: 'The personal milestone of the schedule.', + personalMilestone: t.relation("personalMilestone", { + description: "The personal milestone of the schedule.", nullable: true, }), }), - }) + }); } @PothosRef() scheduleConnection() { - return this.builder.simpleObject('ScheduleConnection', { + return this.builder.simpleObject("ScheduleConnection", { fields: (t) => ({ totalCount: t.int({ nullable: true, @@ -87,93 +96,93 @@ export class ScheduleSchema extends PothosSchema { type: [this.schedule()], }), }), - }) + }); } @PothosRef() scheduleSlot() { - return this.builder.simpleObject('ScheduleSlot', { + return this.builder.simpleObject("ScheduleSlot", { fields: (t) => ({ slot: t.string({}), start: t.string({}), end: t.string({}), dayOfWeek: t.int({}), }), - }) + }); } @PothosRef() previewSchedule() { - return this.builder.simpleObject('PreviewSchedule', { + return this.builder.simpleObject("PreviewSchedule", { fields: (t) => ({ totalSlots: t.int(), slots: t.field({ type: [this.scheduleSlot()], }), }), - }) + }); } @PothosRef() scheduleDate() { - return this.builder.prismaObject('ScheduleDate', { - description: 'A schedule date in the system.', + return this.builder.prismaObject("ScheduleDate", { + description: "A schedule date in the system.", fields: (t) => ({ - id: t.exposeID('id', { - description: 'The ID of the schedule date.', + id: t.exposeID("id", { + description: "The ID of the schedule date.", }), - scheduleId: t.exposeID('scheduleId', { - description: 'The ID of the schedule the schedule date belongs to.', + scheduleId: t.exposeID("scheduleId", { + description: "The ID of the schedule the schedule date belongs to.", }), - start: t.expose('start', { - type: 'DateTime', + start: t.expose("start", { + type: "DateTime", nullable: false, }), - end: t.expose('end', { - type: 'DateTime', + end: t.expose("end", { + type: "DateTime", nullable: false, }), - status: t.expose('status', { + status: t.expose("status", { type: ScheduleDateStatus, nullable: false, }), - dayOfWeek: t.exposeInt('dayOfWeek', { + dayOfWeek: t.exposeInt("dayOfWeek", { nullable: false, }), - slot: t.exposeInt('slot', { + slot: t.exposeInt("slot", { nullable: false, }), - serviceId: t.exposeID('serviceId', { + serviceId: t.exposeID("serviceId", { nullable: false, }), - orderId: t.exposeID('orderId', { + orderId: t.exposeID("orderId", { nullable: true, }), - participantIds: t.exposeStringList('participantIds', { + participantIds: t.exposeStringList("participantIds", { nullable: false, }), - maxParticipants: t.exposeInt('maxParticipants', { + maxParticipants: t.exposeInt("maxParticipants", { nullable: false, }), - lateStart: t.expose('lateStart', { - type: 'DateTime', + lateStart: t.expose("lateStart", { + type: "DateTime", nullable: true, }), - collaborationSession: t.relation('CollaborationSession', { - description: 'The collaboration session of the schedule date.', + collaborationSession: t.relation("CollaborationSession", { + description: "The collaboration session of the schedule date.", nullable: true, }), - schedule: t.relation('schedule', { - description: 'The schedule the schedule date belongs to.', + schedule: t.relation("schedule", { + description: "The schedule the schedule date belongs to.", }), }), - }) + }); } @PothosRef() scheduleConfigInput() { - return this.builder.inputType('ScheduleConfigInput', { - description: 'A schedule config in the system.', + return this.builder.inputType("ScheduleConfigInput", { + description: "A schedule config in the system.", fields: (t) => ({ midDayBreakTimeStart: t.string({ required: true, @@ -194,12 +203,12 @@ export class ScheduleSchema extends PothosSchema { required: true, }), }), - }) + }); } @PothosRef() scheduleConfigInputForCenter() { - return this.builder.inputType('ScheduleConfigInputForCenter', { + return this.builder.inputType("ScheduleConfigInputForCenter", { fields: (t) => ({ startDate: t.string({ required: true, @@ -214,7 +223,7 @@ export class ScheduleSchema extends PothosSchema { required: true, }), }), - }) + }); } @Pothos() @@ -222,25 +231,30 @@ export class ScheduleSchema extends PothosSchema { this.builder.queryFields((t) => ({ schedule: t.prismaField({ type: this.schedule(), - description: 'Retrieve a single schedule by its unique identifier.', - args: this.builder.generator.findUniqueArgs('Schedule'), + description: "Retrieve a single schedule by its unique identifier.", + args: this.builder.generator.findUniqueArgs("Schedule"), resolve: async (query, _root, args, ctx, _info) => { if (!ctx.me) { - throw new Error('User not found') + throw new Error("User not found"); } // only return schedule belong to center const center = await this.prisma.center.findFirst({ where: { AND: [ - { OR: [{ centerOwnerId: ctx.me.id }, { centerMentors: { some: { mentorId: ctx.me.id } } }] }, + { + OR: [ + { centerOwnerId: ctx.me.id }, + { centerMentors: { some: { mentorId: ctx.me.id } } }, + ], + }, { centerStatus: CenterStatus.APPROVED }, ], }, - }) + }); if (!center) { - throw new Error('Center not found') + throw new Error("Center not found"); } return await this.prisma.schedule.findUnique({ ...query, @@ -248,17 +262,18 @@ export class ScheduleSchema extends PothosSchema { id: args.where?.id, managedService: { service: { centerId: center.id } }, }, - }) + }); }, }), schedules: t.prismaField({ type: [this.schedule()], - args: this.builder.generator.findManyArgs('Schedule'), - description: 'Retrieve a list of schedules with optional filtering, ordering, and pagination.', + args: this.builder.generator.findManyArgs("Schedule"), + description: + "Retrieve a list of schedules with optional filtering, ordering, and pagination.", resolve: async (query, _root, args, ctx, _info) => { if (!ctx.me) { - throw new Error('User not found') + throw new Error("User not found"); } // use case 1: customer query schedules where customer is participant if (ctx.me.role === Role.CUSTOMER) { @@ -268,8 +283,8 @@ export class ScheduleSchema extends PothosSchema { skip: args.skip ?? undefined, take: args.take ?? undefined, where: args.filter ?? undefined, - }) - return schedules + }); + return schedules; } // use case 2: center mentor or center owner query schedules where center mentor or center owner is mentor if (ctx.me.role === Role.CENTER_MENTOR) { @@ -281,9 +296,9 @@ export class ScheduleSchema extends PothosSchema { }, }, }, - }) + }); if (!center) { - throw new Error('Center not found') + throw new Error("Center not found"); } // get all schedules belong to center const schedules = await this.prisma.schedule.findMany({ @@ -293,42 +308,50 @@ export class ScheduleSchema extends PothosSchema { orderBy: args.orderBy ?? undefined, where: { AND: [ - { managedService: { service: { centerId: center.id }, mentorId: ctx.me.id } }, + { + managedService: { + service: { centerId: center.id }, + mentorId: ctx.me.id, + }, + }, ...(args.filter ? [args.filter] : []), ], }, - }) - return schedules + }); + return schedules; } // use case 3: Center owner query all schedules belong to center if (ctx.me.role === Role.CENTER_OWNER) { const center = await this.prisma.center.findFirst({ where: { centerOwnerId: ctx.me.id }, - }) + }); if (!center) { - throw new Error('Center not found') + throw new Error("Center not found"); } const schedules = await this.prisma.schedule.findMany({ ...query, where: { - AND: [{ managedService: { service: { centerId: center.id } } }, ...(args.filter ? [args.filter] : [])], + AND: [ + { managedService: { service: { centerId: center.id } } }, + ...(args.filter ? [args.filter] : []), + ], }, orderBy: args.orderBy ?? undefined, skip: args.skip ?? undefined, take: args.take ?? undefined, - }) - return schedules + }); + return schedules; } }, }), scheduleDates: t.prismaField({ type: [this.scheduleDate()], - description: 'Retrieve a list of schedule dates.', - args: this.builder.generator.findManyArgs('ScheduleDate'), + description: "Retrieve a list of schedule dates.", + args: this.builder.generator.findManyArgs("ScheduleDate"), resolve: async (query, _root, args, ctx, _info) => { if (!ctx.me) { - throw new Error('User not found') + throw new Error("User not found"); } return await this.prisma.scheduleDate.findMany({ ...query, @@ -336,15 +359,18 @@ export class ScheduleSchema extends PothosSchema { take: args.take ?? undefined, orderBy: args.orderBy ?? undefined, where: { - AND: [{ participantIds: { has: ctx.me.id } }, ...(args.filter ? [args.filter] : [])], + AND: [ + { participantIds: { has: ctx.me.id } }, + ...(args.filter ? [args.filter] : []), + ], }, - }) + }); }, }), centerPreviewSchedule: t.field({ type: this.previewSchedule(), - description: 'Preview a schedule for center mentor.', + description: "Preview a schedule for center mentor.", args: { scheduleConfigInput: t.arg({ type: this.scheduleConfigInputForCenter(), @@ -352,13 +378,15 @@ export class ScheduleSchema extends PothosSchema { }), }, resolve: async (_parent, args, _context, _info) => { - return await this.scheduleService.createSchedulePreviewForCenter(args.scheduleConfigInput) + return await this.scheduleService.createSchedulePreviewForCenter( + args.scheduleConfigInput + ); }, }), adminPreviewSchedule: t.field({ type: this.previewSchedule(), - description: 'Preview a schedule for admin.', + description: "Preview a schedule for admin.", args: { scheduleConfig: t.arg({ type: this.scheduleConfigInput(), @@ -367,16 +395,20 @@ export class ScheduleSchema extends PothosSchema { resolve: async (_parent, args, _context, _info) => { // if no scheduleConfig, use default config if (!args.scheduleConfig) { - args.scheduleConfig = (await this.appConfigService.getVisibleConfigs()).reduce((acc, curr) => { + args.scheduleConfig = ( + await this.appConfigService.getVisibleConfigs() + ).reduce((acc, curr) => { // @ts-ignore - acc[curr.key] = curr.value - return acc - }, {} as ScheduleConfigType) + acc[curr.key] = curr.value; + return acc; + }, {} as ScheduleConfigType); } - return await this.scheduleService.createSchedulePreviewForSingleDay(args.scheduleConfig) + return await this.scheduleService.createSchedulePreviewForSingleDay( + args.scheduleConfig + ); }, }), - })) + })); /* overlapping case 46836288-bb2c-4da6-892b-a559a480cbf8,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-11-22 00:00:00.000,2024-11-02 00:00:00.000,UNPUBLISHED,,"{3,5}",,"{2,4}" @@ -387,46 +419,58 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1 // Mutations createSchedule: t.prismaField({ type: this.schedule(), - description: 'Create a new schedule.', + description: "Create a new schedule.", args: { schedule: t.arg({ - type: this.builder.generator.getCreateInput('Schedule', ['id', 'status', 'customerId', 'orderId', 'dates']), + type: this.builder.generator.getCreateInput("Schedule", [ + "id", + "status", + "customerId", + "orderId", + "dates", + ]), required: true, }), }, resolve: async (query, _root, args, ctx, _info) => { if (!ctx.me) { - throw new Error('User not found') + throw new Error("User not found"); } - Logger.log('args.schedule', args.schedule) + Logger.log("args.schedule", args.schedule); // reject schedule if start date is today or in the past - if (DateTimeUtils.fromDate(args.schedule.scheduleStart as Date).day <= DateTimeUtils.now().day) { - throw new Error('Start date is in the past or today') + if ( + DateTimeUtils.fromDate(args.schedule.scheduleStart as Date).day <= + DateTimeUtils.now().day + ) { + throw new Error("Start date is in the past or today"); } // generate preview and check if there is any overlapping with other schedules date in same service - const previewSchedule = await this.scheduleService.createSchedulePreviewForCenter({ - startDate: args.schedule.scheduleStart as string, - endDate: args.schedule.scheduleEnd as string, - slots: args.schedule.slots as number[], - days: args.schedule.daysOfWeek as number[], - }) - const existingScheduleDates = await this.prisma.scheduleDate.findMany({ - where: { - AND: [ - { serviceId: args.schedule.managedService.connect?.id }, - { - status: { - notIn: [ - ScheduleDateStatus.COMPLETED, - ScheduleDateStatus.MISSING_MENTOR, - ScheduleDateStatus.MISSING_CUSTOMER, - ScheduleDateStatus.EXPIRED, - ], + const previewSchedule = + await this.scheduleService.createSchedulePreviewForCenter({ + startDate: args.schedule.scheduleStart as string, + endDate: args.schedule.scheduleEnd as string, + slots: args.schedule.slots as number[], + days: args.schedule.daysOfWeek as number[], + }); + const existingScheduleDates = await this.prisma.scheduleDate.findMany( + { + where: { + AND: [ + { serviceId: args.schedule.managedService.connect?.id }, + { + status: { + notIn: [ + ScheduleDateStatus.COMPLETED, + ScheduleDateStatus.MISSING_MENTOR, + ScheduleDateStatus.MISSING_CUSTOMER, + ScheduleDateStatus.EXPIRED, + ], + }, }, - }, - ], - }, - }) + ], + }, + } + ); // check if there is any overlapping with existing schedule dates in same service using DateTimeUtils const isOverlapping = DateTimeUtils.isOverlaps( previewSchedule.slots.map((slot) => ({ @@ -436,18 +480,57 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1 existingScheduleDates.map((date) => ({ start: DateTimeUtils.fromDate(date.start), end: DateTimeUtils.fromDate(date.end), - })), - ) + })) + ); if (isOverlapping) { - Logger.error('Overlapping schedule', 'ScheduleSchema') - throw new Error('Overlapping schedule') + Logger.error("Overlapping schedule", "ScheduleSchema"); + throw new Error("Overlapping schedule"); + } + const scheduleDatesInSchedule = + await this.prisma.scheduleDate.findMany({ + where: { + scheduleId: args.schedule.id, + }, + }); + const overlapSchedule = await this.prisma.scheduleDate.findFirst({ + where: { + AND: [ + { + participantIds: { + has: ctx.me?.id ?? "", + }, + orderId: { + not: null, + }, + dayOfWeek: { + in: Array.isArray(args.schedule.daysOfWeek) + ? args.schedule.daysOfWeek + : [], + }, + slot: { + in: Array.isArray(args.schedule.slots) + ? args.schedule.slots + : [], + }, + scheduleId: { + notIn: scheduleDatesInSchedule.map( + (scheduleDate) => scheduleDate.scheduleId + ), + }, + }, + ], + }, + }); + if (overlapSchedule) { + throw new Error("Overlapping schedule"); } const schedule = await this.prisma.schedule.create({ ...query, data: args.schedule, - }) + }); // generate schedule dates based on data and config - const scheduleDates = await this.scheduleService.generateScheduleDates(schedule) + const scheduleDates = + await this.scheduleService.generateScheduleDates(schedule); // update schedule with schedule dates return await this.prisma.schedule.update({ ...query, @@ -457,16 +540,16 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1 connect: scheduleDates.map((date) => ({ id: date.id })), }, }, - }) + }); }, }), updateScheduleStatus: t.prismaField({ type: this.schedule(), - description: 'Update a schedule status.', + description: "Update a schedule status.", args: { scheduleId: t.arg({ - type: 'String', + type: "String", required: true, }), status: t.arg({ @@ -479,9 +562,9 @@ d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-1 ...query, where: { id: args.scheduleId }, data: { status: args.status }, - }) + }); }, }), - })) + })); } }