update preview date

This commit is contained in:
2024-10-30 17:56:19 +07:00
parent 3114357515
commit a09785ec71
8 changed files with 347 additions and 23 deletions

8
package-lock.json generated
View File

@@ -77,6 +77,7 @@
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
@@ -5534,6 +5535,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/long": { "node_modules/@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",

View File

@@ -99,6 +99,7 @@
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
@@ -123,13 +124,19 @@
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"jest": { "jest": {
"moduleFileExtensions": ["js", "json", "ts"], "moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"collectCoverageFrom": ["**/*.(t|j)s"], "collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
}, },

View File

@@ -22,25 +22,25 @@ export const ConfigConstants: Record<
MID_DAY_BREAK_TIME_START: { MID_DAY_BREAK_TIME_START: {
name: 'Mid Day Break Time Start', name: 'Mid Day Break Time Start',
key: 'MID_DAY_BREAK_TIME_START', key: 'MID_DAY_BREAK_TIME_START',
value: new Date(new Date().setHours(12, 0, 0, 0)).toISOString(), value: new Date(new Date().setUTCHours(12, 0, 0, 0)).toISOString(),
visible: true, visible: true,
}, },
MID_DAY_BREAK_TIME_END: { MID_DAY_BREAK_TIME_END: {
name: 'Mid Day Break Time End', name: 'Mid Day Break Time End',
key: 'MID_DAY_BREAK_TIME_END', key: 'MID_DAY_BREAK_TIME_END',
value: new Date(new Date().setHours(13, 0, 0, 0)).toISOString(), value: new Date(new Date().setUTCHours(13, 0, 0, 0)).toISOString(),
visible: true, visible: true,
}, },
SLOT_START_TIME: { SLOT_START_TIME: {
name: 'Slot Start Time', name: 'Slot Start Time',
key: 'SLOT_START_TIME', key: 'SLOT_START_TIME',
value: new Date(new Date().setHours(8, 0, 0, 0)).toISOString(), value: new Date(new Date().setUTCHours(8, 0, 0, 0)).toISOString(),
visible: true, visible: true,
}, },
SLOT_END_TIME: { SLOT_END_TIME: {
name: 'Slot End Time', name: 'Slot End Time',
key: 'SLOT_END_TIME', key: 'SLOT_END_TIME',
value: new Date(new Date().setHours(22, 0, 0, 0)).toISOString(), value: new Date(new Date().setUTCHours(22, 0, 0, 0)).toISOString(),
visible: true, visible: true,
}, },
} }

View File

@@ -32,6 +32,9 @@ export class AppConfigService implements OnModuleInit {
where: { key }, where: { key },
}) })
} }
async getConfigValue(key: string) {
return (await this.getConfig(key))?.value
}
async getVisibleConfigs() { async getVisibleConfigs() {
return await this.prisma.config.findMany({ return await this.prisma.config.findMany({

View File

@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { ScheduleSchema } from './schedule.schema' import { ScheduleSchema } from './schedule.schema'
import { ScheduleService } from './schedule.service'
@Module({ @Module({
providers: [ScheduleSchema], providers: [ScheduleSchema, ScheduleService],
exports: [ScheduleSchema], exports: [ScheduleSchema, ScheduleService],
}) })
export class ScheduleModule {} export class ScheduleModule {}

View File

@@ -8,12 +8,39 @@ import {
import { Builder } from '../Graphql/graphql.builder' import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
import { ScheduleStatus } from '@prisma/client' import { ScheduleStatus } from '@prisma/client'
import { ScheduleService } from './schedule.service'
import { AppConfigService } from '../AppConfig/appconfig.service'
export type ScheduleConfigType =
| {
midDayBreakTimeStart?: string | null | undefined
midDayBreakTimeEnd?: string | null | undefined
slotDuration?: string | null | undefined
slotBreakDuration?: string | null | undefined
slotEndTime?: string | null | undefined
slotStartTime?: string | null | undefined
}
| null
| undefined
export type ScheduleSlotType = {
slot: string
start: string
end: string
}
export type PreviewScheduleType = {
totalSlots: number
slots: ScheduleSlotType[]
}
@Injectable() @Injectable()
export class ScheduleSchema extends PothosSchema { export class ScheduleSchema extends PothosSchema {
constructor( constructor(
@Inject(SchemaBuilderToken) private readonly builder: Builder, @Inject(SchemaBuilderToken) private readonly builder: Builder,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly scheduleService: ScheduleService,
private readonly appConfigService: AppConfigService,
) { ) {
super() super()
} }
@@ -53,6 +80,29 @@ export class ScheduleSchema extends PothosSchema {
}) })
} }
@PothosRef()
scheduleSlot() {
return this.builder.simpleObject('ScheduleSlot', {
fields: (t) => ({
slot: t.string({}),
start: t.string({}),
end: t.string({}),
}),
})
}
@PothosRef()
previewSchedule() {
return this.builder.simpleObject('PreviewSchedule', {
fields: (t) => ({
totalSlots: t.int(),
slots: t.field({
type: [this.scheduleSlot()],
}),
}),
})
}
@PothosRef() @PothosRef()
scheduleDate() { scheduleDate() {
return this.builder.prismaObject('ScheduleDate', { return this.builder.prismaObject('ScheduleDate', {
@@ -79,6 +129,21 @@ export class ScheduleSchema extends PothosSchema {
}) })
} }
@PothosRef()
scheduleConfigInput() {
return this.builder.inputType('ScheduleConfigInput', {
description: 'A schedule config in the system.',
fields: (t) => ({
midDayBreakTimeStart: t.string(),
midDayBreakTimeEnd: t.string(),
slotDuration: t.string(),
slotBreakDuration: t.string(),
slotEndTime: t.string(),
slotStartTime: t.string(),
}),
})
}
@Pothos() @Pothos()
init(): void { init(): void {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
@@ -109,6 +174,21 @@ export class ScheduleSchema extends PothosSchema {
}) })
}, },
}), }),
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) => {
return await this.scheduleService.createSchedulePreview(
args.scheduleConfig,
)
},
}),
})) }))
} }
} }

View File

@@ -1,10 +1,16 @@
import * as DateTimeUtils from '../common/utils/datetime.utils' import * as DateTimeUtils from '../common/utils/datetime.utils'
import { Injectable } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { PrismaService } from 'src/Prisma/prisma.service' import { PrismaService } from 'src/Prisma/prisma.service'
import { Schedule } from '@prisma/client'
import { AppConfigService } from 'src/AppConfig/appconfig.service' import { AppConfigService } from 'src/AppConfig/appconfig.service'
import {
PreviewScheduleType,
ScheduleConfigType,
ScheduleSlotType,
} from './schedule.schema'
import { Config } from '@prisma/client'
import * as _ from 'lodash'
@Injectable() @Injectable()
export class ScheduleService { export class ScheduleService {
@@ -13,10 +19,176 @@ export class ScheduleService {
private readonly appConfigService: AppConfigService, private readonly appConfigService: AppConfigService,
) {} ) {}
async createSchedule(schedule: Schedule) { async createSchedulePreview(
// get config scheduleConfig: ScheduleConfigType,
const config = await this.appConfigService.getVisibleConfigs() ): Promise<PreviewScheduleType> {
console.log(config) const config: Config[] = await this.appConfigService.getVisibleConfigs()
Logger.log(config)
// process scheduleConfig input by filling with default values from config
const scheduleConfigFilled = this.processScheduleConfig(
scheduleConfig,
config,
)
// generate Slot By config // generate Slot By config
const slots = this.generateSlots(scheduleConfigFilled)
return {
totalSlots: slots.length,
slots: slots,
}
}
generateSlots(scheduleConfigFilled: ScheduleConfigType): ScheduleSlotType[] {
Logger.log(`Generating slots with config: ${scheduleConfigFilled}`)
const slots: ScheduleSlotType[] = []
const numberOfSlots = this.calculateNumberOfSlots(
// @ts-ignore
scheduleConfigFilled?.slotStartTime,
// @ts-ignore
scheduleConfigFilled?.slotEndTime,
// @ts-ignore
scheduleConfigFilled?.slotDuration,
// @ts-ignore
scheduleConfigFilled?.slotBreakDuration,
)
for (let i = 1; i <= numberOfSlots; i++) {
const { startTime, endTime } = this.getSlotStartAndEndTime(
i,
// @ts-ignore
scheduleConfigFilled?.slotDuration,
// @ts-ignore
scheduleConfigFilled?.slotBreakDuration,
// @ts-ignore
scheduleConfigFilled?.slotStartTime,
)
// if the slot is not overlapping with mid day break time, add it to the slots
if (
!this.isOverLapping(
startTime,
endTime,
this.getSpecificDateWithTime(
// @ts-ignore
scheduleConfigFilled?.midDayBreakTimeStart,
),
this.getSpecificDateWithTime(
// @ts-ignore
scheduleConfigFilled?.midDayBreakTimeEnd,
),
)
) {
slots.push({
slot: i.toString(),
start: startTime.toISOString(),
end: endTime.toISOString(),
})
}
}
return slots
}
isOverLapping(
startTime1: Date,
endTime1: Date,
startTime2: Date,
endTime2: Date,
) {
return (
Math.max(startTime1.getTime(), startTime2.getTime()) <
Math.min(endTime1.getTime(), endTime2.getTime())
)
}
calculateNumberOfSlots(
startTime: string,
endTime: string,
slotDuration: string,
slotBreakDuration: string,
) {
const startDate = new Date(startTime)
const endDate = new Date(endTime)
const totalMinutes = (endDate.getTime() - startDate.getTime()) / (60 * 1000)
const numberOfSlots = Math.floor(
totalMinutes / (parseInt(slotDuration) + parseInt(slotBreakDuration)),
)
return numberOfSlots
}
getSlotStartAndEndTime(
slotNumber: number,
slotDuration: string,
slotBreakDuration: string,
slotStartTime: string,
) {
const startTime = new Date(slotStartTime)
startTime.setUTCMinutes(
startTime.getUTCMinutes() +
slotNumber * (parseInt(slotDuration) + parseInt(slotBreakDuration)),
)
const endTime = new Date(startTime)
endTime.setUTCMinutes(endTime.getUTCMinutes() + parseInt(slotDuration))
return { startTime, endTime }
}
processScheduleConfig(
scheduleConfig: ScheduleConfigType,
config: Config[],
): ScheduleConfigType {
// if scheduleConfig is undefined, create a new object and seed all the values with default values from config
if (scheduleConfig === undefined) {
scheduleConfig = config.reduce((acc, curr) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
;(acc as any)[this.sneakyCaseToCamelCase(curr.key)] = curr.value
return acc
}, {})
}
// loop through scheduleConfig and fill with default values from config
for (const key in scheduleConfig) {
if (
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
(scheduleConfig as any)[key] === undefined ||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
(scheduleConfig as any)[key] === null ||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
(scheduleConfig as any)[key] === ''
) {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
;(scheduleConfig as any)[key] =
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
config[this.camelCaseToUpperSneakyCase(key) as any]
}
}
return scheduleConfig
}
camelCaseToUpperSneakyCase(str: string) {
return _.snakeCase(str).toUpperCase()
}
sneakyCaseToCamelCase(str: string) {
return _.camelCase(str.toLowerCase())
}
getTodayWithTime(date: Date) {
const today = new Date()
today.setUTCHours(
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
0,
)
return today
}
getSpecificDateWithTime(date: Date) {
const specificDate = new Date(date)
date = new Date(date)
specificDate.setUTCHours(
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
0,
)
return specificDate
} }
} }

View File

@@ -8,7 +8,7 @@ import {
import { Builder } from '../Graphql/graphql.builder' import { Builder } from '../Graphql/graphql.builder'
import { PrismaService } from '../Prisma/prisma.service' import { PrismaService } from '../Prisma/prisma.service'
import { MinioService } from '../Minio/minio.service' import { MinioService } from '../Minio/minio.service'
import { ServiceStatus } from '@prisma/client' import { Role, ServiceStatus } from '@prisma/client'
import { MailService } from '../Mail/mail.service' import { MailService } from '../Mail/mail.service'
@Injectable() @Injectable()
export class ServiceSchema extends PothosSchema { export class ServiceSchema extends PothosSchema {
@@ -119,7 +119,7 @@ export class ServiceSchema extends PothosSchema {
type: this.service(), type: this.service(),
cursor: 'id', cursor: 'id',
args: this.builder.generator.findManyArgs('Service'), args: this.builder.generator.findManyArgs('Service'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, _args, _ctx, _info) => {
return await this.prisma.service.findMany({ return await this.prisma.service.findMany({
...query, ...query,
}) })
@@ -136,9 +136,57 @@ export class ServiceSchema extends PothosSchema {
description: description:
'Retrieve a list of services with optional filtering, ordering, and pagination.', 'Retrieve a list of services with optional filtering, ordering, and pagination.',
type: [this.service()], type: [this.service()],
args: this.builder.generator.findManyArgs('Service'),
resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.service.findMany({
...query,
where: args.filter ?? undefined,
orderBy: args.orderBy ?? undefined,
skip: args.skip ?? undefined,
take: args.take ?? undefined,
cursor: args.cursor ?? undefined,
})
},
}),
servicesByCenter: t.prismaField({
description:
'Retrieve a list of services with optional filtering, ordering, and pagination.',
type: [this.service()],
args: this.builder.generator.findManyArgs('Service'), args: this.builder.generator.findManyArgs('Service'),
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
// check role if user is mentor or center owner
const role = ctx.http.me.role
if (role !== Role.CENTER_MENTOR && role !== Role.CENTER_OWNER) {
throw new Error('Not allowed')
}
if (role === Role.CENTER_MENTOR) {
// load only service belong to center of current user
const managedServices = await this.prisma.managedService.findMany({
where: { mentorId: ctx.http.me.id },
})
if (!managedServices) {
throw new Error('Managed services not found')
}
// query services that have serviceId in managedServices
args.filter = {
id: { in: managedServices.map((service) => service.serviceId) },
}
}
// if role is center owner, load all services belong to center of current user
if (role === Role.CENTER_OWNER) {
const center = await this.prisma.center.findUnique({
where: { centerOwnerId: ctx.http.me.id },
})
if (!center) {
throw new Error('Center not found')
}
args.filter = { centerId: { in: [center.id] } }
}
return await this.prisma.service.findMany({ return await this.prisma.service.findMany({
...query, ...query,
where: args.filter ?? undefined, where: args.filter ?? undefined,
@@ -158,7 +206,7 @@ export class ServiceSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.service.findUnique({ return await this.prisma.service.findUnique({
...query, ...query,
where: args.input, where: args.input,
@@ -177,11 +225,16 @@ export class ServiceSchema extends PothosSchema {
type: this.service(), type: this.service(),
args: { args: {
input: t.arg({ input: t.arg({
type: this.builder.generator.getCreateInput('Service'), type: this.builder.generator.getCreateInput('Service', ['user']),
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) {
throw new Error('Not allowed')
}
// replace userId with current user id
args.input.user = { connect: { id: ctx.http.me.id } }
return await this.prisma.service.create({ return await this.prisma.service.create({
...query, ...query,
data: args.input, data: args.input,
@@ -201,7 +254,7 @@ export class ServiceSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.service.update({ return await this.prisma.service.update({
...query, ...query,
where: args.where, where: args.where,
@@ -218,7 +271,7 @@ export class ServiceSchema extends PothosSchema {
required: true, required: true,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.service.delete({ return await this.prisma.service.delete({
...query, ...query,
where: args.where, where: args.where,
@@ -242,7 +295,7 @@ export class ServiceSchema extends PothosSchema {
required: false, required: false,
}), }),
}, },
resolve: async (query, root, args, ctx, info) => { resolve: async (query, _root, args, ctx, _info) => {
if (ctx.isSubscription) { if (ctx.isSubscription) {
throw new Error('Not allowed') throw new Error('Not allowed')
} }