Compare commits
10 Commits
a90811af86
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 11221b5702 | |||
| 0117d81818 | |||
| 03cf3f48a7 | |||
| 9aec02568d | |||
| bd4eaea379 | |||
| 9c152b52a3 | |||
| 5fff0db6d7 | |||
| d36460fc12 | |||
| b33ad5e809 | |||
| c2d3ebba09 |
@@ -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 orderTotal = Number(order.total ?? 0);
|
||||||
|
const commission = Number(service.commission ?? 0);
|
||||||
|
const actualRevenue = orderTotal * (1 - commission);
|
||||||
|
if (!isNaN(actualRevenue)) {
|
||||||
|
revenue += actualRevenue;
|
||||||
}
|
}
|
||||||
const commission = service.commission
|
|
||||||
const actualRevenue = (order.total || 0) - (order.total || 0) * commission
|
|
||||||
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(),
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,46 +419,62 @@ 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 =
|
||||||
startDate: args.schedule.scheduleStart as string,
|
await this.scheduleService.createSchedulePreviewForCenter({
|
||||||
endDate: args.schedule.scheduleEnd as string,
|
startDate: args.schedule.scheduleStart as string,
|
||||||
slots: args.schedule.slots as number[],
|
endDate: args.schedule.scheduleEnd as string,
|
||||||
days: args.schedule.daysOfWeek as number[],
|
slots: args.schedule.slots as number[],
|
||||||
})
|
days: args.schedule.daysOfWeek as number[],
|
||||||
const existingScheduleDates = await this.prisma.scheduleDate.findMany({
|
});
|
||||||
where: {
|
|
||||||
AND: [
|
const existingScheduleDates = await this.prisma.scheduleDate.findMany(
|
||||||
{ serviceId: args.schedule.managedService.connect?.id },
|
{
|
||||||
{
|
where: {
|
||||||
status: {
|
AND: [
|
||||||
notIn: [
|
{ serviceId: args.schedule.managedService.connect?.id },
|
||||||
ScheduleDateStatus.COMPLETED,
|
{
|
||||||
ScheduleDateStatus.MISSING_MENTOR,
|
status: {
|
||||||
ScheduleDateStatus.MISSING_CUSTOMER,
|
notIn: [
|
||||||
ScheduleDateStatus.EXPIRED,
|
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
|
// 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 },
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user