- Updated ScheduleSchema to implement role-based querying for schedules. - Added logic to handle different user roles: CUSTOMER, CENTER_MENTOR, and CENTER_OWNER. - Improved error handling for cases where the center is not found for mentors and owners. - This change enhances the flexibility and usability of the schedule querying functionality.
442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common'
|
|
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
|
import { Builder } from '../Graphql/graphql.builder'
|
|
import { PrismaService } from '../Prisma/prisma.service'
|
|
import { CenterStatus, ScheduleDateStatus, ScheduleStatus } from '@prisma/client'
|
|
import { ScheduleService } from './schedule.service'
|
|
import { AppConfigService } from '../AppConfig/appconfig.service'
|
|
import { ScheduleConfigType } from './schedule'
|
|
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
|
|
import { Role } from '@prisma/client'
|
|
@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,
|
|
) {
|
|
super()
|
|
}
|
|
|
|
@PothosRef()
|
|
schedule() {
|
|
return this.builder.prismaObject('Schedule', {
|
|
description: 'A schedule in the system.',
|
|
fields: (t) => ({
|
|
id: t.exposeID('id', {
|
|
description: 'The ID of the schedule.',
|
|
}),
|
|
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.',
|
|
nullable: false,
|
|
}),
|
|
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.',
|
|
nullable: true,
|
|
}),
|
|
scheduleStart: t.expose('scheduleStart', {
|
|
type: 'DateTime',
|
|
nullable: false,
|
|
}),
|
|
scheduleEnd: t.expose('scheduleEnd', {
|
|
type: 'DateTime',
|
|
nullable: false,
|
|
}),
|
|
slots: t.exposeIntList('slots', {
|
|
nullable: false,
|
|
}),
|
|
daysOfWeek: t.exposeIntList('daysOfWeek', {
|
|
nullable: false,
|
|
}),
|
|
dates: t.relation('dates', {
|
|
description: 'The dates of the schedule.',
|
|
}),
|
|
status: t.expose('status', {
|
|
type: ScheduleStatus,
|
|
nullable: false,
|
|
}),
|
|
managedService: t.relation('managedService', {
|
|
description: 'The managed service the schedule belongs to.',
|
|
nullable: false,
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@PothosRef()
|
|
scheduleConnection() {
|
|
return this.builder.simpleObject('ScheduleConnection', {
|
|
fields: (t) => ({
|
|
totalCount: t.int({
|
|
nullable: true,
|
|
}),
|
|
schedules: t.field({
|
|
type: [this.schedule()],
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@PothosRef()
|
|
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', {
|
|
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.',
|
|
fields: (t) => ({
|
|
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.',
|
|
}),
|
|
start: t.expose('start', {
|
|
type: 'DateTime',
|
|
nullable: false,
|
|
}),
|
|
end: t.expose('end', {
|
|
type: 'DateTime',
|
|
nullable: false,
|
|
}),
|
|
status: t.expose('status', {
|
|
type: ScheduleDateStatus,
|
|
nullable: false,
|
|
}),
|
|
dayOfWeek: t.exposeInt('dayOfWeek', {
|
|
nullable: false,
|
|
}),
|
|
slot: t.exposeInt('slot', {
|
|
nullable: false,
|
|
}),
|
|
serviceId: t.exposeID('serviceId', {
|
|
nullable: false,
|
|
}),
|
|
orderId: t.exposeID('orderId', {
|
|
nullable: true,
|
|
}),
|
|
participantIds: t.exposeStringList('participantIds', {
|
|
nullable: false,
|
|
}),
|
|
maxParticipants: t.exposeInt('maxParticipants', {
|
|
nullable: false,
|
|
}),
|
|
lateStart: t.expose('lateStart', {
|
|
type: 'DateTime',
|
|
nullable: true,
|
|
}),
|
|
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.',
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@PothosRef()
|
|
scheduleConfigInput() {
|
|
return this.builder.inputType('ScheduleConfigInput', {
|
|
description: 'A schedule config in the system.',
|
|
fields: (t) => ({
|
|
midDayBreakTimeStart: t.string({
|
|
required: true,
|
|
}),
|
|
midDayBreakTimeEnd: t.string({
|
|
required: true,
|
|
}),
|
|
slotDuration: t.string({
|
|
required: true,
|
|
}),
|
|
slotBreakDuration: t.string({
|
|
required: true,
|
|
}),
|
|
dayStartTime: t.string({
|
|
required: true,
|
|
}),
|
|
dayEndTime: t.string({
|
|
required: true,
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@PothosRef()
|
|
scheduleConfigInputForCenter() {
|
|
return this.builder.inputType('ScheduleConfigInputForCenter', {
|
|
fields: (t) => ({
|
|
startDate: t.string({
|
|
required: true,
|
|
}),
|
|
endDate: t.string({
|
|
required: true,
|
|
}),
|
|
slots: t.intList({
|
|
required: true,
|
|
}),
|
|
days: t.intList({
|
|
required: true,
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
@Pothos()
|
|
init(): void {
|
|
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'),
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Cannot retrieve schedule in subscription')
|
|
}
|
|
if (!ctx.http?.me?.id) {
|
|
throw new Error('User not found')
|
|
}
|
|
// only return schedule belong to center
|
|
|
|
const center = await this.prisma.center.findFirst({
|
|
where: {
|
|
AND: [
|
|
{ OR: [{ centerOwnerId: ctx.http.me.id }, { centerMentors: { some: { mentorId: ctx.http.me.id } } }] },
|
|
{ centerStatus: CenterStatus.APPROVED },
|
|
],
|
|
},
|
|
})
|
|
|
|
if (!center) {
|
|
throw new Error('Center not found')
|
|
}
|
|
return await this.prisma.schedule.findUnique({
|
|
...query,
|
|
where: {
|
|
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.',
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Cannot retrieve schedules in subscription')
|
|
}
|
|
if (!ctx.http?.me?.id) {
|
|
throw new Error('User not found')
|
|
}
|
|
// use case 1: customer query schedules where customer is participant
|
|
if (ctx.http.me.role === Role.CUSTOMER) {
|
|
const schedules = await this.prisma.schedule.findMany({
|
|
...query,
|
|
where: {
|
|
customerId: ctx.http.me.id,
|
|
},
|
|
})
|
|
return schedules
|
|
}
|
|
// use case 2: center mentor or center owner query schedules where center mentor or center owner is mentor
|
|
else if (ctx.http.me.role === Role.CENTER_MENTOR || ctx.http.me.role === Role.CENTER_OWNER) {
|
|
const center = await this.prisma.center.findFirst({
|
|
where: {
|
|
OR: [{ centerOwnerId: ctx.http.me.id }, { centerMentors: { some: { mentorId: ctx.http.me.id } } }],
|
|
},
|
|
include: {
|
|
centerMentors: true,
|
|
},
|
|
})
|
|
if (!center) {
|
|
throw new Error('Center not found')
|
|
}
|
|
const schedules = await this.prisma.schedule.findMany({
|
|
...query,
|
|
skip: args.skip ?? undefined,
|
|
take: args.take ?? undefined,
|
|
orderBy: args.orderBy ?? undefined,
|
|
where: {
|
|
AND: [
|
|
{ managedService: { service: { centerId: center.id } } },
|
|
{
|
|
OR: [
|
|
{ customerId: ctx.http.me.id },
|
|
{ dates: { some: { participantIds: { has: ctx.http.me.id } } } },
|
|
],
|
|
},
|
|
...(args.filter ? [args.filter] : []),
|
|
],
|
|
},
|
|
})
|
|
return schedules
|
|
}
|
|
},
|
|
}),
|
|
|
|
centerPreviewSchedule: t.field({
|
|
type: this.previewSchedule(),
|
|
description: 'Preview a schedule for center mentor.',
|
|
args: {
|
|
scheduleConfigInput: t.arg({
|
|
type: this.scheduleConfigInputForCenter(),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (_parent, args, _context, _info) => {
|
|
return await this.scheduleService.createSchedulePreviewForCenter(args.scheduleConfigInput)
|
|
},
|
|
}),
|
|
|
|
adminPreviewSchedule: t.field({
|
|
type: this.previewSchedule(),
|
|
description: 'Preview a schedule for admin.',
|
|
args: {
|
|
scheduleConfig: t.arg({
|
|
type: this.scheduleConfigInput(),
|
|
}),
|
|
},
|
|
resolve: async (_parent, args, _context, _info) => {
|
|
// if no scheduleConfig, use default config
|
|
if (!args.scheduleConfig) {
|
|
args.scheduleConfig = (await this.appConfigService.getVisibleConfigs()).reduce((acc, curr) => {
|
|
// @ts-ignore
|
|
acc[curr.key] = curr.value
|
|
return acc
|
|
}, {} as ScheduleConfigType)
|
|
}
|
|
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}"
|
|
d72a864e-2f41-45ab-9c9b-bf0512a31883,e9be51fd-2382-4e43-9988-74e76fde4b56,2024-11-22 00:00:00.000,2024-11-02 00:00:00.000,UNPUBLISHED,,"{3,5}",,"{2,4}"
|
|
*/
|
|
|
|
this.builder.mutationFields((t) => ({
|
|
// Mutations
|
|
createSchedule: t.prismaField({
|
|
type: this.schedule(),
|
|
description: 'Create a new schedule.',
|
|
args: {
|
|
schedule: t.arg({
|
|
type: this.builder.generator.getCreateInput('Schedule', ['id', 'status', 'customerId', 'orderId', 'dates']),
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
if (ctx.isSubscription) {
|
|
throw new Error('Cannot create schedule in subscription')
|
|
}
|
|
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')
|
|
}
|
|
// 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: {
|
|
serviceId: args.schedule.managedService.connect?.id,
|
|
},
|
|
})
|
|
// check if there is any overlapping with existing schedule dates in same service using DateTimeUtils
|
|
const isOverlapping = DateTimeUtils.isOverlaps(
|
|
previewSchedule.slots.map((slot) => ({
|
|
start: DateTimeUtils.fromIsoString(slot.start),
|
|
end: DateTimeUtils.fromIsoString(slot.end),
|
|
})),
|
|
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')
|
|
}
|
|
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)
|
|
// update schedule with schedule dates
|
|
return await this.prisma.schedule.update({
|
|
...query,
|
|
where: { id: schedule.id },
|
|
data: {
|
|
dates: {
|
|
connect: scheduleDates.map((date) => ({ id: date.id })),
|
|
},
|
|
},
|
|
})
|
|
},
|
|
}),
|
|
|
|
updateScheduleStatus: t.prismaField({
|
|
type: this.schedule(),
|
|
description: 'Update a schedule status.',
|
|
args: {
|
|
scheduleId: t.arg({
|
|
type: 'String',
|
|
required: true,
|
|
}),
|
|
status: t.arg({
|
|
type: ScheduleStatus,
|
|
required: true,
|
|
}),
|
|
},
|
|
resolve: async (query, _root, args, _ctx, _info) => {
|
|
return await this.prisma.schedule.update({
|
|
...query,
|
|
where: { id: args.scheduleId },
|
|
data: { status: args.status },
|
|
})
|
|
},
|
|
}),
|
|
}))
|
|
}
|
|
}
|