Compare commits

...

10 Commits

Author SHA1 Message Date
11221b5702 fix: round revenue calculation in AnalyticSchema
Some checks failed
CI / build (push) Has been cancelled
- Added logic to round the revenue value to the nearest whole number before returning the analytic data.
- This change aims to ensure that revenue figures are presented in a more user-friendly format, enhancing clarity in analytic reports.
2025-01-18 21:26:06 +07:00
0117d81818 refactor: standardize formatting and improve readability in AnalyticSchema
- Reformatted import statements and code structure for consistency and clarity across the AnalyticSchema.
- Enhanced field descriptions to ensure uniformity and better understanding of the analytics data.
- Updated error messages for improved user feedback during analytic data retrieval.
- Improved organization of query fields for better maintainability and readability.

These changes aim to enhance code readability, maintainability, and user experience within the analytic features.
2025-01-18 00:39:26 +07:00
03cf3f48a7 fix: refine schedule overlap validation in ScheduleSchema
- Updated the logic for checking overlapping schedules by querying existing schedule dates for the same user, improving accuracy in overlap detection.
- Simplified the overlap validation process by consolidating checks into a single function, enhancing code clarity and maintainability.
- This change aims to strengthen the integrity of scheduling operations and provide clearer error messages for users regarding scheduling conflicts.
2025-01-17 23:23:10 +07:00
9aec02568d fix: improve schedule overlap validation in ScheduleSchema
- Enhanced the validation logic to check for overlapping schedules by incorporating checks against existing user schedule dates.
- Added comprehensive overlap detection for new schedules, ensuring they do not conflict with any existing schedules for the user.
- This update aims to improve the integrity of scheduling operations and provide clearer error messages for users regarding scheduling conflicts.
2025-01-17 23:08:32 +07:00
bd4eaea379 fix: enhance schedule overlap validation in OrderSchema
- Removed outdated overlap validation logic and replaced it with a more comprehensive check for existing schedule dates associated with the same user.
- Implemented a new validation mechanism to ensure that the new schedule does not overlap with any existing schedule dates, improving the accuracy of scheduling operations.
- This change aims to enhance the integrity of scheduling processes and provide clearer feedback to users regarding scheduling conflicts.
2025-01-17 22:52:24 +07:00
9c152b52a3 feat: add cache invalidation functionality in GraphQL module
- Introduced a new `invalidateCache` method in the GraphQL module to enhance cache management.
- This addition allows for cache invalidation based on the session ID from request headers, improving data consistency and performance during GraphQL operations.
- Aims to provide better control over cached data, ensuring that users receive the most up-to-date information.
2025-01-17 22:05:29 +07:00
5fff0db6d7 feat: add schedule start and end validation in OrderSchema
- Introduced new validation fields for schedule start and end times in the OrderSchema to ensure accurate scheduling.
- This enhancement aims to improve the integrity of scheduling operations by validating the start and end times against the provided schedule data.
2025-01-17 21:34:44 +07:00
d36460fc12 refactor: standardize formatting and improve readability in ScheduleSchema
- Reformatted import statements and code structure for consistency and clarity.
- Enhanced field descriptions across the ScheduleSchema to ensure uniformity and better understanding.
- Updated error messages for improved user feedback during scheduling operations.
- Improved the organization of query and mutation fields for better maintainability.

These changes aim to enhance code readability, maintainability, and user experience within the scheduling features.
2025-01-17 21:14:17 +07:00
b33ad5e809 fix: enhance schedule overlap validation in OrderSchema
- Added logic to retrieve existing schedule dates for a given schedule ID to improve overlap detection.
- Updated the conditions for checking schedule overlaps to include participant IDs, day of the week, and time slots, ensuring more accurate validation.
- This change aims to enhance the integrity of scheduling operations and provide better feedback for users regarding scheduling conflicts.
2025-01-17 20:59:05 +07:00
c2d3ebba09 fix: update serviceAndCategory update query structure
- Modified the update query in ServiceAndCategorySchema to use a composite key for the `where` clause, ensuring that both `serviceId` and `subCategoryId` are correctly utilized for identifying records.
- This change enhances the accuracy of the update operation, improving data integrity and consistency within the service and category management functionality.
2025-01-16 19:38:25 +07:00
5 changed files with 438 additions and 300 deletions

View File

@@ -1,12 +1,17 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from "@nestjs/common";
import { OrderStatus, Prisma, Role, ServiceStatus } from '@prisma/client' import { OrderStatus, Prisma, Role, ServiceStatus } from "@prisma/client";
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos' import {
import { CenterSchema } from 'src/Center/center.schema' Pothos,
import { Builder } from 'src/Graphql/graphql.builder' PothosRef,
import { OrderSchema } from 'src/Order/order.schema' PothosSchema,
import { PrismaService } from 'src/Prisma/prisma.service' SchemaBuilderToken,
import { ServiceSchema } from 'src/Service/service.schema' } from "@smatch-corp/nestjs-pothos";
import { DateTimeUtils } from 'src/common/utils/datetime.utils' import { CenterSchema } from "src/Center/center.schema";
import { Builder } from "src/Graphql/graphql.builder";
import { OrderSchema } from "src/Order/order.schema";
import { PrismaService } from "src/Prisma/prisma.service";
import { ServiceSchema } from "src/Service/service.schema";
import { DateTimeUtils } from "src/common/utils/datetime.utils";
@Injectable() @Injectable()
export class AnalyticSchema extends PothosSchema { export class AnalyticSchema extends PothosSchema {
constructor( constructor(
@@ -14,154 +19,154 @@ export class AnalyticSchema extends PothosSchema {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly serviceSchema: ServiceSchema, private readonly serviceSchema: ServiceSchema,
private readonly centerSchema: CenterSchema, private readonly centerSchema: CenterSchema,
private readonly orderSchema: OrderSchema, private readonly orderSchema: OrderSchema
) { ) {
super() super();
} }
@PothosRef() @PothosRef()
customerAnalytic() { customerAnalytic() {
return this.builder.simpleObject('CustomerAnalytic', { return this.builder.simpleObject("CustomerAnalytic", {
description: 'A customer analytic in the system.', description: "A customer analytic in the system.",
fields: (t) => ({ fields: (t) => ({
userId: t.string({ userId: t.string({
description: 'The ID of the user.', description: "The ID of the user.",
}), }),
activeServiceCount: t.int({ activeServiceCount: t.int({
description: 'The number of active services.', description: "The number of active services.",
}), }),
totalServiceCount: t.int({ totalServiceCount: t.int({
description: 'The total number of services.', description: "The total number of services.",
}), }),
totalSpent: t.float({ totalSpent: t.float({
description: 'The total amount spent.', description: "The total amount spent.",
}), }),
updatedAt: t.field({ updatedAt: t.field({
type: 'DateTime', type: "DateTime",
description: 'The date the analytic was last updated.', description: "The date the analytic was last updated.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
mentorAnalytic() { mentorAnalytic() {
return this.builder.simpleObject('MentorAnalytic', { return this.builder.simpleObject("MentorAnalytic", {
description: 'A mentor analytic in the system.', description: "A mentor analytic in the system.",
fields: (t) => ({ fields: (t) => ({
userId: t.string({ userId: t.string({
description: 'The ID of the mentor.', description: "The ID of the mentor.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
centerAnalytic() { centerAnalytic() {
return this.builder.simpleObject('CenterAnalytic', { return this.builder.simpleObject("CenterAnalytic", {
description: 'A center analytic in the system.', description: "A center analytic in the system.",
fields: (t) => ({ fields: (t) => ({
centerId: t.string({ centerId: t.string({
description: 'The ID of the center.', description: "The ID of the center.",
}), }),
activeMentorCount: t.int({ activeMentorCount: t.int({
description: 'The number of active mentors.', description: "The number of active mentors.",
}), }),
activeServiceCount: t.int({ activeServiceCount: t.int({
description: 'The number of active services.', description: "The number of active services.",
}), }),
totalServiceCount: t.int({ totalServiceCount: t.int({
description: 'The total number of services.', description: "The total number of services.",
}), }),
revenue: t.int({ revenue: t.int({
description: 'The total revenue.', description: "The total revenue.",
}), }),
rating: t.float({ rating: t.float({
description: 'The average rating.', description: "The average rating.",
nullable: true, nullable: true,
}), }),
updatedAt: t.field({ updatedAt: t.field({
type: 'DateTime', type: "DateTime",
description: 'The date the analytic was last updated.', description: "The date the analytic was last updated.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
platformAnalytic() { platformAnalytic() {
return this.builder.simpleObject('PlatformAnalytic', { return this.builder.simpleObject("PlatformAnalytic", {
description: 'A platform analytic in the system.', description: "A platform analytic in the system.",
fields: (t) => ({ fields: (t) => ({
topServices: t.field({ topServices: t.field({
type: [this.serviceSchema.service()], type: [this.serviceSchema.service()],
description: 'The top services by revenue.', description: "The top services by revenue.",
}), }),
topCenters: t.field({ topCenters: t.field({
type: [this.centerSchema.center()], type: [this.centerSchema.center()],
description: 'The top centers by revenue.', description: "The top centers by revenue.",
}), }),
pendingRefunds: t.field({ pendingRefunds: t.field({
type: [this.orderSchema.order()], type: [this.orderSchema.order()],
description: 'The pending refunds.', description: "The pending refunds.",
}), }),
activeCenterCount: t.int({ activeCenterCount: t.int({
description: 'The number of active centers.', description: "The number of active centers.",
}), }),
totalCenterCount: t.int({ totalCenterCount: t.int({
description: 'The total number of centers.', description: "The total number of centers.",
}), }),
totalUserCount: t.int({ totalUserCount: t.int({
description: 'The total number of users.', description: "The total number of users.",
}), }),
activeMentorCount: t.int({ activeMentorCount: t.int({
description: 'The number of active mentors.', description: "The number of active mentors.",
}), }),
totalMentorCount: t.int({ totalMentorCount: t.int({
description: 'The total number of mentors.', description: "The total number of mentors.",
}), }),
totalServiceCount: t.int({ totalServiceCount: t.int({
description: 'The total number of services.', description: "The total number of services.",
}), }),
totalWorkshopCount: t.int({ totalWorkshopCount: t.int({
description: 'The total number of workshops.', description: "The total number of workshops.",
}), }),
revenue: t.int({ revenue: t.int({
description: 'The total revenue.', description: "The total revenue.",
}), }),
approvedServiceCount: t.int({ approvedServiceCount: t.int({
description: 'The number of approved services.', description: "The number of approved services.",
}), }),
rejectedServiceCount: t.int({ rejectedServiceCount: t.int({
description: 'The number of rejected services.', description: "The number of rejected services.",
}), }),
updatedAt: t.field({ updatedAt: t.field({
type: 'DateTime', type: "DateTime",
description: 'The date the analytic was last updated.', description: "The date the analytic was last updated.",
}), }),
}), }),
}) });
} }
@PothosRef() @PothosRef()
timeframes() { timeframes() {
return this.builder.enumType('Timeframe', { return this.builder.enumType("Timeframe", {
values: ['day', 'week', 'month', 'year'], values: ["day", "week", "month", "year"],
}) });
} }
@PothosRef() @PothosRef()
serviceSortBy() { serviceSortBy() {
return this.builder.enumType('ServiceSortBy', { return this.builder.enumType("ServiceSortBy", {
values: ['order', 'rating'], values: ["order", "rating"],
}) });
} }
@PothosRef() @PothosRef()
centerSortBy() { centerSortBy() {
return this.builder.enumType('CenterSortBy', { return this.builder.enumType("CenterSortBy", {
values: ['revenue', 'rating', 'services'], values: ["revenue", "rating", "services"],
}) });
} }
@Pothos() @Pothos()
@@ -169,13 +174,13 @@ export class AnalyticSchema extends PothosSchema {
this.builder.queryFields((t) => ({ this.builder.queryFields((t) => ({
customerAnalytic: t.field({ customerAnalytic: t.field({
type: this.customerAnalytic(), type: this.customerAnalytic(),
description: 'Retrieve a single customer analytic.', description: "Retrieve a single customer analytic.",
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (!ctx.me) { if (!ctx.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
if (ctx.me.role !== Role.CUSTOMER) { if (ctx.me.role !== Role.CUSTOMER) {
throw new Error('Only customers can access this data') throw new Error("Only customers can access this data");
} }
// calculate analytic // calculate analytic
const activeServiceCount = await this.prisma.order.count({ const activeServiceCount = await this.prisma.order.count({
@@ -192,12 +197,12 @@ export class AnalyticSchema extends PothosSchema {
}, },
}, },
}, },
}) });
const totalServiceCount = await this.prisma.order.count({ const totalServiceCount = await this.prisma.order.count({
where: { where: {
userId: ctx.me.id, userId: ctx.me.id,
}, },
}) });
const totalSpent = await this.prisma.order.aggregate({ const totalSpent = await this.prisma.order.aggregate({
where: { where: {
userId: ctx.me.id, userId: ctx.me.id,
@@ -206,50 +211,50 @@ export class AnalyticSchema extends PothosSchema {
_sum: { _sum: {
total: true, total: true,
}, },
}) });
return { return {
userId: ctx.me.id, userId: ctx.me.id,
activeServiceCount: activeServiceCount, activeServiceCount: activeServiceCount,
totalServiceCount: totalServiceCount, totalServiceCount: totalServiceCount,
totalSpent: totalSpent._sum.total, totalSpent: totalSpent._sum.total,
updatedAt: DateTimeUtils.now(), updatedAt: DateTimeUtils.now(),
} };
}, },
}), }),
mentorAnalytic: t.field({ mentorAnalytic: t.field({
type: this.mentorAnalytic(), type: this.mentorAnalytic(),
description: 'Retrieve a single mentor analytic.', description: "Retrieve a single mentor analytic.",
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (!ctx.me) { if (!ctx.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
if (ctx.me.role !== Role.CENTER_MENTOR) { if (ctx.me.role !== Role.CENTER_MENTOR) {
throw new Error('Only center mentors can access this data') throw new Error("Only center mentors can access this data");
} }
// calculate analytic // calculate analytic
return { return {
userId: ctx.me.id, userId: ctx.me.id,
} };
}, },
}), }),
centerAnalytic: t.field({ centerAnalytic: t.field({
type: this.centerAnalytic(), type: this.centerAnalytic(),
description: 'Retrieve a single center analytic.', description: "Retrieve a single center analytic.",
resolve: async (_parent, _args, ctx, _info) => { resolve: async (_parent, _args, ctx, _info) => {
if (!ctx.me) { if (!ctx.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
if (ctx.me.role !== Role.CENTER_OWNER) { if (ctx.me.role !== Role.CENTER_OWNER) {
throw new Error('Only center owners can access this data') throw new Error("Only center owners can access this data");
} }
// get center by owner id // get center by owner id
const center = await this.prisma.center.findUnique({ const center = await this.prisma.center.findUnique({
where: { where: {
centerOwnerId: ctx.me.id, centerOwnerId: ctx.me.id,
}, },
}) });
if (!center) { if (!center) {
throw new Error('Center not found') throw new Error("Center not found");
} }
// calculate analytic // calculate analytic
@@ -261,39 +266,40 @@ export class AnalyticSchema extends PothosSchema {
}, },
banned: false, banned: false,
}, },
}) });
const activeServiceCount = await this.prisma.service.count({ const activeServiceCount = await this.prisma.service.count({
where: { where: {
centerId: center.id, centerId: center.id,
status: ServiceStatus.APPROVED, status: ServiceStatus.APPROVED,
}, },
}) });
const totalServiceCount = await this.prisma.service.count({ const totalServiceCount = await this.prisma.service.count({
where: { where: {
centerId: center.id, centerId: center.id,
}, },
}) });
// calculate revenue from orders of services in the center and factor in commission percentage // calculate revenue from orders of services in the center and factor in commission percentage
// query all orders of services in the center and calculate actual revenue of each order // query all orders of services in the center and calculate actual revenue of each order
// then sum up the revenue // then sum up the revenue
let revenue = 0 let revenue = 0;
const orders = await this.prisma.order.findMany({ const orders = await this.prisma.order.findMany({
where: { where: {
service: { centerId: center.id }, service: { centerId: center.id },
status: OrderStatus.PAID, status: OrderStatus.PAID,
}, },
}) });
for (const order of orders) { for (const order of orders) {
const service = await this.prisma.service.findUnique({ const service = await this.prisma.service.findUnique({
where: { id: order.serviceId }, where: { id: order.serviceId },
}) });
if (!service) { if (!service) {
continue continue;
} }
const commission = service.commission const commission = service.commission;
const actualRevenue = (order.total || 0) - (order.total || 0) * commission const actualRevenue =
revenue += actualRevenue (order.total || 0) - (order.total || 0) * commission;
revenue += actualRevenue;
} }
return { return {
centerId: center.id, centerId: center.id,
@@ -302,40 +308,40 @@ export class AnalyticSchema extends PothosSchema {
totalServiceCount: totalServiceCount, totalServiceCount: totalServiceCount,
revenue: revenue, revenue: revenue,
updatedAt: DateTimeUtils.now(), updatedAt: DateTimeUtils.now(),
} };
}, },
}), }),
platformAnalytic: t.field({ platformAnalytic: t.field({
type: this.platformAnalytic(), type: this.platformAnalytic(),
args: { args: {
take: t.arg({ take: t.arg({
type: 'Int', type: "Int",
description: 'The number of services to take.', description: "The number of services to take.",
required: true, required: true,
}), }),
serviceSortBy: t.arg({ serviceSortBy: t.arg({
type: this.serviceSortBy(), type: this.serviceSortBy(),
description: 'The field to sort by.', description: "The field to sort by.",
required: true, required: true,
}), }),
centerSortBy: t.arg({ centerSortBy: t.arg({
type: this.centerSortBy(), type: this.centerSortBy(),
description: 'The field to sort by.', description: "The field to sort by.",
required: true, required: true,
}), }),
timeframes: t.arg({ timeframes: t.arg({
type: this.timeframes(), type: this.timeframes(),
description: 'The frame of time Eg day, week, month, year.', description: "The frame of time Eg day, week, month, year.",
required: true, required: true,
}), }),
}, },
description: 'Retrieve a single platform analytic.', description: "Retrieve a single platform analytic.",
resolve: async (_parent, args, ctx, _info) => { resolve: async (_parent, args, ctx, _info) => {
if (!ctx.me) { if (!ctx.me) {
throw new Error('Unauthorized') throw new Error("Unauthorized");
} }
if (ctx.me.role !== Role.ADMIN && ctx.me.role !== Role.MODERATOR) { if (ctx.me.role !== Role.ADMIN && ctx.me.role !== Role.MODERATOR) {
throw new Error('Only admins and moderators can access this data') throw new Error("Only admins and moderators can access this data");
} }
// calculate analytic for services sorted by args.serviceSortBy and args.timeframes // calculate analytic for services sorted by args.serviceSortBy and args.timeframes
const topServices = await this.prisma.service.findMany({ const topServices = await this.prisma.service.findMany({
@@ -348,7 +354,7 @@ export class AnalyticSchema extends PothosSchema {
}, },
}, },
take: args.take, take: args.take,
}) });
// get top centers by args.centerSortBy // get top centers by args.centerSortBy
const topCenters = await this.prisma.center.findMany({ const topCenters = await this.prisma.center.findMany({
orderBy: { orderBy: {
@@ -357,13 +363,13 @@ export class AnalyticSchema extends PothosSchema {
}, },
}, },
take: args.take, take: args.take,
}) });
// get pending refunds // get pending refunds
const pendingRefunds = await this.prisma.order.findMany({ const pendingRefunds = await this.prisma.order.findMany({
where: { where: {
status: OrderStatus.PENDING_REFUND, status: OrderStatus.PENDING_REFUND,
}, },
}) });
// get active center count by center owner not banned and have schedule with dates in the future // get active center count by center owner not banned and have schedule with dates in the future
const activeCenterCount = await this.prisma.center.count({ const activeCenterCount = await this.prisma.center.count({
where: { where: {
@@ -390,47 +396,49 @@ export class AnalyticSchema extends PothosSchema {
}, },
}, },
}, },
}) });
// get total center count // get total center count
const totalCenterCount = await this.prisma.center.count() const totalCenterCount = await this.prisma.center.count();
// get total user count // get total user count
const totalUserCount = await this.prisma.user.count() const totalUserCount = await this.prisma.user.count();
// get active mentor count // get active mentor count
const activeMentorCount = await this.prisma.user.count({ const activeMentorCount = await this.prisma.user.count({
where: { where: {
role: Role.CENTER_MENTOR, role: Role.CENTER_MENTOR,
banned: false, banned: false,
}, },
}) });
// get total mentor count // get total mentor count
const totalMentorCount = await this.prisma.user.count({ const totalMentorCount = await this.prisma.user.count({
where: { where: {
role: Role.CENTER_MENTOR, role: Role.CENTER_MENTOR,
}, },
}) });
// get approved service count // get approved service count
const approvedServiceCount = await this.prisma.service.count({ const approvedServiceCount = await this.prisma.service.count({
where: { where: {
status: ServiceStatus.APPROVED, status: ServiceStatus.APPROVED,
}, },
}) });
// get rejected service count // get rejected service count
const rejectedServiceCount = await this.prisma.service.count({ const rejectedServiceCount = await this.prisma.service.count({
where: { where: {
status: ServiceStatus.REJECTED, status: ServiceStatus.REJECTED,
}, },
}) });
// get total workshop count // get total workshop count
const totalWorkshopCount = await this.prisma.workshop.count() const totalWorkshopCount = await this.prisma.workshop.count();
// get total order count // get total order count
const totalOrderCount = await this.prisma.order.count() const totalOrderCount = await this.prisma.order.count();
// get total service count // get total service count
const totalServiceCount = await this.prisma.service.count() const totalServiceCount = await this.prisma.service.count();
// get revenue // get revenue
let revenue = 0 let revenue = 0;
// query all orders of services in all centers in the past args.timeframes and calculate actual revenue of each order by convert commission percentage to float // query all orders of services in all centers in the past args.timeframes and calculate actual revenue of each order by convert commission percentage to float
// convert args.timeframes to number of days // convert args.timeframes to number of days
const timeframes = DateTimeUtils.subtractDaysFromTimeframe(args.timeframes) const timeframes = DateTimeUtils.subtractDaysFromTimeframe(
args.timeframes
);
const orders = await this.prisma.order.findMany({ const orders = await this.prisma.order.findMany({
where: { where: {
status: OrderStatus.PAID, status: OrderStatus.PAID,
@@ -438,18 +446,23 @@ export class AnalyticSchema extends PothosSchema {
gte: timeframes.toJSDate(), gte: timeframes.toJSDate(),
}, },
}, },
}) });
for (const order of orders) { for (const order of orders) {
const service = await this.prisma.service.findUnique({ const service = await this.prisma.service.findUnique({
where: { id: order.serviceId }, where: { id: order.serviceId },
}) });
if (!service) { if (!service) {
continue continue;
} }
const commission = service.commission const orderTotal = Number(order.total ?? 0);
const actualRevenue = (order.total || 0) - (order.total || 0) * commission const commission = Number(service.commission ?? 0);
revenue += actualRevenue const actualRevenue = orderTotal * (1 - commission);
if (!isNaN(actualRevenue)) {
revenue += actualRevenue;
} }
}
// round revenue to number
revenue = Math.round(revenue);
// return analytic // return analytic
return { return {
topServices: topServices, topServices: topServices,
@@ -467,9 +480,9 @@ export class AnalyticSchema extends PothosSchema {
totalOrderCount: totalOrderCount, totalOrderCount: totalOrderCount,
totalServiceCount: totalServiceCount, totalServiceCount: totalServiceCount,
updatedAt: DateTimeUtils.now(), updatedAt: DateTimeUtils.now(),
} };
}, },
}), }),
})) }));
} }
} }

View File

@@ -160,6 +160,11 @@ import { GraphqlService } from "./graphql.service";
// @ts-expect-error: TODO // @ts-expect-error: TODO
extra.request.headers["x-session-id"] extra.request.headers["x-session-id"]
), ),
invalidateCache: () =>
graphqlService.invalidateCache(
// @ts-expect-error: TODO
extra.request.headers["x-session-id"]
),
}; };
} }
return { return {

View File

@@ -400,37 +400,6 @@ export class OrderSchema extends PothosSchema {
if (userService) { if (userService) {
throw new Error("User has already registered for this service"); throw new Error("User has already registered for this service");
} }
// check if user have any scheduledate overlap time with input schedule
const overlapSchedule = await this.prisma.scheduleDate.findFirst({
where: {
AND: [
{
start: {
equals: args.data.schedule.connect?.scheduleStart as Date,
},
end: {
equals: args.data.schedule.connect?.scheduleEnd as Date,
},
},
{
participantIds: {
has: ctx.me?.id ?? "",
},
},
{
status: {
notIn: [
ScheduleDateStatus.NOT_STARTED,
ScheduleDateStatus.IN_PROGRESS,
],
},
},
],
},
});
if (overlapSchedule) {
throw new Error("User have overlap schedule");
}
// check if input schedule has order id then throw error // check if input schedule has order id then throw error
const schedule = await this.prisma.schedule.findUnique({ const schedule = await this.prisma.schedule.findUnique({
where: { id: args.data.schedule.connect?.id ?? "" }, where: { id: args.data.schedule.connect?.id ?? "" },
@@ -447,6 +416,71 @@ export class OrderSchema extends PothosSchema {
throw new Error("Schedule already has an order"); throw new Error("Schedule already has an order");
} }
} }
// check if scheduleDate have overlap with other scheduleDate of same user
const existingScheduleDates = await this.prisma.scheduleDate.findMany(
{
where: {
AND: [
{
scheduleId: {
not: args.data.schedule.connect?.id ?? "",
},
},
{
participantIds: {
has: ctx.me?.id ?? "",
},
},
{
status: {
notIn: [
ScheduleDateStatus.COMPLETED,
ScheduleDateStatus.EXPIRED,
],
},
},
{
end: {
gte: DateTimeUtils.now().toJSDate(),
},
},
],
},
}
);
// First, fetch the full schedule details to get accurate start and end times
const newSchedule = await this.prisma.schedule.findUnique({
where: { id: args.data.schedule.connect?.id },
include: {
dates: true // Include schedule dates to get accurate timing
}
});
if (!newSchedule) {
throw new Error("Schedule not found");
}
// Check if new schedule overlaps with any existing schedule dates
const hasOverlap = existingScheduleDates.some((existingDate) => {
// Use the actual schedule dates instead of potentially undefined properties
const newScheduleDates = newSchedule.dates;
return newScheduleDates.some(newScheduleDate =>
DateTimeUtils.isOverlap(
DateTimeUtils.fromDate(existingDate.start),
DateTimeUtils.fromDate(existingDate.end),
DateTimeUtils.fromDate(newScheduleDate.start),
DateTimeUtils.fromDate(newScheduleDate.end)
)
);
});
if (hasOverlap) {
throw new Error(
"Schedule date has overlap with existing schedule dates"
);
}
const order = await this.prisma.order.create({ const order = await this.prisma.order.create({
...query, ...query,
data: { data: {

View File

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

View File

@@ -94,7 +94,12 @@ export class ServiceAndCategorySchema extends PothosSchema {
resolve: async (query, _root, args, _ctx, _info) => { resolve: async (query, _root, args, _ctx, _info) => {
return await this.prisma.serviceAndCategory.update({ return await this.prisma.serviceAndCategory.update({
...query, ...query,
where: args.where, where: {
serviceId_subCategoryId: {
serviceId: args.where.serviceId as string,
subCategoryId: args.where.subCategoryId as string,
},
},
data: { data: {
isDeleted: true, isDeleted: true,
}, },